diff --git a/.github/stale.yml b/.github/stale.yml index 1246f73b343..c59ef23f554 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,8 +1,10 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 7 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale +pulls: + daysUntilStale: 7 + daysUntilClose: 30 +issues: + daysUntilStale: 30 + daysUntilClose: 60 +# Items with these labels will never be considered stale exemptLabels: - pinned - security diff --git a/.gitignore b/.gitignore index c2cbc1e97d5..60c24e79c0d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,10 @@ debug pbs.* inventory_url.yaml +# generated log files during tests +analytics/config/testFiles/ +analytics/filesystem/testFiles/ + # autogenerated version file # static/version.txt diff --git a/.travis.yml b/.travis.yml index b46dd356e73..60ee49faf68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - '1.12' - '1.13' + - '1.14.2' go_import_path: github.com/PubMatic-OpenWrap/prebid-server diff --git a/Dockerfile b/Dockerfile index a8fea9c33f6..2c60b9e39b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ RUN apt-get update && \ apt-get -y upgrade && \ apt-get install -y wget RUN cd /tmp && \ - wget https://dl.google.com/go/go1.12.7.linux-amd64.tar.gz && \ - tar -xf go1.12.7.linux-amd64.tar.gz && \ + wget https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz && \ + tar -xf go1.14.2.linux-amd64.tar.gz && \ mv go /usr/local RUN mkdir -p /app/prebid-server/ WORKDIR /app/prebid-server/ diff --git a/README.md b/README.md index f0e0b47572e..b3c795bf803 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/PubMatic-OpenWrap/prebid-server.svg?branch=master)](https://travis-ci.org/PubMatic-OpenWrap/prebid-server) +[![Build Status](https://travis-ci.org/prebid/prebid-server.svg?branch=master)](https://travis-ci.org/prebid/prebid-server) [![Go Report Card](https://goreportcard.com/badge/github.com/PubMatic-OpenWrap/prebid-server?style=flat-square)](https://goreportcard.com/report/github.com/PubMatic-OpenWrap/prebid-server) # Prebid Server @@ -18,9 +18,10 @@ For more information, see: ## Installation -First install [Go 1.12](https://golang.org/doc/install) latest version. +First install [Go](https://golang.org/doc/install) version 1.13 or newer. + Note that prebid-server is using [Go modules](https://blog.golang.org/using-go-modules). -If using Go version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`. +We officially support the most recent two major versions of the Go runtime. However, if you'd like to use a version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`. Download and prepare Prebid Server: diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go index 30d2f59be94..7bb06d0716f 100644 --- a/adapters/adapterstest/test_json.go +++ b/adapters/adapterstest/test_json.go @@ -208,7 +208,7 @@ func diffErrorLists(t *testing.T, description string, actual []error, expected [ t.Helper() if len(expected) != len(actual) { - t.Fatalf("%s had wrong error count. Expected %d, got %d", description, len(expected), len(actual)) + t.Fatalf("%s had wrong error count. Expected %d, got %d (%v)", description, len(expected), len(actual), actual) } for i := 0; i < len(actual); i++ { if expected[i].Comparison == "literal" { @@ -301,7 +301,7 @@ func diffJson(t *testing.T, description string, actual []byte, expected []byte) if diff.Modified() { var left interface{} if err := json.Unmarshal(actual, &left); err != nil { - t.Fatalf("%s json did not match, but unmarhsalling failed. %v", description, err) + t.Fatalf("%s json did not match, but unmarshalling failed. %v", description, err) } printer := formatter.NewAsciiFormatter(left, formatter.AsciiFormatterConfig{ ShowArrayIndex: true, diff --git a/adapters/adgeneration/adgeneration.go b/adapters/adgeneration/adgeneration.go new file mode 100644 index 00000000000..069609f4262 --- /dev/null +++ b/adapters/adgeneration/adgeneration.go @@ -0,0 +1,260 @@ +package adgeneration + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +type AdgenerationAdapter struct { + endpoint string + version string + defaultCurrency string +} + +// Server Responses +type adgServerResponse struct { + Locationid string `json:"locationid"` + Dealid string `json:"dealid"` + Ad string `json:"ad"` + Beacon string `json:"beacon"` + Beaconurl string `json:"beaconurl"` + Cpm float64 `jsons:"cpm"` + Creativeid string `json:"creativeid"` + H uint64 `json:"h"` + W uint64 `json:"w"` + Ttl uint64 `json:"ttl"` + Vastxml string `json:"vastxml,omitempty"` + LandingUrl string `json:"landing_url"` + Scheduleid string `json:"scheduleid"` + Results []interface{} `json:"results"` +} + +func (adg *AdgenerationAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + numRequests := len(request.Imp) + var errs []error + + if numRequests == 0 { + errs = append(errs, &errortypes.BadInput{ + Message: "No impression in the bid request", + }) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + bidRequestArray := make([]*adapters.RequestData, 0, numRequests) + + for index := 0; index < numRequests; index++ { + bidRequestUri, err := adg.getRequestUri(request, index) + if err != nil { + errs = append(errs, err) + return nil, errs + } + bidRequest := &adapters.RequestData{ + Method: "GET", + Uri: bidRequestUri, + Body: nil, + Headers: headers, + } + bidRequestArray = append(bidRequestArray, bidRequest) + } + + return bidRequestArray, errs +} + +func (adg *AdgenerationAdapter) getRequestUri(request *openrtb.BidRequest, index int) (string, error) { + imp := request.Imp[index] + adgExt, err := unmarshalExtImpAdgeneration(&imp) + if err != nil { + return "", &errortypes.BadInput{ + Message: err.Error(), + } + } + uriObj, err := url.Parse(adg.endpoint) + if err != nil { + return "", &errortypes.BadInput{ + Message: err.Error(), + } + } + v := adg.getRawQuery(adgExt.Id, request, &imp) + uriObj.RawQuery = v.Encode() + return uriObj.String(), err +} + +func (adg *AdgenerationAdapter) getRawQuery(id string, request *openrtb.BidRequest, imp *openrtb.Imp) *url.Values { + v := url.Values{} + v.Set("posall", "SSPLOC") + v.Set("id", id) + v.Set("sdktype", "0") + v.Set("hb", "true") + v.Set("t", "json3") + v.Set("currency", adg.getCurrency(request)) + v.Set("sdkname", "prebidserver") + v.Set("adapterver", adg.version) + adSize := getSizes(imp) + if adSize != "" { + v.Set("size", adSize) + } + if request.Site != nil && request.Site.Page != "" { + v.Set("tp", request.Site.Page) + } + return &v +} + +func unmarshalExtImpAdgeneration(imp *openrtb.Imp) (*openrtb_ext.ExtImpAdgeneration, error) { + var bidderExt adapters.ExtImpBidder + var adgExt openrtb_ext.ExtImpAdgeneration + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, err + } + if err := json.Unmarshal(bidderExt.Bidder, &adgExt); err != nil { + return nil, err + } + if adgExt.Id == "" { + return nil, errors.New("No Location ID in ExtImpAdgeneration.") + } + return &adgExt, nil +} + +func getSizes(imp *openrtb.Imp) string { + if imp.Banner == nil || len(imp.Banner.Format) == 0 { + return "" + } + var sizeStr string + for _, v := range imp.Banner.Format { + sizeStr += strconv.FormatUint(v.W, 10) + "×" + strconv.FormatUint(v.H, 10) + "," + } + if len(sizeStr) > 0 && strings.LastIndex(sizeStr, ",") == len(sizeStr)-1 { + sizeStr = sizeStr[:len(sizeStr)-1] + } + return sizeStr +} + +func (adg *AdgenerationAdapter) getCurrency(request *openrtb.BidRequest) string { + if len(request.Cur) <= 0 { + return adg.defaultCurrency + } else { + for _, c := range request.Cur { + if adg.defaultCurrency == c { + return c + } + } + return request.Cur[0] + } +} + +func (adg *AdgenerationAdapter) 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 bidResp adgServerResponse + err := json.Unmarshal(response.Body, &bidResp) + if err != nil { + return nil, []error{err} + } + if len(bidResp.Results) <= 0 { + return nil, nil + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + var impId string + var bitType openrtb_ext.BidType + var adm string + for _, v := range internalRequest.Imp { + adgExt, err := unmarshalExtImpAdgeneration(&v) + if err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: err.Error(), + }, + } + } + if adgExt.Id == bidResp.Locationid { + impId = v.ID + bitType = openrtb_ext.BidTypeBanner + adm = createAd(&bidResp, impId) + bid := openrtb.Bid{ + ID: bidResp.Locationid, + ImpID: impId, + AdM: adm, + Price: bidResp.Cpm, + W: bidResp.W, + H: bidResp.H, + CrID: bidResp.Creativeid, + DealID: bidResp.Dealid, + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bitType, + }) + return bidResponse, nil + } + } + return nil, nil +} + +func createAd(body *adgServerResponse, impId string) string { + ad := body.Ad + if body.Vastxml != "" { + ad = "
" + insertVASTMethod(impId, body.Vastxml) + "" + } + ad = appendChildToBody(ad, body.Beacon) + unwrappedAd := removeWrapper(ad) + if unwrappedAd != "" { + return unwrappedAd + } + return ad +} + +func insertVASTMethod(bidId string, vastxml string) string { + rep := regexp.MustCompile(`/\r?\n/g`) + var replacedVastxml = rep.ReplaceAllString(vastxml, "") + return "" +} + +func appendChildToBody(ad string, data string) string { + rep := regexp.MustCompile(`<\/\s?body>`) + return rep.ReplaceAllString(ad, data+"") +} + +func removeWrapper(ad string) string { + bodyIndex := strings.Index(ad, "") + lastBodyIndex := strings.LastIndex(ad, "") + if bodyIndex == -1 || lastBodyIndex == -1 { + return "" + } + + str := strings.TrimSpace(strings.Replace(strings.Replace(ad[bodyIndex:lastBodyIndex], "", "", 1), "", "", 1)) + return str +} + +func NewAdgenerationAdapter(endpoint string) *AdgenerationAdapter { + return &AdgenerationAdapter{ + endpoint, + "1.0.0", + "JPY", + } +} diff --git a/adapters/adgeneration/adgeneration_test.go b/adapters/adgeneration/adgeneration_test.go new file mode 100644 index 00000000000..2c679e10471 --- /dev/null +++ b/adapters/adgeneration/adgeneration_test.go @@ -0,0 +1,176 @@ +package adgeneration + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "adgenerationtest", NewAdgenerationAdapter("https://d.socdm.com/adsv/v1")) +} + +func TestgetRequestUri(t *testing.T) { + bidder := NewAdgenerationAdapter("https://d.socdm.com/adsv/v1") + // Test items + failedRequest := &openrtb.BidRequest{ + ID: "test-failed-bid-request", + Imp: []openrtb.Imp{ + {ID: "extImpBidder-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{{ "id": "58278" }}`)}, + {ID: "extImpBidder-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"_bidder": { "id": "58278" }}`)}, + {ID: "extImpAdgeneration-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "_id": "58278" }}`)}, + }, + Device: &openrtb.Device{UA: "testUA", IP: "testIP"}, + Site: &openrtb.Site{Page: "https://supership.com"}, + User: &openrtb.User{BuyerUID: "buyerID"}, + } + successRequest := &openrtb.BidRequest{ + ID: "test-success-bid-request", + Imp: []openrtb.Imp{ + {ID: "bidRequest-success-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "id": "58278" }}`)}, + }, + Device: &openrtb.Device{UA: "testUA", IP: "testIP"}, + Site: &openrtb.Site{Page: "https://supership.com"}, + User: &openrtb.User{BuyerUID: "buyerID"}, + } + + numRequests := len(failedRequest.Imp) + for index := 0; index < numRequests; index++ { + httpRequests, err := bidder.getRequestUri(failedRequest, index) + if err == nil { + t.Errorf("getRequestUri: %v did not throw an error", failedRequest.Imp[index]) + } + if httpRequests != "" { + t.Errorf("getRequestUri: %v did return Request: %s", failedRequest.Imp[index], httpRequests) + } + } + numRequests = len(successRequest.Imp) + for index := 0; index < numRequests; index++ { + // RequestUri Test. + httpRequests, err := bidder.getRequestUri(successRequest, index) + if err != nil { + t.Errorf("getRequestUri: %v did throw an error: %v", successRequest.Imp[index], err) + } + if httpRequests == "adapterver="+bidder.version+"¤cy=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html" { + t.Errorf("getRequestUri: %v did return Request: %s", successRequest.Imp[index], httpRequests) + } + // getRawQuery Test. + adgExt, err := unmarshalExtImpAdgeneration(&successRequest.Imp[index]) + if err != nil { + t.Errorf("unmarshalExtImpAdgeneration: %v did throw an error: %v", successRequest.Imp[index], err) + } + rawQuery := bidder.getRawQuery(adgExt.Id, successRequest, &successRequest.Imp[index]) + expectQueries := map[string]string{ + "posall": "SSPLOC", + "id": adgExt.Id, + "sdktype": "0", + "hb": "true", + "currency": bidder.getCurrency(successRequest), + "sdkname": "prebidserver", + "adapterver": bidder.version, + "size": getSizes(&successRequest.Imp[index]), + "tp": successRequest.Site.Name, + } + for key, expectedValue := range expectQueries { + actualValue := rawQuery.Get(key) + if actualValue == "" { + if !(key == "size" || key == "tp") { + t.Errorf("getRawQuery: key %s is required value.", key) + } + } + if actualValue != expectedValue { + t.Errorf("getRawQuery: %s value does not match expected %s, actual %s", key, expectedValue, actualValue) + } + } + } +} + +func TestGetSizes(t *testing.T) { + // Test items + var request *openrtb.Imp + var size string + multiFormatBanner := &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}, {W: 320, H: 50}}} + noFormatBanner := &openrtb.Banner{Format: []openrtb.Format{}} + nativeFormat := &openrtb.Native{} + + request = &openrtb.Imp{Banner: multiFormatBanner} + size = getSizes(request) + if size != "300×250,320×50" { + t.Errorf("%v does not match size.", multiFormatBanner) + } + request = &openrtb.Imp{Banner: noFormatBanner} + size = getSizes(request) + if size != "" { + t.Errorf("%v does not match size.", noFormatBanner) + } + request = &openrtb.Imp{Native: nativeFormat} + size = getSizes(request) + if size != "" { + t.Errorf("%v does not match size.", nativeFormat) + } +} + +func TestGetCurrency(t *testing.T) { + bidder := NewAdgenerationAdapter("https://d.socdm.com/adsv/v1") + // Test items + var request *openrtb.BidRequest + var currency string + innerDefaultCur := []string{"USD", "JPY"} + usdCur := []string{"USD", "EUR"} + + request = &openrtb.BidRequest{Cur: innerDefaultCur} + currency = bidder.getCurrency(request) + if currency != "JPY" { + t.Errorf("%v does not match currency.", innerDefaultCur) + } + request = &openrtb.BidRequest{Cur: usdCur} + currency = bidder.getCurrency(request) + if currency != "USD" { + t.Errorf("%v does not match currency.", usdCur) + } +} + +func TestCreateAd(t *testing.T) { + // Test items + adgBannerImpId := "test-banner-imp" + adgBannerResponse := adgServerResponse{ + Ad: "\n\n\n\n\n
\n\n
\n\n", + Beacon: "", + Beaconurl: "https://dummy-beacon.com", + Cpm: 50, + Creativeid: "DummyDsp_SdkTeam_supership.jp", + H: 300, + W: 250, + Ttl: 10, + LandingUrl: "", + Scheduleid: "111111", + } + matchBannerTag := "
\n\n
\n" + + adgVastImpId := "test-vast-imp" + adgVastResponse := adgServerResponse{ + Ad: "\n\n\n\n\n
\n\n
\n\n", + Beacon: "", + Beaconurl: "https://dummy-beacon.com", + Cpm: 50, + Creativeid: "DummyDsp_SdkTeam_supership.jp", + H: 300, + W: 250, + Ttl: 10, + LandingUrl: "", + Vastxml: "", + Scheduleid: "111111", + } + matchVastTag := "
" + + bannerAd := createAd(&adgBannerResponse, adgBannerImpId) + if bannerAd != matchBannerTag { + t.Errorf("%v does not match createAd.", adgBannerResponse) + } + vastAd := createAd(&adgVastResponse, adgVastImpId) + if vastAd != matchVastTag { + t.Errorf("%v does not match createAd.", adgVastResponse) + } +} diff --git a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json new file mode 100644 index 00000000000..d23a510bee5 --- /dev/null +++ b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json @@ -0,0 +1,151 @@ +{ + "mockBidRequest":{ + "id": "some-request-id", + "site": { + "page": "http://example.com/test.html" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "id": "58278" + } + } + } + ], + "tmax": 500 + }, + "httpCalls": [ + { + "internalRequest": { + "id": "some-request-id", + "site": { + "page": "http://example.com/test.html" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "id": "58278" + } + } + } + ], + "tmax": 500 + }, + "expectedRequest":{ + "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.0¤cy=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + } + }, + "mockResponse":{ + "status": 200, + "body": { + "ad": "\n \n \n +` + +type ResponseAdUnit struct { + ID string `json:"id"` + CrID string `json:"crid"` + Currency string `json:"currency"` + Price string `json:"price"` + Width string `json:"width"` + Height string `json:"height"` + Code string `json:"code"` + WinURL string `json:"winUrl"` + StatsURL string `json:"statsUrl"` + Error string `json:"error"` +} + +type requestData struct { + Url *url.URL + Headers *http.Header + SlaveSizes map[string]string +} + +func NewAdOceanBidder(client *http.Client, endpointTemplateString string) *AdOceanAdapter { + a := &adapters.HTTPAdapter{Client: client} + endpointTemplate, err := template.New("endpointTemplate").Parse(endpointTemplateString) + if err != nil { + glog.Fatal("Unable to parse endpoint template") + return nil + } + + whiteSpace := regexp.MustCompile(`\s+`) + + return &AdOceanAdapter{ + http: a, + endpointTemplate: *endpointTemplate, + measurementCode: whiteSpace.ReplaceAllString(measurementCode, " "), + } +} + +type AdOceanAdapter struct { + http *adapters.HTTPAdapter + endpointTemplate template.Template + measurementCode string +} + +func (a *AdOceanAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: "No impression in the bid request", + }} + } + + consentString := "" + if request.User != nil { + var extUser openrtb_ext.ExtUser + if err := json.Unmarshal(request.User.Ext, &extUser); err == nil { + consentString = extUser.Consent + } + } + + var errors []error + var err error + requestsData := make([]*requestData, 0, len(request.Imp)) + for _, auction := range request.Imp { + requestsData, err = a.addNewBid(requestsData, &auction, request, consentString) + if err != nil { + errors = append(errors, err) + } + } + + httpRequests := make([]*adapters.RequestData, 0, len(requestsData)) + for _, requestData := range requestsData { + httpRequests = append(httpRequests, &adapters.RequestData{ + Method: "GET", + Uri: requestData.Url.String(), + Headers: *requestData.Headers, + }) + } + + return httpRequests, errors +} + +func (a *AdOceanAdapter) addNewBid( + requestsData []*requestData, + imp *openrtb.Imp, + request *openrtb.BidRequest, + consentString string, +) ([]*requestData, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return requestsData, &errortypes.BadInput{ + Message: "Error parsing bidderExt object", + } + } + + var adOceanExt openrtb_ext.ExtImpAdOcean + if err := json.Unmarshal(bidderExt.Bidder, &adOceanExt); err != nil { + return requestsData, &errortypes.BadInput{ + Message: "Error parsing adOceanExt parameters", + } + } + + addedToExistingRequest := addToExistingRequest(requestsData, &adOceanExt, imp, (request.Test == 1)) + if addedToExistingRequest { + return requestsData, nil + } + + slaveSizes := map[string]string{} + slaveSizes[adOceanExt.SlaveID] = getImpSizes(imp) + + url, err := a.makeURL(&adOceanExt, imp, request, slaveSizes, consentString) + if err != nil { + return requestsData, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + if request.Device != nil { + headers.Add("User-Agent", request.Device.UA) + + if request.Device.IP != "" { + headers.Add("X-Forwarded-For", request.Device.IP) + } else if request.Device.IPv6 != "" { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + } + + if request.Site != nil { + headers.Add("Referer", request.Site.Page) + } + + requestsData = append(requestsData, &requestData{ + Url: url, + Headers: &headers, + SlaveSizes: slaveSizes, + }) + + return requestsData, nil +} + +func addToExistingRequest(requestsData []*requestData, newParams *openrtb_ext.ExtImpAdOcean, imp *openrtb.Imp, testImp bool) bool { + auctionID := imp.ID + + for _, requestData := range requestsData { + queryParams := requestData.Url.Query() + masterID := queryParams["id"][0] + + if masterID == newParams.MasterID { + if _, has := requestData.SlaveSizes[newParams.SlaveID]; has { + continue + } + + queryParams.Add("aid", newParams.SlaveID+":"+auctionID) + requestData.SlaveSizes[newParams.SlaveID] = getImpSizes(imp) + setSlaveSizesParam(&queryParams, requestData.SlaveSizes, testImp) + + newUrl := *(requestData.Url) + newUrl.RawQuery = queryParams.Encode() + if len(newUrl.String()) < maxUriLength { + requestData.Url = &newUrl + return true + } + + delete(requestData.SlaveSizes, newParams.SlaveID) + } + } + + return false +} + +func (a *AdOceanAdapter) makeURL( + params *openrtb_ext.ExtImpAdOcean, + imp *openrtb.Imp, + request *openrtb.BidRequest, + slaveSizes map[string]string, + consentString string, +) (*url.URL, error) { + endpointParams := macros.EndpointTemplateParams{Host: params.EmitterDomain} + host, err := macros.ResolveMacros(a.endpointTemplate, endpointParams) + if err != nil { + return nil, &errortypes.BadInput{ + Message: "Unable to parse endpoint url template: " + err.Error(), + } + } + + endpointURL, err := url.Parse(host) + if err != nil { + return nil, &errortypes.BadInput{ + Message: "Malformed URL: " + err.Error(), + } + } + + randomizedPart := 10000000 + rand.Intn(99999999-10000000) + if request.Test == 1 { + randomizedPart = 10000000 + } + endpointURL.Path = "/_" + strconv.Itoa(randomizedPart) + "/ad.json" + + auctionID := imp.ID + queryParams := url.Values{} + queryParams.Add("pbsrv_v", adapterVersion) + queryParams.Add("id", params.MasterID) + queryParams.Add("nc", "1") + queryParams.Add("nosecure", "1") + queryParams.Add("aid", params.SlaveID+":"+auctionID) + if consentString != "" { + queryParams.Add("gdpr_consent", consentString) + queryParams.Add("gdpr", "1") + } + if request.User != nil && request.User.BuyerUID != "" { + queryParams.Add("hcuserid", request.User.BuyerUID) + } + + setSlaveSizesParam(&queryParams, slaveSizes, (request.Test == 1)) + endpointURL.RawQuery = queryParams.Encode() + + return endpointURL, nil +} + +func getImpSizes(imp *openrtb.Imp) string { + if imp.Banner == nil { + return "" + } + + if len(imp.Banner.Format) > 0 { + sizes := make([]string, len(imp.Banner.Format)) + for i, format := range imp.Banner.Format { + sizes[i] = strconv.FormatUint(format.W, 10) + "x" + strconv.FormatUint(format.H, 10) + } + + return strings.Join(sizes, "_") + } + + if imp.Banner.W != nil && imp.Banner.H != nil { + return strconv.FormatUint(*imp.Banner.W, 10) + "x" + strconv.FormatUint(*imp.Banner.H, 10) + } + + return "" +} + +func setSlaveSizesParam(queryParams *url.Values, slaveSizes map[string]string, orderByKey bool) { + sizeValues := make([]string, 0, len(slaveSizes)) + slaveIDs := make([]string, 0, len(slaveSizes)) + for k := range slaveSizes { + slaveIDs = append(slaveIDs, k) + } + + if orderByKey { + sort.Strings(slaveIDs) + } + + for _, slaveID := range slaveIDs { + sizes := slaveSizes[slaveID] + if sizes == "" { + continue + } + + rawSlaveID := strings.Replace(slaveID, "adocean", "", 1) + sizeValues = append(sizeValues, rawSlaveID+"~"+sizes) + } + + if len(sizeValues) > 0 { + queryParams.Set("aosspsizes", strings.Join(sizeValues, "-")) + } +} + +func (a *AdOceanAdapter) MakeBids( + internalRequest *openrtb.BidRequest, + externalRequest *adapters.RequestData, + response *adapters.ResponseData, +) (*adapters.BidderResponse, []error) { + if response.StatusCode != http.StatusOK { + return nil, []error{fmt.Errorf("Unexpected status code: %d. Network error?", response.StatusCode)} + } + + requestURL, _ := url.Parse(externalRequest.Uri) + queryParams := requestURL.Query() + auctionIDs := queryParams["aid"] + + bidResponses := make([]ResponseAdUnit, 0) + if err := json.Unmarshal(response.Body, &bidResponses); err != nil { + return nil, []error{err} + } + + var parsedResponses = adapters.NewBidderResponseWithBidsCapacity(len(auctionIDs)) + var errors []error + var slaveToAuctionIDMap = make(map[string]string, len(auctionIDs)) + + for _, auctionFullID := range auctionIDs { + auctionIDsSlice := strings.SplitN(auctionFullID, ":", 2) + slaveToAuctionIDMap[auctionIDsSlice[0]] = auctionIDsSlice[1] + } + + for _, bid := range bidResponses { + if auctionID, found := slaveToAuctionIDMap[bid.ID]; found { + if bid.Error == "true" { + continue + } + + price, _ := strconv.ParseFloat(bid.Price, 64) + width, _ := strconv.ParseUint(bid.Width, 10, 64) + height, _ := strconv.ParseUint(bid.Height, 10, 64) + adCode, err := a.prepareAdCodeForBid(bid) + if err != nil { + errors = append(errors, err) + continue + } + + parsedResponses.Bids = append(parsedResponses.Bids, &adapters.TypedBid{ + Bid: &openrtb.Bid{ + ID: bid.ID, + ImpID: auctionID, + Price: price, + AdM: adCode, + CrID: bid.CrID, + W: width, + H: height, + }, + BidType: openrtb_ext.BidTypeBanner, + }) + parsedResponses.Currency = bid.Currency + } + } + + return parsedResponses, errors +} + +func (a *AdOceanAdapter) prepareAdCodeForBid(bid ResponseAdUnit) (string, error) { + sspCode, err := url.QueryUnescape(bid.Code) + if err != nil { + return "", err + } + + adCode := fmt.Sprintf(a.measurementCode, bid.WinURL, bid.StatsURL) + sspCode + + return adCode, nil +} diff --git a/adapters/adocean/adocean_test.go b/adapters/adocean/adocean_test.go new file mode 100644 index 00000000000..1088fedd30e --- /dev/null +++ b/adapters/adocean/adocean_test.go @@ -0,0 +1,12 @@ +package adocean + +import ( + "net/http" + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "adoceantest", NewAdOceanBidder(new(http.Client), "https://{{.Host}}")) +} diff --git a/adapters/adocean/adoceantest/exemplary/multi-banner-impression.json b/adapters/adocean/adoceantest/exemplary/multi-banner-impression.json new file mode 100644 index 00000000000..9e4ce06e83a --- /dev/null +++ b/adapters/adocean/adoceantest/exemplary/multi-banner-impression.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b", + "source": { + "tid": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b" + }, + "tmax": 1000, + "imp": [{ + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }, { + "w": 320, + "h": 600 + }] + } + }, { + "id": "secod-twelve", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://192.168.100.203/testing/prebid_server/test.html" + }, + "device": { + "w": 418, + "h": 961 + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aid=adoceanmyaowafpdwlrks%3Asecod-twelve&aosspsizes=myaowafpdwlrks~300x250-myaozpniqismex~300x250_320x600&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [{ + "id": "adoceanmyaozpniqismex", + "price": "1", + "winurl": "https://win-url.com", + "statsUrl": "https://stats-url.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + }, + { + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "https://win-url2.com", + "statsUrl": "https://stats-url2.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + } + ] + } + }], + "expectedBidResponses": [{ + "currency": "EUR", + "bids": [{ + "bid": { + "id": "adoceanmyaozpniqismex", + "impid": "ao-test", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + },{ + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "secod-twelve", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/adocean/adoceantest/exemplary/single-banner-impression.json b/adapters/adocean/adoceantest/exemplary/single-banner-impression.json new file mode 100644 index 00000000000..e6d70f840aa --- /dev/null +++ b/adapters/adocean/adoceantest/exemplary/single-banner-impression.json @@ -0,0 +1,116 @@ +{ + "mockBidRequest": { + "id": "9ed903f4-383d-406b-8011-4f06526cb02c", + "source": { + "tid": "9ed903f4-383d-406b-8011-4f06526cb02c" + }, + "tmax": 1000, + "imp": [ + { + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://example.com/test.html" + }, + "device": { + "w": 1280, + "h": 720, + "ip": "192.168.1.1" + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aosspsizes=myaozpniqismex~300x250&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaozpniqismex", + "price": "1", + "winurl": "https://win-url.com", + "statsUrl": "https://stats-url.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + }, + { + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "", + "statsUrl": "", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "adoceanmyaozpniqismex", + "impid": "ao-test", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/adocean/adoceantest/params/race/banner.json b/adapters/adocean/adoceantest/params/race/banner.json new file mode 100644 index 00000000000..f9f38481350 --- /dev/null +++ b/adapters/adocean/adoceantest/params/race/banner.json @@ -0,0 +1,5 @@ +{ + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" +} diff --git a/adapters/adocean/adoceantest/supplemental/bad-response.json b/adapters/adocean/adoceantest/supplemental/bad-response.json new file mode 100644 index 00000000000..e7b871f9a09 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/bad-response.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "9ed903f4-383d-406b-8011-4f06526cb02c", + "source": { + "tid": "9ed903f4-383d-406b-8011-4f06526cb02c" + }, + "tmax": 1000, + "imp": [ + { + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://example.com/test.html" + }, + "device": { + "w": 1280, + "h": 720, + "ip": "192.168.1.1" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aosspsizes=myaozpniqismex~300x250&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": "{ key: nil }" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type []adocean.ResponseAdUnit", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/encode-error.json b/adapters/adocean/adoceantest/supplemental/encode-error.json new file mode 100644 index 00000000000..8dfd0f83e66 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/encode-error.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "9ed903f4-383d-406b-8011-4f06526cb02c", + "source": { + "tid": "9ed903f4-383d-406b-8011-4f06526cb02c" + }, + "tmax": 1000, + "imp": [ + { + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://example.com/test.html" + }, + "device": { + "w": 1280, + "h": 720, + "ip": "192.168.1.1" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aosspsizes=myaozpniqismex~300x250&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": "adoceanmyaozpniqismex", + "price": "1", + "winurl": "", + "statsUrl": "", + "code": " %a", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + } + ] + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "invalid URL escape \"%a\"", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/network-error.json b/adapters/adocean/adoceantest/supplemental/network-error.json new file mode 100644 index 00000000000..54e528af369 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/network-error.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "9ed903f4-383d-406b-8011-4f06526cb02c", + "source": { + "tid": "9ed903f4-383d-406b-8011-4f06526cb02c" + }, + "tmax": 1000, + "imp": [ + { + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://example.com/test.html" + }, + "device": { + "w": 1280, + "h": 720, + "ip": "192.168.1.1" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aosspsizes=myaozpniqismex~300x250&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Network error?", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/no-bid.json b/adapters/adocean/adoceantest/supplemental/no-bid.json new file mode 100644 index 00000000000..625fb78f3f6 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/no-bid.json @@ -0,0 +1,159 @@ +{ + "mockBidRequest": { + "id": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b", + "source": { + "tid": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b" + }, + "tmax": 1000, + "imp": [{ + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }, { + "id": "ao-test-two", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }, { + "id": "ao-test-three", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://localhost/prebid_server/test.html" + }, + "device": { + "w": 418, + "h": 961 + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aid=adoceanmyaowafpdwlrks%3Aao-test-two&aosspsizes=myaowafpdwlrks~300x250-myaozpniqismex~300x250&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [{ + "id": "adoceanmyaozpniqismex", + "error": "true" + }, + { + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "https://win-url2.com", + "statsUrl": "https://stats-url2.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + } + ] + } + }, { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaowafpdwlrks%3Aao-test-three&aosspsizes=myaowafpdwlrks~300x250&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [{ + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "https://win-url3.com", + "statsUrl": "https://stats-url3.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + }] + } + }], + "expectedBidResponses": [{ + "currency": "EUR", + "bids": [{ + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "ao-test-two", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }, { + "currency": "EUR", + "bids": [{ + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "ao-test-three", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/adocean/adoceantest/supplemental/no-impression.json b/adapters/adocean/adoceantest/supplemental/no-impression.json new file mode 100644 index 00000000000..8f2a8eef351 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/no-impression.json @@ -0,0 +1,36 @@ +{ + "mockBidRequest": { + "id": "9ed903f4-383d-406b-8011-4f06526cb02c", + "source": { + "tid": "9ed903f4-383d-406b-8011-4f06526cb02c" + }, + "tmax": 1000, + "imp": [], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://example.com/test.html" + }, + "device": { + "w": 1280, + "h": 720, + "ip": "192.168.1.1" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the bid request", + "comparison": "literal" + } + ] +} diff --git a/adapters/adocean/adoceantest/supplemental/no-sizes.json b/adapters/adocean/adoceantest/supplemental/no-sizes.json new file mode 100644 index 00000000000..6286d805477 --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/no-sizes.json @@ -0,0 +1,168 @@ +{ + "mockBidRequest": { + "id": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b", + "source": { + "tid": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b" + }, + "tmax": 1000, + "imp": [{ + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + } + }, { + "id": "ao-test-two", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + }, + "banner": { + "format": [] + } + }, { + "id": "ao-test-three", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + }, + "banner": { + "w": 300, + "h": 250 + } + }], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://localhost/prebid_server/test.html" + }, + "device": { + "w": 418, + "h": 961 + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aid=adoceanmyaowafpdwlrks%3Aao-test-two&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [{ + "id": "adoceanmyaozpniqismex", + "price": "1", + "winurl": "https://win-url.com", + "statsUrl": "https://stats-url.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + }, + { + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "https://win-url2.com", + "statsUrl": "https://stats-url2.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + } + ] + } + }, { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaowafpdwlrks%3Aao-test-three&aosspsizes=myaowafpdwlrks~300x250&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [{ + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "https://win-url3.com", + "statsUrl": "https://stats-url3.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + }] + } + }], + "expectedBidResponses": [{ + "currency": "EUR", + "bids": [{ + "bid": { + "id": "adoceanmyaozpniqismex", + "impid": "ao-test", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }, { + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "ao-test-two", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }, { + "currency": "EUR", + "bids": [{ + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "ao-test-three", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/adocean/adoceantest/supplemental/requests-merge.json b/adapters/adocean/adoceantest/supplemental/requests-merge.json new file mode 100644 index 00000000000..e0736ec918f --- /dev/null +++ b/adapters/adocean/adoceantest/supplemental/requests-merge.json @@ -0,0 +1,179 @@ +{ + "mockBidRequest": { + "id": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b", + "source": { + "tid": "b5300274-a7ec-4cdb-bf5b-d75eeb481a6b" + }, + "tmax": 1000, + "imp": [{ + "id": "ao-test", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaozpniqismex" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }, { + "id": "ao-test-two", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }, { + "id": "ao-test-three", + "ext": { + "bidder": { + "emiter": "myao.adocean.pl", + "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", + "slaveId": "adoceanmyaowafpdwlrks" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includewinners": true, + "includebidderkeys": false + } + } + }, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://localhost/prebid_server/test.html" + }, + "device": { + "w": 418, + "h": 961 + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaozpniqismex%3Aao-test&aid=adoceanmyaowafpdwlrks%3Aao-test-two&aosspsizes=myaowafpdwlrks~300x250-myaozpniqismex~300x250&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [{ + "id": "adoceanmyaozpniqismex", + "price": "1", + "winurl": "https://win-url.com", + "statsUrl": "https://stats-url.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + }, + { + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "https://win-url2.com", + "statsUrl": "https://stats-url2.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + } + ] + } + }, { + "expectedRequest": { + "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaowafpdwlrks%3Aao-test-three&aosspsizes=myaowafpdwlrks~300x250&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.1.0" + }, + "mockResponse": { + "status": 200, + "body": [{ + "id": "adoceanmyaowafpdwlrks", + "price": "1", + "winurl": "https://win-url3.com", + "statsUrl": "https://stats-url3.com", + "code": " ", + "currency": "EUR", + "minFloorPrice": "0.01", + "width": "300", + "height": "250", + "crid": "0af345b42983cc4bc0", + "ttl": "300" + }] + } + }], + "expectedBidResponses": [{ + "currency": "EUR", + "bids": [{ + "bid": { + "id": "adoceanmyaozpniqismex", + "impid": "ao-test", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }, { + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "ao-test-two", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }, { + "currency": "EUR", + "bids": [{ + "bid": { + "id": "adoceanmyaowafpdwlrks", + "impid": "ao-test-three", + "price": 1, + "adm": " ", + "crid": "0af345b42983cc4bc0", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/adocean/params_test.go b/adapters/adocean/params_test.go new file mode 100644 index 00000000000..91e2fbdcb67 --- /dev/null +++ b/adapters/adocean/params_test.go @@ -0,0 +1,50 @@ +package adocean + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +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.BidderAdOcean, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected adocean params: %s", validParam) + } + } +} + +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.BidderAdOcean, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"emiter": "myao.adocean.pl", "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", "slaveId": "adoceanmyaozpniqismex"}`, +} + +var invalidParams = []string{ + `{}`, + `{"masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", "slaveId": "adoceanmyaozpniqismex"}`, + `{"emiter": "myao.adocean.pl", "slaveId": "adoceanmyaozpniqismex"}`, + `{"emiter": "myao.adocean.pl", "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7"}`, + `{"emiter": "", "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", "slaveId": "adoceanmyaozpniqismex"}`, + `{"emiter": "myao.adocean.pl", "", "slaveId": "adoceanmyaozpniqismex"}`, + `{"emiter": "myao.adocean.pl", "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", "slaveId": ""}`, + `{"emiter": "myao.adocean.pl", "masterId": "tmYF.DMl7Z utQTJfTpxCOmtNPZoQUDcL.G7", "slaveId": "adoceanmyaozpniqismex"}`, + `{"emiter": "myao.adocean.pl", "masterId": "tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7", "slaveId": "adoceanmy iqismex"}`, +} diff --git a/adapters/adocean/usersync.go b/adapters/adocean/usersync.go new file mode 100644 index 00000000000..4bfe39e11e5 --- /dev/null +++ b/adapters/adocean/usersync.go @@ -0,0 +1,12 @@ +package adocean + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +func NewAdOceanSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("adocean", 328, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/adocean/usersync_test.go b/adapters/adocean/usersync_test.go new file mode 100644 index 00000000000..aa0bcb77e21 --- /dev/null +++ b/adapters/adocean/usersync_test.go @@ -0,0 +1,34 @@ +package adocean + +import ( + "testing" + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestAdOceanSyncer(t *testing.T) { + syncURL := "https://sync-host.com/redataredir/?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&url=localhost%2Fsetuid%3Fbidder%3Dadocean%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3DUUID" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewAdOceanSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "consent-string", + }, + }) + + assert.NoError(t, err) + assert.Equal( + t, + "https://sync-host.com/redataredir/?gdpr=1&gdpr_consent=consent-string&url=localhost%2Fsetuid%3Fbidder%3Dadocean%26gdpr%3D1%26gdpr_consent%3Dconsent-string%26uid%3DUUID", + syncInfo.URL, + ) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 328, syncer.GDPRVendorID()) +} diff --git a/adapters/adoppler/adoppler.go b/adapters/adoppler/adoppler.go new file mode 100644 index 00000000000..b37aa051363 --- /dev/null +++ b/adapters/adoppler/adoppler.go @@ -0,0 +1,210 @@ +package adoppler + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +var bidHeaders http.Header = map[string][]string{ + "Accept": {"application/json"}, + "Content-Type": {"application/json;charset=utf-8"}, + "X-OpenRTB-Version": {"2.5"}, +} + +type adsVideoExt struct { + Duration int `json:"duration"` +} + +type adsImpExt struct { + Video *adsVideoExt `json:"video"` +} + +type AdopplerAdapter struct { + endpoint string +} + +func NewAdopplerBidder(endpoint string) *AdopplerAdapter { + return &AdopplerAdapter{endpoint} +} + +func (ads *AdopplerAdapter) MakeRequests( + req *openrtb.BidRequest, + info *adapters.ExtraRequestInfo, +) ( + []*adapters.RequestData, + []error, +) { + if len(req.Imp) == 0 { + return nil, nil + } + + var datas []*adapters.RequestData + var errs []error + for _, imp := range req.Imp { + ext, err := unmarshalExt(imp.Ext) + if err != nil { + errs = append(errs, &errortypes.BadInput{err.Error()}) + continue + } + + var r openrtb.BidRequest = *req + r.ID = req.ID + "-" + ext.AdUnit + r.Imp = []openrtb.Imp{imp} + + body, err := json.Marshal(r) + if err != nil { + errs = append(errs, err) + continue + } + + uri := fmt.Sprintf("%s/processHeaderBid/%s", + ads.endpoint, url.PathEscape(ext.AdUnit)) + data := &adapters.RequestData{ + Method: "POST", + Uri: uri, + Body: body, + Headers: bidHeaders, + } + datas = append(datas, data) + } + + return datas, errs +} + +func (ads *AdopplerAdapter) MakeBids( + intReq *openrtb.BidRequest, + extReq *adapters.RequestData, + resp *adapters.ResponseData, +) ( + *adapters.BidderResponse, + []error, +) { + if resp.StatusCode == http.StatusNoContent { + return nil, nil + } + if resp.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{"bad request"}} + } + if resp.StatusCode != http.StatusOK { + err := &errortypes.BadServerResponse{ + fmt.Sprintf("unexpected status: %d", resp.StatusCode), + } + return nil, []error{err} + } + + var bidResp openrtb.BidResponse + err := json.Unmarshal(resp.Body, &bidResp) + if err != nil { + err := &errortypes.BadServerResponse{ + fmt.Sprintf("invalid body: %s", err.Error()), + } + return nil, []error{err} + } + + impTypes := make(map[string]openrtb_ext.BidType) + for _, imp := range intReq.Imp { + if _, ok := impTypes[imp.ID]; ok { + return nil, []error{&errortypes.BadInput{ + fmt.Sprintf("duplicate $.imp.id %s", imp.ID), + }} + } + if imp.Banner != nil { + impTypes[imp.ID] = openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + impTypes[imp.ID] = openrtb_ext.BidTypeVideo + } else if imp.Audio != nil { + impTypes[imp.ID] = openrtb_ext.BidTypeAudio + } else if imp.Native != nil { + impTypes[imp.ID] = openrtb_ext.BidTypeNative + } else { + return nil, []error{&errortypes.BadInput{ + "one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required", + }} + } + } + + var bids []*adapters.TypedBid + for _, seatBid := range bidResp.SeatBid { + for _, bid := range seatBid.Bid { + tp, ok := impTypes[bid.ImpID] + if !ok { + err := &errortypes.BadServerResponse{ + fmt.Sprintf("unknown impid: %s", bid.ImpID), + } + return nil, []error{err} + } + + var bidVideo *openrtb_ext.ExtBidPrebidVideo + if tp == openrtb_ext.BidTypeVideo { + adsExt, err := unmarshalAdsExt(bid.Ext) + if err != nil { + return nil, []error{&errortypes.BadServerResponse{err.Error()}} + } + if adsExt == nil || adsExt.Video == nil { + return nil, []error{&errortypes.BadServerResponse{ + "$.seatbid.bid.ext.ads.video required", + }} + } + bidVideo = &openrtb_ext.ExtBidPrebidVideo{ + Duration: adsExt.Video.Duration, + PrimaryCategory: head(bid.Cat), + } + } + bids = append(bids, &adapters.TypedBid{ + Bid: &bid, + BidType: tp, + BidVideo: bidVideo, + }) + } + } + + adsResp := adapters.NewBidderResponseWithBidsCapacity(len(bids)) + adsResp.Bids = bids + + return adsResp, nil +} + +func unmarshalExt(ext json.RawMessage) (*openrtb_ext.ExtImpAdoppler, error) { + var bext adapters.ExtImpBidder + err := json.Unmarshal(ext, &bext) + if err != nil { + return nil, err + } + + var adsExt openrtb_ext.ExtImpAdoppler + err = json.Unmarshal(bext.Bidder, &adsExt) + if err != nil { + return nil, err + } + + if adsExt.AdUnit == "" { + return nil, errors.New("$.imp.ext.adoppler.adunit required") + } + + return &adsExt, nil +} + +func unmarshalAdsExt(ext json.RawMessage) (*adsImpExt, error) { + var e struct { + Ads *adsImpExt `json:"ads"` + } + err := json.Unmarshal(ext, &e) + + return e.Ads, err +} + +func head(s []string) string { + if len(s) == 0 { + return "" + } + + return s[0] +} diff --git a/adapters/adoppler/adoppler_test.go b/adapters/adoppler/adoppler_test.go new file mode 100644 index 00000000000..c3287ed4adb --- /dev/null +++ b/adapters/adoppler/adoppler_test.go @@ -0,0 +1,12 @@ +package adoppler + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + bidder := NewAdopplerBidder("http://adoppler.com") + adapterstest.RunJSONBidderTest(t, "adopplertest", bidder) +} diff --git a/adapters/adoppler/adopplertest/exemplary/multibid.json b/adapters/adoppler/adopplertest/exemplary/multibid.json new file mode 100644 index 00000000000..851f4c5b917 --- /dev/null +++ b/adapters/adoppler/adopplertest/exemplary/multibid.json @@ -0,0 +1,60 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}, + {"id": "imp2", + "video": {"minduration": 120, + "mimes": ["video/mp4"]}, + "ext": {"bidder": {"adunit": "unit2"}}}, + {"id": "imp3", + "native": {"request": "{}"}, + "ext": {"bidder": {"adunit": "unit3"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 200, + "body": {"id": "req1-imp1-resp1", + "seatbid": [{"bid": [{"id": "req1-imp1-bid1", + "impid": "imp1", + "price": 0.12, + "adm": "a banner"}]}], + "cur": "USD"}}}, + {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2", + "body": {"id": "req1-unit2", + "imp": [{"id": "imp2", + "video": {"minduration": 120, + "mimes": ["video/mp4"]}, + "ext": {"bidder": {"adunit": "unit2"}}}]}}, + "mockResponse": {"status": 200, + "body": {"id": "req1-imp2-resp2", + "seatbid": [{"bid": [{"id": "req1-imp2-bid1", + "impid": "imp2", + "price": 0.24, + "adm": "", + "cat": ["IAB1", "IAB2"], + "ext": {"ads": {"video": {"duration": 121}}}}]}], + "cur": "USD"}}}, + {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit3", + "body": {"id": "req1-unit3", + "imp": [{"id": "imp3", + "native": {"request": "{}"}, + "ext": {"bidder": {"adunit": "unit3"}}}]}}, + "mockResponse": {"status": 204, + "body": ""}}], + "expectedBidResponses": [{"currency": "USD", + "bids": [{"bid": {"id": "req1-imp1-bid1", + "impid": "imp1", + "price": 0.12, + "adm": "a banner"}, + "type": "banner"}]}, + {"currency": "USD", + "bids": [{"bid": {"id": "req1-imp2-bid1", + "impid": "imp2", + "price": 0.24, + "adm": "", + "cat": ["IAB1", "IAB2"], + "ext": {"ads": {"video": {"duration": 121}}}}, + "type": "video"}]}]} diff --git a/adapters/adoppler/adopplertest/exemplary/no-bid.json b/adapters/adoppler/adopplertest/exemplary/no-bid.json new file mode 100644 index 00000000000..0e0f13586a8 --- /dev/null +++ b/adapters/adoppler/adopplertest/exemplary/no-bid.json @@ -0,0 +1,13 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 204, + "body": ""}}], + "expectedBidResponses": []} diff --git a/adapters/adoppler/adopplertest/supplemental/bad-request.json b/adapters/adoppler/adopplertest/supplemental/bad-request.json new file mode 100644 index 00000000000..3bdd5a5544e --- /dev/null +++ b/adapters/adoppler/adopplertest/supplemental/bad-request.json @@ -0,0 +1,15 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 400, + "body": ""}}], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [{"value": "bad request", + "comparison": "literal"}]} diff --git a/adapters/adoppler/adopplertest/supplemental/duplicate-imp.json b/adapters/adoppler/adopplertest/supplemental/duplicate-imp.json new file mode 100644 index 00000000000..4382e36c54e --- /dev/null +++ b/adapters/adoppler/adopplertest/supplemental/duplicate-imp.json @@ -0,0 +1,38 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}, + {"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit2"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 200, + "body": {"id": "req1-imp1-resp1", + "seatbid": [{"bid": [{"id": "req1-imp1-bid1", + "impid": "imp1", + "price": 0.12, + "adm": "a banner"}]}], + "cur": "USD"}}}, + {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2", + "body": {"id": "req1-unit2", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit2"}}}]}}, + "mockResponse": {"status": 200, + "body": {"id": "req1-imp1-resp1", + "seatbid": [{"bid": [{"id": "req1-imp1-bid1", + "impid": "imp1", + "price": 0.12, + "adm": "a banner"}]}], + "cur": "USD"}}}], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [{"value": "duplicate $.imp.id imp1", + "comparison": "literal"}, + {"value": "duplicate $.imp.id imp1", + "comparison": "literal"}]} diff --git a/adapters/adoppler/adopplertest/supplemental/invalid-impid.json b/adapters/adoppler/adopplertest/supplemental/invalid-impid.json new file mode 100644 index 00000000000..2e6ecf4a96c --- /dev/null +++ b/adapters/adoppler/adopplertest/supplemental/invalid-impid.json @@ -0,0 +1,20 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 200, + "body": {"id": "req1-imp1-resp1", + "seatbid": [{"bid": [{"id": "req1-imp1-bid1", + "impid": "invalid", + "price": 0.12, + "adm": "a banner"}]}], + "cur": "USD"}}}], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [{"value": "unknown impid: invalid", + "comparison": "literal"}]} diff --git a/adapters/adoppler/adopplertest/supplemental/invalid-response.json b/adapters/adoppler/adopplertest/supplemental/invalid-response.json new file mode 100644 index 00000000000..72420881aec --- /dev/null +++ b/adapters/adoppler/adopplertest/supplemental/invalid-response.json @@ -0,0 +1,15 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 200, + "body": "invalid-json"}}], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [{"value": "invalid body: json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal"}]} diff --git a/adapters/adoppler/adopplertest/supplemental/invalid-video-ext.json b/adapters/adoppler/adopplertest/supplemental/invalid-video-ext.json new file mode 100644 index 00000000000..d9cb6daa55d --- /dev/null +++ b/adapters/adoppler/adopplertest/supplemental/invalid-video-ext.json @@ -0,0 +1,43 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "video": {"minduration": 120, + "mimes": ["video/mp4"]}, + "ext": {"bidder": {"adunit": "unit1"}}}, + {"id": "imp2", + "video": {"minduration": 120, + "mimes": ["video/mp4"]}, + "ext": {"bidder": {"adunit": "unit2"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "video": {"minduration": 120, + "mimes": ["video/mp4"]}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 200, + "body": {"id": "req1-imp1-resp1", + "seatbid": [{"bid": [{"id": "req1-imp1-bid1", + "impid": "imp1", + "price": 0.24, + "adm": "", + "cat": ["IAB1", "IAB2"], + "ext": {}}]}], + "cur": "USD"}}}, + {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2", + "body": {"id": "req1-unit2", + "imp": [{"id": "imp2", + "video": {"minduration": 120, + "mimes": ["video/mp4"]}, + "ext": {"bidder": {"adunit": "unit2"}}}]}}, + "mockResponse": {"status": 200, + "body": {"id": "req1-imp2-resp2", + "seatbid": [{"bid": [{"id": "req1-imp2-bid2", + "impid": "imp2", + "price": 0.24, + "adm": "", + "cat": ["IAB1", "IAB2"], + "ext": ""}]}], + "cur": "USD"}}}], + "expectedMakeBidsErrors": [{"value": "$.seatbid.bid.ext.ads.video required", + "comparison": "literal"}, + {"value": "json: cannot unmarshal string into Go value of type struct { Ads *adoppler.adsImpExt \"json:\\\"ads\\\"\" }", + "comparison": "literal"}]} diff --git a/adapters/adoppler/adopplertest/supplemental/missing-adunit.json b/adapters/adoppler/adopplertest/supplemental/missing-adunit.json new file mode 100644 index 00000000000..82a6a95ed58 --- /dev/null +++ b/adapters/adoppler/adopplertest/supplemental/missing-adunit.json @@ -0,0 +1,9 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {}}}]}, + "httpCalls": [], + "expectedBidResponses": [], + "expectedMakeRequestsErrors": [{"value": "$.imp.ext.adoppler.adunit required", + "comparison": "literal"}]} diff --git a/adapters/adoppler/adopplertest/supplemental/server-error.json b/adapters/adoppler/adopplertest/supplemental/server-error.json new file mode 100644 index 00000000000..df23bac07df --- /dev/null +++ b/adapters/adoppler/adopplertest/supplemental/server-error.json @@ -0,0 +1,15 @@ +{"mockBidRequest": {"id": "req1", + "imp":[{"id": "imp1", + "banner": {"w": 100, + "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}, + "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1", + "body": {"id": "req1-unit1", + "imp": [{"id": "imp1", + "banner": {"w": 100, "h": 200}, + "ext": {"bidder": {"adunit": "unit1"}}}]}}, + "mockResponse": {"status": 500, + "body": ""}}], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [{"value": "unexpected status: 500", + "comparison": "literal"}]} diff --git a/adapters/adpone/adpone.go b/adapters/adpone/adpone.go index b948ff5e383..9064e971fcb 100644 --- a/adapters/adpone/adpone.go +++ b/adapters/adpone/adpone.go @@ -3,9 +3,10 @@ package adpone import ( "encoding/json" "fmt" - "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "net/http" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/errortypes" diff --git a/adapters/adtarget/adtarget.go b/adapters/adtarget/adtarget.go new file mode 100644 index 00000000000..d3d13fd33de --- /dev/null +++ b/adapters/adtarget/adtarget.go @@ -0,0 +1,189 @@ +package adtarget + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +type AdtargetAdapter struct { + endpoint string +} + +type adtargetImpExt struct { + Adtarget openrtb_ext.ExtImpAdtarget `json:"adtarget"` +} + +func (a *AdtargetAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + totalImps := len(request.Imp) + errors := make([]error, 0, totalImps) + imp2source := make(map[int][]int) + + for i := 0; i < totalImps; i++ { + + sourceId, err := validateImpressionAndSetExt(&request.Imp[i]) + + if err != nil { + errors = append(errors, err) + continue + } + + if _, ok := imp2source[sourceId]; !ok { + imp2source[sourceId] = make([]int, 0, totalImps-i) + } + + imp2source[sourceId] = append(imp2source[sourceId], i) + + } + + totalReqs := len(imp2source) + if 0 == totalReqs { + return nil, errors + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + reqs := make([]*adapters.RequestData, 0, totalReqs) + + imps := request.Imp + request.Imp = make([]openrtb.Imp, 0, len(imps)) + for sourceId, impIndexes := range imp2source { + request.Imp = request.Imp[:0] + + for i := 0; i < len(impIndexes); i++ { + request.Imp = append(request.Imp, imps[impIndexes[i]]) + } + + body, err := json.Marshal(request) + if err != nil { + errors = append(errors, fmt.Errorf("error while encoding bidRequest, err: %s", err)) + return nil, errors + } + + reqs = append(reqs, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint + fmt.Sprintf("?aid=%d", sourceId), + Body: body, + Headers: headers, + }) + } + + return reqs, errors +} + +func (a *AdtargetAdapter) MakeBids(bidReq *openrtb.BidRequest, unused *adapters.RequestData, httpRes *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + if httpRes.StatusCode == http.StatusNoContent { + return nil, nil + } + if httpRes.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", httpRes.StatusCode), + }} + } + var bidResp openrtb.BidResponse + if err := json.Unmarshal(httpRes.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("error while decoding response, err: %s", err), + }} + } + + bidResponse := adapters.NewBidderResponse() + var errors []error + + var impOK bool + for _, sb := range bidResp.SeatBid { + for i := 0; i < len(sb.Bid); i++ { + + bid := sb.Bid[i] + + impOK = false + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range bidReq.Imp { + if imp.ID == bid.ImpID { + + impOK = true + + if imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + break + } + } + } + + if !impOK { + errors = append(errors, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("ignoring bid id=%s, request doesn't contain any impression with id=%s", bid.ID, bid.ImpID), + }) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: mediaType, + }) + } + } + + return bidResponse, errors +} + +func validateImpressionAndSetExt(imp *openrtb.Imp) (int, error) { + + if imp.Banner == nil && imp.Video == nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, Adtarget supports only Video and Banner", imp.ID), + } + } + + if 0 == len(imp.Ext) { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, extImpBidder is empty", imp.ID), + } + } + + var bidderExt adapters.ExtImpBidder + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), + } + } + + impExt := openrtb_ext.ExtImpAdtarget{} + err := json.Unmarshal(bidderExt.Bidder, &impExt) + if err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), + } + } + + // common extension for all impressions + var impExtBuffer []byte + + impExtBuffer, err = json.Marshal(&adtargetImpExt{ + Adtarget: impExt, + }) + + if impExt.BidFloor > 0 { + imp.BidFloor = impExt.BidFloor + } + + imp.Ext = impExtBuffer + + return impExt.SourceId, nil +} + +func NewAdtargetBidder(endpoint string) *AdtargetAdapter { + return &AdtargetAdapter{ + endpoint: endpoint, + } +} diff --git a/adapters/adtarget/adtarget_test.go b/adapters/adtarget/adtarget_test.go new file mode 100644 index 00000000000..1fd67dfe7b1 --- /dev/null +++ b/adapters/adtarget/adtarget_test.go @@ -0,0 +1,11 @@ +package adtarget + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "adtargettest", NewAdtargetBidder("http://ghb.console.adtarget.com.tr/pbs/ortb")) +} diff --git a/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json b/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json new file mode 100644 index 00000000000..518268d4fea --- /dev/null +++ b/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json @@ -0,0 +1,88 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "aid": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "adtarget": { + "aid": 1000 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 3.5, + "w": 900, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/adtarget/adtargettest/exemplary/simple-banner.json b/adapters/adtarget/adtargettest/exemplary/simple-banner.json new file mode 100644 index 00000000000..b63739bda0f --- /dev/null +++ b/adapters/adtarget/adtargettest/exemplary/simple-banner.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "aid": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [ + {"w":300,"h":250}, + {"w":300,"h":600} + ] + }, + "bidfloor": 20, + "ext": { + "adtarget": { + "aid": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} \ No newline at end of file diff --git a/adapters/adtarget/adtargettest/exemplary/simple-video.json b/adapters/adtarget/adtargettest/exemplary/simple-video.json new file mode 100644 index 00000000000..4dc4547d7d1 --- /dev/null +++ b/adapters/adtarget/adtargettest/exemplary/simple-video.json @@ -0,0 +1,55 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "aid": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "adtarget": { + "aid": 1000 + } + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} \ No newline at end of file diff --git a/adapters/adtarget/adtargettest/params/race/banner.json b/adapters/adtarget/adtargettest/params/race/banner.json new file mode 100644 index 00000000000..1d6658c71ab --- /dev/null +++ b/adapters/adtarget/adtargettest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "aid": 350975 +} diff --git a/adapters/adtarget/adtargettest/params/race/video.json b/adapters/adtarget/adtargettest/params/race/video.json new file mode 100644 index 00000000000..fe4207ef05c --- /dev/null +++ b/adapters/adtarget/adtargettest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "aid": 331133 +} diff --git a/adapters/adtarget/adtargettest/supplemental/audio.json b/adapters/adtarget/adtargettest/supplemental/audio.json new file mode 100644 index 00000000000..e2148e9db99 --- /dev/null +++ b/adapters/adtarget/adtargettest/supplemental/audio.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "unsupported-audio-request", + "imp": [ + { + "id": "unsupported-audio-imp", + "audio": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-audio-imp, Adtarget supports only Video and Banner", + "comparison": "literal" + } + ] +} diff --git a/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json b/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json new file mode 100644 index 00000000000..a4e487466ea --- /dev/null +++ b/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "aid": 1000 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "adtarget": { + "aid": 1000 + } + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/adtarget/adtargettest/supplemental/native.json b/adapters/adtarget/adtargettest/supplemental/native.json new file mode 100644 index 00000000000..3d9aa6630eb --- /dev/null +++ b/adapters/adtarget/adtargettest/supplemental/native.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "native": { + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, Adtarget supports only Video and Banner", + "comparison": "literal" + } + ] +} diff --git a/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json b/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json new file mode 100644 index 00000000000..1986dfaf13f --- /dev/null +++ b/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json @@ -0,0 +1,26 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "video": { + "w": 100, + "h": 200 + }, + "ext": { + "bidder": { + "aid": "some string instead of int" + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, error while decoding impExt, err: json: cannot unmarshal string into Go struct field ExtImpAdtarget.aid of type int", + "comparison": "literal" + } + ] +} diff --git a/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..0dffdb2bebb --- /dev/null +++ b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,77 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "aid": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "adtarget": { + "aid": 1000 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "SOME-WRONG-IMP-ID", + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/adtarget/params_test.go b/adapters/adtarget/params_test.go new file mode 100644 index 00000000000..61ed4885512 --- /dev/null +++ b/adapters/adtarget/params_test.go @@ -0,0 +1,60 @@ +package adtarget + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/adtarget.json +// These also validate the format of the external API: request.imp[i].ext.adtarget +// TestValidParams makes sure that the adtarget 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.BidderAdtarget, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected adtarget params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the adtarget 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.BidderAdtarget, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"aid":123}`, + `{"aid":123,"placementId":1234}`, + `{"aid":123,"siteId":4321}`, + `{"aid":123,"siteId":0,"bidFloor":0}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"aid":"123"}`, + `{"aid":"0"}`, + `{"aid":"123","placementId":"123"}`, + `{"aid":123, "placementId":"123", "siteId":"321"}`, +} diff --git a/adapters/adtarget/usersync.go b/adapters/adtarget/usersync.go new file mode 100644 index 00000000000..93e57b173f6 --- /dev/null +++ b/adapters/adtarget/usersync.go @@ -0,0 +1,12 @@ +package adtarget + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +func NewAdtargetSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("adtarget", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/adtarget/usersync_test.go b/adapters/adtarget/usersync_test.go new file mode 100644 index 00000000000..ccaf7ee1bf9 --- /dev/null +++ b/adapters/adtarget/usersync_test.go @@ -0,0 +1,37 @@ +package adtarget + +import ( + "fmt" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" + "testing" + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestAdtargetSyncer(t *testing.T) { + syncURL := "//sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=localhost%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D" + fmt.Println("adtarget sync") + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewAdtargetSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + Consent: "123", + }, + CCPA: ccpa.Policy{ + Value: "1-YY", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "//sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr=0&gdpr_consent=123&us_privacy=1-YY&redir=localhost%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D0%26gdpr_consent%3D123%26uid%3D%7Buid%7D", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/adapters/adtelligent/adtelligent.go b/adapters/adtelligent/adtelligent.go index ab35436e351..60989aaa315 100644 --- a/adapters/adtelligent/adtelligent.go +++ b/adapters/adtelligent/adtelligent.go @@ -55,7 +55,6 @@ func (a *AdtelligentAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo * imps := request.Imp request.Imp = make([]openrtb.Imp, 0, len(imps)) - for sourceId, impIds := range imp2source { request.Imp = request.Imp[:0] diff --git a/adapters/adtelligent/adtelligent_test.go b/adapters/adtelligent/adtelligent_test.go index 9b42bbb10d1..ce8d24a3c21 100644 --- a/adapters/adtelligent/adtelligent_test.go +++ b/adapters/adtelligent/adtelligent_test.go @@ -7,5 +7,5 @@ import ( ) func TestJsonSamples(t *testing.T) { - adapterstest.RunJSONBidderTest(t, "adtelligenttest", NewAdtelligentBidder("http://hb.adtelligent.com/auction")) + adapterstest.RunJSONBidderTest(t, "adtelligenttest", NewAdtelligentBidder("http://ghb.adtelligent.com/pbs/ortb")) } diff --git a/adapters/adtelligent/adtelligenttest/exemplary/media-type-mapping.json b/adapters/adtelligent/adtelligenttest/exemplary/media-type-mapping.json index 67ad2fd2915..553ec61833b 100644 --- a/adapters/adtelligent/adtelligenttest/exemplary/media-type-mapping.json +++ b/adapters/adtelligent/adtelligenttest/exemplary/media-type-mapping.json @@ -24,7 +24,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://hb.adtelligent.com/auction?aid=1000", + "uri": "http://ghb.adtelligent.com/pbs/ortb?aid=1000", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/adtelligent/adtelligenttest/exemplary/simple-banner.json b/adapters/adtelligent/adtelligenttest/exemplary/simple-banner.json index 6648229de95..a06477b4d18 100644 --- a/adapters/adtelligent/adtelligenttest/exemplary/simple-banner.json +++ b/adapters/adtelligent/adtelligenttest/exemplary/simple-banner.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://hb.adtelligent.com/auction?aid=1000", + "uri": "http://ghb.adtelligent.com/pbs/ortb?aid=1000", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/adtelligent/adtelligenttest/exemplary/simple-video.json b/adapters/adtelligent/adtelligenttest/exemplary/simple-video.json index 97769651997..f108cc94b17 100644 --- a/adapters/adtelligent/adtelligenttest/exemplary/simple-video.json +++ b/adapters/adtelligent/adtelligenttest/exemplary/simple-video.json @@ -24,7 +24,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://hb.adtelligent.com/auction?aid=1000", + "uri": "http://ghb.adtelligent.com/pbs/ortb?aid=1000", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/adtelligent/adtelligenttest/supplemental/explicit-dimensions.json b/adapters/adtelligent/adtelligenttest/supplemental/explicit-dimensions.json index 9dc279bcd1c..6155e9bc56b 100644 --- a/adapters/adtelligent/adtelligenttest/supplemental/explicit-dimensions.json +++ b/adapters/adtelligent/adtelligenttest/supplemental/explicit-dimensions.json @@ -25,7 +25,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://hb.adtelligent.com/auction?aid=1000", + "uri": "http://ghb.adtelligent.com/pbs/ortb?aid=1000", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json b/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json index 94df34af40d..2e5aeff311f 100644 --- a/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json +++ b/adapters/adtelligent/adtelligenttest/supplemental/wrong-impression-mapping.json @@ -24,7 +24,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://hb.adtelligent.com/auction?aid=1000", + "uri": "http://ghb.adtelligent.com/pbs/ortb?aid=1000", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/advangelists/usersync.go b/adapters/advangelists/usersync.go index 5ba287757b8..b1539d0093d 100644 --- a/adapters/advangelists/usersync.go +++ b/adapters/advangelists/usersync.go @@ -8,5 +8,5 @@ import ( ) func NewAdvangelistsSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("advangelists", 61, temp, adapters.SyncTypeIframe) + return adapters.NewSyncer("advangelists", 0, temp, adapters.SyncTypeIframe) } diff --git a/adapters/advangelists/usersync_test.go b/adapters/advangelists/usersync_test.go index a68472fb4bf..5dae85b11a9 100644 --- a/adapters/advangelists/usersync_test.go +++ b/adapters/advangelists/usersync_test.go @@ -26,6 +26,6 @@ func TestAdvangelistsSyncer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "https://nep.advangelists.com/xp/user-sync?acctid={aid}&&redirect=localhost/setuid?bidder=advangelists&gdpr=1&gdpr_consent=BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA&uid=$UID", syncInfo.URL) assert.Equal(t, "iframe", syncInfo.Type) - assert.EqualValues(t, 61, syncer.GDPRVendorID()) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) assert.Equal(t, false, syncInfo.SupportCORS) } diff --git a/adapters/aja/aja.go b/adapters/aja/aja.go new file mode 100644 index 00000000000..55de9567ff8 --- /dev/null +++ b/adapters/aja/aja.go @@ -0,0 +1,132 @@ +package aja + +import ( + "encoding/json" + "fmt" + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "net/http" +) + +type AJAAdapter struct { + endpoint string +} + +func (a *AJAAdapter) MakeRequests(bidReq *openrtb.BidRequest, extraInfo *adapters.ExtraRequestInfo) (adapterReqs []*adapters.RequestData, errs []error) { + // split imps by tagid + tagIDs := []string{} + impsByTagID := map[string][]openrtb.Imp{} + for _, imp := range bidReq.Imp { + extAJA, err := parseExtAJA(imp) + if err != nil { + errs = append(errs, err) + continue + } + imp.TagID = extAJA.AdSpotID + imp.Ext = nil + if _, ok := impsByTagID[imp.TagID]; !ok { + tagIDs = append(tagIDs, imp.TagID) + } + impsByTagID[imp.TagID] = append(impsByTagID[imp.TagID], imp) + } + + req := *bidReq + for _, tagID := range tagIDs { + req.Imp = impsByTagID[tagID] + body, err := json.Marshal(req) + if err != nil { + errs = append(errs, &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to unmarshal bidrequest ID: %s err: %s", bidReq.ID, err), + }) + continue + } + adapterReqs = append(adapterReqs, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: body, + }) + } + + return +} + +func parseExtAJA(imp openrtb.Imp) (openrtb_ext.ExtImpAJA, error) { + var ( + extImp adapters.ExtImpBidder + extAJA openrtb_ext.ExtImpAJA + ) + + if err := json.Unmarshal(imp.Ext, &extImp); err != nil { + return extAJA, &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to unmarshal ext impID: %s err: %s", imp.ID, err), + } + } + + if err := json.Unmarshal(extImp.Bidder, &extAJA); err != nil { + return extAJA, &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to unmarshal ext.bidder impID: %s err: %s", imp.ID, err), + } + } + + return extAJA, nil +} + +func (a *AJAAdapter) MakeBids(bidReq *openrtb.BidRequest, adapterReq *adapters.RequestData, adapterResp *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapterResp.StatusCode != http.StatusOK { + if adapterResp.StatusCode == http.StatusNoContent { + return nil, nil + } + if adapterResp.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d", adapterResp.StatusCode), + }} + } + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d", adapterResp.StatusCode), + }} + } + + var bidResp openrtb.BidResponse + if err := json.Unmarshal(adapterResp.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to unmarshal bid response: %s", err.Error()), + }} + } + + bidderResp := adapters.NewBidderResponseWithBidsCapacity(len(bidReq.Imp)) + var errors []error + + for _, seatbid := range bidResp.SeatBid { + for _, bid := range seatbid.Bid { + for _, imp := range bidReq.Imp { + if imp.ID == bid.ImpID { + var bidType openrtb_ext.BidType + if imp.Banner != nil { + bidType = openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + bidType = openrtb_ext.BidTypeVideo + } else { + errors = append(errors, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Response received for unexpected type of bid bidID: %s", bid.ID), + }) + continue + } + bidderResp.Bids = append(bidderResp.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + }) + break + } + } + } + } + return bidderResp, errors +} + +func NewAJABidder(endpoint string) adapters.Bidder { + return &AJAAdapter{ + endpoint: endpoint, + } +} diff --git a/adapters/aja/aja_test.go b/adapters/aja/aja_test.go new file mode 100644 index 00000000000..95906b14c2a --- /dev/null +++ b/adapters/aja/aja_test.go @@ -0,0 +1,13 @@ +package aja + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +const testsBidderEndpoint = "https://localhost/bid/4" + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "ajatest", NewAJABidder(testsBidderEndpoint)) +} diff --git a/adapters/aja/ajatest/exemplary/banner-multiple-imps.json b/adapters/aja/ajatest/exemplary/banner-multiple-imps.json new file mode 100644 index 00000000000..8de9a31eadb --- /dev/null +++ b/adapters/aja/ajatest/exemplary/banner-multiple-imps.json @@ -0,0 +1,159 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "asi": "test-asi" + } + } + }, + { + "id": "test-imp-id2", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "asi": "test-asi2" + } + } + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpcalls": [ + { + "expectedRequest": { + "uri": "https://localhost/bid/4", + "headers": {}, + "body": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "test-asi" + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-req-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1, + "adm": "
test
", + "crid": "test-creative-id" + } + ] + } + ], + "bidid": "test-seatbid-id", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "https://localhost/bid/4", + "headers": {}, + "body": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id2", + "banner": { + "w": 300, + "h": 250 + }, + "tagid": "test-asi2" + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-req-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id2", + "impid": "test-imp-id2", + "price": 1, + "adm": "
test2
", + "crid": "test-creative-id2" + } + ] + } + ], + "bidid": "test-seatbid-id", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1, + "adm": "
test
", + "crid": "test-creative-id" + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id2", + "impid": "test-imp-id2", + "price": 1, + "adm": "
test2
", + "crid": "test-creative-id2" + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/aja/ajatest/exemplary/video.json b/adapters/aja/ajatest/exemplary/video.json new file mode 100644 index 00000000000..a7991570bba --- /dev/null +++ b/adapters/aja/ajatest/exemplary/video.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "asi": "test-asi" + } + } + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/bid/4", + "headers": {}, + "body": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "tagid": "test-asi" + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-req-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1, + "adm": "", + "crid": "test-creative-id" + } + ] + } + ], + "bidid": "test-seatbid-id", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1, + "adm": "", + "crid": "test-creative-id" + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/aja/ajatest/params/race/banner.json b/adapters/aja/ajatest/params/race/banner.json new file mode 100644 index 00000000000..6d50c2d1880 --- /dev/null +++ b/adapters/aja/ajatest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "asi": "abc123" +} \ No newline at end of file diff --git a/adapters/aja/ajatest/params/race/video.json b/adapters/aja/ajatest/params/race/video.json new file mode 100644 index 00000000000..6d50c2d1880 --- /dev/null +++ b/adapters/aja/ajatest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "asi": "abc123" +} \ No newline at end of file diff --git a/adapters/aja/ajatest/supplemental/invalid-bid-type.json b/adapters/aja/ajatest/supplemental/invalid-bid-type.json new file mode 100644 index 00000000000..1bba635f731 --- /dev/null +++ b/adapters/aja/ajatest/supplemental/invalid-bid-type.json @@ -0,0 +1,71 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "asi": "test-asi" + } + } + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/bid/4", + "headers": {}, + "body": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test-asi" + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-req-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1, + "adm": "", + "crid": "test-creative-id" + } + ] + } + ], + "bidid": "test-seatbid-id", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [], + + "expectedMakeBidsErrors": [ + { + "value": "Response received for unexpected type of bid bidID: test-bid-id", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/aja/ajatest/supplemental/invalid-ext-bidder.json b/adapters/aja/ajatest/supplemental/invalid-ext-bidder.json new file mode 100644 index 00000000000..b12b431b0ed --- /dev/null +++ b/adapters/aja/ajatest/supplemental/invalid-ext-bidder.json @@ -0,0 +1,36 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "asi": 111 + } + } + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpCalls": [], + + "expectedBidResponses": [], + + "expectedMakeRequestsErrors": [ + { + "value": "Failed to unmarshal ext.bidder impID: test-imp-id err: json: cannot unmarshal number into Go struct field ExtImpAJA.asi of type string", + "comparison": "literal" + } + + ] +} \ No newline at end of file diff --git a/adapters/aja/ajatest/supplemental/invalid-ext.json b/adapters/aja/ajatest/supplemental/invalid-ext.json new file mode 100644 index 00000000000..478222d0ee9 --- /dev/null +++ b/adapters/aja/ajatest/supplemental/invalid-ext.json @@ -0,0 +1,32 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": 111 + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpCalls": [], + + "expectedBidResponses": [], + + "expectedMakeRequestsErrors": [ + { + "value": "Failed to unmarshal ext impID: test-imp-id err: json: cannot unmarshal number into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + + ] +} \ No newline at end of file diff --git a/adapters/aja/ajatest/supplemental/status-bad-request.json b/adapters/aja/ajatest/supplemental/status-bad-request.json new file mode 100644 index 00000000000..a47db8bbca9 --- /dev/null +++ b/adapters/aja/ajatest/supplemental/status-bad-request.json @@ -0,0 +1,64 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "asi": "test-asi" + } + } + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/bid/4", + "headers": {}, + "body": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "tagid": "test-asi" + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + + "expectedBidResponses": [], + + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/aja/ajatest/supplemental/status-internal-server-error.json b/adapters/aja/ajatest/supplemental/status-internal-server-error.json new file mode 100644 index 00000000000..5d36dc5dcdc --- /dev/null +++ b/adapters/aja/ajatest/supplemental/status-internal-server-error.json @@ -0,0 +1,64 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "asi": "test-asi" + } + } + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/bid/4", + "headers": {}, + "body": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "tagid": "test-asi" + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + } + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + + "expectedBidResponses": [], + + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/aja/ajatest/supplemental/status-no-content.json b/adapters/aja/ajatest/supplemental/status-no-content.json new file mode 100644 index 00000000000..e12fd21a26a --- /dev/null +++ b/adapters/aja/ajatest/supplemental/status-no-content.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "asi": "test-asi" + } + } + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/bid/4", + "headers": {}, + "body": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480 + }, + "tagid": "test-asi" + } + ], + "user": { + "buyeruid": "test-uid" + }, + "tmax": 500 + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/aja/usersync.go b/adapters/aja/usersync.go new file mode 100644 index 00000000000..deddbabb1d9 --- /dev/null +++ b/adapters/aja/usersync.go @@ -0,0 +1,12 @@ +package aja + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +func NewAJASyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("aja", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/aja/usersync_test.go b/adapters/aja/usersync_test.go new file mode 100644 index 00000000000..54b3ed01212 --- /dev/null +++ b/adapters/aja/usersync_test.go @@ -0,0 +1,35 @@ +package aja + +import ( + "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" + "testing" + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestAJASyncer(t *testing.T) { + syncURL := "https://ad.as.amanad.adtdp.com/v1/sync/ssp?ssp=4&gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&redir=localhost/setuid?bidder=aja&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&uid=%s" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewAJASyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", + }, + CCPA: ccpa.Policy{ + Value: "C", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://ad.as.amanad.adtdp.com/v1/sync/ssp?ssp=4&gdpr=1&us_privacy=C&redir=localhost/setuid?bidder=aja&gdpr=1&gdpr_consent=BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA&uid=%s", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index a768133bdaf..1b3b42295d7 100644 --- a/adapters/appnexus/appnexus.go +++ b/adapters/appnexus/appnexus.go @@ -87,6 +87,7 @@ type appnexusBidExtAppnexus struct { BrandId int `json:"brand_id"` BrandCategory int `json:"brand_category_id"` CreativeInfo appnexusBidExtCreative `json:"creative_info"` + DealPriority int `json:"deal_priority"` } type appnexusBidExt struct { @@ -543,9 +544,10 @@ func (a *AppNexusAdapter) MakeBids(internalRequest *openrtb.BidRequest, external } bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ - Bid: &bid, - BidType: bidType, - BidVideo: impVideo, + Bid: &bid, + BidType: bidType, + BidVideo: impVideo, + DealPriority: bidExt.Appnexus.DealPriority, }) } else { errs = append(errs, err) diff --git a/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json b/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json index 03c3f4c5880..e0c0435faab 100644 --- a/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json +++ b/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json @@ -80,7 +80,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -118,7 +119,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexusplatformtest/video/simple-video.json b/adapters/appnexus/appnexusplatformtest/video/simple-video.json index 85960427d81..7ee192be2c1 100644 --- a/adapters/appnexus/appnexusplatformtest/video/simple-video.json +++ b/adapters/appnexus/appnexusplatformtest/video/simple-video.json @@ -80,7 +80,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -118,7 +119,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/amp/simple-banner.json b/adapters/appnexus/appnexustest/amp/simple-banner.json index 646359b4267..54e6a143e19 100644 --- a/adapters/appnexus/appnexustest/amp/simple-banner.json +++ b/adapters/appnexus/appnexustest/amp/simple-banner.json @@ -91,7 +91,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -129,7 +130,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/amp/simple-video.json b/adapters/appnexus/appnexustest/amp/simple-video.json index a6f96be34b8..061d5c94369 100644 --- a/adapters/appnexus/appnexustest/amp/simple-video.json +++ b/adapters/appnexus/appnexustest/amp/simple-video.json @@ -82,7 +82,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -120,7 +121,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/exemplary/native-1.1.json b/adapters/appnexus/appnexustest/exemplary/native-1.1.json index 86b75505e0c..189304fdb4c 100644 --- a/adapters/appnexus/appnexustest/exemplary/native-1.1.json +++ b/adapters/appnexus/appnexustest/exemplary/native-1.1.json @@ -96,7 +96,8 @@ "brand_category_id": 350, "auction_id": 5607483846416358664, "bidder_id": 2, - "bid_ad_type": 3 + "bid_ad_type": 3, + "deal_priority": 5 } } } @@ -136,7 +137,8 @@ "brand_category_id": 350, "auction_id": 5607483846416358664, "bidder_id": 2, - "bid_ad_type": 3 + "bid_ad_type": 3, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner.json b/adapters/appnexus/appnexustest/exemplary/simple-banner.json index e5bd311648f..59931fb6ad7 100644 --- a/adapters/appnexus/appnexustest/exemplary/simple-banner.json +++ b/adapters/appnexus/appnexustest/exemplary/simple-banner.json @@ -89,7 +89,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -127,7 +128,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/exemplary/simple-video.json b/adapters/appnexus/appnexustest/exemplary/simple-video.json index 15755c7de37..ced90c39549 100644 --- a/adapters/appnexus/appnexustest/exemplary/simple-video.json +++ b/adapters/appnexus/appnexustest/exemplary/simple-video.json @@ -80,7 +80,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -118,7 +119,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json index d3686af00a9..257905c873f 100644 --- a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json +++ b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json @@ -79,7 +79,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -116,7 +117,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 1, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json index d5c981c6945..c6ad330e3a8 100644 --- a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json +++ b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json @@ -106,7 +106,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -144,7 +145,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/appnexus/appnexustest/supplemental/multi-bid.json b/adapters/appnexus/appnexustest/supplemental/multi-bid.json index 7234551ea3f..9e63bdced95 100644 --- a/adapters/appnexus/appnexustest/supplemental/multi-bid.json +++ b/adapters/appnexus/appnexustest/supplemental/multi-bid.json @@ -89,7 +89,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 4 } } }, @@ -112,7 +113,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }] @@ -150,7 +152,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 4 } } }, @@ -177,7 +180,8 @@ "auction_id": 8189378542222915032, "bid_ad_type": 0, "bidder_id": 2, - "ranking_price": 0.000000 + "ranking_price": 0.000000, + "deal_priority": 5 } } }, diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json index 632629b53a2..f5f92515e26 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json @@ -51,7 +51,7 @@ ] }, "body": { - "id": "test-req-id", + "id": "test-imp-id", "imp": [ { "id": "test-imp-id", @@ -84,7 +84,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174", "platformid": "test-platform-id" } } @@ -92,7 +92,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-id", "seatbid": [ { "bid": [ diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json index 630e26d3f90..bad228d5f18 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json @@ -52,7 +52,7 @@ ] }, "body": { - "id": "test-req-id", + "id": "test-imp-id", "imp": [ { "id": "test-imp-id", @@ -86,7 +86,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174", "platformid": "test-platform-id" } } @@ -94,7 +94,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-id", "seatbid": [ { "bid": [ diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json index 288c7c14e5d..9090d80d099 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json @@ -45,7 +45,7 @@ ] }, "body": { - "id": "test-req-id", + "id": "test-imp-id", "imp": [ { "id": "test-imp-id", @@ -78,7 +78,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174", "platformid": "test-platform-id" } } @@ -86,7 +86,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-id", "seatbid": [ { "bid": [ diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json index 15563c2ada5..22c62f8b821 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json @@ -50,7 +50,7 @@ ] }, "body": { - "id": "test-req-id", + "id": "test-imp-id", "imp": [ { "id": "test-imp-id", @@ -88,7 +88,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174", "platformid": "test-platform-id" } } @@ -96,7 +96,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-id", "seatbid": [ { "bid": [ diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json index 52b7655593a..3edd6569258 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json @@ -53,7 +53,7 @@ ] }, "body": { - "id": "test-req-id", + "id": "test-imp-id", "imp": [ { "id": "test-imp-id", @@ -86,7 +86,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174", "platformid": "test-platform-id" } } @@ -94,7 +94,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-id", "seatbid": [ { "bid": [ diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json index 0fe836af4de..16e8aede10c 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json @@ -70,7 +70,7 @@ ] }, "body": { - "id": "test-req-id", + "id": "test-imp-1", "imp": [ { "id": "test-imp-1", @@ -103,7 +103,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "dfecd103a45daeb2a01728afb8ce78f6738f6007ecfebe1ca616b196e22b43e9", "platformid": "test-platform-id" } } @@ -111,7 +111,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-1", "seatbid": [ { "bid": [ @@ -147,7 +147,7 @@ ] }, "body": { - "id": "test-req-id", + "id": "test-imp-2", "imp": [ { "id": "test-imp-2", @@ -180,7 +180,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "a5fead11a4db86d0f62f57c3d8001640227120c8ef236549f0db010c1dbab399", "platformid": "test-platform-id" } } @@ -188,7 +188,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-2", "seatbid": [ { "bid": [ diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json new file mode 100644 index 00000000000..bb192aad76f --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json @@ -0,0 +1,91 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "publisherid": "123", + "placementid": "456" + } + } + } + ], + "site": { + "domain": "prebid.org", + "page": "prebid.org" + }, + "device": { + "ip": "152.193.6.74" + }, + "user": { + "id": "db089de9-a62e-4861-a881-0ff15e052516", + "buyeruid": "v4_bidder_token" + }, + "tmax": 500 + }, + "httpcalls": [ + { + "expectedRequest": { + "uri": "https://an.facebook.com/placementbid.ortb", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "X-Fb-Pool-Routing-Token": [ + "v4_bidder_token" + ] + }, + "body": { + "id": "test-imp-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "w": -1, + "h": -1 + }, + "tagid": "123_456" + } + ], + "ext": { + "appnexus": { + "hb_source": 5 + }, + "prebid": {} + }, + "site": { + "domain": "prebid.org", + "page": "prebid.org", + "publisher": { + "id": "123" + } + }, + "device": { + "ip": "152.193.6.74" + }, + "user": { + "id": "db089de9-a62e-4861-a881-0ff15e052516", + "buyeruid": "v4_bidder_token" + }, + "tmax": 500, + "ext": { + "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174", + "platformid": "test-platform-id" + } + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json index b99834ab1df..4c561c55276 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json @@ -39,7 +39,7 @@ "expectedRequest": { "uri": "https://an.facebook.com/placementbid.ortb", "body": { - "id": "test-req-id", + "id": "test-imp-id", "imp": [ { "id": "test-imp-id", @@ -72,7 +72,7 @@ }, "tmax": 500, "ext": { - "authentication_id": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174", "platformid": "test-platform-id" } } @@ -80,7 +80,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-req-id", + "id": "test-imp-id", "seatbid": [ { "bid": [ diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go index 19cc0290f15..3bc072a8385 100644 --- a/adapters/audienceNetwork/facebook.go +++ b/adapters/audienceNetwork/facebook.go @@ -130,6 +130,11 @@ func (this *FacebookAdapter) modifyRequest(out *openrtb.BidRequest) error { return err } + // Every outgoing FAN request has a single impression, so we can safely use the unique + // impression ID as the FAN request ID. We need to make sure that we update the request + // ID *BEFORE* we generate the auth ID since its a hash based on the request ID + out.ID = imp.ID + reqExt := facebookReqExt{ PlatformID: this.platformID, AuthID: this.makeAuthID(out), @@ -333,6 +338,12 @@ func modifyImpCustom(json []byte, imp *openrtb.Imp) ([]byte, error) { } func (this *FacebookAdapter) MakeBids(request *openrtb.BidRequest, adapterRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + /* No bid response */ + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + /* Any other http status codes outside of 200 and 204 should be treated as errors */ if response.StatusCode != http.StatusOK { msg := response.Headers.Get("x-fb-an-errors") return nil, []error{&errortypes.BadInput{ @@ -447,3 +458,41 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string) appSecret: appSecret, } } + +func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { + var ( + rID string + pubID string + err error + ) + + // Note, the facebook adserver can only handle single impression requests, so we have to split multi-imp requests into + // multiple request. In order to ensure that every split request has a unique ID, the split request IDs are set to the + // corresponding imp's ID + rID, err = jsonparser.GetString(req.Body, "id") + if err != nil { + return &adapters.RequestData{}, []error{err} + } + + // The publisher ID is either in the app object or the site object, depending on the supply of the request so we need + // to check both + pubID, err = jsonparser.GetString(req.Body, "app", "publisher", "id") + if err != nil { + pubID, err = jsonparser.GetString(req.Body, "site", "publisher", "id") + if err != nil { + return &adapters.RequestData{}, []error{ + errors.New("path [app|site].publisher.id not found in the request"), + } + } + } + + uri := fmt.Sprintf("https://www.facebook.com/audiencenetwork/nurl/?partner=%s&app=%s&auction=%s&ortb_loss_code=2", fa.platformID, pubID, rID) + timeoutReq := adapters.RequestData{ + Method: "GET", + Uri: uri, + Body: nil, + Headers: http.Header{}, + } + + return &timeoutReq, nil +} diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go index 9c89ee74079..b4744dce211 100644 --- a/adapters/audienceNetwork/facebook_test.go +++ b/adapters/audienceNetwork/facebook_test.go @@ -4,7 +4,9 @@ import ( "testing" "time" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" + "github.com/stretchr/testify/assert" ) type tagInfo struct { @@ -40,3 +42,54 @@ type FacebookExt struct { func TestJsonSamples(t *testing.T) { adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret")) } + +func TestMakeTimeoutNoticeApp(t *testing.T) { + req := adapters.RequestData{ + Body: []byte(`{"id":"1234","imp":[{"id":"1234"}],"app":{"publisher":{"id":"5678"}}}`), + } + fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") + + tb, ok := fba.(adapters.TimeoutBidder) + if !ok { + t.Error("Facebook adapter is not a TimeoutAdapter") + } + + toReq, err := tb.MakeTimeoutNotification(&req) + assert.Nil(t, err, "Facebook MakeTimeoutNotification() return an error %v", err) + expectedUri := "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=5678&auction=1234&ortb_loss_code=2" + assert.Equal(t, expectedUri, toReq.Uri, "Facebook timeout notification not returning the expected URI.") +} + +func TestMakeTimeoutNoticeSite(t *testing.T) { + req := adapters.RequestData{ + Body: []byte(`{"id":"1234","imp":[{"id":"1234"}],"site":{"publisher":{"id":"5678"}}}`), + } + fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") + + tb, ok := fba.(adapters.TimeoutBidder) + if !ok { + t.Error("Facebook adapter is not a TimeoutAdapter") + } + + toReq, err := tb.MakeTimeoutNotification(&req) + assert.Nil(t, err, "Facebook MakeTimeoutNotification() return an error %v", err) + expectedUri := "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=5678&auction=1234&ortb_loss_code=2" + assert.Equal(t, expectedUri, toReq.Uri, "Facebook timeout notification not returning the expected URI.") +} + +func TestMakeTimeoutNoticeBadRequest(t *testing.T) { + req := adapters.RequestData{ + Body: []byte(`{"imp":[{{"id":"1234"}}`), + } + fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") + + tb, ok := fba.(adapters.TimeoutBidder) + if !ok { + t.Error("Facebook adapter is not a TimeoutAdapter") + } + + toReq, err := tb.MakeTimeoutNotification(&req) + assert.Empty(t, toReq.Uri, "Facebook MakeTimeoutNotification() did not return nil", err) + assert.NotNil(t, err, "Facebook MakeTimeoutNotification() did not return an error") + +} diff --git a/adapters/avocet/avocet.go b/adapters/avocet/avocet.go new file mode 100644 index 00000000000..ef6e1bb4344 --- /dev/null +++ b/adapters/avocet/avocet.go @@ -0,0 +1,124 @@ +package avocet + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +// AvocetAdapter implements a adapters.Bidder compatible with the Avocet advertising platform. +type AvocetAdapter struct { + // Endpoint is a http endpoint to use when making requests to the Avocet advertising platform. + Endpoint string +} + +func (a *AvocetAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + if len(request.Imp) == 0 { + return nil, nil + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + body, err := json.Marshal(request) + if err != nil { + return nil, []error{&errortypes.FailedToRequestBids{ + Message: err.Error(), + }} + } + reqData := &adapters.RequestData{ + Method: http.MethodPost, + Uri: a.Endpoint, + Body: body, + Headers: headers, + } + return []*adapters.RequestData{reqData}, nil +} + +type avocetBidExt struct { + Avocet avocetBidExtension `json:"avocet"` +} + +type avocetBidExtension struct { + Duration int `json:"duration"` + DealPriority int `json:"deal_priority"` +} + +func (a *AvocetAdapter) 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.StatusOK { + var errStr string + if len(response.Body) > 0 { + errStr = string(response.Body) + } else { + errStr = "no response body" + } + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("received status code: %v error: %s", response.StatusCode, errStr), + }} + } + + var br openrtb.BidResponse + err := json.Unmarshal(response.Body, &br) + if err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: err.Error(), + }} + } + var errs []error + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + for i := range br.SeatBid { + for j := range br.SeatBid[i].Bid { + var ext avocetBidExt + if len(br.SeatBid[i].Bid[j].Ext) > 0 { + err := json.Unmarshal(br.SeatBid[i].Bid[j].Ext, &ext) + if err != nil { + errs = append(errs, err) + continue + } + } + tbid := &adapters.TypedBid{ + Bid: &br.SeatBid[i].Bid[j], + DealPriority: ext.Avocet.DealPriority, + } + tbid.BidType = getBidType(br.SeatBid[i].Bid[j], ext) + if tbid.BidType == openrtb_ext.BidTypeVideo { + tbid.BidVideo = &openrtb_ext.ExtBidPrebidVideo{ + Duration: ext.Avocet.Duration, + } + } + bidResponse.Bids = append(bidResponse.Bids, tbid) + } + } + return bidResponse, nil +} + +// getBidType returns the openrtb_ext.BidType for the provided bid. +func getBidType(bid openrtb.Bid, ext avocetBidExt) openrtb_ext.BidType { + if ext.Avocet.Duration != 0 { + return openrtb_ext.BidTypeVideo + } + switch bid.API { + case openrtb.APIFrameworkVPAID10, openrtb.APIFrameworkVPAID20: + return openrtb_ext.BidTypeVideo + default: + return openrtb_ext.BidTypeBanner + } +} + +// NewAvocetAdapter returns a new AvocetAdapter using the provided endpoint. +func NewAvocetAdapter(endpoint string) *AvocetAdapter { + return &AvocetAdapter{ + Endpoint: endpoint, + } +} diff --git a/adapters/avocet/avocet/exemplary/banner.json b/adapters/avocet/avocet/exemplary/banner.json new file mode 100644 index 00000000000..b5e308ea725 --- /dev/null +++ b/adapters/avocet/avocet/exemplary/banner.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement": "5ea9601ac865f911007f1b6a" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.staging.avct.cloud/ortb/bid/5e722ee9bd6df11d063a8013", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement": "5ea9601ac865f911007f1b6a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "bidid": "dd87f80c-16a0-43c8-a673-b94b3ea4d417", + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "adm": "", + "adomain": ["avocet.io"], + "cid": "5b51e2d689654741306813a4", + "crid": "5b51e49634f2021f127ff7c9", + "h": 250, + "id": "bc708396-9202-437b-b726-08b9864cb8b8", + "impid": "test-imp-id", + "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5b51e49634f2021f127ff7c9.jpeg", + "language": "en", + "price": 15.64434783, + "w": 300 + } + ], + "seat": "TEST_SEAT_ID" + } + ] + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "adm": "", + "adomain": ["avocet.io"], + "cid": "5b51e2d689654741306813a4", + "crid": "5b51e49634f2021f127ff7c9", + "h": 250, + "id": "bc708396-9202-437b-b726-08b9864cb8b8", + "impid": "test-imp-id", + "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5b51e49634f2021f127ff7c9.jpeg", + "language": "en", + "price": 15.64434783, + "w": 300 + }, + "type": "banner" + } + ] +} diff --git a/adapters/avocet/avocet/exemplary/video.json b/adapters/avocet/avocet/exemplary/video.json new file mode 100644 index 00000000000..2398256b0dd --- /dev/null +++ b/adapters/avocet/avocet/exemplary/video.json @@ -0,0 +1,104 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1920, + "h": 1080 + }, + "ext": { + "bidder": { + "placement": "5ea9601ac865f911007f1b6a" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bid.staging.avct.cloud/ortb/bid/5e722ee9bd6df11d063a8013", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1920, + "h": 1080 + }, + "ext": { + "bidder": { + "placement": "5ea9601ac865f911007f1b6a" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "bidid": "a0eec3aa-f9f6-42fb-9aa4-f1b5656d4f42", + "id": "749d36d7-c993-455f-aefd-ffd8a7e3ccf", + "seatbid": [ + { + "bid": [ + { + "adm": "Avocet", + "adomain": ["avocet.io"], + "cid": "5b51e2d689654741306813a4", + "crid": "5ec530e32d57fe1100f17d87", + "h": 396, + "id": "3d4c2d45-5a8c-43b8-9e15-4f48ac45204f", + "impid": "dfp-ad--top-above-nav", + "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5ec530e32d57fe1100f17d87.jpeg", + "language": "en", + "price": 15.64434783, + "w": 600, + "ext": { + "avocet": { + "duration": 30 + } + } + } + ], + "seat": "TEST_SEAT_ID" + } + ] + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "adm": "Avocet", + "adomain": ["avocet.io"], + "cid": "5b51e2d689654741306813a4", + "crid": "5ec530e32d57fe1100f17d87", + "h": 396, + "id": "3d4c2d45-5a8c-43b8-9e15-4f48ac45204f", + "impid": "dfp-ad--top-above-nav", + "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5ec530e32d57fe1100f17d87.jpeg", + "language": "en", + "price": 15.64434783, + "w": 600, + "ext": { + "avocet": { + "duration": 30 + } + } + }, + "type": "video" + } + ] +} diff --git a/adapters/avocet/avocet_test.go b/adapters/avocet/avocet_test.go new file mode 100644 index 00000000000..9c8d3d07932 --- /dev/null +++ b/adapters/avocet/avocet_test.go @@ -0,0 +1,301 @@ +package avocet + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "avocet", NewAvocetAdapter("https://bid.staging.avct.cloud/ortb/bid/5e722ee9bd6df11d063a8013")) +} + +func TestAvocetAdapter_MakeRequests(t *testing.T) { + type fields struct { + Endpoint string + } + type args struct { + request *openrtb.BidRequest + reqInfo *adapters.ExtraRequestInfo + } + type reqData []*adapters.RequestData + tests := []struct { + name string + fields fields + args args + want []*adapters.RequestData + wantErrs []error + }{ + { + name: "return nil if zero imps", + fields: fields{Endpoint: "https://bid.avct.cloud"}, + args: args{ + &openrtb.BidRequest{}, + nil, + }, + want: nil, + wantErrs: nil, + }, + { + name: "makes POST request with JSON content", + fields: fields{Endpoint: "https://bid.avct.cloud"}, + args: args{ + &openrtb.BidRequest{Imp: []openrtb.Imp{{}}}, + nil, + }, + want: reqData{ + &adapters.RequestData{ + Method: http.MethodPost, + Uri: "https://bid.avct.cloud", + Body: []byte(`{"id":"","imp":[{"id":""}]}`), + Headers: map[string][]string{ + "Accept": {"application/json"}, + "Content-Type": {"application/json;charset=utf-8"}, + }, + }, + }, + wantErrs: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &AvocetAdapter{ + Endpoint: tt.fields.Endpoint, + } + got, got1 := a.MakeRequests(tt.args.request, tt.args.reqInfo) + if len(got) != len(tt.want) { + t.Errorf("AvocetAdapter.MakeRequests() got %v requests, wanted %v requests", len(got), len(tt.want)) + } + if len(got) == len(tt.want) { + for i := range tt.want { + if !reflect.DeepEqual(got[i], tt.want[i]) { + t.Errorf("AvocetAdapter.MakeRequests() got = %v, want %v", got[i], tt.want[i]) + } + } + } + if !reflect.DeepEqual(got1, tt.wantErrs) { + t.Errorf("AvocetAdapter.MakeRequests() got1 = %v, want %v", got1, tt.wantErrs) + } + }) + } +} + +func TestAvocetAdapter_MakeBids(t *testing.T) { + type fields struct { + Endpoint string + } + type args struct { + internalRequest *openrtb.BidRequest + externalRequest *adapters.RequestData + response *adapters.ResponseData + } + tests := []struct { + name string + fields fields + args args + want *adapters.BidderResponse + errs []error + }{ + { + name: "204 No Content indicates no bids", + fields: fields{Endpoint: "https://bid.avct.cloud"}, + args: args{ + nil, + nil, + &adapters.ResponseData{StatusCode: http.StatusNoContent}, + }, + want: nil, + errs: nil, + }, + { + name: "Non-200 return error", + fields: fields{Endpoint: "https://bid.avct.cloud"}, + args: args{ + nil, + nil, + &adapters.ResponseData{StatusCode: http.StatusBadRequest, Body: []byte("message")}, + }, + want: nil, + errs: []error{&errortypes.BadServerResponse{Message: "received status code: 400 error: message"}}, + }, + { + name: "200 response containing banner bids", + fields: fields{Endpoint: "https://bid.avct.cloud"}, + args: args{ + nil, + nil, + &adapters.ResponseData{StatusCode: http.StatusOK, Body: validBannerBidResponseBody}, + }, + want: &adapters.BidderResponse{ + Currency: "USD", + Bids: []*adapters.TypedBid{ + { + Bid: &validBannerBid, + BidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + errs: nil, + }, + { + name: "200 response containing video bids", + fields: fields{Endpoint: "https://bid.avct.cloud"}, + args: args{ + nil, + nil, + &adapters.ResponseData{StatusCode: http.StatusOK, Body: validVideoBidResponseBody}, + }, + want: &adapters.BidderResponse{ + Currency: "USD", + Bids: []*adapters.TypedBid{ + { + Bid: &validVideoBid, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{ + Duration: 30, + }, + }, + }, + }, + errs: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &AvocetAdapter{ + Endpoint: tt.fields.Endpoint, + } + got, got1 := a.MakeBids(tt.args.internalRequest, tt.args.externalRequest, tt.args.response) + if !reflect.DeepEqual(got, tt.want) { + gotb, _ := json.Marshal(got) + wantb, _ := json.Marshal(tt.want) + t.Errorf("AvocetAdapter.MakeBids() got = %s, want %s", string(gotb), string(wantb)) + } + if !reflect.DeepEqual(got1, tt.errs) { + t.Errorf("AvocetAdapter.MakeBids() got1 = %v, want %v", got1, tt.errs) + } + }) + } +} + +func Test_getBidType(t *testing.T) { + type args struct { + bid openrtb.Bid + ext avocetBidExt + } + tests := []struct { + name string + args args + want openrtb_ext.BidType + }{ + { + name: "VPAID 1.0", + args: args{openrtb.Bid{API: openrtb.APIFrameworkVPAID10}, avocetBidExt{}}, + want: openrtb_ext.BidTypeVideo, + }, + { + name: "VPAID 2.0", + args: args{openrtb.Bid{API: openrtb.APIFrameworkVPAID20}, avocetBidExt{}}, + want: openrtb_ext.BidTypeVideo, + }, + { + name: "other", + args: args{openrtb.Bid{}, avocetBidExt{}}, + want: openrtb_ext.BidTypeBanner, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getBidType(tt.args.bid, tt.args.ext); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getBidType() = %v, want %v", got, tt.want) + } + }) + } +} + +var validBannerBid = openrtb.Bid{ + AdM: "", + ADomain: []string{"avocet.io"}, + CID: "5b51e2d689654741306813a4", + CrID: "5b51e49634f2021f127ff7c9", + H: 250, + ID: "bc708396-9202-437b-b726-08b9864cb8b8", + ImpID: "test-imp-id", + IURL: "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5b51e49634f2021f127ff7c9.jpeg", + Language: "en", + Price: 15.64434783, + W: 300, +} + +var validBannerBidResponseBody = []byte(`{ + "bidid": "dd87f80c-16a0-43c8-a673-b94b3ea4d417", + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "adm": "", + "adomain": ["avocet.io"], + "cid": "5b51e2d689654741306813a4", + "crid": "5b51e49634f2021f127ff7c9", + "h": 250, + "id": "bc708396-9202-437b-b726-08b9864cb8b8", + "impid": "test-imp-id", + "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5b51e49634f2021f127ff7c9.jpeg", + "language": "en", + "price": 15.64434783, + "w": 300 + } + ], + "seat": "TEST_SEAT_ID" + } + ] +}`) + +var validVideoBid = openrtb.Bid{ + AdM: "Avocet", + ADomain: []string{"avocet.io"}, + CID: "5b51e2d689654741306813a4", + CrID: "5ec530e32d57fe1100f17d87", + H: 396, + ID: "3d4c2d45-5a8c-43b8-9e15-4f48ac45204f", + ImpID: "dfp-ad--top-above-nav", + IURL: "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5ec530e32d57fe1100f17d87.jpeg", + Language: "en", + Price: 15.64434783, + W: 600, + Ext: []byte(`{"avocet":{"duration":30}}`), +} + +var validVideoBidResponseBody = []byte(`{ + "bidid": "dd87f80c-16a0-43c8-a673-b94b3ea4d417", + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "adm": "Avocet", + "adomain": ["avocet.io"], + "cid": "5b51e2d689654741306813a4", + "crid": "5ec530e32d57fe1100f17d87", + "h": 396, + "id": "3d4c2d45-5a8c-43b8-9e15-4f48ac45204f", + "impid": "dfp-ad--top-above-nav", + "iurl": "https://cdn.staging.avocet.io/snapshots/5b51dd1634f2021f127ff7c0/5ec530e32d57fe1100f17d87.jpeg", + "language": "en", + "price": 15.64434783, + "w": 600, + "ext": {"avocet":{"duration":30}} + } + ], + "seat": "TEST_SEAT_ID" + } + ] +}`) diff --git a/adapters/avocet/usersync.go b/adapters/avocet/usersync.go new file mode 100644 index 00000000000..ec4f25dd952 --- /dev/null +++ b/adapters/avocet/usersync.go @@ -0,0 +1,12 @@ +package avocet + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +func NewAvocetSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("avocet", 63, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/avocet/usersync_test.go b/adapters/avocet/usersync_test.go new file mode 100644 index 00000000000..be4890df91a --- /dev/null +++ b/adapters/avocet/usersync_test.go @@ -0,0 +1,35 @@ +package avocet + +import ( + "testing" + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestAvocetSyncer(t *testing.T) { + syncURL := "https://ads.avct.cloud/getuid?&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url=%2Fsetuid%3Fbidder%3Davocet%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7B%7BUUID%7D%7D" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewAvocetSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "ConsentString", + }, + CCPA: ccpa.Policy{ + Value: "PrivacyString", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://ads.avct.cloud/getuid?&gdpr=1&gdpr_consent=ConsentString&us_privacy=PrivacyString&url=%2Fsetuid%3Fbidder%3Davocet%26gdpr%3D1%26gdpr_consent%3DConsentString%26uid%3D%7B%7BUUID%7D%7D", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 63, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/adapters/beachfront/beachfront.go b/adapters/beachfront/beachfront.go index 22f185b9195..c7d224c31a7 100644 --- a/adapters/beachfront/beachfront.go +++ b/adapters/beachfront/beachfront.go @@ -4,26 +4,27 @@ import ( "encoding/json" "errors" "fmt" - "github.com/PubMatic-OpenWrap/openrtb" - "github.com/PubMatic-OpenWrap/prebid-server/adapters" - "github.com/PubMatic-OpenWrap/prebid-server/errortypes" - "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "net/http" "reflect" "strconv" "strings" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/golang/glog" ) const Seat = "beachfront" const BidCapacity = 5 -const bannerEndpoint = "https://display.bfmio.com/prebid_display" -const videoEndpoint = "https://reachms.bfmio.com/bid.json?exchange_id" +const defaultVideoEndpoint = "https://reachms.bfmio.com/bid.json?exchange_id" const nurlVideoEndpointSuffix = "&prebidserver" const beachfrontAdapterName = "BF_PREBID_S2S" -const beachfrontAdapterVersion = "0.8.0" +const beachfrontAdapterVersion = "0.9.0" const minBidFloor = 0.01 @@ -31,6 +32,12 @@ const DefaultVideoWidth = 300 const DefaultVideoHeight = 250 type BeachfrontAdapter struct { + bannerEndpoint string + extraInfo ExtraInfo +} + +type ExtraInfo struct { + VideoEndpoint string `json:"video_endpoint,omitempty"` } type beachfrontRequests struct { @@ -138,7 +145,7 @@ func (a *BeachfrontAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *a if err == nil { reqs[0] = &adapters.RequestData{ Method: "POST", - Uri: bannerEndpoint, + Uri: a.bannerEndpoint, Body: bytes, Headers: headers, } @@ -159,7 +166,7 @@ func (a *BeachfrontAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *a if err == nil { reqs[j+nurlBump] = &adapters.RequestData{ Method: "POST", - Uri: videoEndpoint + "=" + beachfrontRequests.ADMVideo[j].AppId, + Uri: a.extraInfo.VideoEndpoint + "=" + beachfrontRequests.ADMVideo[j].AppId, Body: bytes, Headers: headers, } @@ -178,7 +185,7 @@ func (a *BeachfrontAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *a bytes = append([]byte(`{"isPrebid":true,`), bytes[1:]...) reqs[j+admBump] = &adapters.RequestData{ Method: "POST", - Uri: videoEndpoint + "=" + beachfrontRequests.NurlVideo[j].AppId + nurlVideoEndpointSuffix, + Uri: a.extraInfo.VideoEndpoint + "=" + beachfrontRequests.NurlVideo[j].AppId + nurlVideoEndpointSuffix, Body: bytes, Headers: headers, } @@ -518,13 +525,13 @@ func (a *BeachfrontAdapter) MakeBids(internalRequest *openrtb.BidRequest, extern bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &bids[i], - BidType: getBidType(externalRequest), + BidType: a.getBidType(externalRequest), BidVideo: &impVideo, }) } else { bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &bids[i], - BidType: getBidType(externalRequest), + BidType: a.getBidType(externalRequest), }) } } @@ -532,6 +539,15 @@ func (a *BeachfrontAdapter) MakeBids(internalRequest *openrtb.BidRequest, extern return bidResponse, errs } +func (a *BeachfrontAdapter) getBidType(externalRequest *adapters.RequestData) openrtb_ext.BidType { + t := strings.Split(externalRequest.Uri, "=")[0] + if t == a.extraInfo.VideoEndpoint { + return openrtb_ext.BidTypeVideo + } + + return openrtb_ext.BidTypeBanner +} + func postprocess(response *adapters.ResponseData, xtrnal openrtb.BidRequest, uri string, id string) ([]openrtb.Bid, []error) { var beachfrontResp []beachfrontResponseSlot var errs = make([]error, 0) @@ -629,13 +645,13 @@ func getBeachfrontExtension(imp openrtb.Imp) (openrtb_ext.ExtImpBeachfront, erro } func getDomain(page string) string { - protoUrl := strings.Split(page, "//") + protoURL := strings.Split(page, "//") var domainPage string - if len(protoUrl) > 1 { - domainPage = protoUrl[1] + if len(protoURL) > 1 { + domainPage = protoURL[1] } else { - domainPage = protoUrl[0] + domainPage = protoURL[0] } return strings.Split(domainPage, "/")[0] @@ -643,9 +659,9 @@ func getDomain(page string) string { } func isSecure(page string) int8 { - protoUrl := strings.Split(page, "://") + protoURL := strings.Split(page, "://") - if len(protoUrl) > 1 && protoUrl[0] == "https" { + if len(protoURL) > 1 && protoURL[0] == "https" { return 1 } @@ -663,19 +679,25 @@ func getIP(ip string) string { return ip } -func getBidType(externalRequest *adapters.RequestData) openrtb_ext.BidType { - t := strings.Split(externalRequest.Uri, "=")[0] - if t == videoEndpoint { - return openrtb_ext.BidTypeVideo - } - - return openrtb_ext.BidTypeBanner -} - func removeVideoElement(slice []beachfrontVideoRequest, s int) []beachfrontVideoRequest { return append(slice[:s], slice[s+1:]...) } -func NewBeachfrontBidder() *BeachfrontAdapter { - return &BeachfrontAdapter{} +func NewBeachfrontBidder(bannerEndpoint string, extraAdapterInfo string) adapters.Bidder { + var extraInfo ExtraInfo + + if len(extraAdapterInfo) == 0 { + extraAdapterInfo = "{\"video_endpoint\":\"" + defaultVideoEndpoint + "\"}" + } + + if err := json.Unmarshal([]byte(extraAdapterInfo), &extraInfo); err != nil { + glog.Fatal("Invalid Beachfront extra adapter info: " + err.Error()) + return nil + } + + if extraInfo.VideoEndpoint == "" { + extraInfo.VideoEndpoint = defaultVideoEndpoint + } + + return &BeachfrontAdapter{bannerEndpoint: bannerEndpoint, extraInfo: extraInfo} } diff --git a/adapters/beachfront/beachfront_test.go b/adapters/beachfront/beachfront_test.go index 683b0ac90c9..905fbde6c8b 100644 --- a/adapters/beachfront/beachfront_test.go +++ b/adapters/beachfront/beachfront_test.go @@ -7,5 +7,5 @@ import ( ) func TestJsonSamples(t *testing.T) { - adapterstest.RunJSONBidderTest(t, "beachfronttest", new(BeachfrontAdapter)) + adapterstest.RunJSONBidderTest(t, "beachfronttest", NewBeachfrontBidder("https://display.bfmio.com/prebid_display", "{\"video_endpoint\":\"https://reachms.bfmio.com/bid.json?exchange_id\"}")) } diff --git a/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json b/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json index ffcea194cdd..51ce4e9295e 100644 --- a/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json +++ b/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json @@ -56,7 +56,7 @@ "dnt": 0, "ua": "", "adapterName": "BF_PREBID_S2S", - "adapterVersion": "0.8.0", + "adapterVersion": "0.9.0", "user": { } } diff --git a/adapters/beachfront/beachfronttest/exemplary/simple-mix.json b/adapters/beachfront/beachfronttest/exemplary/simple-mix.json index 6d8e483ee6d..eb5d9b07abc 100644 --- a/adapters/beachfront/beachfronttest/exemplary/simple-mix.json +++ b/adapters/beachfront/beachfronttest/exemplary/simple-mix.json @@ -85,7 +85,7 @@ "buyeruid": "some-buyer" }, "adapterName": "BF_PREBID_S2S", - "adapterVersion": "0.8.0", + "adapterVersion": "0.9.0", "ip": "192.168.255.255", "requestId": "61b87329-8790-47b7-90dd-c53ae7ce1723" } diff --git a/adapters/beachfront/beachfronttest/supplemental/minimal-banner-empty_array-200.json b/adapters/beachfront/beachfronttest/supplemental/minimal-banner-empty_array-200.json index f189b2c8c79..7bdbc73cd5e 100644 --- a/adapters/beachfront/beachfronttest/supplemental/minimal-banner-empty_array-200.json +++ b/adapters/beachfront/beachfronttest/supplemental/minimal-banner-empty_array-200.json @@ -56,7 +56,7 @@ "dnt": 0, "ua": "", "adapterName": "BF_PREBID_S2S", - "adapterVersion": "0.8.0", + "adapterVersion": "0.9.0", "user": { } } diff --git a/adapters/beachfront/beachfronttest/supplemental/minimal-site-banner.json b/adapters/beachfront/beachfronttest/supplemental/minimal-site-banner.json index b610c96f58a..27b24357247 100644 --- a/adapters/beachfront/beachfronttest/supplemental/minimal-site-banner.json +++ b/adapters/beachfront/beachfronttest/supplemental/minimal-site-banner.json @@ -56,7 +56,7 @@ "dnt": 0, "ua": "", "adapterName": "BF_PREBID_S2S", - "adapterVersion": "0.8.0", + "adapterVersion": "0.9.0", "user": { } } diff --git a/adapters/beachfront/beachfronttest/supplemental/mobile-banner.json b/adapters/beachfront/beachfronttest/supplemental/mobile-banner.json index d47393b7caf..ea38d7adae7 100644 --- a/adapters/beachfront/beachfronttest/supplemental/mobile-banner.json +++ b/adapters/beachfront/beachfronttest/supplemental/mobile-banner.json @@ -87,7 +87,7 @@ }, "adapterName":"BF_PREBID_S2S", - "adapterVersion":"0.8.0", + "adapterVersion":"0.9.0", "ip":"192.168.255.255", "requestId":"763e3312-19d5-4b07-a61d-890147e863a1" } diff --git a/adapters/beachfront/beachfronttest/supplemental/multi-banner.json b/adapters/beachfront/beachfronttest/supplemental/multi-banner.json index c4120787852..46699511a9c 100644 --- a/adapters/beachfront/beachfronttest/supplemental/multi-banner.json +++ b/adapters/beachfront/beachfronttest/supplemental/multi-banner.json @@ -96,7 +96,7 @@ "dnt": 1, "ua": "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16", "adapterName": "BF_PREBID_S2S", - "adapterVersion": "0.8.0", + "adapterVersion": "0.9.0", "user": { "buyeruid": "some-buyer", "id": "some-user" diff --git a/adapters/beachfront/usersync.go b/adapters/beachfront/usersync.go index cfb099a80c6..f355697f4e0 100644 --- a/adapters/beachfront/usersync.go +++ b/adapters/beachfront/usersync.go @@ -7,6 +7,12 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/usersync" ) +var VENDOR_ID uint16 = 335 + func NewBeachfrontSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("beachfront", 0, temp, adapters.SyncTypeIframe) + return adapters.NewSyncer( + "beachfront", + VENDOR_ID, + temp, + adapters.SyncTypeIframe) } diff --git a/adapters/beachfront/usersync_test.go b/adapters/beachfront/usersync_test.go index 38efd0a54d7..e0aed3f5479 100644 --- a/adapters/beachfront/usersync_test.go +++ b/adapters/beachfront/usersync_test.go @@ -30,6 +30,6 @@ func TestBeachfrontSyncer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "https://sync.bfmio.com/sync_s2s?gdpr=A&us_privacy=C&url=https%3A%2F%2Flocalhost%3A8888%2Fsetuid%3Fbidder%3Dbeachfront%26gdpr%3DA%26gdpr_consent%3DB%26uid%3D%5Bio_cid%5D", syncInfo.URL) assert.Equal(t, "iframe", syncInfo.Type) - assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.EqualValues(t, uint16(335), syncer.GDPRVendorID()) assert.Equal(t, false, syncInfo.SupportCORS) } diff --git a/adapters/beintoo/beintoo.go b/adapters/beintoo/beintoo.go new file mode 100644 index 00000000000..77b6b260700 --- /dev/null +++ b/adapters/beintoo/beintoo.go @@ -0,0 +1,222 @@ +package beintoo + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +type BeintooAdapter struct { + endpoint string +} + +func (a *BeintooAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errors []error + + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("No Imps in Bid Request"), + }} + } + + if errors := preprocess(request); errors != nil && len(errors) > 0 { + return nil, append(errors, &errortypes.BadInput{ + Message: fmt.Sprintf("Error in preprocess of Imp, err: %s", errors), + }) + } + + data, err := json.Marshal(request) + if err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Error in packaging request to JSON"), + }} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + if request.Device != nil { + addHeaderIfNonEmpty(headers, "User-Agent", request.Device.UA) + addHeaderIfNonEmpty(headers, "X-Forwarded-For", request.Device.IP) + addHeaderIfNonEmpty(headers, "Accept-Language", request.Device.Language) + if request.Device.DNT != nil { + addHeaderIfNonEmpty(headers, "DNT", strconv.Itoa(int(*request.Device.DNT))) + } + } + if request.Site != nil { + addHeaderIfNonEmpty(headers, "Referer", request.Site.Page) + } + + return []*adapters.RequestData{{ + Method: "POST", + Uri: a.endpoint, + Body: data, + Headers: headers, + }}, errors +} + +func unpackImpExt(imp *openrtb.Imp) (*openrtb_ext.ExtImpBeintoo, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: err.Error(), + } + } + + var beintooExt openrtb_ext.ExtImpBeintoo + if err := json.Unmarshal(bidderExt.Bidder, &beintooExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, invalid ImpExt", imp.ID), + } + } + + tagIDValidation, err := strconv.ParseInt(beintooExt.TagID, 10, 64) + if err != nil || tagIDValidation == 0 { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, invalid tagid must be a String of numbers", imp.ID), + } + } + + return &beintooExt, nil +} + +func buildImpBanner(imp *openrtb.Imp) error { + imp.Ext = nil + + if imp.Banner == nil { + return &errortypes.BadInput{ + Message: fmt.Sprintf("Request needs to include a Banner object"), + } + } + + bannerCopy := *imp.Banner + banner := &bannerCopy + + if banner.W == nil && banner.H == nil { + if len(banner.Format) == 0 { + return &errortypes.BadInput{ + Message: fmt.Sprintf("Need at least one size to build request"), + } + } + format := banner.Format[0] + banner.Format = banner.Format[1:] + banner.W = &format.W + banner.H = &format.H + imp.Banner = banner + } + + return nil +} + +// Add Beintoo required properties to Imp object +func addImpProps(imp *openrtb.Imp, secure *int8, BeintooExt *openrtb_ext.ExtImpBeintoo) { + imp.TagID = BeintooExt.TagID + imp.Secure = secure + + if BeintooExt.BidFloor != "" { + bidFloor, err := strconv.ParseFloat(BeintooExt.BidFloor, 64) + if err != nil { + bidFloor = 0 + } + + if bidFloor > 0 { + imp.BidFloor = bidFloor + } + } + + return +} + +// Adding header fields to request header +func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) { + if len(headerValue) > 0 { + headers.Add(headerName, headerValue) + } +} + +// Handle request errors and formatting to be sent to Beintoo +func preprocess(request *openrtb.BidRequest) []error { + errors := make([]error, 0, len(request.Imp)) + resImps := make([]openrtb.Imp, 0, len(request.Imp)) + secure := int8(0) + + if request.Site != nil && request.Site.Page != "" { + pageURL, err := url.Parse(request.Site.Page) + if err == nil && pageURL.Scheme == "https" { + secure = int8(1) + } + } + + for _, imp := range request.Imp { + beintooExt, err := unpackImpExt(&imp) + if err != nil { + errors = append(errors, err) + return errors + } + + addImpProps(&imp, &secure, beintooExt) + + if err := buildImpBanner(&imp); err != nil { + errors = append(errors, err) + return errors + } + resImps = append(resImps, imp) + } + + request.Imp = resImps + + return errors +} + +// MakeBids make the bids for the bid response. +func (a *BeintooAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + if response.StatusCode == http.StatusNoContent { + // no bid response + return nil, nil + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Invalid Status Returned: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + var bidResp openrtb.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unable to unpackage bid response. Error: %s", err.Error()), + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + sb.Bid[i].ImpID = sb.Bid[i].ID + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: "banner", + }) + } + } + + return bidResponse, nil + +} + +func NewBeintooBidder(endpoint string) *BeintooAdapter { + return &BeintooAdapter{ + endpoint: endpoint, + } +} diff --git a/adapters/beintoo/beintoo_test.go b/adapters/beintoo/beintoo_test.go new file mode 100644 index 00000000000..d5a61c26209 --- /dev/null +++ b/adapters/beintoo/beintoo_test.go @@ -0,0 +1,12 @@ +package beintoo + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + beintooAdapter := NewBeintooBidder("https://ib.beintoo.com") + adapterstest.RunJSONBidderTest(t, "beintootest", beintooAdapter) +} diff --git a/adapters/beintoo/beintootest/exemplary/minimal-banner.json b/adapters/beintoo/beintootest/exemplary/minimal-banner.json new file mode 100644 index 00000000000..60e481c507c --- /dev/null +++ b/adapters/beintoo/beintootest/exemplary/minimal-banner.json @@ -0,0 +1,117 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [{ + "id": "some_test_ad_id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + } + }], + "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", + "dnt": 1 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site?with=some¶meters=here" + } + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ib.beintoo.com", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "http://www.publisher.com/awesome/site?with=some¶meters=here" + ], + "Dnt": [ + "1" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36" + ] + }, + "body": { + "id": "some_test_auction", + "imp": [{ + "id": "some_test_ad_id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "tagid": "25251", + "secure": 0 + }], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site?with=some¶meters=here" + }, + "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", + "dnt": 1 + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some_test_auction", + "seatbid": [{ + "seat": "12356", + "bid": [{ + "adm": "
" +const adSourceURL = "https://ad.yieldlab.net/d/%v/%v/%v?%v" +const creativeID = "%v%v%v" diff --git a/adapters/yieldlab/params_test.go b/adapters/yieldlab/params_test.go new file mode 100644 index 00000000000..f66121e35e8 --- /dev/null +++ b/adapters/yieldlab/params_test.go @@ -0,0 +1,63 @@ +package yieldlab + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/yieldlab.json +// +// These also validate the format of the external API: request.imp[i].ext.yieldlab + +// TestValidParams makes sure that the yieldlab 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.BidderYieldlab, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected yieldlab params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the yieldlab 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.BidderYieldlab, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"adslotId": "123","supplyId":"23456","adSize":"100x100"}`, + `{"adslotId": "123","supplyId":"23456","adSize":"100x100","extId":"asdf"}`, + `{"adslotId": "123","supplyId":"23456","adSize":"100x100","extId":"asdf","targeting":{"a":"b"}}`, + `{"adslotId": "123","supplyId":"23456","adSize":"100x100","targeting":{"a":"b"}}`, + `{"adslotId": "123","supplyId":"23456","adSize":"100x100","targeting":{"a":"b"}}`, +} + +var invalidParams = []string{ + `{"supplyId":"23456","adSize":"100x100"}`, + `{"adslotId": "123","adSize":"100x100","extId":"asdf"}`, + `{"adslotId": "123","supplyId":"23456","extId":"asdf","targeting":{"a":"b"}}`, + `{"adslotId": "123","supplyId":"23456"}`, + `{"adSize":"100x100","supplyId":"23456"}`, + `{"adslotId": "123","adSize":"100x100"}`, + `{"supplyId":"23456"}`, + `{"adslotId": "123"}`, + `{}`, + `[]`, + `{"a":"b"}`, + `null`, +} diff --git a/adapters/yieldlab/types.go b/adapters/yieldlab/types.go new file mode 100644 index 00000000000..90612700713 --- /dev/null +++ b/adapters/yieldlab/types.go @@ -0,0 +1,29 @@ +package yieldlab + +import ( + "strconv" + "time" +) + +type bidResponse struct { + ID uint64 `json:"id"` + Price uint `json:"price"` + Advertiser string `json:"advertiser"` + Adsize string `json:"adsize"` + Pid uint64 `json:"pid"` + Did uint64 `json:"did"` + Pvid string `json:"pvid"` +} + +type cacheBuster func() string + +type weekGenerator func() string + +var defaultCacheBuster cacheBuster = func() string { + return strconv.FormatInt(time.Now().Unix(), 10) +} + +var defaultWeekGenerator weekGenerator = func() string { + _, week := time.Now().ISOWeek() + return strconv.Itoa(week) +} diff --git a/adapters/yieldlab/usersync.go b/adapters/yieldlab/usersync.go new file mode 100644 index 00000000000..a0462e19e6e --- /dev/null +++ b/adapters/yieldlab/usersync.go @@ -0,0 +1,12 @@ +package yieldlab + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +func NewYieldlabSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("yieldlab", 70, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/yieldlab/usersync_test.go b/adapters/yieldlab/usersync_test.go new file mode 100644 index 00000000000..cdca7f9f417 --- /dev/null +++ b/adapters/yieldlab/usersync_test.go @@ -0,0 +1,26 @@ +package yieldlab + +import ( + "testing" + "text/template" + + "github.com/stretchr/testify/assert" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" +) + +func TestYieldlabSyncer(t *testing.T) { + temp := template.Must(template.New("sync-template").Parse("https://ad.yieldlab.net/mr?t=2&pid=9140838&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirectUri=http%3A%2F%2Flocalhost%2F%2Fsetuid%3Fbidder%3Dyieldlab%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25YL_UID%25%25")) + syncer := NewYieldlabSyncer(temp) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + }, + }) + assert.NoError(t, err) + assert.Equal(t, "https://ad.yieldlab.net/mr?t=2&pid=9140838&gdpr=0&gdpr_consent=&redirectUri=http%3A%2F%2Flocalhost%2F%2Fsetuid%3Fbidder%3Dyieldlab%26gdpr%3D0%26gdpr_consent%3D%26uid%3D%25%25YL_UID%25%25", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 70, syncer.GDPRVendorID()) + assert.False(t, syncInfo.SupportCORS) +} diff --git a/adapters/yieldlab/yieldlab.go b/adapters/yieldlab/yieldlab.go new file mode 100644 index 00000000000..f9b1f136915 --- /dev/null +++ b/adapters/yieldlab/yieldlab.go @@ -0,0 +1,314 @@ +package yieldlab + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strconv" + "strings" + + "github.com/PubMatic-OpenWrap/openrtb" + "golang.org/x/text/currency" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +// YieldlabAdapter connects the Yieldlab API to prebid server +type YieldlabAdapter struct { + endpoint string + cacheBuster cacheBuster + getWeek weekGenerator +} + +// NewYieldlabBidder returns a new YieldlabBidder instance +func NewYieldlabBidder(endpoint string) *YieldlabAdapter { + return &YieldlabAdapter{ + endpoint: endpoint, + cacheBuster: defaultCacheBuster, + getWeek: defaultWeekGenerator, + } +} + +// Builds endpoint url based on adapter-specific pub settings from imp.ext +func (a *YieldlabAdapter) makeEndpointURL(req *openrtb.BidRequest, params *openrtb_ext.ExtImpYieldlab) (string, error) { + uri, err := url.Parse(a.endpoint) + if err != nil { + return "", fmt.Errorf("failed to parse yieldlab endpoint: %v", err) + } + + uri.Path = path.Join(uri.Path, params.AdslotID) + q := uri.Query() + q.Set("content", "json") + q.Set("pvid", "true") + q.Set("ts", a.cacheBuster()) + q.Set("t", a.makeTargetingValues(params)) + + if req.User != nil && req.User.BuyerUID != "" { + q.Set("ids", "ylid:"+req.User.BuyerUID) + } + + if req.Device != nil { + q.Set("yl_rtb_ifa", req.Device.IFA) + q.Set("yl_rtb_devicetype", fmt.Sprintf("%v", req.Device.DeviceType)) + + if req.Device.ConnectionType != nil { + q.Set("yl_rtb_connectiontype", fmt.Sprintf("%v", req.Device.ConnectionType.Val())) + } + + if req.Device.Geo != nil { + q.Set("lat", fmt.Sprintf("%v", req.Device.Geo.Lat)) + q.Set("lon", fmt.Sprintf("%v", req.Device.Geo.Lon)) + } + } + + if req.App != nil { + q.Set("pubappname", req.App.Name) + q.Set("pubbundlename", req.App.Bundle) + } + + gdpr, consent, err := a.getGDPR(req) + if err != nil { + return "", err + } + if gdpr != "" && consent != "" { + q.Set("gdpr", gdpr) + q.Set("consent", consent) + } + + uri.RawQuery = q.Encode() + + return uri.String(), nil +} + +func (a *YieldlabAdapter) getGDPR(request *openrtb.BidRequest) (string, string, error) { + gdpr := "" + var extRegs openrtb_ext.ExtRegs + if request.Regs != nil { + if err := json.Unmarshal(request.Regs.Ext, &extRegs); err != nil { + return "", "", fmt.Errorf("failed to parse ExtRegs in Yieldlab GDPR check: %v", err) + } + if extRegs.GDPR != nil && (*extRegs.GDPR == 0 || *extRegs.GDPR == 1) { + gdpr = strconv.Itoa(int(*extRegs.GDPR)) + } + } + + consent := "" + if request.User != nil && request.User.Ext != nil { + var extUser openrtb_ext.ExtUser + if err := json.Unmarshal(request.User.Ext, &extUser); err != nil { + return "", "", fmt.Errorf("failed to parse ExtUser in Yieldlab GDPR check: %v", err) + } + consent = extUser.Consent + } + + return gdpr, consent, nil +} + +func (a *YieldlabAdapter) makeTargetingValues(params *openrtb_ext.ExtImpYieldlab) string { + values := url.Values{} + for k, v := range params.Targeting { + values.Set(k, v) + } + return values.Encode() +} + +func (a *YieldlabAdapter) MakeRequests(request *openrtb.BidRequest, _ *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + if len(request.Imp) == 0 { + return nil, []error{fmt.Errorf("invalid request %+v, no Impressions given", request)} + } + + bidURL, err := a.makeEndpointURL(request, a.mergeParams(a.parseRequest(request))) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Accept", "application/json") + if request.Site != nil { + headers.Add("Referer", request.Site.Page) + } + if request.Device != nil { + headers.Add("User-Agent", request.Device.UA) + headers.Add("X-Forwarded-For", request.Device.IP) + } + if request.User != nil { + headers.Add("Cookie", "id="+request.User.BuyerUID) + } + + return []*adapters.RequestData{{ + Method: "GET", + Uri: bidURL, + Headers: headers, + }}, nil +} + +// parseRequest extracts the Yieldlab request information from the request +func (a *YieldlabAdapter) parseRequest(request *openrtb.BidRequest) []*openrtb_ext.ExtImpYieldlab { + params := make([]*openrtb_ext.ExtImpYieldlab, 0) + + for i := 0; i < len(request.Imp); i++ { + bidderExt := new(adapters.ExtImpBidder) + if err := json.Unmarshal(request.Imp[i].Ext, bidderExt); err != nil { + continue + } + + yieldlabExt := new(openrtb_ext.ExtImpYieldlab) + if err := json.Unmarshal(bidderExt.Bidder, yieldlabExt); err != nil { + continue + } + + params = append(params, yieldlabExt) + } + + return params +} + +func (a *YieldlabAdapter) mergeParams(params []*openrtb_ext.ExtImpYieldlab) *openrtb_ext.ExtImpYieldlab { + var adSlotIds []string + targeting := make(map[string]string) + + for _, p := range params { + adSlotIds = append(adSlotIds, p.AdslotID) + for k, v := range p.Targeting { + targeting[k] = v + } + } + + return &openrtb_ext.ExtImpYieldlab{ + AdslotID: strings.Join(adSlotIds, adSlotIdSeparator), + Targeting: targeting, + } +} + +// MakeBids make the bids for the bid response. +func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode != 200 { + return nil, []error{ + &errortypes.BadServerResponse{ + Message: fmt.Sprintf("failed to resolve bids from yieldlab response: Unexpected response code %v", response.StatusCode), + }, + } + } + + bids := make([]*bidResponse, 0) + if err := json.Unmarshal(response.Body, &bids); err != nil { + return nil, []error{ + &errortypes.BadServerResponse{ + Message: fmt.Sprintf("failed to parse bids response from yieldlab: %v", err), + }, + } + } + + params := a.parseRequest(internalRequest) + + bidderResponse := &adapters.BidderResponse{ + Currency: currency.EUR.String(), + Bids: []*adapters.TypedBid{}, + } + + for i, bid := range bids { + width, height, err := splitSize(bid.Adsize) + if err != nil { + return nil, []error{err} + } + + req := a.findBidReq(bid.ID, params) + if req == nil { + return nil, []error{ + fmt.Errorf("failed to find yieldlab request for adslotID %v. This is most likely a programming issue", bid.ID), + } + } + + var bidType openrtb_ext.BidType + responseBid := &openrtb.Bid{ + ID: strconv.FormatUint(bid.ID, 10), + Price: float64(bid.Price) / 100, + ImpID: internalRequest.Imp[i].ID, + CrID: a.makeCreativeID(req, bid), + DealID: strconv.FormatUint(bid.Pid, 10), + W: width, + H: height, + } + + if internalRequest.Imp[i].Video != nil { + bidType = openrtb_ext.BidTypeVideo + responseBid.NURL = a.makeAdSourceURL(internalRequest, req, bid) + + } else if internalRequest.Imp[i].Banner != nil { + bidType = openrtb_ext.BidTypeBanner + responseBid.AdM = a.makeBannerAdSource(internalRequest, req, bid) + } else { + // Yieldlab adapter currently doesn't support Audio and Native ads + continue + } + + bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ + BidType: bidType, + Bid: responseBid, + }) + } + + return bidderResponse, nil +} + +func (a *YieldlabAdapter) findBidReq(adslotID uint64, params []*openrtb_ext.ExtImpYieldlab) *openrtb_ext.ExtImpYieldlab { + slotIdStr := strconv.FormatUint(adslotID, 10) + for _, p := range params { + if p.AdslotID == slotIdStr { + return p + } + } + + return nil +} + +func (a *YieldlabAdapter) makeBannerAdSource(req *openrtb.BidRequest, ext *openrtb_ext.ExtImpYieldlab, res *bidResponse) string { + return fmt.Sprintf(adSourceBanner, a.makeAdSourceURL(req, ext, res)) +} + +func (a *YieldlabAdapter) makeAdSourceURL(req *openrtb.BidRequest, ext *openrtb_ext.ExtImpYieldlab, res *bidResponse) string { + val := url.Values{} + val.Set("ts", a.cacheBuster()) + val.Set("id", ext.ExtId) + val.Set("pvid", res.Pvid) + + if req.User != nil { + val.Set("ids", "ylid:"+req.User.BuyerUID) + } + + gdpr, consent, err := a.getGDPR(req) + if err == nil && gdpr != "" && consent != "" { + val.Set("gdpr", gdpr) + val.Set("consent", consent) + } + + return fmt.Sprintf(adSourceURL, ext.AdslotID, ext.SupplyID, res.Adsize, val.Encode()) +} + +func (a *YieldlabAdapter) makeCreativeID(req *openrtb_ext.ExtImpYieldlab, bid *bidResponse) string { + return fmt.Sprintf(creativeID, req.AdslotID, bid.Pid, a.getWeek()) +} + +func splitSize(size string) (uint64, uint64, error) { + sizeParts := strings.Split(size, adsizeSeparator) + if len(sizeParts) != 2 { + return 0, 0, nil + } + + width, err := strconv.ParseUint(sizeParts[0], 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse yieldlab adsize: %v", err) + } + + height, err := strconv.ParseUint(sizeParts[1], 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse yieldlab adsize: %v", err) + } + + return width, height, nil + +} diff --git a/adapters/yieldlab/yieldlab_test.go b/adapters/yieldlab/yieldlab_test.go new file mode 100644 index 00000000000..274d00e7cd2 --- /dev/null +++ b/adapters/yieldlab/yieldlab_test.go @@ -0,0 +1,128 @@ +package yieldlab + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +const testURL = "https://ad.yieldlab.net/testing/" + +var testCacheBuster cacheBuster = func() string { + return "testing" +} + +var testWeekGenerator weekGenerator = func() string { + return "33" +} + +func newTestYieldlabBidder(endpoint string) *YieldlabAdapter { + return &YieldlabAdapter{ + endpoint: endpoint, + cacheBuster: testCacheBuster, + getWeek: testWeekGenerator, + } +} + +func TestNewYieldlabBidder(t *testing.T) { + bid := NewYieldlabBidder(testURL) + assert.NotNil(t, bid) + assert.Equal(t, bid.endpoint, testURL) + assert.NotNil(t, bid.cacheBuster) + assert.NotNil(t, bid.getWeek) +} + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "yieldlabtest", newTestYieldlabBidder(testURL)) +} + +func Test_splitSize(t *testing.T) { + type args struct { + size string + } + tests := []struct { + name string + args args + want uint64 + want1 uint64 + wantErr bool + }{ + { + name: "valid", + args: args{ + size: "300x800", + }, + want: 300, + want1: 800, + wantErr: false, + }, + { + name: "empty", + args: args{ + size: "", + }, + want: 0, + want1: 0, + wantErr: false, + }, + { + name: "invalid", + args: args{ + size: "test", + }, + want: 0, + want1: 0, + wantErr: false, + }, + { + name: "invalid_height", + args: args{ + size: "200xtest", + }, + want: 0, + want1: 0, + wantErr: true, + }, + { + name: "invalid_width", + args: args{ + size: "testx200", + }, + want: 0, + want1: 0, + wantErr: true, + }, + { + name: "invalid_separator", + args: args{ + size: "200y200", + }, + want: 0, + want1: 0, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := splitSize(tt.args.size) + if (err != nil) != tt.wantErr { + t.Errorf("splitSize() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("splitSize() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("splitSize() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestYieldlabAdapter_makeEndpointURL_invalidEndpoint(t *testing.T) { + bid := NewYieldlabBidder("test$:/something§") + _, err := bid.makeEndpointURL(nil, nil) + assert.Error(t, err) +} diff --git a/adapters/yieldlab/yieldlabtest/exemplary/banner.json b/adapters/yieldlab/yieldlabtest/exemplary/banner.json new file mode 100644 index 00000000000..8dd94404097 --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/exemplary/banner.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "adSize": "728x90", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } + } + ], + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c" + }, + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + }, + "site": { + "id": "fake-site-id", + "publisher": { + "id": "1" + }, + "page": "http://localhost:9090/gdpr.html" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Cookie": [ + "id=34a53e82-0dc3-4815-8b7e-b725ede0361c" + ], + "Referer": [ + "http://localhost:9090/gdpr.html" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ], + "X-Forwarded-For": [ + "169.254.13.37" + ] + }, + "uri": "https://ad.yieldlab.net/testing/12345?content=json&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": 12345, + "price": 201, + "advertiser": "yieldlab", + "adsize": "728x90", + "pid": 1234, + "did": 5678, + "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "adm": "", + "crid": "12345123433", + "dealid": "1234", + "id": "12345", + "impid": "test-imp-id", + "price": 2.01, + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/yieldlab/yieldlabtest/exemplary/gdpr.json b/adapters/yieldlab/yieldlabtest/exemplary/gdpr.json new file mode 100644 index 00000000000..381ba688e09 --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/exemplary/gdpr.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "adSize": "728x90", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } + } + ], + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "site": { + "id": "fake-site-id", + "publisher": { + "id": "1" + }, + "page": "http://localhost:9090/gdpr.html" + }, + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c", + "ext": { + "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Cookie": [ + "id=34a53e82-0dc3-4815-8b7e-b725ede0361c" + ], + "Referer": [ + "http://localhost:9090/gdpr.html" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ], + "X-Forwarded-For": [ + "169.254.13.37" + ] + }, + "uri": "https://ad.yieldlab.net/testing/12345?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&content=json&gdpr=1&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": 12345, + "price": 201, + "advertiser": "yieldlab", + "adsize": "728x90", + "pid": 1234, + "did": 5678, + "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "adm": "", + "crid": "12345123433", + "dealid": "1234", + "id": "12345", + "impid": "test-imp-id", + "price": 2.01, + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/yieldlab/yieldlabtest/exemplary/video.json b/adapters/yieldlab/yieldlabtest/exemplary/video.json new file mode 100644 index 00000000000..9e970ae79b5 --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/exemplary/video.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "adSize": "728x90", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + }, + "video": { + "context": "instream", + "mimes": [ + "video/mp4" + ], + "playerSize": [ + [ + 400, + 600 + ] + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2 + ], + "w": 1, + "h": 2, + "startdelay": 1, + "placement": 1, + "playbackmethod": [ + 2 + ] + } + } + ], + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c" + }, + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + }, + "site": { + "id": "fake-site-id", + "publisher": { + "id": "1" + }, + "page": "http://localhost:9090/gdpr.html" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Cookie": [ + "id=34a53e82-0dc3-4815-8b7e-b725ede0361c" + ], + "Referer": [ + "http://localhost:9090/gdpr.html" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ], + "X-Forwarded-For": [ + "169.254.13.37" + ] + }, + "uri": "https://ad.yieldlab.net/testing/12345?content=json&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": 12345, + "price": 201, + "advertiser": "yieldlab", + "adsize": "728x90", + "pid": 1234, + "did": 5678, + "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "crid": "12345123433", + "dealid": "1234", + "id": "12345", + "impid": "test-imp-id", + "nurl": "https://ad.yieldlab.net/d/12345/123456789/728x90?id=abc&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&pvid=40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5&ts=testing", + "price": 2.01, + "w": 728, + "h": 90 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/yieldlab/yieldlabtest/exemplary/video_app.json b/adapters/yieldlab/yieldlabtest/exemplary/video_app.json new file mode 100644 index 00000000000..67d526b3400 --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/exemplary/video_app.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "adSize": "728x90", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + }, + "video": { + "context": "instream", + "mimes": [ + "video/mp4" + ], + "playerSize": [ + [ + 400, + 600 + ] + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2 + ], + "w": 1, + "h": 2, + "startdelay": 1, + "placement": 1, + "playbackmethod": [ + 2 + ] + } + } + ], + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Cookie": [ + "id=34a53e82-0dc3-4815-8b7e-b725ede0361c" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ], + "X-Forwarded-For": [ + "169.254.13.37" + ] + }, + "uri": "https://ad.yieldlab.net/testing/12345?content=json&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pubappname=Awesome+App&pubbundlename=com.app.awesome&pvid=true&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": 12345, + "price": 201, + "advertiser": "yieldlab", + "adsize": "728x90", + "pid": 1234, + "did": 5678, + "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "crid": "12345123433", + "dealid": "1234", + "id": "12345", + "impid": "test-imp-id", + "nurl": "https://ad.yieldlab.net/d/12345/123456789/728x90?id=abc&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&pvid=40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5&ts=testing", + "price": 2.01, + "w": 728, + "h": 90 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/yieldmo/usersync.go b/adapters/yieldmo/usersync.go index 16fa10e5b78..25d65f229a2 100644 --- a/adapters/yieldmo/usersync.go +++ b/adapters/yieldmo/usersync.go @@ -8,5 +8,5 @@ import ( ) func NewYieldmoSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("yieldmo", 0, temp, adapters.SyncTypeRedirect) + return adapters.NewSyncer("yieldmo", 173, temp, adapters.SyncTypeRedirect) } diff --git a/adapters/yieldmo/usersync_test.go b/adapters/yieldmo/usersync_test.go index 10cba77a060..1212efdb878 100644 --- a/adapters/yieldmo/usersync_test.go +++ b/adapters/yieldmo/usersync_test.go @@ -25,6 +25,6 @@ func TestYieldmoSyncer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "//ads.yieldmo.com/pbsync?gdpr=0&gdpr_consent=&us_privacy=&redirectUri=http%3A%2F%2Flocalhost%2F%2Fsetuid%3Fbidder%3Dyieldmo%26gdpr%3D0%26gdpr_consent%3D%26uid%3D%24UID", syncInfo.URL) assert.Equal(t, "redirect", syncInfo.Type) - assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.EqualValues(t, 173, syncer.GDPRVendorID()) assert.False(t, syncInfo.SupportCORS) } diff --git a/adapters/yieldone/params_test.go b/adapters/yieldone/params_test.go new file mode 100644 index 00000000000..e0142334d6e --- /dev/null +++ b/adapters/yieldone/params_test.go @@ -0,0 +1,48 @@ +package yieldone + +import ( + "encoding/json" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "testing" +) + +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.BidderYieldone, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Yieldone params: %s", validParam) + } + } +} + +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.BidderYieldone, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"placementId": "123"}`, +} + +var invalidParams = []string{ + `null`, + `nil`, + ``, + `[]`, + `true`, + `2`, + `{"invalid_param": "123"}`, + `{"placementId": 123}`, +} diff --git a/adapters/yieldone/usersync.go b/adapters/yieldone/usersync.go new file mode 100644 index 00000000000..333550aa775 --- /dev/null +++ b/adapters/yieldone/usersync.go @@ -0,0 +1,12 @@ +package yieldone + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +func NewYieldoneSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("yieldone", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/yieldone/usersync_test.go b/adapters/yieldone/usersync_test.go new file mode 100644 index 00000000000..730f9103017 --- /dev/null +++ b/adapters/yieldone/usersync_test.go @@ -0,0 +1,30 @@ +package yieldone + +import ( + "testing" + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestYieldoneSyncer(t *testing.T) { + syncURL := "//not_localhost/synclocalhost%2Fsetuid%3Fbidder%3Dyieldone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewYieldoneSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "//not_localhost/synclocalhost%2Fsetuid%3Fbidder%3Dyieldone%26gdpr%3D0%26gdpr_consent%3D%26uid%3D%24UID", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/adapters/yieldone/yieldone.go b/adapters/yieldone/yieldone.go new file mode 100644 index 00000000000..7b0f35a7dc7 --- /dev/null +++ b/adapters/yieldone/yieldone.go @@ -0,0 +1,144 @@ +package yieldone + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" +) + +type YieldoneAdapter struct { + endpoint string +} + +// MakeRequests makes the HTTP requests which should be made to fetch bids. +func (a *YieldoneAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errors = make([]error, 0) + + var validImps []openrtb.Imp + for i := 0; i < len(request.Imp); i++ { + if err := preprocess(&request.Imp[i]); err == nil { + validImps = append(validImps, request.Imp[i]) + } else { + errors = append(errors, err) + } + } + + request.Imp = validImps + + reqJSON, err := json.Marshal(request) + if err != nil { + errors = append(errors, err) + return nil, errors + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }}, errors +} + +// MakeBids unpacks the server's response into Bids. +func (a *YieldoneAdapter) 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 bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp) + if err != nil { + return nil, []error{err} + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + }) + } + } + return bidResponse, nil + +} + +// NewYieldoneBidder configure bidder endpoint +func NewYieldoneBidder(endpoint string) *YieldoneAdapter { + return &YieldoneAdapter{ + endpoint: endpoint, + } +} + +func preprocess(imp *openrtb.Imp) error { + + var ext adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &ext); err != nil { + return err + } + var impressionExt openrtb_ext.ExtImpYieldone + if err := json.Unmarshal(ext.Bidder, &impressionExt); err != nil { + return err + } + + if imp.Banner != nil { + bannerCopy := *imp.Banner + if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 { + firstFormat := bannerCopy.Format[0] + bannerCopy.W = &(firstFormat.W) + bannerCopy.H = &(firstFormat.H) + } + imp.Banner = &bannerCopy + } + + return nil +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) (openrtb_ext.BidType, error) { + for _, imp := range imps { + if imp.ID == impID { + if imp.Banner != nil { + return openrtb_ext.BidTypeBanner, nil + } + + if imp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unknown impression type for ID: \"%s\"", impID), + } + } + } + + // This shouldnt happen. Lets handle it just incase by returning an error. + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to find impression for ID: \"%s\"", impID), + } +} diff --git a/adapters/yieldone/yieldone_test.go b/adapters/yieldone/yieldone_test.go new file mode 100644 index 00000000000..c61925411d9 --- /dev/null +++ b/adapters/yieldone/yieldone_test.go @@ -0,0 +1,11 @@ +package yieldone + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "yieldonetest", NewYieldoneBidder("http://localhost/prebid")) +} diff --git a/adapters/yieldone/yieldonetest/exemplary/simple-banner.json b/adapters/yieldone/yieldonetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..f84476f1e86 --- /dev/null +++ b/adapters/yieldone/yieldonetest/exemplary/simple-banner.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://localhost/prebid", + "body": { + "id": "test-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "seat": "yieldone", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + }] + }], + "cur": "JPY" + } + } + }], + + "expectedBidResponses": [{ + "currency": "JPY", + "bids": [{ + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} diff --git a/adapters/yieldone/yieldonetest/exemplary/simple-video.json b/adapters/yieldone/yieldonetest/exemplary/simple-video.json new file mode 100644 index 00000000000..dc313abede7 --- /dev/null +++ b/adapters/yieldone/yieldonetest/exemplary/simple-video.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "41993" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://localhost/prebid", + "body": { + "id": "test-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "41993" + } + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "seat": "yieldone", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad-vast", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + }] + }], + "cur": "JPY" + } + } + }], + + "expectedBidResponses": [{ + "currency": "JPY", + "bids": [{ + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad-vast", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250 + }, + "type": "video" + }] + }] +} diff --git a/adapters/yieldone/yieldonetest/params/race/banner.json b/adapters/yieldone/yieldonetest/params/race/banner.json new file mode 100644 index 00000000000..c88180845eb --- /dev/null +++ b/adapters/yieldone/yieldonetest/params/race/banner.json @@ -0,0 +1,4 @@ +{ + "placementId": "36891" +} + diff --git a/adapters/yieldone/yieldonetest/supplemental/bad_response.json b/adapters/yieldone/yieldonetest/supplemental/bad_response.json new file mode 100644 index 00000000000..fa993a2fff5 --- /dev/null +++ b/adapters/yieldone/yieldonetest/supplemental/bad_response.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": "{\"id\"data.lost" + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/yieldone/yieldonetest/supplemental/status_204.json b/adapters/yieldone/yieldonetest/supplemental/status_204.json new file mode 100644 index 00000000000..b1c9304a35a --- /dev/null +++ b/adapters/yieldone/yieldonetest/supplemental/status_204.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + + "expectedBidResponses": [] +} diff --git a/adapters/yieldone/yieldonetest/supplemental/status_400.json b/adapters/yieldone/yieldonetest/supplemental/status_400.json new file mode 100644 index 00000000000..1cb172bb371 --- /dev/null +++ b/adapters/yieldone/yieldonetest/supplemental/status_400.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/yieldone/yieldonetest/supplemental/status_418.json b/adapters/yieldone/yieldonetest/supplemental/status_418.json new file mode 100644 index 00000000000..30cc16adde5 --- /dev/null +++ b/adapters/yieldone/yieldonetest/supplemental/status_418.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://localhost/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "36891" + } + } + } + ] + } + }, + "mockResponse": { + "status": 418, + "body": {} + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 418. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/zeroclickfraud/usersync.go b/adapters/zeroclickfraud/usersync.go new file mode 100644 index 00000000000..a5435335ab8 --- /dev/null +++ b/adapters/zeroclickfraud/usersync.go @@ -0,0 +1,12 @@ +package zeroclickfraud + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +func NewZeroClickFraudSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("zeroclickfraud", 0, temp, adapters.SyncTypeIframe) +} diff --git a/adapters/zeroclickfraud/usersync_test.go b/adapters/zeroclickfraud/usersync_test.go new file mode 100644 index 00000000000..e6a54fd9a5c --- /dev/null +++ b/adapters/zeroclickfraud/usersync_test.go @@ -0,0 +1,34 @@ +package zeroclickfraud + +import ( + "testing" + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestZeroClickFraudSyncer(t *testing.T) { + syncURL := "https://s.0cf.io/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r=https%3A%2F%2Flocalhost%3A8888%2Fsetuid%3Fbidder%3Dzeroclickfraud%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewZeroClickFraudSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", + }, + CCPA: ccpa.Policy{ + Value: "1NYN", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://s.0cf.io/sync?gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw&us_privacy=1NYN&r=https%3A%2F%2Flocalhost%3A8888%2Fsetuid%3Fbidder%3Dzeroclickfraud%26gdpr%3D1%26gdpr_consent%3DBONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw%26uid%3D%24%7Buid%7D", syncInfo.URL) + assert.Equal(t, "iframe", syncInfo.Type) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/adapters/zeroclickfraud/zeroclickfraud.go b/adapters/zeroclickfraud/zeroclickfraud.go new file mode 100644 index 00000000000..560f4456580 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraud.go @@ -0,0 +1,187 @@ +package zeroclickfraud + +import ( + "encoding/json" + "fmt" + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/macros" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/golang/glog" + "net/http" + "strconv" + "text/template" +) + +type ZeroClickFraudAdapter struct { + EndpointTemplate template.Template +} + +func (a *ZeroClickFraudAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + errs := make([]error, 0, len(request.Imp)) + headers := http.Header{ + "Content-Type": {"application/json"}, + "Accept": {"application/json"}, + } + + // Pull the host and source ID info from the bidder params. + reqImps, err := splitImpressions(request.Imp) + + if err != nil { + errs = append(errs, err) + } + + requests := []*adapters.RequestData{} + + for reqExt, reqImp := range reqImps { + request.Imp = reqImp + reqJson, err := json.Marshal(request) + + if err != nil { + errs = append(errs, err) + continue + } + + urlParams := macros.EndpointTemplateParams{Host: reqExt.Host, SourceId: strconv.Itoa(reqExt.SourceId)} + url, err := macros.ResolveMacros(a.EndpointTemplate, urlParams) + + if err != nil { + errs = append(errs, err) + continue + } + + request := adapters.RequestData{ + Method: "POST", + Uri: url, + Body: reqJson, + Headers: headers} + + requests = append(requests, &request) + } + + return requests, errs +} + +/* +internal original request in OpenRTB, external = result of us having converted it (what comes out of MakeRequests) +*/ +func (a *ZeroClickFraudAdapter) 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("ERR, bad input %d", response.StatusCode), + }} + } else if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("ERR, response with status %d", response.StatusCode), + }} + } + + var bidResp openrtb.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponse() + bidResponse.Currency = bidResp.Cur + + for _, seatBid := range bidResp.SeatBid { + for i := 0; i < len(seatBid.Bid); i++ { + bid := seatBid.Bid[i] + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: getMediaType(bid.ImpID, internalRequest.Imp), + }) + } + } + + return bidResponse, nil +} + +func splitImpressions(imps []openrtb.Imp) (map[openrtb_ext.ExtImpZeroClickFraud][]openrtb.Imp, error) { + + var m = make(map[openrtb_ext.ExtImpZeroClickFraud][]openrtb.Imp) + + for _, imp := range imps { + bidderParams, err := getBidderParams(&imp) + if err != nil { + return nil, err + } + + m[*bidderParams] = append(m[*bidderParams], imp) + } + + return m, nil +} + +func getBidderParams(imp *openrtb.Imp) (*openrtb_ext.ExtImpZeroClickFraud, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Missing bidder ext: %s", err.Error()), + } + } + var zeroclickfraudExt openrtb_ext.ExtImpZeroClickFraud + if err := json.Unmarshal(bidderExt.Bidder, &zeroclickfraudExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("Cannot Resolve host or sourceId: %s", err.Error()), + } + } + + if zeroclickfraudExt.SourceId < 1 { + return nil, &errortypes.BadInput{ + Message: "Invalid/Missing SourceId", + } + } + + if len(zeroclickfraudExt.Host) < 1 { + return nil, &errortypes.BadInput{ + Message: "Invalid/Missing Host", + } + } + + return &zeroclickfraudExt, nil +} + +func getMediaType(impID string, imps []openrtb.Imp) openrtb_ext.BidType { + + bidType := openrtb_ext.BidTypeBanner + + for _, imp := range imps { + if imp.ID == impID { + if imp.Video != nil { + bidType = openrtb_ext.BidTypeVideo + break + } else if imp.Native != nil { + bidType = openrtb_ext.BidTypeNative + break + } else { + bidType = openrtb_ext.BidTypeBanner + break + } + } + } + + return bidType +} + +func NewZeroClickFraudBidder(endpoint string) *ZeroClickFraudAdapter { + template, err := template.New("endpointTemplate").Parse(endpoint) + if err != nil { + glog.Fatal("Unable to parse endpoint url template") + return nil + } + + return &ZeroClickFraudAdapter{EndpointTemplate: *template} +} diff --git a/adapters/zeroclickfraud/zeroclickfraud_test.go b/adapters/zeroclickfraud/zeroclickfraud_test.go new file mode 100644 index 00000000000..a72bfdf8c80 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraud_test.go @@ -0,0 +1,11 @@ +package zeroclickfraud + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "zeroclickfraudtest", NewZeroClickFraudBidder("http://{{.Host}}/openrtb2?sid={{.SourceId}}")) +} diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/multi-request.json b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/multi-request.json new file mode 100644 index 00000000000..70bfb9645c8 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/multi-request.json @@ -0,0 +1,160 @@ +{ + "mockBidRequest": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + },{ + "id": "some-impression-id2", + "banner": + { + "format": [{ + "w": 300, + "h": 600 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": + { + "uri": "http://q.0cf.io/openrtb2?sid=906295", + "body": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + },{ + "id": "some-impression-id2", + "banner": + { + "format": [ + { + "w": 300, + "h": 600 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + } + }, + "mockResponse": + { + "status": 200, + "body": + { + "id": "some-request-id", + "bidid": "183975330-5-29038-2", + "seatbid": [ + { + "seat": "906295", + "bid": [ + { + "id": "2181314349", + "impid": "some-impression-id", + "adm": "
Datablocks provides world class \"Software as a Service\" (SaaS) solutions to its clients.
www.zeroclickfraud.com
\"\"
", + "price": 13.37, + "cid": "906293", + "adid": "906297", + "crid": "906299", + "w": 300, + "h": 250 + }] + }], + "cur": "USD", + "ext": + {} + } + } + }], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": + { + "id": "2181314349", + "impid": "some-impression-id", + "adm": "
Datablocks provides world class \"Software as a Service\" (SaaS) solutions to its clients.
www.zeroclickfraud.com
\"\"", + "price": 13.37, + "cid": "906293", + "adid": "906297", + "crid": "906299", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/native.json b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/native.json new file mode 100644 index 00000000000..dcf9064f29d --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/native.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "native": + { + "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}},{\"id\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":2,\"data\":{\"type\":1,\"len\":200}},{\"id\":3,\"data\":{\"type\":2,\"len\":15000}},{\"id\":4,\"data\":{\"type\":6,\"len\":40}}]}", + "ver": "1.1" + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "user": + { + "buyeruid": "4610943261" + }, + "at": 1, + "tmax": 500 + }, + "httpcalls": [ + { + "expectedRequest": + { + "uri": "http://q.0cf.io/openrtb2?sid=906295", + "body": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "native": + { + "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":0,\"required\":1,\"title\":{\"len\":500}},{\"id\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":2,\"data\":{\"type\":1,\"len\":200}},{\"id\":3,\"data\":{\"type\":2,\"len\":15000}},{\"id\":4,\"data\":{\"type\":6,\"len\":40}}]}", + "ver": "1.1" + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "user": + { + "buyeruid": "4610943261" + }, + "at": 1, + "tmax": 500 + } + }, + "mockResponse": + { + "status":200, + "body": { + "id": "some-request-id", + "bidid": "183975330-3-29038-2", + "seatbid": [ + { + "seat": "906295", + "bid": [ + { + "id": "2181314346", + "impid": "some-impression-id", + "adm": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{ \"id\":0,\"required\":1,\"title\":{\"text\":\"Datablocks Inc.\"}},{ \"id\":3,\"required\":0,\"data\":{\"value\":\"Datablocks provides world class \\\"Software as a Service\\\" (SaaS) solutions to its clients.\"}}],\"link\":{\"url\":\"https://t.0cf.io/c/267237/?fcid=43154325321\"},\"imptrackers\":[\"https://t.0cf.io/i/267237/?fcid=43154325321&pixel=1\"],\"jstracker\":[]}}", + "price": 13.37, + "cid": "906293", + "adid": "906297", + "crid": "906299", + "ext": {} + }] + }], + "cur": "USD", + "ext": {} + } + } + }], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "2181314346", + "impid": "some-impression-id", + "adm": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{ \"id\":0,\"required\":1,\"title\":{\"text\":\"Datablocks Inc.\"}},{ \"id\":3,\"required\":0,\"data\":{\"value\":\"Datablocks provides world class \\\"Software as a Service\\\" (SaaS) solutions to its clients.\"}}],\"link\":{\"url\":\"https://t.0cf.io/c/267237/?fcid=43154325321\"},\"imptrackers\":[\"https://t.0cf.io/i/267237/?fcid=43154325321&pixel=1\"],\"jstracker\":[]}}", + "price": 13.37, + "cid": "906293", + "adid": "906297", + "crid": "906299", + "ext": {} + }, + "type":"native" + } + ] + }] +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-banner.json b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-banner.json new file mode 100644 index 00000000000..1d5ee3b3a52 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": + { + "uri": "http://q.0cf.io/openrtb2?sid=906295", + "body": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + } + }, + "mockResponse": + { + "status": 200, + "body": + { + "id": "some-request-id", + "bidid": "183975330-5-29038-2", + "seatbid": [ + { + "seat": "906295", + "bid": [ + { + "id": "2181314349", + "impid": "some-impression-id", + "adm": "
Datablocks provides world class \"Software as a Service\" (SaaS) solutions to its clients.
www.zeroclickfraud.com.net
\"\"", + "price": 13.37, + "cid": "906293", + "adid": "906297", + "crid": "906299", + "w": 300, + "h": 250 + }] + }], + "cur": "USD", + "ext": + {} + } + } + }], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": + { + "id": "2181314349", + "impid": "some-impression-id", + "adm": "
Datablocks provides world class \"Software as a Service\" (SaaS) solutions to its clients.
www.zeroclickfraud.com.net
\"\"", + "price": 13.37, + "cid": "906293", + "adid": "906297", + "crid": "906299", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-video.json b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-video.json new file mode 100644 index 00000000000..949e74602dd --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-video.json @@ -0,0 +1,138 @@ +{ + "mockBidRequest": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": + { + "mimes":[ + "video/x-flv" + ], + "w": 500, + "h": 400, + "minduration": 30 + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + }, + + "httpCalls": [ + { + "expectedRequest": + { + "uri": "http://q.0cf.io/openrtb2?sid=906295", + "body": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": + { + "mimes":[ + "video/x-flv" + ], + "w": 500, + "h": 400, + "minduration": 30 + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 906295 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + } + }, + "mockResponse": + { + "status": 200, + "body": + { + "id": "some-request-id", + "bidid": "183975330-4-29038-2", + "seatbid": [ + { + "seat": "906295", + "bid": [ + { + "id": "2181314347", + "impid": "some-impression-id", + "nurl": "https://t.0cf.io/wm/267237/?fcid=2181314347", + "price": 13.37, + "cid": "906293", + "adid": "906297", + "crid": "906729", + "w": 500, + "h": 400, + "ext": + { + "type": "CPM" + } + }] + }], + "cur": "USD", + "ext": + {} + } + } + }], + + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": + { + "id": "2181314347", + "impid": "some-impression-id", + "price": 13.37, + "nurl": "https://t.0cf.io/wm/267237/?fcid=2181314347", + "adid": "906297", + "cid": "906293", + "crid": "906729", + "w": 500, + "h": 400, + "ext": + { + "type": "CPM" + } + }, + "type": "video" + }] + }] +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/params/race/banner.json b/adapters/zeroclickfraud/zeroclickfraudtest/params/race/banner.json new file mode 100644 index 00000000000..cff0af83143 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/params/race/banner.json @@ -0,0 +1,4 @@ +{ + "sourceId": 906295, + "host": "q.0cf.io" +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/params/race/native.json b/adapters/zeroclickfraud/zeroclickfraudtest/params/race/native.json new file mode 100644 index 00000000000..cff0af83143 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/params/race/native.json @@ -0,0 +1,4 @@ +{ + "sourceId": 906295, + "host": "q.0cf.io" +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/params/race/video.json b/adapters/zeroclickfraud/zeroclickfraudtest/params/race/video.json new file mode 100644 index 00000000000..cff0af83143 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/params/race/video.json @@ -0,0 +1,4 @@ +{ + "sourceId": 906295, + "host": "q.0cf.io" +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-host.json b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-host.json new file mode 100644 index 00000000000..cee5efbe760 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-host.json @@ -0,0 +1,33 @@ +{ + "mockBidRequest": { + "id": "bad-host-test", + "imp": [ + { + "id": "bad-host-test-imp", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": + { + "bidder": + { + "host": "", + "sourceId": 123 + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Invalid/Missing Host", + "comparison": "literal" + } + ] +} diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-response-body.json b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-response-body.json new file mode 100644 index 00000000000..84d6bd9d889 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-response-body.json @@ -0,0 +1,88 @@ +{ + "mockBidRequest": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 123 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": + { + "uri": "http://q.0cf.io/openrtb2?sid=123", + "body": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 123 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + } + }, + "mockResponse": + { + "status": 200, + "body":"foobar" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-server-response.json b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-server-response.json new file mode 100644 index 00000000000..fdea4f109a7 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-server-response.json @@ -0,0 +1,88 @@ +{ + "mockBidRequest": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 123 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": + { + "uri": "http://q.0cf.io/openrtb2?sid=123", + "body": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 123 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + } + }, + "mockResponse": + { + "status": 500, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "ERR, response with status 500", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-sourceId.json b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-sourceId.json new file mode 100644 index 00000000000..4d86c32cd58 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-sourceId.json @@ -0,0 +1,35 @@ +{ + "mockBidRequest": { + "id": "bad-sourceId-test", + "imp": [ + { + "id": "bad-sourceId-test-imp", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 0 + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Invalid/Missing SourceId", + "comparison": "literal" + } + ] + + +} diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-ext.json b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-ext.json new file mode 100644 index 00000000000..68d29e880b9 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-ext.json @@ -0,0 +1,27 @@ +{ + "mockBidRequest": { + "id": "missing-extbid-test", + "imp": [ + { + "id": "missing-extbid-test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Missing bidder ext: unexpected end of JSON input", + "comparison": "literal" + } + ] + + +} diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-extparam.json b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-extparam.json new file mode 100644 index 00000000000..d272cd5347c --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-extparam.json @@ -0,0 +1,30 @@ +{ + "mockBidRequest": { + "id": "missing-extbid-test", + "imp": [ + { + "id": "missing-extbid-test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "sourceId":54326 + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Cannot Resolve host or sourceId: unexpected end of JSON input", + "comparison": "literal" + } + ] + + +} diff --git a/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/no-content-response.json b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/no-content-response.json new file mode 100644 index 00000000000..3a36d6e04b2 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraudtest/supplemental/no-content-response.json @@ -0,0 +1,82 @@ +{ + "mockBidRequest": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 123 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": + { + "uri": "http://q.0cf.io/openrtb2?sid=123", + "body": + { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": + { + "format": [ + { + "w": 300, + "h": 250 + }] + }, + "ext": + { + "bidder": + { + "host": "q.0cf.io", + "sourceId": 123 + } + } + }], + "site": + { + "page": "prebid.org" + }, + "device": + { + "ip": "8.8.8.10" + }, + "at": 1, + "tmax": 500 + } + }, + "mockResponse": + { + "status": 204 + } + }], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/analytics/config/config_test.go b/analytics/config/config_test.go index e9847a3902e..3ae1fa3f82d 100644 --- a/analytics/config/config_test.go +++ b/analytics/config/config_test.go @@ -22,7 +22,7 @@ func TestSampleModule(t *testing.T) { Response: &openrtb.BidResponse{}, }) if count != 1 { - t.Errorf("PBSAnalyticsModule failed at LogAuctionObejct") + t.Errorf("PBSAnalyticsModule failed at LogAuctionObject") } am.LogSetUIDObject(&analytics.SetUIDObject{ @@ -33,12 +33,12 @@ func TestSampleModule(t *testing.T) { Success: true, }) if count != 2 { - t.Errorf("PBSAnalyticsModule failed at LogSetUIDObejct") + t.Errorf("PBSAnalyticsModule failed at LogSetUIDObject") } am.LogCookieSyncObject(&analytics.CookieSyncObject{}) if count != 3 { - t.Errorf("PBSAnalyticsModule failed at LogCookieSyncObejct") + t.Errorf("PBSAnalyticsModule failed at LogCookieSyncObject") } am.LogAmpObject(&analytics.AmpObject{}) diff --git a/config/config.go b/config/config.go old mode 100644 new mode 100755 index bac9ee17e6f..e27358b3d44 --- a/config/config.go +++ b/config/config.go @@ -23,6 +23,7 @@ type Configuration struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` Client HTTPClient `mapstructure:"http_client"` + CacheClient HTTPClient `mapstructure:"http_client_cache"` AdminPort int `mapstructure:"admin_port"` EnableGzip bool `mapstructure:"enable_gzip"` // StatusResponse is the string which will be returned by the /status endpoint when things are OK. @@ -48,6 +49,7 @@ type Configuration struct { AMPTimeoutAdjustment int64 `mapstructure:"amp_timeout_adjustment_ms"` GDPR GDPR `mapstructure:"gdpr"` CCPA CCPA `mapstructure:"ccpa"` + LMT LMT `mapstructure:"lmt"` CurrencyConverter CurrencyConverter `mapstructure:"currency_converter"` DefReqConfig DefReqConfig `mapstructure:"default_request"` @@ -63,6 +65,10 @@ type Configuration struct { AccountRequired bool `mapstructure:"account_required"` // Local private file containing SSL certificates PemCertsFile string `mapstructure:"certificates_file"` + // Custom headers to handle request timeouts from queueing infrastructure + RequestTimeoutHeaders RequestTimeoutHeaders `mapstructure:"request_timeout_headers"` + // Debug/logging flags go here + Debug Debug `mapstructure:"debug"` } const MIN_COOKIE_SIZE_BYTES = 500 @@ -102,6 +108,7 @@ func (cfg *Configuration) validate() configErrors { errs = cfg.GDPR.validate(errs) errs = cfg.CurrencyConverter.validate(errs) errs = validateAdapters(cfg.Adapters, errs) + errs = cfg.Debug.validate(errs) return errs } @@ -134,12 +141,21 @@ func (cfg *AuctionTimeouts) LimitAuctionTimeout(requested time.Duration) time.Du return requested } +// Privacy is a grouping of privacy related configs to assist in dependency injection. +type Privacy struct { + CCPA CCPA + GDPR GDPR + LMT LMT +} + type GDPR struct { HostVendorID int `mapstructure:"host_vendor_id"` UsersyncIfAmbiguous bool `mapstructure:"usersync_if_ambiguous"` Timeouts GDPRTimeouts `mapstructure:"timeouts_ms"` NonStandardPublishers []string `mapstructure:"non_standard_publishers,flow"` NonStandardPublisherMap map[string]int + TCF2 TCF2 `mapstructure:"tcf2"` + AMPException bool `mapstructure:"amp_exception"` } func (cfg *GDPR) validate(errs configErrors) configErrors { @@ -162,10 +178,34 @@ func (t *GDPRTimeouts) ActiveTimeout() time.Duration { return time.Duration(t.ActiveVendorlistFetch) * time.Millisecond } +// TCF2 defines the TCF2 specific configurations for GDPR +type TCF2 struct { + Enabled bool `mapstructure:"enabled"` + Purpose1 PurposeDetail `mapstructure:"purpose1"` + Purpose2 PurposeDetail `mapstructure:"purpose2"` + Purpose7 PurposeDetail `mapstructure:"purpose7"` + SpecialPurpose1 PurposeDetail `mapstructure:"special_purpose1"` + PurposeOneTreatment PurposeOneTreatement `mapstructure:"purpose_one_treatement"` +} + +// Making a purpose struct so purpose specific details can be added later. +type PurposeDetail struct { + Enabled bool `mapstructure:"enabled"` +} + +type PurposeOneTreatement struct { + Enabled bool `mapstructure:"enabled"` + AccessAllowed bool `mapstructure:"access_allowed"` +} + type CCPA struct { Enforce bool `mapstructure:"enforce"` } +type LMT struct { + Enforce bool `mapstructure:"enforce"` +} + type Analytics struct { File FileLogs `mapstructure:"file"` } @@ -199,6 +239,11 @@ type HostCookie struct { TTL int64 `mapstructure:"ttl_days"` } +type RequestTimeoutHeaders struct { + RequestTimeInQueue string `mapstructure:"request_time_in_queue"` + RequestTimeoutInQueue string `mapstructure:"request_timeout_in_queue"` +} + func (cfg *HostCookie) TTLDuration() time.Duration { return time.Duration(cfg.TTL) * time.Hour * 24 } @@ -206,6 +251,7 @@ func (cfg *HostCookie) TTLDuration() time.Duration { const ( dummyHost string = "dummyhost.com" dummyPublisherID string = "12" + dummyAccountID string = "some_account" dummyGDPR string = "0" dummyGDPRConsent string = "someGDPRConsentString" dummyCCPA string = "1NYN" @@ -214,7 +260,7 @@ const ( type Adapter struct { Endpoint string `mapstructure:"endpoint"` // Required // UserSyncURL is the URL returned by /cookie_sync for this Bidder. It is _usually_ optional. - // If not defined, sensible defaults will be derved based on the config.external_url. + // If not defined, sensible defaults will be derived based on the config.external_url. // Note that some Bidders don't have sensible defaults, because their APIs require an ID that will vary // from one PBS host to another. // @@ -256,7 +302,7 @@ func validateAdapterEndpoint(endpoint string, adapterName string, errs configErr return append(errs, fmt.Errorf("Invalid endpoint template: %s for adapter: %s. %v", endpoint, adapterName, err)) } // Resolve macros (if any) in the endpoint URL - resolvedEndpoint, err := macros.ResolveMacros(*endpointTemplate, macros.EndpointTemplateParams{Host: dummyHost, PublisherID: dummyPublisherID}) + resolvedEndpoint, err := macros.ResolveMacros(*endpointTemplate, macros.EndpointTemplateParams{Host: dummyHost, PublisherID: dummyPublisherID, AccountID: dummyAccountID}) if err != nil { return append(errs, fmt.Errorf("Unable to resolve endpoint: %s for adapter: %s. %v", endpoint, adapterName, err)) } @@ -420,6 +466,30 @@ type DefReqFiles struct { FileName string `mapstructure:"name"` } +type Debug struct { + TimeoutNotification TimeoutNotification `mapstructure:"timeout_notification"` +} + +func (cfg *Debug) validate(errs configErrors) configErrors { + return cfg.TimeoutNotification.validate(errs) +} + +type TimeoutNotification struct { + // Log timeout notifications in the application log + Log bool `mapstructure:"log"` + // Fraction of notifications to log + SamplingRate float32 `mapstructure:"sampling_rate"` + // Only log failures + FailOnly bool `mapstructure:"fail_only"` +} + +func (cfg *TimeoutNotification) validate(errs configErrors) configErrors { + if cfg.SamplingRate < 0.0 || cfg.SamplingRate > 1.0 { + errs = append(errs, fmt.Errorf("debug.timeout_notification.sampling_rate must be positive and not greater than 1.0. Got %f", cfg.SamplingRate)) + } + return errs +} + // New uses viper to get our server configurations. func New(v *viper.Viper) (*Configuration, error) { var c Configuration @@ -487,20 +557,29 @@ func (cfg *Configuration) setDerivedDefaults() { syncRedirectEndpoint := url.QueryEscape(cfg.ExternalURL + SETUID_ENDPOINT) setDefaultUsersync(cfg.Adapters, openrtb_ext.Bidder33Across, "https://ic.tynt.com/r/d?m=xch&rt=html&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&ru="+syncRedirectEndpoint+"bidder%3D33across%26uid%3D33XUSERID33X&id=zzz000000000002zzz") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdform, "https://cm.adform.net/cookie?redirect_url="+syncRedirectEndpoint+"bidder%3Dadform%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + // openrtb_ext.BidderAdgeneration doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernel, "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+syncRedirectEndpoint+"bidder%3Dadkernel%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernelAdn, "https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+syncRedirectEndpoint+"bidder%3DadkernelAdn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdpone, "https://usersync.adpone.com/csync?redir="+syncRedirectEndpoint+"bidder%3Dadpone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtelligent, "https://sync.adtelligent.com/csync?t=p&ep=0&redir="+syncRedirectEndpoint+"bidder%3Dadtelligent%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtarget, "https://sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dadtarget%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtelligent, "https://sync.adtelligent.com/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dadtelligent%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdmixer, "https://inv-nets.admixer.net/adxcm.aspx?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=1&rurl="+syncRedirectEndpoint+"bidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24") + // openrtb_ext.BidderAdOcean doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdvangelists, "https://nep.advangelists.com/xp/user-sync?acctid={aid}&&redirect="+syncRedirectEndpoint+"bidder%3Dadvangelists%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAJA, "https://ad.as.amanad.adtdp.com/v1/sync/ssp?ssp=4&gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Daja%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25s") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAppnexus, "https://ib.adnxs.com/getuid?"+syncRedirectEndpoint+"bidder%3Dadnxs%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAvocet, "https://ads.avct.cloud/getuid?&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url="+syncRedirectEndpoint+"bidder%3Davocet%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7B%7BUUID%7D%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeachfront, "https://sync.bfmio.com/sync_s2s?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&url="+syncRedirectEndpoint+"bidder%3Dbeachfront%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bio_cid%5D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeintoo, "https://ib.beintoo.com/um?ssp=pbs&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+syncRedirectEndpoint+"bidder%3Dbeintoo%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBrightroll, "https://pr-bh.ybp.yahoo.com/sync/appnexusprebidserver/?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url="+syncRedirectEndpoint+"bidder%3Dbrightroll%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConsumable, "https://e.serverbid.com/udb/9969/match?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/prebid/match?rurl="+syncRedirectEndpoint+"bidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/match/bounce/current?version=1&networkId=72582&rurl="+syncRedirectEndpoint+"bidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderCpmstar, "https://server.cpmstar.com/usersync.aspx?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+syncRedirectEndpoint+"bidder%3Dcpmstar%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderDatablocks, "https://sync.v5prebid.datablocks.net/s2ssync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+syncRedirectEndpoint+"bidder%3Ddatablocks%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderDmx, "https://dmx.districtm.io/s/v1/img/s/10007?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirect="+syncRedirectEndpoint+"bidder%3Ddatablocks%26gdpr%3D%24%7Bgdpr%7D%26gdpr_consent%3D%24%7Bgdpr_consent%7D%26uid%3D%24%7Buid%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEmxDigital, "https://cs.emxdgt.com/um?ssp=pbs&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+syncRedirectEndpoint+"bidder%3Demx_digital%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEngageBDR, "https://match.bnmla.com/usersync/s2s_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+syncRedirectEndpoint+"bidder%3Dengagebdr%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEPlanning, "https://ads.us.e-planning.net/uspd/1/?du=https%3A%2F%2Fads.us.e-planning.net%2Fgetuid%2F1%2F5a1ad71d2d53a0f5%3F"+syncRedirectEndpoint+"bidder%3Deplanning%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEPlanning, "https://ads.us.e-planning.net/uspd/1/?du="+syncRedirectEndpoint+"bidder%3Deplanning%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") // openrtb_ext.BidderFacebook doesn't have a good default. // openrtb_ext.BidderGamma doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGamoshi, "https://rtb.gamoshi.io/user_sync_prebid?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+syncRedirectEndpoint+"bidder%3Dgamoshi%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bgusr%5D") @@ -510,27 +589,36 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum-sec.casalemedia.com/usermatchredir?s=186523&cb="+syncRedirectEndpoint+"bidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLifestreet, "https://ads.lfstmedia.com/idsync/137062?synced=1&ttl=1s&rurl="+syncRedirectEndpoint+"bidder%3Dlifestreet%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLockerDome, "https://lockerdome.com/usync/prebidserver?pid="+cfg.Adapters["lockerdome"].PlatformID+"&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+syncRedirectEndpoint+"bidder%3Dlockerdome%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7B%7Buid%7D%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLunaMedia, "https://api.lunamedia.io/xp/user-sync?redirect="+syncRedirectEndpoint+"bidder%3Dlunamedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMarsmedia, "https://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+syncRedirectEndpoint+"bidder%3Dmarsmedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMgid, "https://cm.mgid.com/m?cdsp=363893&adu="+syncRedirectEndpoint+"bidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOpenx, "https://rtb.openx.net/sync/prebid?r="+syncRedirectEndpoint+"bidder%3Dopenx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNanoInteractive, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+syncRedirectEndpoint+"bidder%3Dnanointeractive%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNinthDecimal, "https://rtb.ninthdecimal.com/xp/user-sync?acctid={aid}&&redirect="+syncRedirectEndpoint+"bidder%3Dninthdecimal%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOpenx, "https://rtb.openx.net/sync/prebid?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+syncRedirectEndpoint+"bidder%3Dopenx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderPubmatic, "https://ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&predirect="+syncRedirectEndpoint+"bidder%3Dpubmatic%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderPulsepoint, "https://bh.contextweb.com/rtset?pid=561205&ev=1&rurl="+syncRedirectEndpoint+"bidder%3Dpulsepoint%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25VGUID%25%25") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderRhythmone, "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Drhythmone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BRX_UUID%5D") // openrtb_ext.BidderRTBHouse doesn't have a good default. // openrtb_ext.BidderRubicon doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSharethrough, "https://match.sharethrough.com/FGMrCMMc/v1?redirectUri="+syncRedirectEndpoint+"bidder%3Dsharethrough%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartRTB, "https://market-global.smrtb.com/sync/all?nid=smartrtb&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&rr="+syncRedirectEndpoint+"bidder%253Dsmartrtb%2526gdpr%253D{{.GDPR}}%2526gdpr_consent%253D{{.GDPRConsent}}%2526uid%253D%257BXID%257D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSomoaudience, "https://publisher-east.mobileadtrading.com/usersync?ru="+syncRedirectEndpoint+"bidder%3Dsomoaudience%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSonobi, "https://sync.go.sonobi.com/us.gif?loc="+syncRedirectEndpoint+"bidder%3Dsonobi%26consent_string%3D{{.GDPR}}%26gdpr%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSovrn, "https://ap.lijit.com/pixel?redir="+syncRedirectEndpoint+"bidder%3Dsovrn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSynacormedia, "https://sync.technoratimedia.com/services?srv=cs&pid=70&cb="+syncRedirectEndpoint+"bidder%3Dsynacormedia%26uid%3D%5BUSER_ID%5D") // openrtb_ext.BidderTappx doesn't have a good default. - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTelaria, "https://pbs.publishers.tremorhub.com/pubsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redir="+syncRedirectEndpoint+"%2Fsetuid%3Fbidder%3Dtelaria%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Btvid%5D") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTriplelift, "https://eb2.3lift.com/getuid?gpdr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dtriplelift%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTripleliftNative, "https://eb2.3lift.com/sync?gpdr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dtriplelift_native%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTelaria, "https://pbs.publishers.tremorhub.com/pubsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redir="+syncRedirectEndpoint+"bidder%3Dtelaria%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Btvid%5D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTriplelift, "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dtriplelift%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTripleliftNative, "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dtriplelift_native%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUcfunnel, "https://sync.aralego.com/idsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&usprivacy={{.USPrivacy}}&redirect="+syncRedirectEndpoint+"bidder%3Ducfunnel%26uid%3DSspCookieUserId") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUnruly, "https://usermatch.targeting.unrulymedia.com/pbsync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+syncRedirectEndpoint+"bidder%3Dunruly%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderValueImpression, "https://rtb.valueimpression.com/usersync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+syncRedirectEndpoint+"bidder%3Dvalueimpression%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderVisx, "https://t.visx.net/s2s_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+syncRedirectEndpoint+"bidder%3Dvisx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") // openrtb_ext.BidderVrtcal doesn't have a good default. + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldlab, "https://ad.yieldlab.net/mr?t=2&pid=9140838&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+syncRedirectEndpoint+"bidder%3Dyieldlab%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25YL_UID%25%25") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldmo, "https://ads.yieldmo.com/pbsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+syncRedirectEndpoint+"bidder%3Dyieldmo%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldone, "https://y.one.impact-ad.jp/hbs_cs?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+syncRedirectEndpoint+"bidder%3Dyieldone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderZeroClickFraud, "https://s.0cf.io/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+syncRedirectEndpoint+"bidder%3Dzeroclickfraud%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D") } func setDefaultUsersync(m map[string]Adapter, bidder openrtb_ext.BidderName, defaultValue string) { @@ -583,6 +671,9 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("http_client.max_idle_connections", 400) v.SetDefault("http_client.max_idle_connections_per_host", 10) v.SetDefault("http_client.idle_connection_timeout_seconds", 60) + v.SetDefault("http_client_cache.max_idle_connections", 10) + v.SetDefault("http_client_cache.max_idle_connections_per_host", 2) + v.SetDefault("http_client_cache.idle_connection_timeout_seconds", 60) // no metrics configured by default (metrics{host|database|username|password}) v.SetDefault("metrics.disabled_metrics.account_adapter_details", false) v.SetDefault("metrics.influxdb.host", "") @@ -662,38 +753,57 @@ func SetupViper(v *viper.Viper, filename string) { // If you're using one of these, make sure you check out the documentation (https://github.com/PubMatic-OpenWrap/prebid-server/tree/master/docs/bidders) // for them and specify all the parameters they need for them to work correctly. v.SetDefault("adapters.audiencenetwork.disabled", true) - + //v.SetDefault("adapters.rubicon.disabled", true) v.SetDefault("adapters.33across.endpoint", "http://ssc.33across.com/api/v1/hb") v.SetDefault("adapters.33across.partner_id", "") + v.SetDefault("adapters.dmx.endpoint", "https://dmx.districtm.io/b/v2") + v.SetDefault("adapters.adtelligent.endpoint", "http://hb.adtelligent.com/auction") v.SetDefault("adapters.adform.endpoint", "http://adx.adform.net/adx") + v.SetDefault("adapters.adgeneration.endpoint", "https://d.socdm.com/adsv/v1") + v.SetDefault("adapters.adhese.endpoint", "https://ads-{{.AccountID}}.adhese.com/json") v.SetDefault("adapters.adkernel.endpoint", "http://{{.Host}}/hb?zone={{.ZoneID}}") v.SetDefault("adapters.adkerneladn.endpoint", "http://{{.Host}}/rtbpub?account={{.PublisherID}}") + v.SetDefault("adapters.admixer.endpoint", "http://inv-nets.admixer.net/pbs.aspx") + v.SetDefault("adapters.adocean.endpoint", "https://{{.Host}}") + v.SetDefault("adapters.adoppler.endpoint", "http://app.trustedmarketplace.io/ads") v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server") - v.SetDefault("adapters.adtelligent.endpoint", "http://hb.adtelligent.com/auction") + v.SetDefault("adapters.adtarget.endpoint", "http://ghb.console.adtarget.com.tr/pbs/ortb") + v.SetDefault("adapters.adtelligent.endpoint", "http://ghb.adtelligent.com/pbs/ortb") v.SetDefault("adapters.advangelists.endpoint", "http://nep.advangelists.com/xp/get?pubid={{.PublisherID}}") + v.SetDefault("adapters.aja.endpoint", "https://ad.as.amanad.adtdp.com/v1/bid/4") v.SetDefault("adapters.applogy.endpoint", "http://rtb.applogy.com/v1/prebid") v.SetDefault("adapters.appnexus.endpoint", "http://ib.adnxs.com/openrtb2") // Docs: https://wiki.appnexus.com/display/supply/Incoming+Bid+Request+from+SSPs v.SetDefault("adapters.appnexus.platform_id", "5") + v.SetDefault("adapters.avocet.disabled", true) v.SetDefault("adapters.beachfront.endpoint", "https://display.bfmio.com/prebid_display") + v.SetDefault("adapters.beachfront.extra_info", "{\"video_endpoint\":\"https://reachms.bfmio.com/bid.json?exchange_id\"}") + v.SetDefault("adapters.beintoo.endpoint", "https://ib.beintoo.com/um") 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.cpmstar.endpoint", "https://server.cpmstar.com/openrtbbidrq.aspx") v.SetDefault("adapters.datablocks.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}") v.SetDefault("adapters.emx_digital.endpoint", "https://hb.emxdgt.com") v.SetDefault("adapters.engagebdr.endpoint", "http://dsp.bnmla.com/hb") - v.SetDefault("adapters.eplanning.endpoint", "http://ads.us.e-planning.net/hb/1") + v.SetDefault("adapters.eplanning.endpoint", "http://rtb.e-planning.net/pbs/1") v.SetDefault("adapters.gamma.endpoint", "https://hb.gammaplatform.com/adx/request/") v.SetDefault("adapters.gamoshi.endpoint", "https://rtb.gamoshi.io") v.SetDefault("adapters.grid.endpoint", "http://grid.bidswitch.net/sp_bid?sp=prebid") v.SetDefault("adapters.gumgum.endpoint", "https://g2.gumgum.com/providers/prbds2s/bid") v.SetDefault("adapters.improvedigital.endpoint", "http://ad.360yield.com/pbs") v.SetDefault("adapters.ix.endpoint", "http://appnexus-us-east.lb.indexww.com/transbidder?p=184932") + v.SetDefault("adapters.kidoz.endpoint", "http://prebid-adapter.kidoz.net/openrtb2/auction?src=prebid-server") v.SetDefault("adapters.kubient.endpoint", "http://kbntx.ch/prebid") v.SetDefault("adapters.lifestreet.endpoint", "https://prebid.s2s.lfstmedia.com/adrequest") v.SetDefault("adapters.lockerdome.endpoint", "https://lockerdome.com/ladbid/prebidserver/openrtb2") + v.SetDefault("adapters.lunamedia.endpoint", "http://api.lunamedia.io/xp/get?pubid={{.PublisherID}}") v.SetDefault("adapters.marsmedia.endpoint", "https://bid306.rtbsrv.com/bidder/?bid=f3xtet") v.SetDefault("adapters.mgid.endpoint", "https://prebid.mgid.com/prebid/") + v.SetDefault("adapters.mobilefuse.endpoint", "http://mfx-us-east.mobilefuse.com/openrtb?pub_id={{.PublisherID}}") + v.SetDefault("adapters.nanointeractive.endpoint", "https://ad.audiencemanager.de/hbs") + v.SetDefault("adapters.ninthdecimal.endpoint", "http://rtb.ninthdecimal.com/xp/get?pubid={{.PublisherID}}") v.SetDefault("adapters.openx.endpoint", "http://rtb.openx.net/prebid") + v.SetDefault("adapters.orbidder.endpoint", "https://orbidder.otto.de/openrtb2") v.SetDefault("adapters.pubmatic.endpoint", "https://hbopenbid.pubmatic.com/translator?source=prebid-server") v.SetDefault("adapters.pubnative.endpoint", "http://dsp.pubnative.net/bid/v1/request") v.SetDefault("adapters.pulsepoint.endpoint", "http://bid.contextweb.com/header/s/ortb/prebid-s2s") @@ -701,6 +811,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.rtbhouse.endpoint", "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids") v.SetDefault("adapters.rubicon.endpoint", "http://exapi-us-east.rubiconproject.com/a/api/exchange.json") v.SetDefault("adapters.sharethrough.endpoint", "http://btlr.sharethrough.com/FGMrCMMc/v1") + v.SetDefault("adapters.smartrtb.endpoint", "http://market-east.smrtb.com/json/publisher/rtb?pubid={{.PublisherID}}") v.SetDefault("adapters.somoaudience.endpoint", "http://publisher-east.mobileadtrading.com/rtb/bid") v.SetDefault("adapters.sonobi.endpoint", "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af") v.SetDefault("adapters.sovrn.endpoint", "http://ap.lijit.com/rtb/bid?src=prebid_server") @@ -710,12 +821,18 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.telaria.endpoint", "https://ads.tremorhub.com/ad/rtb/prebid") v.SetDefault("adapters.triplelift_native.disabled", true) v.SetDefault("adapters.triplelift_native.extra_info", "{\"publisher_whitelist\":[]}") - v.SetDefault("adapters.triplelift.endpoint", "https://tlx.3lift.com/s2s/auction?supplier_id=20") + v.SetDefault("adapters.triplelift.endpoint", "https://tlx.3lift.com/s2s/auction?sra=1&supplier_id=20") + v.SetDefault("adapters.ucfunnel.endpoint", "http://apac-hk-adx.aralego.com/prebid") v.SetDefault("adapters.unruly.endpoint", "http://targeting.unrulymedia.com/openrtb/2.2") + v.SetDefault("adapters.valueimpression.endpoint", "https://rtb.valueimpression.com/endpoint") v.SetDefault("adapters.verizonmedia.disabled", true) v.SetDefault("adapters.visx.endpoint", "https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard") v.SetDefault("adapters.vrtcal.endpoint", "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1804") + v.SetDefault("adapters.yeahmobi.endpoint", "https://{{.Host}}/prebid/bid") + v.SetDefault("adapters.yieldlab.endpoint", "https://ad.yieldlab.net/yp/") v.SetDefault("adapters.yieldmo.endpoint", "https://ads.yieldmo.com/exchange/prebid-server") + v.SetDefault("adapters.yieldone.endpoint", "https://y.one.impact-ad.jp/hbs_imp") + v.SetDefault("adapters.zeroclickfraud.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}") v.SetDefault("max_request_size", 1024*256) v.SetDefault("analytics.file.filename", "") @@ -725,7 +842,17 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("gdpr.timeouts_ms.init_vendorlist_fetches", 0) v.SetDefault("gdpr.timeouts_ms.active_vendorlist_fetch", 0) v.SetDefault("gdpr.non_standard_publishers", []string{""}) + v.SetDefault("gdpr.tcf2.enabled", true) + v.SetDefault("gdpr.tcf2.purpose1.enabled", true) + v.SetDefault("gdpr.tcf2.purpose2.enabled", true) + v.SetDefault("gdpr.tcf2.purpose4.enabled", true) + v.SetDefault("gdpr.tcf2.purpose7.enabled", true) + v.SetDefault("gdpr.tcf2.special_purpose1.enabled", true) + v.SetDefault("gdpr.tcf2.purpose_one_treatement.enabled", true) + v.SetDefault("gdpr.tcf2.purpose_one_treatement.access_allowed", true) + v.SetDefault("gdpr.amp_exception", false) v.SetDefault("ccpa.enforce", false) + v.SetDefault("lmt.enforce", true) v.SetDefault("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json") v.SetDefault("currency_converter.fetch_interval_seconds", 1800) // fetch currency rates every 30 minutes v.SetDefault("default_request.type", "") @@ -736,6 +863,13 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("account_required", false) v.SetDefault("certificates_file", "") + v.SetDefault("request_timeout_headers.request_time_in_queue", "") + v.SetDefault("request_timeout_headers.request_timeout_in_queue", "") + + v.SetDefault("debug.timeout_notification.log", false) + v.SetDefault("debug.timeout_notification.sampling_rate", 0.0) + v.SetDefault("debug.timeout_notification.fail_only", false) + // Set environment variable support: v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvPrefix("PBS") diff --git a/config/config_test.go b/config/config_test.go index 87511795a56..7853bbed1af 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -43,6 +43,8 @@ gdpr: non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"] ccpa: enforce: true +lmt: + enforce: true host_cookie: cookie_name: userid family: prebid @@ -68,6 +70,10 @@ http_client: max_idle_connections: 500 max_idle_connections_per_host: 20 idle_connection_timeout_seconds: 30 +http_client_cache: + max_idle_connections: 1 + max_idle_connections_per_host: 2 + idle_connection_timeout_seconds: 3 currency_converter: fetch_url: https://currency.prebid.org fetch_interval_seconds: 1800 @@ -214,6 +220,9 @@ func TestFullConfig(t *testing.T) { cmpInts(t, "http_client.max_idle_connections", cfg.Client.MaxIdleConns, 500) cmpInts(t, "http_client.max_idle_connections_per_host", cfg.Client.MaxIdleConnsPerHost, 20) cmpInts(t, "http_client.idle_connection_timeout_seconds", cfg.Client.IdleConnTimeout, 30) + cmpInts(t, "http_client_cache.max_idle_connections", cfg.CacheClient.MaxIdleConns, 1) + cmpInts(t, "http_client_cache.max_idle_connections_per_host", cfg.CacheClient.MaxIdleConnsPerHost, 2) + cmpInts(t, "http_client_cache.idle_connection_timeout_seconds", cfg.CacheClient.IdleConnTimeout, 3) cmpInts(t, "gdpr.host_vendor_id", cfg.GDPR.HostVendorID, 15) cmpBools(t, "gdpr.usersync_if_ambiguous", cfg.GDPR.UsersyncIfAmbiguous, true) @@ -233,6 +242,7 @@ func TestFullConfig(t *testing.T) { cmpBools(t, "cfg.GDPR.NonStandardPublisherMap", found, false) cmpBools(t, "ccpa.enforce", cfg.CCPA.Enforce, true) + cmpBools(t, "lmt.enforce", cfg.LMT.Enforce, true) //Assert the NonStandardPublishers was correctly unmarshalled cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[0], "spamAppID") @@ -263,6 +273,8 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "adapters.audiencenetwork.usersync_url", cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].UserSyncURL, "http://facebook.com/ortb/prebid-s2s") cmpStrings(t, "adapters.audiencenetwork.platform_id", cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].PlatformID, "abcdefgh1234") cmpStrings(t, "adapters.audiencenetwork.app_secret", cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].AppSecret, "987abc") + cmpStrings(t, "adapters.beachfront.endpoint", cfg.Adapters[string(openrtb_ext.BidderBeachfront)].Endpoint, "https://display.bfmio.com/prebid_display") + cmpStrings(t, "adapters.beachfront.extra_info", cfg.Adapters[string(openrtb_ext.BidderBeachfront)].ExtraAdapterInfo, "{\"video_endpoint\":\"https://reachms.bfmio.com/bid.json?exchange_id\"}") cmpStrings(t, "adapters.ix.endpoint", cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderIx))].Endpoint, "http://ixtest.com/api") cmpStrings(t, "adapters.rubicon.endpoint", cfg.Adapters[string(openrtb_ext.BidderRubicon)].Endpoint, "http://rubitest.com/api") cmpStrings(t, "adapters.rubicon.usersync_url", cfg.Adapters[string(openrtb_ext.BidderRubicon)].UserSyncURL, "http://pixel.rubiconproject.com/sync.php?p=prebid") @@ -410,13 +422,21 @@ func TestCookieSizeError(t *testing.T) { } for i := range testCases { if testCases[i].expectError { - assert.Error(t, isValidCookieSize(testCases[i].cookieHost.MaxCookieSizeBytes), fmt.Sprintf("Configuration.HostCooki.MaxCookieSizeBytes less than MIN_COOKIE_SIZE_BYTES = %d and not equal to zero should return an error", MIN_COOKIE_SIZE_BYTES)) + assert.Error(t, isValidCookieSize(testCases[i].cookieHost.MaxCookieSizeBytes), fmt.Sprintf("Configuration.HostCookie.MaxCookieSizeBytes less than MIN_COOKIE_SIZE_BYTES = %d and not equal to zero should return an error", MIN_COOKIE_SIZE_BYTES)) } else { - assert.NoError(t, isValidCookieSize(testCases[i].cookieHost.MaxCookieSizeBytes), fmt.Sprintf("Configuration.HostCooki.MaxCookieSizeBytes greater than MIN_COOKIE_SIZE_BYTES = %d or equal to zero should not return an error", MIN_COOKIE_SIZE_BYTES)) + assert.NoError(t, isValidCookieSize(testCases[i].cookieHost.MaxCookieSizeBytes), fmt.Sprintf("Configuration.HostCookie.MaxCookieSizeBytes greater than MIN_COOKIE_SIZE_BYTES = %d or equal to zero should not return an error", MIN_COOKIE_SIZE_BYTES)) } } } +func TestValidateDebug(t *testing.T) { + cfg := newDefaultConfig(t) + cfg.Debug.TimeoutNotification.SamplingRate = 1.1 + + err := cfg.validate() + assert.NotNil(t, err, "cfg.debug.timeout_notification.sampling_rate should not be allowed to be greater than 1.0, but it was allowed") +} + func newDefaultConfig(t *testing.T) *Configuration { v := viper.New() SetupViper(v, "") diff --git a/config/stored_requests.go b/config/stored_requests.go index 0d9e773205e..04e400f9b7c 100644 --- a/config/stored_requests.go +++ b/config/stored_requests.go @@ -402,7 +402,7 @@ func (cfg *PostgresUpdatePolling) validate(errs configErrors) configErrors { return errs } -// MakeQuery builds a query which can fetch numReqs Stored Requetss and numImps Stored Imps. +// MakeQuery builds a query which can fetch numReqs Stored Requests and numImps Stored Imps. // See the docs on PostgresConfig.QueryTemplate for a description of how it works. func (cfg *PostgresFetcherQueriesSlim) MakeQuery(numReqs int, numImps int) (query string) { return resolve(cfg.QueryTemplate, numReqs, numImps) diff --git a/config/util/loggers.go b/config/util/loggers.go new file mode 100644 index 00000000000..88702e68763 --- /dev/null +++ b/config/util/loggers.go @@ -0,0 +1,24 @@ +package util + +import ( + "math/rand" +) + +type logMsg func(string, ...interface{}) + +type randomGenerator func() float32 + +// LogRandomSample will log a randam sample of the messages it is sent, based on the chance to log +// chance = 1.0 => always log, +// chance = 0.0 => never log +func LogRandomSample(msg string, logger logMsg, chance float32) { + logRandomSampleImpl(msg, logger, chance, rand.Float32) +} + +func logRandomSampleImpl(msg string, logger logMsg, chance float32, randGenerator randomGenerator) { + if chance < 1.0 && randGenerator() > chance { + // this is the chance we don't log anything + return + } + logger(msg) +} diff --git a/config/util/loggers_test.go b/config/util/loggers_test.go new file mode 100644 index 00000000000..4bfab967ec4 --- /dev/null +++ b/config/util/loggers_test.go @@ -0,0 +1,32 @@ +package util + +import ( + "bytes" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLogRandomSample(t *testing.T) { + + const expected string = `This is test line 2 +This is test line 3 +` + + myRand := rand.New(rand.NewSource(1337)) + var buf bytes.Buffer + + mylogger := func(msg string, args ...interface{}) { + buf.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...)) + } + + logRandomSampleImpl("This is test line 1", mylogger, 0.5, myRand.Float32) + logRandomSampleImpl("This is test line 2", mylogger, 0.5, myRand.Float32) + logRandomSampleImpl("This is test line 3", mylogger, 0.5, myRand.Float32) + logRandomSampleImpl("This is test line 4", mylogger, 0.5, myRand.Float32) + logRandomSampleImpl("This is test line 5", mylogger, 0.5, myRand.Float32) + + assert.EqualValues(t, expected, buf.String()) +} diff --git a/currencies/rate_converter.go b/currencies/rate_converter.go index 63f09bd3c2e..6c6ed172652 100644 --- a/currencies/rate_converter.go +++ b/currencies/rate_converter.go @@ -172,11 +172,15 @@ func (rc *RateConverter) Rates() Conversions { // GetInfo returns setup information about the converter func (rc *RateConverter) GetInfo() ConverterInfo { + var rates *map[string]map[string]float64 + if rc.Rates() != nil { + rates = rc.Rates().GetRates() + } return converterInfo{ source: rc.syncSourceURL, fetchingInterval: rc.fetchingInterval, lastUpdated: rc.LastUpdated(), - rates: rc.Rates().GetRates(), + rates: rates, } } diff --git a/currencies/rate_converter_test.go b/currencies/rate_converter_test.go index 63ccd035c0c..5c6b4821d8c 100644 --- a/currencies/rate_converter_test.go +++ b/currencies/rate_converter_test.go @@ -66,6 +66,7 @@ func TestFetch_Success(t *testing.T) { rates := currencyConverter.Rates() assert.NotNil(t, rates, "Rates() should not return nil") assert.Equal(t, expectedRates, rates, "Rates() doesn't return expected rates") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } func TestFetch_Fail404(t *testing.T) { @@ -92,6 +93,7 @@ func TestFetch_Fail404(t *testing.T) { assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs)) assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set") assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } func TestFetch_FailErrorHttpClient(t *testing.T) { @@ -118,6 +120,7 @@ func TestFetch_FailErrorHttpClient(t *testing.T) { assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs)) assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set") assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } func TestFetch_FailBadSyncURL(t *testing.T) { @@ -134,6 +137,7 @@ func TestFetch_FailBadSyncURL(t *testing.T) { // Verify: assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set") assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } func TestFetch_FailBadJSON(t *testing.T) { @@ -174,6 +178,7 @@ func TestFetch_FailBadJSON(t *testing.T) { assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs)) assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set") assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } func TestFetch_InvalidRemoteResponseContent(t *testing.T) { @@ -201,6 +206,7 @@ func TestFetch_InvalidRemoteResponseContent(t *testing.T) { assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs)) assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set") assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } func TestInit(t *testing.T) { @@ -264,6 +270,7 @@ func TestInit(t *testing.T) { assert.NotEqual(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated should be set") rates := currencyConverter.Rates() assert.Equal(t, expectedRates, rates, "Conversions.Rates weren't the expected ones") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") if ticksCount == expectedTicks { currencyConverter.StopPeriodicFetching() @@ -361,6 +368,7 @@ func TestInitWithZeroDuration(t *testing.T) { assert.Equal(t, (time.Time{}), currencyConverter.LastUpdated(), "LastUpdated() shouldn't be set") _, ok := currencyConverter.Rates().(*currencies.ConstantRates) assert.True(t, ok, "Rates should be type of `currencies.ConstantRates`") + assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } func TestRates(t *testing.T) { diff --git a/docs/bidders/adtarget.md b/docs/bidders/adtarget.md new file mode 100644 index 00000000000..b658a728a2b --- /dev/null +++ b/docs/bidders/adtarget.md @@ -0,0 +1,5 @@ +# Adtarget bidder + +To use the Adtarget bidder you will need an aid from an exchange account on [https://console.adtarget.com.tr](adtarget.com.tr). + +For further information, please contact kamil@adtarget.com.tr \ No newline at end of file diff --git a/docs/bidders/appnexus.md b/docs/bidders/appnexus.md index 8b706adc122..e4032313f25 100644 --- a/docs/bidders/appnexus.md +++ b/docs/bidders/appnexus.md @@ -15,7 +15,7 @@ The AppNexus endpoint expects `imp.displaymanagerver` to be populated for mobile requests, however not all SDKs will populate this field. If the `imp.displaymanagerver` field is not supplied for an `imp`, but `request.app.ext.prebid.source` and `request.app.ext.prebid.version` are supplied, the adapter will fill in a value for -`diplaymanagerver`. It will concatonate the two `app` fields as `-` fo fill in +`diplaymanagerver`. It will concatenate the two `app` fields as `-` fo fill in the empty `displaymanagerver` before sending the request to AppNexus. ## Test Request diff --git a/docs/bidders/audienceNetwork.md b/docs/bidders/audienceNetwork.md index 04357d616b1..d55e8218a81 100644 --- a/docs/bidders/audienceNetwork.md +++ b/docs/bidders/audienceNetwork.md @@ -3,6 +3,6 @@ ## Mobile Bids Audience Network will not bid on requests made from device simulators. -When testingfor Mobile bids, you must make bid requests using a real device. +When testing for Mobile bids, you must make bid requests using a real device. **Note:** Audience Network is disabled by default. Please enable it in the app config if you wish to use it. Make sure you provide the partnerID for the auctions to run correctly. \ No newline at end of file diff --git a/docs/bidders/avocet.md b/docs/bidders/avocet.md new file mode 100644 index 00000000000..6aa67391af4 --- /dev/null +++ b/docs/bidders/avocet.md @@ -0,0 +1,5 @@ +# Avocet Bidder + +Please contact Avocet at info@avocet.io if you would like to get started selling inventory via the Avocet platform. + +**Note:** Avocet is disabled by default. Please enable it in the app config if you wish to use it. This can be done by setting `adapters.avocet.disabled` to `false` and by setting `adapters.avocet.endpoint` to a valid Avocet endpoint url. \ No newline at end of file diff --git a/docs/bidders/beachfront.md b/docs/bidders/beachfront.md index bb51db41081..bde7049a185 100644 --- a/docs/bidders/beachfront.md +++ b/docs/bidders/beachfront.md @@ -1,7 +1,13 @@ # Beachfront bidder -To use the beachfront bidder you will need an appId from an exchange -account on [https://platform.beachfront.io](platform.beachfront.io). +To use the beachfront bidder you will need an appId (Exchange Id) from an exchange +account on [platform.beachfront.io](https://platform.beachfront.io). For further information, please contact adops@beachfront.com. +As seen in the JSON response from \{your PBS server\}\/bidder\/params [(example)](https://prebid.adnxs.com/pbs/v1/bidders/params), the beachfront bidder can take either an "appId" parameter, or an "appIds" parameter. If the request is for one media type, the appId parameter should be used with the value of the Exchange Id on the Beachfront platform. + +The appIds parameter is for requesting a mix of banner and video. It has two parameters, "banner", and "video" for the appIds of two appropriately configured exchanges on the platform. The appIds parameter can be sent with just one of its two parameters and it will behave like the appId parameter. + +If the request includes an appId configured for a video response, the videoResponseType parameter can be defined as "nurl", "adm" or "both". These will apply to all video returned. If it is not defined, the response type will be a nurl. The definitions for "nurl" vs. "adm" are here: (https://github.com/PubMatic-OpenWrap/openrtb/blob/master/openrtb2/bid.go). + diff --git a/docs/bidders/kidoz.md b/docs/bidders/kidoz.md new file mode 100644 index 00000000000..433dd71c2ca --- /dev/null +++ b/docs/bidders/kidoz.md @@ -0,0 +1,9 @@ +# Kidoz Bidder + +Kidoz is exclusively for Mobile app COPPA compatible ads, 100% kid relevant and appropriate. + +In order for a company to receive bids from Kidoz, they must first open a publisher account at Kidoz.net +(https://accounts.kidoz.net/publishers/register) and accept the Kidoz Terms and Conditions and Privacy Policy. +Kidoz publishers must confirm that all of their content properties are COPPA and GDPR compliant and perform no monitoring +or tracking of U13 users in their operations. New publishers are provided a Publisher ID and AccessToken, this can also +be used to login to their dashboard at the Kidoz.net portal to monitor their account activity. diff --git a/docs/bidders/openx.md b/docs/bidders/openx.md new file mode 100644 index 00000000000..c366db3ab61 --- /dev/null +++ b/docs/bidders/openx.md @@ -0,0 +1,62 @@ +# OpenX Bidder + +OpenX supports the following parameters: + +| property | type | required? | description | example | +|----------|------|-----------|-------------|---------| +| unit | string | required | The ad unit id | "10092842" | +| delDomain | string | required | The delivery domain for the customer | "sademo-d.openx.net" | +| customFloor | number | optional | The minimum CPM price in USD | 1.50 - sets a $1.50 floor | +| customParams | object | optional | User-defined targeting key-value pairs | {key1: "v1", key2: ["v2","v3"]} | + +If you have any questions regarding setting up, please reach out to your account manager or + + +## Test Request + +### App Impression Object +``` +{ + "id": "test-impression-id", + "banner": { + "format": [ + { + "w": 480, + "h": 300 + }, + { + "w": 480, + "h": 320 + } + ] + }, + "ext": { + "openx": { + "delDomain": "mobile-d.openx.net", + "unit": "541028953" + } + } +} +``` + + +### Web +``` +{ + "id": "div1", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "openx": { + "unit": "540949380", + "delDomain": "sademo-d.openx.net" + }, + } +} +``` \ No newline at end of file diff --git a/docs/bidders/pubmatic.md b/docs/bidders/pubmatic.md new file mode 100644 index 00000000000..610108b2e07 --- /dev/null +++ b/docs/bidders/pubmatic.md @@ -0,0 +1,33 @@ +# PubMatic Bidder + +## Test Request + +The following test parameters can be used to verify that Prebid Server is working properly with the +PubMatic adapter. This example includes an `imp` object with an PubMatic test publisher ID, ad slot, +and sizes that would match with the test creative. + +``` +"imp":[ + { + "id":“"some-impression-id”, + "banner":{ + "format":[ + { + "w":300, + "h":250 + }, + { + "w":300, + "h":600 + } + ] + }, + "ext":{ + "pubmatic":{ + "publisherId":“156276”, + "adSlot":"pubmatic_test" + } + } + } + ] +``` \ No newline at end of file diff --git a/docs/bidders/pubnative.md b/docs/bidders/pubnative.md new file mode 100644 index 00000000000..a25cafe0cd5 --- /dev/null +++ b/docs/bidders/pubnative.md @@ -0,0 +1,62 @@ +# Pubnative Bidder + +## Prerequisite +Before adding PubNative as a new bidder, there are 3 prerequisites: +- As a Publisher, you need to have Prebid Mobile SDK integrated. +- You need a configured Prebid Server (either self-hosted or hosted by 3rd party). +- You need to be integrated with Ad Server SDK (e.g. Mopub) or internal product which communicates with Prebid Mobile SDK. + +Please see [documentation](https://developers.pubnative.net/docs/prebid-adding-pubnative-as-a-bidder) for more info. + +## Configuration + +- bidder should be always set to "pubnative" (`imp.ext.pubnative`) +- zone_id (int) should be always set to 1, unless special use case agreed with our account manager. (`imp.ext.pubnative.zone_id`) +- app_auth_token (string) is unique per publisher app. Please contact our account manager to obtain yours. (`imp.ext.pubnative.app_auth_token`) + +An example is illustrated in a section below. + +## Testing + +Please consult with our Account Manager for testing. +We need to confirm that your ad request is correctly received by our system. + +The following test parameters can be used to verify that Prebid Server is working properly with the +Pubnative adapter. + +The following json can be used to do a request to prebid server for verifying its integration with Pubnative adapter. + +```json +{ + "id": "some-impression-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "pubnative": { + "zone_id": 1, + "app_auth_token": "b620e282f3c74787beedda34336a4821" + } + } + } + ], + "device": { + "os": "android", + "h": 700, + "w": 375 + }, + "tmax": 500, + "test": 1 +} +``` \ No newline at end of file diff --git a/docs/bidders/rubicon.md b/docs/bidders/rubicon.md index 8136e2da405..ea376da427d 100644 --- a/docs/bidders/rubicon.md +++ b/docs/bidders/rubicon.md @@ -1,7 +1,7 @@ # Rubicon Bidder -Please contact your Rubicon Project account manager to get set up with a login and cookie-sync URL to run your own Prebid Server. You will be given instructions, including the available endpoints. +Please contact your Rubicon Project account manager or globalsupport@rubiconproject.com to get set up with a login and cookie-sync URL to run your own Prebid Server. You will be given instructions, including the available endpoints. **Note:** Rubicon is disabled by default. Please enable it in the app config if you wish to use it. Make sure you provide the correct cookie-sync URL in order for cookie-syncs to work properly. -[Rubicon Project Prebid.js test parameters](https://github.com/PubMatic-OpenWrap/Prebid.js/blob/master/modules/rubiconBidAdapter.md) will work for server as well. +[Rubicon Project Prebid.js test parameters](https://github.com/prebid/Prebid.js/blob/master/modules/rubiconBidAdapter.md) will work for server as well. diff --git a/docs/bidders/smartrtb.md b/docs/bidders/smartrtb.md new file mode 100644 index 00000000000..ffa88f663e8 --- /dev/null +++ b/docs/bidders/smartrtb.md @@ -0,0 +1,39 @@ +# SmartRTB Bidder + +[SmartRTB](https://smrtb.com/) supports the following parameters to be present in the `ext` object of impression requests: + +- "pub_id" type string - Required. Publisher ID assigned to you. +- "zone_id" type string - Optional. Enables mapping for further settings and reporting in the Marketplace UI. +- "force_bid" type bool - Optional. If zone ID is mapped, this may be set to always return fake sample bids (banner, video) + +Please contact us to create a new Smart RTB Marketplace account, and for any assistance in configuration. +You may email info@smrtb.com for inquiries. + +## Test Request + +This sample request is our global test placement and should always return a branded banner bid. + +``` + { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "test", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "smartrtb": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + }] + } +``` diff --git a/docs/bidders/sovrn.md b/docs/bidders/sovrn.md index 544cb8a6764..bc6d42333e8 100644 --- a/docs/bidders/sovrn.md +++ b/docs/bidders/sovrn.md @@ -1,3 +1,3 @@ Sovrn supports 2 parameters to be present in the `ext` object of impressions sent to it: - tagid: a string containing the sovrn-specific id(s) for the publisher's ad tag(s) they would like to bid with. This is a required field -- bidfloor: The minimium acceptable bid, in CPM, using US Dollars. This is an optional field. \ No newline at end of file +- bidfloor: The minimum acceptable bid, in CPM, using US Dollars. This is an optional field. \ No newline at end of file diff --git a/docs/developers/add-new-bidder.md b/docs/developers/add-new-bidder.md index e68185fdd1c..d76a1fd2fbf 100644 --- a/docs/developers/add-new-bidder.md +++ b/docs/developers/add-new-bidder.md @@ -46,6 +46,16 @@ If bidder is going to support long form video make sure bidder has: Note: `bid.bidVideo.PrimaryCategory` or `TypedBid.bid.Cat` should be specified. To learn more about IAB categories, please refer to this convenience link (not the final official definition): [IAB categories](https://adtagmacros.com/list-of-iab-categories-for-advertisement/) +### Timeout notification support +This is an optional feature. If you wish to get timeout notifications when a bid request from PBS times out, you can implement the +`MakeTimeoutNotification` method in your adapter. If you do not wish timeout notification, do not implement the method. + +`func (a *Adapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error)` + +Here the `RequestData` supplied as an argument is the request returned from `MakeRequests` that timed out. If an adapter generates +multiple requests, and more than one of them times out, then there will be a call to `MakeTimeoutNotification` for each failed +request. The function should then return a `RequestData` object that will be the timeout notification to be sent to the bidder, or a list of errors encountered trying to create the timeout notification request. Timeout notifications will not generate subsequent timeout notifications if they timeout or fail. + ## Test Your Bidder ### Automated Tests diff --git a/docs/developers/automated-tests.md b/docs/developers/automated-tests.md index 7e09fb85f34..93bd28f6187 100644 --- a/docs/developers/automated-tests.md +++ b/docs/developers/automated-tests.md @@ -9,7 +9,7 @@ To reproduce these tests locally, use: ## Writing Tests -Tests for `some-file.go` should be placed in the file `some-file_test.go` in the same paackage. +Tests for `some-file.go` should be placed in the file `some-file_test.go` in the same package. For more info on how to write tests in Go, see [the Go docs](https://golang.org/pkg/testing/). ## Adapter Tests diff --git a/docs/developers/cookie-syncs.md b/docs/developers/cookie-syncs.md index 36c6b85b636..75a3e3b0ef8 100644 --- a/docs/developers/cookie-syncs.md +++ b/docs/developers/cookie-syncs.md @@ -1,6 +1,6 @@ # Cookie Sync Technical Details -This document describes the mechancis of a Prebid Server cookie sync. +This document describes the mechanics of a Prebid Server cookie sync. ## Motivation diff --git a/docs/developers/default-request.md b/docs/developers/default-request.md index 2337ccd8da0..f071d91bad6 100644 --- a/docs/developers/default-request.md +++ b/docs/developers/default-request.md @@ -1,6 +1,6 @@ # Server Based Global Default Request -This allows a defaut stored request to be defined that allows the server to set up some defaults for all incoming requests. A request specified stored request will override these defaults, and of course any options specified directly in the stored request override both. The default stored request is only read on server startup, it is meant as an installation static default rather than a dynamic tuning option. +This allows a default stored request to be defined that allows the server to set up some defaults for all incoming requests. A request specified stored request will override these defaults, and of course any options specified directly in the stored request override both. The default stored request is only read on server startup, it is meant as an installation static default rather than a dynamic tuning option. A common use case is to "hard code" aliases into the server. This saves having to specify them on all incoming requests, and/or on all stored requests. To help support automation and alias discovery we can flag that any aliases found in the file be added to the bidder info endpoints. @@ -35,8 +35,8 @@ The `filename` option is the path/filename of a JSON file containing the default ``` This will be JSON merged into the incoming requests at the top level. These will be used as fallbacks which can be overridden by both Stored Requests _and_ the incoming HTTP request payload. -The `info` option determines if the alised bidders will be exposed on the `/info` endpoints. If true the alias name will be added to the list returned by -`/info/bidders` and the info JSON for the core bidder will be coppied into `/info/bidder/{biddername}` with the addition of the field +The `info` option determines if the aliased bidders will be exposed on the `/info` endpoints. If true the alias name will be added to the list returned by +`/info/bidders` and the info JSON for the core bidder will be copied into `/info/bidder/{biddername}` with the addition of the field `"alias_of": "{coreBidder}"` to indicate that it is an aliases, and of which core bidder. Turning the info support on may be useful for hosts that want to support automation around the `/info` endpoints that will include the predefined aliases. This config option may be deprecated in a future version to promote a consistency in the endpoint functionality, depending on the perceived need for the option. diff --git a/docs/endpoints/openrtb2/amp.md b/docs/endpoints/openrtb2/amp.md index b792ae6ec5d..16fa451ef36 100644 --- a/docs/endpoints/openrtb2/amp.md +++ b/docs/endpoints/openrtb2/amp.md @@ -100,7 +100,7 @@ This endpoint supports the following query parameters: 6. `curl` - the canonical URL of the page 7. `timeout` - the publisher-specified timeout for the RTC callout - A configuration option `amp_timeout_adjustment_ms` may be set to account for estimated latency so that Prebid Server can handle timeouts from adapters and respond to the AMP RTC request before it times out. -8. `debug` - When set to `1`, the respones will contain extra info for debugging. +8. `debug` - When set to `1`, the response will contain extra info for debugging. For information on how these get from AMP into this endpoint, see [this pull request adding the query params to the Prebid callout](https://github.com/ampproject/amphtml/pull/14155) and [this issue adding support for network-level RTC macros](https://github.com/ampproject/amphtml/issues/12374). diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 12c1b94ec1c..c8cce0338eb 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -14,53 +14,94 @@ This endpoint runs an auction with the given OpenRTB 2.5 bid request. ### Sample request -The [Prebid sample ad](http://prebid.org/examples/pbjs_demo.html) can be loaded with the request sample [here](../../../endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json). +This is a sample OpenRTB 2.5 bid request for a Xandr (formerly AppNexus) test placement. Please note, the Xandr Ad Server will only +respond with a bid if the "test" field is set to 1. -Other examples can be found in [endpoints/openrtb2/sample-requests/valid-whole/exemplary](../../../endpoints/openrtb2/sample-requests/valid-whole/exemplary). +``` +{ + "id": "some-request-id", + "test": 1, + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "some-impression-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + }], + "tmax": 500 +} +``` + +Additional examples can be found in [endpoints/openrtb2/sample-requests/valid-whole](../../../endpoints/openrtb2/sample-requests/valid-whole). ### Sample Response This endpoint will respond with either: -- An OpenRTB 2.5 BidResponse, or -- An HTTP 400 status code if the request is malformed +- An OpenRTB 2.5 bid response, or +- HTTP 400 if the request is malformed, or +- HTTP 503 if the account or app specified in the request is blacklisted -A "hello world" response from the prebid sample ad request is shown below. +This is the corresponding response to the above sample OpenRTB 2.5 bid request, with the `ext.debug` field removed and the `seatbid.bid.adm` field simplified. ``` { "id": "some-request-id", - "seatbid": [ - { - "seat": "appnexus" - "bid": [ - { - "id": "4625436751433509010", - "impid": "some-impression-id", - "price": 0.5, - "adm": "", - "adid": "29681110", - "adomain": [ - "appnexus.com" - ], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250, - "ext": { - "bidder": { - "appnexus": { - "brand_id": 1, - "auction_id": 6127490747252133000, - "bidder_id": 2 - } - } + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "145556724130495288", + "impid": "some-impression-id", + "price": 0.01, + "adm": "", + "adid": "107987536", + "adomain": [ + "appnexus.com" + ], + "iurl": "https://nym1-ib.adnxs.com/cr?id=107987536", + "cid": "3532", + "crid": "107987536", + "w": 600, + "h": 500, + "ext": { + "prebid": { + "type": "banner", + "video": { + "duration": 0, + "primary_category": "" + } + }, + "bidder": { + "appnexus": { + "brand_id": 1, + "auction_id": 7311907164510136364, + "bidder_id": 2, + "bid_ad_type": 0 } } - ] - } - ] + } + }] + }], + "cur": "USD", + "ext": { + "responsetimemillis": { + "appnexus": 10 + }, + "tmaxrequest": 500 + } } ``` @@ -69,12 +110,12 @@ A "hello world" response from the prebid sample ad request is shown below. #### Conventions OpenRTB 2.5 permits exchanges to define their own extensions to any object from the spec. -These fall under the `ext` property of JSON objects. +These fall under the `ext` field of JSON objects. If `ext` is defined on an object, Prebid Server uses the following conventions: -1. `ext` in "Request objects" uses `ext.prebid` and/or `ext.{anyBidderCode}`. -2. `ext` on "Response objects" uses `ext.prebid` and/or `ext.bidder`. +1. `ext` in "request objects" uses `ext.prebid` and/or `ext.{anyBidderCode}`. +2. `ext` on "response objects" uses `ext.prebid` and/or `ext.bidder`. The only exception here is the top-level `BidResponse`, because it's bidder-independent. `ext.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders. @@ -84,9 +125,9 @@ Exceptions are made for extensions with "standard" recommendations: - `request.user.ext.digitrust` -- To support Digitrust - `request.regs.ext.gdpr` and `request.user.ext.consent` -- To support GDPR +- `request.regs.us_privacy` -- To support CCPA - `request.site.ext.amp` -- To identify AMP as the request source - `request.app.ext.source` and `request.app.ext.version` -- To support identifying the displaymanager/SDK in mobile apps. If given, we expect these to be strings. -- `request.regs.coppa` -- to support COPPA #### Bid Adjustments @@ -95,8 +136,14 @@ If you find that some bidders use Gross bids, publishers can adjust for it with ``` { - "appnexus: 0.8, - "rubicon": 0.7 + "ext": { + "prebid": { + "bidadjustmentfactors": { + "appnexus": 0.8, + "rubicon": 0.7 + } + } + } } ``` @@ -114,17 +161,21 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta ``` { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max":20.00, - "increment":0.10 // This is equivalent to the deprecated "pricegranularity": "medium" - } - ] - }, - "includewinners": false // Optional param defaulting to true - "includebidderkeys": false // Optional param defaulting to true + "ext": { + "prebid": { + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [{ + "max": 20.00, + "increment": 0.10 // This is equivalent to the deprecated "pricegranularity": "medium" + }] + }, + "includewinners": false, // Optional param defaulting to true + "includebidderkeys": false // Optional param defaulting to true + } + } + } } ``` The list of price granularity ranges must be given in order of increasing `max` values. If `precision` is omitted, it will default to `2`. The minimum of a range will be 0 or the previous `max`. Any cmp above the largest `max` will go in the `max` pricebucket. @@ -136,32 +187,49 @@ One of "includewinners" or "includebidderkeys" must be true (both default to tru MediaType PriceGranularity (PBS-Java only) - when a single OpenRTB request contains multiple impressions with different mediatypes, or a single impression supports multiple formats, the different mediatypes may need different price granularities. If `mediatypepricegranularity` is present, `pricegranularity` would only be used for any mediatypes not specified. ``` - "ext": { - "prebid": { - "targeting": { - "mediatypepricegranularity": { - "banner": { "ranges": [ - {"max": 20, "increment": 0.5} - ]}, - "video": { "ranges": [ - {"max": 10, "increment": 1}, - {"max": 20, "increment": 2}, - {"max": 50, "increment": 5} - ]} - } - } - "includewinners": true - } - } +{ + "ext": { + "prebid": { + "targeting": { + "mediatypepricegranularity": { + "banner": { + "ranges": [ + {"max": 20, "increment": 0.5} + ] + }, + "video": { + "ranges": [ + {"max": 10, "increment": 1}, + {"max": 20, "increment": 2}, + {"max": 50, "increment": 5} + ] + } + } + }, + "includewinners": true + } + } +} ``` **Response format** (returned in `bid.ext.prebid.targeting`) ``` { - "hb_bidder_{bidderName}": "The seatbid.seat which contains this bid", - "hb_size_{bidderName}": "A string like '300x250' using bid.w and bid.h for this bid", - "hb_pb_{bidderName}": "The bid.cpm, rounded down based on the price granularity." + "seatbid": [{ + "bid": [{ + ... + "ext": { + "prebid": { + "targeting": { + "hb_bidder_{bidderName}": "The seatbid.seat which contains this bid", + "hb_size_{bidderName}": "A string like '300x250' using bid.w and bid.h for this bid", + "hb_pb_{bidderName}": "The bid.cpm, rounded down based on the price granularity." + } + } + } + }] + }] } ``` @@ -174,7 +242,7 @@ will be truncated to only include the first 20 characters. #### Cookie syncs Each Bidder should receive their own ID in the `request.user.buyeruid` property. -Prebid Server has three ways to popualte this field. In order of priority: +Prebid Server has three ways to populate this field. In order of priority: 1. If the request payload contains `request.user.buyeruid`, then that value will be sent to all Bidders. In most cases, this is probably a bad idea. @@ -183,8 +251,16 @@ In most cases, this is probably a bad idea. ``` { - "appnexus": "some-appnexus-id", - "rubicon": "some-rubicon-id" + "user": { + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "some-appnexus-id", + "rubicon": "some-rubicon-id" + } + } + } + } } ``` @@ -199,7 +275,7 @@ for each Bidder by using the `/cookie_sync` endpoint, and calling the URLs that #### Native Request -For each native request, the `assets` objects's `id` field must not be defined. Prebid Server will set this automatically, using the index of the asset in the array as the ID. +For each native request, the `assets` object's `id` field must not be defined. Prebid Server will set this automatically, using the index of the asset in the array as the ID. #### Bidder Aliases @@ -209,22 +285,20 @@ This can be used to request bids from the same Bidder with different params. For ``` { - "imp": [ - { - "id": "some-impression-id", - "video": { - "mimes": ["video/mp4"] + "imp": [{ + "id": "some-impression-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 123 }, - "ext": { - "appnexus: { - "placementId": 123 - }, - "districtm": { - "placementId": 456 - } + "districtm": { + "placementId": 456 } } - ], + }], "ext": { "prebid": { "aliases": { @@ -236,7 +310,7 @@ This can be used to request bids from the same Bidder with different params. For ``` For all intents and purposes, the alias will be treated as another Bidder. This new Bidder will behave exactly -like the original, except that the Response will contain seprate SeatBids, and any Targeting keys +like the original, except that the Response will contain separate SeatBids, and any Targeting keys will be formed using the alias' name. If an alias overlaps with a core Bidder's name, then the alias will take precedence. @@ -245,15 +319,13 @@ This prevents breaking API changes as new Bidders are added to the project. For example, if the Request defines an alias like this: ``` -{ "aliases": { "appnexus": "rubicon" } -} ``` then any `imp.ext.appnexus` params will actually go to the **rubicon** adapter. -It will become impossible to fetch bids from Appnexus within that Request. +It will become impossible to fetch bids from AppNexus within that Request. #### Bidder Response Times @@ -273,19 +345,17 @@ For example, a request may return this in `response.ext` ``` { - "errors": { - "appnexus": [ - { + "ext": { + "errors": { + "appnexus": [{ "code": 2, "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio." - } - ], - "rubicon": [ - { + }], + "rubicon": [{ "code": 1, "message": "The request exceeded the timeout allocated" - } - ] + }] + } } } ``` @@ -319,7 +389,15 @@ A typical `storedrequest` value looks like this: ``` { - "id": "some-id" + "imp": [{ + "ext": { + "prebid": { + "storedrequest": { + "id": "some-id" + } + } + } + }] } ``` @@ -331,12 +409,18 @@ Bids can be temporarily cached on the server by sending the following data as `r ``` { - "bids": {}, - "vastxml": {} + "ext": { + "prebid": { + "cache": { + "bids": {}, + "vastxml": {} + } + } + } } ``` -Both `bids` and `vastxml` are optional, but one of the two is required. Thils property will have no effect +Both `bids` and `vastxml` are optional, but one of the two is required if you want to cache bids. This property will have no effect unless `request.ext.prebid.targeting` is also set in the request. If `bids` is present, Prebid Server will make a _best effort_ to include these extra @@ -374,16 +458,14 @@ The values will be numbers that indicate the minimum allowed size for the ad, as Example: ``` { - "imp": [ - { - ... - "banner": { - ... - } - "instl": 1, + "imp": [{ + ... + "banner": { ... } - ] + "instl": 1, + ... + }] "device": { ... "h": 640, @@ -403,12 +485,60 @@ Example: PBS receiving a request for an interstitial imp and these parameters set, it will rewrite the format object within the interstitial imp. If the format array's first object is a size, PBS will take it as the max size for the interstitial. If that size is 1x1, it will look up the device's size and use that as the max size. If the format is not present, it will also use the device size as the max size. (1x1 support so that you don't have to omit the format object to use the device size) PBS with interstitial support will come preconfigured with a list of common ad sizes. Preferentially organized by weighing the larger and more common sizes first. But no guarantees to the ordering will be made. PBS will generate a new format list for the interstitial imp by traversing this list and picking the first 10 sizes that fall within the imp's max size and minimum percentage size. There will be no attempt to favor aspect ratios closer to the original size's aspect ratio. The limit of 10 is enforced to ensure we don't overload bidders with an overlong list. All the interstitial parameters will still be passed to the bidders, so they may recognize them and use their own size matching algorithms if they prefer. +#### Currency Support + +To set the desired 'ad server currency', use the standard OpenRTB `cur` attribute. Note that Prebid Server only looks at the first currency in the array. + +``` + "cur": ["USD"] +``` + +If you want or need to define currency conversion rates (e.g. for currencies that your Prebid Server doesn't support), +define ext.prebid.currency.rates. (Currently supported in PBS-Java only) + +``` +"ext": { + "prebid": { + "currency": { + "rates": { + "USD": { "UAH": 24.47, "ETB": 32.04 } + } + } + } +} +``` + +If it exists, a rate defined in ext.prebid.currency.rates has the highest priority. +If a currency rate doesn't exist in the request, the external file will be used. + +#### Supply Chain Support + + +Basic supply chains are passed to Prebid Server on `source.ext.schain` and passed through to bid adapters. Prebid Server does not currently offer the ability to add a node to the supply chain. + +Bidder-specific schains (PBS-Java only): + +``` +ext.prebid.schains: [ + { bidders: ["bidderA"], schain: { SCHAIN OBJECT 1}}, + { bidders: ["*"], schain: { SCHAIN OBJECT 2}} +] +``` +In this scenario, Prebid Server sends the first schain object to `bidderA` and the second schain object to everyone else. + +If there's already an source.ext.schain and a bidder is named in ext.prebid.schains (or covered by the wildcard condition), ext.prebid.schains takes precedent. + +#### Rewarded Video (PBS-Java only) + +Rewarded video is a way to incentivize users to watch ads by giving them 'points' for viewing an ad. A Prebid Server +client can declare a given adunit as eligible for rewards by declaring `imp.ext.prebid.is_rewarded_inventory:1`. + #### Stored Responses (PBS-Java only) While testing SDK and video integrations, it's important, but often difficult, to get consistent responses back from bidders that cover a range of scenarios like different CPM values, deals, etc. Prebid Server supports a debugging workflow in two ways: - a stored-auction-response that covers multiple bidder responses -- multiple stored-bid-reponses at the bidder adapter level +- multiple stored-bid-responses at the bidder adapter level **Single Stored Auction Response ID** @@ -567,33 +697,33 @@ It specifies where in the OpenRTB request non-standard attributes should be pass ``` { - ext: { - prebid: { - data: { bidders: [ 'rubicon', 'appnexus' ] } // these are the bidders allowed to see protected data + "ext": { + "prebid": { + "data": { "bidders": [ "rubicon", "appnexus" ] } // these are the bidders allowed to see protected data } }, - site: { - keywords: "", - search: "", - ext: { + "site": { + "keywords": "", + "search": "", + "ext": { data: { GLOBAL CONTEXT DATA } // only seen by bidders named in ext.prebid.data.bidders[] } }, - user: { - keywords: "", - gender: "", - yob: 1999, - geo: {}, - ext: { + "user": { + "keywords": "", + "gender": "", + "yob": 1999, + "geo": {}, + "ext": { data: { GLOBAL USER DATA } // only seen by bidders named in ext.prebid.data.bidders[] } }, - imp: [ - ext: { - context: { - keywords: "", - search: "", - data: { ADUNIT SPECFIC CONTEXT DATA } // can be seen by all bidders + "imp": [ + "ext": { + "context": { + "keywords": "", + "search": "", + "data": { ADUNIT SPECFIC CONTEXT DATA } // can be seen by all bidders } } ] diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index ad385b40fd9..9c3b9878efa 100644 --- a/endpoints/auction_test.go +++ b/endpoints/auction_test.go @@ -407,6 +407,7 @@ type auctionMockPermissions struct { allowBidderSync bool allowHostCookies bool allowPI bool + allowGeo bool } func (m *auctionMockPermissions) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { @@ -417,8 +418,12 @@ func (m *auctionMockPermissions) BidderSyncAllowed(ctx context.Context, bidder o return m.allowBidderSync, nil } -func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return m.allowPI, nil +func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return m.allowPI, m.allowGeo, nil +} + +func (m *auctionMockPermissions) AMPException() bool { + return false } func TestBidSizeValidate(t *testing.T) { diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 26ab5f85f18..eef441b854f 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -377,8 +377,12 @@ func (g *gdprPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi return ok, nil } -func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return true, nil +func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return true, true, nil +} + +func (g *gdprPerms) AMPException() bool { + return false } func TestSetSecureParam(t *testing.T) { diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 8b3f654f0b9..cb36528417b 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -20,8 +20,6 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/privacy" - "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" - "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/empty_fetcher" "github.com/PubMatic-OpenWrap/prebid-server/usersync" @@ -36,6 +34,7 @@ type AmpResponse struct { Targeting map[string]string `json:"targeting"` Debug *openrtb_ext.ExtResponseDebug `json:"debug,omitempty"` Errors map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError `json:"errors,omitempty"` + Warnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError `json:"warnings,omitempty"` } // NewAmpEndpoint modifies the OpenRTB endpoint to handle AMP requests. This will basically modify the parsing @@ -71,7 +70,9 @@ func NewAmpEndpoint( disabledBidders, defRequest, defReqJSON, - bidderMap}).AmpAuction), nil + bidderMap, + nil, + nil}).AmpAuction), nil } @@ -120,13 +121,13 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin") req, errL := deps.parseAmpRequest(r) + ao.Errors = append(ao.Errors, errL...) - if fatalError(errL) { + if errortypes.ContainsFatalError(errL) { w.WriteHeader(http.StatusBadRequest) - for _, err := range errL { + for _, err := range errortypes.FatalOnly(errL) { w.Write([]byte(fmt.Sprintf("Invalid request format: %s\n", err.Error()))) } - ao.Errors = append(ao.Errors, errL...) labels.RequestStatus = pbsmetrics.RequestStatusBadInput return } @@ -150,22 +151,22 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h // Blacklist account now that we have resolved the value if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { errL = append(errL, acctIdErr) - erVal := errortypes.DecodeError(acctIdErr) - if erVal == errortypes.BlacklistedAppCode || erVal == errortypes.BlacklistedAcctCode { + errCode := errortypes.ReadCode(acctIdErr) + if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.BlacklistedAcctErrorCode { w.WriteHeader(http.StatusServiceUnavailable) labels.RequestStatus = pbsmetrics.RequestStatusBlacklisted - } else { //erVal == errortypes.AcctRequiredCode + } else { w.WriteHeader(http.StatusBadRequest) labels.RequestStatus = pbsmetrics.RequestStatusBadInput } - for _, err := range errL { + for _, err := range errortypes.FatalOnly(errL) { w.Write([]byte(fmt.Sprintf("Invalid request format: %s\n", err.Error()))) } - ao.Errors = append(ao.Errors, errL...) + ao.Errors = append(ao.Errors, acctIdErr) return } - response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories) + response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories, nil) ao.AuctionResponse = response if err != nil { @@ -205,6 +206,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h } } } + // Extract any errors var extResponse openrtb_ext.ExtBidResponse eRErr := json.Unmarshal(response.Ext, &extResponse) @@ -212,10 +214,20 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h ao.Errors = append(ao.Errors, fmt.Errorf("AMP response: failed to unpack OpenRTB response.ext, debug info cannot be forwarded: %v", eRErr)) } + warnings := make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError) + for _, v := range errortypes.WarningOnly(errL) { + bidderErr := openrtb_ext.ExtBidderError{ + Code: errortypes.ReadCode(v), + Message: v.Error(), + } + warnings[openrtb_ext.BidderNameGeneral] = append(warnings[openrtb_ext.BidderNameGeneral], bidderErr) + } + // Now JSONify the targets for the AMP response. ampResponse := AmpResponse{ Targeting: targets, Errors: extResponse.Errors, + Warnings: warnings, } ao.AmpTargetingValues = targets @@ -251,8 +263,8 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h // If the errors list has at least one element, then no guarantees are made about the returned request. func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openrtb.BidRequest, errs []error) { // Load the stored request for the AMP ID. - req, errs = deps.loadRequestJSONForAmp(httpRequest) - if len(errs) > 0 { + req, e := deps.loadRequestJSONForAmp(httpRequest) + if errs = append(errs, e...); errortypes.ContainsFatalError(errs) { return } @@ -260,18 +272,15 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr deps.setFieldsImplicitly(httpRequest, req) // Need to ensure cache and targeting are turned on - errs = defaultRequestExt(req) - if len(errs) > 0 { + e = defaultRequestExt(req) + if errs = append(errs, e...); errortypes.ContainsFatalError(errs) { return } // At this point, we should have a valid request that definitely has Targeting and Cache turned on - errL := deps.validateRequest(req) - if len(errL) > 0 { - errs = append(errs, errL...) - } - + e = deps.validateRequest(req) + errs = append(errs, e...) return } @@ -286,9 +295,6 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req return } - debugParam := httpRequest.FormValue("debug") - debug := debugParam == "1" - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond) defer cancel() @@ -308,7 +314,8 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req return } - if debug { + debugParam := httpRequest.FormValue("debug") + if debugParam == "1" { req.Test = 1 } @@ -335,18 +342,15 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req *req.Imp[0].Secure = 1 } - err := deps.overrideWithParams(httpRequest, req) - if err != nil { - errs = []error{err} - } - + errs = deps.overrideWithParams(httpRequest, req) return } -func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *openrtb.BidRequest) error { +func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *openrtb.BidRequest) []error { if req.Site == nil { req.Site = &openrtb.Site{} } + // Override the stored request sizes with AMP ones, if they exist. if req.Imp[0].Banner != nil { width := parseFormInt(httpRequest, "w", 0) @@ -382,16 +386,17 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope req.Imp[0].TagID = slot } - privacyPolicies := privacy.Policies{ - GDPR: gdpr.Policy{ - Consent: httpRequest.URL.Query().Get("gdpr_consent"), - }, - CCPA: ccpa.Policy{ - Value: httpRequest.URL.Query().Get("us_privacy"), - }, - } - if err := privacyPolicies.Write(req); err != nil { - return err + consent := readConsent(httpRequest.URL) + if consent != "" { + if policies, ok := privacy.ReadPoliciesFromConsent(consent); ok { + if err := policies.Write(req); err != nil { + return []error{err} + } + } else { + return []error{&errortypes.InvalidPrivacyConsent{ + Message: fmt.Sprintf("Consent '%s' is not recognized as either CCPA or GDPR TCF.", consent), + }} + } } if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil { @@ -402,31 +407,34 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope } func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format { + var formats []openrtb.Format if overrideWidth != 0 && overrideHeight != 0 { - return []openrtb.Format{{ + formats = []openrtb.Format{{ W: overrideWidth, H: overrideHeight, }} } else if overrideWidth != 0 && height != 0 { - return []openrtb.Format{{ + formats = []openrtb.Format{{ W: overrideWidth, H: height, }} } else if width != 0 && overrideHeight != 0 { - return []openrtb.Format{{ + formats = []openrtb.Format{{ W: width, H: overrideHeight, }} - } else if parsedSizes := parseMultisize(multisize); len(parsedSizes) != 0 { - return parsedSizes } else if width != 0 && height != 0 { - return []openrtb.Format{{ + formats = []openrtb.Format{{ W: width, H: height, }} } - return nil + if parsedSizes := parseMultisize(multisize); len(parsedSizes) != 0 { + formats = append(formats, parsedSizes...) + } + + return formats } func setWidths(formats []openrtb.Format, width uint64) { @@ -532,3 +540,12 @@ func setAmpExt(site *openrtb.Site, value string) { site.Ext = json.RawMessage(`{"amp":` + value + `}`) } } + +func readConsent(url *url.URL) string { + if v := url.Query().Get("consent_string"); v != "" { + return v + } + + // Fallback to 'gdpr_consent' for compatability until it's no longer used by AMP. + return url.Query().Get("gdpr_consent") +} diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 299124b691a..259992dbe20 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -81,9 +81,8 @@ func TestGoodAmpRequests(t *testing.T) { if response.Debug != nil { t.Errorf("Debug present but not requested") } - if _, ok := response.Errors[openrtb_ext.BidderOpenx]; !ok { - t.Errorf("OpenX error message is not present. (%v)", response.Errors) - } + + assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors, "errors") } } @@ -122,357 +121,471 @@ func TestAMPPageInfo(t *testing.T) { assert.Equal(t, "test.somepage.co.uk", exchange.lastRequest.Site.Domain) } -func TestConsentThroughEndpoint(t *testing.T) { - // gdpr consent string that will come inside our http.Request query - const consentString = "BOa71ZYOa71ZYAbABBENA8-AAAAbN7_______9______9uz_Gv_r_f__33e8_39v_h_7_-___m_-3zV4-_lvR11yPA1OrfIrwFhiAw" - const DigiTurstID = "digitrustId" - - // Generate a marshaled openrtb.BidRequest that DOESN'T come with a gdpr consent string - fullMarshaledBidRequest, err := getTestBidRequest(false, false, "", DigiTurstID) - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) +func TestGDPRConsent(t *testing.T) { + consent := "BOu5On0Ou5On0ADACHENAO7pqzAAppY" + existingConsent := "BONV8oqONXwgmADACHENAO7pqzAAppY" + digitrust := &openrtb_ext.ExtUserDigiTrust{ + ID: "anyDigitrustID", + KeyV: 1, + Pref: 0, + } + + testCases := []struct { + description string + consent string + userExt *openrtb_ext.ExtUser + nilUser bool + expectedUserExt openrtb_ext.ExtUser + }{ + { + description: "Nil User", + consent: consent, + nilUser: true, + expectedUserExt: openrtb_ext.ExtUser{ + Consent: consent, + }, + }, + { + description: "Nil User Ext", + consent: consent, + userExt: nil, + expectedUserExt: openrtb_ext.ExtUser{ + Consent: consent, + }, + }, + { + description: "Overrides Existing Consent", + consent: consent, + userExt: &openrtb_ext.ExtUser{ + Consent: existingConsent, + }, + expectedUserExt: openrtb_ext.ExtUser{ + Consent: consent, + }, + }, + { + description: "Overrides Existing Consent - With Sibling Data", + consent: consent, + userExt: &openrtb_ext.ExtUser{ + Consent: existingConsent, + DigiTrust: digitrust, + }, + expectedUserExt: openrtb_ext.ExtUser{ + Consent: consent, + DigiTrust: digitrust, + }, + }, + { + description: "Does Not Override Existing Consent If Empty", + consent: "", + userExt: &openrtb_ext.ExtUser{ + Consent: existingConsent, + }, + expectedUserExt: openrtb_ext.ExtUser{ + Consent: existingConsent, + }, + }, } - stored := map[string]json.RawMessage{ - "1": json.RawMessage(fullMarshaledBidRequest), - } + for _, test := range testCases { + // Build Request + bid, err := getTestBidRequest(test.nilUser, test.userExt, true, nil) + if err != nil { + t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) + } - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - exchange := &mockAmpExchange{} + // Simulated Stored Request Backend + stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} + + // Build Exchange Endpoint + mockExchange := &mockAmpExchange{} + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + endpoint, _ := NewAmpEndpoint( + mockExchange, + newParamsValidator(t), + &mockAmpStoredReqFetcher{stored}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + metrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + []byte{}, + openrtb_ext.BidderMap, + ) + + // Invoke Endpoint + request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_string=%s", test.consent), nil) + responseRecorder := httptest.NewRecorder() + endpoint(responseRecorder, request, nil) + + // Parse Response + var response AmpResponse + if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } - endpoint, _ := NewAmpEndpoint( - exchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{stored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BidderMap, - ) - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&gdpr_consent=%s", consentString), nil) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) + // Assert Result + result := mockExchange.lastRequest + if !assert.NotNil(t, result, test.description+":lastRequest") { + return + } + if !assert.NotNil(t, result.User, test.description+":lastRequest.User") { + return + } + if !assert.NotNil(t, result.User.Ext, test.description+":lastRequest.User.Ext") { + return + } + var ue openrtb_ext.ExtUser + err = json.Unmarshal(result.User.Ext, &ue) + if !assert.NoError(t, err, test.description+":deserialize") { + return + } + assert.Equal(t, test.expectedUserExt, ue, test.description) + assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors, test.description+":errors") + assert.Empty(t, response.Warnings, test.description+":warnings") + + // Invoke Endpoint With Legacy Param + requestLegacy := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&gdpr_consent=%s", test.consent), nil) + responseRecorderLegacy := httptest.NewRecorder() + endpoint(responseRecorderLegacy, requestLegacy, nil) + + // Parse Resonse + var responseLegacy AmpResponse + if err := json.Unmarshal(responseRecorderLegacy.Body.Bytes(), &responseLegacy); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { - return - } - // Assert our bidRequest had a valid "User" field - if !assert.NotNil(t, exchange.lastRequest.User, "Resulting bid request should have a valid User field after passing consent string through endpoint") { - return - } - // Assert our bidRequest had a valid "User.Ext" field - if !assert.NotNil(t, exchange.lastRequest.User.Ext, "Resulting bid request should have a valid Ext field after passing consent string through endpoint") { - return + // Assert Result With Legacy Param + resultLegacy := mockExchange.lastRequest + if !assert.NotNil(t, resultLegacy, test.description+":legacy:lastRequest") { + return + } + if !assert.NotNil(t, resultLegacy.User, test.description+":legacy:lastRequest.User") { + return + } + if !assert.NotNil(t, resultLegacy.User.Ext, test.description+":legacy:lastRequest.User.Ext") { + return + } + var ueLegacy openrtb_ext.ExtUser + err = json.Unmarshal(resultLegacy.User.Ext, &ueLegacy) + if !assert.NoError(t, err, test.description+":legacy:deserialize") { + return + } + assert.Equal(t, test.expectedUserExt, ueLegacy, test.description+":legacy") + assert.Equal(t, expectedErrorsFromHoldAuction, responseLegacy.Errors, test.description+":legacy:errors") + assert.Empty(t, responseLegacy.Warnings, test.description+":legacy:warnings") } - - // Assert string `consent` is found in the User.Ext at all - assert.NotContainsf(t, fullMarshaledBidRequest, "consent:"+consentString, "Expected bid request to contain consent string %s \n", consentString) - - // Assert the last request has a valid User object with a consent string equal to that on the URL query - var ue openrtb_ext.ExtUser - err = json.Unmarshal(exchange.lastRequest.User.Ext, &ue) - assert.NoError(t, err, "Error unmarshalling last processed request") - - // Assert consent string found in `http.Request` was passed correctly to the `User.Ext` object - assert.Contains(t, string(request.URL.RawQuery), consentString, "http.Request should come with a consent string in its query") - assert.Equal(t, consentString, ue.Consent, "Consent string unsuccessfully passed to bid request through AMP endpoint") - - // Assert other user properties found originally in our bid request such as `DigiTrust` were not overwritten - assert.Equal(t, DigiTurstID, ue.DigiTrust.ID, "Passing GDPR consent through endpoint should not override http.Request ExtUser fields other than consent") } -func TestConsentThroughEndpointNilUser(t *testing.T) { - // gdpr consent string that will come inside our http.Request query - const consentString = "BOa71ZYOa71ZYAbABBENA8-AAAAbN7_______9______9uz_Gv_r_f__33e8_39v_h_7_-___m_-3zV4-_lvR11yPA1OrfIrwFhiAw" - const DigiTurstID = "digitrustId" - - // Generate a marshaled openrtb.BidRequest that DOESN'T come with a gdpr consent string - fullMarshaledBidRequest, err := getTestBidRequest(true, false, "", DigiTurstID) - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) - } - - stored := map[string]json.RawMessage{ - "1": json.RawMessage(fullMarshaledBidRequest), +func TestCCPAConsent(t *testing.T) { + consent := "1NYN" + existingConsent := "1NNN" + + var gdpr int8 = 1 + + testCases := []struct { + description string + consent string + regsExt *openrtb_ext.ExtRegs + nilRegs bool + expectedRegExt openrtb_ext.ExtRegs + }{ + { + description: "Nil Regs", + consent: consent, + nilRegs: true, + expectedRegExt: openrtb_ext.ExtRegs{ + USPrivacy: consent, + }, + }, + { + description: "Nil Regs Ext", + consent: consent, + regsExt: nil, + expectedRegExt: openrtb_ext.ExtRegs{ + USPrivacy: consent, + }, + }, + { + description: "Overrides Existing Consent", + consent: consent, + regsExt: &openrtb_ext.ExtRegs{ + USPrivacy: existingConsent, + }, + expectedRegExt: openrtb_ext.ExtRegs{ + USPrivacy: consent, + }, + }, + { + description: "Overrides Existing Consent - With Sibling Data", + consent: consent, + regsExt: &openrtb_ext.ExtRegs{ + USPrivacy: existingConsent, + GDPR: &gdpr, + }, + expectedRegExt: openrtb_ext.ExtRegs{ + USPrivacy: consent, + GDPR: &gdpr, + }, + }, + { + description: "Does Not Override Existing Consent If Empty", + consent: "", + regsExt: &openrtb_ext.ExtRegs{ + USPrivacy: existingConsent, + }, + expectedRegExt: openrtb_ext.ExtRegs{ + USPrivacy: existingConsent, + }, + }, } - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - exchange := &mockAmpExchange{} + for _, test := range testCases { + // Build Request + bid, err := getTestBidRequest(true, nil, test.nilRegs, test.regsExt) + if err != nil { + t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) + } - endpoint, _ := NewAmpEndpoint( - exchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{stored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BidderMap, - ) - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&gdpr_consent=%s", consentString), nil) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) + // Simulated Stored Request Backend + stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} + + // Build Exchange Endpoint + mockExchange := &mockAmpExchange{} + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + endpoint, _ := NewAmpEndpoint( + mockExchange, + newParamsValidator(t), + &mockAmpStoredReqFetcher{stored}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + metrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + []byte{}, + openrtb_ext.BidderMap, + ) + + // Invoke Endpoint + request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_string=%s", test.consent), nil) + responseRecorder := httptest.NewRecorder() + endpoint(responseRecorder, request, nil) + + // Parse Response + var response AmpResponse + if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { - return - } - // Assert our bidRequest had a valid "User" field - if !assert.NotNil(t, exchange.lastRequest.User, "Resulting bid request should have a valid User field after passing consent string through endpoint") { - return - } - // Assert our bidRequest had a valid "User.Ext" field - if !assert.NotNil(t, exchange.lastRequest.User.Ext, "Resulting bid request should have a valid User.Ext field after passing consent string through endpoint") { - return + // Assert Result + result := mockExchange.lastRequest + if !assert.NotNil(t, result, test.description+":lastRequest") { + return + } + if !assert.NotNil(t, result.Regs, test.description+":lastRequest.Regs") { + return + } + if !assert.NotNil(t, result.Regs.Ext, test.description+":lastRequest.Regs.Ext") { + return + } + var re openrtb_ext.ExtRegs + err = json.Unmarshal(result.Regs.Ext, &re) + if !assert.NoError(t, err, test.description+":deserialize") { + return + } + assert.Equal(t, test.expectedRegExt, re, test.description) + assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors) + assert.Empty(t, response.Warnings) } - - // Assert string `consent` is found in the User.Ext at all - assert.NotContains(t, fullMarshaledBidRequest, "consent:"+consentString, "This bid request should not contain a consent string. It will be passed the one in the http.Request endpoint") - - // Assert the last request has a valid User object with a consent string equal to that on the URL query - var ue openrtb_ext.ExtUser - err = json.Unmarshal(exchange.lastRequest.User.Ext, &ue) - assert.NoError(t, err, "Error unmarshalling last processed request") - - // Assert consent string found in `http.Request` was passed correctly to the `User.Ext` object - assert.Contains(t, string(request.URL.RawQuery), consentString, "http.Request should come with a consent string in its query") - assert.Equal(t, consentString, ue.Consent, "Consent string unsuccessfully passed to bid request through AMP endpoint") } -func TestConsentThroughEndpointNilUserExt(t *testing.T) { - // gdpr consent string that will come inside our http.Request query - const consentString = "BOa71ZYOa71ZYAbABBENA8-AAAAbN7_______9______9uz_Gv_r_f__33e8_39v_h_7_-___m_-3zV4-_lvR11yPA1OrfIrwFhiAw" - const DigiTurstID = "digitrustId" - - // Generate a marshaled openrtb.BidRequest that DOESN'T come with a gdpr consent string - fullMarshaledBidRequest, err := getTestBidRequest(false, true, "some-consent-string", DigiTurstID) +func TestNoConsent(t *testing.T) { + // Build Request + bid, err := getTestBidRequest(true, nil, true, nil) if err != nil { t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) } - stored := map[string]json.RawMessage{ - "1": json.RawMessage(fullMarshaledBidRequest), - } - - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - exchange := &mockAmpExchange{} + // Simulated Stored Request Backend + stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} + // Build Exchange Endpoint + mockExchange := &mockAmpExchange{} + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) endpoint, _ := NewAmpEndpoint( - exchange, + mockExchange, newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, + metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap, ) - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&gdpr_consent=%s", consentString), nil) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { - return - } - // Assert our bidRequest had a valid "User" field - if !assert.NotNil(t, exchange.lastRequest.User, "Resulting bid request should have a valid User field after passing consent string through endpoint") { - return - } - // Assert our bidRequest had a valid "User.Ext" field - if !assert.NotNil(t, exchange.lastRequest.User.Ext, "Resulting bid request should have a valid Ext field after passing consent string through endpoint") { - return - } - - // Assert string `consent` is found in the User.Ext at all - assert.NotContains(t, fullMarshaledBidRequest, "consent:"+consentString, "This bid request should not contain a consent string. It will be passed the one in the http.Request endpoint") + // Invoke Endpoint + request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil) + responseRecorder := httptest.NewRecorder() + endpoint(responseRecorder, request, nil) - // Assert the last request has a valid User object with a consent string equal to that on the URL query - var ue openrtb_ext.ExtUser - err = json.Unmarshal(exchange.lastRequest.User.Ext, &ue) - assert.NoError(t, err, "Error unmarshalling last processed request") + // Parse Response + var response AmpResponse + if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } - // Assert consent string found in `http.Request` was passed correctly to the `User.Ext` object - assert.Contains(t, string(request.URL.RawQuery), consentString, "http.Request should come with a consent string in its query") - assert.Equal(t, consentString, ue.Consent, "Consent string unsuccessfully passed to bid request through AMP endpoint") + // Assert Result + result := mockExchange.lastRequest + assert.NotNil(t, result, "lastRequest") + assert.Nil(t, result.User, "lastRequest.User") + assert.Nil(t, result.Regs, "lastRequest.Regs") + assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors) + assert.Empty(t, response.Warnings) } -func TestSubstituteRequestConsentWithEndpointConsent(t *testing.T) { - // gdpr consent string that will come inside our http.Request query - const consentString = "BOa71ZYOa71ZYAbABBENA8-AAAAbN7_______9______9uz_Gv_r_f__33e8_39v_h_7_-___m_-3zV4-_lvR11yPA1OrfIrwFhiAw" - const DigiTurstID = "digitrustId" - - // Generate a marshaled openrtb.BidRequest that comes with a gdpr consent string - fullMarshaledBidRequest, err := getTestBidRequest(false, false, "some-consent-string", "digitrustId") +func TestInvalidConsent(t *testing.T) { + // Build Request + bid, err := getTestBidRequest(true, nil, true, nil) if err != nil { t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) } - stored := map[string]json.RawMessage{ - "1": json.RawMessage(fullMarshaledBidRequest), - } - - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - exchange := &mockAmpExchange{} + // Simulated Stored Request Backend + stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} + // Build Exchange Endpoint + mockExchange := &mockAmpExchange{} + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) endpoint, _ := NewAmpEndpoint( - exchange, + mockExchange, newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, + metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap, ) - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&gdpr_consent=%s", consentString), nil) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) - - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { - return - } - // Assert our bidRequest had a valid "User" field - if !assert.NotNil(t, exchange.lastRequest.User) { - return - } - // Assert our bidRequest had a valid "User.Ext" field - if !assert.NotNil(t, exchange.lastRequest.User.Ext) { - return - } - // Assert the last request has a valid User object with a consent string equal to that on the URL query - var ue openrtb_ext.ExtUser - err = json.Unmarshal(exchange.lastRequest.User.Ext, &ue) - assert.NoError(t, err) - - // Assert consent string found in `http.Request` was passed correctly to the `User.Ext` object - assert.Contains(t, string(request.URL.RawQuery), consentString) - assert.Equal(t, consentString, ue.Consent) - - // Assert other user properties found originally in our bid request such as `DigiTrust` were not overwritten - assert.Equal(t, DigiTurstID, ue.DigiTrust.ID) -} -func TestDontSubstituteRequestConsentWithBlankEndpointConsent(t *testing.T) { - // Blank gdpr consent string that will come inside our http.Request query - const httpURLConsentString = "" - const PrebidConsentString = "some-consent-string" - const DigiTurstID = "digitrustId" + // Invoke Endpoint + invalidConsent := "invalid" + request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1&consent_string="+invalidConsent, nil) + responseRecorder := httptest.NewRecorder() + endpoint(responseRecorder, request, nil) - // Generate a marshaled openrtb.BidRequest that comes with a gdpr consent string - fullMarshaledBidRequest, err := getTestBidRequest(false, false, PrebidConsentString, "digitrustId") - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) - } - - stored := map[string]json.RawMessage{ - "1": json.RawMessage(fullMarshaledBidRequest), + // Parse Response + var response AmpResponse + if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) } - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - exchange := &mockAmpExchange{} - - endpoint, _ := NewAmpEndpoint( - exchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{stored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BidderMap, - ) - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&gdpr_consent=%s", httpURLConsentString), nil) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) - - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { - return - } - // Assert our bidRequest had a valid "User" field - if !assert.NotNil(t, exchange.lastRequest.User) { - return - } - // Assert our bidRequest had a valid "User.Ext" field - if !assert.NotNil(t, exchange.lastRequest.User.Ext) { - return + // Assert Result + expectedWarnings := map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError{ + openrtb_ext.BidderNameGeneral: { + { + Code: 10001, + Message: "Consent '" + invalidConsent + "' is not recognized as either CCPA or GDPR TCF.", + }, + }, } - // Assert the last request has a valid User object with a consent string equal to that on the PBS request - var ue openrtb_ext.ExtUser - err = json.Unmarshal(exchange.lastRequest.User.Ext, &ue) - assert.NoError(t, err) - - // Assert consent string found in the PBS request was passed correctly to the `User.Ext` object - assert.Equal(t, PrebidConsentString, ue.Consent) + result := mockExchange.lastRequest + assert.NotNil(t, result, "lastRequest") + assert.Nil(t, result.User, "lastRequest.User") + assert.Nil(t, result.Regs, "lastRequest.Regs") + assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors) + assert.Equal(t, expectedWarnings, response.Warnings) } -func TestDontSubstituteRequestConsentNoEndpointConsent(t *testing.T) { - // Blank gdpr consent string that will come inside our http.Request query - const PrebidConsentString = "some-consent-string" - const DigiTurstID = "digitrustId" - - // Generate a marshaled openrtb.BidRequest that comes with a gdpr consent string - fullMarshaledBidRequest, err := getTestBidRequest(false, false, PrebidConsentString, "digitrustId") - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) - } - - stored := map[string]json.RawMessage{ - "1": json.RawMessage(fullMarshaledBidRequest), +func TestNewAndLegacyConsentBothProvided(t *testing.T) { + validConsentGDPR1 := "BOu5On0Ou5On0ADACHENAO7pqzAAppY" + validConsentGDPR2 := "BONV8oqONXwgmADACHENAO7pqzAAppY" + + testCases := []struct { + description string + consent string + consentLegacy string + userExt *openrtb_ext.ExtUser + expectedUserExt openrtb_ext.ExtUser + }{ + { + description: "New Consent Wins", + consent: validConsentGDPR1, + consentLegacy: validConsentGDPR2, + expectedUserExt: openrtb_ext.ExtUser{ + Consent: validConsentGDPR1, + }, + }, + { + description: "New Consent Wins - Reverse", + consent: validConsentGDPR2, + consentLegacy: validConsentGDPR1, + expectedUserExt: openrtb_ext.ExtUser{ + Consent: validConsentGDPR2, + }, + }, } - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - exchange := &mockAmpExchange{} + for _, test := range testCases { + // Build Request + bid, err := getTestBidRequest(false, nil, true, nil) + if err != nil { + t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) + } - endpoint, _ := NewAmpEndpoint( - exchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{stored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BidderMap, - ) - consentStringLessHttpRequest := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1"), nil) - recorder := httptest.NewRecorder() - endpoint(recorder, consentStringLessHttpRequest, nil) + // Simulated Stored Request Backend + stored := map[string]json.RawMessage{"1": json.RawMessage(bid)} + + // Build Exchange Endpoint + mockExchange := &mockAmpExchange{} + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + endpoint, _ := NewAmpEndpoint( + mockExchange, + newParamsValidator(t), + &mockAmpStoredReqFetcher{stored}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + metrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + []byte{}, + openrtb_ext.BidderMap, + ) + + // Invoke Endpoint + request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&consent_string=%s&gdpr_consent=%s", test.consent, test.consentLegacy), nil) + responseRecorder := httptest.NewRecorder() + endpoint(responseRecorder, request, nil) + + // Parse Response + var response AmpResponse + if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", recorder.Code, recorder.Body.String()) { - return - } - // Assert our bidRequest had a valid "User" field - if !assert.NotNil(t, exchange.lastRequest.User) { - return - } - // Assert our bidRequest had a valid "User.Ext" field - if !assert.NotNil(t, exchange.lastRequest.User.Ext) { - return + // Assert Result + result := mockExchange.lastRequest + if !assert.NotNil(t, result, test.description+":lastRequest") { + return + } + if !assert.NotNil(t, result.User, test.description+":lastRequest.User") { + return + } + if !assert.NotNil(t, result.User.Ext, test.description+":lastRequest.User.Ext") { + return + } + var ue openrtb_ext.ExtUser + err = json.Unmarshal(result.User.Ext, &ue) + if !assert.NoError(t, err, test.description+":deserialize") { + return + } + assert.Equal(t, test.expectedUserExt, ue, test.description) + assert.Equal(t, expectedErrorsFromHoldAuction, response.Errors) + assert.Empty(t, response.Warnings) } - // Assert the last request has a valid User object with a consent string equal to that on the PBS request - var ue openrtb_ext.ExtUser - err = json.Unmarshal(exchange.lastRequest.User.Ext, &ue) - assert.NoError(t, err) - - // Assert consent string found in the PBS request was passed correctly to the `User.Ext` object - assert.Equal(t, PrebidConsentString, ue.Consent) } func TestAMPSiteExt(t *testing.T) { @@ -719,6 +832,24 @@ func TestMultisize(t *testing.T) { }.execute(t) } +func TestSizeWithMultisize(t *testing.T) { + formatOverrideSpec{ + width: 20, + height: 40, + multisize: "200x50,100x60", + expect: []openrtb.Format{{ + W: 20, + H: 40, + }, { + W: 200, + H: 50, + }, { + W: 100, + H: 60, + }}, + }.execute(t) +} + func TestHeightOnly(t *testing.T) { formatOverrideSpec{ height: 200, @@ -739,102 +870,6 @@ func TestWidthOnly(t *testing.T) { }.execute(t) } -func TestCCPAPresent(t *testing.T) { - req, err := getTestBidRequest(false, false, "", "digitrustId") - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) - } - - reqStored := map[string]json.RawMessage{ - "1": json.RawMessage(req), - } - - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - - exchange := &mockAmpExchange{} - - endpoint, _ := NewAmpEndpoint( - exchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{reqStored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BidderMap, - ) - - usPrivacy := "1YYN" - httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1&us_privacy="+usPrivacy, nil) - httpRecorder := httptest.NewRecorder() - endpoint(httpRecorder, httpReq, nil) - - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", httpRecorder.Code, httpRecorder.Body.String()) { - return - } - // Assert our bidRequest had a valid "Regs" field - if !assert.NotNil(t, exchange.lastRequest.Regs) { - return - } - // Assert our bidRequest had a valid "Regs.Ext" field - if !assert.NotNil(t, exchange.lastRequest.Regs.Ext) { - return - } - - var regs openrtb_ext.ExtRegs - err = json.Unmarshal(exchange.lastRequest.Regs.Ext, ®s) - assert.NoError(t, err) - assert.Equal(t, usPrivacy, regs.USPrivacy) -} - -func TestCCPANotPresent(t *testing.T) { - req, err := getTestBidRequest(false, false, "", "digitrustId") - if err != nil { - t.Fatalf("Failed to marshal the complete openrtb.BidRequest object %v", err) - } - - reqStored := map[string]json.RawMessage{ - "1": json.RawMessage(req), - } - - theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - - exchange := &mockAmpExchange{} - - endpoint, _ := NewAmpEndpoint( - exchange, - newParamsValidator(t), - &mockAmpStoredReqFetcher{reqStored}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, - theMetrics, - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{}, - []byte{}, - openrtb_ext.BidderMap, - ) - - httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil) - httpRecorder := httptest.NewRecorder() - endpoint(httpRecorder, httpReq, nil) - - // Assert our bidRequest was valid - if !assert.NotNil(t, exchange.lastRequest, "Endpoint responded with %d: %s", httpRecorder.Code, httpRecorder.Body.String()) { - return - } - - // Assert CCPA Signal Not Found - if exchange.lastRequest.Regs != nil && exchange.lastRequest.Regs.Ext != nil { - var regs openrtb_ext.ExtRegs - err = json.Unmarshal(exchange.lastRequest.Regs.Ext, ®s) - assert.NoError(t, err) - assert.Empty(t, regs.USPrivacy) - } -} - type formatOverrideSpec struct { width uint64 height uint64 @@ -902,7 +937,16 @@ type mockAmpExchange struct { lastRequest *openrtb.BidRequest } -func (m *mockAmpExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher) (*openrtb.BidResponse, error) { +var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError = map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError{ + openrtb_ext.BidderName("openx"): { + { + Code: 1, + Message: "The request exceeded the timeout allocated", + }, + }, +} + +func (m *mockAmpExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { m.lastRequest = bidRequest response := &openrtb.BidResponse{ @@ -926,39 +970,7 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.B return response, nil } -func getTestBidRequest(nilUser bool, nilExt bool, consentString string, digitrustID string) ([]byte, error) { - var userExt openrtb_ext.ExtUser - var userExtData []byte - var err error - - if consentString != "" { - userExt = openrtb_ext.ExtUser{ - Consent: consentString, - DigiTrust: &openrtb_ext.ExtUserDigiTrust{ - ID: digitrustID, - KeyV: 1, - Pref: 0, - }, - } - } else { - userExt = openrtb_ext.ExtUser{ - DigiTrust: &openrtb_ext.ExtUserDigiTrust{ - ID: digitrustID, - KeyV: 1, - Pref: 0, - }, - } - } - - if !nilExt { - userExtData, err = json.Marshal(userExt) - if err != nil { - return nil, err - } - } else { - userExtData = []byte("") - } - +func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, regsExt *openrtb_ext.ExtRegs) ([]byte, error) { var width uint64 = 300 var height uint64 = 300 bidRequest := &openrtb.BidRequest{ @@ -988,6 +1000,16 @@ func getTestBidRequest(nilUser bool, nilExt bool, consentString string, digitrus Page: "some-page", }, } + + var userExtData []byte + if userExt != nil { + var err error + userExtData, err = json.Marshal(userExt) + if err != nil { + return nil, err + } + } + if !nilUser { bidRequest.User = &openrtb.User{ ID: "aUserId", @@ -995,5 +1017,22 @@ func getTestBidRequest(nilUser bool, nilExt bool, consentString string, digitrus Ext: userExtData, } } + + var regsExtData []byte + if regsExt != nil { + var err error + regsExtData, err = json.Marshal(regsExt) + if err != nil { + return nil, err + } + } + + if !nilRegs { + bidRequest.Regs = &openrtb.Regs{ + COPPA: 1, + Ext: regsExtData, + } + } + return json.Marshal(bidRequest) } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 87b4cca09b6..f8552666bc3 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "net/http" "net/url" + "regexp" "strconv" "time" @@ -22,6 +23,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/prebid" + "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/empty_fetcher" @@ -55,7 +57,9 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato disabledBidders, defRequest, defReqJSON, - bidderMap}).Auction), nil + bidderMap, + nil, + nil}).Auction), nil } type endpointDeps struct { @@ -71,6 +75,8 @@ type endpointDeps struct { defaultRequest bool defReqJSON []byte bidderMap map[string]openrtb_ext.BidderName + cache prebid_cache_client.Client + debugLogRegexp *regexp.Regexp } func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -103,7 +109,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http req, errL := deps.parseRequest(r) - if fatalError(errL) && writeError(errL, w, &labels) { + if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) { return } @@ -137,7 +143,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http return } - response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories) + response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories, nil) ao.Request = req ao.Response = response if err != nil { @@ -309,7 +315,12 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { } if err := ccpaPolicy.Validate(); err != nil { - errL = append(errL, &errortypes.Warning{Message: fmt.Sprintf("CCPA value is invalid and will be ignored. (%s)", err.Error())}) + errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)}) + + ccpaPolicy.Value = "" + if err := ccpaPolicy.Write(req); err != nil { + errL = append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err)) + } } impIDs := make(map[string]int, len(req.Imp)) @@ -323,7 +334,7 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { if len(errs) > 0 { errL = append(errL, errs...) } - if fatalError(errs) { + if errortypes.ContainsFatalError(errs) { return errL } } @@ -1185,8 +1196,8 @@ func writeError(errs []error, w http.ResponseWriter, labels *pbsmetrics.Labels) httpStatus := http.StatusBadRequest metricsStatus := pbsmetrics.RequestStatusBadInput for _, err := range errs { - erVal := errortypes.DecodeError(err) - if erVal == errortypes.BlacklistedAppCode || erVal == errortypes.BlacklistedAcctCode { + erVal := errortypes.ReadCode(err) + if erVal == errortypes.BlacklistedAppErrorCode || erVal == errortypes.BlacklistedAcctErrorCode { httpStatus = http.StatusServiceUnavailable metricsStatus = pbsmetrics.RequestStatusBlacklisted break @@ -1202,17 +1213,6 @@ func writeError(errs []error, w http.ResponseWriter, labels *pbsmetrics.Labels) return rc } -// Checks to see if an error in an error list is a fatal error -func fatalError(errL []error) bool { - for _, err := range errL { - errCode := errortypes.DecodeError(err) - if errCode != errortypes.BidderTemporarilyDisabledCode && errCode != errortypes.WarningCode && errCode != errortypes.BidderFailedSchemaValidationCode { - return true - } - } - return false -} - // Returns the effective publisher ID func effectivePubID(pub *openrtb.Publisher) string { if pub != nil { diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 2671d212c17..07d477a3730 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -175,7 +175,7 @@ func TestBadNativeRequests(t *testing.T) { tests.assert(t) } -// TestAliasedRequests makes sure we handle (defuault) aliased bidders properly +// TestAliasedRequests makes sure we handle (default) aliased bidders properly func TestAliasedRequests(t *testing.T) { tests := &getResponseFromDirectory{ dir: "sample-requests/aliased", @@ -289,7 +289,7 @@ func (gr *getResponseFromDirectory) assert(t *testing.T) { filesToAssert = append(filesToAssert, gr.dir+"/"+fileInfo.Name()) } } else { - // Just test the single `gr.file`, and not the entiriety of files that may be found in `gr.dir` + // Just test the single `gr.file`, and not the entirety of files that may be found in `gr.dir` filesToAssert = append(filesToAssert, gr.dir+"/"+gr.file) } @@ -602,7 +602,7 @@ func TestStoredRequests(t *testing.T) { // NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it. // As a side effect this gives us some coverage of the go_metrics piece of the metrics engine. theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - edep := &endpointDeps{&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, false, []byte{}, openrtb_ext.BidderMap} + edep := &endpointDeps{&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, false, []byte{}, openrtb_ext.BidderMap, nil, nil} for i, requestData := range testStoredRequests { newRequest, errList := edep.processStoredRequests(context.Background(), json.RawMessage(requestData)) @@ -638,6 +638,8 @@ func TestOversizedRequest(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -670,6 +672,8 @@ func TestRequestSizeEdgeCase(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -803,10 +807,12 @@ func TestDisabledBidder(t *testing.T) { }, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{"unknownbidder": "The biddder 'unknownbidder' has been disabled."}, + map[string]string{"unknownbidder": "The bidder 'unknownbidder' has been disabled."}, false, []byte{}, openrtb_ext.BidderMap, + nil, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -836,14 +842,16 @@ func TestValidateImpExtDisabledBidder(t *testing.T) { &config.Configuration{MaxRequestSize: int64(8096)}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{"unknownbidder": "The biddder 'unknownbidder' has been disabled."}, + map[string]string{"unknownbidder": "The bidder 'unknownbidder' has been disabled."}, false, []byte{}, openrtb_ext.BidderMap, + nil, + nil, } errs := deps.validateImpExt(imp, nil, 0) assert.JSONEq(t, `{"appnexus":{"placement_id":555}}`, string(imp.Ext)) - assert.Equal(t, []error{&errortypes.BidderTemporarilyDisabled{Message: "The biddder 'unknownbidder' has been disabled."}}, errs) + assert.Equal(t, []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, errs) } func TestEffectivePubID(t *testing.T) { @@ -878,6 +886,8 @@ func TestCurrencyTrunc(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, + nil, } ui := uint64(1) @@ -905,7 +915,7 @@ func TestCurrencyTrunc(t *testing.T) { assert.ElementsMatch(t, errL, []error{&expectedError}) } -func TestCCPAInvalidValueWarning(t *testing.T) { +func TestCCPAInvalid(t *testing.T) { deps := &endpointDeps{ &nobidExchange{}, newParamsValidator(t), @@ -919,6 +929,8 @@ func TestCCPAInvalidValueWarning(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, + nil, } ui := uint64(1) @@ -931,21 +943,23 @@ func TestCCPAInvalidValueWarning(t *testing.T) { W: &ui, H: &ui, }, - Ext: json.RawMessage("{\"appnexus\": {\"placementId\": 5667}}"), + Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`), }, }, Site: &openrtb.Site{ ID: "myID", }, Regs: &openrtb.Regs{ - Ext: json.RawMessage("{\"us_privacy\":\"invalid by length\"}"), + Ext: json.RawMessage(`{"us_privacy":"invalid by length"}`), }, } errL := deps.validateRequest(&req) - expectedError := errortypes.Warning{Message: "CCPA value is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)"} - assert.ElementsMatch(t, errL, []error{&expectedError}) + expectedWarning := errortypes.InvalidPrivacyConsent{Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)"} + assert.ElementsMatch(t, errL, []error{&expectedWarning}) + + assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") } // nobidExchange is a well-behaved exchange which always bids "no bid". @@ -953,7 +967,7 @@ type nobidExchange struct { gotRequest *openrtb.BidRequest } -func (e *nobidExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher) (*openrtb.BidResponse, error) { +func (e *nobidExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { e.gotRequest = bidRequest return &openrtb.BidResponse{ ID: bidRequest.ID, @@ -964,7 +978,7 @@ func (e *nobidExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.Bid type brokenExchange struct{} -func (e *brokenExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher) (*openrtb.BidResponse, error) { +func (e *brokenExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { return nil, errors.New("Critical, unrecoverable error.") } @@ -1324,7 +1338,7 @@ type mockExchange struct { lastRequest *openrtb.BidRequest } -func (m *mockExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher) (*openrtb.BidResponse, error) { +func (m *mockExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { m.lastRequest = bidRequest return &openrtb.BidResponse{ SeatBid: []openrtb.SeatBid{{ diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index 0609a341b21..71eba8f1b71 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -17,6 +17,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/analytics" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" "github.com/PubMatic-OpenWrap/prebid-server/exchange" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" @@ -76,6 +77,8 @@ func NewCTVEndpoint( defRequest, defReqJSON, bidderMap, + nil, + nil, }, }).CTVAuctionEndpoint), nil } @@ -117,7 +120,7 @@ func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.R //Parse ORTB Request and do Standard Validation request, errL = deps.parseRequest(r) - if fatalError(errL) && writeError(errL, w, &deps.labels) { + if errortypes.ContainsFatalError(errL) && writeError(errL, w, &deps.labels) { return } @@ -236,7 +239,7 @@ func (deps *ctvEndpointDeps) holdAuction(request *openrtb.BidRequest, usersyncs return &openrtb.BidResponse{ID: request.ID}, nil } - return deps.ex.HoldAuction(deps.ctx, request, usersyncs, deps.labels, &deps.categories) + return deps.ex.HoldAuction(deps.ctx, request, usersyncs, deps.labels, &deps.categories, nil) } /********************* BidRequest Processing *********************/ diff --git a/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json b/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json index 0a9fe656362..d62f40438b4 100644 --- a/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json +++ b/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json @@ -1,68 +1,69 @@ { - "description": "Video endpoint valid request.", + "description": "Video endpoint valid request due to missing pods.", - "requestPayload": -{ - "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", - "accountid": "555888777", - - "site": { - "page": "prebid.com" - }, - "user": { - "buyeruids": { - "appnexus": "unique_id_an", - "rubicon": "unique_id_rubi" + "requestPayload": { + "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] }, - "gdpr": { - "consentrequired": false, - "consentstring": "something" + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 }, - "yob": 1991, - "gender": "F", - "keywords": "Hotels, Travelling" - }, - "device11": { - "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", - "ip": "123.145.167.10", - "devicetype": 1, - "dnt": 33, - "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", - "lmt": 44, - "os": "mac os", - "w": 640, - "h": 480, - "didsha1": "didsha1", - "didmd5": "didmd5", - "dpidsha1": "dpidsha1", - "dpidmd5": "dpidmd5", - "macsha1": "macsha1", - "macmd5": "macmd5" - }, - "includebrandcategory":{ - "primaryadserver": 1, - "publisher": "" - }, - "video": { - "w": 640, - "h": 480, - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2,3,5,6 - ] - }, - "content": { - "episode": 6, - "title": "episodeName", - "series": "TvName", - "season": "season3", - "len": 900, - "livestream": 0 - }, - "cacheconfig": { - "ttl": 42 + "cacheconfig": { + "ttl": 42 + } } -} } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample.json index caa16f523dc..7ccdbf83a46 100644 --- a/endpoints/openrtb2/sample-requests/video/video_valid_sample.json +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample.json @@ -1,85 +1,86 @@ { "description": "Video endpoint valid request.", - "requestPayload": -{ - "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", - "accountid": "555888777", - "podconfig": { - "durationrangesec": [ - 30 - ], - "requireexactduration": true, - "pods": [ - { - "podid": 1, - "adpoddurationsec": 180, - "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" - }, - { - "podid": 2, - "adpoddurationsec": 150, - "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + "requestPayload": { + "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + "podconfig": { + "durationrangesec": [ + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } } - ] - }, - "site": { - "page": "prebid.com" - }, - "user": { - "buyeruids": { - "appnexus": "unique_id_an", - "rubicon": "unique_id_rubi" }, - "gdpr": { - "consentrequired": false, - "consentstring": "something" + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 }, - "yob": 1991, - "gender": "F", - "keywords": "Hotels, Travelling" - }, - "device11": { - "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", - "ip": "123.145.167.10", - "devicetype": 1, - "dnt": 33, - "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", - "lmt": 44, - "os": "mac os", - "w": 640, - "h": 480, - "didsha1": "didsha1", - "didmd5": "didmd5", - "dpidsha1": "dpidsha1", - "dpidmd5": "dpidmd5", - "macsha1": "macsha1", - "macmd5": "macmd5" - }, - "includebrandcategory":{ - "primaryadserver": 1, - "publisher": "" - }, - "video": { - "w": 640, - "h": 480, - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2,3,5,6 - ] - }, - "content": { - "episode": 6, - "title": "episodeName", - "series": "TvName", - "season": "season3", - "len": 900, - "livestream": 0 - }, - "cacheconfig": { - "ttl": 42 + "cacheconfig": { + "ttl": 42 + } } -} } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_malformed.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_malformed.json new file mode 100644 index 00000000000..b512c68346e --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_malformed.json @@ -0,0 +1,88 @@ +{ + "description": "Video endpoint valid request.", + + "requestPayload": { + "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + "podconfig": { + "durationrangesec": [ + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "${malformed}" + } + }, + "user": { + "buyeruid": "anyId", + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 + }, + "cacheconfig": { + "ttl": 42 + } + } +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_valid.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_valid.json new file mode 100644 index 00000000000..cfa389d4ce2 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_valid.json @@ -0,0 +1,88 @@ +{ + "description": "Video endpoint valid request.", + + "requestPayload": { + "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + "podconfig": { + "durationrangesec": [ + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1NYN" + } + }, + "user": { + "buyeruid": "anyId", + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 + }, + "cacheconfig": { + "ttl": 42 + } + } +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json index 504af2d61cd..c3ad776960a 100644 --- a/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json @@ -1,86 +1,87 @@ { - "description": "Video endpoint valid request.", + "description": "Video endpoint valid request with different durations.", - "requestPayload": -{ - "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", - "accountid": "555888777", - "podconfig": { - "durationrangesec": [ - 15, - 30 - ], - "requireexactduration": true, - "pods": [ - { - "podid": 1, - "adpoddurationsec": 180, - "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" - }, - { - "podid": 2, - "adpoddurationsec": 150, - "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + "requestPayload": { + "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + "podconfig": { + "durationrangesec": [ + 15, + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } } - ] - }, - "site": { - "page": "prebid.com" - }, - "user": { - "buyeruids": { - "appnexus": "unique_id_an", - "rubicon": "unique_id_rubi" }, - "gdpr": { - "consentrequired": false, - "consentstring": "something" + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 }, - "yob": 1991, - "gender": "F", - "keywords": "Hotels, Travelling" - }, - "device11": { - "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", - "ip": "123.145.167.10", - "devicetype": 1, - "dnt": 33, - "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", - "lmt": 44, - "os": "mac os", - "w": 640, - "h": 480, - "didsha1": "didsha1", - "didmd5": "didmd5", - "dpidsha1": "dpidsha1", - "dpidmd5": "dpidmd5", - "macsha1": "macsha1", - "macmd5": "macmd5" - }, - "includebrandcategory":{ - "primaryadserver": 1, - "publisher": "" - }, - "video": { - "w": 640, - "h": 480, - "mimes": [ - "video/mp4" - ], - "protocols": [ - 2,3,5,6 - ] - }, - "content": { - "episode": 6, - "title": "episodeName", - "series": "TvName", - "season": "season3", - "len": 900, - "livestream": 0 - }, - "cacheconfig": { - "ttl": 42 + "cacheconfig": { + "ttl": 42 + } } -} } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json new file mode 100644 index 00000000000..6a9dc605ea2 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json @@ -0,0 +1,85 @@ +{ + "description": "Video endpoint valid request with device data.", + + "requestPayload": { + "podconfig": { + "durationrangesec": [ + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } + } + }, + "device": { + "ua": "TestHeaderSample", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 + }, + "cacheconfig": { + "ttl": 42 + } + } +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json new file mode 100644 index 00000000000..199391865b2 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json @@ -0,0 +1,69 @@ +{ + "description": "Video endpoint valid request without device data.", + + "requestPayload": { + "podconfig": { + "durationrangesec": [ + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } + } + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 + }, + "cacheconfig": { + "ttl": 42 + } + } +} \ No newline at end of file diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 3646972e249..2629eb24454 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -8,6 +8,8 @@ import ( "io" "io/ioutil" "net/http" + "net/url" + "regexp" "strconv" "strings" "time" @@ -15,6 +17,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/errortypes" "github.com/buger/jsonparser" jsonpatch "github.com/evanphx/json-patch" + "github.com/gofrs/uuid" "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/analytics" @@ -22,6 +25,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/exchange" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/usersync" "github.com/golang/glog" @@ -30,14 +34,16 @@ import ( var defaultRequestTimeout int64 = 5000 -func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, videoFetcher stored_requests.Fetcher, categories stored_requests.CategoryFetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, disabledBidders map[string]string, defReqJSON []byte, bidderMap map[string]openrtb_ext.BidderName) (httprouter.Handle, error) { +func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, videoFetcher stored_requests.Fetcher, categories stored_requests.CategoryFetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, disabledBidders map[string]string, defReqJSON []byte, bidderMap map[string]openrtb_ext.BidderName, cache prebid_cache_client.Client) (httprouter.Handle, error) { if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { return nil, errors.New("NewVideoEndpoint requires non-nil arguments.") } defRequest := defReqJSON != nil && len(defReqJSON) > 0 - return httprouter.Handle((&endpointDeps{ex, validator, requestsById, videoFetcher, categories, cfg, met, pbsAnalytics, disabledBidders, defRequest, defReqJSON, bidderMap}).VideoAuctionEndpoint), nil + videoEndpointRegexp := regexp.MustCompile(`[<>]`) + + return httprouter.Handle((&endpointDeps{ex, validator, requestsById, videoFetcher, categories, cfg, met, pbsAnalytics, disabledBidders, defRequest, defReqJSON, bidderMap, cache, videoEndpointRegexp}).VideoAuctionEndpoint), nil } /* @@ -79,7 +85,26 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re CookieFlag: pbsmetrics.CookieFlagUnknown, RequestStatus: pbsmetrics.RequestStatusOK, } + + debugQuery := r.URL.Query().Get("debug") + cacheTTL := int64(3600) + if deps.cfg.CacheURL.DefaultTTLs.Video > 0 { + cacheTTL = int64(deps.cfg.CacheURL.DefaultTTLs.Video) + } + debugLog := exchange.DebugLog{ + Enabled: strings.EqualFold(debugQuery, "true"), + CacheType: prebid_cache_client.TypeXML, + TTL: cacheTTL, + Regexp: deps.debugLogRegexp, + } + defer func() { + if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil { + err := putDebugLogError(deps.cache, &debugLog, start) + if err != nil { + vo.Errors = append(vo.Errors, err) + } + } deps.metricsEngine.RecordRequest(labels) deps.metricsEngine.RecordRequestTime(labels, time.Since(start)) deps.analytics.LogVideoObject(&vo) @@ -91,38 +116,46 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } requestJson, err := ioutil.ReadAll(lr) if err != nil { - handleError(&labels, w, []error{err}, &vo) + handleError(&labels, w, []error{err}, &vo, &debugLog) return } resolvedRequest := requestJson + if debugLog.Enabled { + debugLog.Data.Request = string(requestJson) + if headerBytes, err := json.Marshal(r.Header); err == nil { + debugLog.Data.Headers = string(headerBytes) + } else { + debugLog.Data.Headers = fmt.Sprintf("Unable to marshal headers data: %s", err.Error()) + } + } //load additional data - stored simplified req storedRequestId, err := getVideoStoredRequestId(requestJson) if err != nil { if deps.cfg.VideoStoredRequestRequired { - handleError(&labels, w, []error{err}, &vo) + handleError(&labels, w, []error{err}, &vo, &debugLog) return } } else { storedRequest, errs := deps.loadStoredVideoRequest(context.Background(), storedRequestId) if len(errs) > 0 { - handleError(&labels, w, errs, &vo) + handleError(&labels, w, errs, &vo, &debugLog) return } //merge incoming req with stored video req resolvedRequest, err = jsonpatch.MergePatch(storedRequest, requestJson) if err != nil { - handleError(&labels, w, []error{err}, &vo) + handleError(&labels, w, []error{err}, &vo, &debugLog) return } } //unmarshal and validate combined result - videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest) + videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest, r.Header) if len(errL) > 0 { - handleError(&labels, w, errL, &vo) + handleError(&labels, w, errL, &vo, &debugLog) return } @@ -132,13 +165,17 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re if deps.defaultRequest { if err := json.Unmarshal(deps.defReqJSON, bidReq); err != nil { err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", err) - handleError(&labels, w, []error{err}, &vo) + handleError(&labels, w, []error{err}, &vo, &debugLog) return } } //create full open rtb req from full video request mergeData(videoBidReq, bidReq) + // If debug query param is set, force the response to enable test flag + if debugLog.Enabled { + bidReq.Test = 1 + } initialPodNumber := len(videoBidReq.PodConfig.Pods) if len(podErrors) > 0 { @@ -156,7 +193,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } err := errors.New(fmt.Sprintf("all pods are incorrect: %s", strings.Join(resPodErr, "; "))) errL = append(errL, err) - handleError(&labels, w, errL, &vo) + handleError(&labels, w, errL, &vo, &debugLog) return } @@ -167,8 +204,8 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re deps.setFieldsImplicitly(r, bidReq) // move after merge errL = deps.validateRequest(bidReq) - if len(errL) > 0 { - handleError(&labels, w, errL, &vo) + if errortypes.ContainsFatalError(errL) { + handleError(&labels, w, errL, &vo, &debugLog) return } @@ -195,17 +232,17 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL = append(errL, acctIdErr) - handleError(&labels, w, errL, &vo) + errL := []error{err} + handleError(&labels, w, errL, &vo, &debugLog) return } //execute auction logic - response, err := deps.ex.HoldAuction(ctx, bidReq, usersyncs, labels, &deps.categories) + response, err := deps.ex.HoldAuction(ctx, bidReq, usersyncs, labels, &deps.categories, &debugLog) vo.Request = bidReq vo.Response = response if err != nil { errL := []error{err} - handleError(&labels, w, errL, &vo) + handleError(&labels, w, errL, &vo, &debugLog) return } @@ -213,7 +250,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re bidResp, err := buildVideoResponse(response, podErrors) if err != nil { errL := []error{err} - handleError(&labels, w, errL, &vo) + handleError(&labels, w, errL, &vo, &debugLog) return } if bidReq.Test == 1 { @@ -226,7 +263,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re //resp, err := json.Marshal(response) if err != nil { errL := []error{err} - handleError(&labels, w, errL, &vo) + handleError(&labels, w, errL, &vo, &debugLog) return } @@ -235,6 +272,34 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } +func putDebugLogError(cache prebid_cache_client.Client, debugLog *exchange.DebugLog, start time.Time) error { + debugLog.Data.Response = "No response created" + + debugLog.BuildCacheString() + + data, err := json.Marshal(debugLog.CacheString) + if err != nil { + return err + } + + toCache := []prebid_cache_client.Cacheable{ + { + Type: debugLog.CacheType, + Data: data, + TTLSeconds: debugLog.TTL, + Key: "log_" + debugLog.CacheKey, + }, + } + + if cache != nil { + ctx, cancel := context.WithDeadline(context.Background(), start.Add(time.Duration(100)*time.Millisecond)) + defer cancel() + cache.PutJson(ctx, toCache) + } + + return nil +} + func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) *openrtb_ext.BidRequestVideo { for i := len(podErrors) - 1; i >= 0; i-- { videoReq.PodConfig.Pods = append(videoReq.PodConfig.Pods[:podErrors[i].PodIndex], videoReq.PodConfig.Pods[podErrors[i].PodIndex+1:]...) @@ -242,17 +307,23 @@ func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []P return videoReq } -func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error, vo *analytics.VideoObject) { +func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error, vo *analytics.VideoObject, debugLog *exchange.DebugLog) { + if debugLog != nil && debugLog.Enabled { + if rawUUID, err := uuid.NewV4(); err == nil { + debugLog.CacheKey = rawUUID.String() + } + errL = append(errL, fmt.Errorf("[Debug cache ID: %s]", debugLog.CacheKey)) + } labels.RequestStatus = pbsmetrics.RequestStatusErr var errors string var status int = http.StatusInternalServerError for _, er := range errL { - erVal := errortypes.DecodeError(er) - if erVal == errortypes.BlacklistedAppCode || erVal == errortypes.BlacklistedAcctCode { + erVal := errortypes.ReadCode(er) + if erVal == errortypes.BlacklistedAppErrorCode || erVal == errortypes.BlacklistedAcctErrorCode { status = http.StatusServiceUnavailable labels.RequestStatus = pbsmetrics.RequestStatusBlacklisted break - } else if erVal == errortypes.AcctRequiredCode { + } else if erVal == errortypes.AcctRequiredErrorCode { status = http.StatusBadRequest labels.RequestStatus = pbsmetrics.RequestStatusBadInput break @@ -328,12 +399,10 @@ func max(a, b int) int { return b } -func createImpressionTemplate(imp openrtb.Imp, video openrtb_ext.SimplifiedVideo) openrtb.Imp { - imp.Video = &openrtb.Video{} - imp.Video.W = video.W - imp.Video.H = video.H - imp.Video.Protocols = video.Protocols - imp.Video.MIMEs = video.Mimes +func createImpressionTemplate(imp openrtb.Imp, video *openrtb.Video) openrtb.Imp { + //for every new impression we need to have it's own copy of video object, because we customize it in further processing + newVideo := *video + imp.Video = &newVideo return imp } @@ -471,14 +540,7 @@ func mergeData(videoRequest *openrtb_ext.BidRequestVideo, bidRequest *openrtb.Bi bidRequest.Device = &videoRequest.Device } - if &videoRequest.User != nil { - bidRequest.User = &openrtb.User{ - BuyerUID: videoRequest.User.Buyeruids["appnexus"], //TODO: map to string merging - Yob: videoRequest.User.Yob, - Gender: videoRequest.User.Gender, - Keywords: videoRequest.User.Keywords, - } - } + bidRequest.User = videoRequest.User if len(videoRequest.BCat) != 0 { bidRequest.BCat = videoRequest.BCat @@ -550,8 +612,9 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro } prebid := openrtb_ext.ExtRequestPrebid{ - Cache: &cache, - Targeting: &targeting, + Cache: &cache, + Targeting: &targeting, + SupportDeals: videoRequest.SupportDeals, } extReq := openrtb_ext.ExtRequest{Prebid: prebid} @@ -562,7 +625,7 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro return reqJSON, nil } -func (deps *endpointDeps) parseVideoRequest(request []byte) (req *openrtb_ext.BidRequestVideo, errs []error, podErrors []PodError) { +func (deps *endpointDeps) parseVideoRequest(request []byte, headers http.Header) (req *openrtb_ext.BidRequestVideo, errs []error, podErrors []PodError) { req = &openrtb_ext.BidRequestVideo{} if err := json.Unmarshal(request, &req); err != nil { @@ -570,6 +633,22 @@ func (deps *endpointDeps) parseVideoRequest(request []byte) (req *openrtb_ext.Bi return } + //if Device.UA is not present in request body, init it with user-agent from request header if it's present + if req.Device.UA == "" { + ua := headers.Get("User-Agent") + + //Check UA is encoded. Without it the `+` character would get changed to a space if not actually encoded + if strings.ContainsAny(ua, "%") { + var err error + req.Device.UA, err = url.QueryUnescape(ua) + if err != nil { + req.Device.UA = ua + } + } else { + req.Device.UA = ua + } + } + errL, podErrors := deps.validateVideoRequest(req) if len(errL) > 0 { errs = append(errs, errL...) @@ -659,27 +738,30 @@ func (deps *endpointDeps) validateVideoRequest(req *openrtb_ext.BidRequestVideo) } } - if len(req.Video.Mimes) == 0 { - err := errors.New("request missing required field: Video.Mimes") - errL = append(errL, err) - } else { - mimes := make([]string, 0, 0) - for _, mime := range req.Video.Mimes { - if mime != "" { - mimes = append(mimes, mime) + if req.Video != nil { + if len(req.Video.MIMEs) == 0 { + err := errors.New("request missing required field: Video.Mimes") + errL = append(errL, err) + } else { + mimes := make([]string, 0, len(req.Video.MIMEs)) + for _, mime := range req.Video.MIMEs { + if mime != "" { + mimes = append(mimes, mime) + } + } + if len(mimes) == 0 { + err := errors.New("request missing required field: Video.Mimes, mime types contains empty strings only") + errL = append(errL, err) } + req.Video.MIMEs = mimes } - if len(mimes) == 0 { - err := errors.New("request missing required field: Video.Mimes, mime types contains empty strings only") + + if len(req.Video.Protocols) == 0 { + err := errors.New("request missing required field: Video.Protocols") errL = append(errL, err) } - if len(mimes) > 0 { - req.Video.Mimes = mimes - } - } - - if len(req.Video.Protocols) == 0 { - err := errors.New("request missing required field: Video.Protocols") + } else { + err := errors.New("request missing required field: Video") errL = append(errL, err) } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index b7c01d53505..c21b4324ba0 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1,11 +1,14 @@ package openrtb2 import ( + "bytes" "context" "encoding/json" "errors" "io/ioutil" + "net/http" "net/http/httptest" + "regexp" "strings" "testing" @@ -16,6 +19,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/exchange" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/empty_fetcher" metrics "github.com/rcrowley/go-metrics" @@ -42,7 +46,7 @@ func TestVideoEndpointImpressionsNumber(t *testing.T) { respBytes := recorder.Body.Bytes() resp := &openrtb_ext.BidResponseVideo{} if err := json.Unmarshal(respBytes, resp); err != nil { - t.Fatalf("Unable to umarshal response.") + t.Fatalf("Unable to unmarshal response.") } assert.Len(t, ex.lastRequest.Imp, 11, "Incorrect number of impressions in request") @@ -170,6 +174,112 @@ func TestCreateBidExtensionExactDurTrueNoPriceRange(t *testing.T) { assert.Equal(t, resExt.Prebid.Targeting.PriceGranularity, openrtb_ext.PriceGranularityFromString("med"), "Price granularity is incorrect") } +func TestVideoEndpointDebugQueryTrue(t *testing.T) { + ex := &mockExchangeVideo{ + cache: &mockCacheClient{}, + } + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + reqBody := string(getRequestPayload(t, reqData)) + req := httptest.NewRequest("POST", "/openrtb2/video?debug=true", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps := mockDeps(t, ex) + deps.VideoAuctionEndpoint(recorder, req, nil) + + if ex.lastRequest == nil { + t.Fatalf("The request never made it into the Exchange.") + } + if !ex.cache.called { + t.Fatalf("Cache was not called when it should have been") + } + + respBytes := recorder.Body.Bytes() + resp := &openrtb_ext.BidResponseVideo{} + if err := json.Unmarshal(respBytes, resp); err != nil { + t.Fatalf("Unable to unmarshal response.") + } + + assert.Len(t, ex.lastRequest.Imp, 11, "Incorrect number of impressions in request") + assert.Equal(t, string(ex.lastRequest.Site.Page), "prebid.com", "Incorrect site page in request") + assert.Equal(t, ex.lastRequest.Site.Content.Series, "TvName", "Incorrect site content series in request") + + assert.Len(t, resp.AdPods, 5, "Incorrect number of Ad Pods in response") + assert.Len(t, resp.AdPods[0].Targeting, 4, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[1].Targeting, 3, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[2].Targeting, 5, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[3].Targeting, 1, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[4].Targeting, 3, "Incorrect Targeting data in response") + + assert.Equal(t, resp.AdPods[4].Targeting[0].HbPbCatDur, "20.00_395_30s", "Incorrect number of Ad Pods in response") +} + +func TestVideoEndpointDebugQueryFalse(t *testing.T) { + ex := &mockExchangeVideo{ + cache: &mockCacheClient{}, + } + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + reqBody := string(getRequestPayload(t, reqData)) + req := httptest.NewRequest("POST", "/openrtb2/video?debug=123", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps := mockDeps(t, ex) + deps.VideoAuctionEndpoint(recorder, req, nil) + + if ex.lastRequest == nil { + t.Fatalf("The request never made it into the Exchange.") + } + if ex.cache.called { + t.Fatalf("Cache was called when it shouldn't have been") + } + + respBytes := recorder.Body.Bytes() + resp := &openrtb_ext.BidResponseVideo{} + if err := json.Unmarshal(respBytes, resp); err != nil { + t.Fatalf("Unable to unmarshal response.") + } + + assert.Len(t, ex.lastRequest.Imp, 11, "Incorrect number of impressions in request") + assert.Equal(t, string(ex.lastRequest.Site.Page), "prebid.com", "Incorrect site page in request") + assert.Equal(t, ex.lastRequest.Site.Content.Series, "TvName", "Incorrect site content series in request") + + assert.Len(t, resp.AdPods, 5, "Incorrect number of Ad Pods in response") + assert.Len(t, resp.AdPods[0].Targeting, 4, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[1].Targeting, 3, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[2].Targeting, 5, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[3].Targeting, 1, "Incorrect Targeting data in response") + assert.Len(t, resp.AdPods[4].Targeting, 3, "Incorrect Targeting data in response") + + assert.Equal(t, resp.AdPods[4].Targeting[0].HbPbCatDur, "20.00_395_30s", "Incorrect number of Ad Pods in response") +} + +func TestVideoEndpointDebugError(t *testing.T) { + ex := &mockExchangeVideo{ + cache: &mockCacheClient{}, + } + reqData, err := ioutil.ReadFile("sample-requests/video/video_invalid_sample.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + reqBody := string(getRequestPayload(t, reqData)) + req := httptest.NewRequest("POST", "/openrtb2/video?debug=true", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps := mockDeps(t, ex) + deps.VideoAuctionEndpoint(recorder, req, nil) + + if !ex.cache.called { + t.Fatalf("Cache was not called when it should have been") + } + + assert.Equal(t, recorder.Code, 500, "Should catch error in request") +} + func TestVideoEndpointNoPods(t *testing.T) { ex := &mockExchangeVideo{} reqData, err := ioutil.ReadFile("sample-requests/video/video_invalid_sample.json") @@ -233,8 +343,8 @@ func TestVideoEndpointValidationsPositive(t *testing.T) { IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ PrimaryAdserver: 1, }, - Video: openrtb_ext.SimplifiedVideo{ - Mimes: mimes, + Video: &openrtb.Video{ + MIMEs: mimes, Protocols: videoProtocols, }, } @@ -271,8 +381,8 @@ func TestVideoEndpointValidationsCritical(t *testing.T) { IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ PrimaryAdserver: 0, }, - Video: openrtb_ext.SimplifiedVideo{ - Mimes: mimes, + Video: &openrtb.Video{ + MIMEs: mimes, Protocols: videoProtocols, }, } @@ -345,8 +455,8 @@ func TestVideoEndpointValidationsPodErrors(t *testing.T) { IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ PrimaryAdserver: 1, }, - Video: openrtb_ext.SimplifiedVideo{ - Mimes: mimes, + Video: &openrtb.Video{ + MIMEs: mimes, Protocols: videoProtocols, }, } @@ -418,8 +528,8 @@ func TestVideoEndpointValidationsSiteAndApp(t *testing.T) { IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ PrimaryAdserver: 1, }, - Video: openrtb_ext.SimplifiedVideo{ - Mimes: mimes, + Video: &openrtb.Video{ + MIMEs: mimes, Protocols: videoProtocols, }, } @@ -473,8 +583,8 @@ func TestVideoEndpointValidationsSiteMissingRequiredField(t *testing.T) { IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ PrimaryAdserver: 1, }, - Video: openrtb_ext.SimplifiedVideo{ - Mimes: mimes, + Video: &openrtb.Video{ + MIMEs: mimes, Protocols: videoProtocols, }, } @@ -484,6 +594,43 @@ func TestVideoEndpointValidationsSiteMissingRequiredField(t *testing.T) { assert.Len(t, podErrors, 0, "Pod errors should be empty") } +func TestVideoEndpointValidationsMissingVideo(t *testing.T) { + ex := &mockExchangeVideo{} + deps := mockDeps(t, ex) + deps.cfg.VideoStoredRequestRequired = true + + req := openrtb_ext.BidRequestVideo{ + StoredRequestId: "123", + PodConfig: openrtb_ext.PodConfig{ + DurationRangeSec: []int{15, 30}, + RequireExactDuration: true, + Pods: []openrtb_ext.Pod{ + { + PodId: 1, + AdPodDurationSec: 30, + ConfigId: "qwerty", + }, + { + PodId: 2, + AdPodDurationSec: 30, + ConfigId: "qwerty", + }, + }, + }, + App: &openrtb.App{ + Bundle: "pbs.com", + }, + IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{ + PrimaryAdserver: 1, + }, + } + + errors, podErrors := deps.validateVideoRequest(&req) + assert.Len(t, podErrors, 0, "Pod errors should be empty") + assert.Len(t, errors, 1, "Errors array should contain 1 error message") + assert.Equal(t, "request missing required field: Video", errors[0].Error(), "Errors array should contain message regarding missing Video field") +} + func TestVideoBuildVideoResponseMissedCacheForOneBid(t *testing.T) { openRtbBidResp := openrtb.BidResponse{} podErrors := make([]PodError, 0) @@ -633,6 +780,13 @@ func TestMergeOpenRTBToVideoRequest(t *testing.T) { Ext: json.RawMessage(`{"gdpr":1,"us_privacy":"1NYY","existing":"any","consent":"anyConsent"}`), } + videoReq.User = &openrtb.User{ + BuyerUID: "test UID", + Yob: 1980, + Keywords: "test keywords", + Ext: json.RawMessage(`{"consent":"test string"}`), + } + mergeData(videoReq, bidReq) assert.Equal(t, videoReq.BCat, bidReq.BCat, "BCat is incorrect") @@ -647,6 +801,8 @@ func TestMergeOpenRTBToVideoRequest(t *testing.T) { assert.Equal(t, videoReq.Site.Page, bidReq.Site.Page, "Device.Site.Page is incorrect") assert.Equal(t, videoReq.Regs, bidReq.Regs, "Regs is incorrect") + + assert.Equal(t, videoReq.User, bidReq.User, "User is incorrect") } func TestHandleError(t *testing.T) { @@ -667,7 +823,7 @@ func TestHandleError(t *testing.T) { recorder := httptest.NewRecorder() err1 := errors.New("Error for testing handleError 1") err2 := errors.New("Error for testing handleError 2") - handleError(&labels, recorder, []error{err1, err2}, &vo) + handleError(&labels, recorder, []error{err1, err2}, &vo, nil) assert.Equal(t, pbsmetrics.RequestStatusErr, labels.RequestStatus, "labels.RequestStatus should indicate an error") assert.Equal(t, 500, recorder.Code, "Error status should be written to writer") @@ -699,6 +855,258 @@ func TestHandleErrorMetrics(t *testing.T) { assert.Equal(t, "request missing required field: PodConfig.Pods", mod.videoObjects[0].Errors[1].Error(), "Second error in AnalyticsObject should have message regarding Pods") } +func TestParseVideoRequestWithUserAgentAndHeader(t *testing.T) { + ex := &mockExchangeVideo{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_with_device_user_agent.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + headers := http.Header{} + headers.Add("User-Agent", "TestHeader") + + deps := mockDeps(t, ex) + reqBody := string(getRequestPayload(t, reqData)) + req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers) + + assert.Equal(t, "TestHeaderSample", req.Device.UA, "Header should be taken from original request") + assert.Equal(t, []error(nil), valErr, "No validation errors should be returned") + assert.Equal(t, make([]PodError, 0), podErr, "No pod errors should be returned") + +} + +func TestParseVideoRequestWithUserAgentAndEmptyHeader(t *testing.T) { + ex := &mockExchangeVideo{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_with_device_user_agent.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + + headers := http.Header{} + + deps := mockDeps(t, ex) + reqBody := string(getRequestPayload(t, reqData)) + req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers) + + assert.Equal(t, "TestHeaderSample", req.Device.UA, "Header should be taken from original request") + assert.Equal(t, []error(nil), valErr, "No validation errors should be returned") + assert.Equal(t, make([]PodError, 0), podErr, "No pod errors should be returned") + +} + +func TestParseVideoRequestWithoutUserAgentWithHeader(t *testing.T) { + ex := &mockExchangeVideo{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_without_device_user_agent.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + + headers := http.Header{} + headers.Add("User-Agent", "TestHeader") + + deps := mockDeps(t, ex) + reqBody := string(getRequestPayload(t, reqData)) + req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers) + + assert.Equal(t, "TestHeader", req.Device.UA, "Device.ua should be taken from request header") + assert.Equal(t, []error(nil), valErr, "No validation errors should be returned") + assert.Equal(t, make([]PodError, 0), podErr, "No pod errors should be returned") + +} + +func TestParseVideoRequestWithoutUserAgentAndEmptyHeader(t *testing.T) { + ex := &mockExchangeVideo{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_without_device_user_agent.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + + headers := http.Header{} + + deps := mockDeps(t, ex) + reqBody := string(getRequestPayload(t, reqData)) + req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers) + + assert.Equal(t, "", req.Device.UA, "Device.ua should be empty") + assert.Equal(t, []error(nil), valErr, "No validation errors should be returned") + assert.Equal(t, make([]PodError, 0), podErr, "No pod errors should be returned") + +} + +func TestParseVideoRequestWithEncodedUserAgentInHeader(t *testing.T) { + ex := &mockExchangeVideo{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_without_device_user_agent.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + + uaEncoded := "Mozilla%2F5.0%20%28Macintosh%3B%20Intel%20Mac%20OS%20X%2010_14_6%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F78.0.3904.87%20Safari%2F537.36" + uaDecoded := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36" + + headers := http.Header{} + headers.Add("User-Agent", uaEncoded) + + deps := mockDeps(t, ex) + reqBody := string(getRequestPayload(t, reqData)) + req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers) + + assert.Equal(t, uaDecoded, req.Device.UA, "Device.ua should be taken from request header") + assert.Equal(t, []error(nil), valErr, "No validation errors should be returned") + assert.Equal(t, make([]PodError, 0), podErr, "No pod errors should be returned") + +} + +func TestParseVideoRequestWithDecodedUserAgentInHeader(t *testing.T) { + ex := &mockExchangeVideo{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_without_device_user_agent.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + + uaDecoded := "Mozilla/5.0+(Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36" + + headers := http.Header{} + headers.Add("User-Agent", uaDecoded) + + deps := mockDeps(t, ex) + reqBody := string(getRequestPayload(t, reqData)) + req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers) + + assert.Equal(t, uaDecoded, req.Device.UA, "Device.ua should be taken from request header") + assert.Equal(t, []error(nil), valErr, "No validation errors should be returned") + assert.Equal(t, make([]PodError, 0), podErr, "No pod errors should be returned") + +} + +func TestHandleErrorDebugLog(t *testing.T) { + vo := analytics.VideoObject{ + Status: 200, + Errors: make([]error, 0), + } + + labels := pbsmetrics.Labels{ + Source: pbsmetrics.DemandUnknown, + RType: pbsmetrics.ReqTypeVideo, + PubID: pbsmetrics.PublisherUnknown, + Browser: "test browser", + CookieFlag: pbsmetrics.CookieFlagUnknown, + RequestStatus: pbsmetrics.RequestStatusOK, + } + + recorder := httptest.NewRecorder() + err1 := errors.New("Error for testing handleError 1") + err2 := errors.New("Error for testing handleError 2") + debugLog := exchange.DebugLog{ + Enabled: true, + CacheType: prebid_cache_client.TypeXML, + Data: exchange.DebugData{ + Request: "test request string", + Headers: "test headers string", + Response: "test response string", + }, + TTL: int64(3600), + Regexp: regexp.MustCompile(`[<>]`), + } + handleError(&labels, recorder, []error{err1, err2}, &vo, &debugLog) + + assert.Equal(t, pbsmetrics.RequestStatusErr, labels.RequestStatus, "labels.RequestStatus should indicate an error") + assert.Equal(t, 500, recorder.Code, "Error status should be written to writer") + assert.Equal(t, 500, vo.Status, "Analytics object should have error status") + assert.Equal(t, 3, len(vo.Errors), "New errors including debug cache ID should be appended to Analytics object Errors") + assert.Equal(t, "Error for testing handleError 1", vo.Errors[0].Error(), "Error in Analytics object should have test error message for first error") + assert.Equal(t, "Error for testing handleError 2", vo.Errors[1].Error(), "Error in Analytics object should have test error message for second error") + assert.NotEmpty(t, debugLog.CacheKey, "DebugLog CacheKey value should have been set") +} + +func TestCreateImpressionTemplate(t *testing.T) { + + imp := openrtb.Imp{} + imp.Video = &openrtb.Video{} + imp.Video.Protocols = []openrtb.Protocol{1, 2} + imp.Video.MIMEs = []string{"video/mp4"} + imp.Video.H = 200 + imp.Video.W = 400 + imp.Video.PlaybackMethod = []openrtb.PlaybackMethod{5, 6} + + video := openrtb.Video{} + video.Protocols = []openrtb.Protocol{3, 4} + video.MIMEs = []string{"video/flv"} + video.H = 300 + video.W = 0 + video.PlaybackMethod = []openrtb.PlaybackMethod{7, 8} + + res := createImpressionTemplate(imp, &video) + assert.Equal(t, res.Video.Protocols, []openrtb.Protocol{3, 4}, "Incorrect video protocols") + assert.Equal(t, res.Video.MIMEs, []string{"video/flv"}, "Incorrect video MIMEs") + assert.Equal(t, int(res.Video.H), 300, "Incorrect video height") + assert.Equal(t, int(res.Video.W), 0, "Incorrect video width") + assert.Equal(t, res.Video.PlaybackMethod, []openrtb.PlaybackMethod{7, 8}, "Incorrect video playback method") +} + +func TestCCPA(t *testing.T) { + testCases := []struct { + description string + testFilePath string + expectConsentString bool + }{ + { + description: "Missing Consent", + testFilePath: "sample-requests/video/video_valid_sample.json", + expectConsentString: false, + }, + { + description: "Valid Consent", + testFilePath: "sample-requests/video/video_valid_sample_ccpa_valid.json", + expectConsentString: true, + }, + { + description: "Malformed Consent", + testFilePath: "sample-requests/video/video_valid_sample_ccpa_malformed.json", + expectConsentString: false, + }, + } + + for _, test := range testCases { + // Load Test Request + requestContainerBytes, err := ioutil.ReadFile(test.testFilePath) + if err != nil { + t.Fatalf("%s: Failed to fetch a valid request: %v", test.description, err) + } + requestBytes := getRequestPayload(t, requestContainerBytes) + + // Create HTTP Request + Response Recorder + httpRequest := httptest.NewRequest("POST", "/openrtb2/video", bytes.NewReader(requestBytes)) + httpResponseRecorder := httptest.NewRecorder() + + // Run Test + ex := &mockExchangeVideo{} + mockDeps(t, ex).VideoAuctionEndpoint(httpResponseRecorder, httpRequest, nil) + + // Validate Request To Exchange + // - An error should never be generated for CCPA problems. + if ex.lastRequest == nil { + t.Fatalf("%s: The request never made it into the exchange.", test.description) + } + extRegs := &openrtb_ext.ExtRegs{} + if err = json.Unmarshal(ex.lastRequest.Regs.Ext, extRegs); err != nil { + t.Fatalf("%s: Failed to unmarshal reg.ext in request to the exchange: %v", test.description, err) + } + if test.expectConsentString { + assert.Len(t, extRegs.USPrivacy, 4, test.description+":consent") + } else { + assert.Empty(t, extRegs.USPrivacy, test.description+":consent") + } + + // Validate HTTP Response + responseBytes := httpResponseRecorder.Body.Bytes() + response := &openrtb_ext.BidResponseVideo{} + if err := json.Unmarshal(responseBytes, response); err != nil { + t.Fatalf("%s: Unable to unmarshal response.", test.description) + } + assert.Len(t, ex.lastRequest.Imp, 11, test.description+":imps") + assert.Len(t, response.AdPods, 5, test.description+":adpods") + } +} + func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) mockModule := &mockAnalyticsModule{} @@ -715,6 +1123,8 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *p false, []byte{}, openrtb_ext.BidderMap, + nil, + nil, } return edep, theMetrics, mockModule @@ -754,11 +1164,28 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { false, []byte{}, openrtb_ext.BidderMap, + ex.cache, + regexp.MustCompile(`[<>]`), } return edep } +type mockCacheClient struct { + called bool +} + +func (m *mockCacheClient) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) { + if !m.called { + m.called = true + } + return []string{}, []error{} +} + +func (m *mockCacheClient) GetExtCacheData() (string, string) { + return "", "" +} + type mockVideoStoredReqFetcher struct { } @@ -768,10 +1195,14 @@ func (cf mockVideoStoredReqFetcher) FetchRequests(ctx context.Context, requestID type mockExchangeVideo struct { lastRequest *openrtb.BidRequest + cache *mockCacheClient } -func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher) (*openrtb.BidResponse, error) { +func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { m.lastRequest = bidRequest + if debugLog != nil && debugLog.Enabled { + m.cache.called = true + } ext := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"20.00","hb_pb_cat_dur":"20.00_395_30s","hb_size":"1x1", "hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"},"type":"video"},"bidder":{"appnexus":{"brand_id":1,"auction_id":7840037870526938650,"bidder_id":2,"bid_ad_type":1,"creative_info":{"video":{"duration":30,"mimes":["video\/mp4"]}}}}}`) return &openrtb.BidResponse{ SeatBid: []openrtb.SeatBid{{ @@ -806,3 +1237,19 @@ var testVideoStoredImpData = map[string]json.RawMessage{ var testVideoStoredRequestData = map[string]json.RawMessage{ "80ce30c53c16e6ede735f123ef6e32361bfc7b22": json.RawMessage(`{"accountid": "11223344", "site": {"page": "mygame.foo.com"}}`), } + +func loadValidRequest(t *testing.T) *openrtb_ext.BidRequestVideo { + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample.json") + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) + } + + reqBody := getRequestPayload(t, reqData) + + reqVideo := &openrtb_ext.BidRequestVideo{} + if err := json.Unmarshal(reqBody, reqVideo); err != nil { + t.Fatalf("Failed to unmarshal the request: %v", err) + } + + return reqVideo +} diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index df4b873c57f..7b056d85f4b 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -10,12 +10,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/privacy" "github.com/PubMatic-OpenWrap/prebid-server/usersync" + "github.com/stretchr/testify/assert" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" @@ -444,8 +443,12 @@ func (g *mockPermsSetUID) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return false, nil } -func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return g.allowPI, nil +func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return g.allowPI, g.allowPI, nil +} + +func (g *mockPermsSetUID) AMPException() bool { + return false } func newFakeSyncer(familyName string) usersync.Usersyncer { diff --git a/errortypes/code.go b/errortypes/code.go new file mode 100644 index 00000000000..7f3833a46f1 --- /dev/null +++ b/errortypes/code.go @@ -0,0 +1,35 @@ +package errortypes + +// Defines numeric codes for well-known errors. +const ( + UnknownErrorCode = 999 + TimeoutErrorCode = iota + BadInputErrorCode + BlacklistedAppErrorCode + BadServerResponseErrorCode + FailedToRequestBidsErrorCode + BidderTemporarilyDisabledErrorCode + BlacklistedAcctErrorCode + AcctRequiredErrorCode + BidderFailedSchemaValidationErrorCode +) + +// Defines numeric codes for well-known warnings. +const ( + UnknownWarningCode = 10999 + InvalidPrivacyConsentWarningCode = iota + 10000 +) + +// Coder provides an error or warning code with severity. +type Coder interface { + Code() int + Severity() Severity +} + +// ReadCode returns the error or warning code, or UnknownErrorCode if unavailable. +func ReadCode(err error) int { + if e, ok := err.(Coder); ok { + return e.Code() + } + return UnknownErrorCode +} diff --git a/errortypes/code_test.go b/errortypes/code_test.go new file mode 100644 index 00000000000..b2bf53b8340 --- /dev/null +++ b/errortypes/code_test.go @@ -0,0 +1,24 @@ +package errortypes + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadCodeWithCodeDefined(t *testing.T) { + err := &Timeout{Message: "code is defined"} + + result := ReadCode(err) + + assert.Equal(t, result, TimeoutErrorCode) +} + +func TestReadCodeWithCodeNotDefined(t *testing.T) { + err := errors.New("missing error code") + + result := ReadCode(err) + + assert.Equal(t, result, UnknownErrorCode) +} diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index 393f14c7655..353463611b7 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -1,29 +1,5 @@ package errortypes -// These define the error codes for all the errors enumerated in this package -// NoErrorCode is to reserve 0 for non error states. -const ( - NoErrorCode = iota - TimeoutCode - BadInputCode - BlacklistedAppCode - BadServerResponseCode - FailedToRequestBidsCode - BidderTemporarilyDisabledCode - BlacklistedAcctCode - AcctRequiredCode - WarningCode - BidderFailedSchemaValidationCode -) - -// We should use this code for any Error interface that is not in this package -const UnknownErrorCode = 999 - -// Coder provides an interface to use if we want to check the code of an error type created in this package. -type Coder interface { - Code() int -} - // Timeout should be used to flag that a bidder failed to return a response because the PBS timeout timer // expired before a result was received. // @@ -37,7 +13,11 @@ func (err *Timeout) Error() string { } func (err *Timeout) Code() int { - return TimeoutCode + return TimeoutErrorCode +} + +func (err *Timeout) Severity() Severity { + return SeverityFatal } // BadInput should be used when returning errors which are caused by bad input. @@ -53,7 +33,11 @@ func (err *BadInput) Error() string { } func (err *BadInput) Code() int { - return BadInputCode + return BadInputErrorCode +} + +func (err *BadInput) Severity() Severity { + return SeverityFatal } // BlacklistedApp should be used when a request App.ID matches an entry in the BlacklistedApps @@ -69,7 +53,11 @@ func (err *BlacklistedApp) Error() string { } func (err *BlacklistedApp) Code() int { - return BlacklistedAppCode + return BlacklistedAppErrorCode +} + +func (err *BlacklistedApp) Severity() Severity { + return SeverityFatal } // BlacklistedAcct should be used when a request account ID matches an entry in the BlacklistedAccts @@ -85,7 +73,11 @@ func (err *BlacklistedAcct) Error() string { } func (err *BlacklistedAcct) Code() int { - return BlacklistedAcctCode + return BlacklistedAcctErrorCode +} + +func (err *BlacklistedAcct) Severity() Severity { + return SeverityFatal } // AcctRequired should be used when the environment variable ACCOUNT_REQUIRED has been set to not @@ -101,7 +93,11 @@ func (err *AcctRequired) Error() string { } func (err *AcctRequired) Code() int { - return AcctRequiredCode + return AcctRequiredErrorCode +} + +func (err *AcctRequired) Severity() Severity { + return SeverityFatal } // BadServerResponse should be used when returning errors which are caused by bad/unexpected behavior on the remote server. @@ -122,7 +118,11 @@ func (err *BadServerResponse) Error() string { } func (err *BadServerResponse) Code() int { - return BadServerResponseCode + return BadServerResponseErrorCode +} + +func (err *BadServerResponse) Severity() Severity { + return SeverityFatal } // FailedToRequestBids is an error to cover the case where an adapter failed to generate any http requests to get bids, @@ -139,7 +139,11 @@ func (err *FailedToRequestBids) Error() string { } func (err *FailedToRequestBids) Code() int { - return FailedToRequestBidsCode + return FailedToRequestBidsErrorCode +} + +func (err *FailedToRequestBids) Severity() Severity { + return SeverityFatal } // BidderTemporarilyDisabled is used at the request validation step, where we want to continue processing as best we @@ -154,10 +158,14 @@ func (err *BidderTemporarilyDisabled) Error() string { } func (err *BidderTemporarilyDisabled) Code() int { - return BidderTemporarilyDisabledCode + return BidderTemporarilyDisabledErrorCode +} + +func (err *BidderTemporarilyDisabled) Severity() Severity { + return SeverityWarning } -// Warning is a generic warning type, not a serious error +// Warning is a generic non-fatal error. type Warning struct { Message string } @@ -166,9 +174,29 @@ func (err *Warning) Error() string { return err.Message } -// Code returns the error code func (err *Warning) Code() int { - return WarningCode + return UnknownWarningCode +} + +func (err *Warning) Severity() Severity { + return SeverityWarning +} + +// InvalidPrivacyConsent is a warning for when the privacy consent string is invalid and is ignored. +type InvalidPrivacyConsent struct { + Message string +} + +func (err *InvalidPrivacyConsent) Error() string { + return err.Message +} + +func (err *InvalidPrivacyConsent) Code() int { + return InvalidPrivacyConsentWarningCode +} + +func (err *InvalidPrivacyConsent) Severity() Severity { + return SeverityWarning } // BidderFailedSchemaValidation is used at the request validation step, @@ -183,13 +211,9 @@ func (err *BidderFailedSchemaValidation) Error() string { } func (err *BidderFailedSchemaValidation) Code() int { - return BidderFailedSchemaValidationCode + return BidderFailedSchemaValidationErrorCode } -// DecodeError provides the error code for an error, as defined above -func DecodeError(err error) int { - if ce, ok := err.(Coder); ok { - return ce.Code() - } - return UnknownErrorCode +func (err *BidderFailedSchemaValidation) Severity() Severity { + return SeverityWarning } diff --git a/errortypes/severity.go b/errortypes/severity.go new file mode 100644 index 00000000000..0838b09592e --- /dev/null +++ b/errortypes/severity.go @@ -0,0 +1,63 @@ +package errortypes + +// Severity represents the severity level of a bid processing error. +type Severity int + +const ( + // SeverityUnknown represents an unknown severity level. + SeverityUnknown Severity = iota + + // SeverityFatal represents a fatal bid processing error which prevents a bid response. + SeverityFatal + + // SeverityWarning represents a non-fatal bid processing error where invalid or ambiguous + // data in the bid request was ignored. + SeverityWarning +) + +func isFatal(err error) bool { + s, ok := err.(Coder) + return !ok || s.Severity() == SeverityFatal +} + +func isWarning(err error) bool { + s, ok := err.(Coder) + return ok && s.Severity() == SeverityWarning +} + +// ContainsFatalError checks if the error list contains a fatal error. +func ContainsFatalError(errors []error) bool { + for _, err := range errors { + if isFatal(err) { + return true + } + } + + return false +} + +// FatalOnly returns a new error list with only the fatal severity errors. +func FatalOnly(errs []error) []error { + errsFatal := make([]error, 0, len(errs)) + + for _, err := range errs { + if isFatal(err) { + errsFatal = append(errsFatal, err) + } + } + + return errsFatal +} + +// WarningOnly returns a new error list with only the warning severity errors. +func WarningOnly(errs []error) []error { + errsWarning := make([]error, 0, len(errs)) + + for _, err := range errs { + if isWarning(err) { + errsWarning = append(errsWarning, err) + } + } + + return errsWarning +} diff --git a/errortypes/severity_test.go b/errortypes/severity_test.go new file mode 100644 index 00000000000..8330316a8d2 --- /dev/null +++ b/errortypes/severity_test.go @@ -0,0 +1,143 @@ +package errortypes + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +type stubError struct{ severity Severity } + +func (e *stubError) Error() string { return "anyMessage" } +func (e *stubError) Code() int { return 42 } +func (e *stubError) Severity() Severity { return e.severity } + +func TestContainsFatalError(t *testing.T) { + fatalError := &stubError{severity: SeverityFatal} + notFatalError := &stubError{severity: SeverityWarning} + unknownSeverityError := errors.New("anyError") + + testCases := []struct { + description string + errors []error + shouldBeFatal bool + }{ + { + description: "None", + errors: []error{}, + shouldBeFatal: false, + }, + { + description: "One - Fatal", + errors: []error{fatalError}, + shouldBeFatal: true, + }, + { + description: "One - Not Fatal", + errors: []error{notFatalError}, + shouldBeFatal: false, + }, + { + description: "One - Unknown Severity Same As Fatal", + errors: []error{unknownSeverityError}, + shouldBeFatal: true, + }, + { + description: "Mixed", + errors: []error{fatalError, notFatalError, unknownSeverityError}, + shouldBeFatal: true, + }, + } + + for _, tc := range testCases { + result := ContainsFatalError(tc.errors) + assert.Equal(t, tc.shouldBeFatal, result) + } +} + +func TestFatalOnly(t *testing.T) { + fatalError := &stubError{severity: SeverityFatal} + notFatalError := &stubError{severity: SeverityWarning} + unknownSeverityError := errors.New("anyError") + + testCases := []struct { + description string + errs []error + errsShouldBeFatal []error + }{ + { + description: "None", + errs: []error{}, + errsShouldBeFatal: []error{}, + }, + { + description: "One - Fatal", + errs: []error{fatalError}, + errsShouldBeFatal: []error{fatalError}, + }, + { + description: "One - Not Fatal", + errs: []error{notFatalError}, + errsShouldBeFatal: []error{}, + }, + { + description: "One - Unknown Severity Same As Fatal", + errs: []error{unknownSeverityError}, + errsShouldBeFatal: []error{unknownSeverityError}, + }, + { + description: "Mixed", + errs: []error{fatalError, notFatalError, unknownSeverityError}, + errsShouldBeFatal: []error{fatalError, unknownSeverityError}, + }, + } + + for _, tc := range testCases { + result := FatalOnly(tc.errs) + assert.ElementsMatch(t, tc.errsShouldBeFatal, result) + } +} + +func TestWarningOnly(t *testing.T) { + warningError := &stubError{severity: SeverityWarning} + notWarningError := &stubError{severity: SeverityFatal} + unknownSeverityError := errors.New("anyError") + + testCases := []struct { + description string + errs []error + errsShouldBeWarning []error + }{ + { + description: "None", + errs: []error{}, + errsShouldBeWarning: []error{}, + }, + { + description: "One - Warning", + errs: []error{warningError}, + errsShouldBeWarning: []error{warningError}, + }, + { + description: "One - Not Warning", + errs: []error{notWarningError}, + errsShouldBeWarning: []error{}, + }, + { + description: "One - Unknown Severity Not Warning", + errs: []error{unknownSeverityError}, + errsShouldBeWarning: []error{}, + }, + { + description: "One - Mixed", + errs: []error{warningError, notWarningError, unknownSeverityError}, + errsShouldBeWarning: []error{warningError}, + }, + } + + for _, tc := range testCases { + result := WarningOnly(tc.errs) + assert.ElementsMatch(t, tc.errsShouldBeWarning, result) + } +} diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go old mode 100644 new mode 100755 index 13934fdb368..c01dc64da52 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -2,28 +2,38 @@ package exchange import ( "fmt" - "github.com/PubMatic-OpenWrap/prebid-server/adapters/telaria" "net/http" "strings" - "github.com/PubMatic-OpenWrap/prebid-server/adapters/kubient" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/adapters" ttx "github.com/PubMatic-OpenWrap/prebid-server/adapters/33across" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adform" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adgeneration" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adhese" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adkernel" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adkernelAdn" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/admixer" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adocean" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adoppler" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adpone" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adtarget" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adtelligent" "github.com/PubMatic-OpenWrap/prebid-server/adapters/advangelists" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/aja" "github.com/PubMatic-OpenWrap/prebid-server/adapters/applogy" "github.com/PubMatic-OpenWrap/prebid-server/adapters/appnexus" "github.com/PubMatic-OpenWrap/prebid-server/adapters/audienceNetwork" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/avocet" "github.com/PubMatic-OpenWrap/prebid-server/adapters/beachfront" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/beintoo" "github.com/PubMatic-OpenWrap/prebid-server/adapters/brightroll" "github.com/PubMatic-OpenWrap/prebid-server/adapters/consumable" "github.com/PubMatic-OpenWrap/prebid-server/adapters/conversant" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/cpmstar" "github.com/PubMatic-OpenWrap/prebid-server/adapters/datablocks" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/dmx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/emx_digital" "github.com/PubMatic-OpenWrap/prebid-server/adapters/engagebdr" "github.com/PubMatic-OpenWrap/prebid-server/adapters/eplanning" @@ -33,11 +43,18 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters/gumgum" "github.com/PubMatic-OpenWrap/prebid-server/adapters/improvedigital" "github.com/PubMatic-OpenWrap/prebid-server/adapters/ix" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/kidoz" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/kubient" "github.com/PubMatic-OpenWrap/prebid-server/adapters/lifestreet" "github.com/PubMatic-OpenWrap/prebid-server/adapters/lockerdome" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/lunamedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/marsmedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/mgid" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/mobilefuse" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/nanointeractive" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/ninthdecimal" "github.com/PubMatic-OpenWrap/prebid-server/adapters/openx" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/orbidder" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pubmatic" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pubnative" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pulsepoint" @@ -45,19 +62,27 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters/rtbhouse" "github.com/PubMatic-OpenWrap/prebid-server/adapters/rubicon" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sharethrough" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/smartrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters/somoaudience" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sonobi" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sovrn" "github.com/PubMatic-OpenWrap/prebid-server/adapters/spotx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/synacormedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/tappx" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/telaria" "github.com/PubMatic-OpenWrap/prebid-server/adapters/triplelift" "github.com/PubMatic-OpenWrap/prebid-server/adapters/triplelift_native" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/ucfunnel" "github.com/PubMatic-OpenWrap/prebid-server/adapters/unruly" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/valueimpression" "github.com/PubMatic-OpenWrap/prebid-server/adapters/verizonmedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/visx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/vrtcal" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/yeahmobi" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/yieldlab" "github.com/PubMatic-OpenWrap/prebid-server/adapters/yieldmo" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/yieldone" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/zeroclickfraud" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" ) @@ -65,43 +90,59 @@ import ( // The newAdapterMap function is segregated to its own file to make it a simple and clean location for each Adapter // to register itself. No wading through Exchange code to find it. -func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapters.BidderInfos) map[openrtb_ext.BidderName]adaptedBidder { +func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapters.BidderInfos, me pbsmetrics.MetricsEngine) map[openrtb_ext.BidderName]adaptedBidder { ortbBidders := map[openrtb_ext.BidderName]adapters.Bidder{ openrtb_ext.Bidder33Across: ttx.New33AcrossBidder(cfg.Adapters[string(openrtb_ext.Bidder33Across)].Endpoint), openrtb_ext.BidderAdform: adform.NewAdformBidder(client, cfg.Adapters[string(openrtb_ext.BidderAdform)].Endpoint), + openrtb_ext.BidderAdgeneration: adgeneration.NewAdgenerationAdapter(cfg.Adapters[string(openrtb_ext.BidderAdgeneration)].Endpoint), + openrtb_ext.BidderAdhese: adhese.NewAdheseBidder(cfg.Adapters[string(openrtb_ext.BidderAdhese)].Endpoint), openrtb_ext.BidderAdkernel: adkernel.NewAdkernelAdapter(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernel))].Endpoint), openrtb_ext.BidderAdkernelAdn: adkernelAdn.NewAdkernelAdnAdapter(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernelAdn))].Endpoint), + openrtb_ext.BidderAdmixer: admixer.NewAdmixerBidder(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdmixer))].Endpoint), + openrtb_ext.BidderAdOcean: adocean.NewAdOceanBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdOcean))].Endpoint), + openrtb_ext.BidderAdoppler: adoppler.NewAdopplerBidder(cfg.Adapters[string(openrtb_ext.BidderAdoppler)].Endpoint), openrtb_ext.BidderAdpone: adpone.NewAdponeBidder(cfg.Adapters[string(openrtb_ext.BidderAdpone)].Endpoint), + openrtb_ext.BidderAdtarget: adtarget.NewAdtargetBidder(cfg.Adapters[string(openrtb_ext.BidderAdtarget)].Endpoint), openrtb_ext.BidderAdtelligent: adtelligent.NewAdtelligentBidder(cfg.Adapters[string(openrtb_ext.BidderAdtelligent)].Endpoint), openrtb_ext.BidderAdvangelists: advangelists.NewAdvangelistsBidder(cfg.Adapters[string(openrtb_ext.BidderAdvangelists)].Endpoint), + openrtb_ext.BidderAJA: aja.NewAJABidder(cfg.Adapters[string(openrtb_ext.BidderAJA)].Endpoint), openrtb_ext.BidderApplogy: applogy.NewApplogyBidder(cfg.Adapters[string(openrtb_ext.BidderApplogy)].Endpoint), openrtb_ext.BidderAppnexus: appnexus.NewAppNexusBidder(client, cfg.Adapters[string(openrtb_ext.BidderAppnexus)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderAppnexus)].PlatformID), - // 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.BidderDatablocks: datablocks.NewDatablocksBidder(cfg.Adapters[string(openrtb_ext.BidderDatablocks)].Endpoint), - openrtb_ext.BidderEmxDigital: emx_digital.NewEmxDigitalBidder(cfg.Adapters[string(openrtb_ext.BidderEmxDigital)].Endpoint), - openrtb_ext.BidderEngageBDR: engagebdr.NewEngageBDRBidder(client, cfg.Adapters[string(openrtb_ext.BidderEngageBDR)].Endpoint), - openrtb_ext.BidderEPlanning: eplanning.NewEPlanningBidder(client, cfg.Adapters[string(openrtb_ext.BidderEPlanning)].Endpoint), + openrtb_ext.BidderAvocet: avocet.NewAvocetAdapter(cfg.Adapters[string(openrtb_ext.BidderAvocet)].Endpoint), + openrtb_ext.BidderBeachfront: beachfront.NewBeachfrontBidder(cfg.Adapters[string(openrtb_ext.BidderBeachfront)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderBeachfront)].ExtraAdapterInfo), + openrtb_ext.BidderBeintoo: beintoo.NewBeintooBidder(cfg.Adapters[string(openrtb_ext.BidderBeintoo)].Endpoint), + 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.BidderCpmstar: cpmstar.NewCpmstarBidder(cfg.Adapters[string(openrtb_ext.BidderCpmstar)].Endpoint), + openrtb_ext.BidderDatablocks: datablocks.NewDatablocksBidder(cfg.Adapters[string(openrtb_ext.BidderDatablocks)].Endpoint), + openrtb_ext.BidderDmx: dmx.NewDmxBidder(cfg.Adapters[string(openrtb_ext.BidderDmx)].Endpoint), + openrtb_ext.BidderEmxDigital: emx_digital.NewEmxDigitalBidder(cfg.Adapters[string(openrtb_ext.BidderEmxDigital)].Endpoint), + openrtb_ext.BidderEngageBDR: engagebdr.NewEngageBDRBidder(client, cfg.Adapters[string(openrtb_ext.BidderEngageBDR)].Endpoint), + openrtb_ext.BidderEPlanning: eplanning.NewEPlanningBidder(client, cfg.Adapters[string(openrtb_ext.BidderEPlanning)].Endpoint), openrtb_ext.BidderFacebook: audienceNetwork.NewFacebookBidder( client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].PlatformID, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].AppSecret), - openrtb_ext.BidderGamma: gamma.NewGammaBidder(cfg.Adapters[string(openrtb_ext.BidderGamma)].Endpoint), - openrtb_ext.BidderGamoshi: gamoshi.NewGamoshiBidder(cfg.Adapters[string(openrtb_ext.BidderGamoshi)].Endpoint), - openrtb_ext.BidderGrid: grid.NewGridBidder(cfg.Adapters[string(openrtb_ext.BidderGrid)].Endpoint), - openrtb_ext.BidderGumGum: gumgum.NewGumGumBidder(cfg.Adapters[string(openrtb_ext.BidderGumGum)].Endpoint), - openrtb_ext.BidderImprovedigital: improvedigital.NewImprovedigitalBidder(cfg.Adapters[string(openrtb_ext.BidderImprovedigital)].Endpoint), - openrtb_ext.BidderKubient: kubient.NewKubientBidder(cfg.Adapters[string(openrtb_ext.BidderKubient)].Endpoint), - openrtb_ext.BidderLockerDome: lockerdome.NewLockerDomeBidder(cfg.Adapters[string(openrtb_ext.BidderLockerDome)].Endpoint), - openrtb_ext.BidderMarsmedia: marsmedia.NewMarsmediaBidder(cfg.Adapters[string(openrtb_ext.BidderMarsmedia)].Endpoint), - openrtb_ext.BidderMgid: mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].Endpoint), - openrtb_ext.BidderOpenx: openx.NewOpenxBidder(cfg.Adapters[string(openrtb_ext.BidderOpenx)].Endpoint), - openrtb_ext.BidderPubmatic: pubmatic.NewPubmaticBidder(client, cfg.Adapters[string(openrtb_ext.BidderPubmatic)].Endpoint), - openrtb_ext.BidderPubnative: pubnative.NewPubnativeBidder(cfg.Adapters[string(openrtb_ext.BidderPubnative)].Endpoint), - openrtb_ext.BidderRhythmone: rhythmone.NewRhythmoneBidder(cfg.Adapters[string(openrtb_ext.BidderRhythmone)].Endpoint), - openrtb_ext.BidderRTBHouse: rtbhouse.NewRTBHouseBidder(cfg.Adapters[string(openrtb_ext.BidderRTBHouse)].Endpoint), + openrtb_ext.BidderGamma: gamma.NewGammaBidder(cfg.Adapters[string(openrtb_ext.BidderGamma)].Endpoint), + openrtb_ext.BidderGamoshi: gamoshi.NewGamoshiBidder(cfg.Adapters[string(openrtb_ext.BidderGamoshi)].Endpoint), + openrtb_ext.BidderGrid: grid.NewGridBidder(cfg.Adapters[string(openrtb_ext.BidderGrid)].Endpoint), + openrtb_ext.BidderGumGum: gumgum.NewGumGumBidder(cfg.Adapters[string(openrtb_ext.BidderGumGum)].Endpoint), + openrtb_ext.BidderImprovedigital: improvedigital.NewImprovedigitalBidder(cfg.Adapters[string(openrtb_ext.BidderImprovedigital)].Endpoint), + openrtb_ext.BidderKidoz: kidoz.NewKidozBidder(cfg.Adapters[string(openrtb_ext.BidderKidoz)].Endpoint), + openrtb_ext.BidderKubient: kubient.NewKubientBidder(cfg.Adapters[string(openrtb_ext.BidderKubient)].Endpoint), + openrtb_ext.BidderLockerDome: lockerdome.NewLockerDomeBidder(cfg.Adapters[string(openrtb_ext.BidderLockerDome)].Endpoint), + openrtb_ext.BidderLunaMedia: lunamedia.NewLunaMediaBidder(cfg.Adapters[string(openrtb_ext.BidderLunaMedia)].Endpoint), + openrtb_ext.BidderMarsmedia: marsmedia.NewMarsmediaBidder(cfg.Adapters[string(openrtb_ext.BidderMarsmedia)].Endpoint), + openrtb_ext.BidderMgid: mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].Endpoint), + openrtb_ext.BidderMobileFuse: mobilefuse.NewMobileFuseBidder(cfg.Adapters[string(openrtb_ext.BidderMobileFuse)].Endpoint), + openrtb_ext.BidderNanoInteractive: nanointeractive.NewNanoIneractiveBidder(cfg.Adapters[string(openrtb_ext.BidderNanoInteractive)].Endpoint), + openrtb_ext.BidderNinthDecimal: ninthdecimal.NewNinthDecimalBidder(cfg.Adapters[string(openrtb_ext.BidderNinthDecimal)].Endpoint), + openrtb_ext.BidderOrbidder: orbidder.NewOrbidderBidder(cfg.Adapters[string(openrtb_ext.BidderOrbidder)].Endpoint), + openrtb_ext.BidderOpenx: openx.NewOpenxBidder(cfg.Adapters[string(openrtb_ext.BidderOpenx)].Endpoint), + openrtb_ext.BidderPubmatic: pubmatic.NewPubmaticBidder(client, cfg.Adapters[string(openrtb_ext.BidderPubmatic)].Endpoint), + openrtb_ext.BidderPubnative: pubnative.NewPubnativeBidder(cfg.Adapters[string(openrtb_ext.BidderPubnative)].Endpoint), + openrtb_ext.BidderRhythmone: rhythmone.NewRhythmoneBidder(cfg.Adapters[string(openrtb_ext.BidderRhythmone)].Endpoint), + openrtb_ext.BidderRTBHouse: rtbhouse.NewRTBHouseBidder(cfg.Adapters[string(openrtb_ext.BidderRTBHouse)].Endpoint), openrtb_ext.BidderRubicon: rubicon.NewRubiconBidder( client, cfg.Adapters[string(openrtb_ext.BidderRubicon)].Endpoint, @@ -110,6 +151,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Tracker), openrtb_ext.BidderSharethrough: sharethrough.NewSharethroughBidder(cfg.Adapters[string(openrtb_ext.BidderSharethrough)].Endpoint), + openrtb_ext.BidderSmartRTB: smartrtb.NewSmartRTBBidder(cfg.Adapters[string(openrtb_ext.BidderSmartRTB)].Endpoint), openrtb_ext.BidderSomoaudience: somoaudience.NewSomoaudienceBidder(cfg.Adapters[string(openrtb_ext.BidderSomoaudience)].Endpoint), openrtb_ext.BidderSonobi: sonobi.NewSonobiBidder(client, cfg.Adapters[string(openrtb_ext.BidderSonobi)].Endpoint), openrtb_ext.BidderSovrn: sovrn.NewSovrnBidder(client, cfg.Adapters[string(openrtb_ext.BidderSovrn)].Endpoint), @@ -119,11 +161,17 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderTelaria: telaria.NewTelariaBidder(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderTelaria))].Endpoint), openrtb_ext.BidderTriplelift: triplelift.NewTripleliftBidder(client, cfg.Adapters[string(openrtb_ext.BidderTriplelift)].Endpoint), openrtb_ext.BidderTripleliftNative: triplelift_native.NewTripleliftNativeBidder(client, cfg.Adapters[string(openrtb_ext.BidderTripleliftNative)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderTripleliftNative)].ExtraAdapterInfo), + openrtb_ext.BidderUcfunnel: ucfunnel.NewUcfunnelBidder(cfg.Adapters[string(openrtb_ext.BidderUcfunnel)].Endpoint), openrtb_ext.BidderUnruly: unruly.NewUnrulyBidder(client, cfg.Adapters[string(openrtb_ext.BidderUnruly)].Endpoint), + openrtb_ext.BidderValueImpression: valueimpression.NewValueImpressionBidder(cfg.Adapters[string(openrtb_ext.BidderValueImpression)].Endpoint), + openrtb_ext.BidderYieldlab: yieldlab.NewYieldlabBidder(cfg.Adapters[string(openrtb_ext.BidderYieldlab)].Endpoint), openrtb_ext.BidderVerizonMedia: verizonmedia.NewVerizonMediaBidder(client, cfg.Adapters[string(openrtb_ext.BidderVerizonMedia)].Endpoint), openrtb_ext.BidderVisx: visx.NewVisxBidder(cfg.Adapters[string(openrtb_ext.BidderVisx)].Endpoint), openrtb_ext.BidderVrtcal: vrtcal.NewVrtcalBidder(cfg.Adapters[string(openrtb_ext.BidderVrtcal)].Endpoint), + openrtb_ext.BidderYeahmobi: yeahmobi.NewYeahmobiBidder(cfg.Adapters[string(openrtb_ext.BidderYeahmobi)].Endpoint), openrtb_ext.BidderYieldmo: yieldmo.NewYieldmoBidder(cfg.Adapters[string(openrtb_ext.BidderYieldmo)].Endpoint), + openrtb_ext.BidderYieldone: yieldone.NewYieldoneBidder(cfg.Adapters[string(openrtb_ext.BidderYieldone)].Endpoint), + openrtb_ext.BidderZeroClickFraud: zeroclickfraud.NewZeroClickFraudBidder(cfg.Adapters[string(openrtb_ext.BidderZeroClickFraud)].Endpoint), } legacyBidders := map[openrtb_ext.BidderName]adapters.Adapter{ @@ -150,7 +198,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter for name, bidder := range ortbBidders { // Clean out any disabled bidders if infos[string(name)].Status == adapters.StatusActive { - allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client) + allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client, cfg, me) } } diff --git a/exchange/adapter_map_test.go b/exchange/adapter_map_test.go index 433fd13aeab..32321c30bd0 100644 --- a/exchange/adapter_map_test.go +++ b/exchange/adapter_map_test.go @@ -7,11 +7,12 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + metricsConfig "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics/config" ) func TestNewAdapterMap(t *testing.T) { cfg := &config.Configuration{Adapters: blankAdapterConfig(openrtb_ext.BidderList())} - adapterMap := newAdapterMap(nil, cfg, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList())) + adapterMap := newAdapterMap(nil, cfg, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), &metricsConfig.DummyMetricsEngine{}) for _, bidderName := range openrtb_ext.BidderMap { if bidder, ok := adapterMap[bidderName]; bidder == nil || !ok { t.Errorf("adapterMap missing expected Bidder: %s", string(bidderName)) @@ -38,7 +39,7 @@ func TestNewAdapterMapDisabledAdapters(t *testing.T) { } } } - adapterMap := newAdapterMap(nil, &config.Configuration{Adapters: cfgAdapters}, adapters.ParseBidderInfos(cfgAdapters, "../static/bidder-info", bidderList)) + adapterMap := newAdapterMap(nil, &config.Configuration{Adapters: cfgAdapters}, adapters.ParseBidderInfos(cfgAdapters, "../static/bidder-info", bidderList), &metricsConfig.DummyMetricsEngine{}) for _, bidderName := range openrtb_ext.BidderMap { if bidder, ok := adapterMap[bidderName]; bidder == nil || !ok { if inList(bidderList, bidderName) { diff --git a/exchange/auction.go b/exchange/auction.go index 17798d03345..1ead3c616c6 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -3,8 +3,10 @@ package exchange import ( "context" "encoding/json" + "encoding/xml" "errors" "fmt" + "regexp" "strings" "github.com/PubMatic-OpenWrap/openrtb" @@ -15,6 +17,36 @@ import ( "github.com/golang/glog" ) +type DebugLog struct { + Enabled bool + CacheType prebid_cache_client.PayloadType + Data DebugData + TTL int64 + CacheKey string + CacheString string + Regexp *regexp.Regexp +} + +type DebugData struct { + Request string + Headers string + Response string +} + +func (d *DebugLog) BuildCacheString() { + if d.Regexp != nil { + d.Data.Request = fmt.Sprintf(d.Regexp.ReplaceAllString(d.Data.Request, "")) + d.Data.Headers = fmt.Sprintf(d.Regexp.ReplaceAllString(d.Data.Headers, "")) + d.Data.Response = fmt.Sprintf(d.Regexp.ReplaceAllString(d.Data.Response, "")) + } + + d.Data.Request = fmt.Sprintf("%s", d.Data.Request) + d.Data.Headers = fmt.Sprintf("%s", d.Data.Headers) + d.Data.Response = fmt.Sprintf("%s", d.Data.Response) + + d.CacheString = fmt.Sprintf("%s%s%s%s", xml.Header, d.Data.Request, d.Data.Headers, d.Data.Response) +} + func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int) *auction { winningBids := make(map[string]*pbsOrtbBid, numImps) winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName]*pbsOrtbBid, numImps) @@ -60,7 +92,7 @@ func (a *auction) setRoundedPrices(priceGranularity openrtb_ext.PriceGranularity a.roundedPrices = roundedPrices } -func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, targData *targetData, bidRequest *openrtb.BidRequest, ttlBuffer int64, defaultTTLs *config.DefaultTTLs, bidCategory map[string]string) []error { +func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, targData *targetData, bidRequest *openrtb.BidRequest, ttlBuffer int64, defaultTTLs *config.DefaultTTLs, bidCategory map[string]string, debugLog *DebugLog) []error { var bids, vast, includeBidderKeys, includeWinners bool = targData.includeCacheBids, targData.includeCacheVast, targData.includeBidderKeys, targData.includeWinners if !((bids || vast) && (includeBidderKeys || includeWinners)) { return nil @@ -147,6 +179,19 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, } } + if debugLog != nil && debugLog.Enabled { + debugLog.BuildCacheString() + debugLog.CacheKey = hbCacheID + if jsonBytes, err := json.Marshal(debugLog.CacheString); err == nil { + toCache = append(toCache, prebid_cache_client.Cacheable{ + Type: debugLog.CacheType, + Data: jsonBytes, + TTLSeconds: debugLog.TTL, + Key: "log_" + debugLog.CacheKey, + }) + } + } + ids, err := cache.PutJson(ctx, toCache) if err != nil { errs = append(errs, err...) diff --git a/exchange/auction_test.go b/exchange/auction_test.go index 1675abe9094..36e06a7d70a 100644 --- a/exchange/auction_test.go +++ b/exchange/auction_test.go @@ -3,8 +3,10 @@ package exchange import ( "context" "encoding/json" + "encoding/xml" "fmt" "io/ioutil" + "regexp" "strconv" "strings" "testing" @@ -40,6 +42,61 @@ func TestMakeVASTNurl(t *testing.T) { assert.Equal(t, expect, vast) } +func TestBuildCacheString(t *testing.T) { + testCases := []struct { + description string + debugLog DebugLog + expectedDebugLog DebugLog + }{ + { + description: "DebugLog strings should have tags and be formatted", + debugLog: DebugLog{ + Data: DebugData{ + Request: "test request string", + Headers: "test headers string", + Response: "test response string", + }, + Regexp: regexp.MustCompile(`[<>]`), + }, + expectedDebugLog: DebugLog{ + Data: DebugData{ + Request: "test request string", + Headers: "test headers string", + Response: "test response string", + }, + Regexp: regexp.MustCompile(`[<>]`), + }, + }, + { + description: "DebugLog strings should have no < or > characters", + debugLog: DebugLog{ + Data: DebugData{ + Request: "test request string", + Headers: "test string", + }, + Regexp: regexp.MustCompile(`[<>]`), + }, + expectedDebugLog: DebugLog{ + Data: DebugData{ + Request: "testtest request string/test", + Headers: "test headers string", + Response: "test response string", + }, + Regexp: regexp.MustCompile(`[<>]`), + }, + }, + } + + for _, test := range testCases { + test.expectedDebugLog.CacheString = fmt.Sprintf("%s%s%s%s", xml.Header, test.expectedDebugLog.Data.Request, test.expectedDebugLog.Data.Headers, test.expectedDebugLog.Data.Response) + + test.debugLog.BuildCacheString() + + assert.Equal(t, test.expectedDebugLog, test.debugLog, test.description) + } +} + // TestCacheJSON executes tests for all the *.json files in cachetest. // customcachekey.json test here verifies custom cache key not used for non-vast video func TestCacheJSON(t *testing.T) { @@ -188,7 +245,7 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) { winningBidsByBidder: winningBidsByBidder, roundedPrices: roundedPrices, } - _ = testAuction.doCache(ctx, cache, targData, &specData.BidRequest, 60, &specData.DefaultTTLs, bidCategory) + _ = testAuction.doCache(ctx, cache, targData, &specData.BidRequest, 60, &specData.DefaultTTLs, bidCategory, &specData.DebugLog) if len(specData.ExpectedCacheables) > len(cache.items) { t.Errorf("%s: [CACHE_ERROR] Less elements were cached than expected \n", fileDisplayName) @@ -232,6 +289,7 @@ type cacheSpec struct { TargetDataIncludeBidderKeys bool `json:"targetDataIncludeBidderKeys"` TargetDataIncludeCacheBids bool `json:"targetDataIncludeCacheBids"` TargetDataIncludeCacheVast bool `json:"targetDataIncludeCacheVast"` + DebugLog DebugLog `json:"debugLog,omitempty"` } type pbsBid struct { diff --git a/exchange/bidder.go b/exchange/bidder.go index c52fc54f8cc..f3d8e794b60 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -8,14 +8,20 @@ import ( "fmt" "io/ioutil" "net/http" + "time" + + "github.com/PubMatic-OpenWrap/prebid-server/config/util" + "github.com/golang/glog" "github.com/PubMatic-OpenWrap/openrtb" nativeRequests "github.com/PubMatic-OpenWrap/openrtb/native/request" nativeResponse "github.com/PubMatic-OpenWrap/openrtb/native/response" "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/currencies" "github.com/PubMatic-OpenWrap/prebid-server/errortypes" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "golang.org/x/net/context/ctxhttp" ) @@ -50,11 +56,13 @@ type adaptedBidder interface { // pbsOrtbBid.bidType will become "response.seatbid[i].bid.ext.prebid.type" in the final OpenRTB response. // pbsOrtbBid.bidTargets does not need to be filled out by the Bidder. It will be set later by the exchange. // pbsOrtbBid.bidVideo is optional but should be filled out by the Bidder if bidType is video. +// pbsOrtbBid.dealPriority will become "response.seatbid[i].bid.dealPriority" in the final OpenRTB response. type pbsOrtbBid struct { - bid *openrtb.Bid - bidType openrtb_ext.BidType - bidTargets map[string]string - bidVideo *openrtb_ext.ExtBidPrebidVideo + bid *openrtb.Bid + bidType openrtb_ext.BidType + bidTargets map[string]string + bidVideo *openrtb_ext.ExtBidPrebidVideo + dealPriority int } // pbsOrtbSeatBid is a SeatBid returned by an adaptedBidder. @@ -79,16 +87,20 @@ type pbsOrtbSeatBid struct { // // The name refers to the "Adapter" architecture pattern, and should not be confused with a Prebid "Adapter" // (which is being phased out and replaced by Bidder for OpenRTB auctions) -func adaptBidder(bidder adapters.Bidder, client *http.Client) adaptedBidder { +func adaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me pbsmetrics.MetricsEngine) adaptedBidder { return &bidderAdapter{ - Bidder: bidder, - Client: client, + Bidder: bidder, + Client: client, + DebugConfig: cfg.Debug, + me: me, } } type bidderAdapter struct { - Bidder adapters.Bidder - Client *http.Client + Bidder adapters.Bidder + Client *http.Client + DebugConfig config.Debug + me pbsmetrics.MetricsEngine } func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo, debug bool) (*pbsOrtbSeatBid, []error) { @@ -182,10 +194,11 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.Bi bidResponse.Bids[i].Bid.Price = bidResponse.Bids[i].Bid.Price * bidAdjustment * conversionRate } seatBid.bids = append(seatBid.bids, &pbsOrtbBid{ - bid: bidResponse.Bids[i].Bid, - bidType: bidResponse.Bids[i].BidType, - bidVideo: bidResponse.Bids[i].BidVideo, - bidTargets: bidResponse.Bids[i].BidTargets, + bid: bidResponse.Bids[i].Bid, + bidType: bidResponse.Bids[i].BidType, + bidVideo: bidResponse.Bids[i].BidVideo, + bidTargets: bidResponse.Bids[i].BidTargets, + dealPriority: bidResponse.Bids[i].DealPriority, }) } } else { @@ -205,7 +218,7 @@ func addNativeTypes(bid *openrtb.Bid, request *openrtb.BidRequest) (*nativeRespo var errs []error var nativeMarkup *nativeResponse.Response if err := json.Unmarshal(json.RawMessage(bid.AdM), &nativeMarkup); err != nil || len(nativeMarkup.Assets) == 0 { - // Some bidders are returning non-IAB complaiant native markup. In this case Prebid server will not be able to add types. E.g Facebook + // Some bidders are returning non-IAB compliant native markup. In this case Prebid server will not be able to add types. E.g Facebook return nil, errs } @@ -221,26 +234,43 @@ func addNativeTypes(bid *openrtb.Bid, request *openrtb.BidRequest) (*nativeRespo } for _, asset := range nativeMarkup.Assets { - setAssetTypes(asset, nativePayload) + if err := setAssetTypes(asset, nativePayload); err != nil { + errs = append(errs, err) + } } return nativeMarkup, errs } -func setAssetTypes(asset nativeResponse.Asset, nativePayload nativeRequests.Request) { +func setAssetTypes(asset nativeResponse.Asset, nativePayload nativeRequests.Request) error { if asset.Img != nil { - tempAsset := getAssetByID(asset.ID, nativePayload.Assets) - if tempAsset.Img.Type != 0 { - asset.Img.Type = tempAsset.Img.Type + if tempAsset, err := getAssetByID(asset.ID, nativePayload.Assets); err == nil { + if tempAsset.Img != nil { + if tempAsset.Img.Type != 0 { + asset.Img.Type = tempAsset.Img.Type + } + } else { + return fmt.Errorf("Response has an Image asset with ID:%d present that doesn't exist in the request", asset.ID) + } + } else { + return err } } if asset.Data != nil { - tempAsset := getAssetByID(asset.ID, nativePayload.Assets) - if tempAsset.Data.Type != 0 { - asset.Data.Type = tempAsset.Data.Type + if tempAsset, err := getAssetByID(asset.ID, nativePayload.Assets); err == nil { + if tempAsset.Data != nil { + if tempAsset.Data.Type != 0 { + asset.Data.Type = tempAsset.Data.Type + } + } else { + return fmt.Errorf("Response has a Data asset with ID:%d present that doesn't exist in the request", asset.ID) + } + } else { + return err } } + return nil } func getNativeImpByImpID(impID string, request *openrtb.BidRequest) (*openrtb.Native, error) { @@ -252,13 +282,13 @@ func getNativeImpByImpID(impID string, request *openrtb.BidRequest) (*openrtb.Na return nil, errors.New("Could not find native imp") } -func getAssetByID(id int64, assets []nativeRequests.Asset) nativeRequests.Asset { +func getAssetByID(id int64, assets []nativeRequests.Asset) (nativeRequests.Asset, error) { for _, asset := range assets { if id == asset.ID { - return asset + return asset, nil } } - return nativeRequests.Asset{} + return nativeRequests.Asset{}, fmt.Errorf("Unable to find asset with ID:%d in the request", id) } // makeExt transforms information about the HTTP call into the contract class for the PBS response. @@ -296,6 +326,14 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques if err != nil { if err == context.DeadlineExceeded { err = &errortypes.Timeout{Message: err.Error()} + if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); ok { + // Toss the timeout notification call into a go routine, as we are out of time' + // and cannot delay processing. We don't do anything result, as there is not much + // we can do about a timeout notification failure. We do not want to get stuck in + // a loop of trying to report timeouts to the timeout notifications. + go bidder.doTimeoutNotification(tb, req) + } + } return &httpCallInfo{ request: req, @@ -329,6 +367,47 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques } } +func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData) { + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + toReq, errL := timeoutBidder.MakeTimeoutNotification(req) + if toReq != nil && len(errL) == 0 { + httpReq, err := http.NewRequest(toReq.Method, toReq.Uri, bytes.NewBuffer(toReq.Body)) + if err == nil { + httpReq.Header = req.Headers + httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq) + success := (err == nil && httpResp.StatusCode >= 200 && httpResp.StatusCode < 300) + bidder.me.RecordTimeoutNotice(success) + if bidder.DebugConfig.TimeoutNotification.Log && !(bidder.DebugConfig.TimeoutNotification.FailOnly && success) { + var msg string + if err == nil { + msg = fmt.Sprintf("TimeoutNotification: status:(%d) body:%s", httpResp.StatusCode, string(toReq.Body)) + } else { + msg = fmt.Sprintf("TimeoutNotification: error:(%s) body:%s", err.Error(), string(toReq.Body)) + } + // If logging is turned on, and logging is not disallowed via FailOnly + util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate) + } + } else { + bidder.me.RecordTimeoutNotice(false) + if bidder.DebugConfig.TimeoutNotification.Log { + msg := fmt.Sprintf("TimeoutNotification: Failed to make timeout request: method(%s), uri(%s), error(%s)", toReq.Method, toReq.Uri, err.Error()) + util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate) + } + } + } else if bidder.DebugConfig.TimeoutNotification.Log { + reqJSON, err := json.Marshal(req) + var msg string + if err == nil { + msg = fmt.Sprintf("TimeoutNotification: Failed to generate timeout request: error(%s), bidder request(%s)", errL[0].Error(), string(reqJSON)) + } else { + msg = fmt.Sprintf("TimeoutNotification: Failed to generate timeout request: error(%s), bidder request marshal failed(%s)", errL[0].Error(), err.Error()) + } + util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate) + } + +} + type httpCallInfo struct { request *adapters.RequestData response *adapters.ResponseData diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index ec227342d0e..ebf9eccbf9d 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -12,9 +12,14 @@ import ( "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/currencies" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + metricsConfig "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics/config" "github.com/stretchr/testify/assert" + + nativeRequests "github.com/PubMatic-OpenWrap/openrtb/native/request" + nativeResponse "github.com/PubMatic-OpenWrap/openrtb/native/response" ) // TestSingleBidder makes sure that the following things work if the Bidder needs only one request. @@ -39,13 +44,15 @@ func TestSingleBidder(t *testing.T) { Bid: &openrtb.Bid{ Price: firstInitialPrice, }, - BidType: openrtb_ext.BidTypeBanner, + BidType: openrtb_ext.BidTypeBanner, + DealPriority: 4, }, { Bid: &openrtb.Bid{ Price: secondInitialPrice, }, - BidType: openrtb_ext.BidTypeVideo, + BidType: openrtb_ext.BidTypeVideo, + DealPriority: 5, }, }, } @@ -59,7 +66,7 @@ func TestSingleBidder(t *testing.T) { }, bidResponse: mockBidderResponse, } - bidder := adaptBidder(bidderImpl, server.Client()) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverterDefault() seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) @@ -88,6 +95,9 @@ func TestSingleBidder(t *testing.T) { if typedBid.BidType != seatBid.bids[index].bidType { t.Errorf("Bid %d did not have the right type. Expected %s, got %s", index, typedBid.BidType, seatBid.bids[index].bidType) } + if typedBid.DealPriority != seatBid.bids[index].dealPriority { + t.Errorf("Bid %d did not have the right deal priority. Expected %s, got %s", index, typedBid.BidType, seatBid.bids[index].bidType) + } } if mockBidderResponse.Bids[0].Bid.Price != bidAdjustment*firstInitialPrice { t.Errorf("Bid[0].Price was not adjusted properly. Expected %f, got %f", bidAdjustment*firstInitialPrice, mockBidderResponse.Bids[0].Bid.Price) @@ -144,7 +154,7 @@ func TestMultiBidder(t *testing.T) { }}, bidResponse: mockBidderResponse, } - bidder := adaptBidder(bidderImpl, server.Client()) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverterDefault() seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) @@ -502,7 +512,7 @@ func TestMultiCurrencies(t *testing.T) { ) // Execute: - bidder := adaptBidder(bidderImpl, server.Client()) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, @@ -651,7 +661,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { } // Execute: - bidder := adaptBidder(bidderImpl, server.Client()) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverterDefault() seatBid, errs := bidder.requestBid( context.Background(), @@ -818,7 +828,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { } // Execute: - bidder := adaptBidder(bidderImpl, server.Client()) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, @@ -934,7 +944,7 @@ func TestServerCallDebugging(t *testing.T) { Headers: http.Header{}, }, } - bidder := adaptBidder(bidderImpl, server.Client()) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverterDefault() bids, _ := bidder.requestBid( @@ -1047,7 +1057,7 @@ func TestMobileNativeTypes(t *testing.T) { }, bidResponse: tc.mockBidderResponse, } - bidder := adaptBidder(bidderImpl, server.Client()) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverterDefault() seatBids, _ := bidder.requestBid( @@ -1069,7 +1079,7 @@ func TestMobileNativeTypes(t *testing.T) { } func TestErrorReporting(t *testing.T) { - bidder := adaptBidder(&bidRejector{}, nil) + bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) currencyConverter := currencies.NewRateConverterDefault() bids, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) if bids != nil { @@ -1083,6 +1093,205 @@ func TestErrorReporting(t *testing.T) { } } +func TestSetAssetTypes(t *testing.T) { + testCases := []struct { + respAsset nativeResponse.Asset + nativeReq nativeRequests.Request + expectedErr string + desc string + }{ + { + respAsset: nativeResponse.Asset{ + ID: 1, + Img: &nativeResponse.Image{ + URL: "http://some-url", + }, + }, + nativeReq: nativeRequests.Request{ + Assets: []nativeRequests.Asset{ + { + ID: 1, + Img: &nativeRequests.Image{ + Type: 2, + }, + }, + { + ID: 2, + Data: &nativeRequests.Data{ + Type: 4, + }, + }, + }, + }, + expectedErr: "", + desc: "Matching image asset exists in the request and asset type is set correctly", + }, + { + respAsset: nativeResponse.Asset{ + ID: 2, + Data: &nativeResponse.Data{ + Label: "some label", + }, + }, + nativeReq: nativeRequests.Request{ + Assets: []nativeRequests.Asset{ + { + ID: 1, + Img: &nativeRequests.Image{ + Type: 2, + }, + }, + { + ID: 2, + Data: &nativeRequests.Data{ + Type: 4, + }, + }, + }, + }, + expectedErr: "", + desc: "Matching data asset exists in the request and asset type is set correctly", + }, + { + respAsset: nativeResponse.Asset{ + ID: 1, + Img: &nativeResponse.Image{ + URL: "http://some-url", + }, + }, + nativeReq: nativeRequests.Request{ + Assets: []nativeRequests.Asset{ + { + ID: 2, + Img: &nativeRequests.Image{ + Type: 2, + }, + }, + }, + }, + expectedErr: "Unable to find asset with ID:1 in the request", + desc: "Matching image asset with the same ID doesn't exist in the request", + }, + { + respAsset: nativeResponse.Asset{ + ID: 2, + Data: &nativeResponse.Data{ + Label: "some label", + }, + }, + nativeReq: nativeRequests.Request{ + Assets: []nativeRequests.Asset{ + { + ID: 2, + Img: &nativeRequests.Image{ + Type: 2, + }, + }, + }, + }, + expectedErr: "Response has a Data asset with ID:2 present that doesn't exist in the request", + desc: "Assets with same ID in the req and resp are of different types", + }, + { + respAsset: nativeResponse.Asset{ + ID: 1, + Img: &nativeResponse.Image{ + URL: "http://some-url", + }, + }, + nativeReq: nativeRequests.Request{ + Assets: []nativeRequests.Asset{ + { + ID: 1, + Data: &nativeRequests.Data{ + Type: 2, + }, + }, + }, + }, + expectedErr: "Response has an Image asset with ID:1 present that doesn't exist in the request", + desc: "Assets with same ID in the req and resp are of different types", + }, + } + + for _, test := range testCases { + err := setAssetTypes(test.respAsset, test.nativeReq) + if len(test.expectedErr) != 0 { + assert.EqualError(t, err, test.expectedErr, "Test Case: %s", test.desc) + continue + } else { + assert.NoError(t, err, "Test Case: %s", test.desc) + } + + for _, asset := range test.nativeReq.Assets { + if asset.Img != nil && test.respAsset.Img != nil { + assert.Equal(t, asset.Img.Type, test.respAsset.Img.Type, "Asset type not set correctly. Test Case: %s", test.desc) + } + if asset.Data != nil && test.respAsset.Data != nil { + assert.Equal(t, asset.Data.Type, test.respAsset.Data.Type, "Asset type not set correctly. Test Case: %s", test.desc) + } + } + } +} + +func TestTimeoutNotificationOff(t *testing.T) { + respBody := "{\"bid\":false}" + respStatus := 200 + server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + defer server.Close() + + bidderImpl := ¬ifingBidder{ + notiRequest: adapters.RequestData{ + Method: "GET", + Uri: server.URL + "/notify/me", + Body: nil, + Headers: http.Header{}, + }, + } + bidder := &bidderAdapter{ + Bidder: bidderImpl, + Client: server.Client(), + DebugConfig: config.Debug{}, + me: &metricsConfig.DummyMetricsEngine{}, + } + if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok { + t.Error("Failed to cast bidder to a TimeoutBidder") + } else { + bidder.doTimeoutNotification(tb, &adapters.RequestData{}) + } +} + +func TestTimeoutNotificationOn(t *testing.T) { + respBody := "{\"bid\":false}" + respStatus := 200 + server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + defer server.Close() + + bidderImpl := ¬ifingBidder{ + notiRequest: adapters.RequestData{ + Method: "GET", + Uri: server.URL + "/notify/me", + Body: nil, + Headers: http.Header{}, + }, + } + bidder := &bidderAdapter{ + Bidder: bidderImpl, + Client: server.Client(), + DebugConfig: config.Debug{ + TimeoutNotification: config.TimeoutNotification{ + Log: true, + }, + }, + me: &metricsConfig.DummyMetricsEngine{}, + } + if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok { + t.Error("Failed to cast bidder to a TimeoutBidder") + } else { + bidder.doTimeoutNotification(tb, &adapters.RequestData{}) + } +} + type goodSingleBidder struct { bidRequest *openrtb.BidRequest httpRequest *adapters.RequestData @@ -1156,3 +1365,19 @@ func (bidder *bidRejector) MakeBids(internalRequest *openrtb.BidRequest, externa bidder.httpResponse = response return nil, []error{errors.New("Can't make a response.")} } + +type notifingBidder struct { + notiRequest adapters.RequestData +} + +func (bidder *notifingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + return nil, nil +} + +func (bidder *notifingBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + return nil, nil +} + +func (bidder *notifingBidder) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { + return &bidder.notiRequest, nil +} diff --git a/exchange/cachetest/debuglog_disabled.json b/exchange/cachetest/debuglog_disabled.json new file mode 100644 index 00000000000..88d6332cb09 --- /dev/null +++ b/exchange/cachetest/debuglog_disabled.json @@ -0,0 +1,58 @@ +{ + "debugLog": { + "Enabled": false, + "CacheType": "xml", + "TTL": 3600, + "Data": { + "Request": "test request string", + "Headers": "test headers string", + "Response": "test response string" + } + }, + "bidRequest": { + "imp": [{ + "id": "oneImp", + "exp": 600 + }, { + "id": "twoImp" + }] + }, + "pbsBids": [{ + "bid":{ + "id": "bidOne", + "impid": "oneImp", + "price": 7.64 + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "bidTwo", + "impid": "twoImp", + "price": 5.64 + }, + "bidType": "video", + "bidder": "pubmatic" + }], + "expectedCacheables": [ + { + "Type": "json", + "TTLSeconds": 660, + "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" + }, { + "Type": "json", + "TTLSeconds": 3660, + "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}" + } + ], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners":true, + "targetDataIncludeBidderKeys":true, + "targetDataIncludeCacheBids":true, + "targetDataIncludeCacheVast":false +} diff --git a/exchange/cachetest/debuglog_enabled.json b/exchange/cachetest/debuglog_enabled.json new file mode 100644 index 00000000000..670b694f7a7 --- /dev/null +++ b/exchange/cachetest/debuglog_enabled.json @@ -0,0 +1,62 @@ +{ + "debugLog": { + "Enabled": true, + "CacheType": "xml", + "TTL": 3600, + "Data": { + "Request": "test request string", + "Headers": "test headers string", + "Response": "test response string" + } + }, + "bidRequest": { + "imp": [{ + "id": "oneImp", + "exp": 600 + }, { + "id": "twoImp" + }] + }, + "pbsBids": [{ + "bid":{ + "id": "bidOne", + "impid": "oneImp", + "price": 7.64 + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "bidTwo", + "impid": "twoImp", + "price": 5.64 + }, + "bidType": "video", + "bidder": "pubmatic" + }], + "expectedCacheables": [ + { + "Type": "json", + "TTLSeconds": 660, + "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" + }, { + "Type": "json", + "TTLSeconds": 3660, + "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}" + }, { + "Type": "xml", + "TTLSeconds": 3600, + "Data": "\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cLog\u003e\u003cRequest\u003etest request string\u003c/Request\u003e\u003cHeaders\u003etest headers string\u003c/Headers\u003e\u003cResponse\u003etest response string\u003c/Response\u003e\u003c/Log\u003e" + } + ], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners":true, + "targetDataIncludeBidderKeys":true, + "targetDataIncludeCacheBids":true, + "targetDataIncludeCacheVast":false +} diff --git a/exchange/exchange.go b/exchange/exchange.go index 9f1f33805ab..fe57255f457 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -4,11 +4,13 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "math/rand" "net/http" "runtime/debug" "sort" + "strings" "time" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" @@ -28,7 +30,7 @@ import ( // Exchange runs Auctions. Implementations must be threadsafe, and will be shared across many goroutines. type Exchange interface { // HoldAuction executes an OpenRTB v2.5 Auction. - HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher) (*openrtb.BidResponse, error) + HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) } // IdFetcher can find the user's ID for a specific Bidder. @@ -46,13 +48,16 @@ type exchange struct { currencyConverter *currencies.RateConverter UsersyncIfAmbiguous bool defaultTTLs config.DefaultTTLs - enforceCCPA bool + privacyConfig config.Privacy } // Container to pass out response ext data from the GetAllBids goroutines back into the main thread type seatResponseExtra struct { ResponseTimeMillis int Errors []openrtb_ext.ExtBidderError + // httpCalls is the list of debugging info. It should only be populated if the request.test == 1. + // This will become response.ext.debug.httpcalls.{bidder} on the final Response. + HttpCalls []*openrtb_ext.ExtHttpCall } type bidResponseWrapper struct { @@ -64,7 +69,7 @@ type bidResponseWrapper struct { func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, infos adapters.BidderInfos, gDPR gdpr.Permissions, currencyConverter *currencies.RateConverter) Exchange { e := new(exchange) - e.adapterMap = newAdapterMap(client, cfg, infos) + e.adapterMap = newAdapterMap(client, cfg, infos, metricsEngine) e.cache = cache e.cacheTime = time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond e.me = metricsEngine @@ -72,11 +77,15 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con e.currencyConverter = currencyConverter e.UsersyncIfAmbiguous = cfg.GDPR.UsersyncIfAmbiguous e.defaultTTLs = cfg.CacheURL.DefaultTTLs - e.enforceCCPA = cfg.CCPA.Enforce + e.privacyConfig = config.Privacy{ + CCPA: cfg.CCPA, + GDPR: cfg.GDPR, + LMT: cfg.LMT, + } return e } -func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher) (*openrtb.BidResponse, error) { +func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) { debug := false if bidRequest.Ext != nil { var requestExt openrtb_ext.ExtRequest @@ -108,7 +117,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque // Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels) - cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.enforceCCPA) + cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) // List of bidders we have requests for. liveAdapters := listBiddersWithRequests(cleanRequests) @@ -153,32 +162,136 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions, debug) var auc *auction = nil + var bidResponseExt *openrtb_ext.ExtBidResponse = nil if anyBidsReturned { var bidCategory map[string]string //If includebrandcategory is present in ext then CE feature is on. if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil { var err error - bidCategory, adapterBids, err = applyCategoryMapping(ctx, requestExt, adapterBids, *categoriesFetcher, targData) + var rejections []string + bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, *categoriesFetcher, targData) if err != nil { return nil, fmt.Errorf("Error in category mapping : %s", err.Error()) } + for _, message := range rejections { + errs = append(errs, errors.New(message)) + } } auc = newAuction(adapterBids, len(bidRequest.Imp)) if targData != nil { auc.setRoundedPrices(targData.priceGranularity) - cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &e.defaultTTLs, bidCategory) + + if requestExt.Prebid.SupportDeals { + dealErrs := applyDealSupport(bidRequest, auc, bidCategory) + errs = append(errs, dealErrs...) + } + + if debugLog != nil && debugLog.Enabled { + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, debug, errs) + if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil { + debugLog.Data.Response = string(bidRespExtBytes) + } else { + debugLog.Data.Response = "Unable to marshal response ext for debugging" + errs = append(errs, errors.New(debugLog.Data.Response)) + } + } + + cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &e.defaultTTLs, bidCategory, debugLog) if len(cacheErrs) > 0 { errs = append(errs, cacheErrs...) } targData.setTargeting(auc, bidRequest.App != nil, bidCategory) + + // Ensure caching errors are added if the bid response ext has already been created + if bidResponseExt != nil && len(cacheErrs) > 0 { + bidderCacheErrs := errsToBidderErrors(cacheErrs) + bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = append(bidResponseExt.Errors[openrtb_ext.PrebidExtKey], bidderCacheErrs...) + } } + } // Build the response - return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, debug, errs) + return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, bidResponseExt, debug, errs) +} + +type DealTierInfo struct { + Prefix string `json:"prefix"` + MinDealTier int `json:"minDealTier"` +} + +type DealTier struct { + Info *DealTierInfo `json:"dealTier,omitempty"` +} + +type BidderDealTier struct { + DealInfo map[string]*DealTier +} + +// applyDealSupport updates targeting keys with deal prefixes if minimum deal tier exceeded +func applyDealSupport(bidRequest *openrtb.BidRequest, auc *auction, bidCategory map[string]string) []error { + errs := []error{} + impDealMap := getDealTiers(bidRequest) + + for impID, topBidsPerImp := range auc.winningBidsByBidder { + impDeal := impDealMap[impID].DealInfo + for bidder, topBidPerBidder := range topBidsPerImp { + bidderString := bidder.String() + + if topBidPerBidder.dealPriority > 0 { + if validateAndNormalizeDealTier(impDeal[bidderString]) { + updateHbPbCatDur(topBidPerBidder, impDeal[bidderString].Info, bidCategory) + } else { + errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", bidderString, impID)) + } + } + } + } + + return errs +} + +// getDealTiers creates map of impression to bidder deal tier configuration +func getDealTiers(bidRequest *openrtb.BidRequest) map[string]*BidderDealTier { + impDealMap := make(map[string]*BidderDealTier) + + for _, imp := range bidRequest.Imp { + var bidderDealTier BidderDealTier + err := json.Unmarshal(imp.Ext, &bidderDealTier.DealInfo) + if err != nil { + continue + } + + impDealMap[imp.ID] = &bidderDealTier + } + + return impDealMap +} + +func validateAndNormalizeDealTier(impDeal *DealTier) bool { + if impDeal == nil || impDeal.Info == nil { + return false + } + // Remove whitespace from prefix before checking if it can be used + impDeal.Info.Prefix = strings.ReplaceAll(impDeal.Info.Prefix, " ", "") + return len(impDeal.Info.Prefix) > 0 && impDeal.Info.MinDealTier > 0 +} + +func updateHbPbCatDur(bid *pbsOrtbBid, dealTierInfo *DealTierInfo, bidCategory map[string]string) { + if bid.dealPriority >= dealTierInfo.MinDealTier { + prefixTier := fmt.Sprintf("%s%d_", dealTierInfo.Prefix, bid.dealPriority) + + if oldCatDur, ok := bidCategory[bid.bid.ID]; ok { + oldCatDurSplit := strings.SplitAfterN(oldCatDur, "_", 2) + oldCatDurSplit[0] = prefixTier + + newCatDur := strings.Join(oldCatDurSplit, "") + bidCategory[bid.bid.ID] = newCatDur + } + } } func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auctionCtx context.Context, cancel context.CancelFunc) { @@ -203,7 +316,7 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext for bidderName, req := range cleanRequests { // Here we actually call the adapters and collect the bids. coreBidder := resolveBidder(string(bidderName), aliases) - bidderRunner := e.recoverSafely(func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) { + bidderRunner := e.recoverSafely(cleanRequests, func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) { // Passing in aName so a doesn't change out from under the go routine if bidlabels.Adapter == "" { glog.Errorf("Exchange: bidlables for %s (%s) missing adapter string", aName, coreBidder) @@ -231,6 +344,10 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext // Structure to record extra tracking data generated during bidding ae := new(seatResponseExtra) ae.ResponseTimeMillis = int(elapsed / time.Millisecond) + if bids != nil { + ae.HttpCalls = bids.httpCalls + } + // Timing statistics e.me.RecordAdapterTime(*bidlabels, time.Since(start)) serr := errsToBidderErrors(err) @@ -253,7 +370,12 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext // Wait for the bidders to do their thing for i := 0; i < len(cleanRequests); i++ { brw := <-chBids - adapterBids[brw.bidder] = brw.adapterBids + + //if bidder returned no bids back - remove bidder from further processing + if brw.adapterBids != nil && len(brw.adapterBids.bids) != 0 { + adapterBids[brw.bidder] = brw.adapterBids + } + //but we need to add all bidders data to adapterExtra to have metrics and other metadata adapterExtra[brw.bidder] = brw.adapterExtra if !bidsFound && adapterBids[brw.bidder] != nil && len(adapterBids[brw.bidder].bids) > 0 { @@ -264,11 +386,24 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext return adapterBids, adapterExtra, bidsFound } -func (e *exchange) recoverSafely(inner func(openrtb_ext.BidderName, openrtb_ext.BidderName, *openrtb.BidRequest, *pbsmetrics.AdapterLabels, currencies.Conversions), chBids chan *bidResponseWrapper) func(openrtb_ext.BidderName, openrtb_ext.BidderName, *openrtb.BidRequest, *pbsmetrics.AdapterLabels, currencies.Conversions) { +func (e *exchange) recoverSafely(cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest, inner func(openrtb_ext.BidderName, openrtb_ext.BidderName, *openrtb.BidRequest, *pbsmetrics.AdapterLabels, currencies.Conversions), chBids chan *bidResponseWrapper) func(openrtb_ext.BidderName, openrtb_ext.BidderName, *openrtb.BidRequest, *pbsmetrics.AdapterLabels, currencies.Conversions) { return func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) { defer func() { if r := recover(); r != nil { - glog.Errorf("OpenRTB auction recovered panic from Bidder %s: %v. Stack trace is: %v", coreBidder, r, string(debug.Stack())) + + allBidders := "" + sb := strings.Builder{} + for k := range cleanRequests { + sb.WriteString(string(k)) + sb.WriteString(",") + } + if sb.Len() > 0 { + allBidders = sb.String()[:sb.Len()-1] + } + + glog.Errorf("OpenRTB auction recovered panic from Bidder %s: %v. "+ + "Account id: %s, All Bidders: %s, Stack trace is: %v", + coreBidder, r, bidlabels.PubID, allBidders, string(debug.Stack())) e.me.RecordAdapterPanic(*bidlabels) // Let the master request know that there is no data here brw := new(bidResponseWrapper) @@ -294,14 +429,14 @@ func errorsToMetric(errs []error) map[pbsmetrics.AdapterError]struct{} { ret := make(map[pbsmetrics.AdapterError]struct{}, len(errs)) var s struct{} for _, err := range errs { - switch errortypes.DecodeError(err) { - case errortypes.TimeoutCode: + switch errortypes.ReadCode(err) { + case errortypes.TimeoutErrorCode: ret[pbsmetrics.AdapterErrorTimeout] = s - case errortypes.BadInputCode: + case errortypes.BadInputErrorCode: ret[pbsmetrics.AdapterErrorBadInput] = s - case errortypes.BadServerResponseCode: + case errortypes.BadServerResponseErrorCode: ret[pbsmetrics.AdapterErrorBadServerResponse] = s - case errortypes.FailedToRequestBidsCode: + case errortypes.FailedToRequestBidsErrorCode: ret[pbsmetrics.AdapterErrorFailedToRequestBids] = s default: ret[pbsmetrics.AdapterErrorUnknown] = s @@ -313,14 +448,14 @@ func errorsToMetric(errs []error) map[pbsmetrics.AdapterError]struct{} { func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError { serr := make([]openrtb_ext.ExtBidderError, len(errs)) for i := 0; i < len(errs); i++ { - serr[i].Code = errortypes.DecodeError(errs[i]) + serr[i].Code = errortypes.ReadCode(errs[i]) serr[i].Message = errs[i].Error() } return serr } // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester -func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb.BidRequest, resolvedRequest json.RawMessage, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, debug bool, errList []error) (*openrtb.BidResponse, error) { +func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb.BidRequest, resolvedRequest json.RawMessage, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, debug bool, errList []error) (*openrtb.BidResponse, error) { bidResponse := new(openrtb.BidResponse) bidResponse.ID = bidRequest.ID @@ -343,7 +478,9 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ bidResponse.SeatBid = seatBids - bidResponseExt := e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, debug, errList) + if bidResponseExt == nil { + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, debug, errList) + } buffer := &bytes.Buffer{} enc := json.NewEncoder(buffer) enc.SetEscapeHTML(false) @@ -353,7 +490,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ return bidResponse, err } -func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, error) { +func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) { res := make(map[string]string) type bidDedupe struct { @@ -372,6 +509,7 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest var primaryAdServer string var publisher string var err error + var rejections []string var translateCategories = true if includeBrandCategory && brandCatExt.WithCategory { @@ -383,7 +521,7 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest //if ext.prebid.targeting.includebrandcategory present but primaryadserver/publisher not present then error out the request right away. primaryAdServer, err = getPrimaryAdServer(brandCatExt.PrimaryAdServer) //1-Freewheel 2-DFP if err != nil { - return res, seatBids, err + return res, seatBids, rejections, err } publisher = brandCatExt.Publisher } @@ -395,6 +533,7 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest bidsToRemove := make([]int, 0) for bidInd := range seatBid.bids { bid := seatBid.bids[bidInd] + bidID := bid.bid.ID var duration int var category string var pb string @@ -409,6 +548,7 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest //TODO: add metrics //on receiving bids from adapters if no unique IAB category is returned or if no ad server category is returned discard the bid bidsToRemove = append(bidsToRemove, bidInd) + rejections = updateRejections(rejections, bidID, "Bid did not contain a category") continue } if translateCategories { @@ -418,6 +558,8 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest //TODO: add metrics //if mapping required but no mapping file is found then discard the bid bidsToRemove = append(bidsToRemove, bidInd) + reason := fmt.Sprintf("Category mapping file for primary ad server: '%s', publisher: '%s' not found", primaryAdServer, publisher) + rejections = updateRejections(rejections, bidID, reason) continue } } else { @@ -437,6 +579,7 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest //if the bid is above the range of the listed durations (and outside the buffer), reject the bid if duration > durationRange[len(durationRange)-1] { bidsToRemove = append(bidsToRemove, bidInd) + rejections = updateRejections(rejections, bidID, "Bid duration exceeds maximum allowed") continue } for _, dur := range durationRange { @@ -460,11 +603,13 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest if dupe.bidderName == bidderName { // An older bid from the current bidder bidsToRemove = append(bidsToRemove, dupe.bidIndex) + rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") } else { // An older bid from a different seatBid we've already finished with oldSeatBid := (seatBids)[dupe.bidderName] if len(oldSeatBid.bids) == 1 { seatBidsToRemove = append(seatBidsToRemove, bidderName) + rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") } else { oldSeatBid.bids = append(oldSeatBid.bids[:dupe.bidIndex], oldSeatBid.bids[dupe.bidIndex+1:]...) } @@ -473,11 +618,12 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest } else { // Remove this bid bidsToRemove = append(bidsToRemove, bidInd) + rejections = updateRejections(rejections, bidID, "Bid was deduplicated") continue } } - res[bid.bid.ID] = categoryDuration - dedupe[categoryDuration] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bid.bid.ID} + res[bidID] = categoryDuration + dedupe[categoryDuration] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID} } if len(bidsToRemove) > 0 { @@ -496,19 +642,16 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest } } - if len(seatBidsToRemove) > 0 { - if len(seatBidsToRemove) == len(seatBids) { - //delete all seat bids - seatBids = nil - } else { - for _, seatBidInd := range seatBidsToRemove { - delete(seatBids, seatBidInd) - } - - } + for _, seatBidInd := range seatBidsToRemove { + seatBids[seatBidInd].bids = nil } - return res, seatBids, nil + return res, seatBids, rejections, nil +} + +func updateRejections(rejections []string, bidID string, reason string) []string { + message := fmt.Sprintf("bid rejected [bid ID: %s] reason: %s", bidID, reason) + return append(rejections, message) } func getPrimaryAdServer(adServerId int) (string, error) { @@ -538,22 +681,20 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb } } - for a, b := range adapterBids { - if b != nil { - if debug { - // Fill debug info - bidResponseExt.Debug.HttpCalls[a] = b.httpCalls - } + for bidderName, responseExtra := range adapterExtra { + + if debug { + bidResponseExt.Debug.HttpCalls[bidderName] = responseExtra.HttpCalls } // Only make an entry for bidder errors if the bidder reported any. - if len(adapterExtra[a].Errors) > 0 { - bidResponseExt.Errors[a] = adapterExtra[a].Errors + if len(responseExtra.Errors) > 0 { + bidResponseExt.Errors[bidderName] = responseExtra.Errors } if len(errList) > 0 { bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = errsToBidderErrors(errList) } - bidResponseExt.ResponseTimeMillis[a] = adapterExtra[a].ResponseTimeMillis - // Defering the filling of bidResponseExt.Usersync[a] until later + bidResponseExt.ResponseTimeMillis[bidderName] = responseExtra.ResponseTimeMillis + // Defering the filling of bidResponseExt.Usersync[bidderName] until later } return bidResponseExt @@ -575,7 +716,7 @@ func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.B ext, err := json.Marshal(sbExt) if err != nil { extError := openrtb_ext.ExtBidderError{ - Code: errortypes.DecodeError(err), + Code: errortypes.ReadCode(err), Message: fmt.Sprintf("Error writing SeatBid.Ext: %s", err.Error()), } adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, extError) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index bf568322279..350438b1be6 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "regexp" "strconv" "strings" "testing" @@ -126,7 +127,7 @@ func TestCharacterEscape(t *testing.T) { var errList []error /* 4) Build bid response */ - bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, false, errList) + bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, nil, false, errList) /* 5) Assert we have no errors and one '&' character as we are supposed to */ if err != nil { @@ -170,7 +171,7 @@ func TestGetBidCacheInfo(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() - e := NewExchange(server.Client(), pbc.NewClient(&cfg.CacheURL, &cfg.ExtCacheURL, testEngine), cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange) + e := NewExchange(server.Client(), pbc.NewClient(&http.Client{}, &cfg.CacheURL, &cfg.ExtCacheURL, testEngine), cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange) /* 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */ liveAdapters := []openrtb_ext.BidderName{bidderName} @@ -278,7 +279,7 @@ func TestGetBidCacheInfo(t *testing.T) { var errList []error /* 4) Build bid response */ - bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, false, errList) + bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, nil, false, errList) /* 5) Assert we have no errors and the bid response we expected*/ assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error") @@ -449,7 +450,7 @@ func TestBidResponseCurrency(t *testing.T) { // Run tests for i := range testCases { - actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, false, errList) + actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, nil, false, errList) assert.NoError(t, err, fmt.Sprintf("[TEST_FAILED] e.buildBidResponse resturns error in test: %s Error message: %s \n", testCases[i].description, err)) assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext)) } @@ -482,13 +483,18 @@ func TestRaceIntegration(t *testing.T) { Endpoint: server.URL, PlatformID: "abc", } + cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderBeachfront))] = config.Adapter{ + Endpoint: server.URL, + ExtraAdapterInfo: "{\"video_endpoint\":\"" + server.URL + "\"}", + } + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") if error != nil { t.Errorf("Failed to create a category Fetcher: %v", error) } theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) ex := NewExchange(server.Client(), &wellBehavedCache{}, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()) - _, err := ex.HoldAuction(context.Background(), newRaceCheckingRequest(t), &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher) + _, err := ex.HoldAuction(context.Background(), newRaceCheckingRequest(t), &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) } @@ -576,7 +582,15 @@ func TestPanicRecovery(t *testing.T) { panicker := func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) { panic("panic!") } - recovered := e.recoverSafely(panicker, chBids) + cleanReqs := map[openrtb_ext.BidderName]*openrtb.BidRequest{ + "bidder1": { + ID: "b-1", + }, + "bidder2": { + ID: "b-2", + }, + } + recovered := e.recoverSafely(cleanReqs, panicker, chBids) apnLabels := pbsmetrics.AdapterLabels{ Source: pbsmetrics.DemandWeb, RType: pbsmetrics.ReqTypeORTB2Web, @@ -665,7 +679,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) { if error != nil { t.Errorf("Failed to create a category Fetcher: %v", error) } - _, err := e.HoldAuction(context.Background(), request, &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher) + _, err := e.HoldAuction(context.Background(), request, &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) } @@ -725,13 +739,28 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { if len(errs) != 0 { t.Fatalf("%s: Failed to parse aliases", filename) } - ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, spec.EnforceCCPA) + + privacyConfig := config.Privacy{ + CCPA: config.CCPA{ + Enforce: spec.EnforceCCPA, + }, + LMT: config.LMT{ + Enforce: spec.EnforceLMT, + }, + } + + ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig) biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest) categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") if error != nil { t.Errorf("Failed to create a category Fetcher: %v", error) } - bid, err := ex.HoldAuction(context.Background(), &spec.IncomingRequest.OrtbRequest, mockIdFetcher(spec.IncomingRequest.Usersyncs), pbsmetrics.Labels{}, &categoriesFetcher) + debugLog := &DebugLog{} + if spec.DebugLog != nil { + *debugLog = *spec.DebugLog + debugLog.Regexp = regexp.MustCompile(`[<>]`) + } + bid, err := ex.HoldAuction(context.Background(), &spec.IncomingRequest.OrtbRequest, mockIdFetcher(spec.IncomingRequest.Usersyncs), pbsmetrics.Labels{}, &categoriesFetcher, debugLog) responseTimes := extractResponseTimes(t, filename, bid) for _, bidderName := range biddersInAuction { if _, ok := responseTimes[bidderName]; !ok { @@ -750,6 +779,22 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { } } } + if spec.DebugLog != nil { + if spec.DebugLog.Enabled { + if len(debugLog.Data.Response) == 0 { + t.Errorf("%s: DebugLog response was not modified when it should have been", filename) + } + } else { + if len(debugLog.Data.Response) != 0 { + t.Errorf("%s: DebugLog response was modified when it shouldn't have been", filename) + } + } + } + if spec.IncomingRequest.OrtbRequest.Test == 1 { + //compare debug info + diffJson(t, "Debug info modified", bid.Ext, spec.Response.Ext) + + } } func findBiddersInAuction(t *testing.T, context string, req *openrtb.BidRequest) []string { @@ -789,7 +834,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb.BidResponse } } -func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, enforceCCPA bool) Exchange { +func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy) Exchange { adapters := make(map[openrtb_ext.BidderName]adaptedBidder) for _, bidderName := range openrtb_ext.BidderMap { if spec, ok := expectations[string(bidderName)]; ok { @@ -827,7 +872,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] gDPR: gdpr.AlwaysAllow{}, currencyConverter: currencies.NewRateConverterDefault(), UsersyncIfAmbiguous: false, - enforceCCPA: enforceCCPA, + privacyConfig: privacyConfig, } } @@ -913,10 +958,10 @@ func TestCategoryMapping(t *testing.T) { bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} - bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}} - bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}} - bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, 0} + bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -930,9 +975,11 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) assert.Equal(t, nil, err, "Category mapping error should be empty") + assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") + assert.Equal(t, "bid rejected [bid ID: bid_id4] reason: Category mapping file for primary ad server: 'freewheel', publisher: '' not found", rejections[0], "Rejection message did not match expected") assert.Equal(t, "10.00_Electronics_30s", bidCategory["bid_id1"], "Category mapping doesn't match") assert.Equal(t, "20.00_Sports_50s", bidCategory["bid_id2"], "Category mapping doesn't match") assert.Equal(t, "20.00_AdapterOverride_30s", bidCategory["bid_id3"], "Category mapping override from adapter didn't take") @@ -966,10 +1013,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} - bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}} - bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}} - bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}} + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, 0} + bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, 0} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -983,9 +1030,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) assert.Equal(t, nil, err, "Category mapping error should be empty") + assert.Empty(t, rejections, "There should be no bid rejection messages") assert.Equal(t, "10.00_30s", bidCategory["bid_id1"], "Category mapping doesn't match") assert.Equal(t, "20.00_40s", bidCategory["bid_id2"], "Category mapping doesn't match") assert.Equal(t, "20.00_30s", bidCategory["bid_id3"], "Category mapping doesn't match") @@ -1019,9 +1067,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} - bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}} - bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -1034,9 +1082,11 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) assert.Equal(t, nil, err, "Category mapping error should be empty") + assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") + assert.Equal(t, "bid rejected [bid ID: bid_id3] reason: Category mapping file for primary ad server: 'freewheel', publisher: '' not found", rejections[0], "Rejection message did not match expected") assert.Equal(t, "10.00_Electronics_30s", bidCategory["bid_id1"], "Category mapping doesn't match") assert.Equal(t, "20.00_Sports_50s", bidCategory["bid_id2"], "Category mapping doesn't match") assert.Equal(t, 2, len(adapterBids[bidderName1].bids), "Bidders number doesn't match") @@ -1099,9 +1149,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} - bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}} - bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} innerBids := []*pbsOrtbBid{ &bid1_1, @@ -1114,9 +1164,10 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) assert.Equal(t, nil, err, "Category mapping error should be empty") + assert.Empty(t, rejections, "There should be no bid rejection messages") assert.Equal(t, "10.00_IAB1-3_30s", bidCategory["bid_id1"], "Category should not be translated") assert.Equal(t, "20.00_IAB1-4_50s", bidCategory["bid_id2"], "Category should not be translated") assert.Equal(t, "20.00_IAB1-1000_30s", bidCategory["bid_id3"], "Bid should not be rejected") @@ -1149,10 +1200,10 @@ func TestCategoryDedupe(t *testing.T) { bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid4 := openrtb.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} - bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}} - bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} - bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}} + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, 0} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} selectedBids := make(map[string]int) expectedCategories := map[string]string{ @@ -1179,9 +1230,12 @@ func TestCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, requestExt, adapterBids, categoriesFetcher, targData) assert.Equal(t, nil, err, "Category mapping error should be empty") + assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages") + assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_id(1|3)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected") + assert.Equal(t, "bid rejected [bid ID: bid_id4] reason: Category mapping file for primary ad server: 'freewheel', publisher: '' not found", rejections[1], "Rejection message did not match expected") assert.Equal(t, 2, len(adapterBids[bidderName1].bids), "Bidders number doesn't match") assert.Equal(t, 2, len(bidCategory), "Bidders category mapping doesn't match") @@ -1196,11 +1250,396 @@ func TestCategoryDedupe(t *testing.T) { assert.NotEqual(t, numIterations, selectedBids["bid_id3"], "Bid 3 made it through every time") } +func TestBidRejectionErrors(t *testing.T) { + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + requestExt := newExtRequest() + requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + invalidReqExt := newExtRequest() + invalidReqExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50} + invalidReqExt.Prebid.Targeting.IncludeBrandCategory.PrimaryAdServer = 2 + invalidReqExt.Prebid.Targeting.IncludeBrandCategory.Publisher = "some_publisher" + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + bidderName := openrtb_ext.BidderName("appnexus") + + testCases := []struct { + description string + reqExt openrtb_ext.ExtRequest + bids []*openrtb.Bid + duration int + expectedRejections []string + expectedCatDur string + }{ + { + description: "Bid should be rejected due to not containing a category", + reqExt: requestExt, + bids: []*openrtb.Bid{ + {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{}, W: 1, H: 1}, + }, + duration: 30, + expectedRejections: []string{ + "bid rejected [bid ID: bid_id1] reason: Bid did not contain a category", + }, + }, + { + description: "Bid should be rejected due to missing category mapping file", + reqExt: invalidReqExt, + bids: []*openrtb.Bid{ + {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, + }, + duration: 30, + expectedRejections: []string{ + "bid rejected [bid ID: bid_id1] reason: Category mapping file for primary ad server: 'dfp', publisher: 'some_publisher' not found", + }, + }, + { + description: "Bid should be rejected due to duration exceeding maximum", + reqExt: requestExt, + bids: []*openrtb.Bid{ + {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, + }, + duration: 70, + expectedRejections: []string{ + "bid rejected [bid ID: bid_id1] reason: Bid duration exceeds maximum allowed", + }, + }, + { + description: "Bid should be rejected due to duplicate bid", + reqExt: requestExt, + bids: []*openrtb.Bid{ + {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, + {ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: []string{"IAB1-1"}, W: 1, H: 1}, + }, + duration: 30, + expectedRejections: []string{ + "bid rejected [bid ID: bid_id1] reason: Bid was deduplicated", + }, + expectedCatDur: "10.00_VideoGames_30s", + }, + } + + for _, test := range testCases { + innerBids := []*pbsOrtbBid{} + for _, bid := range test.bids { + currentBid := pbsOrtbBid{ + bid, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, 0, + } + innerBids = append(innerBids, ¤tBid) + } + + seatBid := pbsOrtbSeatBid{innerBids, "USD", nil, nil} + + adapterBids[bidderName] = &seatBid + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, test.reqExt, adapterBids, categoriesFetcher, targData) + + if len(test.expectedCatDur) > 0 { + // Bid deduplication case + assert.Equal(t, 1, len(adapterBids[bidderName].bids), "Bidders number doesn't match") + assert.Equal(t, 1, len(bidCategory), "Bidders category mapping doesn't match") + assert.Equal(t, test.expectedCatDur, bidCategory["bid_id1"], "Bid category did not contain expected hb_pb_cat_dur") + } else { + assert.Empty(t, adapterBids[bidderName].bids, "Bidders number doesn't match") + assert.Empty(t, bidCategory, "Bidders category mapping doesn't match") + } + + assert.Empty(t, err, "Category mapping error should be empty") + assert.Equal(t, test.expectedRejections, rejections, test.description) + } +} + +func TestUpdateRejections(t *testing.T) { + rejections := []string{} + + rejections = updateRejections(rejections, "bid_id1", "some reason 1") + rejections = updateRejections(rejections, "bid_id2", "some reason 2") + + assert.Equal(t, 2, len(rejections), "Rejections should contain 2 rejection messages") + assert.Containsf(t, rejections, "bid rejected [bid ID: bid_id1] reason: some reason 1", "Rejection message did not match expected") + assert.Containsf(t, rejections, "bid rejected [bid ID: bid_id2] reason: some reason 2", "Rejection message did not match expected") +} + +func TestApplyDealSupport(t *testing.T) { + testCases := []struct { + description string + dealPriority int + impExt json.RawMessage + targ map[string]string + expectedHbPbCatDur string + expectedDealErr string + }{ + { + description: "hb_pb_cat_dur should be modified", + dealPriority: 5, + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + targ: map[string]string{ + "hb_pb_cat_dur": "12.00_movies_30s", + }, + expectedHbPbCatDur: "tier5_movies_30s", + expectedDealErr: "", + }, + { + description: "hb_pb_cat_dur should not be modified due to priority not exceeding min", + dealPriority: 9, + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}`), + targ: map[string]string{ + "hb_pb_cat_dur": "12.00_medicine_30s", + }, + expectedHbPbCatDur: "12.00_medicine_30s", + expectedDealErr: "", + }, + { + description: "hb_pb_cat_dur should not be modified due to invalid config", + dealPriority: 5, + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}`), + targ: map[string]string{ + "hb_pb_cat_dur": "12.00_games_30s", + }, + expectedHbPbCatDur: "12.00_games_30s", + expectedDealErr: "dealTier configuration invalid for bidder 'appnexus', imp ID 'imp_id1'", + }, + { + description: "hb_pb_cat_dur should not be modified due to deal priority of 0", + dealPriority: 0, + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + targ: map[string]string{ + "hb_pb_cat_dur": "12.00_auto_30s", + }, + expectedHbPbCatDur: "12.00_auto_30s", + expectedDealErr: "", + }, + } + + bidderName := openrtb_ext.BidderName("appnexus") + for _, test := range testCases { + bidRequest := &openrtb.BidRequest{ + ID: "some-request-id", + Imp: []openrtb.Imp{ + { + ID: "imp_id1", + Ext: test.impExt, + }, + }, + } + + bid := pbsOrtbBid{&openrtb.Bid{ID: "123456"}, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority} + bidCategory := map[string]string{ + bid.bid.ID: test.targ["hb_pb_cat_dur"], + } + + auc := &auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp_id1": { + bidderName: &bid, + }, + }, + } + + dealErrs := applyDealSupport(bidRequest, auc, bidCategory) + + assert.Equal(t, test.expectedHbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][bidderName].bid.ID], test.description) + if len(test.expectedDealErr) > 0 { + assert.Containsf(t, dealErrs, errors.New(test.expectedDealErr), "Expected error message not found in deal errors") + } + } +} + +func TestGetDealTiers(t *testing.T) { + testCases := []struct { + impExt json.RawMessage + bidderResult map[string]bool // true indicates bidder had valid config, false indicates invalid + }{ + { + impExt: json.RawMessage(`{"validbase": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + bidderResult: map[string]bool{ + "validbase": true, + }, + }, + { + impExt: json.RawMessage(`{"validmultiple1": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}, "validmultiple2": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + bidderResult: map[string]bool{ + "validmultiple1": true, + "validmultiple2": true, + }, + }, + { + impExt: json.RawMessage(`{"nodealtier": {"placementId": 10433394}}`), + bidderResult: map[string]bool{ + "nodealtier": false, + }, + }, + { + impExt: json.RawMessage(`{"validbase": {"placementId": 10433394}, "onedealTier2": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + bidderResult: map[string]bool{ + "onedealTier2": true, + "validbase": false, + }, + }, + } + + filledDealTier := DealTier{ + Info: &DealTierInfo{ + Prefix: "tier", + MinDealTier: 5, + }, + } + emptyDealTier := DealTier{} + + for _, test := range testCases { + bidRequest := &openrtb.BidRequest{ + ID: "some-request-id", + Imp: []openrtb.Imp{ + { + ID: "imp_id1", + Ext: test.impExt, + }, + }, + } + + impDealMap := getDealTiers(bidRequest) + + for bidder, valid := range test.bidderResult { + if valid { + assert.Equal(t, &filledDealTier, impDealMap["imp_id1"].DealInfo[bidder], "DealTier should be filled with config data") + } else { + assert.Equal(t, &emptyDealTier, impDealMap["imp_id1"].DealInfo[bidder], "DealTier should be empty") + } + } + } +} + +func TestValidateAndNormalizeDealTier(t *testing.T) { + testCases := []struct { + description string + params json.RawMessage + expectedResult bool + }{ + { + description: "BidderDealTier should be valid", + params: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + expectedResult: true, + }, + { + description: "BidderDealTier should be invalid due to empty prefix", + params: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}`), + expectedResult: false, + }, + { + description: "BidderDealTier should be invalid due to empty dealTier", + params: json.RawMessage(`{"appnexus": {"dealTier": {}, "placementId": 10433394}}`), + expectedResult: false, + }, + { + description: "BidderDealTier should be invalid due to missing minDealTier", + params: json.RawMessage(`{"appnexus": {"dealTier": {"prefix": "tier"}, "placementId": 10433394}}`), + expectedResult: false, + }, + { + description: "BidderDealTier should be invalid due to missing dealTier", + params: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), + expectedResult: false, + }, + { + description: "BidderDealTier should be invalid due to prefix containing all whitespace", + params: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": " "}, "placementId": 10433394}}`), + expectedResult: false, + }, + { + description: "BidderDealTier should be valid after removing whitespace", + params: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": " prefixwith sp aces "}, "placementId": 10433394}}`), + expectedResult: true, + }, + } + + for _, test := range testCases { + var bidderDealTier BidderDealTier + err := json.Unmarshal(test.params, &bidderDealTier.DealInfo) + if err != nil { + assert.Fail(t, "Unable to unmarshal JSON data for testing BidderDealTier") + } + + assert.Equal(t, test.expectedResult, validateAndNormalizeDealTier(bidderDealTier.DealInfo["appnexus"]), test.description) + } +} + +func TestUpdateHbPbCatDur(t *testing.T) { + testCases := []struct { + description string + targ map[string]string + dealTier *DealTierInfo + dealPriority int + expectedHbPbCatDur string + }{ + { + description: "hb_pb_cat_dur should be updated with prefix and tier", + targ: map[string]string{ + "hb_pb": "12.00", + "hb_pb_cat_dur": "12.00_movies_30s", + }, + dealTier: &DealTierInfo{ + Prefix: "tier", + MinDealTier: 5, + }, + dealPriority: 5, + expectedHbPbCatDur: "tier5_movies_30s", + }, + { + description: "hb_pb_cat_dur should not be updated due to bid priority", + targ: map[string]string{ + "hb_pb": "12.00", + "hb_pb_cat_dur": "12.00_auto_30s", + }, + dealTier: &DealTierInfo{ + Prefix: "tier", + MinDealTier: 10, + }, + dealPriority: 6, + expectedHbPbCatDur: "12.00_auto_30s", + }, + { + description: "hb_pb_cat_dur should be updated with prefix and tier", + targ: map[string]string{ + "hb_pb": "12.00", + "hb_pb_cat_dur": "12.00_medicine_30s", + }, + dealTier: &DealTierInfo{ + Prefix: "tier", + MinDealTier: 1, + }, + dealPriority: 7, + expectedHbPbCatDur: "tier7_medicine_30s", + }, + } + + for _, test := range testCases { + bid := pbsOrtbBid{&openrtb.Bid{ID: "123456"}, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority} + bidCategory := map[string]string{ + bid.bid.ID: test.targ["hb_pb_cat_dur"], + } + + updateHbPbCatDur(&bid, test.dealTier, bidCategory) + + assert.Equal(t, test.expectedHbPbCatDur, bidCategory[bid.bid.ID], test.description) + } +} + type exchangeSpec struct { IncomingRequest exchangeRequest `json:"incomingRequest"` OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` Response exchangeResponse `json:"response,omitempty"` EnforceCCPA bool `json:"enforceCcpa"` + EnforceLMT bool `json:"enforceLmt"` + DebugLog *DebugLog `json:"debuglog,omitempty"` } type exchangeRequest struct { @@ -1211,6 +1650,7 @@ type exchangeRequest struct { type exchangeResponse struct { Bids *openrtb.BidResponse `json:"bids"` Error string `json:"error,omitempty"` + Ext json.RawMessage `json:"ext,omitempty"` } type bidderSpec struct { @@ -1224,8 +1664,9 @@ type bidderRequest struct { } type bidderResponse struct { - SeatBid *bidderSeatBid `json:"pbsSeatBid,omitempty"` - Errors []string `json:"errors,omitempty"` + SeatBid *bidderSeatBid `json:"pbsSeatBid,omitempty"` + Errors []string `json:"errors,omitempty"` + HttpCalls []*openrtb_ext.ExtHttpCall `json:"httpCalls,omitempty"` } // bidderSeatBid is basically a subset of pbsOrtbSeatBid from exchange/bidder.go. @@ -1282,7 +1723,13 @@ func (b *validatingBidder) requestBid(ctx context.Context, request *openrtb.BidR } seatBid = &pbsOrtbSeatBid{ - bids: bids, + bids: bids, + httpCalls: mockResponse.HttpCalls, + } + } else { + seatBid = &pbsOrtbSeatBid{ + bids: nil, + httpCalls: mockResponse.HttpCalls, } } @@ -1359,7 +1806,7 @@ func diffJson(t *testing.T, description string, actual []byte, expected []byte) if diff.Modified() { var left interface{} if err := json.Unmarshal(actual, &left); err != nil { - t.Fatalf("%s json did not match, but unmarhsalling failed. %v", description, err) + t.Fatalf("%s json did not match, but unmarshalling failed. %v", description, err) } printer := formatter.NewAsciiFormatter(left, formatter.AsciiFormatterConfig{ ShowArrayIndex: true, diff --git a/exchange/exchangetest/debuglog_disabled.json b/exchange/exchangetest/debuglog_disabled.json new file mode 100644 index 00000000000..9902dea4bbc --- /dev/null +++ b/exchange/exchangetest/debuglog_disabled.json @@ -0,0 +1,232 @@ +{ + "debugLog": { + "Enabled": false, + "CacheType": "xml", + "TTL": 3600, + "Data": { + "Request": "test request string", + "Headers": "test headers string", + "Response": "" + } + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "test": 1, + "ext": { + "prebid": { + "debug" :1, + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + } + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30, + "PrimaryCategory": "" + } + }, + { + "ortbBid": { + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 0.6, + "w": 300, + "h": 500, + "crid": "creative-3", + "cat": [ + "IAB1-2" + ] + }, + "bidType": "video" + } + ] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.20", + "hb_pb_appnexus": "0.20", + "hb_pb_cat_dur": "0.20_VideoGames_0s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + } + } + } + }, + { + "cat": [ + "IAB1-2" + ], + "crid": "creative-3", + "ext": { + "prebid": { + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.50", + "hb_pb_appnexus": "0.50", + "hb_pb_cat_dur": "0.50_HomeDecor_0s", + "hb_pb_cat_dur_appnex": "0.50_HomeDecor_0s", + "hb_size": "300x500", + "hb_size_appnexus": "300x500" + }, + "type": "video" + } + }, + "h": 500, + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 0.6, + "w": 300 + } + ] + } + ] + }, + "ext": { + "debug": { + "httpcalls": { + "appnexus": null + }, + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "test": 1, + "ext": { + "prebid": { + "debug": 1, + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/debuglog_enabled.json b/exchange/exchangetest/debuglog_enabled.json new file mode 100644 index 00000000000..3b307b67e55 --- /dev/null +++ b/exchange/exchangetest/debuglog_enabled.json @@ -0,0 +1,232 @@ +{ + "debugLog": { + "Enabled": true, + "CacheType": "xml", + "TTL": 3600, + "Data": { + "Request": "test request string", + "Headers": "test headers string", + "Response": "" + } + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "test": 1, + "ext": { + "prebid": { + "debug":1, + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + } + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30, + "PrimaryCategory": "" + } + }, + { + "ortbBid": { + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 0.6, + "w": 300, + "h": 500, + "crid": "creative-3", + "cat": [ + "IAB1-2" + ] + }, + "bidType": "video" + } + ] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.20", + "hb_pb_appnexus": "0.20", + "hb_pb_cat_dur": "0.20_VideoGames_0s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + } + } + } + }, + { + "cat": [ + "IAB1-2" + ], + "crid": "creative-3", + "ext": { + "prebid": { + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.50", + "hb_pb_appnexus": "0.50", + "hb_pb_cat_dur": "0.50_HomeDecor_0s", + "hb_pb_cat_dur_appnex": "0.50_HomeDecor_0s", + "hb_size": "300x500", + "hb_size_appnexus": "300x500" + }, + "type": "video" + } + }, + "h": 500, + "id": "apn-other-bid", + "impid": "imp-id-2", + "price": 0.6, + "w": 300 + } + ] + } + ] + }, + "ext": { + "debug": { + "httpcalls": { + "appnexus": null + }, + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "test": 1, + "ext": { + "prebid": { + "debug":1, + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/lmt-featureflag-off.json b/exchange/exchangetest/lmt-featureflag-off.json new file mode 100644 index 00000000000..9a15c87953e --- /dev/null +++ b/exchange/exchangetest/lmt-featureflag-off.json @@ -0,0 +1,63 @@ +{ + "enforceLmt": false, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }], + "device": { + "lmt": 1 + }, + "user": { + "id": "some-id", + "buyeruid": "some-buyer-id" + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "device": { + "lmt": 1 + }, + "user": { + "id": "some-id", + "buyeruid": "some-buyer-id" + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/lmt-featureflag-on.json b/exchange/exchangetest/lmt-featureflag-on.json new file mode 100644 index 00000000000..440f8c76472 --- /dev/null +++ b/exchange/exchangetest/lmt-featureflag-on.json @@ -0,0 +1,61 @@ +{ + "enforceLmt": true, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + } + } + }], + "device": { + "lmt": 1 + }, + "user": { + "id": "some-id", + "buyeruid": "some-buyer-id" + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "device": { + "lmt": 1 + }, + "user": { + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/request-multi-bidders-debug-info.json b/exchange/exchangetest/request-multi-bidders-debug-info.json new file mode 100644 index 00000000000..db16dbe6013 --- /dev/null +++ b/exchange/exchangetest/request-multi-bidders-debug-info.json @@ -0,0 +1,230 @@ +{ + "incomingRequest": { + "ortbRequest": { + "test": 1, + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + }, + "audienceNetwork": { + "placementId": "some-placement" + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 2 + }, + "audienceNetwork": { + "placementId": "some-other-placement" + } + } + } + ], + "ext": { + "prebid": { + "debug":1, + "targeting": { + "durationRangeSec": [ + 15, + 30 + ], + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withCategory": true, + "translateCategories": true + } + } + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "httpCalls": [ + { + "uri": "appnexusTest.com", + "requestbody": "appnexusTestRequestBody", + "responsebody": "appnexusTestResponseBody", + "status": 200 + } + ], + "pbsSeatBid": { + "pbsBids": [ + { + "ortbBid": { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 12.00, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + } + } + ] + } + } + }, + "audienceNetwork": { + "mockResponse": { + "httpCalls": [ + { + "uri": "audienceNetworkTest.com", + "requestbody": "audienceNetworkTestRequestBody", + "responsebody": "audienceNetworkTestResponseBody", + "status": 200 + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 12.00, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "prebid": { + "type": "", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_size": "200x250", + "hb_size_appnexus": "200x250", + "hb_pb": "12.00", + "hb_pb_appnexus": "12.00", + "hb_pb_cat_dur": "12.00_VideoGames_15s", + "hb_pb_cat_dur_appnex": "12.00_VideoGames_15s" + } + } + } + } + ] + } + ] + }, + "ext": { + "debug": { + "httpcalls": { + "appnexus": [ + { + "uri": "appnexusTest.com", + "requestbody": "appnexusTestRequestBody", + "responsebody": "appnexusTestResponseBody", + "status": 200 + } + ], + "audienceNetwork": [ + { + "uri": "audienceNetworkTest.com", + "requestbody": "audienceNetworkTestRequestBody", + "responsebody": "audienceNetworkTestResponseBody", + "status": 200 + } + ] + }, + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 1 + }, + "audienceNetwork": { + "placementId": "some-placement" + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 2 + }, + "audienceNetwork": { + "placementId": "some-other-placement" + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "test": 1, + "ext": { + + "prebid": { + "debug": 1, + "targeting": { + "durationRangeSec": [ + 15, + 30 + ], + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withCategory": true, + "translateCategories": true + } + } + } + } + } + } + } + } +} + + diff --git a/exchange/exchangetest/request-multi-bidders-one-no-resp.json b/exchange/exchangetest/request-multi-bidders-one-no-resp.json new file mode 100644 index 00000000000..b7179ccb02e --- /dev/null +++ b/exchange/exchangetest/request-multi-bidders-one-no-resp.json @@ -0,0 +1,122 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 1 + }, + "audienceNetwork": { + "placementId": "some-placement" + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": ["video/mp4"] + }, + "ext": { + "appnexus": { + "placementId": 2 + }, + "audienceNetwork": { + "placementId": "some-other-placement" + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "durationRangeSec": [15,30], + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withCategory": true, + "translateCategories": true + } + } + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [ + { + "ortbBid": { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 12.00, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + } + } + ] + } + } + }, + "audienceNetwork": { + "mockResponse": { + + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 12.00, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": ["IAB1-1"], + "ext": { + "prebid": { + "type": "", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_size": "200x250", + "hb_size_appnexus": "200x250", + "hb_pb": "12.00", + "hb_pb_appnexus": "12.00", + "hb_pb_cat_dur": "12.00_VideoGames_15s", + "hb_pb_cat_dur_appnex": "12.00_VideoGames_15s" + } + } + } + }] + } + ] + } + } +} + + diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index d7459bfc059..59b92332100 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -8,12 +8,14 @@ import ( "testing" "time" + "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/currencies" "github.com/PubMatic-OpenWrap/prebid-server/gdpr" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" metricsConf "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics/config" + metricsConfig "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics/config" "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters" @@ -106,7 +108,7 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op if error != nil { t.Errorf("Failed to create a category Fetcher: %v", error) } - bidResp, err := ex.HoldAuction(context.Background(), req, &mockFetcher{}, pbsmetrics.Labels{}, &categoriesFetcher) + bidResp, err := ex.HoldAuction(context.Background(), req, &mockFetcher{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) if err != nil { t.Fatalf("Unexpected errors running auction: %v", err) @@ -132,7 +134,7 @@ func buildAdapterMap(bids map[openrtb_ext.BidderName][]*openrtb.Bid, mockServerU adapterMap[bidder] = adaptBidder(&mockTargetingBidder{ mockServerURL: mockServerURL, bids: bids, - }, client) + }, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) } return adapterMap } diff --git a/exchange/utils.go b/exchange/utils.go index 333d5e6b0ea..ada0edbae05 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -7,11 +7,13 @@ import ( "math/rand" "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/gdpr" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/privacy" "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/lmt" "github.com/buger/jsonparser" ) @@ -26,8 +28,8 @@ func cleanOpenRTBRequests(ctx context.Context, blables map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels, gDPR gdpr.Permissions, - usersyncIfAmbiguous, - enforceCCPA bool) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) { + usersyncIfAmbiguous bool, + privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) { impsByBidder, errs := splitImps(orig.Imp) if len(errs) > 0 { @@ -43,30 +45,41 @@ func cleanOpenRTBRequests(ctx context.Context, gdpr := extractGDPR(orig, usersyncIfAmbiguous) consent := extractConsent(orig) - isAMP := labels.RType == pbsmetrics.ReqTypeAMP + ampGDPRException := (labels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException() - privacyEnforcement := privacy.Enforcement{ - COPPA: orig.Regs != nil && orig.Regs.COPPA == 1, + var ccpaPolicy ccpa.Policy + if privacyConfig.CCPA.Enforce { + ccpaPolicy, _ = ccpa.ReadPolicy(orig) + } + + var lmtPolicy lmt.Policy + if privacyConfig.LMT.Enforce { + lmtPolicy = lmt.ReadPolicy(orig) } - if enforceCCPA { - ccpaPolicy, _ := ccpa.ReadPolicy(orig) - privacyEnforcement.CCPA = ccpaPolicy.ShouldEnforce() + // request level privacy policies + privacyEnforcement := privacy.Enforcement{ + CCPA: ccpaPolicy.ShouldEnforce(), + COPPA: orig.Regs != nil && orig.Regs.COPPA == 1, + LMT: lmtPolicy.ShouldEnforce(), } + // bidder level privacy policies for bidder, bidReq := range requestsByBidder { if gdpr == 1 { coreBidder := resolveBidder(bidder.String(), aliases) var publisherID = labels.PubID - ok, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) + ok, geo, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) privacyEnforcement.GDPR = !ok && err == nil + privacyEnforcement.GDPRGeo = !geo && err == nil } else { privacyEnforcement.GDPR = false + privacyEnforcement.GDPRGeo = false } - privacyEnforcement.Apply(bidReq, isAMP) + privacyEnforcement.Apply(bidReq, ampGDPRException) } return diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 2b9be831840..bd1be73ff3b 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/stretchr/testify/assert" @@ -24,11 +25,15 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return true, nil } -func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { +func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { if bidder == "appnexus" { - return true, nil + return true, true, nil } - return false, nil + return false, false, nil +} + +func (p *permissionsMock) AMPException() bool { + return false } func assertReq(t *testing.T, reqByBidders map[openrtb_ext.BidderName]*openrtb.BidRequest, @@ -65,8 +70,17 @@ func TestCleanOpenRTBRequests(t *testing.T) { applyCOPPA: false, consentedVendors: map[string]bool{"appnexus": true, "brightroll": true}}, } + privacyConfig := config.Privacy{ + CCPA: config.CCPA{ + Enforce: true, + }, + LMT: config.LMT{ + Enforce: true, + }, + } + for _, test := range testCases { - reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, true) + reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -95,9 +109,80 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { } for _, test := range testCases { - req := newCCPABidRequest(t) + req := newBidRequest(t) + req.Regs = &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`), + } + + privacyConfig := config.Privacy{ + CCPA: config.CCPA{ + Enforce: test.enforceCCPA, + }, + } - results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, test.enforceCCPA) + results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + result := results["appnexus"] + + assert.Nil(t, errs) + + if test.expectDataScrub { + assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") + assert.Equal(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5") + } else { + assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") + assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5") + } + } +} + +func TestCleanOpenRTBRequestsLMT(t *testing.T) { + var ( + enabled int8 = 1 + disabled int8 = 0 + ) + testCases := []struct { + description string + lmt *int8 + enforceLMT bool + expectDataScrub bool + }{ + { + description: "Feature Flag Enabled - OpenTRB Enabled", + lmt: &enabled, + enforceLMT: true, + expectDataScrub: true, + }, + { + description: "Feature Flag Disabled - OpenTRB Enabled", + lmt: &enabled, + enforceLMT: false, + expectDataScrub: false, + }, + { + description: "Feature Flag Enabled - OpenTRB Disabled", + lmt: &disabled, + enforceLMT: true, + expectDataScrub: false, + }, + { + description: "Feature Flag Disabled - OpenTRB Disabled", + lmt: &disabled, + enforceLMT: false, + expectDataScrub: false, + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.Device.Lmt = test.lmt + + privacyConfig := config.Privacy{ + LMT: config.LMT{ + Enforce: test.enforceLMT, + }, + } + + results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) result := results["appnexus"] assert.Nil(t, errs) @@ -159,8 +244,7 @@ func newAdapterAliasBidRequest(t *testing.T) *openrtb.BidRequest { } } -func newCCPABidRequest(t *testing.T) *openrtb.BidRequest { - dnt := int8(1) +func newBidRequest(t *testing.T) *openrtb.BidRequest { return &openrtb.BidRequest{ Site: &openrtb.Site{ Page: "www.some.domain.com", @@ -174,7 +258,6 @@ func newCCPABidRequest(t *testing.T) *openrtb.BidRequest { UA: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", IFA: "ifa", IP: "132.173.230.74", - DNT: &dnt, Language: "EN", }, Source: &openrtb.Source{ @@ -185,9 +268,6 @@ func newCCPABidRequest(t *testing.T) *openrtb.BidRequest { BuyerUID: "their-id", Ext: json.RawMessage(`{"digitrust":{"id":"digi-id","keyv":1,"pref":1}}`), }, - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`), - }, Imp: []openrtb.Imp{{ ID: "some-imp-id", Banner: &openrtb.Banner{ diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 4bd2302b651..b4cb336986a 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -6,25 +6,34 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/prebid/go-gdpr/vendorlist" ) type Permissions interface { // Determines whether or not the host company is allowed to read/write cookies. // - // If the consent string was nonsenical, the returned error will be an ErrorMalformedConsent. + // If the consent string was nonsensical, the returned error will be an ErrorMalformedConsent. HostCookiesAllowed(ctx context.Context, consent string) (bool, error) // Determines whether or not the given bidder is allowed to user personal info for ad targeting. // - // If the consent string was nonsenical, the returned error will be an ErrorMalformedConsent. + // If the consent string was nonsensical, the returned error will be an ErrorMalformedConsent. BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, consent string) (bool, error) // Determines whether or not to send PI information to a bidder, or mask it out. // - // If the consent string was nonsenical, the returned error will be an ErrorMalformedConsent. - PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) + // If the consent string was nonsensical, the returned error will be an ErrorMalformedConsent. + PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) + + // Exposes the AMP execption flag + AMPException() bool } +const ( + tCF1 uint8 = 1 + tCF2 uint8 = 2 +) + // NewPermissions gets an instance of the Permissions for use elsewhere in the project. func NewPermissions(ctx context.Context, cfg config.GDPR, vendorIDs map[openrtb_ext.BidderName]uint16, client *http.Client) Permissions { // If the host doesn't buy into the IAB GDPR consent framework, then save some cycles and let all syncs happen. @@ -33,9 +42,11 @@ func NewPermissions(ctx context.Context, cfg config.GDPR, vendorIDs map[openrtb_ } return &permissionsImpl{ - cfg: cfg, - vendorIDs: vendorIDs, - fetchVendorList: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker), + cfg: cfg, + vendorIDs: vendorIDs, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tCF1), + tCF2: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tCF2)}, } } diff --git a/gdpr/impl.go b/gdpr/impl.go index 54e1fbf57e9..7fa6fde588f 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -2,12 +2,16 @@ package gdpr import ( "context" + "fmt" - "github.com/prebid/go-gdpr/consentconstants" - "github.com/prebid/go-gdpr/vendorconsent" - "github.com/prebid/go-gdpr/vendorlist" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/prebid/go-gdpr/api" + tcf1constants "github.com/prebid/go-gdpr/consentconstants" + consentconstants "github.com/prebid/go-gdpr/consentconstants/tcf2" + "github.com/prebid/go-gdpr/vendorconsent" + tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2" + "github.com/prebid/go-gdpr/vendorlist" ) // This file implements GDPR permissions for the app. @@ -18,7 +22,7 @@ import ( type permissionsImpl struct { cfg config.GDPR vendorIDs map[openrtb_ext.BidderName]uint16 - fetchVendorList func(ctx context.Context, id uint16) (vendorlist.VendorList, error) + fetchVendorList map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error) } func (p *permissionsImpl) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { @@ -38,10 +42,10 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_ return false, nil } -func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { +func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { _, ok := p.cfg.NonStandardPublisherMap[PublisherID] if ok { - return true, nil + return true, true, nil } id, ok := p.vendorIDs[bidder] @@ -50,10 +54,14 @@ func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrt } if consent == "" { - return p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } - return false, nil + return false, false, nil +} + +func (p *permissionsImpl) AMPException() bool { + return p.cfg.AMPException } func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consent string) (bool, error) { @@ -71,36 +79,108 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen return false, nil } + // InfoStorageAccess is the same across TCF 1 and TCF 2 + if parsedConsent.Version() == 2 { + if !p.cfg.TCF2.Purpose1.Enabled { + // We are not enforcing purpose 1 + return true, nil + } + consent, ok := parsedConsent.(tcf2.ConsentMetadata) + if !ok { + err := fmt.Errorf("Unable to access TCF2 parsed consent") + return false, err + } + return p.checkPurpose(consent, vendor, vendorID, consentconstants.InfoStorageAccess), nil + } if vendor.Purpose(consentconstants.InfoStorageAccess) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && parsedConsent.VendorConsent(vendorID) { return true, nil } - return false, nil } -func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, error) { +func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, error) { // If we're not given a consent string, respect the preferences in the app config. if consent == "" { - return p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent) if err != nil { - return false, err + return false, false, err } if vendor == nil { - return false, nil + return false, false, nil } - if (vendor.Purpose(consentconstants.InfoStorageAccess) || vendor.LegitimateInterest(consentconstants.InfoStorageAccess)) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && (vendor.Purpose(consentconstants.AdSelectionDeliveryReporting) || vendor.LegitimateInterest(consentconstants.AdSelectionDeliveryReporting)) && parsedConsent.PurposeAllowed(consentconstants.AdSelectionDeliveryReporting) && parsedConsent.VendorConsent(vendorID) { - return true, nil + if parsedConsent.Version() == 2 { + if p.cfg.TCF2.Enabled { + return p.allowPITCF2(parsedConsent, vendor, vendorID) + } + if (vendor.Purpose(consentconstants.InfoStorageAccess) || vendor.LegitimateInterest(consentconstants.InfoStorageAccess)) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && (vendor.Purpose(consentconstants.PersonalizationProfile) || vendor.LegitimateInterest(consentconstants.PersonalizationProfile)) && parsedConsent.PurposeAllowed(consentconstants.PersonalizationProfile) && parsedConsent.VendorConsent(vendorID) { + return true, true, nil + } + } else { + if (vendor.Purpose(tcf1constants.InfoStorageAccess) || vendor.LegitimateInterest(tcf1constants.InfoStorageAccess)) && parsedConsent.PurposeAllowed(tcf1constants.InfoStorageAccess) && (vendor.Purpose(tcf1constants.AdSelectionDeliveryReporting) || vendor.LegitimateInterest(tcf1constants.AdSelectionDeliveryReporting)) && parsedConsent.PurposeAllowed(tcf1constants.AdSelectionDeliveryReporting) && parsedConsent.VendorConsent(vendorID) { + return true, true, nil + } + } + return false, false, nil +} + +func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16) (allowPI bool, allowGeo bool, err error) { + consent, ok := parsedConsent.(tcf2.ConsentMetadata) + err = nil + allowPI = false + allowGeo = false + if !ok { + err = fmt.Errorf("Unable to access TCF2 parsed consent") + return + } + if p.cfg.TCF2.SpecialPurpose1.Enabled { + allowGeo = consent.SpecialFeatureOptIn(1) && vendor.SpecialPurpose(1) + } else { + allowGeo = true + } + // Set to true so any purpose check can flip it to false + allowPI = true + if p.cfg.TCF2.Purpose1.Enabled { + allowPI = allowPI && p.checkPurpose(consent, vendor, vendorID, consentconstants.InfoStorageAccess) + } + if p.cfg.TCF2.Purpose2.Enabled { + allowPI = allowPI && p.checkPurpose(consent, vendor, vendorID, consentconstants.BasicAdserving) } + if p.cfg.TCF2.Purpose7.Enabled { + allowPI = allowPI && p.checkPurpose(consent, vendor, vendorID, consentconstants.AdPerformance) + } + return +} - return false, nil +const pubRestrictNotAllowed = 0 +const pubRestrictRequireConsent = 1 +const pubRestrictRequireLegitInterest = 2 + +func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose tcf1constants.Purpose) bool { + if purpose == consentconstants.InfoStorageAccess && p.cfg.TCF2.PurposeOneTreatment.Enabled && consent.PurposeOneTreatment() { + return p.cfg.TCF2.PurposeOneTreatment.AccessAllowed + } + if consent.CheckPubRestriction(uint8(purpose), pubRestrictNotAllowed, vendorID) { + return false + } + if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireConsent, vendorID) { + return vendor.PurposeStrict(purpose) && consent.PurposeAllowed(purpose) && consent.VendorConsent(vendorID) + } + if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireLegitInterest, vendorID) { + // Need LITransparency here + return vendor.LegitimateInterestStrict(purpose) && consent.PurposeLITransparency(purpose) && consent.VendorLegitInterest(vendorID) + } + purposeAllowed := vendor.Purpose(purpose) && consent.PurposeAllowed(purpose) && consent.VendorConsent(vendorID) + legitInterest := vendor.LegitimateInterest(purpose) && consent.PurposeLITransparency(purpose) && consent.VendorLegitInterest(vendorID) + + return purposeAllowed || legitInterest } -func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent vendorconsent.VendorConsents, vendor vendorlist.Vendor, err error) { +func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent api.VendorConsents, vendor api.Vendor, err error) { parsedConsent, err = vendorconsent.ParseString(consent) if err != nil { err = &ErrorMalformedConsent{ @@ -110,7 +190,11 @@ func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, cons return } - vendorList, err := p.fetchVendorList(ctx, parsedConsent.VendorListVersion()) + version := parsedConsent.Version() + if version < 1 || version > 2 { + return + } + vendorList, err := p.fetchVendorList[version](ctx, parsedConsent.VendorListVersion()) if err != nil { return } @@ -130,6 +214,10 @@ func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.B return true, nil } -func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) { - return true, nil +func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { + return true, true, nil +} + +func (a AlwaysAllow) AMPException() bool { + return false } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 685aba8cb0e..0635ee4e512 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -10,6 +10,9 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/prebid/go-gdpr/vendorlist" + "github.com/prebid/go-gdpr/vendorlist2" + + "github.com/stretchr/testify/assert" ) func TestNoConsentButAllowByDefault(t *testing.T) { @@ -18,8 +21,11 @@ func TestNoConsentButAllowByDefault(t *testing.T) { HostVendorID: 3, UsersyncIfAmbiguous: true, }, - vendorIDs: nil, - fetchVendorList: failedListFetcher, + vendorIDs: nil, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: failedListFetcher, + tCF2: failedListFetcher, + }, } allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "") assertBoolsEqual(t, true, allowSync) @@ -35,8 +41,11 @@ func TestNoConsentAndRejectByDefault(t *testing.T) { HostVendorID: 3, UsersyncIfAmbiguous: false, }, - vendorIDs: nil, - fetchVendorList: failedListFetcher, + vendorIDs: nil, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: failedListFetcher, + tCF2: failedListFetcher, + }, } allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "") assertBoolsEqual(t, false, allowSync) @@ -49,10 +58,10 @@ func TestNoConsentAndRejectByDefault(t *testing.T) { func TestAllowedSyncs(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, + purposes: []int{1}, }, 3: { - purposes: []uint8{1}, + purposes: []int{1}, }, }) perms := permissionsImpl{ @@ -63,9 +72,14 @@ func TestAllowedSyncs(t *testing.T) { openrtb_ext.BidderAppnexus: 2, openrtb_ext.BidderPubmatic: 3, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), - }), + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + }, } allowSync, err := perms.HostCookiesAllowed(context.Background(), "BON3PCUON3PCUABABBAAABoAAAAAMw") @@ -80,10 +94,10 @@ func TestAllowedSyncs(t *testing.T) { func TestProhibitedPurposes(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, // cookie reads/writes + purposes: []int{1}, // cookie reads/writes }, 3: { - purposes: []uint8{3}, // ad personalization + purposes: []int{3}, // ad personalization }, }) perms := permissionsImpl{ @@ -94,9 +108,14 @@ func TestProhibitedPurposes(t *testing.T) { openrtb_ext.BidderAppnexus: 2, openrtb_ext.BidderPubmatic: 3, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), - }), + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + }, } allowSync, err := perms.HostCookiesAllowed(context.Background(), "BON3PCUON3PCUABABBAAABAAAAAAMw") @@ -111,10 +130,10 @@ func TestProhibitedPurposes(t *testing.T) { func TestProhibitedVendors(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, // cookie reads/writes + purposes: []int{1}, // cookie reads/writes }, 3: { - purposes: []uint8{3}, // ad personalization + purposes: []int{3}, // ad personalization }, }) perms := permissionsImpl{ @@ -125,9 +144,14 @@ func TestProhibitedVendors(t *testing.T) { openrtb_ext.BidderAppnexus: 2, openrtb_ext.BidderPubmatic: 3, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), - }), + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + }, } allowSync, err := perms.HostCookiesAllowed(context.Background(), "BOS2bx5OS2bx5ABABBAAABoAAAAAFA") @@ -144,7 +168,10 @@ func TestMalformedConsent(t *testing.T) { cfg: config.GDPR{ HostVendorID: 2, }, - fetchVendorList: listFetcher(nil), + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: listFetcher(nil), + tCF2: listFetcher(nil), + }, } sync, err := perms.HostCookiesAllowed(context.Background(), "BON") @@ -155,10 +182,10 @@ func TestMalformedConsent(t *testing.T) { func TestAllowPersonalInfo(t *testing.T) { vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ 2: { - purposes: []uint8{1}, // cookie reads/writes + purposes: []int{1}, // cookie reads/writes }, 3: { - purposes: []uint8{1, 3}, // ad personalization + purposes: []int{1, 3}, // ad personalization }, }) perms := permissionsImpl{ @@ -169,27 +196,388 @@ func TestAllowPersonalInfo(t *testing.T) { openrtb_ext.BidderAppnexus: 2, openrtb_ext.BidderPubmatic: 3, }, - fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), - }), + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 1: parseVendorListData(t, vendorListData), + }), + }, } // PI needs both purposes to succeed - allowPI, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") + allowPI, _, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") assertNilErr(t, err) assertBoolsEqual(t, false, allowPI) - allowPI, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderPubmatic, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") + allowPI, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderPubmatic, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") assertNilErr(t, err) assertBoolsEqual(t, true, allowPI) // Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array perms.cfg.NonStandardPublisherMap = map[string]int{"appNexusAppID": 1} - allowPI, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") + allowPI, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") assertNilErr(t, err) assertBoolsEqual(t, true, allowPI) } +var tcf2BasicPurposes = map[uint16]*purposes{ + 2: {purposes: []int{1}}, //cookie reads/writes + 6: {purposes: []int{1, 2, 4}}, // ad personalization + 8: {purposes: []int{1, 7}}, + 10: {purposes: []int{2, 4, 7}}, + 32: {purposes: []int{1, 2, 4, 7}}, +} +var tcf2LegitInterests = map[uint16]*purposes{ + 6: {purposes: []int{7}}, + 8: {purposes: []int{2, 4}}, +} +var tcf2SpecialPuproses = map[uint16]*purposes{ + 6: {purposes: []int{1}}, + 10: {purposes: []int{1}}, +} +var tcf2FlexPurposes = map[uint16]*purposes{ + 6: {purposes: []int{1, 2, 4, 7}}, +} +var tcf2Config = config.GDPR{ + HostVendorID: 2, + TCF2: config.TCF2{ + Enabled: true, + Purpose1: config.PurposeDetail{Enabled: true}, + Purpose2: config.PurposeDetail{Enabled: true}, + Purpose7: config.PurposeDetail{Enabled: true}, + SpecialPurpose1: config.PurposeDetail{Enabled: true}, + }, +} + +type tcf2TestDef struct { + description string + bidder openrtb_ext.BidderName + consent string + allowPI bool + allowGeo bool +} + +func TestAllowPersonalInfoTCF2(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + // PI needs all purposes to succeed + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + allowPI: true, + allowGeo: true, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + allowPI: true, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + // Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array + perms.cfg.NonStandardPublisherMap = map[string]int{"appNexusAppID": 1} + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed") + assert.EqualValuesf(t, true, allowPI, "AllowPI failure") + assert.EqualValuesf(t, true, allowGeo, "AllowGeo failure") + +} + +func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 32, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 15: parseVendorListDataV2(t, vendorListData), + }), + }, + } + + // COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA - vendors 1-10 legit interest only, + // Pub restriction on purpose 7, consent only ... no allowPI will pass, no Special purpose 1 consent + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", + allowPI: false, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 10, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.TCF2.PurposeOneTreatment.Enabled = true + perms.cfg.TCF2.PurposeOneTreatment.AccessAllowed = true + + // COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA Purpose one flag set + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: true, + allowGeo: true, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: true, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 10, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.TCF2.PurposeOneTreatment.Enabled = true + perms.cfg.TCF2.PurposeOneTreatment.AccessAllowed = false + + // COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA Purpose one flag set + testDefs := []tcf2TestDef{ + { + description: "Appnexus vendor test, insufficient purposes claimed", + bidder: openrtb_ext.BidderAppnexus, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: false, + }, + { + description: "Pubmatic vendor test, flex purposes claimed", + bidder: openrtb_ext.BidderPubmatic, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: true, + }, + { + description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", + bidder: openrtb_ext.BidderRubicon, + consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", + allowPI: false, + allowGeo: false, + }, + } + + for _, td := range testDefs { + allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed for %s", td.description) + assert.EqualValuesf(t, td.allowPI, allowPI, "AllowPI failure on %s", td.description) + assert.EqualValuesf(t, td.allowGeo, allowGeo, "AllowGeo failure on %s", td.description) + } +} + +func TestAllowSyncTCF2(t *testing.T) { + vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") + assert.EqualValuesf(t, true, allowSync, "HostCookiesAllowed failure") + + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderRubicon, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") + assert.EqualValuesf(t, true, allowSync, "BidderSyncAllowed failure") +} + +func TestProhibitedPurposeSyncTCF2(t *testing.T) { + basicPurposes := tcf2BasicPurposes + basicPurposes[8] = &purposes{purposes: []int{7}} + vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.HostVendorID = 8 + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") + assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure") + + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderRubicon, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") + assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure") +} + +func TestProhibitedVendorSyncTCF2(t *testing.T) { + basicPurposes := tcf2BasicPurposes + basicPurposes[10] = &purposes{purposes: []int{1}} + vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + perms := permissionsImpl{ + cfg: tcf2Config, + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + openrtb_ext.BidderPubmatic: 6, + openrtb_ext.BidderRubicon: 8, + openrtb_ext.BidderOpenx: 10, + }, + fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ + tCF1: nil, + tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + 34: parseVendorListDataV2(t, vendorListData), + }), + }, + } + perms.cfg.HostVendorID = 10 + + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 4, 6 + allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") + assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure") + + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderRubicon, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") + assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure") +} + func parseVendorListData(t *testing.T, data string) vendorlist.VendorList { t.Helper() parsed, err := vendorlist.ParseEagerly([]byte(data)) @@ -199,6 +587,15 @@ func parseVendorListData(t *testing.T, data string) vendorlist.VendorList { return parsed } +func parseVendorListDataV2(t *testing.T, data string) vendorlist.VendorList { + t.Helper() + parsed, err := vendorlist2.ParseEagerly([]byte(data)) + if err != nil { + t.Fatalf("Failed to parse vendor list data. %v", err) + } + return parsed +} + func listFetcher(lists map[uint16]vendorlist.VendorList) func(context.Context, uint16) (vendorlist.VendorList, error) { return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { data, ok := lists[id] diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index f0e5b4e16d4..5cbcbfac784 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -10,13 +10,15 @@ import ( "sync/atomic" "time" + "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/golang/glog" + "github.com/prebid/go-gdpr/api" "github.com/prebid/go-gdpr/vendorlist" - "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/prebid/go-gdpr/vendorlist2" "golang.org/x/net/context/ctxhttp" ) -type saveVendors func(uint16, vendorlist.VendorList) +type saveVendors func(uint16, api.VendorList) // This file provides the vendorlist-fetching function for Prebid Server. // @@ -24,22 +26,22 @@ type saveVendors func(uint16, vendorlist.VendorList) // // Nothing in this file is exported. Public APIs can be found in gdpr.go -func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16) string) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { +func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16, uint8) string, TCFVer uint8) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { // These save and load functions can be used to store & retrieve lists from our cache. save, load := newVendorListCache() withTimeout, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) defer cancel() - populateCache(withTimeout, client, urlMaker, save) + populateCache(withTimeout, client, urlMaker, save, TCFVer) - saveOneSometimes := newOccasionalSaver(cfg.Timeouts.ActiveTimeout()) + saveOneSometimes := newOccasionalSaver(cfg.Timeouts.ActiveTimeout(), TCFVer) return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { list := load(id) if list != nil { return list, nil } - saveOneSometimes(ctx, client, urlMaker(id), save) + saveOneSometimes(ctx, client, urlMaker(id, TCFVer), save) list = load(id) if list != nil { return list, nil @@ -49,17 +51,23 @@ func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http } // populateCache saves all the known versions of the vendor list for future use. -func populateCache(ctx context.Context, client *http.Client, urlMaker func(uint16) string, saver saveVendors) { - latestVersion := saveOne(ctx, client, urlMaker(0), saver) +func populateCache(ctx context.Context, client *http.Client, urlMaker func(uint16, uint8) string, saver saveVendors, TCFVer uint8) { + latestVersion := saveOne(ctx, client, urlMaker(0, TCFVer), saver, TCFVer) for i := uint16(1); i < latestVersion; i++ { - saveOne(ctx, client, urlMaker(i), saver) + saveOne(ctx, client, urlMaker(i, TCFVer), saver, TCFVer) } } // Make a URL which can be used to fetch a given version of the Global Vendor List. If the version is 0, // this will fetch the latest version. -func vendorListURLMaker(version uint16) string { +func vendorListURLMaker(version uint16, TCFVer uint8) string { + if TCFVer == 2 { + if version == 0 { + return "https://vendorlist.consensu.org/v2/vendor-list.json" + } + return "https://vendorlist.consensu.org/v2/archives/vendor-list-v" + strconv.Itoa(int(version)) + ".json" + } if version == 0 { return "https://vendorlist.consensu.org/vendorlist.json" } @@ -71,7 +79,7 @@ func vendorListURLMaker(version uint16) string { // The goal here is to update quickly when new versions of the VendorList are released, but not wreck // server performance if a bad CMP starts sending us malformed consent strings that advertize a version // that doesn't exist yet. -func newOccasionalSaver(timeout time.Duration) func(ctx context.Context, client *http.Client, url string, saver saveVendors) { +func newOccasionalSaver(timeout time.Duration, TCFVer uint8) func(ctx context.Context, client *http.Client, url string, saver saveVendors) { lastSaved := &atomic.Value{} lastSaved.Store(time.Time{}) @@ -80,13 +88,13 @@ func newOccasionalSaver(timeout time.Duration) func(ctx context.Context, client if now.Sub(lastSaved.Load().(time.Time)).Minutes() > 10 { withTimeout, cancel := context.WithTimeout(ctx, timeout) defer cancel() - saveOne(withTimeout, client, url, saver) + saveOne(withTimeout, client, url, saver, TCFVer) lastSaved.Store(now) } } } -func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors) uint16 { +func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors, cTFVer uint8) uint16 { req, err := http.NewRequest("GET", url, nil) if err != nil { glog.Errorf("Failed to build GET %s request. Cookie syncs may be affected: %v", url, err) @@ -109,8 +117,12 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen glog.Errorf("GET %s returned %d. Cookie syncs may be affected.", url, resp.StatusCode) return 0 } - - newList, err := vendorlist.ParseEagerly(respBody) + var newList api.VendorList + if cTFVer == 2 { + newList, err = vendorlist2.ParseEagerly(respBody) + } else { + newList, err = vendorlist.ParseEagerly(respBody) + } if err != nil { glog.Errorf("GET %s returned malformed JSON. Cookie syncs may be affected. Error was %v. Body was %s", url, err, string(respBody)) return 0 @@ -120,13 +132,13 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen return newList.Version() } -func newVendorListCache() (save func(id uint16, list vendorlist.VendorList), load func(id uint16) vendorlist.VendorList) { +func newVendorListCache() (save func(id uint16, list api.VendorList), load func(id uint16) api.VendorList) { cache := &sync.Map{} - save = func(id uint16, list vendorlist.VendorList) { + save = func(id uint16, list api.VendorList) { cache.Store(id, list) } - load = func(id uint16) vendorlist.VendorList { + load = func(id uint16) api.VendorList { list, ok := cache.Load(id) if ok { return list.(vendorlist.VendorList) diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index af75aaeb541..32d7ef351b3 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -15,12 +15,12 @@ import ( func TestVendorFetch(t *testing.T) { vendorListOne := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2, 3}, + purposes: []int{1, 2, 3}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ @@ -29,7 +29,7 @@ func TestVendorFetch(t *testing.T) { }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) list, err := fetcher(context.Background(), 1) assertNilErr(t, err) vendor := list.Vendor(32) @@ -47,12 +47,12 @@ func TestVendorFetch(t *testing.T) { func TestLazyFetch(t *testing.T) { firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ 3: { - purposes: []uint8{1}, + purposes: []int{1}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ @@ -61,7 +61,7 @@ func TestLazyFetch(t *testing.T) { }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) list, err := fetcher(context.Background(), 2) assertNilErr(t, err) @@ -73,7 +73,7 @@ func TestLazyFetch(t *testing.T) { func TestInitialTimeout(t *testing.T) { list := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ @@ -83,7 +83,7 @@ func TestInitialTimeout(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), time.Time{}) defer cancel() - fetcher := newVendorListFetcher(ctx, testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(ctx, testConfig(), server.Client(), testURLMaker(server), 1) _, err := fetcher(context.Background(), 1) // This should do a lazy fetch, even though the initial call failed assertNilErr(t, err) } @@ -91,12 +91,12 @@ func TestInitialTimeout(t *testing.T) { func TestFetchThrottling(t *testing.T) { vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) vendorListThree := mockVendorListData(t, 3, map[uint16]*purposes{ 32: { - purposes: []uint8{1, 2}, + purposes: []int{1, 2}, }, }) server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ @@ -106,7 +106,7 @@ func TestFetchThrottling(t *testing.T) { }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) _, err := fetcher(context.Background(), 2) assertNilErr(t, err) _, err = fetcher(context.Background(), 3) @@ -117,7 +117,7 @@ func TestMalformedVendorlistFetch(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"}))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) _, err := fetcher(context.Background(), 1) assertErr(t, err, false) } @@ -126,15 +126,17 @@ func TestMissingVendorlistFetch(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"}))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) _, err := fetcher(context.Background(), 2) assertErr(t, err, false) } func TestVendorListMaker(t *testing.T) { - assertStringsEqual(t, "https://vendorlist.consensu.org/vendorlist.json", vendorListURLMaker(0)) - assertStringsEqual(t, "https://vendorlist.consensu.org/v-2/vendorlist.json", vendorListURLMaker(2)) - assertStringsEqual(t, "https://vendorlist.consensu.org/v-12/vendorlist.json", vendorListURLMaker(12)) + assertStringsEqual(t, "https://vendorlist.consensu.org/vendorlist.json", vendorListURLMaker(0, 1)) + assertStringsEqual(t, "https://vendorlist.consensu.org/v-2/vendorlist.json", vendorListURLMaker(2, 1)) + assertStringsEqual(t, "https://vendorlist.consensu.org/v-12/vendorlist.json", vendorListURLMaker(12, 1)) + assertStringsEqual(t, "https://vendorlist.consensu.org/v2/vendor-list.json", vendorListURLMaker(0, 2)) + assertStringsEqual(t, "https://vendorlist.consensu.org/v2/archives/vendor-list-v7.json", vendorListURLMaker(7, 2)) } // mockServer returns a handler which returns the given response for each global vendor list version. @@ -172,8 +174,8 @@ func mockServer(latestVersion int, responses map[int]string) func(http.ResponseW func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purposes) string { type vendorContract struct { - ID uint16 `json:"id"` - Purposes []uint8 `json:"purposeIds"` + ID uint16 `json:"id"` + Purposes []int `json:"purposeIds"` } type vendorListContract struct { @@ -201,9 +203,75 @@ func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purpos return string(data) } -func testURLMaker(server *httptest.Server) func(uint16) string { +type purposeMap map[uint16]*purposes + +func mockVendorListDataTCF2(t *testing.T, version uint16, basicPurposes purposeMap, legitInterests purposeMap, flexPurposes purposeMap, specialPurposes purposeMap) string { + type vendorContract struct { + ID uint16 `json:"id"` + Purposes []int `json:"purposes"` + LegIntPurposes []int `json:"legIntPurposes"` + FlexiblePurposes []int `json:"flexiblePurposes"` + SpecialPurposes []int `json:"specialPurposes"` + } + + type vendorListContract struct { + Version uint16 `json:"vendorListVersion"` + Vendors map[string]vendorContract `json:"vendors"` + } + + vendors := make(map[string]vendorContract, len(basicPurposes)) + for id, purpose := range basicPurposes { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.Purposes = purpose.purposes + vendors[sid] = vendor + } + + for id, purpose := range legitInterests { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.LegIntPurposes = purpose.purposes + vendors[sid] = vendor + } + + for id, purpose := range flexPurposes { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.FlexiblePurposes = purpose.purposes + vendors[sid] = vendor + } + + for id, purpose := range specialPurposes { + sid := strconv.Itoa(int(id)) + vendor, ok := vendors[sid] + if !ok { + vendor = vendorContract{ID: id} + } + vendor.SpecialPurposes = purpose.purposes + vendors[sid] = vendor + } + + obj := vendorListContract{ + Version: version, + Vendors: vendors, + } + data, err := json.Marshal(obj) + assertNilErr(t, err) + return string(data) +} + +func testURLMaker(server *httptest.Server) func(uint16, uint8) string { url := server.URL - return func(version uint16) string { + return func(version uint16, TCFVer uint8) string { return url + "?version=" + strconv.Itoa(int(version)) } } @@ -218,5 +286,5 @@ func testConfig() config.GDPR { } type purposes struct { - purposes []uint8 + purposes []int } diff --git a/go.mod b/go.mod index 30866e4eb40..949125b8594 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ module github.com/PubMatic-OpenWrap/prebid-server -go 1.12 +go 1.13 require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/DATA-DOG/go-sqlmock v1.3.0 github.com/NYTimes/gziphandler v1.1.1 github.com/OneOfOne/xxhash v1.2.5 // indirect + github.com/PubMatic-OpenWrap/etree v1.0.1 github.com/PubMatic-OpenWrap/openrtb v11.0.1-0.20200228131822-5216ebe65c0c+incompatible github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect @@ -32,7 +33,7 @@ require ( github.com/onsi/ginkgo v1.10.1 // indirect github.com/onsi/gomega v1.7.0 // indirect github.com/pelletier/go-toml v1.2.0 // indirect - github.com/prebid/go-gdpr v0.6.0 + github.com/prebid/go-gdpr v0.8.2 github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect @@ -47,7 +48,7 @@ require ( github.com/spf13/pflag v1.0.2 // indirect github.com/spf13/viper v1.1.0 github.com/stretchr/objx v0.1.1 // indirect - github.com/stretchr/testify v1.3.0 + github.com/stretchr/testify v1.5.1 github.com/vrischmann/go-metrics-influxdb v0.0.0-20160917065939-43af8332c303 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -55,8 +56,9 @@ require ( github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect - golang.org/x/net v0.0.0-20180906233101-161cd47e91fd + golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect golang.org/x/text v0.3.0 - gopkg.in/yaml.v2 v2.2.1 + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index ec388905643..98713ba6857 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI= github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/PubMatic-OpenWrap/etree v1.0.1 h1:Q8sZ99MuXKmAx2v4XThKjwlstgadZffiRbNwUG0Ey1U= +github.com/PubMatic-OpenWrap/etree v1.0.1/go.mod h1:5Y8qgcuDoy3XXG907UXkGnVTwihF16rXyJa4zRT7hOE= github.com/PubMatic-OpenWrap/openrtb v11.0.1-0.20200228131822-5216ebe65c0c+incompatible h1:BGwndVLu0ncwweHnofXzLo+SnRMe04Bq3KFfELLzif4= github.com/PubMatic-OpenWrap/openrtb v11.0.1-0.20200228131822-5216ebe65c0c+incompatible/go.mod h1:Ply/+GFe6FLkPMLV8Yh8xW0MpqclQyVf7m4PRsnaLDY= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco= @@ -70,8 +72,8 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prebid/go-gdpr v0.6.0 h1:/GKrygGkUbsgd96HIkjAu7/6CHtRedvcojRtfAd4Igc= -github.com/prebid/go-gdpr v0.6.0/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= +github.com/prebid/go-gdpr v0.8.2 h1:mN2jKYZZpJkCYFQB/nDTJoPpuGYblOYP2UUzOzRggII= +github.com/prebid/go-gdpr v0.8.2/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed h1:0dloFFFNNDG7c+8qtkYw2FdADrWy9s5cI8wHp6tK3Mg= github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= @@ -103,6 +105,8 @@ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/vrischmann/go-metrics-influxdb v0.0.0-20160917065939-43af8332c303 h1:Va10CytCCYRm4xBTses5ZDeDjeIQjhaiC9nRCe/yflI= github.com/vrischmann/go-metrics-influxdb v0.0.0-20160917065939-43af8332c303/go.mod h1:Xdcad1nGVhQfhoV0go+/4WaI/RZkWlvfjkVCdpMTxPY= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -117,15 +121,21 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3Ifn github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= @@ -136,3 +146,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/macros/macros.go b/macros/macros.go index 9f9cfad2dcd..a9f77ea95fa 100644 --- a/macros/macros.go +++ b/macros/macros.go @@ -11,6 +11,7 @@ type EndpointTemplateParams struct { PublisherID string ZoneID string SourceId string + AccountID string } // UserSyncTemplateParams specifies params for an user sync URL template diff --git a/main.go b/main.go index 8feab60ead9..0fa4454026b 100644 --- a/main.go +++ b/main.go @@ -48,26 +48,28 @@ func main() { */ func InitPrebidServer(configFile string) { + //init contents rand.Seed(time.Now().UnixNano()) - v := viper.New() - config.SetupViper(v, configFile) - v.SetConfigFile(configFile) - v.ReadInConfig() - - cfg, err := config.New(v) - + + //main contents + cfg, err := loadConfig(configFile) if err != nil { glog.Fatalf("Configuration could not be loaded or did not pass validation: %v", err) } - if err := serve(Rev, cfg); err != nil { + err = serve(Rev, cfg) + if err != nil { glog.Errorf("prebid-server failed: %v", err) } } -func loadConfig() (*config.Configuration, error) { +//const configFileName = "pbs" + +func loadConfig(configFileName string) (*config.Configuration, error) { v := viper.New() - config.SetupViper(v, "pbs") // filke = filename + config.SetupViper(v, configFileName) + v.SetConfigFile(configFileName) + v.ReadInConfig() return config.New(v) } @@ -116,4 +118,4 @@ func CookieSync(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { func SyncerMap() map[openrtb_ext.BidderName]usersync.Usersyncer { return router.SyncerMap() -} \ No newline at end of file +} diff --git a/openrtb_ext/adpod_test.go b/openrtb_ext/adpod_test.go index 6f17c13e3ea..b6f5d98b3f9 100644 --- a/openrtb_ext/adpod_test.go +++ b/openrtb_ext/adpod_test.go @@ -1,11 +1,6 @@ package openrtb_ext -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - +/* func TestVideoAdPod_Validate(t *testing.T) { type fields struct { MinAds *int @@ -303,4 +298,7 @@ func TestExtVideoAdPod_Validate(t *testing.T) { assert.Equal(t, tt.wantErr, actualErr) }) } -} \ No newline at end of file +} + + +*/ diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index c9c6f36332b..3b297c7ab5d 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -42,9 +42,9 @@ type BidType string const ( BidTypeBanner BidType = "banner" - BidTypeVideo = "video" - BidTypeAudio = "audio" - BidTypeNative = "native" + BidTypeVideo BidType = "video" + BidTypeAudio BidType = "audio" + BidTypeNative BidType = "native" ) func BidTypes() []BidType { @@ -87,7 +87,7 @@ const ( HbpbConstantKey TargetingKey = "hb_pb" // HbEnvKey exists to support the Prebid Universal Creative. If it exists, the only legal value is mobile-app. - // It will exist only if the incoming bidRequest defiend request.app instead of request.site. + // It will exist only if the incoming bidRequest defined request.app instead of request.site. HbEnvKey TargetingKey = "hb_env" // HbCacheHost and HbCachePath exist to supply cache host and path as targeting parameters diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go index af86cffd417..cbaa47d4f49 100644 --- a/openrtb_ext/bid_request_video.go +++ b/openrtb_ext/bid_request_video.go @@ -43,7 +43,7 @@ type BidRequestVideo struct { // object; optional // Description: // Container object for the user of of the actual device - User SimplifiedUser `json:"user,omitempty"` + User *openrtb.User `json:"user,omitempty"` // Attribute: // device @@ -67,7 +67,7 @@ type BidRequestVideo struct { // object; required // Description: // Player container object - Video SimplifiedVideo `json:"video,omitempty"` + Video *openrtb.Video `json:"video,omitempty"` // Attribute: // content @@ -136,6 +136,14 @@ type BidRequestVideo struct { // Description: // Contains the OpenRTB Regs object to be passed to OpenRTB request Regs *openrtb.Regs `json:"regs,omitempty"` + + // Attribute: + // supportdeals + // Type: + // bool; optional + // Description: + // Indicates that the response should update key to include prefix and tier + SupportDeals bool `json:"supportdeals,omitempty"` } type PodConfig struct { @@ -217,86 +225,3 @@ type Cacheconfig struct { // Time to Live for a cache entry specified in seconds Ttl int `json:"ttl"` } - -type Gdpr struct { - // Attribute: - // consentrequired - // Type: - // boolean; optional - // Indicates whether GDPR is in effect - ConsentRequired bool `json:"consentrequired"` - - // Attribute: - // consentstring - // Type: - // string; optional - // Contains the data structure developed by the GDPR - ConsentString string `json:"consentstring"` -} - -type SimplifiedUser struct { - // Attribute: - // buyeruids - // Type: - // map; optional - // ID of the stored config that corresponds to a single pod request - Buyeruids map[string]string `json:"buyeruids"` - - // Attribute: - // gdpr - // Type: - // object; optional - // Container object for GDPR - Gdpr Gdpr `json:"gdpr"` - - // Attribute: - // yob - // Type: - // int; optional - // Year of birth as a 4-digit integer - Yob int64 `json:"yob"` - - // Attribute: - // gender - // Type: - // string; optional - // Gender, where “M” = male, “F” = female, “O” = known to be other - Gender string `json:"gender"` - - // Attribute: - // keywords - // Type: - // string; optional - // Comma separated list of keywords, interests, or intent. - Keywords string `json:"keywords"` -} - -type SimplifiedVideo struct { - // Attribute: - // w - // Type: - // uint64; optional - // Width of video - W uint64 `json:"w"` - - // Attribute: - // h - // Type: - // uint64; optional - // Height of video - H uint64 `json:"h"` - - // Attribute: - // mimes - // Type: - // array of strings; optional - // Video mime types - Mimes []string `json:"mimes"` - - // Attribute: - // protocols - // Type: - // array of objects; optional - // protocols - Protocols []openrtb.Protocol `json:"protocols"` -} diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go old mode 100644 new mode 100755 index cb7a51929cd..424e4c37103 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -17,23 +17,38 @@ const schemaDirectory = "static/bidder-params" // BidderName may refer to a bidder ID, or an Alias which is defined in the request. type BidderName string +// BidderNameGeneral is reserved for non-bidder specific messages when using a map keyed on the bidder name. +const BidderNameGeneral = BidderName("general") + // These names _must_ coincide with the bidder code in Prebid.js, if an adapter also exists in that project. // Please keep these (and the BidderMap) alphabetized to minimize merge conflicts among adapter submissions. +// The bidder name 'general' is not allowed since it has special meaning in message maps. const ( Bidder33Across BidderName = "33across" BidderAdform BidderName = "adform" + BidderAdgeneration BidderName = "adgeneration" + BidderAdhese BidderName = "adhese" BidderAdkernel BidderName = "adkernel" BidderAdkernelAdn BidderName = "adkernelAdn" BidderAdpone BidderName = "adpone" + BidderAdmixer BidderName = "admixer" + BidderAdOcean BidderName = "adocean" + BidderAdtarget BidderName = "adtarget" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" + BidderAJA BidderName = "aja" BidderApplogy BidderName = "applogy" BidderAppnexus BidderName = "appnexus" + BidderAdoppler BidderName = "adoppler" + BidderAvocet BidderName = "avocet" BidderBeachfront BidderName = "beachfront" + BidderBeintoo BidderName = "beintoo" BidderBrightroll BidderName = "brightroll" BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" + BidderCpmstar BidderName = "cpmstar" BidderDatablocks BidderName = "datablocks" + BidderDmx BidderName = "dmx" BidderEmxDigital BidderName = "emx_digital" BidderEngageBDR BidderName = "engagebdr" BidderEPlanning BidderName = "eplanning" @@ -44,12 +59,18 @@ const ( BidderGumGum BidderName = "gumgum" BidderImprovedigital BidderName = "improvedigital" BidderIx BidderName = "ix" + BidderKidoz BidderName = "kidoz" BidderKubient BidderName = "kubient" BidderLifestreet BidderName = "lifestreet" BidderLockerDome BidderName = "lockerdome" + BidderLunaMedia BidderName = "lunamedia" BidderMarsmedia BidderName = "marsmedia" BidderMgid BidderName = "mgid" + BidderMobileFuse BidderName = "mobilefuse" + BidderNanoInteractive BidderName = "nanointeractive" + BidderNinthDecimal BidderName = "ninthdecimal" BidderOpenx BidderName = "openx" + BidderOrbidder BidderName = "orbidder" BidderPubmatic BidderName = "pubmatic" BidderPubnative BidderName = "pubnative" BidderPulsepoint BidderName = "pulsepoint" @@ -57,6 +78,7 @@ const ( BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSharethrough BidderName = "sharethrough" + BidderSmartRTB BidderName = "smartrtb" BidderSomoaudience BidderName = "somoaudience" BidderSonobi BidderName = "sonobi" BidderSovrn BidderName = "sovrn" @@ -66,29 +88,47 @@ const ( BidderTelaria BidderName = "telaria" BidderTriplelift BidderName = "triplelift" BidderTripleliftNative BidderName = "triplelift_native" + BidderUcfunnel BidderName = "ucfunnel" BidderUnruly BidderName = "unruly" + BidderValueImpression BidderName = "valueimpression" BidderVerizonMedia BidderName = "verizonmedia" BidderVisx BidderName = "visx" BidderVrtcal BidderName = "vrtcal" + BidderYeahmobi BidderName = "yeahmobi" + BidderYieldlab BidderName = "yieldlab" BidderYieldmo BidderName = "yieldmo" + BidderYieldone BidderName = "yieldone" + BidderZeroClickFraud BidderName = "zeroclickfraud" ) // BidderMap stores all the valid OpenRTB 2.x Bidders in the project. This map *must not* be mutated. +// The bidder name 'general' is not allowed since it has special meaning in message maps. var BidderMap = map[string]BidderName{ "33across": Bidder33Across, "adform": BidderAdform, + "adgeneration": BidderAdgeneration, + "adhese": BidderAdhese, "adkernel": BidderAdkernel, "adkernelAdn": BidderAdkernelAdn, + "admixer": BidderAdmixer, + "adocean": BidderAdOcean, "adpone": BidderAdpone, + "adtarget": BidderAdtarget, "adtelligent": BidderAdtelligent, "advangelists": BidderAdvangelists, + "aja": BidderAJA, "applogy": BidderApplogy, "appnexus": BidderAppnexus, + "adoppler": BidderAdoppler, + "avocet": BidderAvocet, "beachfront": BidderBeachfront, + "beintoo": BidderBeintoo, "brightroll": BidderBrightroll, "consumable": BidderConsumable, "conversant": BidderConversant, + "cpmstar": BidderCpmstar, "datablocks": BidderDatablocks, + "dmx": BidderDmx, "emx_digital": BidderEmxDigital, "engagebdr": BidderEngageBDR, "eplanning": BidderEPlanning, @@ -99,12 +139,18 @@ var BidderMap = map[string]BidderName{ "gumgum": BidderGumGum, "improvedigital": BidderImprovedigital, "ix": BidderIx, + "kidoz": BidderKidoz, "kubient": BidderKubient, "lifestreet": BidderLifestreet, "lockerdome": BidderLockerDome, + "lunamedia": BidderLunaMedia, "marsmedia": BidderMarsmedia, "mgid": BidderMgid, + "mobilefuse": BidderMobileFuse, + "nanointeractive": BidderNanoInteractive, + "ninthdecimal": BidderNinthDecimal, "openx": BidderOpenx, + "orbidder": BidderOrbidder, "pubmatic": BidderPubmatic, "pubnative": BidderPubnative, "pulsepoint": BidderPulsepoint, @@ -112,6 +158,7 @@ var BidderMap = map[string]BidderName{ "rtbhouse": BidderRTBHouse, "rubicon": BidderRubicon, "sharethrough": BidderSharethrough, + "smartrtb": BidderSmartRTB, "somoaudience": BidderSomoaudience, "sonobi": BidderSonobi, "sovrn": BidderSovrn, @@ -121,11 +168,17 @@ var BidderMap = map[string]BidderName{ "telaria": BidderTelaria, "triplelift": BidderTriplelift, "triplelift_native": BidderTripleliftNative, + "ucfunnel": BidderUcfunnel, "unruly": BidderUnruly, + "valueimpression": BidderValueImpression, "verizonmedia": BidderVerizonMedia, "visx": BidderVisx, "vrtcal": BidderVrtcal, + "yeahmobi": BidderYeahmobi, + "yieldlab": BidderYieldlab, "yieldmo": BidderYieldmo, + "yieldone": BidderYieldone, + "zeroclickfraud": BidderZeroClickFraud, } // BidderList returns the values of the BidderMap diff --git a/openrtb_ext/bidders_test.go b/openrtb_ext/bidders_test.go index 454a2454f31..d49b23237ed 100644 --- a/openrtb_ext/bidders_test.go +++ b/openrtb_ext/bidders_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/stretchr/testify/assert" "github.com/xeipuuv/gojsonschema" ) @@ -49,21 +50,14 @@ func TestInvalidParams(t *testing.T) { } } -func TestBidderList(t *testing.T) { - list := BidderList() +func TestBidderListMatchesBidderMap(t *testing.T) { + bidders := BidderList() for _, bidderName := range BidderMap { - adapterInList(t, bidderName, list) + assert.Contains(t, bidders, bidderName) } } -func adapterInList(t *testing.T, a BidderName, l []BidderName) { - found := false - for _, n := range l { - if a == n { - found = true - } - } - if !found { - t.Errorf("Adapter %s not found in the adapter map!", a) - } +func TestBidderListDoesNotDefineGeneral(t *testing.T) { + bidders := BidderList() + assert.NotContains(t, bidders, BidderNameGeneral) } diff --git a/openrtb_ext/device.go b/openrtb_ext/device.go index 9179c9c929d..afbea276988 100644 --- a/openrtb_ext/device.go +++ b/openrtb_ext/device.go @@ -3,8 +3,8 @@ package openrtb_ext import ( "strconv" - "github.com/buger/jsonparser" "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/buger/jsonparser" ) // PrebidExtKey represents the prebid extension key used in requests diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go index d3bcc9c73d1..ed3a88d62eb 100644 --- a/openrtb_ext/imp.go +++ b/openrtb_ext/imp.go @@ -20,6 +20,9 @@ type ExtImp struct { type ExtImpPrebid struct { StoredRequest *ExtStoredRequest `json:"storedrequest"` + // Rewarded inventory signal, can be 0 or 1 + IsRewardedInventory int8 `json:"is_rewarded_inventory"` + // NOTE: This is not part of the official API, we are not expecting clients // migrate from imp[...].ext.${BIDDER} to imp[...].ext.prebid.bidder.${BIDDER} // at this time diff --git a/openrtb_ext/imp_adgeneration.go b/openrtb_ext/imp_adgeneration.go new file mode 100644 index 00000000000..97834f2c926 --- /dev/null +++ b/openrtb_ext/imp_adgeneration.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpAdgeneration struct { + Id string `json:"id"` +} diff --git a/openrtb_ext/imp_adhese.go b/openrtb_ext/imp_adhese.go new file mode 100644 index 00000000000..1c822018b24 --- /dev/null +++ b/openrtb_ext/imp_adhese.go @@ -0,0 +1,12 @@ +package openrtb_ext + +import ( + "encoding/json" +) + +type ExtImpAdhese struct { + Account string `json:"account"` + Location string `json:"location"` + Format string `json:"format"` + Keywords json.RawMessage `json:"targets,omitempty"` +} diff --git a/openrtb_ext/imp_admixer.go b/openrtb_ext/imp_admixer.go new file mode 100644 index 00000000000..ce122ae0029 --- /dev/null +++ b/openrtb_ext/imp_admixer.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ExtImpAdmixer struct { + ZoneId string `json:"zone"` + CustomBidFloor float64 `json:"customFloor"` + CustomParams map[string]interface{} `json:"customParams"` +} diff --git a/openrtb_ext/imp_adocean.go b/openrtb_ext/imp_adocean.go new file mode 100644 index 00000000000..e690e929778 --- /dev/null +++ b/openrtb_ext/imp_adocean.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ExtImpAdOcean struct { + EmitterDomain string `json:"emiter"` + MasterID string `json:"masterId"` + SlaveID string `json:"slaveId"` +} diff --git a/openrtb_ext/imp_adoppler.go b/openrtb_ext/imp_adoppler.go new file mode 100644 index 00000000000..4b3ba97ce05 --- /dev/null +++ b/openrtb_ext/imp_adoppler.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpAdoppler struct { + AdUnit string `json:"adunit"` +} diff --git a/openrtb_ext/imp_adtarget.go b/openrtb_ext/imp_adtarget.go new file mode 100644 index 00000000000..a8ac70a17d1 --- /dev/null +++ b/openrtb_ext/imp_adtarget.go @@ -0,0 +1,9 @@ +package openrtb_ext + +// ExtImpAdtarget defines the contract for bidrequest.imp[i].ext.adtarget +type ExtImpAdtarget struct { + SourceId int `json:"aid"` + PlacementId int `json:"placementId,omitempty"` + SiteId int `json:"siteId,omitempty"` + BidFloor float64 `json:"bidFloor,omitempty"` +} diff --git a/openrtb_ext/imp_aja.go b/openrtb_ext/imp_aja.go new file mode 100644 index 00000000000..db04fa3f3ac --- /dev/null +++ b/openrtb_ext/imp_aja.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpAJA struct { + AdSpotID string `json:"asi"` +} diff --git a/openrtb_ext/imp_avocet.go b/openrtb_ext/imp_avocet.go new file mode 100644 index 00000000000..7c9ca8c6eed --- /dev/null +++ b/openrtb_ext/imp_avocet.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpAvocet defines the contract for bidrequest.imp[i].ext.avocet +type ExtImpAvocet struct { + Placement string `json:"placement,omitempty"` + PlacementCode string `json:"placement_code,omitempty"` +} diff --git a/openrtb_ext/imp_beintoo.go b/openrtb_ext/imp_beintoo.go new file mode 100644 index 00000000000..fe7599919d8 --- /dev/null +++ b/openrtb_ext/imp_beintoo.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpBeintoo struct { + TagID string `json:"tagid"` + BidFloor string `json:"bidfloor,omitempty"` +} diff --git a/openrtb_ext/imp_cpmstar.go b/openrtb_ext/imp_cpmstar.go new file mode 100644 index 00000000000..0b74f4d437d --- /dev/null +++ b/openrtb_ext/imp_cpmstar.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpCpmstar struct { + PoolId int `json:"placementId"` + SubPoolId int `json:"subpoolId,omitempty"` +} diff --git a/openrtb_ext/imp_grid.go b/openrtb_ext/imp_grid.go new file mode 100644 index 00000000000..d38e610d7a5 --- /dev/null +++ b/openrtb_ext/imp_grid.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpGrid defines the contract for bidrequest.imp[i].ext.grid +type ExtImpGrid struct { + Uid int `json:"uid"` +} diff --git a/openrtb_ext/imp_kidoz.go b/openrtb_ext/imp_kidoz.go new file mode 100644 index 00000000000..45f9866a425 --- /dev/null +++ b/openrtb_ext/imp_kidoz.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpKidoz struct { + AccessToken string `json:"access_token"` + PublisherID string `json:"publisher_id"` +} diff --git a/openrtb_ext/imp_lunamedia.go b/openrtb_ext/imp_lunamedia.go new file mode 100755 index 00000000000..e7e4dd6593c --- /dev/null +++ b/openrtb_ext/imp_lunamedia.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpLunaMedia struct { + PublisherID string `json:"pubid"` + Placement string `json:"placement,omitempty"` +} diff --git a/openrtb_ext/imp_mobilefuse.go b/openrtb_ext/imp_mobilefuse.go new file mode 100644 index 00000000000..ea53c5914f1 --- /dev/null +++ b/openrtb_ext/imp_mobilefuse.go @@ -0,0 +1,8 @@ +package openrtb_ext + +// ExtImpMobileFuse defines the contract for bidrequest.imp[i].ext.mobilefuse +type ExtImpMobileFuse struct { + PlacementId int `json:"placement_id"` + PublisherId int `json:"pub_id"` + TagidSrc string `json:"tagid_src"` +} diff --git a/openrtb_ext/imp_nanointeractive.go b/openrtb_ext/imp_nanointeractive.go new file mode 100644 index 00000000000..28db5be0d07 --- /dev/null +++ b/openrtb_ext/imp_nanointeractive.go @@ -0,0 +1,10 @@ +package openrtb_ext + +// ExtImpNanoInteractive defines the contract for bidrequest.imp[i].ext.nanointeractive +type ExtImpNanoInteractive struct { + Pid string `json:"pid"` + Nq []string `json:"nq, omitempty"` + Category string `json:"category, omitempty"` + SubId string `json:"subId, omitempty"` + Ref string `json:"ref, omitempty"` +} diff --git a/openrtb_ext/imp_ninthdecimal.go b/openrtb_ext/imp_ninthdecimal.go new file mode 100755 index 00000000000..8fb794dbdf2 --- /dev/null +++ b/openrtb_ext/imp_ninthdecimal.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpNinthDecimal struct { + PublisherID string `json:"pubid"` + Placement string `json:"placement,omitempty"` +} diff --git a/openrtb_ext/imp_orbidder.go b/openrtb_ext/imp_orbidder.go new file mode 100644 index 00000000000..ad141bdbcdf --- /dev/null +++ b/openrtb_ext/imp_orbidder.go @@ -0,0 +1,8 @@ +package openrtb_ext + +// ExtImpOrbidder defines the contract for bidrequest.imp[i].ext.openx +type ExtImpOrbidder struct { + AccountId string `json:"accountId"` + PlacementId string `json:"placementId"` + BidFloor float64 `json:"bidfloor"` +} diff --git a/openrtb_ext/imp_rubicon.go b/openrtb_ext/imp_rubicon.go index d588af82184..ee43d9659b8 100644 --- a/openrtb_ext/imp_rubicon.go +++ b/openrtb_ext/imp_rubicon.go @@ -12,6 +12,7 @@ type ExtImpRubicon struct { Inventory json.RawMessage `json:"inventory,omitempty"` Visitor json.RawMessage `json:"visitor,omitempty"` Video rubiconVideoParams `json:"video"` + Debug impExtRubiconDebug `json:"debug,omitempty"` } // rubiconVideoParams defines the contract for bidrequest.imp[i].ext.rubicon.video @@ -23,3 +24,8 @@ type rubiconVideoParams struct { Skip int `json:"skip,omitempty"` SkipDelay int `json:"skipdelay,omitempty"` } + +// rubiconVideoParams defines the contract for bidrequest.imp[i].ext.rubicon.debug +type impExtRubiconDebug struct { + CpmOverride float64 `json:"cpmoverride,omitempty"` +} diff --git a/openrtb_ext/imp_smartrtb.go b/openrtb_ext/imp_smartrtb.go new file mode 100644 index 00000000000..d056046bf9d --- /dev/null +++ b/openrtb_ext/imp_smartrtb.go @@ -0,0 +1,8 @@ +package openrtb_ext + +type ExtImpSmartRTB struct { + PubID string `json:"pub_id,omitempty"` + MedID string `json:"med_id,omitempty"` + ZoneID string `json:"zone_id,omitempty"` + ForceBid bool `json:"force_bid,omitempty"` +} diff --git a/openrtb_ext/imp_synacormedia.go b/openrtb_ext/imp_synacormedia.go index 1b044ceaa9c..af48c7dfd01 100644 --- a/openrtb_ext/imp_synacormedia.go +++ b/openrtb_ext/imp_synacormedia.go @@ -3,4 +3,5 @@ package openrtb_ext // ExtImpSynacormedia defines the contract for bidrequest.imp[i].ext.synacormedia type ExtImpSynacormedia struct { SeatId string `json:"seatId"` + TagId string `json:"tagId"` } diff --git a/openrtb_ext/imp_ucfunnel.go b/openrtb_ext/imp_ucfunnel.go new file mode 100644 index 00000000000..408c1e0a35e --- /dev/null +++ b/openrtb_ext/imp_ucfunnel.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpUcfunnel defines the contract for bidrequest.imp[i].ext.ucfunnel +type ExtImpUcfunnel struct { + AdUnitId string `json:"adunitid"` + PartnerId string `json:"partnerid"` +} diff --git a/openrtb_ext/imp_valueimpression.go b/openrtb_ext/imp_valueimpression.go new file mode 100644 index 00000000000..7c5c70ee0a7 --- /dev/null +++ b/openrtb_ext/imp_valueimpression.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpValueImpression struct { + SiteId string `json:"siteId"` +} diff --git a/openrtb_ext/imp_yeahmobi.go b/openrtb_ext/imp_yeahmobi.go new file mode 100644 index 00000000000..6c1c045d705 --- /dev/null +++ b/openrtb_ext/imp_yeahmobi.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpYeahmobi defines the contract for bidrequest.imp[i].ext.yeahmobi +type ExtImpYeahmobi struct { + PubId string `json:"pubId"` + ZoneId string `json:"zoneId"` +} diff --git a/openrtb_ext/imp_yieldlab.go b/openrtb_ext/imp_yieldlab.go new file mode 100644 index 00000000000..604b7e8ceab --- /dev/null +++ b/openrtb_ext/imp_yieldlab.go @@ -0,0 +1,10 @@ +package openrtb_ext + +// ExtImpYieldlab defines the contract for bidrequest.imp[i].ext.yieldlab +type ExtImpYieldlab struct { + AdslotID string `json:"adslotId"` + SupplyID string `json:"supplyId"` + AdSize string `json:"adSize"` + Targeting map[string]string `json:"targeting"` + ExtId string `json:"extId"` +} diff --git a/openrtb_ext/imp_yieldone.go b/openrtb_ext/imp_yieldone.go new file mode 100644 index 00000000000..6eee563b448 --- /dev/null +++ b/openrtb_ext/imp_yieldone.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpYieldone defines the contract for bidrequest.imp[i].ext.yieldone +type ExtImpYieldone struct { + PlacementId string `json:"placementId"` +} diff --git a/openrtb_ext/imp_zeroclickfraud.go b/openrtb_ext/imp_zeroclickfraud.go new file mode 100644 index 00000000000..ae82fcacd9a --- /dev/null +++ b/openrtb_ext/imp_zeroclickfraud.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpZeroClickFraud defines the contract for bidrequest.imp[i].ext.datablocks +type ExtImpZeroClickFraud struct { + SourceId int `json:"sourceId"` + Host string `json:"host"` +} diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index d64e65fdbaf..a0c74af6891 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -17,6 +17,7 @@ type ExtRequestPrebid struct { Cache *ExtRequestPrebidCache `json:"cache,omitempty"` StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` Targeting *ExtRequestTargeting `json:"targeting,omitempty"` + SupportDeals bool `json:"supportdeals,omitempty"` Debug int `json:"debug,omitempty"` BidderParams interface{} `json:"bidderparams,omitempty"` } @@ -149,10 +150,11 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error { } prevMax = gr.Max } - } else { - return errors.New("Price granularity error: empty granularity definition supplied") + *pg = PriceGranularity(pgraw) + return nil } - *pg = PriceGranularity(pgraw) + // Default to medium if no ranges are specified + *pg = priceGranularityMed return nil } @@ -160,7 +162,7 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error { func PriceGranularityFromString(gran string) PriceGranularity { switch gran { case "low": - return priceGranulrityLow + return priceGranularityLow case "med", "medium": // Seems that PBS was written with medium = "med", so hacking that in return priceGranularityMed @@ -175,7 +177,7 @@ func PriceGranularityFromString(gran string) PriceGranularity { return PriceGranularity{} } -var priceGranulrityLow = PriceGranularity{ +var priceGranularityLow = PriceGranularity{ Precision: 2, Ranges: []GranularityRange{{ Min: 0, diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index 860334af98f..e4046a622db 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -8,12 +8,12 @@ import ( "github.com/stretchr/testify/assert" ) -// Test the unmashalling of the prebid extensions and setting default Price Granularity +// Test the unmarshalling of the prebid extensions and setting default Price Granularity func TestExtRequestTargeting(t *testing.T) { extRequest := &ExtRequest{} err := json.Unmarshal([]byte(ext1), extRequest) if err != nil { - t.Errorf("ext1 Unmashall falure: %s", err.Error()) + t.Errorf("ext1 Unmarshall failure: %s", err.Error()) } if extRequest.Prebid.Targeting != nil { t.Error("ext1 Targeting is not nil") @@ -22,7 +22,7 @@ func TestExtRequestTargeting(t *testing.T) { extRequest = &ExtRequest{} err = json.Unmarshal([]byte(ext2), extRequest) if err != nil { - t.Errorf("ext2 Unmashall falure: %s", err.Error()) + t.Errorf("ext2 Unmarshall failure: %s", err.Error()) } if extRequest.Prebid.Targeting == nil { t.Error("ext2 Targeting is nil") @@ -36,7 +36,7 @@ func TestExtRequestTargeting(t *testing.T) { extRequest = &ExtRequest{} err = json.Unmarshal([]byte(ext3), extRequest) if err != nil { - t.Errorf("ext3 Unmashall falure: %s", err.Error()) + t.Errorf("ext3 Unmarshall failure: %s", err.Error()) } if extRequest.Prebid.Targeting == nil { t.Error("ext3 Targeting is nil") @@ -175,11 +175,22 @@ var validGranularityTests []granularityTestData = []granularityTestData{ }, }, }, + { + json: []byte(`{}`), + target: priceGranularityMed, + }, + { + json: []byte(`{"precision": 2}`), + target: priceGranularityMed, + }, + { + json: []byte(`{"precision": 2, "ranges":[]}`), + target: priceGranularityMed, + }, } func TestGranularityUnmarshalBad(t *testing.T) { tests := [][]byte{ - []byte(`{}`), []byte(`[]`), []byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`), []byte(`{"ranges":[{"max":20, "increment": -1}]}`), diff --git a/pbs/pbsrequest_test.go b/pbs/pbsrequest_test.go index 29c40cec427..566057473b8 100644 --- a/pbs/pbsrequest_test.go +++ b/pbs/pbsrequest_test.go @@ -8,9 +8,9 @@ import ( "strings" "testing" - "github.com/magiconair/properties/assert" "github.com/PubMatic-OpenWrap/prebid-server/cache/dummycache" "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/magiconair/properties/assert" ) const mimeVideoMp4 = "video/mp4" diff --git a/pbs/usersync.go b/pbs/usersync.go index c48f5e944a6..dbc2e27e4eb 100644 --- a/pbs/usersync.go +++ b/pbs/usersync.go @@ -9,13 +9,13 @@ import ( "strings" "time" - "github.com/golang/glog" - "github.com/julienschmidt/httprouter" "github.com/PubMatic-OpenWrap/prebid-server/analytics" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/ssl" "github.com/PubMatic-OpenWrap/prebid-server/usersync" + "github.com/golang/glog" + "github.com/julienschmidt/httprouter" ) // Recaptcha code from https://github.com/haisum/recaptcha/blob/master/recaptcha.go diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index 299661638d2..ce6c0f5a707 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -181,6 +181,20 @@ func (me *MultiMetricsEngine) RecordPrebidCacheRequestTime(success bool, length } } +// RecordRequestQueueTime across all engines +func (me *MultiMetricsEngine) RecordRequestQueueTime(success bool, requestType pbsmetrics.RequestType, length time.Duration) { + for _, thisME := range *me { + thisME.RecordRequestQueueTime(success, requestType, length) + } +} + +// RecordTimeoutNotice across all engines +func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) { + for _, thisME := range *me { + thisME.RecordTimeoutNotice(success) + } +} + // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests) type DummyMetricsEngine struct{} @@ -251,3 +265,11 @@ func (me *DummyMetricsEngine) RecordStoredImpCacheResult(cacheResult pbsmetrics. // RecordPrebidCacheRequestTime as a noop func (me *DummyMetricsEngine) RecordPrebidCacheRequestTime(success bool, length time.Duration) { } + +// RecordRequestQueueTime as a noop +func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType pbsmetrics.RequestType, length time.Duration) { +} + +// RecordTimeoutNotice as a noop +func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) { +} diff --git a/pbsmetrics/config/metrics_test.go b/pbsmetrics/config/metrics_test.go index 7de78b99983..26635569969 100644 --- a/pbsmetrics/config/metrics_test.go +++ b/pbsmetrics/config/metrics_test.go @@ -115,6 +115,9 @@ func TestMultiMetricsEngine(t *testing.T) { for i := 0; i < 3; i++ { metricsEngine.RecordImps(impTypeLabels) } + + metricsEngine.RecordRequestQueueTime(false, pbsmetrics.ReqTypeVideo, time.Duration(1)) + //Make the metrics engine, instantiated here with goEngine, fill its RequestStatuses[RequestType][pbsmetrics.RequestStatusXX] with the new boolean values added to pbsmetrics.Labels VerifyMetrics(t, "RequestStatuses.OpenRTB2.OK", goEngine.RequestStatuses[pbsmetrics.ReqTypeORTB2Web][pbsmetrics.RequestStatusOK].Count(), 5) VerifyMetrics(t, "RequestStatuses.Legacy.OK", goEngine.RequestStatuses[pbsmetrics.ReqTypeLegacy][pbsmetrics.RequestStatusOK].Count(), 0) @@ -148,6 +151,9 @@ func TestMultiMetricsEngine(t *testing.T) { } VerifyMetrics(t, "AdapterMetrics.AppNexus.GotBidsMeter", goEngine.AdapterMetrics[openrtb_ext.BidderAppnexus].GotBidsMeter.Count(), 0) VerifyMetrics(t, "AdapterMetrics.AppNexus.NoBidMeter", goEngine.AdapterMetrics[openrtb_ext.BidderAppnexus].NoBidMeter.Count(), 5) + + VerifyMetrics(t, "RecordRequestQueueTime.Video.Rejected", goEngine.RequestsQueueTimer[pbsmetrics.ReqTypeVideo][false].Count(), 1) + VerifyMetrics(t, "RecordRequestQueueTime.Video.Accepted", goEngine.RequestsQueueTimer[pbsmetrics.ReqTypeVideo][true].Count(), 0) } func VerifyMetrics(t *testing.T, name string, actual int64, expected int64) { diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go index df757728a38..cf634cc5ae1 100644 --- a/pbsmetrics/go_metrics.go +++ b/pbsmetrics/go_metrics.go @@ -24,6 +24,7 @@ type Metrics struct { SafariRequestMeter metrics.Meter SafariNoCookieMeter metrics.Meter RequestTimer metrics.Timer + RequestsQueueTimer map[RequestType]map[bool]metrics.Timer PrebidCacheRequestTimerSuccess metrics.Timer PrebidCacheRequestTimerError metrics.Timer StoredReqCacheMeter map[CacheResult]metrics.Meter @@ -47,6 +48,9 @@ type Metrics struct { ImpsTypeAudio metrics.Meter ImpsTypeNative metrics.Meter + TimeoutNotificationSuccess metrics.Meter + TimeoutNotificationFailure metrics.Meter + AdapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics // Don't export accountMetrics because we need helper functions here to insure its properly populated dynamically accountMetrics map[string]*accountMetrics @@ -111,6 +115,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa SafariRequestMeter: blankMeter, SafariNoCookieMeter: blankMeter, RequestTimer: blankTimer, + RequestsQueueTimer: make(map[RequestType]map[bool]metrics.Timer), PrebidCacheRequestTimerSuccess: blankTimer, PrebidCacheRequestTimerError: blankTimer, StoredReqCacheMeter: make(map[CacheResult]metrics.Meter), @@ -129,6 +134,9 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa ImpsTypeAudio: blankMeter, ImpsTypeNative: blankMeter, + TimeoutNotificationSuccess: blankMeter, + TimeoutNotificationFailure: blankMeter, + AdapterMetrics: make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)), accountMetrics: make(map[string]*accountMetrics), MetricsDisabled: disableMetrics, @@ -146,6 +154,11 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa } } + //to minimize memory usage, queuedTimeout metric is now supported for video endpoint only + //boolean value represents 2 general request statuses: accepted and rejected + newMetrics.RequestsQueueTimer["video"] = make(map[bool]metrics.Timer) + newMetrics.RequestsQueueTimer["video"][true] = blankTimer + newMetrics.RequestsQueueTimer["video"][false] = blankTimer return newMetrics } @@ -191,13 +204,20 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d statusMap[stat] = metrics.GetOrRegisterMeter("requests."+string(stat)+"."+string(typ), registry) } } + for _, cacheRes := range CacheResults() { newMetrics.StoredReqCacheMeter[cacheRes] = metrics.GetOrRegisterMeter(fmt.Sprintf("stored_request_cache_%s", string(cacheRes)), registry) newMetrics.StoredImpCacheMeter[cacheRes] = metrics.GetOrRegisterMeter(fmt.Sprintf("stored_imp_cache_%s", string(cacheRes)), registry) } + newMetrics.RequestsQueueTimer["video"][true] = metrics.GetOrRegisterTimer("queued_requests.video.accepted", registry) + newMetrics.RequestsQueueTimer["video"][false] = metrics.GetOrRegisterTimer("queued_requests.video.rejected", registry) + newMetrics.userSyncSet[unknownBidder] = metrics.GetOrRegisterMeter("usersync.unknown.sets", registry) newMetrics.userSyncGDPRPrevent[unknownBidder] = metrics.GetOrRegisterMeter("usersync.unknown.gdpr_prevent", registry) + + newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry) + newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry) return newMetrics } @@ -526,6 +546,22 @@ func (me *Metrics) RecordPrebidCacheRequestTime(success bool, length time.Durati } } +func (me *Metrics) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) { + if requestType == ReqTypeVideo { //remove this check when other request types are supported + me.RequestsQueueTimer[requestType][success].Update(length) + } + +} + +func (me *Metrics) RecordTimeoutNotice(success bool) { + if success { + me.TimeoutNotificationSuccess.Mark(1) + } else { + me.TimeoutNotificationFailure.Mark(1) + } + return +} + func doMark(bidder openrtb_ext.BidderName, meters map[openrtb_ext.BidderName]metrics.Meter) { met, ok := meters[bidder] if ok { diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go index 69565108499..d888385da16 100644 --- a/pbsmetrics/go_metrics_test.go +++ b/pbsmetrics/go_metrics_test.go @@ -50,6 +50,12 @@ func TestNewMetrics(t *testing.T) { ensureContains(t, registry, "requests.badinput.video", m.RequestStatuses[ReqTypeVideo][RequestStatusBadInput]) ensureContains(t, registry, "requests.err.video", m.RequestStatuses[ReqTypeVideo][RequestStatusErr]) ensureContains(t, registry, "requests.networkerr.video", m.RequestStatuses[ReqTypeVideo][RequestStatusNetworkErr]) + + ensureContains(t, registry, "queued_requests.video.rejected", m.RequestsQueueTimer[ReqTypeVideo][false]) + ensureContains(t, registry, "queued_requests.video.accepted", m.RequestsQueueTimer[ReqTypeVideo][true]) + + ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess) + ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure) } func TestRecordBidType(t *testing.T) { diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index 8ef9cfb8950..770f5750335 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -154,11 +154,12 @@ func CookieTypes() []CookieFlag { // Request/return status const ( - RequestStatusOK RequestStatus = "ok" - RequestStatusBadInput RequestStatus = "badinput" - RequestStatusErr RequestStatus = "err" - RequestStatusNetworkErr RequestStatus = "networkerr" - RequestStatusBlacklisted RequestStatus = "blacklistedacctorapp" + RequestStatusOK RequestStatus = "ok" + RequestStatusBadInput RequestStatus = "badinput" + RequestStatusErr RequestStatus = "err" + RequestStatusNetworkErr RequestStatus = "networkerr" + RequestStatusBlacklisted RequestStatus = "blacklistedacctorapp" + RequestStatusQueueTimeout RequestStatus = "queuetimeout" ) func RequestStatuses() []RequestStatus { @@ -168,6 +169,7 @@ func RequestStatuses() []RequestStatus { RequestStatusErr, RequestStatusNetworkErr, RequestStatusBlacklisted, + RequestStatusQueueTimeout, } } @@ -248,7 +250,7 @@ func RequestActions() []RequestAction { // MetricsEngine is a generic interface to record PBS metrics into the desired backend // The first three metrics function fire off once per incoming request, so total metrics -// will equal the total numer of incoming requests. The remaining 5 fire off per outgoing +// will equal the total number of incoming requests. The remaining 5 fire off per outgoing // request to a bidder adapter, so will record a number of hits per incoming request. The // two groups should be consistent within themselves, but comparing numbers between groups // is generally not useful. @@ -272,4 +274,6 @@ type MetricsEngine interface { RecordStoredReqCacheResult(cacheResult CacheResult, inc int) RecordStoredImpCacheResult(cacheResult CacheResult, inc int) RecordPrebidCacheRequestTime(success bool, length time.Duration) + RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) + RecordTimeoutNotice(sucess bool) } diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go index 7287fcc294b..d5661f4bfe4 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -96,3 +96,13 @@ func (me *MetricsEngineMock) RecordStoredImpCacheResult(cacheResult CacheResult, func (me *MetricsEngineMock) RecordPrebidCacheRequestTime(success bool, length time.Duration) { me.Called(success, length) } + +// RecordRequestQueueTime mock +func (me *MetricsEngineMock) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) { + me.Called(success, requestType, length) +} + +// RecordTimeoutNotice mock +func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) { + me.Called(success) +} diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go index 7654dd54f82..e27451c4bd6 100644 --- a/pbsmetrics/prometheus/preload.go +++ b/pbsmetrics/prometheus/preload.go @@ -1,6 +1,7 @@ package prometheusmetrics import ( + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/prometheus/client_golang/prometheus" ) @@ -91,6 +92,13 @@ func preloadLabelValues(m *Metrics) { adapterLabel: adapterValues, actionLabel: actionValues, }) + + //to minimize memory usage, queuedTimeout metric is now supported for video endpoint only + //boolean value represents 2 general request statuses: accepted and rejected + preloadLabelValuesForHistogram(m.requestsQueueTimer, map[string][]string{ + requestTypeLabel: {string(pbsmetrics.ReqTypeVideo)}, + requestStatusLabel: {requestSuccessLabel, requestRejectLabel}, + }) } func preloadLabelValuesForCounter(counter *prometheus.CounterVec, labelsWithValues map[string][]string) { diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index ecd76b74bf3..9c06d6032f4 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -24,9 +24,11 @@ type Metrics struct { prebidCacheWriteTimer *prometheus.HistogramVec requests *prometheus.CounterVec requestsTimer *prometheus.HistogramVec + requestsQueueTimer *prometheus.HistogramVec requestsWithoutCookie *prometheus.CounterVec storedImpressionsCacheResult *prometheus.CounterVec storedRequestCacheResult *prometheus.CounterVec + timeout_notifications *prometheus.CounterVec // Adapter Metrics adapterBids *prometheus.CounterVec @@ -73,11 +75,22 @@ const ( markupDeliveryNurl = "nurl" ) +const ( + requestSuccessLabel = "requestAcceptedLabel" + requestRejectLabel = "requestRejectedLabel" +) + +const ( + requestSuccessful = "ok" + requestFailed = "failed" +) + // NewMetrics initializes a new Prometheus metrics instance with preloaded label values. func NewMetrics(cfg config.PrometheusMetrics) *Metrics { requestTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1} - cacheWriteTimeBuckts := []float64{0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1} + cacheWriteTimeBuckets := []float64{0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1} priceBuckets := []float64{250, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000} + queuedRequestTimeBuckets := []float64{0, 1, 5, 30, 60, 120, 180, 240, 300} metrics := Metrics{} metrics.Registry = prometheus.NewRegistry() @@ -112,7 +125,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "prebidcache_write_time_seconds", "Seconds to write to Prebid Cache labeled by success or failure. Failure timing is limited by Prebid Server enforced timeouts.", []string{successLabel}, - cacheWriteTimeBuckts) + cacheWriteTimeBuckets) metrics.requests = newCounter(cfg, metrics.Registry, "requests", @@ -140,6 +153,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of stored request cache requests attempts by hits or miss.", []string{cacheResultLabel}) + metrics.timeout_notifications = newCounter(cfg, metrics.Registry, + "timeout_notification", + "Count of timeout notifications triggered, and if they were successfully sent.", + []string{successLabel}) + metrics.adapterBids = newCounter(cfg, metrics.Registry, "adapter_bids", "Count of bids labeled by adapter and markup delivery type (adm or nurl).", @@ -187,6 +205,12 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of total requests to Prebid Server labeled by account.", []string{accountLabel}) + metrics.requestsQueueTimer = newHistogram(cfg, metrics.Registry, + "request_queue_time", + "Seconds request was waiting in queue", + []string{requestTypeLabel, requestStatusLabel}, + queuedRequestTimeBuckets) + preloadLabelValues(&metrics) return &metrics @@ -374,3 +398,26 @@ func (m *Metrics) RecordPrebidCacheRequestTime(success bool, length time.Duratio successLabel: strconv.FormatBool(success), }).Observe(length.Seconds()) } + +func (m *Metrics) RecordRequestQueueTime(success bool, requestType pbsmetrics.RequestType, length time.Duration) { + successLabelFormatted := requestRejectLabel + if success { + successLabelFormatted = requestSuccessLabel + } + m.requestsQueueTimer.With(prometheus.Labels{ + requestTypeLabel: string(requestType), + requestStatusLabel: successLabelFormatted, + }).Observe(length.Seconds()) +} + +func (m *Metrics) RecordTimeoutNotice(success bool) { + if success { + m.timeout_notifications.With(prometheus.Labels{ + successLabel: requestSuccessful, + }).Inc() + } else { + m.timeout_notifications.With(prometheus.Labels{ + successLabel: requestFailed, + }).Inc() + } +} diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index 42395cf6c51..21f182e2094 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -571,7 +571,7 @@ func TestAdapterRequestMetrics(t *testing.T) { var totalCount float64 var totalCookieNoCount float64 var totalCookieYesCount float64 - var totalCookieUnknowmCount float64 + var totalCookieUnknownCount float64 var totalHasBidsCount float64 processMetrics(m.adapterRequests, func(m dto.Metric) { isMetricForAdapter := false @@ -597,7 +597,7 @@ func TestAdapterRequestMetrics(t *testing.T) { case string(pbsmetrics.CookieFlagYes): totalCookieYesCount += value case string(pbsmetrics.CookieFlagUnknown): - totalCookieUnknowmCount += value + totalCookieUnknownCount += value } } } @@ -606,7 +606,7 @@ func TestAdapterRequestMetrics(t *testing.T) { assert.Equal(t, test.expectedCount, totalCount, test.description+":total") assert.Equal(t, test.expectedCookieNoCount, totalCookieNoCount, test.description+":cookie=no") assert.Equal(t, test.expectedCookieYesCount, totalCookieYesCount, test.description+":cookie=yes") - assert.Equal(t, test.expectedCookieUnknownCount, totalCookieUnknowmCount, test.description+":cookie=unknown") + assert.Equal(t, test.expectedCookieUnknownCount, totalCookieUnknownCount, test.description+":cookie=unknown") assert.Equal(t, test.expectedHasBidsCount, totalHasBidsCount, test.description+":hasBids") } } @@ -881,6 +881,69 @@ func TestMetricAccumulationSpotCheck(t *testing.T) { expectedValue) } +func TestRecordRequestQueueTimeMetric(t *testing.T) { + performTest := func(m *Metrics, requestStatus bool, requestType pbsmetrics.RequestType, timeInSec float64) { + m.RecordRequestQueueTime(requestStatus, requestType, time.Duration(timeInSec*float64(time.Second))) + } + + testCases := []struct { + description string + status string + testCase func(m *Metrics) + expectedCount uint64 + expectedSum float64 + }{ + { + description: "Success", + status: requestSuccessLabel, + testCase: func(m *Metrics) { + performTest(m, true, pbsmetrics.ReqTypeVideo, 2) + }, + expectedCount: 1, + expectedSum: 2, + }, + { + description: "TimeoutError", + status: requestRejectLabel, + testCase: func(m *Metrics) { + performTest(m, false, pbsmetrics.ReqTypeVideo, 50) + }, + expectedCount: 1, + expectedSum: 50, + }, + } + + m := createMetricsForTesting() + for _, test := range testCases { + + test.testCase(m) + + result := getHistogramFromHistogramVecByTwoKeys(m.requestsQueueTimer, requestTypeLabel, "video", requestStatusLabel, test.status) + assertHistogram(t, test.description, result, test.expectedCount, test.expectedSum) + } +} + +func TestTimeoutNotifications(t *testing.T) { + m := createMetricsForTesting() + + m.RecordTimeoutNotice(true) + m.RecordTimeoutNotice(true) + m.RecordTimeoutNotice(false) + + assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeout_notifications, + float64(2), + prometheus.Labels{ + successLabel: requestSuccessful, + }) + + assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeout_notifications, + float64(1), + prometheus.Labels{ + successLabel: requestFailed, + }) + +} + func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) { m := dto.Metric{} counter.Write(&m) @@ -906,6 +969,24 @@ func getHistogramFromHistogramVec(histogram *prometheus.HistogramVec, labelKey, return result } +func getHistogramFromHistogramVecByTwoKeys(histogram *prometheus.HistogramVec, label1Key, label1Value, label2Key, label2Value string) dto.Histogram { + var result dto.Histogram + processMetrics(histogram, func(m dto.Metric) { + for ind, label := range m.GetLabel() { + if label.GetName() == label1Key && label.GetValue() == label1Value { + valInd := ind + if ind == 1 { + valInd = 0 + } + if m.Label[valInd].GetName() == label2Key && m.Label[valInd].GetValue() == label2Value { + result = *m.GetHistogram() + } + } + } + }) + return result +} + func processMetrics(collector prometheus.Collector, handler func(m dto.Metric)) { collectorChan := make(chan prometheus.Metric) go func() { diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go index 0ede3ff3bce..314cc3e3d42 100644 --- a/prebid_cache_client/client.go +++ b/prebid_cache_client/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -46,14 +47,9 @@ type Cacheable struct { Key string } -func NewClient(conf *config.Cache, extCache *config.ExternalCache, metrics pbsmetrics.MetricsEngine) Client { +func NewClient(httpClient *http.Client, conf *config.Cache, extCache *config.ExternalCache, metrics pbsmetrics.MetricsEngine) Client { return &clientImpl{ - httpClient: &http.Client{ - Transport: &http.Transport{ - MaxIdleConns: 10, - IdleConnTimeout: 65, - }, - }, + httpClient: httpClient, putUrl: conf.GetBaseURL() + "/cache", externalCacheHost: extCache.Host, externalCachePath: extCache.Path, @@ -92,15 +88,13 @@ func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []s postBody, err := encodeValues(values) if err != nil { - glog.Errorf("Error creating JSON for prebid cache: %v", err) - errs = append(errs, fmt.Errorf("Error creating JSON for prebid cache: %v", err)) + logError(&errs, "Error creating JSON for prebid cache: %v", err) return uuidsToReturn, errs } httpReq, err := http.NewRequest("POST", c.putUrl, bytes.NewReader(postBody)) if err != nil { - glog.Errorf("Error creating POST request to prebid cache: %v", err) - errs = append(errs, fmt.Errorf("Error creating POST request to prebid cache: %v", err)) + logError(&errs, "Error creating POST request to prebid cache: %v", err) return uuidsToReturn, errs } @@ -112,9 +106,7 @@ func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []s elapsedTime := time.Since(startTime) if err != nil { c.metrics.RecordPrebidCacheRequestTime(false, elapsedTime) - friendlyErr := fmt.Errorf("Error sending the request to Prebid Cache: %v; Duration=%v", err, elapsedTime) - glog.Error(friendlyErr) - errs = append(errs, friendlyErr) + logError(&errs, "Error sending the request to Prebid Cache: %v; Duration=%v, Items=%v, Payload Size=%v", err, elapsedTime, len(values), len(postBody)) return uuidsToReturn, errs } defer anResp.Body.Close() @@ -122,23 +114,19 @@ func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []s responseBody, err := ioutil.ReadAll(anResp.Body) if anResp.StatusCode != 200 { - glog.Errorf("Prebid Cache call to %s returned %d: %s", putURL, anResp.StatusCode, responseBody) - errs = append(errs, fmt.Errorf("Prebid Cache call to %s returned %d: %s", putURL, anResp.StatusCode, responseBody)) + logError(&errs, "Prebid Cache call to %s returned %d: %s", putURL, anResp.StatusCode, responseBody) return uuidsToReturn, errs } currentIndex := 0 processResponse := func(uuidObj []byte, _ jsonparser.ValueType, _ int, err error) { if uuid, valueType, _, err := jsonparser.Get(uuidObj, "uuid"); err != nil { - glog.Errorf("Prebid Cache returned a bad value at index %d. Error was: %v. Response body was: %s", currentIndex, err, string(responseBody)) - errs = append(errs, fmt.Errorf("Prebid Cache returned a bad value at index %d. Error was: %v. Response body was: %s", currentIndex, err, string(responseBody))) + logError(&errs, "Prebid Cache returned a bad value at index %d. Error was: %v. Response body was: %s", currentIndex, err, string(responseBody)) } else if valueType != jsonparser.String { - glog.Errorf("Prebid Cache returned a %v at index %d in: %v", valueType, currentIndex, string(responseBody)) - errs = append(errs, fmt.Errorf("Prebid Cache returned a %v at index %d in: %v", valueType, currentIndex, string(responseBody))) + logError(&errs, "Prebid Cache returned a %v at index %d in: %v", valueType, currentIndex, string(responseBody)) } else { if uuidsToReturn[currentIndex], err = jsonparser.ParseString(uuid); err != nil { - glog.Errorf("Prebid Cache response index %d could not be parsed as string: %v", currentIndex, err) - errs = append(errs, fmt.Errorf("Prebid Cache response index %d could not be parsed as string: %v", currentIndex, err)) + logError(&errs, "Prebid Cache response index %d could not be parsed as string: %v", currentIndex, err) uuidsToReturn[currentIndex] = "" } } @@ -146,17 +134,20 @@ func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []s } if _, err := jsonparser.ArrayEach(responseBody, processResponse, "responses"); err != nil { - glog.Errorf("Error interpreting Prebid Cache response: %v\nResponse was: %s", err, string(responseBody)) - errs = append(errs, fmt.Errorf("Error interpreting Prebid Cache response: %v\nResponse was: %s", err, string(responseBody))) + logError(&errs, "Error interpreting Prebid Cache response: %v\nResponse was: %s", err, string(responseBody)) return uuidsToReturn, errs } return uuidsToReturn, errs } +func logError(errs *[]error, format string, a ...interface{}) { + msg := fmt.Sprintf(format, a...) + glog.Error(msg) + *errs = append(*errs, errors.New(msg)) +} + func encodeValues(values []Cacheable) ([]byte, error) { - // This function assumes that m is non-nil and has at least one element. - // clientImp.PutBids should respect this. var buf bytes.Buffer buf.WriteString(`{"puts":[`) for i := 0; i < len(values); i++ { diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go index 47a0a78d7c0..393aacc2dfe 100644 --- a/prebid_cache_client/client_test.go +++ b/prebid_cache_client/client_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "strconv" @@ -17,7 +18,6 @@ import ( "github.com/stretchr/testify/mock" ) -// Prevents #197 func TestEmptyPut(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("The server should not be called.") @@ -72,32 +72,70 @@ func TestBadResponse(t *testing.T) { } func TestCancelledContext(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testCases := []struct { + description string + cacheable []Cacheable + expectedItems int + expectedPayloadSize int + }{ + { + description: "1 Item", + cacheable: []Cacheable{ + { + Type: TypeJSON, + Data: json.RawMessage("true"), + }, + }, + expectedItems: 1, + expectedPayloadSize: 39, + }, + { + description: "2 Items", + cacheable: []Cacheable{ + { + Type: TypeJSON, + Data: json.RawMessage("true"), + }, + { + Type: TypeJSON, + Data: json.RawMessage("false"), + }, + }, + expectedItems: 2, + expectedPayloadSize: 69, + }, + } + + // Initialize Stub Server + stubHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) - server := httptest.NewServer(handler) - defer server.Close() + stubServer := httptest.NewServer(stubHandler) + defer stubServer.Close() - metricsMock := &pbsmetrics.MetricsEngineMock{} - metricsMock.On("RecordPrebidCacheRequestTime", false, mock.Anything).Once() + // Run Tests + for _, testCase := range testCases { + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.On("RecordPrebidCacheRequestTime", false, mock.Anything).Once() - client := &clientImpl{ - httpClient: server.Client(), - putUrl: server.URL, - metrics: metricsMock, - } + client := &clientImpl{ + httpClient: stubServer.Client(), + putUrl: stubServer.URL, + metrics: metricsMock, + } - ctx, cancel := context.WithCancel(context.Background()) - cancel() - ids, _ := client.PutJson(ctx, []Cacheable{{ - Type: TypeJSON, - Data: json.RawMessage("true"), - }, - }) - assertIntEqual(t, len(ids), 1) - assertStringEqual(t, ids[0], "") + ctx, cancel := context.WithCancel(context.Background()) + cancel() + ids, errs := client.PutJson(ctx, testCase.cacheable) - metricsMock.AssertExpectations(t) + expectedErrorMessage := fmt.Sprintf("Items=%v, Payload Size=%v", testCase.expectedItems, testCase.expectedPayloadSize) + + assert.Equal(t, testCase.expectedItems, len(ids), testCase.description+":ids") + assert.Len(t, errs, 1) + assert.Contains(t, errs[0].Error(), "Error sending the request to Prebid Cache: context canceled", testCase.description+":error") + assert.Contains(t, errs[0].Error(), expectedErrorMessage, testCase.description+":error_dimensions") + metricsMock.AssertExpectations(t) + } } func TestSuccessfulPut(t *testing.T) { @@ -195,11 +233,9 @@ func TestStripCacheHostAndPath(t *testing.T) { }, } for _, test := range testInput { - //start client - cacheClient := NewClient(&inCacheURL, &test.inExtCacheURL, &metricsConf.DummyMetricsEngine{}) + cacheClient := NewClient(&http.Client{}, &inCacheURL, &test.inExtCacheURL, &metricsConf.DummyMetricsEngine{}) cHost, cPath := cacheClient.GetExtCacheData() - //assert assert.Equal(t, test.expectedHost, cHost) assert.Equal(t, test.expectedPath, cPath) } diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index 353bfbaa636..d4299af8cf2 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -3,10 +3,10 @@ package ccpa import ( "encoding/json" "errors" + "fmt" "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" - "github.com/buger/jsonparser" ) // Policy represents the CCPA regulation for an OpenRTB bid request. @@ -14,7 +14,7 @@ type Policy struct { Value string } -// ReadPolicy extracts the CCPA regulation policy from an OpenRTB regs ext. +// ReadPolicy extracts the CCPA regulation policy from an OpenRTB request. func ReadPolicy(req *openrtb.BidRequest) (Policy, error) { policy := Policy{} @@ -32,6 +32,10 @@ func ReadPolicy(req *openrtb.BidRequest) (Policy, error) { // Write mutates an OpenRTB bid request with the context of the CCPA policy. func (p Policy) Write(req *openrtb.BidRequest) error { if p.Value == "" { + return clearPolicy(req) + } + + if req == nil { return nil } @@ -40,44 +44,94 @@ func (p Policy) Write(req *openrtb.BidRequest) error { } if req.Regs.Ext == nil { - req.Regs.Ext = json.RawMessage(`{"us_privacy":"` + p.Value + `"}`) + ext, err := json.Marshal(openrtb_ext.ExtRegs{USPrivacy: p.Value}) + if err == nil { + req.Regs.Ext = ext + } + return err + } + + var extMap map[string]interface{} + err := json.Unmarshal(req.Regs.Ext, &extMap) + if err == nil { + extMap["us_privacy"] = p.Value + ext, err := json.Marshal(extMap) + if err == nil { + req.Regs.Ext = ext + } + } + return err +} + +func clearPolicy(req *openrtb.BidRequest) error { + if req == nil { + return nil + } + + if req.Regs == nil { + return nil + } + + if len(req.Regs.Ext) == 0 { return nil } - var err error - req.Regs.Ext, err = jsonparser.Set(req.Regs.Ext, []byte(`"`+p.Value+`"`), "us_privacy") + var extMap map[string]interface{} + err := json.Unmarshal(req.Regs.Ext, &extMap) + if err == nil { + delete(extMap, "us_privacy") + if len(extMap) == 0 { + req.Regs.Ext = nil + } else { + ext, err := json.Marshal(extMap) + if err == nil { + req.Regs.Ext = ext + } + return err + } + } + return err } -// Validate returns an error if the CCPA regulation value does not adhere to the IAB spec. +// Validate returns an error if the CCPA policy does not adhere to the IAB spec. func (p Policy) Validate() error { - if p.Value == "" { + if err := ValidateConsent(p.Value); err != nil { + return fmt.Errorf("request.regs.ext.us_privacy %s", err.Error()) + } + + return nil +} + +// ValidateConsent returns an error if the CCPA consent string does not adhere to the IAB spec. +func ValidateConsent(consent string) error { + if consent == "" { return nil } - if len(p.Value) != 4 { - return errors.New("request.regs.ext.us_privacy must contain 4 characters") + if len(consent) != 4 { + return errors.New("must contain 4 characters") } - if p.Value[0] != '1' { - return errors.New("request.regs.ext.us_privacy must specify version 1") + if consent[0] != '1' { + return errors.New("must specify version 1") } var c byte - c = p.Value[1] + c = consent[1] if c != 'N' && c != 'Y' && c != '-' { - return errors.New("request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice") + return errors.New("must specify 'N', 'Y', or '-' for the explicit notice") } - c = p.Value[2] + c = consent[2] if c != 'N' && c != 'Y' && c != '-' { - return errors.New("request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale") + return errors.New("must specify 'N', 'Y', or '-' for the opt-out sale") } - c = p.Value[3] + c = consent[3] if c != 'N' && c != 'Y' && c != '-' { - return errors.New("request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement") + return errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement") } return nil diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go index 1b33b4ca55f..647f85481b3 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -71,6 +71,17 @@ func TestRead(t *testing.T) { }, expectedError: true, }, + { + description: "Injection Attack", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + }, + }, + expectedPolicy: Policy{ + Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", + }, + }, } for _, test := range testCases { @@ -101,6 +112,48 @@ func TestWrite(t *testing.T) { request: &openrtb.BidRequest{}, expected: &openrtb.BidRequest{}, }, + { + description: "Disabled - Nil Request", + policy: Policy{Value: ""}, + request: nil, + expected: nil, + }, + { + description: "Disabled - Empty Regs.Ext", + policy: Policy{Value: ""}, + request: &openrtb.BidRequest{Regs: &openrtb.Regs{}}, + expected: &openrtb.BidRequest{Regs: &openrtb.Regs{}}, + }, + { + description: "Disabled - Remove From Request", + policy: Policy{Value: ""}, + request: &openrtb.BidRequest{Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"toBeRemoved"}`)}}, + expected: &openrtb.BidRequest{Regs: &openrtb.Regs{}}, + }, + { + description: "Disabled - Remove From Request, Leave Other req Values", + policy: Policy{Value: ""}, + request: &openrtb.BidRequest{Regs: &openrtb.Regs{ + COPPA: 42, + Ext: json.RawMessage(`{"us_privacy":"toBeRemoved"}`)}}, + expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ + COPPA: 42}}, + }, + { + description: "Disabled - Remove From Request, Leave Other req.ext Values", + policy: Policy{Value: ""}, + request: &openrtb.BidRequest{Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeRemoved"}`)}}, + expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"existing":"any"}`)}}, + }, + { + description: "Enabled - Nil Request", + policy: Policy{Value: "anyValue"}, + request: nil, + expected: nil, + }, { description: "Enabled With Nil Request Regs Object", policy: Policy{Value: "anyValue"}, @@ -138,6 +191,32 @@ func TestWrite(t *testing.T) { Ext: json.RawMessage(`malformed`)}}, expectedError: true, }, + { + description: "Injection Attack With Nil Request Regs Object", + policy: Policy{Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}, + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + }}, + }, + { + description: "Injection Attack With Nil Request Regs Ext Object", + policy: Policy{Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}, + request: &openrtb.BidRequest{Regs: &openrtb.Regs{}}, + expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + }}, + }, + { + description: "Injection Attack With Existing Request Regs Ext Object", + policy: Policy{Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}, + request: &openrtb.BidRequest{Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"existing":"any"}`), + }}, + expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"existing":"any","us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + }}, + }, } for _, test := range testCases { @@ -154,74 +233,148 @@ func TestWrite(t *testing.T) { func TestValidate(t *testing.T) { testCases := []struct { - description string - policy Policy - expected string + description string + policy Policy + expectedError string }{ { - description: "Valid", - policy: Policy{Value: "1NYN"}, - expected: "", + description: "Valid", + policy: Policy{Value: "1NYN"}, + expectedError: "", }, { - description: "Valid - Not Applicable", - policy: Policy{Value: "1---"}, - expected: "", + description: "Valid - Not Applicable", + policy: Policy{Value: "1---"}, + expectedError: "", }, { - description: "Valid - Empty", - policy: Policy{Value: ""}, - expected: "", + description: "Valid - Empty", + policy: Policy{Value: ""}, + expectedError: "", }, { - description: "Invalid Length", - policy: Policy{Value: "1NY"}, - expected: "request.regs.ext.us_privacy must contain 4 characters", + description: "Invalid Length", + policy: Policy{Value: "1NY"}, + expectedError: "request.regs.ext.us_privacy must contain 4 characters", }, { - description: "Invalid Version", - policy: Policy{Value: "2---"}, - expected: "request.regs.ext.us_privacy must specify version 1", + description: "Invalid Version", + policy: Policy{Value: "2---"}, + expectedError: "request.regs.ext.us_privacy must specify version 1", }, { - description: "Invalid Explicit Notice Char", - policy: Policy{Value: "1X--"}, - expected: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice", + description: "Invalid Explicit Notice Char", + policy: Policy{Value: "1X--"}, + expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice", }, { - description: "Invalid Explicit Notice Case", - policy: Policy{Value: "1y--"}, - expected: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice", + description: "Invalid Explicit Notice Case", + policy: Policy{Value: "1y--"}, + expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice", }, { - description: "Invalid Opt-Out Sale Char", - policy: Policy{Value: "1-X-"}, - expected: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale", + description: "Invalid Opt-Out Sale Char", + policy: Policy{Value: "1-X-"}, + expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale", }, { - description: "Invalid Opt-Out Sale Case", - policy: Policy{Value: "1-y-"}, - expected: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale", + description: "Invalid Opt-Out Sale Case", + policy: Policy{Value: "1-y-"}, + expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale", }, { - description: "Invalid LSPA Char", - policy: Policy{Value: "1--X"}, - expected: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement", + description: "Invalid LSPA Char", + policy: Policy{Value: "1--X"}, + expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement", }, { - description: "Invalid LSPA Case", - policy: Policy{Value: "1--y"}, - expected: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement", + description: "Invalid LSPA Case", + policy: Policy{Value: "1--y"}, + expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement", }, } for _, test := range testCases { result := test.policy.Validate() - if test.expected == "" { + if test.expectedError == "" { + assert.NoError(t, result, test.description) + } else { + assert.EqualError(t, result, test.expectedError, test.description) + } + } +} + +func TestValidateConsent(t *testing.T) { + testCases := []struct { + description string + consent string + expectedError string + }{ + { + description: "Valid", + consent: "1NYN", + expectedError: "", + }, + { + description: "Valid - Not Applicable", + consent: "1---", + expectedError: "", + }, + { + description: "Invalid Empty", + consent: "", + expectedError: "", + }, + { + description: "Invalid Length", + consent: "1NY", + expectedError: "must contain 4 characters", + }, + { + description: "Invalid Version", + consent: "2---", + expectedError: "must specify version 1", + }, + { + description: "Invalid Explicit Notice Char", + consent: "1X--", + expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + }, + { + description: "Invalid Explicit Notice Case", + consent: "1y--", + expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + }, + { + description: "Invalid Opt-Out Sale Char", + consent: "1-X-", + expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + }, + { + description: "Invalid Opt-Out Sale Case", + consent: "1-y-", + expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + }, + { + description: "Invalid LSPA Char", + consent: "1--X", + expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + }, + { + description: "Invalid LSPA Case", + consent: "1--y", + expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + }, + } + + for _, test := range testCases { + result := ValidateConsent(test.consent) + + if test.expectedError == "" { assert.NoError(t, result, test.description) } else { - assert.EqualError(t, result, test.expected, test.description) + assert.EqualError(t, result, test.expectedError, test.description) } } } diff --git a/privacy/enforcement.go b/privacy/enforcement.go index 6fc36a158d5..fe81848181e 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -6,38 +6,36 @@ import ( // Enforcement represents the privacy policies to enforce for an OpenRTB bid request. type Enforcement struct { - CCPA bool - COPPA bool - GDPR bool + CCPA bool + COPPA bool + GDPR bool + GDPRGeo bool + LMT bool } // Any returns true if at least one privacy policy requires enforcement. func (e Enforcement) Any() bool { - return e.CCPA || e.COPPA || e.GDPR + return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo || e.LMT } // Apply cleans personally identifiable information from an OpenRTB bid request. -func (e Enforcement) Apply(bidRequest *openrtb.BidRequest, isAMP bool) { - e.apply(bidRequest, isAMP, NewScrubber()) +func (e Enforcement) Apply(bidRequest *openrtb.BidRequest, ampGDPRException bool) { + e.apply(bidRequest, ampGDPRException, NewScrubber()) } -func (e Enforcement) apply(bidRequest *openrtb.BidRequest, isAMP bool, scrubber Scrubber) { +func (e Enforcement) apply(bidRequest *openrtb.BidRequest, ampGDPRException bool, scrubber Scrubber) { if bidRequest != nil && e.Any() { - bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceMacAndIFA(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) - bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(isAMP), e.getGeoScrubStrategy()) + bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) + bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(ampGDPRException), e.getGeoScrubStrategy()) } } -func (e Enforcement) getDeviceMacAndIFA() bool { - return e.COPPA -} - func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 { if e.COPPA { return ScrubStrategyIPV6Lowest32 } - if e.GDPR || e.CCPA { + if e.GDPR || e.CCPA || e.LMT { return ScrubStrategyIPV6Lowest16 } @@ -49,27 +47,25 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { return ScrubStrategyGeoFull } - if e.GDPR || e.CCPA { + if e.GDPRGeo || e.CCPA || e.LMT { return ScrubStrategyGeoReducedPrecision } return ScrubStrategyGeoNone } -func (e Enforcement) getUserScrubStrategy(isAMP bool) ScrubStrategyUser { +func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUser { if e.COPPA { - return ScrubStrategyUserFull + return ScrubStrategyUserIDAndDemographic } - // There's no way for AMP to send a GDPR consent string yet so it's hard - // to know if the vendor is consented or not and therefore for AMP requests - // we keep the BuyerUID as is for GDPR. - if e.GDPR && isAMP { + if e.GDPR && ampGDPRException { return ScrubStrategyUserNone } - if e.GDPR || e.CCPA { - return ScrubStrategyUserBuyerIDOnly + // If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above) + if e.CCPA || e.GDPR || e.LMT { + return ScrubStrategyUserID } return ScrubStrategyUserNone diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index ffc2aa0856b..90af24b27ea 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -15,31 +15,37 @@ func TestAny(t *testing.T) { description string }{ { + description: "All False", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: false, + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: false, + LMT: false, }, - expected: false, - description: "All False", + expected: false, }, { + description: "All True", enforcement: Enforcement{ - CCPA: true, - COPPA: true, - GDPR: true, + CCPA: true, + COPPA: true, + GDPR: true, + GDPRGeo: true, + LMT: true, }, - expected: true, - description: "All True", + expected: true, }, { + description: "Mixed", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: false, + CCPA: false, + COPPA: true, + GDPR: false, + GDPRGeo: false, + LMT: true, }, - expected: true, - description: "Mixed", + expected: true, }, } @@ -51,160 +57,234 @@ func TestAny(t *testing.T) { func TestApply(t *testing.T) { testCases := []struct { - enforcement Enforcement - isAMP bool - expectedDeviceMacAndIFA bool - expectedDeviceIPv6 ScrubStrategyIPV6 - expectedDeviceGeo ScrubStrategyGeo - expectedUser ScrubStrategyUser - expectedUserGeo ScrubStrategyGeo - description string + description string + enforcement Enforcement + ampGDPRException bool + expectedDeviceIPv6 ScrubStrategyIPV6 + expectedDeviceGeo ScrubStrategyGeo + expectedUser ScrubStrategyUser + expectedUserGeo ScrubStrategyGeo }{ { + description: "All Enforced", + enforcement: Enforcement{ + CCPA: true, + COPPA: true, + GDPR: true, + GDPRGeo: true, + LMT: true, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceGeo: ScrubStrategyGeoFull, + expectedUser: ScrubStrategyUserIDAndDemographic, + expectedUserGeo: ScrubStrategyGeoFull, + }, + { + description: "CCPA Only", + enforcement: Enforcement{ + CCPA: true, + COPPA: false, + GDPR: false, + GDPRGeo: false, + LMT: false, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "COPPA Only", enforcement: Enforcement{ - CCPA: true, - COPPA: true, - GDPR: true, + CCPA: false, + COPPA: true, + GDPR: false, + GDPRGeo: false, + LMT: false, }, - isAMP: true, - expectedDeviceMacAndIFA: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, - expectedDeviceGeo: ScrubStrategyGeoFull, - expectedUser: ScrubStrategyUserFull, - expectedUserGeo: ScrubStrategyGeoFull, - description: "All Enforced - Most Strict", + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceGeo: ScrubStrategyGeoFull, + expectedUser: ScrubStrategyUserIDAndDemographic, + expectedUserGeo: ScrubStrategyGeoFull, }, { + description: "GDPR Only", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: false, + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: true, + LMT: false, }, - isAMP: false, - expectedDeviceMacAndIFA: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, - expectedDeviceGeo: ScrubStrategyGeoFull, - expectedUser: ScrubStrategyUserFull, - expectedUserGeo: ScrubStrategyGeoFull, - description: "COPPA", + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { + description: "GDPR Only, ampGDPRException", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: true, + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: true, + LMT: false, }, - isAMP: false, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserBuyerIDOnly, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "GDPR", + ampGDPRException: true, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserNone, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { + description: "CCPA Only, ampGDPRException", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: true, + CCPA: true, + COPPA: false, + GDPR: false, + GDPRGeo: false, + LMT: false, }, - isAMP: true, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserNone, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "GDPR For AMP", + ampGDPRException: true, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { + description: "COPPA and GDPR, ampGDPRException", enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, + CCPA: false, + COPPA: true, + GDPR: true, + GDPRGeo: true, + LMT: false, }, - isAMP: false, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserBuyerIDOnly, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "CCPA", + ampGDPRException: true, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceGeo: ScrubStrategyGeoFull, + expectedUser: ScrubStrategyUserIDAndDemographic, + expectedUserGeo: ScrubStrategyGeoFull, }, { + description: "GDPR Only, no Geo", enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: false, + LMT: false, }, - isAMP: true, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserBuyerIDOnly, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "CCPA For AMP", + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoNone, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoNone, }, { + description: "GDPR Only, Geo only", enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: true, + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: true, + LMT: false, }, - isAMP: true, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserNone, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "GDPR And CCPA For AMP", + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6None, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserNone, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "LMT Only", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: false, + LMT: true, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "LMT Only, ampGDPRException", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: false, + LMT: true, + }, + ampGDPRException: true, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, } for _, test := range testCases { req := &openrtb.BidRequest{ - Device: &openrtb.Device{DIDSHA1: "before"}, - User: &openrtb.User{ID: "before"}, + Device: &openrtb.Device{}, + User: &openrtb.User{}, } - device := &openrtb.Device{DIDSHA1: "after"} - user := &openrtb.User{ID: "after"} + replacedDevice := &openrtb.Device{} + replacedUser := &openrtb.User{} m := &mockScrubber{} - m.On("ScrubDevice", req.Device, test.expectedDeviceMacAndIFA, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(device).Once() - m.On("ScrubUser", req.User, test.expectedUser, test.expectedUserGeo).Return(user).Once() + m.On("ScrubDevice", req.Device, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(replacedDevice).Once() + m.On("ScrubUser", req.User, test.expectedUser, test.expectedUserGeo).Return(replacedUser).Once() - test.enforcement.apply(req, test.isAMP, m) + test.enforcement.apply(req, test.ampGDPRException, m) m.AssertExpectations(t) - assert.Equal(t, device, req.Device, "Device Set Correctly") - assert.Equal(t, user, req.User, "User Set Correctly") + assert.Same(t, replacedDevice, req.Device, "Device") + assert.Same(t, replacedUser, req.User, "User") } } func TestApplyNoneApplicable(t *testing.T) { - enforcement := Enforcement{} - device := &openrtb.Device{DIDSHA1: "original"} - user := &openrtb.User{ID: "original"} - req := &openrtb.BidRequest{ - Device: device, - User: user, + req := &openrtb.BidRequest{} + + m := &mockScrubber{} + + enforcement := Enforcement{ + CCPA: false, + COPPA: false, + GDPR: false, + LMT: false, } + enforcement.apply(req, false, m) + m.AssertNotCalled(t, "ScrubDevice") + m.AssertNotCalled(t, "ScrubUser") +} + +func TestApplyNil(t *testing.T) { m := &mockScrubber{} - enforcement.apply(req, true, m) + enforcement := Enforcement{} + enforcement.apply(nil, false, m) m.AssertNotCalled(t, "ScrubDevice") m.AssertNotCalled(t, "ScrubUser") - assert.Equal(t, device, req.Device, "Device Set Correctly") - assert.Equal(t, user, req.User, "User Set Correctly") } type mockScrubber struct { mock.Mock } -func (m *mockScrubber) ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { - args := m.Called(device, macAndIFA, ipv6, geo) +func (m *mockScrubber) ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { + args := m.Called(device, ipv6, geo) return args.Get(0).(*openrtb.Device) } diff --git a/privacy/gdpr/policy.go b/privacy/gdpr/policy.go index ae2790d2c22..9c910b5e6f2 100644 --- a/privacy/gdpr/policy.go +++ b/privacy/gdpr/policy.go @@ -2,9 +2,10 @@ package gdpr import ( "encoding/json" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/openrtb" - "github.com/buger/jsonparser" + "github.com/prebid/go-gdpr/vendorconsent" ) // Policy represents the GDPR regulation for an OpenRTB bid request. @@ -24,11 +25,27 @@ func (p Policy) Write(req *openrtb.BidRequest) error { } if req.User.Ext == nil { - req.User.Ext = json.RawMessage(`{"consent":"` + p.Consent + `"}`) - return nil + ext, err := json.Marshal(openrtb_ext.ExtUser{Consent: p.Consent}) + if err == nil { + req.User.Ext = ext + } + return err + } + + var extMap map[string]interface{} + err := json.Unmarshal(req.User.Ext, &extMap) + if err == nil { + extMap["consent"] = p.Consent + ext, err := json.Marshal(extMap) + if err == nil { + req.User.Ext = ext + } } + return err +} - var err error - req.User.Ext, err = jsonparser.Set(req.User.Ext, []byte(`"`+p.Consent+`"`), "consent") +// ValidateConsent returns an error if the GDPR consent string does not adhere to the IAB TCF spec. +func ValidateConsent(consent string) error { + _, err := vendorconsent.ParseString(consent) return err } diff --git a/privacy/gdpr/policy_test.go b/privacy/gdpr/policy_test.go index 5e3b6e15e7b..ff1b8827a2f 100644 --- a/privacy/gdpr/policy_test.go +++ b/privacy/gdpr/policy_test.go @@ -42,7 +42,7 @@ func TestWrite(t *testing.T) { request: &openrtb.BidRequest{User: &openrtb.User{ Ext: json.RawMessage(`{"existing":"any"}`)}}, expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"existing":"any","consent":"anyConsent"}`)}}, + Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, }, { description: "Enabled With Existing Request User Ext Object - Overwrites", @@ -50,7 +50,7 @@ func TestWrite(t *testing.T) { request: &openrtb.BidRequest{User: &openrtb.User{ Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}}, expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"existing":"any","consent":"anyConsent"}`)}}, + Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, }, { description: "Enabled With Existing Malformed Request User Ext Object", @@ -59,6 +59,32 @@ func TestWrite(t *testing.T) { Ext: json.RawMessage(`malformed`)}}, expectedError: true, }, + { + description: "Injection Attack With Nil Request User Object", + policy: Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}, + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + }}, + }, + { + description: "Injection Attack With Nil Request User Ext Object", + policy: Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}, + request: &openrtb.BidRequest{User: &openrtb.User{}}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), + }}, + }, + { + description: "Injection Attack With Existing Request User Ext Object", + policy: Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}, + request: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"existing":"any"}`), + }}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"","existing":"any"}`), + }}, + }, } for _, test := range testCases { @@ -72,3 +98,32 @@ func TestWrite(t *testing.T) { } } } + +func TestValidateConsent(t *testing.T) { + testCases := []struct { + description string + consent string + expectError bool + }{ + { + description: "Invalid", + consent: "", + expectError: true, + }, + { + description: "Valid", + consent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + expectError: false, + }, + } + + for _, test := range testCases { + result := ValidateConsent(test.consent) + + if test.expectError { + assert.Error(t, result, test.description) + } else { + assert.NoError(t, result, test.description) + } + } +} diff --git a/privacy/lmt/policy.go b/privacy/lmt/policy.go new file mode 100644 index 00000000000..bdbc1a2b34b --- /dev/null +++ b/privacy/lmt/policy.go @@ -0,0 +1,33 @@ +package lmt + +import ( + "github.com/PubMatic-OpenWrap/openrtb" +) + +const ( + trackingUnrestricted = 0 + trackingRestricted = 1 +) + +// Policy represents the LMT (Limit Ad Tracking) policy for an OpenRTB bid request. +type Policy struct { + Signal int + SignalProvided bool +} + +// ReadPolicy extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request. +func ReadPolicy(req *openrtb.BidRequest) Policy { + policy := Policy{} + + if req != nil && req.Device != nil && req.Device.Lmt != nil { + policy.Signal = int(*req.Device.Lmt) + policy.SignalProvided = true + } + + return policy +} + +// ShouldEnforce returns true when the LMT (Limit Ad Tracking) policy is in effect. +func (p Policy) ShouldEnforce() bool { + return p.SignalProvided && p.Signal == trackingRestricted +} diff --git a/privacy/lmt/policy_test.go b/privacy/lmt/policy_test.go new file mode 100644 index 00000000000..12ea1870d2f --- /dev/null +++ b/privacy/lmt/policy_test.go @@ -0,0 +1,128 @@ +package lmt + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestRead(t *testing.T) { + var one int8 = 1 + + testCases := []struct { + description string + request *openrtb.BidRequest + expectedPolicy Policy + }{ + { + description: "Nil Request", + request: nil, + expectedPolicy: Policy{ + Signal: 0, + SignalProvided: false, + }, + }, + { + description: "Nil Device", + request: &openrtb.BidRequest{ + Device: nil, + }, + expectedPolicy: Policy{ + Signal: 0, + SignalProvided: false, + }, + }, + { + description: "Nil Device.Lmt", + request: &openrtb.BidRequest{ + Device: &openrtb.Device{ + Lmt: nil, + }, + }, + expectedPolicy: Policy{ + Signal: 0, + SignalProvided: false, + }, + }, + { + description: "Enabled", + request: &openrtb.BidRequest{ + Device: &openrtb.Device{ + Lmt: &one, + }, + }, + expectedPolicy: Policy{ + Signal: 1, + SignalProvided: true, + }, + }, + } + + for _, test := range testCases { + p := ReadPolicy(test.request) + assert.Equal(t, test.expectedPolicy, p, test.description) + } +} + +func TestShouldEnforce(t *testing.T) { + testCases := []struct { + description string + policy Policy + expected bool + }{ + { + description: "Signal Not Provided - Zero", + policy: Policy{ + Signal: 0, + SignalProvided: false, + }, + expected: false, + }, + { + description: "Signal Not Provided - One", + policy: Policy{ + Signal: 1, + SignalProvided: false, + }, + expected: false, + }, + { + description: "Signal Not Provided - Other", + policy: Policy{ + Signal: 42, + SignalProvided: false, + }, + expected: false, + }, + { + description: "Signal Provided - Zero", + policy: Policy{ + Signal: 0, + SignalProvided: true, + }, + expected: false, + }, + { + description: "Signal Provided - One", + policy: Policy{ + Signal: 1, + SignalProvided: true, + }, + expected: true, + }, + { + description: "Signal Provided - Other", + policy: Policy{ + Signal: 42, + SignalProvided: true, + }, + expected: false, + }, + } + + for _, test := range testCases { + result := test.policy.ShouldEnforce() + assert.Equal(t, test.expected, result, test.description) + } +} diff --git a/privacy/policies.go b/privacy/policies.go index a1d14d96273..837d2fa05c3 100644 --- a/privacy/policies.go +++ b/privacy/policies.go @@ -33,3 +33,28 @@ func writePolicies(req *openrtb.BidRequest, writers []policyWriter) error { return nil } + +// ReadPoliciesFromConsent inspects the consent string kind and sets the corresponding values in a new Policies object. +func ReadPoliciesFromConsent(consent string) (Policies, bool) { + if len(consent) == 0 { + return Policies{}, false + } + + if err := gdpr.ValidateConsent(consent); err == nil { + return Policies{ + GDPR: gdpr.Policy{ + Consent: consent, + }, + }, true + } + + if err := ccpa.ValidateConsent(consent); err == nil { + return Policies{ + CCPA: ccpa.Policy{ + Value: consent, + }, + }, true + } + + return Policies{}, false +} diff --git a/privacy/policies_test.go b/privacy/policies_test.go index 03e5f6aaef3..a7650193892 100644 --- a/privacy/policies_test.go +++ b/privacy/policies_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -75,3 +77,43 @@ func (m *mockPolicyWriter) Write(req *openrtb.BidRequest) error { args := m.Called(req) return args.Error(0) } + +func TestReadPoliciesFromConsent(t *testing.T) { + testCases := []struct { + description string + consent string + expectedResultValue Policies + expectedResultOK bool + }{ + { + description: "Empty String", + consent: "", + expectedResultValue: Policies{}, + expectedResultOK: false, + }, + { + description: "CCPA", + consent: "1NYN", + expectedResultValue: Policies{CCPA: ccpa.Policy{Value: "1NYN"}}, + expectedResultOK: true, + }, + { + description: "GDPR TCF 1.0", + consent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + expectedResultValue: Policies{GDPR: gdpr.Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY"}}, + expectedResultOK: true, + }, + { + description: "Invalid", + consent: "any invalid", + expectedResultValue: Policies{}, + expectedResultOK: false, + }, + } + + for _, test := range testCases { + resultValue, resultOK := ReadPoliciesFromConsent(test.consent) + assert.Equal(t, test.expectedResultValue, resultValue, test.description+":value") + assert.Equal(t, test.expectedResultOK, resultOK, test.description+":ok") + } +} diff --git a/privacy/scrubber.go b/privacy/scrubber.go index 0906f8a126b..0bb1029faf5 100644 --- a/privacy/scrubber.go +++ b/privacy/scrubber.go @@ -1,6 +1,7 @@ package privacy import ( + "encoding/json" "strings" "github.com/PubMatic-OpenWrap/openrtb" @@ -38,19 +39,19 @@ const ( type ScrubStrategyUser int const ( - // ScrubStrategyUserNone does not remove user data. + // ScrubStrategyUserNone does not remove non-location data. ScrubStrategyUserNone ScrubStrategyUser = iota - // ScrubStrategyUserFull removes the user's buyer id, exchange id year of birth, and gender. - ScrubStrategyUserFull + // ScrubStrategyUserIDAndDemographic removes the user's buyer id, exchange id year of birth, and gender. + ScrubStrategyUserIDAndDemographic - // ScrubStrategyUserBuyerIDOnly removes the user's buyer id. - ScrubStrategyUserBuyerIDOnly + // ScrubStrategyUserID removes the user's buyer id. + ScrubStrategyUserID ) // Scrubber removes PII from parts of an OpenRTB request. type Scrubber interface { - ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device + ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User } @@ -61,25 +62,21 @@ func NewScrubber() Scrubber { return scrubber{} } -func (scrubber) ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { +func (scrubber) ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { if device == nil { return nil } deviceCopy := *device - deviceCopy.DIDMD5 = "" deviceCopy.DIDSHA1 = "" deviceCopy.DPIDMD5 = "" deviceCopy.DPIDSHA1 = "" + deviceCopy.IFA = "" + deviceCopy.MACMD5 = "" + deviceCopy.MACSHA1 = "" deviceCopy.IP = scrubIPV4(device.IP) - if macAndIFA { - deviceCopy.MACSHA1 = "" - deviceCopy.MACMD5 = "" - deviceCopy.IFA = "" - } - switch ipv6 { case ScrubStrategyIPV6Lowest16: deviceCopy.IPv6 = scrubIPV6Lowest16Bits(device.IPv6) @@ -105,13 +102,16 @@ func (scrubber) ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo Sc userCopy := *user switch strategy { - case ScrubStrategyUserFull: + case ScrubStrategyUserIDAndDemographic: userCopy.BuyerUID = "" userCopy.ID = "" + userCopy.Ext = scrubUserExtIDs(userCopy.Ext) userCopy.Yob = 0 userCopy.Gender = "" - case ScrubStrategyUserBuyerIDOnly: + case ScrubStrategyUserID: userCopy.BuyerUID = "" + userCopy.ID = "" + userCopy.Ext = scrubUserExtIDs(userCopy.Ext) } switch geo { @@ -169,13 +169,7 @@ func scrubGeoFull(geo *openrtb.Geo) *openrtb.Geo { return nil } - geoCopy := *geo - geoCopy.Lat = 0 - geoCopy.Lon = 0 - geoCopy.Metro = "" - geoCopy.City = "" - geoCopy.ZIP = "" - return &geoCopy + return &openrtb.Geo{} } func scrubGeoPrecision(geo *openrtb.Geo) *openrtb.Geo { @@ -188,3 +182,29 @@ func scrubGeoPrecision(geo *openrtb.Geo) *openrtb.Geo { geoCopy.Lon = float64(int(geo.Lon*100.0+0.5)) / 100.0 // Round Longitude return &geoCopy } + +func scrubUserExtIDs(userExt json.RawMessage) json.RawMessage { + if len(userExt) == 0 { + return userExt + } + + var userExtParsed map[string]json.RawMessage + err := json.Unmarshal(userExt, &userExtParsed) + if err != nil { + return userExt + } + + _, hasEids := userExtParsed["eids"] + _, hasDigitrust := userExtParsed["digitrust"] + if hasEids || hasDigitrust { + delete(userExtParsed, "eids") + delete(userExtParsed, "digitrust") + + result, err := json.Marshal(userExtParsed) + if err == nil { + return result + } + } + + return userExt +} diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go index 084de46c278..f33bb5fd996 100644 --- a/privacy/scrubber_test.go +++ b/privacy/scrubber_test.go @@ -1,6 +1,7 @@ package privacy import ( + "encoding/json" "testing" "github.com/PubMatic-OpenWrap/openrtb" @@ -28,13 +29,13 @@ func TestScrubDevice(t *testing.T) { } testCases := []struct { + description string expected *openrtb.Device - isMacAndIFA bool ipv6 ScrubStrategyIPV6 geo ScrubStrategyGeo - description string }{ { + description: "IPv6 Lowest 32 & Geo Full", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -47,12 +48,11 @@ func TestScrubDevice(t *testing.T) { IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", Geo: &openrtb.Geo{}, }, - isMacAndIFA: true, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoFull, - description: "Full Scrubbing", + ipv6: ScrubStrategyIPV6Lowest32, + geo: ScrubStrategyGeoFull, }, { + description: "IPv6 Lowest 16 & Geo Full", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -65,12 +65,11 @@ func TestScrubDevice(t *testing.T) { IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", Geo: &openrtb.Geo{}, }, - isMacAndIFA: true, - ipv6: ScrubStrategyIPV6Lowest16, - geo: ScrubStrategyGeoFull, - description: "IPv6 Lowest 16", + ipv6: ScrubStrategyIPV6Lowest16, + geo: ScrubStrategyGeoFull, }, { + description: "IPv6 None & Geo Full", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -83,12 +82,11 @@ func TestScrubDevice(t *testing.T) { IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", Geo: &openrtb.Geo{}, }, - isMacAndIFA: true, - ipv6: ScrubStrategyIPV6None, - geo: ScrubStrategyGeoFull, - description: "IPv6 None", + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoFull, }, { + description: "IPv6 Lowest 32 & Geo Reduced", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -107,12 +105,57 @@ func TestScrubDevice(t *testing.T) { ZIP: "some zip", }, }, - isMacAndIFA: true, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoReducedPrecision, - description: "Geo Reduced Precision", + ipv6: ScrubStrategyIPV6Lowest32, + geo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "IPv6 Lowest 16 & Geo Reduced", + expected: &openrtb.Device{ + DIDMD5: "", + DIDSHA1: "", + DPIDMD5: "", + DPIDSHA1: "", + MACSHA1: "", + MACMD5: "", + IFA: "", + IP: "1.2.3.0", + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", + Geo: &openrtb.Geo{ + Lat: 123.46, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + ipv6: ScrubStrategyIPV6Lowest16, + geo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "IPv6 None & Geo Reduced", + expected: &openrtb.Device{ + DIDMD5: "", + DIDSHA1: "", + DPIDMD5: "", + DPIDSHA1: "", + MACSHA1: "", + MACMD5: "", + IFA: "", + IP: "1.2.3.0", + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + Geo: &openrtb.Geo{ + Lat: 123.46, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoReducedPrecision, }, { + description: "IPv6 Lowest 32 & Geo None", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -131,43 +174,75 @@ func TestScrubDevice(t *testing.T) { ZIP: "some zip", }, }, - isMacAndIFA: true, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoNone, - description: "Geo None", + ipv6: ScrubStrategyIPV6Lowest32, + geo: ScrubStrategyGeoNone, }, { + description: "IPv6 Lowest 16 & Geo None", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", DPIDMD5: "", DPIDSHA1: "", - MACSHA1: "anyMACSHA1", - MACMD5: "anyMACMD5", - IFA: "anyIFA", + MACSHA1: "", + MACMD5: "", + IFA: "", IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: &openrtb.Geo{}, + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", + Geo: &openrtb.Geo{ + Lat: 123.456, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + ipv6: ScrubStrategyIPV6Lowest16, + geo: ScrubStrategyGeoNone, + }, + { + description: "IPv6 None & Geo None", + expected: &openrtb.Device{ + DIDMD5: "", + DIDSHA1: "", + DPIDMD5: "", + DPIDSHA1: "", + MACSHA1: "", + MACMD5: "", + IFA: "", + IP: "1.2.3.0", + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + Geo: &openrtb.Geo{ + Lat: 123.456, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, }, - isMacAndIFA: false, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoFull, - description: "Without MAC Address And IFA Scrubbing", + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoNone, }, } for _, test := range testCases { - result := NewScrubber().ScrubDevice(device, test.isMacAndIFA, test.ipv6, test.geo) + result := NewScrubber().ScrubDevice(device, test.ipv6, test.geo) assert.Equal(t, test.expected, result, test.description) } } +func TestScrubDeviceNil(t *testing.T) { + result := NewScrubber().ScrubDevice(nil, ScrubStrategyIPV6None, ScrubStrategyGeoNone) + assert.Nil(t, result) +} + func TestScrubUser(t *testing.T) { user := &openrtb.User{ - BuyerUID: "anyBuyerUID", ID: "anyID", + BuyerUID: "anyBuyerUID", Yob: 42, Gender: "anyGender", + Ext: json.RawMessage(`{"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), Geo: &openrtb.Geo{ Lat: 123.456, Lon: 678.89, @@ -178,53 +253,134 @@ func TestScrubUser(t *testing.T) { } testCases := []struct { - expected *openrtb.User - strategy ScrubStrategyUser - geo ScrubStrategyGeo description string + expected *openrtb.User + scrubUser ScrubStrategyUser + scrubGeo ScrubStrategyGeo }{ { + description: "User ID And Demographic & Geo Full", expected: &openrtb.User{ - BuyerUID: "", ID: "", + BuyerUID: "", Yob: 0, Gender: "", + Ext: json.RawMessage(`{}`), Geo: &openrtb.Geo{}, }, - strategy: ScrubStrategyUserFull, - geo: ScrubStrategyGeoFull, - description: "Full Scrubbing", + scrubUser: ScrubStrategyUserIDAndDemographic, + scrubGeo: ScrubStrategyGeoFull, }, { + description: "User ID And Demographic & Geo Reduced", expected: &openrtb.User{ + ID: "", + BuyerUID: "", + Yob: 0, + Gender: "", + Ext: json.RawMessage(`{}`), + Geo: &openrtb.Geo{ + Lat: 123.46, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + scrubUser: ScrubStrategyUserIDAndDemographic, + scrubGeo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "User ID And Demographic & Geo None", + expected: &openrtb.User{ + ID: "", + BuyerUID: "", + Yob: 0, + Gender: "", + Ext: json.RawMessage(`{}`), + Geo: &openrtb.Geo{ + Lat: 123.456, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + scrubUser: ScrubStrategyUserIDAndDemographic, + scrubGeo: ScrubStrategyGeoNone, + }, + { + description: "User ID & Geo Full", + expected: &openrtb.User{ + ID: "", BuyerUID: "", - ID: "anyID", Yob: 42, Gender: "anyGender", + Ext: json.RawMessage(`{}`), Geo: &openrtb.Geo{}, }, - strategy: ScrubStrategyUserBuyerIDOnly, - geo: ScrubStrategyGeoFull, - description: "User Buyer ID Only", + scrubUser: ScrubStrategyUserID, + scrubGeo: ScrubStrategyGeoFull, }, { + description: "User ID & Geo Reduced", + expected: &openrtb.User{ + ID: "", + BuyerUID: "", + Yob: 42, + Gender: "anyGender", + Ext: json.RawMessage(`{}`), + Geo: &openrtb.Geo{ + Lat: 123.46, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + scrubUser: ScrubStrategyUserID, + scrubGeo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "User ID & Geo None", + expected: &openrtb.User{ + ID: "", + BuyerUID: "", + Yob: 42, + Gender: "anyGender", + Ext: json.RawMessage(`{}`), + Geo: &openrtb.Geo{ + Lat: 123.456, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + scrubUser: ScrubStrategyUserID, + scrubGeo: ScrubStrategyGeoNone, + }, + { + description: "User None & Geo Full", expected: &openrtb.User{ - BuyerUID: "anyBuyerUID", ID: "anyID", + BuyerUID: "anyBuyerUID", Yob: 42, Gender: "anyGender", + Ext: json.RawMessage(`{"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), Geo: &openrtb.Geo{}, }, - strategy: ScrubStrategyUserNone, - geo: ScrubStrategyGeoFull, - description: "User None", + scrubUser: ScrubStrategyUserNone, + scrubGeo: ScrubStrategyGeoFull, }, { + description: "User None & Geo Reduced", expected: &openrtb.User{ - BuyerUID: "", - ID: "", - Yob: 0, - Gender: "", + ID: "anyID", + BuyerUID: "anyBuyerUID", + Yob: 42, + Gender: "anyGender", + Ext: json.RawMessage(`{"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), Geo: &openrtb.Geo{ Lat: 123.46, Lon: 678.89, @@ -233,16 +389,17 @@ func TestScrubUser(t *testing.T) { ZIP: "some zip", }, }, - strategy: ScrubStrategyUserFull, - geo: ScrubStrategyGeoReducedPrecision, - description: "Geo Reduced Precision", + scrubUser: ScrubStrategyUserNone, + scrubGeo: ScrubStrategyGeoReducedPrecision, }, { + description: "User None & Geo None", expected: &openrtb.User{ - BuyerUID: "", - ID: "", - Yob: 0, - Gender: "", + ID: "anyID", + BuyerUID: "anyBuyerUID", + Yob: 42, + Gender: "anyGender", + Ext: json.RawMessage(`{"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), Geo: &openrtb.Geo{ Lat: 123.456, Lon: 678.89, @@ -251,18 +408,22 @@ func TestScrubUser(t *testing.T) { ZIP: "some zip", }, }, - strategy: ScrubStrategyUserFull, - geo: ScrubStrategyGeoNone, - description: "Geo None", + scrubUser: ScrubStrategyUserNone, + scrubGeo: ScrubStrategyGeoNone, }, } for _, test := range testCases { - result := NewScrubber().ScrubUser(user, test.strategy, test.geo) + result := NewScrubber().ScrubUser(user, test.scrubUser, test.scrubGeo) assert.Equal(t, test.expected, result, test.description) } } +func TestScrubUserNil(t *testing.T) { + result := NewScrubber().ScrubUser(nil, ScrubStrategyUserNone, ScrubStrategyGeoNone) + assert.Nil(t, result) +} + func TestScrubIPV4(t *testing.T) { testCases := []struct { IP string @@ -432,3 +593,92 @@ func TestScrubGeoPrecisionWhenNil(t *testing.T) { result := scrubGeoPrecision(nil) assert.Nil(t, result) } + +func TestScrubUserExtIDs(t *testing.T) { + testCases := []struct { + description string + userExt json.RawMessage + expected json.RawMessage + }{ + { + description: "Nil", + userExt: nil, + expected: nil, + }, + { + description: "Empty String", + userExt: json.RawMessage(``), + expected: json.RawMessage(``), + }, + { + description: "Empty Object", + userExt: json.RawMessage(`{}`), + expected: json.RawMessage(`{}`), + }, + { + description: "Do Nothing When Malformed", + userExt: json.RawMessage(`malformed`), + expected: json.RawMessage(`malformed`), + }, + { + description: "Do Nothing When No IDs Present", + userExt: json.RawMessage(`{"anyExisting":42}}`), + expected: json.RawMessage(`{"anyExisting":42}}`), + }, + { + description: "Remove eids + digitrust", + userExt: json.RawMessage(`{"eids":[{"source":"anySource","id":"anyId","uids":[{"id":"anyId","ext":{"id":42}}],"ext":{"id":42}}],"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), + expected: json.RawMessage(`{}`), + }, + { + description: "Remove eids + digitrust - With Other Data", + userExt: json.RawMessage(`{"anyExisting":42,"eids":[{"source":"anySource","id":"anyId","uids":[{"id":"anyId","ext":{"id":42}}],"ext":{"id":42}}],"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), + expected: json.RawMessage(`{"anyExisting":42}`), + }, + { + description: "Remove eids + digitrust - With Other Nested Data", + userExt: json.RawMessage(`{"anyExisting":{"existing":42},"eids":[{"source":"anySource","id":"anyId","uids":[{"id":"anyId","ext":{"id":42}}],"ext":{"id":42}}],"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), + expected: json.RawMessage(`{"anyExisting":{"existing":42}}`), + }, + { + description: "Remove eids Only", + userExt: json.RawMessage(`{"eids":[{"source":"anySource","id":"anyId","uids":[{"id":"anyId","ext":{"id":42}}],"ext":{"id":42}}]}`), + expected: json.RawMessage(`{}`), + }, + { + description: "Remove eids Only - Empty Array", + userExt: json.RawMessage(`{"eids":[]}`), + expected: json.RawMessage(`{}`), + }, + { + description: "Remove eids Only - With Other Data", + userExt: json.RawMessage(`{"anyExisting":42,"eids":[{"source":"anySource","id":"anyId","uids":[{"id":"anyId","ext":{"id":42}}],"ext":{"id":42}}]}`), + expected: json.RawMessage(`{"anyExisting":42}`), + }, + { + description: "Remove eids Only - With Other Nested Data", + userExt: json.RawMessage(`{"anyExisting":{"existing":42},"eids":[{"source":"anySource","id":"anyId","uids":[{"id":"anyId","ext":{"id":42}}],"ext":{"id":42}}]}`), + expected: json.RawMessage(`{"anyExisting":{"existing":42}}`), + }, + { + description: "Remove digitrust Only", + userExt: json.RawMessage(`{"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), + expected: json.RawMessage(`{}`), + }, + { + description: "Remove digitrust Only - With Other Data", + userExt: json.RawMessage(`{"anyExisting":42,"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), + expected: json.RawMessage(`{"anyExisting":42}`), + }, + { + description: "Remove digitrust Only - With Other Nested Data", + userExt: json.RawMessage(`{"anyExisting":{"existing":42},"digitrust":{"id":"anyId","keyv":4,"pref":8}}`), + expected: json.RawMessage(`{"anyExisting":{"existing":42}}`), + }, + } + + for _, test := range testCases { + result := scrubUserExtIDs(test.userExt) + assert.Equal(t, test.expected, result, test.description) + } +} diff --git a/router/aspects/request_timeout_handler.go b/router/aspects/request_timeout_handler.go new file mode 100644 index 00000000000..230c68cdc02 --- /dev/null +++ b/router/aspects/request_timeout_handler.go @@ -0,0 +1,49 @@ +package aspects + +import ( + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/julienschmidt/httprouter" + "net/http" + "strconv" + "time" +) + +func QueuedRequestTimeout(f httprouter.Handle, reqTimeoutHeaders config.RequestTimeoutHeaders, metricsEngine pbsmetrics.MetricsEngine, requestType pbsmetrics.RequestType) httprouter.Handle { + + return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + + reqTimeInQueue := r.Header.Get(reqTimeoutHeaders.RequestTimeInQueue) + reqTimeout := r.Header.Get(reqTimeoutHeaders.RequestTimeoutInQueue) + + //If request timeout headers are not specified - process request as usual + if reqTimeInQueue == "" || reqTimeout == "" { + f(w, r, params) + return + } + + reqTimeFloat, reqTimeFloatErr := strconv.ParseFloat(reqTimeInQueue, 64) + reqTimeoutFloat, reqTimeoutFloatErr := strconv.ParseFloat(reqTimeout, 64) + + //Return HTTP 500 if request timeout headers are incorrect (wrong format) + if reqTimeFloatErr != nil || reqTimeoutFloatErr != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Request timeout headers are incorrect (wrong format)")) + return + } + + reqTimeDuration := time.Duration(reqTimeFloat * float64(time.Second)) + + //Return HTTP 408 if requests stays too long in queue + if reqTimeFloat >= reqTimeoutFloat { + w.WriteHeader(http.StatusRequestTimeout) + w.Write([]byte("Queued request processing time exceeded maximum")) + metricsEngine.RecordRequestQueueTime(false, requestType, reqTimeDuration) + return + } + + metricsEngine.RecordRequestQueueTime(true, requestType, reqTimeDuration) + f(w, r, params) + } + +} diff --git a/router/aspects/request_timeout_handler_test.go b/router/aspects/request_timeout_handler_test.go new file mode 100644 index 00000000000..9baffb131ae --- /dev/null +++ b/router/aspects/request_timeout_handler_test.go @@ -0,0 +1,117 @@ +package aspects + +import ( + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/julienschmidt/httprouter" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const reqTimeInQueueHeaderName = "X-Ngx-Request-Time" +const reqTimeoutHeaderName = "X-Request-Timeout" + +func TestAny(t *testing.T) { + testCases := []struct { + reqTimeInQueue string + reqTimeOut string + setHeaders bool + expectedRespCode int + expectedRespCodeMessage string + expectedRespBody string + expectedRespBodyMessage string + requestStatusMetrics bool + }{ + { + //TestQueuedRequestTimeoutWithTimeout + reqTimeInQueue: "6", + reqTimeOut: "5", + setHeaders: true, + expectedRespCode: http.StatusRequestTimeout, + expectedRespCodeMessage: "Http response code is incorrect, should be 408", + expectedRespBody: "Queued request processing time exceeded maximum", + expectedRespBodyMessage: "Body should have error message", + requestStatusMetrics: false, + }, + { + //TestQueuedRequestTimeoutNoTimeout + reqTimeInQueue: "0.9", + reqTimeOut: "5", + setHeaders: true, + expectedRespCode: http.StatusOK, + expectedRespCodeMessage: "Http response code is incorrect, should be 200", + expectedRespBody: "Executed", + expectedRespBodyMessage: "Body should be present in response", + requestStatusMetrics: true, + }, + { + //TestQueuedRequestNoHeaders + reqTimeInQueue: "", + reqTimeOut: "", + setHeaders: false, + expectedRespCode: http.StatusOK, + expectedRespCodeMessage: "Http response code is incorrect, should be 200", + expectedRespBody: "Executed", + expectedRespBodyMessage: "Body should be present in response", + requestStatusMetrics: true, + }, + { + //TestQueuedRequestSomeHeaders + reqTimeInQueue: "2", + reqTimeOut: "", + setHeaders: true, + expectedRespCode: http.StatusOK, + expectedRespCodeMessage: "Http response code is incorrect, should be 200", + expectedRespBody: "Executed", + expectedRespBodyMessage: "Body should be present in response", + requestStatusMetrics: true, + }, + } + + for _, test := range testCases { + reqTimeFloat, _ := strconv.ParseFloat(test.reqTimeInQueue, 64) + result := ExecuteAspectRequest(t, test.reqTimeInQueue, test.reqTimeOut, test.setHeaders, pbsmetrics.ReqTypeVideo, test.requestStatusMetrics, reqTimeFloat) + assert.Equal(t, test.expectedRespCode, result.Code, test.expectedRespCodeMessage) + assert.Equal(t, test.expectedRespBody, string(result.Body.Bytes()), test.expectedRespBodyMessage) + } +} + +func MockEndpoint() httprouter.Handle { + return httprouter.Handle(MockHandler) +} + +func MockHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + w.Write([]byte("Executed")) +} + +func ExecuteAspectRequest(t *testing.T, timeInQueue string, reqTimeout string, setHeaders bool, requestType pbsmetrics.RequestType, status bool, requestDuration float64) *httptest.ResponseRecorder { + rw := httptest.NewRecorder() + req, err := http.NewRequest("POST", "/test", nil) + if err != nil { + assert.Fail(t, "Unable create mock http request") + } + if setHeaders { + req.Header.Set(reqTimeInQueueHeaderName, timeInQueue) + req.Header.Set(reqTimeoutHeaderName, reqTimeout) + } + + customHeaders := config.RequestTimeoutHeaders{reqTimeInQueueHeaderName, reqTimeoutHeaderName} + + metrics := &pbsmetrics.MetricsEngineMock{} + + metrics.On("RecordRequestQueueTime", status, requestType, time.Duration(requestDuration*float64(time.Second))).Once() + + handler := QueuedRequestTimeout(MockEndpoint(), customHeaders, metrics, requestType) + + r := httprouter.New() + r.POST("/test", handler) + + r.ServeHTTP(rw, req) + + return rw +} diff --git a/router/router.go b/router/router.go index f8039dff212..6da9800ba43 100644 --- a/router/router.go +++ b/router/router.go @@ -12,6 +12,8 @@ import ( "strings" "time" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adform" "github.com/PubMatic-OpenWrap/prebid-server/adapters/appnexus" @@ -35,12 +37,10 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/exchange" "github.com/PubMatic-OpenWrap/prebid-server/gdpr" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" - "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" metricsConf "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics/config" pbc "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" "github.com/PubMatic-OpenWrap/prebid-server/ssl" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" - "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/empty_fetcher" storedRequestsConf "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/config" "github.com/PubMatic-OpenWrap/prebid-server/usersync" "github.com/PubMatic-OpenWrap/prebid-server/usersync/usersyncers" @@ -64,8 +64,10 @@ var ( g_analytics analytics.PBSAnalyticsModule g_disabledBidders map[string]string g_categoriesFetcher stored_requests.CategoryFetcher + g_videoFetcher stored_requests.Fetcher g_bidderMap map[string]openrtb_ext.BidderName g_defReqJSON []byte + g_cacheClient pbc.Client ) // NewJsonDirectoryServer is used to serve .json files from a directory as a single blob. For example, @@ -201,7 +203,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r glog.Infof("Could not read certificates file: %s \n", readCertErr.Error()) } - theClient := &http.Client{ + generalHttpClient := &http.Client{ Transport: &http.Transport{ MaxIdleConns: cfg.Client.MaxIdleConns, MaxIdleConnsPerHost: cfg.Client.MaxIdleConnsPerHost, @@ -209,6 +211,15 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r TLSClientConfig: &tls.Config{RootCAs: certPool}, }, } + + cacheHttpClient := &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: cfg.CacheClient.MaxIdleConns, + MaxIdleConnsPerHost: cfg.CacheClient.MaxIdleConnsPerHost, + IdleConnTimeout: time.Duration(cfg.CacheClient.IdleConnTimeout) * time.Second, + }, + } + // Hack because of how legacy handles districtm legacyBidderList := openrtb_ext.BidderList() legacyBidderList = append(legacyBidderList, openrtb_ext.BidderName("districtm")) @@ -216,8 +227,8 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r g_cfg = cfg var db *sql.DB // Metrics engine - r.MetricsEngine = metricsConf.NewMetricsEngine(cfg, legacyBidderList) - db, _, g_storedReqFetcher, _, g_categoriesFetcher, _ = storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, theClient, r.Router) + g_metrics = metricsConf.NewMetricsEngine(cfg, legacyBidderList) + db, _, g_storedReqFetcher, _, g_categoriesFetcher, g_videoFetcher = storedRequestsConf.NewStoredRequests(cfg, g_metrics, generalHttpClient, r.Router) // todo(zachbadgett): better shutdown //r.Shutdown = shutdown @@ -227,62 +238,62 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r g_analytics = analyticsConf.NewPBSAnalytics(&cfg.Analytics) - // Metrics engine - g_metrics = metricsConf.NewMetricsEngine(cfg, legacyBidderList) - g_paramsValidator, err = openrtb_ext.NewBidderParamsValidator(schemaDirectory) if err != nil { glog.Fatalf("Failed to create the bidder params validator. %v", err) } - g_disabledBidders = map[string]string{ - "indexExchange": "Bidder \"indexExchange\" has been deprecated and is no longer available. Please use bidder \"ix\" and note that the bidder params have changed.", - } - //var bidderList []openrtb_ext.BidderName - p, _ := filepath.Abs(infoDirectory) bidderInfos := adapters.ParseBidderInfos(cfg.Adapters, p, openrtb_ext.BidderList()) + g_disabledBidders = map[string]string{ + "indexExchange": "Bidder \"indexExchange\" has been deprecated and is no longer available. Please use bidder \"ix\" and note that the bidder params have changed.", + } g_bidderMap = exchange.DisableBidders(bidderInfos, g_disabledBidders) _, g_defReqJSON = readDefaultRequest(cfg.DefReqConfig) g_syncers = usersyncers.NewSyncerMap(cfg) - g_gdprPerms = gdpr.NewPermissions(context.Background(), cfg.GDPR, adapters.GDPRAwareSyncerIDs(g_syncers), theClient) + g_gdprPerms = gdpr.NewPermissions(context.Background(), cfg.GDPR, adapters.GDPRAwareSyncerIDs(g_syncers), generalHttpClient) exchanges = newExchangeMap(cfg) - - g_ex = exchange.NewExchange(theClient, pbc.NewClient(&cfg.CacheURL, &cfg.ExtCacheURL, r.MetricsEngine), cfg, g_metrics, bidderInfos, g_gdprPerms, rateConvertor) + g_cacheClient = pbc.NewClient(cacheHttpClient, &cfg.CacheURL, &cfg.ExtCacheURL, g_metrics) + g_ex = exchange.NewExchange(generalHttpClient, g_cacheClient, cfg, g_metrics, bidderInfos, g_gdprPerms, rateConvertor) /* - openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, bidderMap, categoriesFetcher) - - if err != nil { - glog.Fatalf("Failed to create the openrtb endpoint handler. %v", err) - } - - ampEndpoint, err := openrtb2.NewAmpEndpoint(theExchange, paramsValidator, ampFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, bidderMap, categoriesFetcher) - - if err != nil { - glog.Fatalf("Failed to create the amp endpoint handler. %v", err) - } - - videoEndpoint, err := openrtb2.NewVideoEndpoint(theExchange, paramsValidator, fetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, bidderMap, categoriesFetcher) - if err != nil { - glog.Fatalf("Failed to create the video endpoint handler. %v", err) - } - - r.POST("/auction", endpoints.Auction(cfg, syncers, gdprPerms, r.MetricsEngine, dataCache, exchanges)) - r.POST("/openrtb2/auction", openrtbEndpoint) - r.POST("/openrtb2/video", videoEndpoint) - r.GET("/openrtb2/amp", ampEndpoint) - r.GET("/info/bidders", infoEndpoints.NewBiddersEndpoint(defaultAliases)) - r.GET("/info/bidders/:bidderName", infoEndpoints.NewBidderDetailsEndpoint(bidderInfos, defaultAliases)) - r.GET("/bidders/params", NewJsonDirectoryServer(schemaDirectory, paramsValidator, defaultAliases)) - r.POST("/cookie_sync", endpoints.NewCookieSyncEndpoint(syncers, cfg, gdprPerms, r.MetricsEngine, pbsAnalytics)) - r.GET("/status", endpoints.NewStatusEndpoint(cfg.StatusResponse)) - r.GET("/", serveIndex) - r.ServeFiles("/static/*filepath", http.Dir("static")) + openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, categoriesFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap) + + if err != nil { + glog.Fatalf("Failed to create the openrtb endpoint handler. %v", err) + } + + ampEndpoint, err := openrtb2.NewAmpEndpoint(theExchange, paramsValidator, ampFetcher, categoriesFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap) + + if err != nil { + glog.Fatalf("Failed to create the amp endpoint handler. %v", err) + } + + videoEndpoint, err := openrtb2.NewVideoEndpoint(theExchange, paramsValidator, fetcher, videoFetcher, categoriesFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap, cacheClient) + if err != nil { + glog.Fatalf("Failed to create the video endpoint handler. %v", err) + } + + requestTimeoutHeaders := config.RequestTimeoutHeaders{} + if cfg.RequestTimeoutHeaders != requestTimeoutHeaders { + videoEndpoint = aspects.QueuedRequestTimeout(videoEndpoint, cfg.RequestTimeoutHeaders, r.MetricsEngine, pbsmetrics.ReqTypeVideo) + } + + r.POST("/auction", endpoints.Auction(cfg, syncers, gdprPerms, r.MetricsEngine, dataCache, exchanges)) + r.POST("/openrtb2/auction", openrtbEndpoint) + r.POST("/openrtb2/video", videoEndpoint) + r.GET("/openrtb2/amp", ampEndpoint) + r.GET("/info/bidders", infoEndpoints.NewBiddersEndpoint(defaultAliases)) + r.GET("/info/bidders/:bidderName", infoEndpoints.NewBidderDetailsEndpoint(bidderInfos, defaultAliases)) + r.GET("/bidders/params", NewJsonDirectoryServer(schemaDirectory, paramsValidator, defaultAliases)) + r.POST("/cookie_sync", endpoints.NewCookieSyncEndpoint(syncers, cfg, gdprPerms, r.MetricsEngine, pbsAnalytics)) + r.GET("/status", endpoints.NewStatusEndpoint(cfg.StatusResponse)) + r.GET("/", serveIndex) + r.ServeFiles("/static/*filepath", http.Dir("static")) userSyncDeps := &pbs.UserSyncDeps{ HostCookieConfig: &(cfg.HostCookie), @@ -292,14 +303,15 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r PBSAnalytics: pbsAnalytics, } - r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg.HostCookie, syncers, gdprPerms, pbsAnalytics, r.MetricsEngine)) - r.GET("/getuids", endpoints.NewGetUIDsEndpoint(cfg.HostCookie)) - r.POST("/optout", userSyncDeps.OptOut) - r.GET("/optout", userSyncDeps.OptOut) + r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg.HostCookie, syncers, gdprPerms, pbsAnalytics, r.MetricsEngine)) + r.GET("/getuids", endpoints.NewGetUIDsEndpoint(cfg.HostCookie)) + r.POST("/optout", userSyncDeps.OptOut) + r.GET("/optout", userSyncDeps.OptOut) */ return r, nil } +//OrtbAuctionEndpointWrapper Openwrap wrapper method for calling /openrtb2/auction endpoint func OrtbAuctionEndpointWrapper(w http.ResponseWriter, r *http.Request) error { ortbAuctionEndpoint, err := openrtb2.NewEndpoint(g_ex, g_paramsValidator, g_storedReqFetcher, g_categoriesFetcher, g_cfg, g_metrics, g_analytics, g_disabledBidders, g_defReqJSON, g_bidderMap) if err != nil { @@ -309,8 +321,9 @@ func OrtbAuctionEndpointWrapper(w http.ResponseWriter, r *http.Request) error { return nil } +//VideoAuctionEndpointWrapper Openwrap wrapper method for calling /openrtb2/video endpoint func VideoAuctionEndpointWrapper(w http.ResponseWriter, r *http.Request) error { - videoAuctionEndpoint, err := openrtb2.NewCTVEndpoint(g_ex, g_paramsValidator, g_storedReqFetcher, empty_fetcher.EmptyFetcher{}, g_categoriesFetcher, g_cfg, g_metrics, g_analytics, g_disabledBidders, g_defReqJSON, g_bidderMap) + videoAuctionEndpoint, err := openrtb2.NewCTVEndpoint(g_ex, g_paramsValidator, g_storedReqFetcher, g_videoFetcher, g_categoriesFetcher, g_cfg, g_metrics, g_analytics, g_disabledBidders, g_defReqJSON, g_bidderMap) if err != nil { return err } @@ -318,26 +331,31 @@ func VideoAuctionEndpointWrapper(w http.ResponseWriter, r *http.Request) error { return nil } +//AuctionWrapper Openwrap wrapper method for calling /auction endpoint func AuctionWrapper(w http.ResponseWriter, r *http.Request) { auction := endpoints.Auction(g_cfg, g_syncers, g_gdprPerms, g_metrics, dataCache, exchanges) auction(w, r, nil) } +//GetUIDSWrapper Openwrap wrapper method for calling /getuids endpoint func GetUIDSWrapper(w http.ResponseWriter, r *http.Request) { getUID := endpoints.NewGetUIDsEndpoint(g_cfg.HostCookie) getUID(w, r, nil) } +//SetUIDSWrapper Openwrap wrapper method for calling /setuid endpoint func SetUIDSWrapper(w http.ResponseWriter, r *http.Request) { setUID := endpoints.NewSetUIDEndpoint(g_cfg.HostCookie, g_syncers, g_gdprPerms, g_analytics, g_metrics) setUID(w, r, nil) } +//CookieSync Openwrap wrapper method for calling /cookie_sync endpoint func CookieSync(w http.ResponseWriter, r *http.Request) { cookiesync := endpoints.NewCookieSyncEndpoint(g_syncers, g_cfg, g_gdprPerms, g_metrics, g_analytics) cookiesync(w, r, nil) } +//SyncerMap Returns map of bidder and its usersync info func SyncerMap() map[openrtb_ext.BidderName]usersync.Usersyncer { return g_syncers } diff --git a/server/listener.go b/server/listener.go index fe9aea19e3f..b324f0a5c94 100644 --- a/server/listener.go +++ b/server/listener.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/golang/glog" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/golang/glog" ) // monitorableListener tracks any opened connections in the metrics. diff --git a/server/server.go b/server/server.go index c0a3111b57c..87cc4a27c32 100644 --- a/server/server.go +++ b/server/server.go @@ -12,10 +12,10 @@ import ( "time" "github.com/NYTimes/gziphandler" - "github.com/golang/glog" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" metricsconfig "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics/config" + "github.com/golang/glog" ) // Listen blocks forever, serving PBS requests on the given port. This will block forever, until the process is shut down. diff --git a/ssl/ssl_test.go b/ssl/ssl_test.go index c4c29d149ef..b72fb7ae9a3 100644 --- a/ssl/ssl_test.go +++ b/ssl/ssl_test.go @@ -38,7 +38,7 @@ func TestCertsFromFilePoolDontExist(t *testing.T) { // Assert loaded certificates by looking at the length of the subjects array of strings assert.NoError(t, err, "Error thrown by AppendPEMFileToRootCAPool while loading file %s: %v", certificatesFile, err) subjects := certPool.Subjects() - assert.Equal(t, len(subjects), 1, "We only loaded one vertificate from the file, len(subjects) should equal 1") + assert.Equal(t, len(subjects), 1, "We only loaded one certificate from the file, len(subjects) should equal 1") } func TestAppendPEMFileToRootCAPoolFail(t *testing.T) { diff --git a/static/adapter/appnexus/opts.json b/static/adapter/appnexus/opts.json index bd6f8af3e8b..41ee3c8f313 100644 --- a/static/adapter/appnexus/opts.json +++ b/static/adapter/appnexus/opts.json @@ -5,13 +5,14 @@ "3": "IAB10-1", "4": "IAB2-3", "5": "IAB19-8", + "6": "IAB22-1", "7": "IAB18-1", - "8": "IAB14-1", + "8": "IAB12-3", "9": "IAB5-1", "10": "IAB4-5", "11": "IAB13-4", - "13": "IAB19-2", "12": "IAB8-7", + "13": "IAB9-7", "14": "IAB7-1", "15": "IAB20-18", "16": "IAB10-7", @@ -20,30 +21,79 @@ "19": "IAB18-4", "20": "IAB1-5", "21": "IAB1-6", + "22": "IAB3-4", "23": "IAB19-13", "24": "IAB22-2", + "25": "IAB3-9", + "26": "IAB17-18", "27": "IAB19-6", "28": "IAB1-7", - "29": "IAB9-5", + "29": "IAB9-30", "30": "IAB20-7", "31": "IAB20-17", "32": "IAB7-32", "33": "IAB16-5", "34": "IAB19-34", + "35": "IAB11-5", + "36": "IAB12-3", "37": "IAB11-4", - "38": "IAB23", + "38": "IAB12-3", "39": "IAB9-30", "41": "IAB7-44", + "42": "IAB7-1", + "43": "IAB7-30", + "50": "IAB19-30", "51": "IAB17-12", + "52": "IAB19-30", "53": "IAB3-1", "55": "IAB13-2", + "56": "IAB19-30", + "57": "IAB19-30", + "58": "IAB7-39", + "59": "IAB22-1", + "60": "IAB7-39", "61": "IAB21-3", - "62": "IAB6-4", - "63": "IAB15-10", + "62": "IAB5-1", + "63": "IAB12-3", + "64": "IAB20-18", "65": "IAB11-2", + "66": "IAB17-18", "67": "IAB9-9", - "69": "IAB7-1", - "71": "IAB22-2", - "74": "IAB8-5" - } + "68": "IAB9-5", + "69": "IAB7-44", + "71": "IAB22-3", + "73": "IAB19-30", + "74": "IAB8-5", + "78": "IAB22-1", + "85": "IAB12-2", + "86": "IAB22-3", + "87": "IAB11-3", + "112": "IAB7-32", + "113": "IAB7-32", + "114": "IAB7-32", + "115": "IAB7-32", + "118": "IAB9-5", + "119": "IAB9-5", + "120": "IAB9-5", + "121": "IAB9-5", + "122": "IAB9-5", + "123": "IAB9-5", + "124": "IAB9-5", + "125": "IAB9-5", + "126": "IAB9-5", + "127": "IAB22-1", + "132": "IAB1-2", + "133": "IAB19-30", + "137": "IAB3-9", + "138": "IAB19-3", + "140": "IAB2-3", + "141": "IAB2-1", + "142": "IAB2-3", + "143": "IAB17-13", + "166": "IAB11-4", + "175": "IAB3-1", + "176": "IAB13-4", + "182": "IAB8-9", + "183": "IAB3-5" + } } diff --git a/static/bidder-info/33across.yaml b/static/bidder-info/33across.yaml index f0a4447099f..84ba6d68611 100644 --- a/static/bidder-info/33across.yaml +++ b/static/bidder-info/33across.yaml @@ -1,9 +1,9 @@ maintainer: - email: "dev@33across.com" + email: "headerbidding@33across.com" capabilities: app: mediaTypes: - banner site: mediaTypes: - - banner \ No newline at end of file + - banner diff --git a/static/bidder-info/adgeneration.yaml b/static/bidder-info/adgeneration.yaml new file mode 100644 index 00000000000..55f653143dd --- /dev/null +++ b/static/bidder-info/adgeneration.yaml @@ -0,0 +1,10 @@ +maintainer: + email: "ssp-ope@supership.jp" +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner + diff --git a/static/bidder-info/adhese.yaml b/static/bidder-info/adhese.yaml new file mode 100644 index 00000000000..742d78344ce --- /dev/null +++ b/static/bidder-info/adhese.yaml @@ -0,0 +1,11 @@ +maintainer: + email: info@adhese.com +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-info/admixer.yaml b/static/bidder-info/admixer.yaml new file mode 100644 index 00000000000..64ad2024058 --- /dev/null +++ b/static/bidder-info/admixer.yaml @@ -0,0 +1,15 @@ +maintainer: + email: "prebid@admixer.net" +capabilities: + app: + mediaTypes: + - banner + - video + - native + - audio + site: + mediaTypes: + - banner + - video + - native + - audio \ No newline at end of file diff --git a/static/bidder-info/adocean.yaml b/static/bidder-info/adocean.yaml new file mode 100644 index 00000000000..2f31fe92eaf --- /dev/null +++ b/static/bidder-info/adocean.yaml @@ -0,0 +1,6 @@ +maintainer: + email: "aoteam@gemius.com" +capabilities: + site: + mediaTypes: + - banner diff --git a/static/bidder-info/adoppler.yaml b/static/bidder-info/adoppler.yaml new file mode 100644 index 00000000000..1b10103923e --- /dev/null +++ b/static/bidder-info/adoppler.yaml @@ -0,0 +1,11 @@ +maintainer: + email: pbs@adoppler.com +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/adtarget.yaml b/static/bidder-info/adtarget.yaml new file mode 100644 index 00000000000..d52f18ac697 --- /dev/null +++ b/static/bidder-info/adtarget.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "kamil@adtarget.com.tr" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/advangelists.yaml b/static/bidder-info/advangelists.yaml index e1bc6c0a19b..aed9900d0e7 100644 --- a/static/bidder-info/advangelists.yaml +++ b/static/bidder-info/advangelists.yaml @@ -1,5 +1,5 @@ maintainer: - email: "lokesh@advangelists.com" + email: "prebid@advangelists.com" capabilities: site: mediaTypes: diff --git a/static/bidder-info/aja.yaml b/static/bidder-info/aja.yaml new file mode 100644 index 00000000000..53f43689172 --- /dev/null +++ b/static/bidder-info/aja.yaml @@ -0,0 +1,13 @@ +maintainer: + email: "dev@aja-kk.co.jp" +capabilities: + site: + mediaTypes: + - banner + - video + + app: + mediaTypes: + - banner + - video + diff --git a/static/bidder-info/appnexus.yaml b/static/bidder-info/appnexus.yaml index 585c59b91c6..f1e7ca23cfb 100644 --- a/static/bidder-info/appnexus.yaml +++ b/static/bidder-info/appnexus.yaml @@ -1,5 +1,5 @@ maintainer: - email: "info@prebid.org" + email: "prebid-server@xandr.com" capabilities: app: mediaTypes: diff --git a/static/bidder-info/audienceNetwork.yaml b/static/bidder-info/audienceNetwork.yaml index 34700f2f929..56230bf3f9a 100644 --- a/static/bidder-info/audienceNetwork.yaml +++ b/static/bidder-info/audienceNetwork.yaml @@ -1,5 +1,5 @@ maintainer: - email: "info@prebid.org" + email: "none" capabilities: site: mediaTypes: diff --git a/static/bidder-info/avocet.yaml b/static/bidder-info/avocet.yaml new file mode 100644 index 00000000000..ea98982d69c --- /dev/null +++ b/static/bidder-info/avocet.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "developers@avocet.io" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/beintoo.yaml b/static/bidder-info/beintoo.yaml new file mode 100644 index 00000000000..fcca29220cf --- /dev/null +++ b/static/bidder-info/beintoo.yaml @@ -0,0 +1,6 @@ +maintainer: + email: "adops@beintoo.com" +capabilities: + site: + mediaTypes: + - banner diff --git a/static/bidder-info/brightroll.yaml b/static/bidder-info/brightroll.yaml index 14d9a45f268..f913be6da8c 100644 --- a/static/bidder-info/brightroll.yaml +++ b/static/bidder-info/brightroll.yaml @@ -1,5 +1,5 @@ maintainer: - email: "smithaa@oath.com" + email: "dsp-supply-prebid@verizonmedia.com" capabilities: app: mediaTypes: diff --git a/static/bidder-info/conversant.yaml b/static/bidder-info/conversant.yaml index ce67700e380..017f0e0c57e 100644 --- a/static/bidder-info/conversant.yaml +++ b/static/bidder-info/conversant.yaml @@ -1,5 +1,5 @@ maintainer: - email: "mediapsr@conversantmedia.com" + email: "CNVR_PublisherIntegration@conversantmedia.com" capabilities: app: mediaTypes: diff --git a/static/bidder-info/cpmstar.yaml b/static/bidder-info/cpmstar.yaml new file mode 100644 index 00000000000..097dfddd5b0 --- /dev/null +++ b/static/bidder-info/cpmstar.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "prebid@cpmstar.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/datablocks.yaml b/static/bidder-info/datablocks.yaml index 9bf7e780914..43f00a63eae 100644 --- a/static/bidder-info/datablocks.yaml +++ b/static/bidder-info/datablocks.yaml @@ -1,5 +1,5 @@ maintainer: - email: "henry@datablocks.net" + email: "prebid@datablocks.net" capabilities: app: mediaTypes: diff --git a/static/bidder-info/dmx.yaml b/static/bidder-info/dmx.yaml new file mode 100644 index 00000000000..d6e54178db4 --- /dev/null +++ b/static/bidder-info/dmx.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "steve@districtm.net" +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/engagebdr.yaml b/static/bidder-info/engagebdr.yaml index d2f7476235f..57c359e451d 100644 --- a/static/bidder-info/engagebdr.yaml +++ b/static/bidder-info/engagebdr.yaml @@ -1,5 +1,5 @@ maintainer: - email: "admin@engagebdr.com" + email: "tech@engagebdr.com" capabilities: app: mediaTypes: diff --git a/static/bidder-info/gamoshi.yaml b/static/bidder-info/gamoshi.yaml index 71120ed057e..c3ed3ff10e4 100644 --- a/static/bidder-info/gamoshi.yaml +++ b/static/bidder-info/gamoshi.yaml @@ -1,5 +1,5 @@ maintainer: - email: "moses@gamoshi.com" + email: "dev@gamoshi.com" capabilities: app: mediaTypes: diff --git a/static/bidder-info/gumgum.yaml b/static/bidder-info/gumgum.yaml index b8a3981c9f0..0feca7cdf73 100644 --- a/static/bidder-info/gumgum.yaml +++ b/static/bidder-info/gumgum.yaml @@ -1,5 +1,5 @@ maintainer: - email: "pubtech@gumgum.com" + email: "prebid@gumgum.com" capabilities: site: mediaTypes: diff --git a/static/bidder-info/ix.yaml b/static/bidder-info/ix.yaml index ff29ec03f77..326989ae9fe 100644 --- a/static/bidder-info/ix.yaml +++ b/static/bidder-info/ix.yaml @@ -1,5 +1,5 @@ maintainer: - email: "info@prebid.org" + email: "pdu-supply-prebid@indexexchange.com" capabilities: site: mediaTypes: diff --git a/static/bidder-info/kidoz.yaml b/static/bidder-info/kidoz.yaml new file mode 100644 index 00000000000..e2a9eee3fc7 --- /dev/null +++ b/static/bidder-info/kidoz.yaml @@ -0,0 +1,11 @@ +maintainer: + email: prebid-support@kidoz.net +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/lunamedia.yaml b/static/bidder-info/lunamedia.yaml new file mode 100644 index 00000000000..4cabdc4a381 --- /dev/null +++ b/static/bidder-info/lunamedia.yaml @@ -0,0 +1,13 @@ +maintainer: + email: "josh@lunamedia.io" +capabilities: + site: + mediaTypes: + - banner + - video + + app: + mediaTypes: + - banner + - video + diff --git a/static/bidder-info/mobilefuse.yaml b/static/bidder-info/mobilefuse.yaml new file mode 100644 index 00000000000..178e407d927 --- /dev/null +++ b/static/bidder-info/mobilefuse.yaml @@ -0,0 +1,7 @@ +maintainer: + email: prebid@mobilefuse.com +capabilities: + app: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/nanointeractive.yaml b/static/bidder-info/nanointeractive.yaml new file mode 100644 index 00000000000..244e7602950 --- /dev/null +++ b/static/bidder-info/nanointeractive.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "development@nanointeractive.com" +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner diff --git a/static/bidder-info/ninthdecimal.yaml b/static/bidder-info/ninthdecimal.yaml new file mode 100755 index 00000000000..eda7d222a5f --- /dev/null +++ b/static/bidder-info/ninthdecimal.yaml @@ -0,0 +1,13 @@ +maintainer: + email: "abudig@ninthdecimal.com" +capabilities: + site: + mediaTypes: + - banner + - video + + app: + mediaTypes: + - banner + - video + diff --git a/static/bidder-info/openx.yaml b/static/bidder-info/openx.yaml index ce2b67db7da..d16a7d73038 100644 --- a/static/bidder-info/openx.yaml +++ b/static/bidder-info/openx.yaml @@ -1,9 +1,10 @@ maintainer: - email: "team-openx@openx.com" + email: "prebid@openx.com" capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner diff --git a/static/bidder-info/orbidder.yaml b/static/bidder-info/orbidder.yaml new file mode 100644 index 00000000000..c683087d197 --- /dev/null +++ b/static/bidder-info/orbidder.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "realtime-siggi@otto.de" +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner \ No newline at end of file diff --git a/static/bidder-info/pubmatic.yaml b/static/bidder-info/pubmatic.yaml index ccca6a777e7..4009d439352 100644 --- a/static/bidder-info/pubmatic.yaml +++ b/static/bidder-info/pubmatic.yaml @@ -4,6 +4,7 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner diff --git a/static/bidder-info/pulsepoint.yaml b/static/bidder-info/pulsepoint.yaml index b9fd32427b1..716e453000e 100644 --- a/static/bidder-info/pulsepoint.yaml +++ b/static/bidder-info/pulsepoint.yaml @@ -1,5 +1,5 @@ maintainer: - email: "info@prebid.org" + email: "ExchangeTeam@pulsepoint.com" capabilities: app: mediaTypes: diff --git a/static/bidder-info/rtbhouse.yaml b/static/bidder-info/rtbhouse.yaml index 4b899eb3e56..f15af6ca2e1 100644 --- a/static/bidder-info/rtbhouse.yaml +++ b/static/bidder-info/rtbhouse.yaml @@ -1,5 +1,5 @@ maintainer: - email: "inventory.devel@rtbhouse.com" + email: "prebid@rtbhouse.com" capabilities: site: mediaTypes: diff --git a/static/bidder-info/smartrtb.yaml b/static/bidder-info/smartrtb.yaml new file mode 100644 index 00000000000..c26184f91b7 --- /dev/null +++ b/static/bidder-info/smartrtb.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "engineering@smrtb.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/sonobi.yaml b/static/bidder-info/sonobi.yaml index f49fa2812b0..6d39319a9f5 100644 --- a/static/bidder-info/sonobi.yaml +++ b/static/bidder-info/sonobi.yaml @@ -1,5 +1,5 @@ maintainer: - email: "apex@sonobi.com" + email: "apex.prebid@sonobi.com" capabilities: site: mediaTypes: diff --git a/static/bidder-info/ucfunnel.yaml b/static/bidder-info/ucfunnel.yaml new file mode 100644 index 00000000000..288b0b3f1b8 --- /dev/null +++ b/static/bidder-info/ucfunnel.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "support@ucfunnel.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/valueimpression.yaml b/static/bidder-info/valueimpression.yaml new file mode 100644 index 00000000000..1d64abcb68f --- /dev/null +++ b/static/bidder-info/valueimpression.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "info@valueimpression.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/verizonmedia.yaml b/static/bidder-info/verizonmedia.yaml index da5725eec34..024cafadec0 100644 --- a/static/bidder-info/verizonmedia.yaml +++ b/static/bidder-info/verizonmedia.yaml @@ -1,6 +1,6 @@ maintainer: - email: "hb-fe-tech@verizonmedia.com" + email: "dsp-supply-prebid@verizonmedia.com" capabilities: site: mediaTypes: - - banner \ No newline at end of file + - banner diff --git a/static/bidder-info/visx.yaml b/static/bidder-info/visx.yaml index dd4f6c660de..b6a16e4c2d0 100644 --- a/static/bidder-info/visx.yaml +++ b/static/bidder-info/visx.yaml @@ -1,6 +1,9 @@ maintainer: - email: "service@yoc.com" + email: "supply.partners@yoc.com" capabilities: site: mediaTypes: - banner + app: + mediaTypes: + - banner diff --git a/static/bidder-info/yeahmobi.yaml b/static/bidder-info/yeahmobi.yaml new file mode 100644 index 00000000000..063b09d0f75 --- /dev/null +++ b/static/bidder-info/yeahmobi.yaml @@ -0,0 +1,8 @@ +maintainer: + email: "junping.zhao@yeahmobi.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-info/yieldlab.yaml b/static/bidder-info/yieldlab.yaml new file mode 100644 index 00000000000..654e6c749cb --- /dev/null +++ b/static/bidder-info/yieldlab.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "solutions@yieldlab.de" +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/yieldmo.yaml b/static/bidder-info/yieldmo.yaml index 7d6c0af67cd..514f17455ea 100644 --- a/static/bidder-info/yieldmo.yaml +++ b/static/bidder-info/yieldmo.yaml @@ -1,5 +1,5 @@ maintainer: - email: "progsupport@yieldmo.com" + email: "prebid@yieldmo.com" capabilities: site: mediaTypes: diff --git a/static/bidder-info/yieldone.yaml b/static/bidder-info/yieldone.yaml new file mode 100644 index 00000000000..74aef46d24f --- /dev/null +++ b/static/bidder-info/yieldone.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "y1dev@platform-one.co.jp" +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/zeroclickfraud.yaml b/static/bidder-info/zeroclickfraud.yaml new file mode 100644 index 00000000000..527c0065600 --- /dev/null +++ b/static/bidder-info/zeroclickfraud.yaml @@ -0,0 +1,13 @@ +maintainer: + email: "support@datablocks.net" +capabilities: + app: + mediaTypes: + - banner + - native + - video + site: + mediaTypes: + - banner + - native + - video diff --git a/static/bidder-params/adform.json b/static/bidder-params/adform.json index 308ae3e9414..67f09623ee4 100644 --- a/static/bidder-params/adform.json +++ b/static/bidder-params/adform.json @@ -16,7 +16,7 @@ "mkv": { "type": "string", "description": "Comma-separated key-value pairs. Forbidden symbols: &. Example: mkv='color:blue,length:350'", - "pattern": "^(\\s*|(([^,:&]*[^,:&\\s]+[^,:&]*)+:[^,:&]*,)*(([^,:&]*[^,:&\\s]+[^,:&]*)+:[^,:&]*,?))$" + "pattern": "^(\\s*|((\\s*[^,:&\\s]+\\s*:[^,:&]*)(,\\s*[^,:&\\s]+\\s*:[^,:&]*)*))$" }, "mkw": { "type": "string", diff --git a/static/bidder-params/adgeneration.json b/static/bidder-params/adgeneration.json new file mode 100644 index 00000000000..a4f761c3603 --- /dev/null +++ b/static/bidder-params/adgeneration.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdGeneration Adapter Params", + "description": "A schema which validates params accepted by the AdGeneration adapter", + + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Ad ID registered by AdGeneration." + } + }, + "required": ["id"] + } + \ No newline at end of file diff --git a/static/bidder-params/adhese.json b/static/bidder-params/adhese.json new file mode 100644 index 00000000000..a1bd608b7a8 --- /dev/null +++ b/static/bidder-params/adhese.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adhese Adapter Parameters", + "description": "Validation for parameters handled by the Adhese adapter", + "type": "object", + "properties": { + "account": { + "type": "string", + "description": "Your Adhese account name. If unknown, please contact your sales rep" + }, + "location": { + "type": "string", + "description": "The location you want to refer to for a specific section or page, as defined in your Adhese inventory" + }, + "format": { + "type": "string", + "description": "The format you accept for this unit, as defined in your Adhese inventory" + }, + "targets": { + "type": "object", + "description": "Target params, as defined in your Adhese setup." + } + }, + "required": ["account", "location", "format"] +} diff --git a/static/bidder-params/admixer.json b/static/bidder-params/admixer.json new file mode 100644 index 00000000000..886e33ff2bb --- /dev/null +++ b/static/bidder-params/admixer.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Admixer Adapter Params", + "description": "A schema which validates params accepted by the Admixer adapter", + + "type": "object", + "properties": { + "zone": { + "type": "string", + "description": "Zone ID.", + "pattern": "^([a-fA-F\\d\\-]{36})$" + }, + "customFloor": { + "type": "number", + "description": "The minimum CPM price in USD.", + "minimum": 0 + }, + "customParams": { + "type": "object", + "description": "User-defined targeting key-value pairs." + } + }, + + "required": ["zone"] +} diff --git a/static/bidder-params/adocean.json b/static/bidder-params/adocean.json new file mode 100644 index 00000000000..7530c64784c --- /dev/null +++ b/static/bidder-params/adocean.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdOcean Adapter Params", + "description": "A schema which validates params accepted by the AdOcean adapter", + "type": "object", + "properties": { + "emiter": { + "type": "string", + "description": "AdOcean emiter", + "pattern": ".+" + }, + "masterId": { + "type": "string", + "description": "Master's id", + "pattern": "^[\\w.]+$" + }, + "slaveId": { + "type": "string", + "description": "Slave's id", + "pattern": "^adocean[\\w.]+$" + } + }, + "required": ["emiter", "masterId", "slaveId"] +} diff --git a/static/bidder-params/adoppler.json b/static/bidder-params/adoppler.json new file mode 100644 index 00000000000..c2bdde4f60f --- /dev/null +++ b/static/bidder-params/adoppler.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adoppler Adapter Params", + "description": "A schema which validates params accepted by the Adoppler adapter", + "type": "object", + "properties": { + "adunit": { + "type": "string", + "description": "AdUnit to bid against to." + } + }, + "required": ["adunit"] +} diff --git a/static/bidder-params/adtarget.json b/static/bidder-params/adtarget.json new file mode 100644 index 00000000000..195bf2dd430 --- /dev/null +++ b/static/bidder-params/adtarget.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adtarget Adapter Params", + "description": "A schema which validates params accepted by the Adtarget adapter", + + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "siteId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "aid": { + "type": "integer", + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": ["aid"] +} diff --git a/static/bidder-params/advangelists.json b/static/bidder-params/advangelists.json index c1b13d21767..1788cbe1dc8 100644 --- a/static/bidder-params/advangelists.json +++ b/static/bidder-params/advangelists.json @@ -8,6 +8,10 @@ "type": "string", "description": "An id used to identify Advangelists publisher.", "minLength": 8 + }, + "placement": { + "type": "string", + "description": "An id used to identify placements." } }, "required": ["pubid"] diff --git a/static/bidder-params/aja.json b/static/bidder-params/aja.json new file mode 100644 index 00000000000..c15a4e04d12 --- /dev/null +++ b/static/bidder-params/aja.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AJA Adapter Params", + "description": "A schema which validates params accepted by the AJA adapter", + "type": "object", + "properties": { + "asi": { + "type": "string", + "description": "Ad spot ID" + } + }, + "required": ["asi"] +} diff --git a/static/bidder-params/avocet.json b/static/bidder-params/avocet.json new file mode 100644 index 00000000000..f27e5950f7c --- /dev/null +++ b/static/bidder-params/avocet.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Avocet Adapter Params", + "description": "A schema which validates params accepted by the Avocet adapter", + "type": "object", + "properties": { + "placement": { + "type": "string", + "description": "An Avocet placement ID" + }, + "placement_code": { + "type": "string", + "description": "An Avocet placement external code" + } + }, + "oneOf": [ + { + "required": ["placement"] + }, + { + "required": ["placement_code"] + } + ] +} diff --git a/static/bidder-params/beintoo.json b/static/bidder-params/beintoo.json new file mode 100644 index 00000000000..52bb0351124 --- /dev/null +++ b/static/bidder-params/beintoo.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Beintoo Adapter Params", + "description": "A schema which validates params accepted by the Beintoo adapter", + "type": "object", + "properties": { + "tagid" : { + "type": "string", + "description": "The id of an inventory target" + }, + "bidfloor": { + "type": "string", + "description": "The minimum price acceptable for a bid" + } + }, + + "required": ["tagid"] + } diff --git a/static/bidder-params/cpmstar.json b/static/bidder-params/cpmstar.json new file mode 100644 index 00000000000..576b503e793 --- /dev/null +++ b/static/bidder-params/cpmstar.json @@ -0,0 +1,19 @@ + +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Cpmstar Adapter Params", + "description": "Schema to validate params accepted by the Cpmstar adapter", + + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "Cpmstar-specific ID for ad pool" + }, + "subpoolId": { + "type": "integer", + "description": "Cpmstar-specific ID for ad subpool" + } + }, + "required": ["placementId"] + } diff --git a/static/bidder-params/dmx.json b/static/bidder-params/dmx.json new file mode 100644 index 00000000000..4c0df65e3d4 --- /dev/null +++ b/static/bidder-params/dmx.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "District M DMX Adapter Params", + "description": "A schema which validates params accepted by the DMX adapter", + "type": "object", + "properties": { + "memberid" : { + "type": "string", + "description": "Represent boost MemberId from districtm UI" + }, + "tagid": { + "type": "string", + "description": "Represent the placement ID, this value is optional" + }, + "bidfloor": { + "type": "string", + "description": "The minimum price acceptable for a bid" + } + }, + + "required": ["memberid"] +} \ No newline at end of file diff --git a/static/bidder-params/grid.json b/static/bidder-params/grid.json index 7a0cf3da8c5..67f9b12f115 100644 --- a/static/bidder-params/grid.json +++ b/static/bidder-params/grid.json @@ -3,6 +3,11 @@ "title": "TheMediaGrid Adapter Params", "description": "A schema which validates params accepted by TheMediaGrid adapter", "type": "object", - "properties": {}, + "properties": { + "uid": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + } + }, "required": [] } diff --git a/static/bidder-params/kidoz.json b/static/bidder-params/kidoz.json new file mode 100644 index 00000000000..79e2edc2fd2 --- /dev/null +++ b/static/bidder-params/kidoz.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Kidoz Adapter Params", + "description": "A schema which validates params accepted by the Kidoz adapter", + "type": "object", + "properties": { + "access_token": { + "$ref": "#/definitions/non-empty-string", + "description": "Kidoz access_token" + }, + "publisher_id": { + "$ref": "#/definitions/non-empty-string", + "description": "Kidoz publisher_id" + } + }, + "definitions": { + "non-empty-string": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "access_token", + "publisher_id" + ] +} \ No newline at end of file diff --git a/static/bidder-params/lunamedia.json b/static/bidder-params/lunamedia.json new file mode 100644 index 00000000000..1aa18cee6b9 --- /dev/null +++ b/static/bidder-params/lunamedia.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "LunaMedia Adapter Params", + "description": "A schema which validates params accepted by the LunaMedia adapter", + "type": "object", + "properties": { + "pubid": { + "type": "string", + "description": "An id used to identify LunaMedia publisher.", + "minLength": 8 + }, + "placement": { + "type": "string", + "description": "A placement created on adserver." + } + }, + "required": ["pubid"] +} diff --git a/static/bidder-params/mobilefuse.json b/static/bidder-params/mobilefuse.json new file mode 100644 index 00000000000..15f17148072 --- /dev/null +++ b/static/bidder-params/mobilefuse.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MobileFuse Adapter Params", + "description": "A schema which validates params accepted by the MobileFuse adapter", + "type": "object", + "properties": { + "placement_id": { + "type": "integer", + "description": "An ID which identifies this specific inventory placement" + }, + "pub_id": { + "type": "integer", + "description": "An ID which identifies the publisher selling the inventory." + }, + "tagid_src": { + "type": "string", + "description": "ext if passing publisher's ids, empty if passing MobileFuse IDs in placement_id field. Defaults to empty" + } + }, + "required": [ + "placement_id", + "pub_id" + ] +} \ No newline at end of file diff --git a/static/bidder-params/nanointeractive.json b/static/bidder-params/nanointeractive.json new file mode 100644 index 00000000000..707dff2fa50 --- /dev/null +++ b/static/bidder-params/nanointeractive.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "NanoInteractive Adapter Params", + "description": "A schema which validates params accepted by the NanoInteractive adapter", + "type": "object", + "properties": { + "pid": { + "type": "string", + "description": "Placement idd" + }, + "nq": { + "type": "array", + "items": { + "type": "string" + }, + "description": "search queries" + }, + "category": { + "type": "string", + "description": "IAB Category" + }, + "subId": { + "type": "string", + "description": "any segment value provided by publisher" + }, + "ref" : { + "type": "string", + "description": "referer" + } + }, + "required": ["pid"] +} \ No newline at end of file diff --git a/static/bidder-params/ninthdecimal.json b/static/bidder-params/ninthdecimal.json new file mode 100755 index 00000000000..f230361d77e --- /dev/null +++ b/static/bidder-params/ninthdecimal.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "NinthDecimal Adapter Params", + "description": "A schema which validates params accepted by the NinthDecimal adapter", + "type": "object", + "properties": { + "pubid": { + "type": "string", + "description": "An id used to identify NinthDecimal publisher.", + "minLength": 8 + }, + "placement": { + "type": "string", + "description": "A placement created on adserver." + } + }, + "required": ["pubid"] +} diff --git a/static/bidder-params/orbidder.json b/static/bidder-params/orbidder.json new file mode 100644 index 00000000000..d986b23284e --- /dev/null +++ b/static/bidder-params/orbidder.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Orbidder Adapter Params", + "description": "A schema which validates params accepted by the Orbidder adapter", + + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "The marketer's accountId." + }, + "placementId": { + "type": "string", + "description": "The placementId of the ad unit." + }, + "bidfloor": { + "type": "number", + "description": "The minimum CPM price in EUR.", + "minimum": 0 + } + }, + + "required": ["accountId", "placementId"] +} \ No newline at end of file diff --git a/static/bidder-params/smartrtb.json b/static/bidder-params/smartrtb.json new file mode 100644 index 00000000000..3bbaab10736 --- /dev/null +++ b/static/bidder-params/smartrtb.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "SmartRTB Adapter Params", + "description": "Required parameters for the SmartRTB server adapter", + "type": "object", + "properties": { + "pub_id": { + "type": "string", + "description": "Assigned publisher ID", + "minLength": 4 + }, + "med_id": { + "type": "string", + "description": "Property ID not zone ID not provided" + }, + "zone_id": { + "type": "string", + "description": "Specific zone ID for this placement, belonging to app/site", + "minLength": 20 + }, + "force_bid": { + "type": "boolean", + "description": "Force bids with a test creative" + } + }, + "required": [ "pub_id" ] + } diff --git a/static/bidder-params/synacormedia.json b/static/bidder-params/synacormedia.json index b2dff8faca1..8c74ada2e85 100644 --- a/static/bidder-params/synacormedia.json +++ b/static/bidder-params/synacormedia.json @@ -8,6 +8,10 @@ "seatId": { "type": "string", "description": "The seat id." + }, + "tagId": { + "type": "string", + "description": "The tag id." } }, diff --git a/static/bidder-params/ucfunnel.json b/static/bidder-params/ucfunnel.json new file mode 100644 index 00000000000..d39d006cf1f --- /dev/null +++ b/static/bidder-params/ucfunnel.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Ucfunnel Adapter Params", + "description": "A schema which validates params accepted by the Ucfunnel adapter", + "type": "object", + "properties": { + "adunitid": { + "type": "string", + "description": "ID for ad unit" + }, + "partnerid": { + "type": "string", + "description": "ID for partner" + } + }, + "required": ["partnerid"] +} diff --git a/static/bidder-params/valueimpression.json b/static/bidder-params/valueimpression.json new file mode 100644 index 00000000000..5b9c32c592e --- /dev/null +++ b/static/bidder-params/valueimpression.json @@ -0,0 +1,15 @@ + +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "ValueImpression Adapter Params", + "description": "Schema to validate params accepted by the ValueImpression adapter", + + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID" + } + }, + "required": ["siteId"] + } diff --git a/static/bidder-params/yeahmobi.json b/static/bidder-params/yeahmobi.json new file mode 100644 index 00000000000..fe26fa7255a --- /dev/null +++ b/static/bidder-params/yeahmobi.json @@ -0,0 +1,21 @@ + +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Yeahmobi Adapter Params", + "description": "A schema which validates params accepted by the Yeahmobi adapter", + + "type": "object", + "properties": { + "pubId": { + "type": "string", + "description": "Publisher ID", + "minLength": 1 + }, + "zoneId": { + "type": "string", + "description": "Zone Id", + "minLength": 1 + } + }, + "required": ["pubId", "zoneId"] +} \ No newline at end of file diff --git a/static/bidder-params/yieldlab.json b/static/bidder-params/yieldlab.json new file mode 100644 index 00000000000..900d65da6e5 --- /dev/null +++ b/static/bidder-params/yieldlab.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Yieldlab Adapter Params", + "description": "A schema which validates params accepted by the Yieldlab adapter", + "type": "object", + "properties": { + "adslotId": { + "type": "string", + "description": "Yieldlab ID of the ad slot" + }, + "supplyId": { + "type": "string", + "description": "Yieldlab ID of the supply" + }, + "adSize": { + "type": "string", + "description": "Size of the adslot in pixel, e.g. 200x50" + }, + "extId": { + "type": "string", + "description": "External ID used for reporting" + }, + "targeting": { + "type": "object", + "description": "Targeting information in key value pairs" + } + }, + "required": [ + "adslotId", + "supplyId", + "adSize" + ] +} diff --git a/static/bidder-params/yieldone.json b/static/bidder-params/yieldone.json new file mode 100644 index 00000000000..15d7acec177 --- /dev/null +++ b/static/bidder-params/yieldone.json @@ -0,0 +1,15 @@ + +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Yieldone Adapter Params", + "description": "A schema which validates params accepted by the Yieldone adapter", + + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "Internal Yieldone Placement ID" + } + }, + "required": ["placementId"] + } diff --git a/static/bidder-params/zeroclickfraud.json b/static/bidder-params/zeroclickfraud.json new file mode 100644 index 00000000000..1c5e3c633b4 --- /dev/null +++ b/static/bidder-params/zeroclickfraud.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "ZeroClickFraud Adapter Params", + "description": "A schema which validates params accepted by the ZeroClickFraud adapter", + + "type": "object", + "properties": { + "sourceId": { + "type": "integer", + "minimum": 1, + "description": "Website Source Id" + }, + "host": { + "type": "string", + "description": "Network Host to request from" + } + }, + "required": ["host", "sourceId"] +} diff --git a/static/category-mapping/freewheel/freewheel.json b/static/category-mapping/freewheel/freewheel.json index 21fbca67426..1c4a4fa2471 100644 --- a/static/category-mapping/freewheel/freewheel.json +++ b/static/category-mapping/freewheel/freewheel.json @@ -3,1176 +3,1180 @@ "id": "404", "name": "Publishing" }, - "IAB1-2": { - "id": "392", - "name": "Entertainment" - }, - "IAB1-5": { - "id": "419", - "name": "Filmed Entertainment" - }, - "IAB1-6": { - "id": "392", - "name": "Entertainment" - }, - "IAB1-7": { - "id": "392", - "name": "Entertainment" - }, - "IAB2-1": { - "id": "399", - "name": "Automotive" - }, - "IAB2-2": { - "id": "399", - "name": "Automotive" - }, - "IAB2-3": { - "id": "399", - "name": "Automotive" - }, - "IAB2-4": { - "id": "399", - "name": "Automotive" - }, - "IAB2-5": { - "id": "399", - "name": "Automotive" - }, - "IAB2-6": { - "id": "399", - "name": "Automotive" - }, - "IAB2-7": { - "id": "399", - "name": "Automotive" - }, - "IAB2-8": { - "id": "399", - "name": "Automotive" - }, - "IAB2-9": { - "id": "399", - "name": "Automotive" - }, - "IAB2-10": { - "id": "399", - "name": "Automotive" - }, - "IAB2-11": { - "id": "399", - "name": "Automotive" - }, - "IAB2-12": { - "id": "399", - "name": "Automotive" - }, - "IAB2-13": { - "id": "399", - "name": "Automotive" - }, - "IAB2-14": { - "id": "399", - "name": "Automotive" - }, - "IAB2-15": { - "id": "399", - "name": "Automotive" - }, - "IAB2-16": { - "id": "399", - "name": "Automotive" - }, - "IAB2-17": { - "id": "399", - "name": "Automotive" - }, - "IAB2-18": { - "id": "399", - "name": "Automotive" - }, - "IAB2-19": { - "id": "399", - "name": "Automotive" - }, - "IAB2-20": { - "id": "399", - "name": "Automotive" - }, - "IAB2-21": { - "id": "399", - "name": "Automotive" - }, - "IAB2-22": { - "id": "399", - "name": "Automotive" - }, - "IAB2-23": { - "id": "399", - "name": "Automotive" - }, - "IAB3-1": { - "id": "393", - "name": "Business Services" - }, - "IAB3-2": { - "id": "393", - "name": "Business Services" - }, - "IAB3-3": { - "id": "393", - "name": "Business Services" - }, - "IAB3-4": { - "id": "409", - "name": "Computing Product" - }, - "IAB3-5": { - "id": "393", - "name": "Business Services" - }, - "IAB3-6": { - "id": "393", - "name": "Business Services" - }, - "IAB3-7": { - "id": "398", - "name": "Government/Municipal" - }, - "IAB3-8": { - "id": "393", - "name": "Business Services" - }, - "IAB3-9": { - "id": "393", - "name": "Business Services" - }, - "IAB3-10": { - "id": "393", - "name": "Business Services" - }, - "IAB3-11": { - "id": "393", - "name": "Business Services" - }, - "IAB3-12": { - "id": "393", - "name": "Business Services" - }, - "IAB4-1": { - "id": "393", - "name": "Business Services" - }, - "IAB4-2": { - "id": "405", - "name": "Educational Services" - }, - "IAB4-3": { - "id": "405", - "name": "Educational Services" - }, - "IAB4-4": { - "id": "393", - "name": "Business Services" - }, - "IAB4-5": { - "id": "393", - "name": "Business Services" - }, - "IAB4-6": { - "id": "393", - "name": "Business Services" - }, - "IAB4-7": { - "id": "406", - "name": "Health Care Services" - }, - "IAB4-8": { - "id": "405", - "name": "Educational Services" - }, - "IAB4-9": { - "id": "417", - "name": "Telecommunications" - }, - "IAB4-10": { - "id": "429", - "name": "Military" - }, - "IAB4-11": { - "id": "393", - "name": "Business Services" - }, - "IAB5-1": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-2": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-3": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-4": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-5": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-6": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-7": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-8": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-9": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-10": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-11": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-12": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-13": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-14": { - "id": "405", - "name": "Educational Services" - }, - "IAB5-15": { - "id": "405", - "name": "Educational Services" - }, - "IAB7-1": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-2": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-3": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-4": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-5": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-6": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-7": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-8": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-9": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-10": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-11": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-12": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-13": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-14": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-15": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-16": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-17": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-18": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-19": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-20": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-21": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-22": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-23": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-24": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-25": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-26": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-27": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-28": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-29": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-30": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-31": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-32": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-33": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-34": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-35": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-36": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-37": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-38": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-39": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-40": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-41": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-42": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-43": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-44": { - "id": "406", - "name": "Health Care Services" - }, - "IAB7-45": { - "id": "406", - "name": "Health Care Services" - }, - "IAB8-1": { - "id": "394", - "name": "Food" - }, - "IAB8-2": { - "id": "394", - "name": "Food" - }, - "IAB8-3": { - "id": "394", - "name": "Food" - }, - "IAB8-4": { - "id": "394", - "name": "Food" - }, - "IAB8-5": { - "id": "400", - "name": "Beer/Wine/Liquor" - }, - "IAB8-6": { - "id": "401", - "name": "Beverages" - }, - "IAB8-7": { - "id": "394", - "name": "Food" - }, - "IAB8-8": { - "id": "394", - "name": "Food" - }, - "IAB8-9": { - "id": "407", - "name": "Restaurant/Fast Food" - }, - "IAB8-10": { - "id": "394", - "name": "Food" - }, - "IAB8-11": { - "id": "394", - "name": "Food" - }, - "IAB8-12": { - "id": "394", - "name": "Food" - }, - "IAB8-13": { - "id": "394", - "name": "Food" - }, - "IAB8-14": { - "id": "394", - "name": "Food" - }, - "IAB8-15": { - "id": "394", - "name": "Food" - }, - "IAB8-16": { - "id": "394", - "name": "Food" - }, - "IAB8-17": { - "id": "394", - "name": "Food" - }, - "IAB8-18": { - "id": "400", - "name": "Beer/Wine/Liquor" - }, - "IAB9-1": { - "id": "392", - "name": "Entertainment" - }, - "IAB9-3": { - "id": "418", - "name": "Jewelry" - }, - "IAB9-5": { - "id": "413", - "name": "Gaming" - }, - "IAB9-6": { - "id": "412", - "name": "Household Products" - }, - "IAB9-9": { - "id": "426", - "name": "Tobacco" - }, - "IAB9-11": { - "id": "404", - "name": "Publishing" - }, - "IAB9-15": { - "id": "404", - "name": "Publishing" - }, - "IAB9-16": { - "id": "392", - "name": "Entertainment" - }, - "IAB9-18": { - "id": "393", - "name": "Business Services" - }, - "IAB9-19": { - "id": "418", - "name": "Jewelry" - }, - "IAB9-23": { - "id": "424", - "name": "Photographic Equipment" - }, - "IAB9-24": { - "id": "392", - "name": "Entertainment" - }, - "IAB9-25": { - "id": "392", - "name": "Entertainment" - }, - "IAB9-30": { - "id": "392", - "name": "Entertainment" - }, - "IAB10-1": { - "id": "415", - "name": "Appliances" - }, - "IAB10-5": { - "id": "434", - "name": "Home Furnishings" - }, - "IAB10-6": { - "id": "434", - "name": "Home Furnishings" - }, - "IAB10-7": { - "id": "434", - "name": "Home Furnishings" - }, - "IAB10-8": { - "id": "393", - "name": "Business Services" - }, - "IAB10-9": { - "id": "434", - "name": "Home Furnishings" - }, - "IAB11-1": { - "id": "398", - "name": "Government/Municipal" - }, - "IAB11-2": { - "id": "398", - "name": "Government/Municipal" - }, - "IAB11-3": { - "id": "398", - "name": "Government/Municipal" - }, - "IAB11-4": { - "id": "398", - "name": "Government/Municipal" - }, - "IAB11-5": { - "id": "398", - "name": "Government/Municipal" - }, - "IAB12-1": { - "id": "438", - "name": "News" - }, - "IAB12-2": { - "id": "438", - "name": "News" - }, - "IAB12-3": { - "id": "438", - "name": "News" - }, - "IAB13-1": { - "id": "393", - "name": "Business Services" - }, - "IAB13-2": { - "id": "393", - "name": "Business Services" - }, - "IAB13-3": { - "id": "438", - "name": "News" - }, - "IAB13-4": { - "id": "391", - "name": "Financial Services" - }, - "IAB13-5": { - "id": "393", - "name": "Business Services" - }, - "IAB13-6": { - "id": "436", - "name": "Insurance" - }, - "IAB13-7": { - "id": "393", - "name": "Business Services" - }, - "IAB13-8": { - "id": "393", - "name": "Business Services" - }, - "IAB13-9": { - "id": "393", - "name": "Business Services" - }, - "IAB13-10": { - "id": "393", - "name": "Business Services" - }, - "IAB13-11": { - "id": "393", - "name": "Business Services" - }, - "IAB13-12": { - "id": "393", - "name": "Business Services" - }, - "IAB16-1": { - "id": "423", - "name": "Pet Food/Supplies" - }, - "IAB16-2": { - "id": "423", - "name": "Pet Food/Supplies" - }, - "IAB16-3": { - "id": "423", - "name": "Pet Food/Supplies" - }, - "IAB16-4": { - "id": "423", - "name": "Pet Food/Supplies" - }, - "IAB16-5": { - "id": "423", - "name": "Pet Food/Supplies" - }, - "IAB16-6": { - "id": "423", - "name": "Pet Food/Supplies" - }, - "IAB16-7": { - "id": "423", - "name": "Pet Food/Supplies" - }, - "IAB17-1": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-2": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-3": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-4": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-5": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-6": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-7": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-8": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-9": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-10": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-11": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-12": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-13": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-14": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-15": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-16": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-17": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-18": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-19": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-20": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-21": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-22": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-23": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-24": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-25": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-26": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-27": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-28": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-29": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-30": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-31": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-32": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-33": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-34": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-35": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-36": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-37": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-38": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-39": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-40": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-41": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-42": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-43": { - "id": "425", - "name": "Professional Sports" - }, - "IAB17-44": { - "id": "425", - "name": "Professional Sports" - }, - "IAB18-1": { - "id": "411", - "name": "Cosmetics/Toiletries" - }, - "IAB18-2": { - "id": "397", - "name": "Apparel" - }, - "IAB18-3": { - "id": "397", - "name": "Apparel" - }, - "IAB18-4": { - "id": "418", - "name": "Jewelry" - }, - "IAB18-5": { - "id": "397", - "name": "Apparel" - }, - "IAB18-6": { - "id": "397", - "name": "Apparel" - }, - "IAB19-2": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-3": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-4": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-5": { - "id": "424", - "name": "Photographic Equipment" - }, - "IAB19-6": { - "id": "417", - "name": "Telecommunications" - }, - "IAB19-7": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-8": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-9": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-10": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-11": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-12": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-13": { - "id": "404", - "name": "Publishing" - }, - "IAB19-14": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-15": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-16": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-17": { - "id": "419", - "name": "Filmed Entertainment" - }, - "IAB19-18": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-19": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-20": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-21": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-22": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-23": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-24": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-25": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-26": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-27": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-28": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-29": { - "id": "392", - "name": "Entertainment" - }, - "IAB19-30": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-31": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-32": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-33": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-34": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-35": { - "id": "409", - "name": "Computing Product" - }, - "IAB19-36": { - "id": "409", - "name": "Computing Product" - }, - "IAB20-1": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-2": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-3": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-4": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-5": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-6": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-7": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-8": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-9": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-10": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-11": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-12": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-13": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-14": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-15": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-16": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-17": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-18": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-19": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-20": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-21": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-22": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-23": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-24": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-25": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-26": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB20-27": { - "id": "395", - "name": "Travel/Hotels/Airlines" - }, - "IAB21-1": { - "id": "416", - "name": "Real Estate" - }, - "IAB21-2": { - "id": "416", - "name": "Real Estate" - }, - "IAB21-3": { - "id": "416", - "name": "Real Estate" - }, - "IAB22-1": { - "id": "416", - "name": "Real Estate" - }, - "IAB22-2": { - "id": "416", - "name": "Real Estate" - }, - "IAB22-3": { - "id": "416", - "name": "Real Estate" - } + "IAB1-2": { + "id": "392", + "name": "Entertainment" + }, + "IAB1-5": { + "id": "419", + "name": "Filmed Entertainment" + }, + "IAB1-6": { + "id": "392", + "name": "Entertainment" + }, + "IAB1-7": { + "id": "392", + "name": "Entertainment" + }, + "IAB2-1": { + "id": "399", + "name": "Automotive" + }, + "IAB2-2": { + "id": "399", + "name": "Automotive" + }, + "IAB2-3": { + "id": "399", + "name": "Automotive" + }, + "IAB2-4": { + "id": "399", + "name": "Automotive" + }, + "IAB2-5": { + "id": "399", + "name": "Automotive" + }, + "IAB2-6": { + "id": "399", + "name": "Automotive" + }, + "IAB2-7": { + "id": "399", + "name": "Automotive" + }, + "IAB2-8": { + "id": "399", + "name": "Automotive" + }, + "IAB2-9": { + "id": "399", + "name": "Automotive" + }, + "IAB2-10": { + "id": "399", + "name": "Automotive" + }, + "IAB2-11": { + "id": "399", + "name": "Automotive" + }, + "IAB2-12": { + "id": "399", + "name": "Automotive" + }, + "IAB2-13": { + "id": "399", + "name": "Automotive" + }, + "IAB2-14": { + "id": "399", + "name": "Automotive" + }, + "IAB2-15": { + "id": "399", + "name": "Automotive" + }, + "IAB2-16": { + "id": "399", + "name": "Automotive" + }, + "IAB2-17": { + "id": "399", + "name": "Automotive" + }, + "IAB2-18": { + "id": "399", + "name": "Automotive" + }, + "IAB2-19": { + "id": "399", + "name": "Automotive" + }, + "IAB2-20": { + "id": "399", + "name": "Automotive" + }, + "IAB2-21": { + "id": "399", + "name": "Automotive" + }, + "IAB2-22": { + "id": "399", + "name": "Automotive" + }, + "IAB2-23": { + "id": "399", + "name": "Automotive" + }, + "IAB3-1": { + "id": "393", + "name": "Business Services" + }, + "IAB3-2": { + "id": "393", + "name": "Business Services" + }, + "IAB3-3": { + "id": "393", + "name": "Business Services" + }, + "IAB3-4": { + "id": "408", + "name": "Office Equipment/Supplies" + }, + "IAB3-5": { + "id": "390", + "name": "Manufacturing" + }, + "IAB3-6": { + "id": "393", + "name": "Business Services" + }, + "IAB3-7": { + "id": "398", + "name": "Government/Municipal" + }, + "IAB3-8": { + "id": "393", + "name": "Business Services" + }, + "IAB3-9": { + "id": "393", + "name": "Business Services" + }, + "IAB3-10": { + "id": "393", + "name": "Business Services" + }, + "IAB3-11": { + "id": "393", + "name": "Business Services" + }, + "IAB3-12": { + "id": "393", + "name": "Business Services" + }, + "IAB4-1": { + "id": "393", + "name": "Business Services" + }, + "IAB4-2": { + "id": "405", + "name": "Educational Services" + }, + "IAB4-3": { + "id": "405", + "name": "Educational Services" + }, + "IAB4-4": { + "id": "393", + "name": "Business Services" + }, + "IAB4-5": { + "id": "393", + "name": "Business Services" + }, + "IAB4-6": { + "id": "393", + "name": "Business Services" + }, + "IAB4-7": { + "id": "406", + "name": "Health Care Services" + }, + "IAB4-8": { + "id": "405", + "name": "Educational Services" + }, + "IAB4-9": { + "id": "417", + "name": "Telecommunications" + }, + "IAB4-10": { + "id": "429", + "name": "Military" + }, + "IAB4-11": { + "id": "393", + "name": "Business Services" + }, + "IAB5-1": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-2": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-3": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-4": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-5": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-6": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-7": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-8": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-9": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-10": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-11": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-12": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-13": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-14": { + "id": "405", + "name": "Educational Services" + }, + "IAB5-15": { + "id": "405", + "name": "Educational Services" + }, + "IAB7-1": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-2": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-3": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-4": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-5": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-6": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-7": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-8": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-9": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-10": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-11": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-12": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-13": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-14": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-15": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-16": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-17": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-18": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-19": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-20": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-21": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-22": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-23": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-24": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-25": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-26": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-27": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-28": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-29": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-30": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-31": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-32": { + "id": "402", + "name": "Pharmaceuticals" + }, + "IAB7-33": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-34": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-35": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-36": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-37": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-38": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-39": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-40": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-41": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-42": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-43": { + "id": "406", + "name": "Health Care Services" + }, + "IAB7-44": { + "id": "433", + "name": "Drug Stores" + }, + "IAB7-45": { + "id": "406", + "name": "Health Care Services" + }, + "IAB8-1": { + "id": "394", + "name": "Food" + }, + "IAB8-2": { + "id": "394", + "name": "Food" + }, + "IAB8-3": { + "id": "394", + "name": "Food" + }, + "IAB8-4": { + "id": "394", + "name": "Food" + }, + "IAB8-5": { + "id": "400", + "name": "Beer/Wine/Liquor" + }, + "IAB8-6": { + "id": "401", + "name": "Beverages" + }, + "IAB8-7": { + "id": "394", + "name": "Food" + }, + "IAB8-8": { + "id": "394", + "name": "Food" + }, + "IAB8-9": { + "id": "407", + "name": "Restaurant/Fast Food" + }, + "IAB8-10": { + "id": "394", + "name": "Food" + }, + "IAB8-11": { + "id": "394", + "name": "Food" + }, + "IAB8-12": { + "id": "394", + "name": "Food" + }, + "IAB8-13": { + "id": "394", + "name": "Food" + }, + "IAB8-14": { + "id": "394", + "name": "Food" + }, + "IAB8-15": { + "id": "394", + "name": "Food" + }, + "IAB8-16": { + "id": "394", + "name": "Food" + }, + "IAB8-17": { + "id": "394", + "name": "Food" + }, + "IAB8-18": { + "id": "400", + "name": "Beer/Wine/Liquor" + }, + "IAB9-1": { + "id": "392", + "name": "Entertainment" + }, + "IAB9-3": { + "id": "418", + "name": "Jewelry" + }, + "IAB9-5": { + "id": "414", + "name": "Gambling" + }, + "IAB9-6": { + "id": "412", + "name": "Household Products" + }, + "IAB9-7": { + "id": "413", + "name": "Gaming" + }, + "IAB9-9": { + "id": "426", + "name": "Tobacco" + }, + "IAB9-11": { + "id": "404", + "name": "Publishing" + }, + "IAB9-15": { + "id": "404", + "name": "Publishing" + }, + "IAB9-16": { + "id": "392", + "name": "Entertainment" + }, + "IAB9-18": { + "id": "393", + "name": "Business Services" + }, + "IAB9-19": { + "id": "418", + "name": "Jewelry" + }, + "IAB9-23": { + "id": "424", + "name": "Photographic Equipment" + }, + "IAB9-24": { + "id": "392", + "name": "Entertainment" + }, + "IAB9-25": { + "id": "392", + "name": "Entertainment" + }, + "IAB9-30": { + "id": "427", + "name": "Toys/Games" + }, + "IAB10-1": { + "id": "415", + "name": "Appliances" + }, + "IAB10-5": { + "id": "434", + "name": "Home Furnishings" + }, + "IAB10-6": { + "id": "434", + "name": "Home Furnishings" + }, + "IAB10-7": { + "id": "434", + "name": "Home Furnishings" + }, + "IAB10-8": { + "id": "393", + "name": "Business Services" + }, + "IAB10-9": { + "id": "434", + "name": "Home Furnishings" + }, + "IAB11-1": { + "id": "398", + "name": "Government/Municipal" + }, + "IAB11-2": { + "id": "398", + "name": "Government/Municipal" + }, + "IAB11-3": { + "id": "398", + "name": "Government/Municipal" + }, + "IAB11-4": { + "id": "398", + "name": "Government/Municipal" + }, + "IAB11-5": { + "id": "421", + "name": "Associations" + }, + "IAB12-1": { + "id": "438", + "name": "News" + }, + "IAB12-2": { + "id": "438", + "name": "News" + }, + "IAB12-3": { + "id": "438", + "name": "News" + }, + "IAB13-1": { + "id": "393", + "name": "Business Services" + }, + "IAB13-2": { + "id": "393", + "name": "Business Services" + }, + "IAB13-3": { + "id": "438", + "name": "News" + }, + "IAB13-4": { + "id": "391", + "name": "Financial Services" + }, + "IAB13-5": { + "id": "393", + "name": "Business Services" + }, + "IAB13-6": { + "id": "436", + "name": "Insurance" + }, + "IAB13-7": { + "id": "393", + "name": "Business Services" + }, + "IAB13-8": { + "id": "393", + "name": "Business Services" + }, + "IAB13-9": { + "id": "393", + "name": "Business Services" + }, + "IAB13-10": { + "id": "393", + "name": "Business Services" + }, + "IAB13-11": { + "id": "393", + "name": "Business Services" + }, + "IAB13-12": { + "id": "393", + "name": "Business Services" + }, + "IAB16-1": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB16-2": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB16-3": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB16-4": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB16-5": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB16-6": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB16-7": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB17-1": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-2": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-3": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-4": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-5": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-6": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-7": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-8": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-9": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-10": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-11": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-12": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-13": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-14": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-15": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-16": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-17": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-18": { + "id": "412", + "name": "Household Products" + }, + "IAB17-19": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-20": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-21": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-22": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-23": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-24": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-25": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-26": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-27": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-28": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-29": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-30": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-31": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-32": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-33": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-34": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-35": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-36": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-37": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-38": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-39": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-40": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-41": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-42": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-43": { + "id": "425", + "name": "Professional Sports" + }, + "IAB17-44": { + "id": "425", + "name": "Professional Sports" + }, + "IAB18-1": { + "id": "411", + "name": "Cosmetics/Toiletries" + }, + "IAB18-2": { + "id": "397", + "name": "Apparel" + }, + "IAB18-3": { + "id": "397", + "name": "Apparel" + }, + "IAB18-4": { + "id": "418", + "name": "Jewelry" + }, + "IAB18-5": { + "id": "397", + "name": "Apparel" + }, + "IAB18-6": { + "id": "397", + "name": "Apparel" + }, + "IAB19-2": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-3": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-4": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-5": { + "id": "424", + "name": "Photographic Equipment" + }, + "IAB19-6": { + "id": "417", + "name": "Telecommunications" + }, + "IAB19-7": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-8": { + "id": "432", + "name": "Audio and Video Equipment" + }, + "IAB19-9": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-10": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-11": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-12": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-13": { + "id": "404", + "name": "Publishing" + }, + "IAB19-14": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-15": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-16": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-17": { + "id": "419", + "name": "Filmed Entertainment" + }, + "IAB19-18": { + "id": "431", + "name": "Computing" + }, + "IAB19-19": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-20": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-21": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-22": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-23": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-24": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-25": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-26": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-27": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-28": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-29": { + "id": "392", + "name": "Entertainment" + }, + "IAB19-30": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-31": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-32": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-33": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-34": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-35": { + "id": "409", + "name": "Computing Product" + }, + "IAB19-36": { + "id": "409", + "name": "Computing Product" + }, + "IAB20-1": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-2": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-3": { + "id": "428", + "name": "Aerospace" + }, + "IAB20-4": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-5": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-6": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-7": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-8": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-9": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-10": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-11": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-12": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-13": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-14": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-15": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-16": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-17": { + "id": "396", + "name": "Amusement and Recreation" + }, + "IAB20-18": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-19": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-20": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-21": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-22": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-23": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-24": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-25": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-26": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB20-27": { + "id": "395", + "name": "Travel/Hotels/Airlines" + }, + "IAB21-1": { + "id": "416", + "name": "Real Estate" + }, + "IAB21-2": { + "id": "416", + "name": "Real Estate" + }, + "IAB21-3": { + "id": "416", + "name": "Real Estate" + }, + "IAB22-1": { + "id": "403", + "name": "Retail Stores/Chains" + }, + "IAB22-2": { + "id": "403", + "name": "Retail Stores/Chains" + }, + "IAB22-3": { + "id": "410", + "name": "Product" + } } \ No newline at end of file diff --git a/stored_requests/backends/db_fetcher/fetcher.go b/stored_requests/backends/db_fetcher/fetcher.go index 07bd1d6d0bf..33009e2dc73 100644 --- a/stored_requests/backends/db_fetcher/fetcher.go +++ b/stored_requests/backends/db_fetcher/fetcher.go @@ -7,8 +7,8 @@ import ( "github.com/lib/pq" - "github.com/golang/glog" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/golang/glog" ) func NewFetcher(db *sql.DB, queryMaker func(int, int) string) stored_requests.AllFetcher { @@ -113,7 +113,7 @@ func appendErrors(dataType string, ids []string, data map[string]json.RawMessage // // These errors are documented here: https://www.postgresql.org/docs/9.3/static/errcodes-appendix.html func isBadInput(err error) bool { - // Unfortunately, Postgres queries will fail if a non-UUID is passedd into a query for a UUID column. For example: + // Unfortunately, Postgres queries will fail if a non-UUID is passed into a query for a UUID column. For example: // // SELECT uuid, data, dataType FROM stored_requests WHERE uuid IN ('abc'); // diff --git a/stored_requests/caches/memory/cache.go b/stored_requests/caches/memory/cache.go index 1edc6e58413..4262ea21021 100644 --- a/stored_requests/caches/memory/cache.go +++ b/stored_requests/caches/memory/cache.go @@ -5,10 +5,10 @@ import ( "encoding/json" "sync" - "github.com/coocood/freecache" - "github.com/golang/glog" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/coocood/freecache" + "github.com/golang/glog" ) // NewCache returns an in-memory Cache which evicts items if: diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go index f0935256f85..2d979e4cd35 100644 --- a/stored_requests/config/config.go +++ b/stored_requests/config/config.go @@ -8,8 +8,6 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" - "github.com/golang/glog" - "github.com/julienschmidt/httprouter" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/db_fetcher" @@ -22,6 +20,8 @@ import ( apiEvents "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events/api" httpEvents "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events/http" postgresEvents "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events/postgres" + "github.com/golang/glog" + "github.com/julienschmidt/httprouter" ) // This gets set to the connection string used when a database connection is made. We only support a single diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go index bce6056bed2..e40c0fea733 100644 --- a/stored_requests/config/config_test.go +++ b/stored_requests/config/config_test.go @@ -10,12 +10,12 @@ import ( "testing" sqlmock "github.com/DATA-DOG/go-sqlmock" - "github.com/julienschmidt/httprouter" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/empty_fetcher" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/http_fetcher" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" httpEvents "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events/http" + "github.com/julienschmidt/httprouter" ) func TestNewEmptyFetcher(t *testing.T) { diff --git a/stored_requests/events/api/api.go b/stored_requests/events/api/api.go index a37fadd36b2..8fb6f6be9eb 100644 --- a/stored_requests/events/api/api.go +++ b/stored_requests/events/api/api.go @@ -5,8 +5,8 @@ import ( "io/ioutil" "net/http" - "github.com/julienschmidt/httprouter" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" + "github.com/julienschmidt/httprouter" ) type eventsAPI struct { diff --git a/stored_requests/events/events_test.go b/stored_requests/events/events_test.go index eba0683de51..84bbc1c6b13 100644 --- a/stored_requests/events/events_test.go +++ b/stored_requests/events/events_test.go @@ -23,7 +23,7 @@ func TestListen(t *testing.T) { TTL: -1, }) - // create channels to syncronize + // create channels to synchronize saveOccurred := make(chan struct{}) invalidateOccurred := make(chan struct{}) listener := NewEventListener( diff --git a/stored_requests/events/http/http.go b/stored_requests/events/http/http.go index 1139b00bbfb..a9f26d0c9d2 100644 --- a/stored_requests/events/http/http.go +++ b/stored_requests/events/http/http.go @@ -11,8 +11,8 @@ import ( "golang.org/x/net/context/ctxhttp" - "github.com/buger/jsonparser" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" + "github.com/buger/jsonparser" "github.com/golang/glog" ) @@ -142,7 +142,7 @@ func (e *HTTPEvents) refresh(ticker <-chan time.Time) { } } -// proceess unpacks the HTTP response and sends the relevant events to the channels. +// parse unpacks the HTTP response and sends the relevant events to the channels. // It returns true if everything was successful, and false if any errors occurred. func (e *HTTPEvents) parse(endpoint string, resp *httpCore.Response, err error) (*responseContract, bool) { if err != nil { diff --git a/stored_requests/events/postgres/polling.go b/stored_requests/events/postgres/polling.go index 3bfecfb41a6..f6d388ead70 100644 --- a/stored_requests/events/postgres/polling.go +++ b/stored_requests/events/postgres/polling.go @@ -7,8 +7,8 @@ import ( "encoding/json" "time" - "github.com/golang/glog" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" + "github.com/golang/glog" ) // PollForUpdates returns an EventProducer which checks the database for updates every refreshRate. diff --git a/stored_requests/events/postgres/startup.go b/stored_requests/events/postgres/startup.go index 620bca645bf..c65d117e78b 100644 --- a/stored_requests/events/postgres/startup.go +++ b/stored_requests/events/postgres/startup.go @@ -4,8 +4,8 @@ import ( "context" "database/sql" - "github.com/golang/glog" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" + "github.com/golang/glog" ) // This function queries the database to get all the data, and is guaranteed to return diff --git a/stored_requests/fetcher.go b/stored_requests/fetcher.go index d0f7f5969ec..d3dc44bb65b 100644 --- a/stored_requests/fetcher.go +++ b/stored_requests/fetcher.go @@ -30,7 +30,7 @@ type CategoryFetcher interface { FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) } -// AllFetcher is an iterface that encapsulates both the original Fetcher and the CategoryFetcher +// AllFetcher is an interface that encapsulates both the original Fetcher and the CategoryFetcher type AllFetcher interface { FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go old mode 100644 new mode 100755 index 14b52db7924..aaead65de33 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -1,26 +1,31 @@ package usersyncers import ( - "github.com/PubMatic-OpenWrap/prebid-server/adapters/telaria" "strings" "text/template" - "github.com/PubMatic-OpenWrap/prebid-server/adapters/adpone" - - "github.com/golang/glog" ttx "github.com/PubMatic-OpenWrap/prebid-server/adapters/33across" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adform" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adkernel" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adkernelAdn" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/admixer" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adocean" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adpone" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/adtarget" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adtelligent" "github.com/PubMatic-OpenWrap/prebid-server/adapters/advangelists" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/aja" "github.com/PubMatic-OpenWrap/prebid-server/adapters/appnexus" "github.com/PubMatic-OpenWrap/prebid-server/adapters/audienceNetwork" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/avocet" "github.com/PubMatic-OpenWrap/prebid-server/adapters/beachfront" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/beintoo" "github.com/PubMatic-OpenWrap/prebid-server/adapters/brightroll" "github.com/PubMatic-OpenWrap/prebid-server/adapters/consumable" "github.com/PubMatic-OpenWrap/prebid-server/adapters/conversant" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/cpmstar" "github.com/PubMatic-OpenWrap/prebid-server/adapters/datablocks" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/dmx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/emx_digital" "github.com/PubMatic-OpenWrap/prebid-server/adapters/engagebdr" "github.com/PubMatic-OpenWrap/prebid-server/adapters/eplanning" @@ -32,8 +37,11 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters/ix" "github.com/PubMatic-OpenWrap/prebid-server/adapters/lifestreet" "github.com/PubMatic-OpenWrap/prebid-server/adapters/lockerdome" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/lunamedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/marsmedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/mgid" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/nanointeractive" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/ninthdecimal" "github.com/PubMatic-OpenWrap/prebid-server/adapters/openx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pubmatic" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pulsepoint" @@ -41,20 +49,28 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters/rtbhouse" "github.com/PubMatic-OpenWrap/prebid-server/adapters/rubicon" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sharethrough" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/smartrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters/somoaudience" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sonobi" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sovrn" "github.com/PubMatic-OpenWrap/prebid-server/adapters/synacormedia" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/telaria" "github.com/PubMatic-OpenWrap/prebid-server/adapters/triplelift" "github.com/PubMatic-OpenWrap/prebid-server/adapters/triplelift_native" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/ucfunnel" "github.com/PubMatic-OpenWrap/prebid-server/adapters/unruly" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/valueimpression" "github.com/PubMatic-OpenWrap/prebid-server/adapters/verizonmedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/visx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/vrtcal" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/yieldlab" "github.com/PubMatic-OpenWrap/prebid-server/adapters/yieldmo" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/yieldone" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/zeroclickfraud" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/usersync" + "github.com/golang/glog" ) // NewSyncerMap returns a map of all the usersyncer objects. @@ -67,15 +83,23 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderAdform, adform.NewAdformSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdkernel, adkernel.NewAdkernelSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdkernelAdn, adkernelAdn.NewAdkernelAdnSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderAdmixer, admixer.NewAdmixerSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderAdOcean, adocean.NewAdOceanSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdpone, adpone.NewadponeSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderAdtarget, adtarget.NewAdtargetSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdtelligent, adtelligent.NewAdtelligentSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdvangelists, advangelists.NewAdvangelistsSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderAJA, aja.NewAJASyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAppnexus, appnexus.NewAppnexusSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderAvocet, avocet.NewAvocetSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderBeachfront, beachfront.NewBeachfrontSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderBeintoo, beintoo.NewBeintooSyncer) 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.BidderCpmstar, cpmstar.NewCpmstarSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderDatablocks, datablocks.NewDatablocksSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderDmx, dmx.NewDmxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEmxDigital, emx_digital.NewEMXDigitalSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEngageBDR, engagebdr.NewEngageBDRSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEPlanning, eplanning.NewEPlanningSyncer) @@ -88,8 +112,11 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderIx, ix.NewIxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderLifestreet, lifestreet.NewLifestreetSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderLockerDome, lockerdome.NewLockerDomeSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderLunaMedia, lunamedia.NewLunaMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderMarsmedia, marsmedia.NewMarsmediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderMgid, mgid.NewMgidSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderNanoInteractive, nanointeractive.NewNanoInteractiveSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderNinthDecimal, ninthdecimal.NewNinthDecimalSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderOpenx, openx.NewOpenxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderPubmatic, pubmatic.NewPubmaticSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderPulsepoint, pulsepoint.NewPulsepointSyncer) @@ -100,15 +127,21 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderSomoaudience, somoaudience.NewSomoaudienceSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderSonobi, sonobi.NewSonobiSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderSovrn, sovrn.NewSovrnSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderSmartRTB, smartrtb.NewSmartRTBSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderSynacormedia, synacormedia.NewSynacorMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderTelaria, telaria.NewTelariaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderTriplelift, triplelift.NewTripleliftSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderTripleliftNative, triplelift_native.NewTripleliftSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderUcfunnel, ucfunnel.NewUcfunnelSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderUnruly, unruly.NewUnrulySyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderValueImpression, valueimpression.NewValueImpressionSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderVerizonMedia, verizonmedia.NewVerizonMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderVisx, visx.NewVisxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderVrtcal, vrtcal.NewVrtcalSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderYieldlab, yieldlab.NewYieldlabSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderYieldmo, yieldmo.NewYieldmoSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderYieldone, yieldone.NewYieldoneSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderZeroClickFraud, zeroclickfraud.NewZeroClickFraudSyncer) return syncers } diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go old mode 100644 new mode 100755 index e5fc1099938..0bc2f6a458d --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -18,15 +18,23 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderAdform): syncConfig, string(openrtb_ext.BidderAdkernel): syncConfig, string(openrtb_ext.BidderAdkernelAdn): syncConfig, + string(openrtb_ext.BidderAdmixer): syncConfig, + string(openrtb_ext.BidderAdOcean): syncConfig, string(openrtb_ext.BidderAdpone): syncConfig, + string(openrtb_ext.BidderAdtarget): syncConfig, string(openrtb_ext.BidderAdtelligent): syncConfig, string(openrtb_ext.BidderAdvangelists): syncConfig, + string(openrtb_ext.BidderAJA): syncConfig, string(openrtb_ext.BidderAppnexus): syncConfig, + string(openrtb_ext.BidderAvocet): syncConfig, string(openrtb_ext.BidderBeachfront): syncConfig, + string(openrtb_ext.BidderBeintoo): syncConfig, string(openrtb_ext.BidderBrightroll): syncConfig, string(openrtb_ext.BidderConsumable): syncConfig, string(openrtb_ext.BidderConversant): syncConfig, + string(openrtb_ext.BidderCpmstar): syncConfig, string(openrtb_ext.BidderDatablocks): syncConfig, + string(openrtb_ext.BidderDmx): syncConfig, string(openrtb_ext.BidderEmxDigital): syncConfig, string(openrtb_ext.BidderEngageBDR): syncConfig, string(openrtb_ext.BidderEPlanning): syncConfig, @@ -39,8 +47,11 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderIx): syncConfig, string(openrtb_ext.BidderLifestreet): syncConfig, string(openrtb_ext.BidderLockerDome): syncConfig, + string(openrtb_ext.BidderLunaMedia): syncConfig, string(openrtb_ext.BidderMarsmedia): syncConfig, string(openrtb_ext.BidderMgid): syncConfig, + string(openrtb_ext.BidderNanoInteractive): syncConfig, + string(openrtb_ext.BidderNinthDecimal): syncConfig, string(openrtb_ext.BidderOpenx): syncConfig, string(openrtb_ext.BidderPubmatic): syncConfig, string(openrtb_ext.BidderPulsepoint): syncConfig, @@ -51,24 +62,37 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderSomoaudience): syncConfig, string(openrtb_ext.BidderSonobi): syncConfig, string(openrtb_ext.BidderSovrn): syncConfig, + string(openrtb_ext.BidderSmartRTB): syncConfig, string(openrtb_ext.BidderSynacormedia): syncConfig, string(openrtb_ext.BidderTelaria): syncConfig, string(openrtb_ext.BidderTriplelift): syncConfig, string(openrtb_ext.BidderTripleliftNative): syncConfig, + string(openrtb_ext.BidderUcfunnel): syncConfig, string(openrtb_ext.BidderUnruly): syncConfig, + string(openrtb_ext.BidderValueImpression): syncConfig, + string(openrtb_ext.BidderYieldlab): syncConfig, string(openrtb_ext.BidderVerizonMedia): syncConfig, string(openrtb_ext.BidderVisx): syncConfig, string(openrtb_ext.BidderVrtcal): syncConfig, string(openrtb_ext.BidderYieldmo): syncConfig, + string(openrtb_ext.BidderYieldone): syncConfig, + string(openrtb_ext.BidderZeroClickFraud): syncConfig, }, } adaptersWithoutSyncers := map[openrtb_ext.BidderName]bool{ - openrtb_ext.BidderApplogy: true, - openrtb_ext.BidderTappx: true, - openrtb_ext.BidderKubient: true, - openrtb_ext.BidderPubnative: true, - openrtb_ext.BidderSpotX: true, + openrtb_ext.BidderAdgeneration: true, + openrtb_ext.BidderAdhese: true, + openrtb_ext.BidderAdoppler: true, + openrtb_ext.BidderApplogy: true, + openrtb_ext.BidderKidoz: true, + openrtb_ext.BidderKubient: true, + openrtb_ext.BidderMobileFuse: true, + openrtb_ext.BidderOrbidder: true, + openrtb_ext.BidderPubnative: true, + openrtb_ext.BidderSpotX: true, + openrtb_ext.BidderTappx: true, + openrtb_ext.BidderYeahmobi: true, } for bidder, config := range cfg.Adapters { diff --git a/validate.sh b/validate.sh index b5210550393..b81ade344d2 100755 --- a/validate.sh +++ b/validate.sh @@ -27,11 +27,11 @@ GOGLOB="${GOGLOB/ docs/}" GOGLOB="${GOGLOB/ vendor/}" # Check that there are no formatting issues -GOFMT_LINES=`gofmt -s -l $GOGLOB | wc -l | xargs` +GOFMT_LINES=`gofmt -s -l $GOGLOB | tr '\\\\' '/' | wc -l | xargs` if $AUTOFMT; then # if there are files with formatting issues, they will be automatically corrected using the gofmt -w command if [[ $GOFMT_LINES -ne 0 ]]; then - FMT_FILES=`gofmt -s -l $GOGLOB | xargs` + FMT_FILES=`gofmt -s -l $GOGLOB | tr '\\\\' '/' | xargs` for FILE in $FMT_FILES; do echo "Running: gofmt -s -w $FILE" `gofmt -s -w $FILE`