From 148b1f1708913490752de798ca640b427e471ea2 Mon Sep 17 00:00:00 2001 From: Winston-Yieldmo <46379634+Winston-Yieldmo@users.noreply.github.com> Date: Wed, 15 Jan 2020 15:12:30 -0500 Subject: [PATCH 001/381] adding yieldmo vendor id to usersync (#1166) --- adapters/yieldmo/usersync.go | 2 +- adapters/yieldmo/usersync_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/yieldmo/usersync.go b/adapters/yieldmo/usersync.go index f853bbb86a5..041e7e8f073 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 5ae437c8f00..598710ec742 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) } From 7d9b1ece872c8fb1a339ebba99f0878ac121de16 Mon Sep 17 00:00:00 2001 From: evanmsmrtb Date: Thu, 16 Jan 2020 10:13:54 -0600 Subject: [PATCH 002/381] Add SmartRTB adapter (#1071) --- adapters/smartrtb/smartrtb.go | 189 ++++++++++++++++++ adapters/smartrtb/smartrtb_test.go | 11 + .../smartrtbtest/exemplary/banner.json | 134 +++++++++++++ .../smartrtbtest/exemplary/video.json | 134 +++++++++++++ .../smartrtbtest/params/race/banner.json | 5 + .../smartrtbtest/params/race/video.json | 5 + .../supplemental/bad-bidder-ext.json | 31 +++ .../supplemental/bad-imp-ext.json | 32 +++ .../supplemental/bad-pub-value-empty.json | 37 ++++ .../supplemental/bad-pub-value.json | 37 ++++ .../supplemental/bad-request.json | 70 +++++++ .../smartrtbtest/supplemental/empty-imps.json | 14 ++ .../supplemental/invalid-bid-ext.json | 93 +++++++++ .../supplemental/invalid-bid-format.json | 95 +++++++++ .../supplemental/invalid-bid-json.json | 76 +++++++ .../supplemental/invalid-imp-ext.json | 32 +++ .../smartrtbtest/supplemental/nobid.json | 69 +++++++ .../supplemental/non-http-ok.json | 76 +++++++ adapters/smartrtb/usersync.go | 12 ++ adapters/smartrtb/usersync_test.go | 20 ++ config/config.go | 2 + docs/bidders/smartrtb.md | 39 ++++ exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_smartrtb.go | 8 + static/bidder-info/smartrtb.yaml | 11 + static/bidder-params/smartrtb.json | 27 +++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 29 files changed, 1266 insertions(+) create mode 100644 adapters/smartrtb/smartrtb.go create mode 100644 adapters/smartrtb/smartrtb_test.go create mode 100644 adapters/smartrtb/smartrtbtest/exemplary/banner.json create mode 100644 adapters/smartrtb/smartrtbtest/exemplary/video.json create mode 100644 adapters/smartrtb/smartrtbtest/params/race/banner.json create mode 100644 adapters/smartrtb/smartrtbtest/params/race/video.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/bad-bidder-ext.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/bad-imp-ext.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value-empty.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/bad-request.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/empty-imps.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-ext.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-format.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-json.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/invalid-imp-ext.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/nobid.json create mode 100644 adapters/smartrtb/smartrtbtest/supplemental/non-http-ok.json create mode 100644 adapters/smartrtb/usersync.go create mode 100644 adapters/smartrtb/usersync_test.go create mode 100644 docs/bidders/smartrtb.md create mode 100644 openrtb_ext/imp_smartrtb.go create mode 100644 static/bidder-info/smartrtb.yaml create mode 100644 static/bidder-params/smartrtb.json diff --git a/adapters/smartrtb/smartrtb.go b/adapters/smartrtb/smartrtb.go new file mode 100644 index 00000000000..5edaae6f289 --- /dev/null +++ b/adapters/smartrtb/smartrtb.go @@ -0,0 +1,189 @@ +package smartrtb + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/golang/glog" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// Base adapter structure. +type SmartRTBAdapter struct { + EndpointTemplate template.Template +} + +// Bid request extension appended to downstream request. +// PubID are non-empty iff request.{App,Site} or +// request.{App,Site}.Publisher are nil, respectively. +type bidRequestExt struct { + PubID string `json:"pub_id,omitempty"` + ZoneID string `json:"zone_id,omitempty"` + ForceBid bool `json:"force_bid,omitempty"` +} + +// bidExt.CreativeType values. +const ( + creativeTypeBanner string = "BANNER" + creativeTypeVideo = "VIDEO" + creativeTypeNative = "NATIVE" + creativeTypeAudio = "AUDIO" +) + +// Bid response extension from downstream. +type bidExt struct { + CreativeType string `json:"format"` +} + +func NewSmartRTBBidder(endpointTemplate string) adapters.Bidder { + template, err := template.New("endpointTemplate").Parse(endpointTemplate) + if err != nil { + glog.Fatal("Template URL error") + return nil + } + return &SmartRTBAdapter{EndpointTemplate: *template} +} + +func (adapter *SmartRTBAdapter) buildEndpointURL(pubID string) (string, error) { + endpointParams := macros.EndpointTemplateParams{PublisherID: pubID} + return macros.ResolveMacros(adapter.EndpointTemplate, endpointParams) +} + +func parseExtImp(dst *bidRequestExt, imp *openrtb.Imp) error { + var ext adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &ext); err != nil { + return adapters.BadInput(err.Error()) + } + + var src openrtb_ext.ExtImpSmartRTB + if err := json.Unmarshal(ext.Bidder, &src); err != nil { + return adapters.BadInput(err.Error()) + } + + if dst.PubID == "" { + dst.PubID = src.PubID + } + + if src.ZoneID != "" { + imp.TagID = src.ZoneID + } + return nil +} + +func (s *SmartRTBAdapter) MakeRequests(brq *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var imps []openrtb.Imp + var err error + ext := bidRequestExt{} + nrImps := len(brq.Imp) + errs := make([]error, 0, nrImps) + + for i := 0; i < nrImps; i++ { + imp := brq.Imp[i] + if imp.Banner == nil && imp.Video == nil { + continue + } + + err = parseExtImp(&ext, &imp) + if err != nil { + errs = append(errs, err) + continue + } + + imps = append(imps, imp) + } + + if len(imps) == 0 { + return nil, errs + } + + if ext.PubID == "" { + return nil, append(errs, adapters.BadInput("Cannot infer publisher ID from bid ext")) + } + + brq.Ext, err = json.Marshal(ext) + if err != nil { + return nil, append(errs, err) + } + + brq.Imp = imps + + rq, err := json.Marshal(brq) + if err != nil { + return nil, append(errs, err) + } + + url, err := s.buildEndpointURL(ext.PubID) + if err != nil { + return nil, append(errs, err) + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("x-openrtb-version", "2.5") + return []*adapters.RequestData{{ + Method: "POST", + Uri: url, + Body: rq, + Headers: headers, + }}, errs +} + +func (s *SmartRTBAdapter) MakeBids( + brq *openrtb.BidRequest, drq *adapters.RequestData, + rs *adapters.ResponseData, +) (*adapters.BidderResponse, []error) { + if rs.StatusCode == http.StatusNoContent { + return nil, nil + } else if rs.StatusCode == http.StatusBadRequest { + return nil, []error{adapters.BadInput("Invalid request.")} + } else if rs.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected HTTP status %d.", rs.StatusCode), + }} + } + + var brs openrtb.BidResponse + if err := json.Unmarshal(rs.Body, &brs); err != nil { + return nil, []error{err} + } + + rv := adapters.NewBidderResponseWithBidsCapacity(5) + for _, seat := range brs.SeatBid { + for i := range seat.Bid { + var ext bidExt + if err := json.Unmarshal(seat.Bid[i].Ext, &ext); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Invalid bid extension from endpoint.", + }} + } + + var btype openrtb_ext.BidType + switch ext.CreativeType { + case creativeTypeBanner: + btype = openrtb_ext.BidTypeBanner + case creativeTypeVideo: + btype = openrtb_ext.BidTypeVideo + default: + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unsupported creative type %s.", + ext.CreativeType), + }} + } + + seat.Bid[i].Ext = nil + + rv.Bids = append(rv.Bids, &adapters.TypedBid{ + Bid: &seat.Bid[i], + BidType: btype, + }) + } + } + return rv, nil +} diff --git a/adapters/smartrtb/smartrtb_test.go b/adapters/smartrtb/smartrtb_test.go new file mode 100644 index 00000000000..3f76ed044a8 --- /dev/null +++ b/adapters/smartrtb/smartrtb_test.go @@ -0,0 +1,11 @@ +package smartrtb + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "smartrtbtest", NewSmartRTBBidder("http://market-east.smrtb.com/json/publisher/rtb?pubid=test")) +} diff --git a/adapters/smartrtb/smartrtbtest/exemplary/banner.json b/adapters/smartrtb/smartrtbtest/exemplary/banner.json new file mode 100644 index 00000000000..436f6298a16 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/exemplary/banner.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp123", + "tagid": "N4zTDq3PPEHBIODv7cXK", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "abc", + "seatbid": [ + { + "bid": [ + { + "adm": "hi", + "crid": "test_banner_crid", + "cid": "test_cid", + "impid": "imp123", + "id": "1", + "price": 0.01, + "ext": { + "format": "BANNER" + } + } + ] + }, + { + "bid": [ + { + "adm": "", + "crid": "test_video_crid", + "cid": "test_cid", + "impid": "imp123", + "id": "2", + "price": 0.01, + "ext": { + "format": "VIDEO" + } + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "hi", + "crid": "test_banner_crid", + "cid": "test_cid", + "impid": "imp123", + "price": 0.01, + "id": "1" + }, + "type": "banner" + }, + { + "bid": { + "adm": "", + "crid": "test_video_crid", + "cid": "test_cid", + "impid": "imp123", + "price": 0.01, + "id": "2" + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/exemplary/video.json b/adapters/smartrtb/smartrtbtest/exemplary/video.json new file mode 100644 index 00000000000..436f6298a16 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/exemplary/video.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp123", + "tagid": "N4zTDq3PPEHBIODv7cXK", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "abc", + "seatbid": [ + { + "bid": [ + { + "adm": "hi", + "crid": "test_banner_crid", + "cid": "test_cid", + "impid": "imp123", + "id": "1", + "price": 0.01, + "ext": { + "format": "BANNER" + } + } + ] + }, + { + "bid": [ + { + "adm": "", + "crid": "test_video_crid", + "cid": "test_cid", + "impid": "imp123", + "id": "2", + "price": 0.01, + "ext": { + "format": "VIDEO" + } + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "hi", + "crid": "test_banner_crid", + "cid": "test_cid", + "impid": "imp123", + "price": 0.01, + "id": "1" + }, + "type": "banner" + }, + { + "bid": { + "adm": "", + "crid": "test_video_crid", + "cid": "test_cid", + "impid": "imp123", + "price": 0.01, + "id": "2" + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/params/race/banner.json b/adapters/smartrtb/smartrtbtest/params/race/banner.json new file mode 100644 index 00000000000..207a504539f --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/params/race/banner.json @@ -0,0 +1,5 @@ +{ + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true +} \ No newline at end of file diff --git a/adapters/smartrtb/smartrtbtest/params/race/video.json b/adapters/smartrtb/smartrtbtest/params/race/video.json new file mode 100644 index 00000000000..207a504539f --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/params/race/video.json @@ -0,0 +1,5 @@ +{ + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true +} \ No newline at end of file diff --git a/adapters/smartrtb/smartrtbtest/supplemental/bad-bidder-ext.json b/adapters/smartrtb/smartrtbtest/supplemental/bad-bidder-ext.json new file mode 100644 index 00000000000..b261415de4d --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/bad-bidder-ext.json @@ -0,0 +1,31 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": null + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/bad-imp-ext.json b/adapters/smartrtb/smartrtbtest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..1c0f57d2f34 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/bad-imp-ext.json @@ -0,0 +1,32 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "publisher": { }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": null + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value-empty.json b/adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value-empty.json new file mode 100644 index 00000000000..aca21036b24 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value-empty.json @@ -0,0 +1,37 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "publisher": { }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Cannot infer publisher ID from bid ext", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value.json b/adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value.json new file mode 100644 index 00000000000..93b45c747fd --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/bad-pub-value.json @@ -0,0 +1,37 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": 0, + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal number into Go struct field ExtImpSmartRTB.pub_id of type string", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/bad-request.json b/adapters/smartrtb/smartrtbtest/supplemental/bad-request.json new file mode 100644 index 00000000000..cf03832ddff --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/bad-request.json @@ -0,0 +1,70 @@ +{ + "mockBidRequest": { + "id": "abc", + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "fake", + "force_bid": false + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "imp": [{ + "id": "imp123", + "tagid": "fake", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "fake", + "force_bid": false + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 400 + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Invalid request.", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/empty-imps.json b/adapters/smartrtb/smartrtbtest/supplemental/empty-imps.json new file mode 100644 index 00000000000..a92add70825 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/empty-imps.json @@ -0,0 +1,14 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "publisher": { }, + "imp": [ + { + "id": "imp123" + } + ] + } +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-ext.json b/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-ext.json new file mode 100644 index 00000000000..49527e1ecd4 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-ext.json @@ -0,0 +1,93 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp123", + "tagid": "N4zTDq3PPEHBIODv7cXK", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "abc", + "seatbid": [ + { + "bid": [ + { + "adm": "hi", + "crid": "test_banner_crid", + "cid": "test_cid", + "impid": "imp123", + "id": "1", + "price": 0.01, + "ext": "notvalidjsonhaha" + } + ] + } + ] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Invalid bid extension from endpoint.", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-format.json b/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-format.json new file mode 100644 index 00000000000..2f6bc07edb8 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-format.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp123", + "tagid": "N4zTDq3PPEHBIODv7cXK", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "abc", + "seatbid": [ + { + "bid": [ + { + "adm": "hi", + "crid": "test_banner_crid", + "cid": "test_cid", + "impid": "imp123", + "id": "1", + "price": 0.01, + "ext": { + "format": "ALIEN_FORMAT" + } + } + ] + } + ] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unsupported creative type ALIEN_FORMAT.", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-json.json b/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-json.json new file mode 100644 index 00000000000..c56e7f21515 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/invalid-bid-json.json @@ -0,0 +1,76 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp123", + "tagid": "N4zTDq3PPEHBIODv7cXK", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXK", + "force_bid": true + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": "imnotyourfather" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/invalid-imp-ext.json b/adapters/smartrtb/smartrtbtest/supplemental/invalid-imp-ext.json new file mode 100644 index 00000000000..13485f797ba --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/invalid-imp-ext.json @@ -0,0 +1,32 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "publisher": { }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": "notjsontho" + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/nobid.json b/adapters/smartrtb/smartrtbtest/supplemental/nobid.json new file mode 100644 index 00000000000..2733d8dba96 --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/nobid.json @@ -0,0 +1,69 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXKz", + "force_bid": false + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp123", + "tagid": "N4zTDq3PPEHBIODv7cXKz", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXKz", + "force_bid": false + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/smartrtb/smartrtbtest/supplemental/non-http-ok.json b/adapters/smartrtb/smartrtbtest/supplemental/non-http-ok.json new file mode 100644 index 00000000000..3acafadc62f --- /dev/null +++ b/adapters/smartrtb/smartrtbtest/supplemental/non-http-ok.json @@ -0,0 +1,76 @@ +{ + "mockBidRequest": { + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "imp123", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXKz", + "force_bid": false + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://market-east.smrtb.com/json/publisher/rtb?pubid=test", + "body":{ + "id": "abc", + "site": { + "page": "prebid.org" + }, + "imp": [{ + "id": "imp123", + "tagid": "N4zTDq3PPEHBIODv7cXKz", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "pub_id": "test", + "zone_id": "N4zTDq3PPEHBIODv7cXKz", + "force_bid": false + } + } + }], + "ext": { + "pub_id": "test" + } + } + }, + "mockResponse": { + "status": 500 + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Unexpected HTTP status 500.", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartrtb/usersync.go b/adapters/smartrtb/usersync.go new file mode 100644 index 00000000000..2f7b1dc3339 --- /dev/null +++ b/adapters/smartrtb/usersync.go @@ -0,0 +1,12 @@ +package smartrtb + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewSmartRTBSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("smartrtb", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/smartrtb/usersync_test.go b/adapters/smartrtb/usersync_test.go new file mode 100644 index 00000000000..ae3ae5dc007 --- /dev/null +++ b/adapters/smartrtb/usersync_test.go @@ -0,0 +1,20 @@ +package smartrtb + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/stretchr/testify/assert" +) + +func TestSmartRTBSyncer(t *testing.T) { + temp := template.Must(template.New("sync-template").Parse("http://market-east.smrtb.com/sync/all?nid=smartrtb&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&url=localhost%2Fsetuid%3Fbidder%smartrtb%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")) + syncer := NewSmartRTBSyncer(temp) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{}) + assert.NoError(t, err) + assert.Equal(t, "http://market-east.smrtb.com/sync/all?nid=smartrtb&gdpr=&gdpr_consent=&url=localhost%2Fsetuid%3Fbidder%smartrtb%26gdpr%3D%26gdpr_consent%3D%26uid%3D%24%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/config/config.go b/config/config.go index 722aae1395c..8dc0bb14526 100644 --- a/config/config.go +++ b/config/config.go @@ -518,6 +518,7 @@ func (cfg *Configuration) setDerivedDefaults() { // 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%252Fsetuid%253Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsonobi%26consent_string%3D{{.GDPR}}%26gdpr%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSovrn, "https://ap.lijit.com/pixel?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsovrn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -700,6 +701,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") 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/exchange/adapter_map.go b/exchange/adapter_map.go index 30a31727d6b..5795ad2c197 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -44,6 +44,7 @@ import ( "github.com/prebid/prebid-server/adapters/rtbhouse" "github.com/prebid/prebid-server/adapters/rubicon" "github.com/prebid/prebid-server/adapters/sharethrough" + "github.com/prebid/prebid-server/adapters/smartrtb" "github.com/prebid/prebid-server/adapters/somoaudience" "github.com/prebid/prebid-server/adapters/sonobi" "github.com/prebid/prebid-server/adapters/sovrn" @@ -108,6 +109,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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 9621b23dc81..f02a16b4350 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -57,6 +57,7 @@ const ( BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSharethrough BidderName = "sharethrough" + BidderSmartRTB BidderName = "smartrtb" BidderSomoaudience BidderName = "somoaudience" BidderSonobi BidderName = "sonobi" BidderSovrn BidderName = "sovrn" @@ -110,6 +111,7 @@ var BidderMap = map[string]BidderName{ "rtbhouse": BidderRTBHouse, "rubicon": BidderRubicon, "sharethrough": BidderSharethrough, + "smartrtb": BidderSmartRTB, "somoaudience": BidderSomoaudience, "sonobi": BidderSonobi, "sovrn": BidderSovrn, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 6277993238a..2bfa5514063 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -40,6 +40,7 @@ import ( "github.com/prebid/prebid-server/adapters/rtbhouse" "github.com/prebid/prebid-server/adapters/rubicon" "github.com/prebid/prebid-server/adapters/sharethrough" + "github.com/prebid/prebid-server/adapters/smartrtb" "github.com/prebid/prebid-server/adapters/somoaudience" "github.com/prebid/prebid-server/adapters/sonobi" "github.com/prebid/prebid-server/adapters/sovrn" @@ -99,6 +100,7 @@ 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.BidderTriplelift, triplelift.NewTripleliftSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderTripleliftNative, triplelift_native.NewTripleliftSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index dd2b5b5d1e9..3fb1d4d7fa4 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -51,6 +51,7 @@ 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.BidderTriplelift): syncConfig, string(openrtb_ext.BidderTripleliftNative): syncConfig, From bd43afbca80114e76d1b14a2843b71f8a5f62894 Mon Sep 17 00:00:00 2001 From: CPMStar Date: Thu, 16 Jan 2020 08:14:14 -0800 Subject: [PATCH 003/381] Added new adapter for CPMStar ad network banners and video (#1159) --- adapters/cpmstar/cpmstar.go | 161 ++++++++++++++++++ adapters/cpmstar/cpmstar_test.go | 11 ++ .../exemplary/banner-and-video.json | 154 +++++++++++++++++ .../cpmstar/cpmstartest/exemplary/banner.json | 100 +++++++++++ .../cpmstar/cpmstartest/exemplary/video.json | 55 ++++++ .../cpmstartest/supplemental/audio.json | 25 +++ .../supplemental/explicit-dimensions.json | 58 +++++++ .../invalid-response-no-bids.json | 50 ++++++ .../invalid-response-unmarshall-error.json | 66 +++++++ .../cpmstartest/supplemental/native.json | 25 +++ .../supplemental/no-imps-in-request.json | 18 ++ .../supplemental/server-error-code.json | 53 ++++++ .../supplemental/server-no-content.json | 45 +++++ .../supplemental/wrong-impression-ext.json | 26 +++ .../wrong-impression-mapping.json | 77 +++++++++ adapters/cpmstar/params_test.go | 54 ++++++ adapters/cpmstar/usersync.go | 13 ++ adapters/cpmstar/usersync_test.go | 25 +++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_cpmstar.go | 6 + static/bidder-info/cpmstar.yaml | 11 ++ static/bidder-params/cpmstar.json | 19 +++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 26 files changed, 1061 insertions(+) create mode 100644 adapters/cpmstar/cpmstar.go create mode 100644 adapters/cpmstar/cpmstar_test.go create mode 100644 adapters/cpmstar/cpmstartest/exemplary/banner-and-video.json create mode 100644 adapters/cpmstar/cpmstartest/exemplary/banner.json create mode 100644 adapters/cpmstar/cpmstartest/exemplary/video.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/audio.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/explicit-dimensions.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/invalid-response-no-bids.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/invalid-response-unmarshall-error.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/native.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/no-imps-in-request.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/server-error-code.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/server-no-content.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/wrong-impression-ext.json create mode 100644 adapters/cpmstar/cpmstartest/supplemental/wrong-impression-mapping.json create mode 100644 adapters/cpmstar/params_test.go create mode 100644 adapters/cpmstar/usersync.go create mode 100644 adapters/cpmstar/usersync_test.go create mode 100644 openrtb_ext/imp_cpmstar.go create mode 100644 static/bidder-info/cpmstar.yaml create mode 100644 static/bidder-params/cpmstar.json diff --git a/adapters/cpmstar/cpmstar.go b/adapters/cpmstar/cpmstar.go new file mode 100644 index 00000000000..ef6abe70cb7 --- /dev/null +++ b/adapters/cpmstar/cpmstar.go @@ -0,0 +1,161 @@ +package cpmstar + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type Adapter struct { + endpoint string +} + +func (a *Adapter) MakeRequests(request *openrtb.BidRequest, unused *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var adapterRequests []*adapters.RequestData + + if err := preprocess(request); err != nil { + errs = append(errs, err) + return nil, errs + } + + adapterReq, err := a.makeRequest(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + adapterRequests = append(adapterRequests, adapterReq) + + return adapterRequests, errs +} + +func (a *Adapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, error) { + var err error + + jsonBody, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: jsonBody, + Headers: headers, + }, nil +} + +func preprocess(request *openrtb.BidRequest) error { + if len(request.Imp) == 0 { + return &errortypes.BadInput{ + Message: "No Imps in Bid Request", + } + } + for i := 0; i < len(request.Imp); i++ { + var imp = &request.Imp[i] + var bidderExt adapters.ExtImpBidder + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + if err := validateImp(imp); err != nil { + return err + } + + var extImp openrtb_ext.ExtImpCpmstar + if err := json.Unmarshal(bidderExt.Bidder, &extImp); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + imp.Ext = bidderExt.Bidder + } + + return nil +} + +func validateImp(imp *openrtb.Imp) error { + if imp.Banner == nil && imp.Video == nil { + return &errortypes.BadInput{ + Message: "Only Banner and Video bid-types are supported at this time", + } + } + return nil +} + +// MakeBids based on cpmstar server response +func (a *Adapter) MakeBids(bidRequest *openrtb.BidRequest, unused *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if responseData.StatusCode == http.StatusNoContent { + return nil, nil + } + + if responseData.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected HTTP status code: %d. Run with request.debug = 1 for more info", responseData.StatusCode), + }} + } + + var bidResponse openrtb.BidResponse + + if err := json.Unmarshal(responseData.Body, &bidResponse); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: err.Error(), + }} + } + + if len(bidResponse.SeatBid) == 0 { + return nil, nil + } + + rv := adapters.NewBidderResponseWithBidsCapacity(len(bidResponse.SeatBid[0].Bid)) + var errors []error + + for _, seatbid := range bidResponse.SeatBid { + for _, bid := range seatbid.Bid { + foundMatchingBid := false + bidType := openrtb_ext.BidTypeBanner + for _, imp := range bidRequest.Imp { + if imp.ID == bid.ImpID { + foundMatchingBid = true + if imp.Banner != nil { + bidType = openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + bidType = openrtb_ext.BidTypeVideo + } + break + } + } + + if foundMatchingBid { + rv.Bids = append(rv.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + }) + } else { + errors = append(errors, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("bid id='%s' could not find valid impid='%s'", bid.ID, bid.ImpID), + }) + } + } + } + return rv, errors +} + +func NewCpmstarBidder(endpoint string) *Adapter { + return &Adapter{ + endpoint: endpoint, + } +} diff --git a/adapters/cpmstar/cpmstar_test.go b/adapters/cpmstar/cpmstar_test.go new file mode 100644 index 00000000000..0a7f43f5ee7 --- /dev/null +++ b/adapters/cpmstar/cpmstar_test.go @@ -0,0 +1,11 @@ +package cpmstar + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "cpmstartest", NewCpmstarBidder("//host")) +} diff --git a/adapters/cpmstar/cpmstartest/exemplary/banner-and-video.json b/adapters/cpmstar/cpmstartest/exemplary/banner-and-video.json new file mode 100644 index 00000000000..d4dcb4b8677 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/exemplary/banner-and-video.json @@ -0,0 +1,154 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": 154, + "subpoolId": 123 + } + } + }, + { + "id": "test-video-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "placementId": 154, + "subpoolId": 654 + } + } + } + ], + "site": { + "id": "fake-site-id" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "placementId": 154, + "subpoolId": 123 + } + }, + { + "id": "test-video-imp-id", + "video": { + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 640 + }, + "ext": { + "placementId": 154, + "subpoolId": 654 + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "cpmstar", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-video-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "sample.com" + ], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576 + }, + "type": "banner" + }, + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29484110", + "adomain": [ + "sample.com" + ], + "cid": "958", + "crid": "29484110", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] +} \ No newline at end of file diff --git a/adapters/cpmstar/cpmstartest/exemplary/banner.json b/adapters/cpmstar/cpmstartest/exemplary/banner.json new file mode 100644 index 00000000000..0bbe3060a63 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/exemplary/banner.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "placementId": 154, + "subpoolId": 123 + } + } + } + ], + "site": { + "id": "fake-site-id" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "placementId": 154, + "subpoolId": 123 + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "cpmstar", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-banner-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-banner-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} + \ No newline at end of file diff --git a/adapters/cpmstar/cpmstartest/exemplary/video.json b/adapters/cpmstar/cpmstartest/exemplary/video.json new file mode 100644 index 00000000000..a0213cbdac1 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/exemplary/video.json @@ -0,0 +1,55 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-video-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 154, + "subpoolId": 123 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-video-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "placementId": 154, + "subpoolId": 123 + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/cpmstar/cpmstartest/supplemental/audio.json b/adapters/cpmstar/cpmstartest/supplemental/audio.json new file mode 100644 index 00000000000..18ff171c7f5 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/audio.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "unsupported-audio-request", + "imp": [ + { + "id": "unsupported-audio-imp", + "audio": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 154 + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Only Banner and Video bid-types are supported at this time", + "comparison": "literal" + } + ] +} diff --git a/adapters/cpmstar/cpmstartest/supplemental/explicit-dimensions.json b/adapters/cpmstar/cpmstartest/supplemental/explicit-dimensions.json new file mode 100644 index 00000000000..b8aad87514d --- /dev/null +++ b/adapters/cpmstar/cpmstartest/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": { + "placementId": 154, + "subpoolId": 123 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "placementId": 154, + "subpoolId": 123 + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} \ No newline at end of file diff --git a/adapters/cpmstar/cpmstartest/supplemental/invalid-response-no-bids.json b/adapters/cpmstar/cpmstartest/supplemental/invalid-response-no-bids.json new file mode 100644 index 00000000000..a3cb9114caa --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/invalid-response-no-bids.json @@ -0,0 +1,50 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "w": 90, + "h": 728 + }, + "ext": { + "bidder": { + "placementId": 154 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "h": 728, + "w": 90 + }, + "ext": { + "placementId": 154 + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some_test_auction", + "seatbid": [ + ], + "cur": "USD" + } + } + } + ] +} \ No newline at end of file diff --git a/adapters/cpmstar/cpmstartest/supplemental/invalid-response-unmarshall-error.json b/adapters/cpmstar/cpmstartest/supplemental/invalid-response-unmarshall-error.json new file mode 100644 index 00000000000..e20acefe2c3 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/invalid-response-unmarshall-error.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "w": 90, + "h": 728 + }, + "ext": { + "bidder": { + "placementId": 154 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "h": 728, + "w": 90 + }, + "ext": { + "placementId": 154 + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some_test_auction", + "seatbid": [ + { + "bid": [ + { + "id": "uuid", + "impid": "some_test_ad", + "w": "728", + "h": 90 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go struct field Bid(\\.seatbid\\.bid)?\\.w of type uint64", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/cpmstar/cpmstartest/supplemental/native.json b/adapters/cpmstar/cpmstartest/supplemental/native.json new file mode 100644 index 00000000000..a02db78db0f --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/native.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "native": { + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": 154 + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Only Banner and Video bid-types are supported at this time", + "comparison": "literal" + } + ] +} diff --git a/adapters/cpmstar/cpmstartest/supplemental/no-imps-in-request.json b/adapters/cpmstar/cpmstartest/supplemental/no-imps-in-request.json new file mode 100644 index 00000000000..274a34227cf --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/no-imps-in-request.json @@ -0,0 +1,18 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site?with=some¶meters=here" + } + }, + + "expectedMakeRequestsErrors": [ + { + "value": "No Imps in Bid Request", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/cpmstar/cpmstartest/supplemental/server-error-code.json b/adapters/cpmstar/cpmstartest/supplemental/server-error-code.json new file mode 100644 index 00000000000..21e697d13f1 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/server-error-code.json @@ -0,0 +1,53 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 600, + "h": 300 + }, + "ext": { + "bidder": { + "placementId": 154 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 600, + "h": 300 + }, + "ext": { + "placementId": 154 + } + } + ] + } + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Unexpected HTTP status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] + } diff --git a/adapters/cpmstar/cpmstartest/supplemental/server-no-content.json b/adapters/cpmstar/cpmstartest/supplemental/server-no-content.json new file mode 100644 index 00000000000..e56db95f8e8 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/server-no-content.json @@ -0,0 +1,45 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": 154 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "placementId": 154 + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] + } diff --git a/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-ext.json b/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-ext.json new file mode 100644 index 00000000000..1e8de0acc66 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-ext.json @@ -0,0 +1,26 @@ +{ + "mockBidRequest": { + "id": "rqid", + "imp": [ + { + "id": "impid", + "video": { + "w": 100, + "h": 200 + }, + "ext": { + "bidder": { + "placementId": "BOGUS" + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go struct field ExtImpCpmstar.placementId of type int", + "comparison": "literal" + } + ] +} diff --git a/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-mapping.json b/adapters/cpmstar/cpmstartest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..6ab02db0ca7 --- /dev/null +++ b/adapters/cpmstar/cpmstartest/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": { + "poolid": 154, + "subpoolid": 123 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "poolid": 154, + "subpoolid": 123 + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "BOGUS-IMPID", + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "bid id='test-bid-id' could not find valid impid='BOGUS-IMPID'", + "comparison": "regex" + } +] +} \ No newline at end of file diff --git a/adapters/cpmstar/params_test.go b/adapters/cpmstar/params_test.go new file mode 100644 index 00000000000..cee471a8322 --- /dev/null +++ b/adapters/cpmstar/params_test.go @@ -0,0 +1,54 @@ +package cpmstar + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/cpmstar.json +// These also validate the format of the external API: request.imp[i].ext.cpmstar +// TestValidParams makes sure that the Cpmstar 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.BidderCpmstar, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Cpmstar params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the Cpmstar 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.BidderCpmstar, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"placementId": 154}`, + `{"placementId": 154, "subpoolId": 123}`, +} + +var invalidParams = []string{ + `{}`, + `null`, + `true`, + `154`, + `{"placementId": "154"}`, // placementId should be numeric + `{"placementId": 154, "subpoolId": "123"}`, // placementId and subpoolId should both be numeric + `{"invalid_param": 123}`, +} diff --git a/adapters/cpmstar/usersync.go b/adapters/cpmstar/usersync.go new file mode 100644 index 00000000000..9c864e24ce3 --- /dev/null +++ b/adapters/cpmstar/usersync.go @@ -0,0 +1,13 @@ +package cpmstar + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +//NewCpmstarSyncer : +func NewCpmstarSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("cpmstar", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/cpmstar/usersync_test.go b/adapters/cpmstar/usersync_test.go new file mode 100644 index 00000000000..dae55e6302e --- /dev/null +++ b/adapters/cpmstar/usersync_test.go @@ -0,0 +1,25 @@ +package cpmstar + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/stretchr/testify/assert" +) + +func TestCpmstarSyncer(t *testing.T) { + syncURL := "https://server.cpmstar.com/usersync.aspx?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri=http%3A%2F%2Flocalhost:8000%2Fsetuid%3Fbidder%3Dcpmstar%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26us_privacy%3D{{.USPrivacy}}%26uid%3D%24UID" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewCpmstarSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{}) + + assert.NoError(t, err) + assert.Equal(t, "https://server.cpmstar.com/usersync.aspx?gdpr=&gdpr_consent=&us_privacy=&redirectUri=http%3A%2F%2Flocalhost:8000%2Fsetuid%3Fbidder%3Dcpmstar%26gdpr%3D%26gdpr_consent%3D%26us_privacy%3D%26uid%3D%24UID", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.False(t, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 8dc0bb14526..d9b6ee6e55d 100644 --- a/config/config.go +++ b/config/config.go @@ -496,6 +496,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBrightroll, "https://pr-bh.ybp.yahoo.com/sync/appnexusprebidserver/?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/prebid/match?rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderCpmstar, "https://server.cpmstar.com/usersync.aspx?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dengagebdr%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") @@ -678,6 +679,7 @@ func SetupViper(v *viper.Viper, filename string) { 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 5795ad2c197..95f5b7f5882 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -22,6 +22,7 @@ import ( "github.com/prebid/prebid-server/adapters/brightroll" "github.com/prebid/prebid-server/adapters/consumable" "github.com/prebid/prebid-server/adapters/conversant" + "github.com/prebid/prebid-server/adapters/cpmstar" "github.com/prebid/prebid-server/adapters/datablocks" "github.com/prebid/prebid-server/adapters/emx_digital" "github.com/prebid/prebid-server/adapters/engagebdr" @@ -79,6 +80,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderCpmstar: cpmstar.NewCpmstarBidder(cfg.Adapters[string(openrtb_ext.BidderCpmstar)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index f02a16b4350..7a3f24eb07f 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -33,6 +33,7 @@ const ( BidderBrightroll BidderName = "brightroll" BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" + BidderCpmstar BidderName = "cpmstar" BidderDatablocks BidderName = "datablocks" BidderEmxDigital BidderName = "emx_digital" BidderEngageBDR BidderName = "engagebdr" @@ -87,6 +88,7 @@ var BidderMap = map[string]BidderName{ "brightroll": BidderBrightroll, "consumable": BidderConsumable, "conversant": BidderConversant, + "cpmstar": BidderCpmstar, "datablocks": BidderDatablocks, "emx_digital": BidderEmxDigital, "engagebdr": BidderEngageBDR, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 2bfa5514063..5447cd28800 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -19,6 +19,7 @@ import ( "github.com/prebid/prebid-server/adapters/brightroll" "github.com/prebid/prebid-server/adapters/consumable" "github.com/prebid/prebid-server/adapters/conversant" + "github.com/prebid/prebid-server/adapters/cpmstar" "github.com/prebid/prebid-server/adapters/datablocks" "github.com/prebid/prebid-server/adapters/emx_digital" "github.com/prebid/prebid-server/adapters/engagebdr" @@ -75,6 +76,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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.BidderEmxDigital, emx_digital.NewEMXDigitalSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEngageBDR, engagebdr.NewEngageBDRSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 3fb1d4d7fa4..ded8fd2bd78 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -26,6 +26,7 @@ func TestNewSyncerMap(t *testing.T) { 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.BidderEmxDigital): syncConfig, string(openrtb_ext.BidderEngageBDR): syncConfig, From 85022d14f7d93d58b3b4e64434dec15b629a66fa Mon Sep 17 00:00:00 2001 From: johnwier <49074029+johnwier@users.noreply.github.com> Date: Wed, 22 Jan 2020 21:32:02 -0800 Subject: [PATCH 004/381] Update the Conversant sync pixel (#1161) --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index d9b6ee6e55d..079d5b1f4c7 100644 --- a/config/config.go +++ b/config/config.go @@ -495,7 +495,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeachfront, "https://sync.bfmio.com/sync_s2s?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbeachfront%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bio_cid%5D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBrightroll, "https://pr-bh.ybp.yahoo.com/sync/appnexusprebidserver/?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/prebid/match?rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/prebid/match/bounce/current?rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26networkId%3D72582%26version%3D1%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderCpmstar, "https://server.cpmstar.com/usersync.aspx?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Demx_digital%26uid%3D%24UID") From 31c2204793cf0dc6c28ba9a49a861c7b0da4e54c Mon Sep 17 00:00:00 2001 From: rpanchyk Date: Thu, 23 Jan 2020 17:04:18 +0200 Subject: [PATCH 005/381] Add imp.ext.is_rewarded_inventory flag for rewarded video in Rubicon (#1170) --- adapters/rubicon/rubicon.go | 9 ++++++- adapters/rubicon/rubicon_test.go | 42 ++++++++++++++++++++++++++++++++ openrtb_ext/imp.go | 3 +++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index e7461c48f7e..46caf262108 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -129,6 +129,7 @@ type rubiconVideoParams struct { type rubiconVideoExt struct { Skip int `json:"skip,omitempty"` SkipDelay int `json:"skipdelay,omitempty"` + VideoType string `json:"videotype,omitempty"` RP rubiconVideoExtRP `json:"rp"` } @@ -693,8 +694,14 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap continue } + // if imp.ext.is_rewarded_inventory = 1, set imp.video.ext.videotype = "rewarded" + var videoType = "" + if bidderExt.Prebid != nil && bidderExt.Prebid.IsRewardedInventory == 1 { + videoType = "rewarded" + } + videoCopy := *thisImp.Video - videoExt := rubiconVideoExt{Skip: rubiconExt.Video.Skip, SkipDelay: rubiconExt.Video.SkipDelay, RP: rubiconVideoExtRP{SizeID: rubiconExt.Video.VideoSizeID}} + videoExt := rubiconVideoExt{Skip: rubiconExt.Video.Skip, SkipDelay: rubiconExt.Video.SkipDelay, VideoType: videoType, RP: rubiconVideoExtRP{SizeID: rubiconExt.Video.VideoSizeID}} videoCopy.Ext, err = json.Marshal(&videoExt) thisImp.Video = &videoCopy thisImp.Banner = nil diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index dd9cea62bc7..d386daed5b1 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -1270,6 +1270,48 @@ func TestOpenRTBRequestWithVideoImpEvenIfImpHasBannerButAllRequiredVideoFields(t assert.NotNil(t, rubiconReq.Imp[0].Video, "Video object must be in request impression") } +func TestOpenRTBRequestWithVideoImpAndEnabledRewardedInventoryFlag(t *testing.T) { + bidder := new(RubiconAdapter) + + request := &openrtb.BidRequest{ + ID: "test-request-id", + Imp: []openrtb.Imp{{ + ID: "test-imp-id", + Video: &openrtb.Video{ + W: 640, + H: 360, + MIMEs: []string{"video/mp4"}, + Protocols: []openrtb.Protocol{openrtb.ProtocolVAST10}, + MaxDuration: 30, + Linearity: 1, + API: []openrtb.APIFramework{}, + }, + Ext: json.RawMessage(`{ + "prebid":{ + "is_rewarded_inventory": 1 + }, + "bidder": { + "video": {"size_id": 1} + }}`), + }}, + } + + reqs, _ := bidder.MakeRequests(request, &adapters.ExtraRequestInfo{}) + + rubiconReq := &openrtb.BidRequest{} + if err := json.Unmarshal(reqs[0].Body, rubiconReq); err != nil { + t.Fatalf("Unexpected error while decoding request: %s", err) + } + + videoExt := &rubiconVideoExt{} + if err := json.Unmarshal(rubiconReq.Imp[0].Video.Ext, &videoExt); err != nil { + t.Fatal("Error unmarshalling request.imp[i].video.ext object.") + } + + assert.Equal(t, "rewarded", videoExt.VideoType, + "Unexpected VideoType. Got %s. Expected %s", videoExt.VideoType, "rewarded") +} + func TestOpenRTBEmptyResponse(t *testing.T) { httpResp := &adapters.ResponseData{ StatusCode: http.StatusNoContent, diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go index 499d1f631bf..0e8f224b884 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 From 95200afb3cdcb054f120be21014ae3ff67221cfd Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 23 Jan 2020 16:12:48 +0100 Subject: [PATCH 006/381] [currencies] fix GetInfo() null ref issue (#1169) This CL fixes the null ref on `RateConverter.GetInfo()` when rates are nil. Issue: #1136 --- currencies/rate_converter.go | 6 +++++- currencies/rate_converter_test.go | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) 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 f9a14895347..cb5e2a0be54 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) { From 84e2c26cd5df586e2422363ac5398235849d81fe Mon Sep 17 00:00:00 2001 From: Kevin Kerr Date: Fri, 24 Jan 2020 08:37:50 -0500 Subject: [PATCH 007/381] Fix triplelift User Sync (#1173) --- config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 079d5b1f4c7..282ae9dc2b5 100644 --- a/config/config.go +++ b/config/config.go @@ -525,8 +525,8 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSovrn, "https://ap.lijit.com/pixel?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsynacormedia%26uid%3D%5BUSER_ID%5D") // openrtb_ext.BidderTappx doesn't have a good default. - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTriplelift, "https://eb2.3lift.com/getuid?gpdr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dtriplelift_native%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTriplelift, "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dtriplelift_native%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUnruly, "https://usermatch.targeting.unrulymedia.com/pbsync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dunruly%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dvisx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") // openrtb_ext.BidderVrtcal doesn't have a good default. From 94cc35467aec9ea6d1abb4943532af0c3a92f6ca Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 27 Jan 2020 14:03:34 -0500 Subject: [PATCH 008/381] Enhance Message For Cache Errors (#1175) --- prebid_cache_client/client.go | 34 ++++++------- prebid_cache_client/client_test.go | 80 ++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go index 6da69f68243..58e2734ed25 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" @@ -92,15 +93,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 +111,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 +119,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 +139,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 d3b5ee4bfaf..1b5b4e38967 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) { From f75de9240d69d37857af96c13a649dcbc5c0b017 Mon Sep 17 00:00:00 2001 From: PubMatic-OpenWrap Date: Tue, 28 Jan 2020 20:43:46 +0530 Subject: [PATCH 009/381] Fix PubMatic Usersync URL (#1178) Co-authored-by: pm-isha-bharti --- adapters/pubmatic/usersync_test.go | 12 ++++++++---- config/config.go | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/adapters/pubmatic/usersync_test.go b/adapters/pubmatic/usersync_test.go index ef81d223377..dd4a086c453 100644 --- a/adapters/pubmatic/usersync_test.go +++ b/adapters/pubmatic/usersync_test.go @@ -5,12 +5,13 @@ import ( "text/template" "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" "github.com/prebid/prebid-server/privacy/gdpr" "github.com/stretchr/testify/assert" ) func TestPubmaticSyncer(t *testing.T) { - syncURL := "//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=localhost%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D" + syncURL := "//ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&predirect=localhost%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D" syncURLTemplate := template.Must( template.New("sync-template").Parse(syncURL), ) @@ -18,13 +19,16 @@ func TestPubmaticSyncer(t *testing.T) { syncer := NewPubmaticSyncer(syncURLTemplate) syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ GDPR: gdpr.Policy{ - Signal: "1", - Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", + Signal: "A", + Consent: "B", + }, + CCPA: ccpa.Policy{ + Value: "C", }, }) assert.NoError(t, err) - assert.Equal(t, "//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=localhost%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D1%26gdpr_consent%3DBONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw%26uid%3D", syncInfo.URL) + assert.Equal(t, "//ads.pubmatic.com/AdServer/js/user_sync.html?gdpr=A&gdpr_consent=B&us_privacy=C&predirect=localhost%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3DA%26gdpr_consent%3DB%26uid%3D", syncInfo.URL) assert.Equal(t, "iframe", syncInfo.Type) assert.EqualValues(t, 76, syncer.GDPRVendorID()) assert.Equal(t, false, syncInfo.SupportCORS) diff --git a/config/config.go b/config/config.go index 282ae9dc2b5..ba6ed05e339 100644 --- a/config/config.go +++ b/config/config.go @@ -513,7 +513,7 @@ func (cfg *Configuration) setDerivedDefaults() { 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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?predirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderPubmatic, "https://ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&predirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BRX_UUID%5D") // openrtb_ext.BidderRTBHouse doesn't have a good default. From 5507b37d96dcfd1dfc2e47efc20bee254bca35b2 Mon Sep 17 00:00:00 2001 From: Corey Kress Date: Fri, 31 Jan 2020 14:09:07 -0500 Subject: [PATCH 010/381] [Synacormedia] Add tagId bidder parameter (#1165) --- adapters/synacormedia/params_test.go | 4 +- adapters/synacormedia/synacormedia.go | 19 ++- .../exemplary/simple-banner.json | 11 +- .../exemplary/simple-video.json | 15 +- .../synacormediatest/params/banner.json | 3 +- .../synacormediatest/params/video.json | 3 +- .../supplemental/audio_response.json | 11 +- .../supplemental/bad_response.json | 11 +- .../supplemental/bad_seat_id.json | 3 +- .../supplemental/missing_seat_id.json | 3 +- .../supplemental/missing_tag_id.json | 30 ++++ .../supplemental/native_response.json | 11 +- .../supplemental/one_bad_ext.json | 136 ++++++++++++++++++ .../supplemental/status_204.json | 11 +- .../supplemental/status_400.json | 11 +- .../supplemental/status_500.json | 11 +- openrtb_ext/imp_synacormedia.go | 1 + static/bidder-params/synacormedia.json | 4 + 18 files changed, 253 insertions(+), 45 deletions(-) create mode 100644 adapters/synacormedia/synacormediatest/supplemental/missing_tag_id.json create mode 100644 adapters/synacormedia/synacormediatest/supplemental/one_bad_ext.json diff --git a/adapters/synacormedia/params_test.go b/adapters/synacormedia/params_test.go index ffd891f4e84..a216818e382 100644 --- a/adapters/synacormedia/params_test.go +++ b/adapters/synacormedia/params_test.go @@ -40,9 +40,9 @@ func TestInvalidParams(t *testing.T) { } var validParams = []string{ - `{"seatId": "123"}`, + `{"seatId": "123", "tagId":"234"}`, } var invalidParams = []string{ - `{"seatId": 123}`, + `{"seatId": 123, "tagId":234}`, } diff --git a/adapters/synacormedia/synacormedia.go b/adapters/synacormedia/synacormedia.go index ccb2798f8cf..2d913f026b4 100644 --- a/adapters/synacormedia/synacormedia.go +++ b/adapters/synacormedia/synacormedia.go @@ -20,6 +20,7 @@ type SynacorMediaAdapter struct { type SyncEndpointTemplateParams struct { SeatId string + TagId string } type ReqExt struct { @@ -55,14 +56,23 @@ func (a *SynacorMediaAdapter) makeRequest(request *openrtb.BidRequest) (*adapter var firstExtImp *openrtb_ext.ExtImpSynacormedia = nil for _, imp := range request.Imp { - validImp, err := getExtImpObj(&imp) + validExtImpObj, err := getExtImpObj(&imp) // getExtImpObj returns {seatId:"", tagId:""} if err != nil { errs = append(errs, err) continue } + // if the bid request is missing seatId or TagId then ignore it + if validExtImpObj.SeatId == "" || validExtImpObj.TagId == "" { + errs = append(errs, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Invalid Impression"), + }) + continue + } + // right here is where we need to take out the tagId and then add it to imp + imp.TagID = validExtImpObj.TagId validImps = append(validImps, imp) if firstExtImp == nil { - firstExtImp = validImp + firstExtImp = validExtImpObj } } @@ -72,11 +82,12 @@ func (a *SynacorMediaAdapter) makeRequest(request *openrtb.BidRequest) (*adapter var err error - if firstExtImp == nil || firstExtImp.SeatId == "" { + if firstExtImp == nil || firstExtImp.SeatId == "" || firstExtImp.TagId == "" { return nil, append(errs, &errortypes.BadServerResponse{ - Message: fmt.Sprintf("Impression missing seat id"), + Message: fmt.Sprintf("Invalid Impression"), }) } + // this is where the empty seatId is filled re = &ReqExt{SeatId: firstExtImp.SeatId} // create JSON Request Body diff --git a/adapters/synacormedia/synacormediatest/exemplary/simple-banner.json b/adapters/synacormedia/synacormediatest/exemplary/simple-banner.json index 013891b6fa8..944e6e549ab 100644 --- a/adapters/synacormedia/synacormediatest/exemplary/simple-banner.json +++ b/adapters/synacormedia/synacormediatest/exemplary/simple-banner.json @@ -14,7 +14,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } @@ -24,15 +25,16 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "test-request-id", "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id":"test-imp-id", + "tagid": "demo1", "banner": { "format": [ {"w":300,"h":250} @@ -40,7 +42,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/exemplary/simple-video.json b/adapters/synacormedia/synacormediatest/exemplary/simple-video.json index f12556105db..2cddd5220f9 100644 --- a/adapters/synacormedia/synacormediatest/exemplary/simple-video.json +++ b/adapters/synacormedia/synacormediatest/exemplary/simple-video.json @@ -10,7 +10,6 @@ "imp": [ { "id": "i3", - "tagid": "3020", "video": { "w": 300, "h": 250, @@ -21,8 +20,9 @@ }, "metric": [], "ext": { - "bidder":{ - "seatId":"1927" + "bidder": { + "seatId":"prebid", + "tagId": "demo1" } } } @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "1", "site": { @@ -40,12 +40,12 @@ "publisher": {} }, "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id": "i3", - "tagid": "3020", + "tagid": "demo1", "video": { "w": 300, "h": 250, @@ -56,7 +56,8 @@ }, "ext": { "bidder":{ - "seatId":"1927" + "seatId":"prebid", + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/params/banner.json b/adapters/synacormedia/synacormediatest/params/banner.json index d6f4e7e9641..bb55ddfc48c 100644 --- a/adapters/synacormedia/synacormediatest/params/banner.json +++ b/adapters/synacormedia/synacormediatest/params/banner.json @@ -1,3 +1,4 @@ { - "seatId": "123" + "seatId": "123", + "tagId": "234" } diff --git a/adapters/synacormedia/synacormediatest/params/video.json b/adapters/synacormedia/synacormediatest/params/video.json index d6f4e7e9641..bb55ddfc48c 100644 --- a/adapters/synacormedia/synacormediatest/params/video.json +++ b/adapters/synacormedia/synacormediatest/params/video.json @@ -1,3 +1,4 @@ { - "seatId": "123" + "seatId": "123", + "tagId": "234" } diff --git a/adapters/synacormedia/synacormediatest/supplemental/audio_response.json b/adapters/synacormedia/synacormediatest/supplemental/audio_response.json index 172a81efa85..752d610aa72 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/audio_response.json +++ b/adapters/synacormedia/synacormediatest/supplemental/audio_response.json @@ -9,7 +9,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } @@ -19,21 +20,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "test-request-id", "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id":"test-imp-id", + "tagid": "demo1", "audio": { "mimes": ["video/mp4"] }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/supplemental/bad_response.json b/adapters/synacormedia/synacormediatest/supplemental/bad_response.json index 0893598bd1d..8e8b9a4d944 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/bad_response.json +++ b/adapters/synacormedia/synacormediatest/supplemental/bad_response.json @@ -18,7 +18,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } @@ -28,15 +29,16 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "test-request-id", "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id":"test-imp-id", + "tagid": "demo1", "banner": { "format": [ {"w":300,"h":250}, @@ -45,7 +47,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/supplemental/bad_seat_id.json b/adapters/synacormedia/synacormediatest/supplemental/bad_seat_id.json index bb144f1c6db..00dd2c23707 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/bad_seat_id.json +++ b/adapters/synacormedia/synacormediatest/supplemental/bad_seat_id.json @@ -14,7 +14,8 @@ }, "ext": { "bidder": { - "seatId": 1927 + "seatId": 1927, + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/supplemental/missing_seat_id.json b/adapters/synacormedia/synacormediatest/supplemental/missing_seat_id.json index a085b6e64f9..b85b88e4189 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/missing_seat_id.json +++ b/adapters/synacormedia/synacormediatest/supplemental/missing_seat_id.json @@ -14,6 +14,7 @@ }, "ext": { "bidder": { + "tagId": "demo1" } } } @@ -22,7 +23,7 @@ "expectedMakeRequestsErrors": [ { - "value": "Impression missing seat id", + "value": "Invalid Impression", "comparison": "literal" } ] diff --git a/adapters/synacormedia/synacormediatest/supplemental/missing_tag_id.json b/adapters/synacormedia/synacormediatest/supplemental/missing_tag_id.json new file mode 100644 index 00000000000..2e1ef6ada65 --- /dev/null +++ b/adapters/synacormedia/synacormediatest/supplemental/missing_tag_id.json @@ -0,0 +1,30 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "seatId": "prebid" + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Invalid Impression", + "comparison": "literal" + } + ] +} diff --git a/adapters/synacormedia/synacormediatest/supplemental/native_response.json b/adapters/synacormedia/synacormediatest/supplemental/native_response.json index 89742d6e0df..1428ac1ccd3 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/native_response.json +++ b/adapters/synacormedia/synacormediatest/supplemental/native_response.json @@ -9,7 +9,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } @@ -19,21 +20,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "test-request-id", "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id":"test-imp-id", + "tagid": "demo1", "native": { "request": "" }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/supplemental/one_bad_ext.json b/adapters/synacormedia/synacormediatest/supplemental/one_bad_ext.json new file mode 100644 index 00000000000..5749aadba98 --- /dev/null +++ b/adapters/synacormedia/synacormediatest/supplemental/one_bad_ext.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-bad", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "seatId": "", + "tagId": "" + } + } + }, + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "seatId": "prebid", + "tagId": "demo1" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", + "body": { + "id": "test-request-id", + "ext": { + "seatId": "prebid" + }, + "imp": [ + { + "id":"test-imp-id", + "tagid": "demo1", + "banner": { + "format": [ + {"w":300,"h":250} + ] + }, + "ext": { + "bidder": { + "seatId": "prebid", + "tagId": "demo1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "1", + "seatbid": [ + { + "bid": [ + { + "id": "test-request-id", + "impid": "test-imp-id", + "price": 2.69, + "adomain": [ + "psacentral.org" + ], + "cid": "mock-crid", + "crid": "mock-cid", + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "synacormedia" + } + ], + "ext": { + "responsetimemillis": { + "synacormedia": 339 + } + } + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adomain": [ + "psacentral.org" + ], + "cid": "mock-crid", + "crid": "mock-cid", + "ext": { + "prebid": { + "type": "banner" + } + }, + "id": "test-request-id", + "impid": "test-imp-id", + "price": 2.69 + }, + "type": "banner" + } + ] + } + ], + "expectedMakeRequestsErrors": [ + { + "value": "Invalid Impression", + "comparison": "literal" + } + ] +} diff --git a/adapters/synacormedia/synacormediatest/supplemental/status_204.json b/adapters/synacormedia/synacormediatest/supplemental/status_204.json index f53ff1ec918..76f97f9cdfa 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/status_204.json +++ b/adapters/synacormedia/synacormediatest/supplemental/status_204.json @@ -18,7 +18,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } @@ -28,15 +29,16 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "test-request-id", "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id":"test-imp-id", + "tagid": "demo1", "banner": { "format": [ {"w":300,"h":250}, @@ -45,7 +47,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/supplemental/status_400.json b/adapters/synacormedia/synacormediatest/supplemental/status_400.json index a0667658e1d..1bb2cf6fa45 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/status_400.json +++ b/adapters/synacormedia/synacormediatest/supplemental/status_400.json @@ -18,7 +18,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } @@ -28,15 +29,16 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "test-request-id", "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id":"test-imp-id", + "tagid": "demo1", "banner": { "format": [ {"w":300,"h":250}, @@ -45,7 +47,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } diff --git a/adapters/synacormedia/synacormediatest/supplemental/status_500.json b/adapters/synacormedia/synacormediatest/supplemental/status_500.json index 125829c2328..37ca398e59e 100644 --- a/adapters/synacormedia/synacormediatest/supplemental/status_500.json +++ b/adapters/synacormedia/synacormediatest/supplemental/status_500.json @@ -18,7 +18,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } @@ -28,15 +29,16 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://1927.technoratimedia.com/openrtb/bids/1927", + "uri": "http://prebid.technoratimedia.com/openrtb/bids/prebid", "body": { "id": "test-request-id", "ext": { - "seatId": "1927" + "seatId": "prebid" }, "imp": [ { "id":"test-imp-id", + "tagid": "demo1", "banner": { "format": [ {"w":300,"h":250}, @@ -45,7 +47,8 @@ }, "ext": { "bidder": { - "seatId": "1927" + "seatId": "prebid", + "tagId": "demo1" } } } 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/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." } }, From b8871e98fcae73332be1d2e48c96b09f27d9e87d Mon Sep 17 00:00:00 2001 From: Seba Perez Date: Fri, 31 Jan 2020 17:11:46 -0300 Subject: [PATCH 011/381] Remove all non-secure calls from eplanning adapter (#1179) --- adapters/eplanning/eplanning_test.go | 2 +- .../eplanning/eplanningtest/exemplary/simple-banner-2.json | 2 +- adapters/eplanning/eplanningtest/exemplary/simple-banner.json | 2 +- adapters/eplanning/eplanningtest/exemplary/two-banners.json | 4 ++-- .../eplanningtest/supplemental/banner-no-size-sends-1x1.json | 4 ++-- .../eplanningtest/supplemental/invalid-response-no-bids.json | 4 ++-- .../supplemental/invalid-response-unmarshall-error.json | 4 ++-- .../eplanningtest/supplemental/server-bad-request.json | 4 ++-- .../eplanningtest/supplemental/server-error-code.json | 4 ++-- .../eplanningtest/supplemental/server-no-content.json | 4 ++-- .../supplemental/site-domain-and-url-correctly-parsed.json | 4 ++-- config/config.go | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/adapters/eplanning/eplanning_test.go b/adapters/eplanning/eplanning_test.go index d1219ce4eef..d2c331d456d 100644 --- a/adapters/eplanning/eplanning_test.go +++ b/adapters/eplanning/eplanning_test.go @@ -8,7 +8,7 @@ import ( ) func TestJsonSamples(t *testing.T) { - eplanningAdapter := NewEPlanningBidder(new(http.Client), "http://ads.us.e-planning.net/hb/1") + eplanningAdapter := NewEPlanningBidder(new(http.Client), "https://ads.us.e-planning.net/hb/1") eplanningAdapter.testing = true adapterstest.RunJSONBidderTest(t, "eplanningtest", eplanningAdapter) } diff --git a/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json b/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json index 596b061576f..f4c8fe0c273 100644 --- a/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json +++ b/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json @@ -28,7 +28,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=300x250:300x250", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=300x250:300x250", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/exemplary/simple-banner.json b/adapters/eplanning/eplanningtest/exemplary/simple-banner.json index 21cbbdae7df..61b7878a8bf 100644 --- a/adapters/eplanning/eplanningtest/exemplary/simple-banner.json +++ b/adapters/eplanning/eplanningtest/exemplary/simple-banner.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de:600x300&uid=2154987&ip=123.123.123.123", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadun_itco_de:600x300&uid=2154987&ip=123.123.123.123", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/exemplary/two-banners.json b/adapters/eplanning/eplanningtest/exemplary/two-banners.json index 15639524207..fccc1a30e7e 100644 --- a/adapters/eplanning/eplanningtest/exemplary/two-banners.json +++ b/adapters/eplanning/eplanningtest/exemplary/two-banners.json @@ -39,7 +39,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300+300x250:300x250&ip=123.123.123.123", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300+300x250:300x250&ip=123.123.123.123", "body": {} }, "mockResponse": { @@ -120,4 +120,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json b/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json index 729115e55de..afa7b9532df 100644 --- a/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json +++ b/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json @@ -19,7 +19,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcodenosize:1x1", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcodenosize:1x1", "body": {} }, "mockResponse": { @@ -67,4 +67,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json index 57db7023360..3bf45a16364 100644 --- a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json +++ b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", "body": {} }, "mockResponse": { @@ -45,4 +45,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json b/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json index fc85a6b45c0..e8d88f17a5e 100644 --- a/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json +++ b/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", "body": {} }, "mockResponse": { @@ -52,4 +52,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json b/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json index 052c0561095..421f47efe3b 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", "body": {} }, "mockResponse": { @@ -55,4 +55,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/eplanning/eplanningtest/supplemental/server-error-code.json b/adapters/eplanning/eplanningtest/supplemental/server-error-code.json index 699968ce398..ceec970ba45 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-error-code.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-error-code.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", "body": {} }, "mockResponse": { @@ -55,4 +55,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/eplanning/eplanningtest/supplemental/server-no-content.json b/adapters/eplanning/eplanningtest/supplemental/server-no-content.json index 9058699af3e..a2e444a9901 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-no-content.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-no-content.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?r=pbs&ncb=1&ur=FILE&e=testadunitcode:600x300", "body": {} }, "mockResponse": { @@ -30,4 +30,4 @@ } ] } - \ No newline at end of file + diff --git a/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json b/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json index 4ce8ef2f692..d889f48189c 100644 --- a/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json +++ b/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json @@ -25,7 +25,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://ads.us.e-planning.net/hb/1/12345/1/www.publisher.com/ROS?r=pbs&ncb=1&ur=http%3A%2F%2Fwww.publisher.com%2Fawesome%2Fsite%3Fwith%3Dsome%26parameters%3Dhere&e=testadunitcode:600x300", + "uri": "https://ads.us.e-planning.net/hb/1/12345/1/www.publisher.com/ROS?r=pbs&ncb=1&ur=http%3A%2F%2Fwww.publisher.com%2Fawesome%2Fsite%3Fwith%3Dsome%26parameters%3Dhere&e=testadunitcode:600x300", "body": {} }, "mockResponse": { @@ -73,4 +73,4 @@ } ] } - \ No newline at end of file + diff --git a/config/config.go b/config/config.go index ba6ed05e339..2f302cc6328 100644 --- a/config/config.go +++ b/config/config.go @@ -683,7 +683,7 @@ func SetupViper(v *viper.Viper, filename string) { 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", "https://ads.us.e-planning.net/hb/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") From 63e7e1df5b11335ed24fed786d6b060eb79d345d Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 5 Feb 2020 14:00:30 -0800 Subject: [PATCH 012/381] Expose Cache HTTP Settings (#1184) --- config/config.go | 4 ++++ config/config_test.go | 7 +++++++ exchange/exchange_test.go | 2 +- prebid_cache_client/client.go | 9 ++------- prebid_cache_client/client_test.go | 4 +--- router/router.go | 18 ++++++++++++++---- 6 files changed, 29 insertions(+), 15 deletions(-) diff --git a/config/config.go b/config/config.go index 2f302cc6328..943d18a95de 100644 --- 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. @@ -583,6 +584,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", "") diff --git a/config/config_test.go b/config/config_test.go index 182a46eef50..78630e071d9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -68,6 +68,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 +218,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) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 31dddae4c74..b8a3ae0eae2 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -170,7 +170,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} diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go index 58e2734ed25..a5730ce7914 100644 --- a/prebid_cache_client/client.go +++ b/prebid_cache_client/client.go @@ -47,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, diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go index 1b5b4e38967..5840d4ea564 100644 --- a/prebid_cache_client/client_test.go +++ b/prebid_cache_client/client_test.go @@ -233,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/router/router.go b/router/router.go index 1994639110c..449ab65a448 100644 --- a/router/router.go +++ b/router/router.go @@ -183,7 +183,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, @@ -191,13 +191,22 @@ 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")) // Metrics engine r.MetricsEngine = metricsConf.NewMetricsEngine(cfg, legacyBidderList) - db, shutdown, fetcher, ampFetcher, categoriesFetcher, videoFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, theClient, r.Router) + db, shutdown, fetcher, ampFetcher, categoriesFetcher, videoFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, generalHttpClient, r.Router) // todo(zachbadgett): better shutdown r.Shutdown = shutdown @@ -223,10 +232,11 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r defaultAliases, defReqJSON := readDefaultRequest(cfg.DefReqConfig) syncers := usersyncers.NewSyncerMap(cfg) - gdprPerms := gdpr.NewPermissions(context.Background(), cfg.GDPR, adapters.GDPRAwareSyncerIDs(syncers), theClient) + gdprPerms := gdpr.NewPermissions(context.Background(), cfg.GDPR, adapters.GDPRAwareSyncerIDs(syncers), generalHttpClient) exchanges = newExchangeMap(cfg) - theExchange := exchange.NewExchange(theClient, pbc.NewClient(&cfg.CacheURL, &cfg.ExtCacheURL, r.MetricsEngine), cfg, r.MetricsEngine, bidderInfos, gdprPerms, rateConvertor) + cacheClient := pbc.NewClient(cacheHttpClient, &cfg.CacheURL, &cfg.ExtCacheURL, r.MetricsEngine) + theExchange := exchange.NewExchange(generalHttpClient, cacheClient, cfg, r.MetricsEngine, bidderInfos, gdprPerms, rateConvertor) openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, categoriesFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap) From 4ff04cced4bd494e4300269563d201a4d4632dd1 Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Thu, 6 Feb 2020 11:19:45 -0800 Subject: [PATCH 013/381] Adding bid rejection messages to debug response (#1181) --- exchange/exchange.go | 43 +++++++----- exchange/exchange_test.go | 139 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 161 insertions(+), 21 deletions(-) diff --git a/exchange/exchange.go b/exchange/exchange.go index 3d9055ca8a6..8c6cac4cfcd 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "math/rand" "net/http" @@ -146,10 +147,14 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque //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)) @@ -340,7 +345,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 { @@ -359,6 +364,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 { @@ -370,7 +376,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 } @@ -382,6 +388,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 @@ -396,6 +403,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 { @@ -405,6 +413,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 { @@ -424,6 +434,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 { @@ -447,11 +458,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:]...) } @@ -460,11 +473,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 { @@ -483,19 +497,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) { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index b8a3ae0eae2..7e199d4b750 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" @@ -930,9 +931,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") @@ -983,9 +986,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") @@ -1034,9 +1038,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") @@ -1114,9 +1120,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") @@ -1179,9 +1186,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,6 +1206,125 @@ 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}, + } + 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") +} + type exchangeSpec struct { IncomingRequest exchangeRequest `json:"incomingRequest"` OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` From fd9bc8f11aa528ef900bd9fd9f63c165acd93a40 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Mon, 10 Feb 2020 16:38:49 -0500 Subject: [PATCH 014/381] Adds timeout notifications for Facebook (#1182) --- adapters/adpone/adpone.go | 3 +- adapters/audienceNetwork/facebook.go | 17 +++++++++++ adapters/audienceNetwork/facebook_test.go | 37 +++++++++++++++++++++++ adapters/bidder.go | 13 ++++++++ exchange/bidder.go | 24 +++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) diff --git a/adapters/adpone/adpone.go b/adapters/adpone/adpone.go index 345a4988580..b1822a0ac07 100644 --- a/adapters/adpone/adpone.go +++ b/adapters/adpone/adpone.go @@ -3,9 +3,10 @@ package adpone import ( "encoding/json" "fmt" - "github.com/prebid/prebid-server/openrtb_ext" "net/http" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go index 706673cbafc..3ece7bb99e4 100644 --- a/adapters/audienceNetwork/facebook.go +++ b/adapters/audienceNetwork/facebook.go @@ -447,3 +447,20 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string) appSecret: appSecret, } } + +func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { + // Note, facebook creates one request per imp, so all these requests will only have one imp in them + auction_id, err := jsonparser.GetString(req.Body, "imp", "[0]", "id") + if err != nil { + return &adapters.RequestData{}, []error{err} + } + + uri := fmt.Sprintf("https://www.facebook.com/audiencenetwork/nurl/?partner=%s&app=%s&auction=%s&ortb_loss_code=2", fa.platformID, fa.platformID, auction_id) + 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 2ce0ef3ba64..1edaabd45d7 100644 --- a/adapters/audienceNetwork/facebook_test.go +++ b/adapters/audienceNetwork/facebook_test.go @@ -4,7 +4,9 @@ import ( "testing" "time" + "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/stretchr/testify/assert" ) type tagInfo struct { @@ -40,3 +42,38 @@ type FacebookExt struct { func TestJsonSamples(t *testing.T) { adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret")) } + +func TestMakeTimeoutNotice(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.Nil(t, err, "Facebook MakeTimeoutNotification() return an error %v", err) + expectedUri := "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=test-platform-id&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/bidder.go b/adapters/bidder.go index 9d3ffb75414..baec4135b6a 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -39,6 +39,19 @@ type Bidder interface { MakeBids(internalRequest *openrtb.BidRequest, externalRequest *RequestData, response *ResponseData) (*BidderResponse, []error) } +// TimeoutBidder is used to identify bidders that support timeout notifications. +type TimeoutBidder interface { + Bidder + + // MakeTimeoutNotice functions much the same as MakeRequests, except it is fed the bidder request that timed out, + // and expects that only one notification "request" will be generated. A use case for multiple timeout notifications + // has not been anticipated. + // + // Do note that if MakeRequests returns multiple requests, and more than one of these times out, MakeTimeoutNotice will be called + // once for each timed out request. + MakeTimeoutNotification(req *RequestData) (*RequestData, []error) +} + type MisconfiguredBidder struct { Name string Error error diff --git a/exchange/bidder.go b/exchange/bidder.go index 5708660057f..d9a28fee175 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "time" "github.com/mxmCherry/openrtb" nativeRequests "github.com/mxmCherry/openrtb/native/request" @@ -295,6 +296,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, @@ -328,6 +337,21 @@ 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 + ctxhttp.Do(ctx, bidder.Client, httpReq) + // No validation yet on sending notifications + } + } + +} + type httpCallInfo struct { request *adapters.RequestData response *adapters.ResponseData From 7762c0c3a926c07b24ef50605545a4987cd4e280 Mon Sep 17 00:00:00 2001 From: Michael Kuryshev Date: Tue, 18 Feb 2020 18:35:49 +0100 Subject: [PATCH 015/381] VIS.X: added app type support (#1194) --- static/bidder-info/visx.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/bidder-info/visx.yaml b/static/bidder-info/visx.yaml index dd4f6c660de..f404a013337 100644 --- a/static/bidder-info/visx.yaml +++ b/static/bidder-info/visx.yaml @@ -4,3 +4,6 @@ capabilities: site: mediaTypes: - banner + app: + mediaTypes: + - banner From 8e382e7b8c12b66e4ce6392078b00c2151ce6f32 Mon Sep 17 00:00:00 2001 From: Viacheslav Chimishuk Date: Fri, 21 Feb 2020 20:14:53 +0200 Subject: [PATCH 016/381] Add Adoppler bidder support. (#1186) * Add Adoppler bidder support. * Address code review comments. Use JSON-templates for testing. * Fix misprint; Add url.PathEscape call for adunit URL parameter. --- adapters/adoppler/adoppler.go | 210 ++++++++++++++++++ adapters/adoppler/adoppler_test.go | 12 + .../adopplertest/exemplary/multibid.json | 60 +++++ .../adopplertest/exemplary/no-bid.json | 13 ++ .../supplemental/bad-request.json | 15 ++ .../supplemental/duplicate-imp.json | 38 ++++ .../supplemental/invalid-impid.json | 20 ++ .../supplemental/invalid-response.json | 15 ++ .../supplemental/invalid-video-ext.json | 43 ++++ .../supplemental/missing-adunit.json | 9 + .../supplemental/server-error.json | 15 ++ config/config.go | 1 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adoppler.go | 5 + static/bidder-info/adoppler.yaml | 11 + static/bidder-params/adoppler.json | 13 ++ usersync/usersyncers/syncer_test.go | 1 + 18 files changed, 485 insertions(+) create mode 100644 adapters/adoppler/adoppler.go create mode 100644 adapters/adoppler/adoppler_test.go create mode 100644 adapters/adoppler/adopplertest/exemplary/multibid.json create mode 100644 adapters/adoppler/adopplertest/exemplary/no-bid.json create mode 100644 adapters/adoppler/adopplertest/supplemental/bad-request.json create mode 100644 adapters/adoppler/adopplertest/supplemental/duplicate-imp.json create mode 100644 adapters/adoppler/adopplertest/supplemental/invalid-impid.json create mode 100644 adapters/adoppler/adopplertest/supplemental/invalid-response.json create mode 100644 adapters/adoppler/adopplertest/supplemental/invalid-video-ext.json create mode 100644 adapters/adoppler/adopplertest/supplemental/missing-adunit.json create mode 100644 adapters/adoppler/adopplertest/supplemental/server-error.json create mode 100644 openrtb_ext/imp_adoppler.go create mode 100644 static/bidder-info/adoppler.yaml create mode 100644 static/bidder-params/adoppler.json diff --git a/adapters/adoppler/adoppler.go b/adapters/adoppler/adoppler.go new file mode 100644 index 00000000000..c604bfeac06 --- /dev/null +++ b/adapters/adoppler/adoppler.go @@ -0,0 +1,210 @@ +package adoppler + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..e7d908df4f1 --- /dev/null +++ b/adapters/adoppler/adoppler_test.go @@ -0,0 +1,12 @@ +package adoppler + +import ( + "testing" + + "github.com/prebid/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/config/config.go b/config/config.go index 943d18a95de..52686422039 100644 --- a/config/config.go +++ b/config/config.go @@ -673,6 +673,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.adform.endpoint", "http://adx.adform.net/adx") v.SetDefault("adapters.adkernel.endpoint", "http://{{.Host}}/hb?zone={{.ZoneID}}") v.SetDefault("adapters.adkerneladn.endpoint", "http://{{.Host}}/rtbpub?account={{.PublisherID}}") + 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.advangelists.endpoint", "http://nep.advangelists.com/xp/get?pubid={{.PublisherID}}") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 95f5b7f5882..d169c1204bf 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -12,6 +12,7 @@ import ( "github.com/prebid/prebid-server/adapters/adform" "github.com/prebid/prebid-server/adapters/adkernel" "github.com/prebid/prebid-server/adapters/adkernelAdn" + "github.com/prebid/prebid-server/adapters/adoppler" "github.com/prebid/prebid-server/adapters/adpone" "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" @@ -71,6 +72,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderAdform: adform.NewAdformBidder(client, cfg.Adapters[string(openrtb_ext.BidderAdform)].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.BidderAdoppler: adoppler.NewAdopplerBidder(cfg.Adapters[string(openrtb_ext.BidderAdoppler)].Endpoint), openrtb_ext.BidderAdpone: adpone.NewAdponeBidder(cfg.Adapters[string(openrtb_ext.BidderAdpone)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 7a3f24eb07f..6e70ef4b6fa 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -29,6 +29,7 @@ const ( BidderAdvangelists BidderName = "advangelists" BidderApplogy BidderName = "applogy" BidderAppnexus BidderName = "appnexus" + BidderAdoppler BidderName = "adoppler" BidderBeachfront BidderName = "beachfront" BidderBrightroll BidderName = "brightroll" BidderConsumable BidderName = "consumable" @@ -84,6 +85,7 @@ var BidderMap = map[string]BidderName{ "advangelists": BidderAdvangelists, "applogy": BidderApplogy, "appnexus": BidderAppnexus, + "adoppler": BidderAdoppler, "beachfront": BidderBeachfront, "brightroll": BidderBrightroll, "consumable": BidderConsumable, 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/static/bidder-info/adoppler.yaml b/static/bidder-info/adoppler.yaml new file mode 100644 index 00000000000..7fa79eda163 --- /dev/null +++ b/static/bidder-info/adoppler.yaml @@ -0,0 +1,11 @@ +maintainer: + email: info@adoppler.com +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video 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/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index ded8fd2bd78..87a9caebf96 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -65,6 +65,7 @@ func TestNewSyncerMap(t *testing.T) { } adaptersWithoutSyncers := map[openrtb_ext.BidderName]bool{ + openrtb_ext.BidderAdoppler: true, openrtb_ext.BidderApplogy: true, openrtb_ext.BidderTappx: true, openrtb_ext.BidderKubient: true, From fd78c23caef9986556b8558443a7e0cb91831a3a Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Wed, 26 Feb 2020 15:54:37 -0800 Subject: [PATCH 017/381] Adding support for deal prefixes (#1183) --- adapters/appnexus/appnexus.go | 8 +- .../exemplary/simple-auction.json | 6 +- .../video/simple-video.json | 6 +- .../appnexustest/amp/simple-banner.json | 6 +- .../appnexustest/amp/simple-video.json | 6 +- .../appnexustest/exemplary/native-1.1.json | 6 +- .../appnexustest/exemplary/simple-banner.json | 6 +- .../appnexustest/exemplary/simple-video.json | 6 +- .../exemplary/video-invalid-category.json | 6 +- .../supplemental/displaymanager-test.json | 6 +- .../appnexustest/supplemental/multi-bid.json | 12 +- adapters/bidder.go | 8 +- endpoints/openrtb2/video_auction.go | 5 +- exchange/bidder.go | 17 +- exchange/bidder_test.go | 9 +- exchange/exchange.go | 82 +++++ exchange/exchange_test.go | 296 ++++++++++++++++-- openrtb_ext/bid_request_video.go | 8 + openrtb_ext/request.go | 1 + 19 files changed, 442 insertions(+), 58 deletions(-) diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index 3986bfd45b0..9bec9bf1e3b 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/bidder.go b/adapters/bidder.go index baec4135b6a..627caf67344 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -108,10 +108,12 @@ func NewBidderResponse() *BidderResponse { // TypedBid.Bid.Ext will become "response.seatbid[i].bid.ext.bidder" in the final OpenRTB response. // TypedBid.BidType will become "response.seatbid[i].bid.ext.prebid.type" in the final OpenRTB response. // TypedBid.BidVideo will become "response.seatbid[i].bid.ext.prebid.video" in the final OpenRTB response. +// TypedBid.DealPriority will become "response.seatbid[i].bid.dealPriority" in the final OpenRTB response. type TypedBid struct { - Bid *openrtb.Bid - BidType openrtb_ext.BidType - BidVideo *openrtb_ext.ExtBidPrebidVideo + Bid *openrtb.Bid + BidType openrtb_ext.BidType + BidVideo *openrtb_ext.ExtBidPrebidVideo + DealPriority int } // RequestData and ResponseData exist so that prebid-server core code can implement its "debug" functionality diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index b8b21b762d7..7c9651af747 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -550,8 +550,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} diff --git a/exchange/bidder.go b/exchange/bidder.go index d9a28fee175..97f64e74bb5 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -51,11 +51,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. @@ -183,9 +185,10 @@ 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, + bid: bidResponse.Bids[i].Bid, + bidType: bidResponse.Bids[i].BidType, + bidVideo: bidResponse.Bids[i].BidVideo, + dealPriority: bidResponse.Bids[i].DealPriority, }) } } else { diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 173bc37ee51..46f63cc66c4 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -39,13 +39,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, }, }, } @@ -88,6 +90,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) diff --git a/exchange/exchange.go b/exchange/exchange.go index 8c6cac4cfcd..ef10180a745 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -10,6 +10,7 @@ import ( "net/http" "runtime/debug" "sort" + "strings" "time" "github.com/prebid/prebid-server/stored_requests" @@ -167,12 +168,93 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque } targData.setTargeting(auc, bidRequest.App != nil, bidCategory) } + + if requestExt.Prebid.SupportDeals { + dealErrs := applyDealSupport(bidRequest, auc) + errs = append(errs, dealErrs...) + } } // Build the response return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, 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) []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) + } 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) { + if bid.dealPriority >= dealTierInfo.MinDealTier { + prefixTier := fmt.Sprintf("%s%d_", dealTierInfo.Prefix, bid.dealPriority) + + if oldCatDur, ok := bid.bidTargets["hb_pb_cat_dur"]; ok { + oldCatDurSplit := strings.SplitAfterN(oldCatDur, "_", 2) + oldCatDurSplit[0] = prefixTier + + newCatDur := strings.Join(oldCatDurSplit, "") + bid.bidTargets["hb_pb_cat_dur"] = newCatDur + } + } +} + func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auctionCtx context.Context, cancel context.CancelFunc) { auctionCtx = ctx cancel = func() {} diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 7e199d4b750..0a64bce0826 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -914,10 +914,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, @@ -969,10 +969,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, @@ -1023,9 +1023,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, @@ -1105,9 +1105,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, @@ -1156,10 +1156,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{ @@ -1288,7 +1288,7 @@ func TestBidRejectionErrors(t *testing.T) { innerBids := []*pbsOrtbBid{} for _, bid := range test.bids { currentBid := pbsOrtbBid{ - bid, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, + bid, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, 0, } innerBids = append(innerBids, ¤tBid) } @@ -1325,6 +1325,264 @@ func TestUpdateRejections(t *testing.T) { 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{}, "video", test.targ, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority} + + auc := &auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp_id1": { + bidderName: &bid, + }, + }, + } + + dealErrs := applyDealSupport(bidRequest, auc) + + assert.Equal(t, test.expectedHbPbCatDur, auc.winningBidsByBidder["imp_id1"][bidderName].bidTargets["hb_pb_cat_dur"], 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{}, "video", test.targ, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority} + + updateHbPbCatDur(&bid, test.dealTier) + + assert.Equal(t, test.expectedHbPbCatDur, bid.bidTargets["hb_pb_cat_dur"], test.description) + } +} + type exchangeSpec struct { IncomingRequest exchangeRequest `json:"incomingRequest"` OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go index 454476857a4..f7ddf203294 100644 --- a/openrtb_ext/bid_request_video.go +++ b/openrtb_ext/bid_request_video.go @@ -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 { diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 9226ff294d5..ee1a0cd0f8b 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"` } // ExtRequestPrebidCache defines the contract for bidrequest.ext.prebid.cache From 7be0a4d68832679d71a0a11f1ff01420866d40b9 Mon Sep 17 00:00:00 2001 From: PubMatic-OpenWrap Date: Fri, 28 Feb 2020 00:25:22 +0530 Subject: [PATCH 018/381] updating default hard-coded list of certs (#1201) Co-authored-by: Shalmali Patil --- ssl/ssl.go | 3230 ++++++++++++++++++++-------------------------------- 1 file changed, 1206 insertions(+), 2024 deletions(-) diff --git a/ssl/ssl.go b/ssl/ssl.go index d05c90154b9..a424cd9f54b 100644 --- a/ssl/ssl.go +++ b/ssl/ssl.go @@ -40,29 +40,6 @@ func AppendPEMFileToRootCAPool(certPool *x509.CertPool, pemFileName string) (*x5 var pemCerts = []byte(` -----BEGIN CERTIFICATE----- -MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJB -VDFIMEYGA1UECgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBp -bSBlbGVrdHIuIERhdGVudmVya2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5R -dWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5RdWFsLTAzMB4XDTA1MDgxNzIyMDAw -MFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgwRgYDVQQKDD9BLVRy -dXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0ZW52 -ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMM -EEEtVHJ1c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQCtPWFuA/OQO8BBC4SAzewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUj -lUC5B3ilJfYKvUWG6Nm9wASOhURh73+nyfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZ -znF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPESU7l0+m0iKsMrmKS1GWH -2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4iHQF63n1 -k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs -2e3Vcuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYD -VR0OBAoECERqlWdVeRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC -AQEAVdRU0VlIXLOThaq/Yy/kgM40ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fG -KOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmrsQd7TZjTXLDR8KdCoLXEjq/+ -8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZdJXDRZslo+S4R -FGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS -mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmE -DNuxUCAKGkq6ahq97BvIxYSazQ== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ @@ -107,74 +84,36 @@ d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UE -AwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00x -CzAJBgNVBAYTAkVTMB4XDTA4MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEW -MBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZF -RElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC -AgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHkWLn7 -09gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7 -XBZXehuDYAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5P -Grjm6gSSrj0RuVFCPYewMYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAK -t0SdE3QrwqXrIhWYENiLxQSfHY9g5QYbm8+5eaA9oiM/Qj9r+hwDezCNzmzAv+Yb -X79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbkHQl/Sog4P75n/TSW9R28 -MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTTxKJxqvQU -fecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI -2Sf23EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyH -K9caUPgn6C9D4zq92Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEae -ZAwUswdbxcJzbPEHXEUkFDWug/FqTYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAP -BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz4SsrSbbXc6GqlPUB53NlTKxQ -MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU9QHnc2VMrFAw -RAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv -bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWIm -fQwng4/F9tqgaHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3 -gvoFNTPhNahXwOf9jU8/kzJPeGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKe -I6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1PwkzQSulgUV1qzOMPPKC8W64iLgpq0i -5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1ThCojz2GuHURwCRi -ipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oIKiMn -MCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZ -o5NjEFIqnxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6 -zqylfDJKZ0DcMDQj3dcEI2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacN -GHk0vFQYXlPKNFHtRQrmjseCNj6nOGOpMCwXEGCSn1WHElkQwg9naRHMTh5+Spqt -r0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3otkYNbn5XOmeUwssfnHdK -Z05phkOTOPu220+DkdRgfks+KzgHVZhepA== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsx -CzAJBgNVBAYTAkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRp -ZmljYWNpw7NuIERpZ2l0YWwgLSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwa -QUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4wHhcNMDYxMTI3MjA0NjI5WhcNMzAw -NDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+U29jaWVkYWQgQ2Ft -ZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJhIFMu -QS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkq -hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeG -qentLhM0R7LQcNzJPNCNyu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzL -fDe3fezTf3MZsGqy2IiKLUV0qPezuMDU2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQ -Y5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU34ojC2I+GdV75LaeHM/J4 -Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP2yYe68yQ -54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+b -MMCm8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48j -ilSH5L887uvDdUhfHjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++Ej -YfDIJss2yKHzMI+ko6Kh3VOz3vCaMh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/zt -A/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK5lw1omdMEWux+IBkAC1vImHF -rEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1bczwmPS9KvqfJ -pxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCB -lTCBkgYEVR0gADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFy -YS5jb20vZHBjLzBaBggrBgEFBQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW50 -7WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2UgcHVlZGVuIGVuY29udHJhciBlbiBs -YSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEfAygPU3zmpFmps4p6 -xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuXEpBc -unvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/ -Jre7Ir5v/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dp -ezy4ydV/NgIlqmjCMRW3MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42 -gzmRkBDI8ck1fj+404HGIGQatlDCIaR43NAvO2STdPCWkPHv+wlaNECW8DYSwaN0 -jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wkeZBWN7PGKX6jD/EpOe9+ -XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f/RWmnkJD -W2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/ -RL5hRqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35r -MDOhYil/SrnhLecUIw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxk -BYn8eNZcLCZDqQ== +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE @@ -235,79 +174,6 @@ c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 -b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMw -MTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML -QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYD -VQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ul -CDtbKRY654eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6n -tGO0/7Gcrjyvd7ZWxbWroulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyl -dI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1Zmne3yzxbrww2ywkEtvrNTVokMsAsJch -PXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJuiGMx1I4S+6+JNM3GOGvDC -+Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8wHQYDVR0O -BBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8E -BTADAQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBl -MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFk -ZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENB -IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxtZBsfzQ3duQH6lmM0MkhHma6X -7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0PhiVYrqW9yTkkz -43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY -eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJl -pz/+0WatC7xrmYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOA -WiFeIc9TVPC6b4nbqKqVz4vjccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 -b3JrMSAwHgYDVQQDExdBZGRUcnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAx -MDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtB -ZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIDAeBgNV -BAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV -6tsfSlbunyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nX -GCwwfQ56HmIexkvA/X1id9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnP -dzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSGAa2Il+tmzV7R/9x98oTaunet3IAIx6eH -1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAwHM+A+WD+eeSI8t0A65RF -62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0GA1UdDgQW -BBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUw -AwEB/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDEL -MAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRU -cnVzdCBUVFAgTmV0d29yazEgMB4GA1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJv -b3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4JNojVhaTdt02KLmuG7jD8WS6 -IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL+YPoRNWyQSW/ -iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao -GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh -4SINhwBk/ox9Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQm -XiLsks3/QppEIW1cxeMiHV9HEufOX1362KqxMy3ZdvJOOjMMK7MtkAY= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3 -b3JrMSMwIQYDVQQDExpBZGRUcnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1 -MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcxCzAJBgNVBAYTAlNFMRQwEgYDVQQK -EwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5ldHdvcmsxIzAh -BgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwq -xBb/4Oxx64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G -87B4pfYOQnrjfxvM0PC3KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i -2O+tCBGaKZnhqkRFmhJePp1tUvznoD1oL/BLcHwTOK28FSXx1s6rosAx1i+f4P8U -WfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GRwVY18BTcZTYJbqukB8c1 -0cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HUMIHRMB0G -A1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0T -AQH/BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6Fr -pGkwZzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQL -ExRBZGRUcnVzdCBUVFAgTmV0d29yazEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlm -aWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBABmrder4i2VhlRO6aQTv -hsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxGGuoYQ992zPlm -hpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X -dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3 -P6CxB9bpT9zeRXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9Y -iQBCYz95OdBEsIJuQRno3eDBiFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5no -xqE= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL @@ -392,81 +258,80 @@ aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc -MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP -bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2 -MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft -ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lk -hsmj76CGv2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym -1BW32J/X3HGrfpq/m44zDyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsW -OqMFf6Dch9Wc/HKpoH145LcxVR5lu9RhsCFg7RAycsWSJR74kEoYeEfffjA3PlAb -2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP8c9GsEsPPt2IYriMqQko -O3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAU -AK3Zo/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB -BQUAA4IBAQB8itEfGDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkF -Zu90821fnZmv9ov761KyBZiibyrFVL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAb -LjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft3OJvx8Fi8eNy1gTIdGcL+oir -oQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43gKd8hdIaC2y+C -MMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds -sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEc -MBoGA1UEChMTQW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBP -bmxpbmUgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2 -MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0Ft -ZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2EgT25saW5lIFJvb3Qg -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC -206B89enfHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFci -KtZHgVdEglZTvYYUAQv8f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2 -JxhP7JsowtS013wMPgwr38oE18aO6lhOqKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9 -BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JNRvCAOVIyD+OEsnpD8l7e -Xz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0gBe4lL8B -PeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67 -Xnfn6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEq -Z8A9W6Wa6897GqidFEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZ -o2C7HK2JNDJiuEMhBnIMoVxtRsX6Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3 -+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnjB453cMor9H124HhnAgMBAAGj -YzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3OpaaEg5+31IqEj -FNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmn -xPBUlgtk87FYT15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2 -LHo1YGwRgJfMqZJS5ivmae2p+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzccc -obGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXgJXUjhx5c3LqdsKyzadsXg8n33gy8 -CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//ZoyzH1kUQ7rVyZ2OuMe -IjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgOZtMA -DjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2F -AjgQ5ANh1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUX -Om/9riW99XJZZLF0KjhfGEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPb -AZO1XB4Y3WRayhgoPmMEEf0cjQAPuDffZ4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQl -Zvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuPcX/9XhmgD0uRuMRUvAaw -RY8mkaKO/qk= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEc -MBoGA1UEChMTSmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRp -b25DQTAeFw0wNzEyMTIxNTAwMDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYT -AkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zlcm5tZW50MRYwFAYDVQQLEw1BcHBs -aWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp23gdE6H -j6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4fl+K -f5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55 -IrmTwcrNwVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cw -FO5cjFW6WY2H/CPek9AEjP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDiht -QWEjdnjDuGWk81quzMKq2edY3rZ+nYVunyoKb58DKTCXKB28t89UKU5RMfkntigm -/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRUWssmP3HMlEYNllPqa0jQ -k/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNVBAYTAkpQ -MRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOC -seODvOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD -ggEBADlqRHZ3ODrso2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJ -hyzjVOGjprIIC8CFqMjSnHH2HZ9g/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+ -eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYDio+nEhEMy/0/ecGc/WLuo89U -DNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmWdupwX3kSa+Sj -B1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL -rosot4LKGAfmt1t06SAZf7IbiVQ= +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE @@ -546,26 +411,6 @@ ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg -Q2xhc3MgMiBDQSAxMB4XDTA2MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzEL -MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD -VQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7McXA0 -ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLX -l18xoS830r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVB -HfCuuCkslFJgNJQ72uA40Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B -5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/RuFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3 -WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNCMEAwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0PAQH/BAQD -AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLP -gcIV1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+ -DKhQ7SLHrQVMdvvt7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKu -BctN518fV4bVIJwo+28TOPX2EZL2fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHs -h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk -LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow @@ -597,26 +442,6 @@ I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3Mg -Q2xhc3MgMyBDQSAxMB4XDTA1MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzEL -MAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MR0wGwYD -VQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKxifZg -isRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//z -NIqeKNc0n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI -+MkcVyzwPX6UvCWThOiaAJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2R -hzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+ -mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNCMEAwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0PAQH/BAQD -AgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFP -Bdy7pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27s -EzNxZy5p+qksP2bAEllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2 -mSlf56oBzKwzqBwKu5HEA6BvtjT5htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yC -e/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQjel/wroQk5PMr+4okoyeYZdow -dXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow @@ -648,61 +473,6 @@ u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq 4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzET -MBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UE -AxMIQ0EgRGlzaWcwHhcNMDYwMzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQsw -CQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcg -YS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgmGErE -Nx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnX -mjxUizkDPw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYD -XcDtab86wYqg6I7ZuUUohwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhW -S8+2rT+MitcE5eN4TPWGqvWP+j1scaMtymfraHtuM6kMgiioTGohQBUgDCZbg8Kp -FhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8wgfwwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0PAQH/BAQD -AgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cu -ZGlzaWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5z -ay9jYS9jcmwvY2FfZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2sv -Y2EvY3JsL2NhX2Rpc2lnLmNybDAaBgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEw -DQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59tWDYcPQuBDRIrRhCA/ec8J9B6 -yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3mkkp7M5+cTxq -EEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/ -CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeB -EicTXxChds6KezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFN -PGO+I++MzVpQuGhU+QqZMxEA4Z7CRneC9VkGjCFMhwnN5ag= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIFaTCCA1GgAwIBAgIJAMMDmu5QkG4oMA0GCSqGSIb3DQEBBQUAMFIxCzAJBgNV -BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu -MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIxMB4XDTEyMDcxOTA5MDY1NloXDTQy -MDcxOTA5MDY1NlowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx -EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjEw -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCqw3j33Jijp1pedxiy3QRk -D2P9m5YJgNXoqqXinCaUOuiZc4yd39ffg/N4T0Dhf9Kn0uXKE5Pn7cZ3Xza1lK/o -OI7bm+V8u8yN63Vz4STN5qctGS7Y1oprFOsIYgrY3LMATcMjfF9DCCMyEtztDK3A -fQ+lekLZWnDZv6fXARz2m6uOt0qGeKAeVjGu74IKgEH3G8muqzIm1Cxr7X1r5OJe -IgpFy4QxTaz+29FHuvlglzmxZcfe+5nkCiKxLU3lSCZpq+Kq8/v8kiky6bM+TR8n -oc2OuRf7JT7JbvN32g0S9l3HuzYQ1VTW8+DiR0jm3hTaYVKvJrT1cU/J19IG32PK -/yHoWQbgCNWEFVP3Q+V8xaCJmGtzxmjOZd69fwX3se72V6FglcXM6pM6vpmumwKj -rckWtc7dXpl4fho5frLABaTAgqWjR56M6ly2vGfb5ipN0gTco65F97yLnByn1tUD -3AjLLhbKXEAz6GfDLuemROoRRRw1ZS0eRWEkG4IupZ0zXWX4Qfkuy5Q/H6MMMSRE -7cderVC6xkGbrPAXZcD4XW9boAo0PO7X6oifmPmvTiT6l7Jkdtqr9O3jw2Dv1fkC -yC2fg69naQanMVXVz0tv/wQFx1isXxYb5dKj6zHbHzMVTdDypVP1y+E9Tmgt2BLd -qvLmTZtJ5cUoobqwWsagtQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud -DwEB/wQEAwIBBjAdBgNVHQ4EFgQUiQq0OJMa5qvum5EY+fU8PjXQ04IwDQYJKoZI -hvcNAQEFBQADggIBADKL9p1Kyb4U5YysOMo6CdQbzoaz3evUuii+Eq5FLAR0rBNR -xVgYZk2C2tXck8An4b58n1KeElb21Zyp9HWc+jcSjxyT7Ff+Bw+r1RL3D65hXlaA -SfX8MPWbTx9BLxyE04nH4toCdu0Jz2zBuByDHBb6lM19oMgY0sidbvW9adRtPTXo -HqJPYNcHKfyyo6SdbhWSVhlMCrDpfNIZTUJG7L399ldb3Zh+pE3McgODWF3vkzpB -emOqfDqo9ayk0d2iLbYq/J8BjuIQscTK5GfbVSUZP/3oNn6z4eGBrxEWi1CXYBmC -AMBrTXO40RMHPuq2MU/wQppt4hF05ZSsjYSVPCGvxdpHyN85YmLLW1AL14FABZyb -7bq2ix4Eb5YgOe2kfSnbSM6C3NQCjR0EMVrHS/BsYVLXtFHCgWzN4funodKSds+x -DzdYpPJScWc/DIh4gInByLUfkmO+p3qKViwaqKactV2zY9ATIKHrkWzQjX2v3wvk -F7mGnjixlAxYjOBVqjtjbZqJYLhkKpLGN/R+Q0O3c+gB53+XD9fyexn9GtePyfqF -a3qdnom2piiZk4hA9z7NUaPK6u95RyG1/jLix8NRb76AdPCkwzryT+lf3xkK8jsT -Q6wxpLPn6/wY1gGp8yqPNg7rtLG8t0zJa7+h89n07eLw4+1knj0vllJPgFOL ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy @@ -734,24 +504,36 @@ zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJD -TjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2 -MDcwOTE0WhcNMjcwNDE2MDcwOTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMF -Q05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzDo+/hn7E7SIX1mlwh -IhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tizVHa6 -dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZO -V/kbZKKTVrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrC -GHn2emU1z5DrvTOTn1OrczvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gN -v7Sg2Ca+I19zN38m5pIEo3/PIKe38zrKy5nLAgMBAAGjczBxMBEGCWCGSAGG+EIB -AQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscCwQ7vptU7ETAPBgNVHRMB -Af8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991SlgrHAsEO -76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnK -OOK5Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvH -ugDnuL8BV8F3RTIMO/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7Hgvi -yJA/qIYM/PmLXoXLT1tLYhFHxUV8BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fL -buXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2G8kS1sHNzYDzAgE8yGnLRUhj -2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5mmxE= +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB @@ -795,60 +577,38 @@ fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEn -MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL -ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMg -b2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAxNjEzNDNaFw0zNzA5MzAxNjEzNDRa -MH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZpcm1hIFNBIENJRiBB -ODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3JnMSIw -IAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0B -AQEFAAOCAQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtb -unXF/KGIJPov7coISjlUxFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0d -BmpAPrMMhe5cG3nCYsS4No41XQEMIwRHNaqbYE6gZj3LJgqcQKH0XZi/caulAGgq -7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jWDA+wWFjbw2Y3npuRVDM3 -0pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFVd9oKDMyX -roDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIG -A1UdEwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5j -aGFtYmVyc2lnbi5vcmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p -26EpW1eLTXYGduHRooowDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIA -BzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hhbWJlcnNpZ24ub3JnMCcGA1Ud -EgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYDVR0gBFEwTzBN -BgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz -aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEB -AAxBl8IahsAifJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZd -p0AJPaxJRUXcLo0waLIJuvvDL8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi -1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wNUPf6s+xCX6ndbcj0dc97wXImsQEc -XCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/nADydb47kMgkdTXg0 -eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1erfu -tGWaIZDgqtCYvDi1czyL+Nw= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEn -MCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQL -ExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENo -YW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYxNDE4WhcNMzcwOTMwMTYxNDE4WjB9 -MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgy -NzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEgMB4G -A1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUA -A4IBDQAwggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0 -Mi+ITaFgCPS3CU6gSS9J1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/s -QJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8Oby4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpV -eAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl6DJWk0aJqCWKZQbua795 -B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c8lCrEqWh -z0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0T -AQH/BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1i -ZXJzaWduLm9yZy9jaGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4w -TcbOX60Qq+UDpfqpFDAOBgNVHQ8BAf8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAH -MCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBjaGFtYmVyc2lnbi5vcmcwKgYD -VR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9yZzBbBgNVHSAE -VDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh -bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0B -AQUFAAOCAQEAPDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUM -bKGKfKX0j//U2K0X1S0E0T9YgOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXi -ryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJPJ7oKXqJ1/6v/2j1pReQvayZzKWG -VwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4IBHNfTIzSJRUTN3c -ecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREest2d/ -AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A== +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV @@ -873,36 +633,36 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjET -MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAk -BgNVBAMMHUNlcnRpbm9taXMgLSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4 -Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNl -cnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYwJAYDVQQDDB1DZXJ0 -aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jY -F1AMnmHawE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N -8y4oH3DfVS9O7cdxbwlyLu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWe -rP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K -/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92NjMD2AR5vpTESOH2VwnHu -7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9qc1pkIuVC -28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6 -lSTClrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1E -nn1So2+WLhl+HPNbxxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB -0iSVL1N6aaLwD4ZFjliCK0wi1F6g530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql09 -5gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna4NH4+ej9Uji29YnfAgMBAAGj -WzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQN -jLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ -KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9s -ov3/4gbIOZ/xWqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZM -OH8oMDX/nyNTt7buFHAAQCvaR6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q -619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40nJ+U8/aGH88bc62UeYdocMMzpXDn -2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1BCxMjidPJC+iKunqj -o3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjvJL1v -nxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG -5ERQL1TEqkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWq -pdEdnV1j6CTmNhTih60bWfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZb -dsLLO7XSAPCjDuGtbkD326C00EauFddEwk01+dIL8hf2rGbVJLJP0RyZwG71fet0 -BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/vgt2Fl43N+bYdJeimUV5 +MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET +MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb +BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz +MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx +FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g +Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2 +fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl +LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV +WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF +TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb +5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc +CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri +wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ +wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG +m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4 +F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng +WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0 +2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF +AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/ +0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw +F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS +g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj +qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN +h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/ +ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V +btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj +Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ +8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW +gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw @@ -927,23 +687,49 @@ kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 l7+ijrRU -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBM -MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD -QTAeFw0wMjA2MTExMDQ2MzlaFw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBM -MRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBD -QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6xwS7TT3zNJc4YPk/E -jG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdLkKWo -ePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GI -ULdtlkIJ89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapu -Ob7kky/ZR6By6/qmW6/KUz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUg -AKpoC6EahQGcxEZjgoi2IrHu/qpGWX7PNSzVttpd90gzFFS269lvzs2I1qsb2pY7 -HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA -uI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+GXYkHAQa -TOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTg -xSvgGrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1q -CjqTE5s7FCMTY5w/0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5x -O/fIR/RpbxXyEV6DHpx8Uq79AtoSqFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs -6GAqm4VKQPNriiTsBhYscw== +MIIFazCCA1OgAwIBAgISESBVg+QtPlRWhS2DN7cs3EYRMA0GCSqGSIb3DQEBDQUA +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBa +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ANpQh7bauKk+nWT6VjOaVj0W5QOVsjQcmm1iBdTYj+eJZJ+622SLZOZ5KmHNr49a +iZFluVj8tANfkT8tEBXgfs+8/H9DZ6itXjYj2JizTfNDnjl8KvzsiNWI7nC9hRYt +6kuJPKNxQv4c/dMcLRC4hlTqQ7jbxofaqK6AJc96Jh2qkbBIb6613p7Y1/oA/caP +0FG7Yn2ksYyy/yARujVjBYZHYEMzkPZHogNPlk2dT8Hq6pyi/jQu3rfKG3akt62f +6ajUeD94/vI4CTYd0hYCyOwqaK/1jpTvLRN6HkJKHRUxrgwEV/xhc/MxVoYxgKDE +EW4wduOU8F8ExKyHcomYxZ3MVwia9Az8fXoFOvpHgDm2z4QTd28n6v+WZxcIbekN +1iNQMLAVdBM+5S//Ds3EC0pd8NgAM0lm66EYfFkuPSi5YXHLtaW6uOrc4nBvCGrc +h2c0798wct3zyT8j/zXhviEpIDCB5BmlIOklynMxdCm+4kLV87ImZsdo/Rmz5yCT +mehd4F6H50boJZwKKSTUzViGUkAksnsPmBIgJPaQbEfIDbsYIC7Z/fyL8inqh3SV +4EJQeIQEQWGw9CEjjy3LKCHyamz0GqbFFLQ3ZU+V/YDI+HLlJWvEYLF7bY5KinPO +WftwenMGE9nTdDckQQoRb5fc5+R+ob0V8rqHDz1oihYHAgMBAAGjYzBhMA4GA1Ud +DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSowcCbkahDFXxd +Bie0KlHYlwuBsTAfBgNVHSMEGDAWgBSowcCbkahDFXxdBie0KlHYlwuBsTANBgkq +hkiG9w0BAQ0FAAOCAgEAnFZvAX7RvUz1isbwJh/k4DgYzDLDKTudQSk0YcbX8ACh +66Ryj5QXvBMsdbRX7gp8CXrc1cqh0DQT+Hern+X+2B50ioUHj3/MeXrKls3N/U/7 +/SMNkPX0XtPGYX2eEeAC7gkE2Qfdpoq3DIMku4NQkv5gdRE+2J2winq14J2by5BS +S7CTKtQ+FjPlnsZlFT5kOwQ/2wyPX1wdaR+v8+khjPPvl/aatxm2hHSco1S1cE5j +2FddUyGbQJJD+tZ3VTNPZNX70Cxqjm0lpu+F6ALEUz65noe8zDUa3qHpimOHZR4R +Kttjd5cUvpoUmRGywO6wT/gUITJDT5+rosuoD6o7BlXGEilXCNQ314cnrUlZp5Gr +RHpejXDbl85IULFzk/bwg2D5zfHhMf1bfHEhYxQUqq/F3pN+aLHsIqKqkHWetUNy +6mSjhEv9DKgma3GX7lZjZuhCVPnHHd/Qj1vfyDBviP4NxDMcU6ij/UgQ8uQKTuEV +V/xuZDDCVRHc6qnNSlSsKWNEz0pAoNZoWRsz+e86i9sgktxChL8Bq4fA1SCC28a5 +g4VCXA9DO2pJNdWY9BW/+mGBDAkgGNLQFwzLSABQ6XaCjGTXOqAHVcweMcDvOrRl +++O/QmueD6i9a5jc2NvLi6Td11n0bt3+qsOR0C5CB8AMTVPNJLFMWx5R9N/pkvo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHDCCAaKgAwIBAgISESDZkc6uo+jF5//pAq/Pc7xVMAoGCCqGSM49BAMDMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBaMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABM0PW1aC3/BFGtat +93nwHcmsltaeTpwftEIRyoa/bfuFo8XlGVzX7qY/aWfYeOKmycTbLXku54uNAm8x +Ik0G42ByRZ0OQneezs/lf4WbGOT8zC5y0xaTTsqZY1yhBSpsBqNjMGEwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNqDYwJ5jtpMxjwj +FNiPwyCrKGBZMB8GA1UdIwQYMBaAFNqDYwJ5jtpMxjwjFNiPwyCrKGBZMAoGCCqG +SM49BAMDA2gAMGUCMHD+sAvZ94OX7PNVHdTcswYO/jOYnYs5kGuUIe22113WTNch +p+e/IQ8rzfcq3IUHnQIxAIYUFuXcsGXCwI4Un78kFmjlvPl5adytRSv3tjFzzAal +U5ORGpOucGpnutee5WEaXw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM @@ -968,6 +754,40 @@ VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI 03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYD VQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0 IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3 @@ -1010,74 +830,6 @@ OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZ d0jQ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIID9zCCAt+gAwIBAgIESJ8AATANBgkqhkiG9w0BAQUFADCBijELMAkGA1UEBhMC -Q04xMjAwBgNVBAoMKUNoaW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24g -Q2VudGVyMUcwRQYDVQQDDD5DaGluYSBJbnRlcm5ldCBOZXR3b3JrIEluZm9ybWF0 -aW9uIENlbnRlciBFViBDZXJ0aWZpY2F0ZXMgUm9vdDAeFw0xMDA4MzEwNzExMjVa -Fw0zMDA4MzEwNzExMjVaMIGKMQswCQYDVQQGEwJDTjEyMDAGA1UECgwpQ2hpbmEg -SW50ZXJuZXQgTmV0d29yayBJbmZvcm1hdGlvbiBDZW50ZXIxRzBFBgNVBAMMPkNo -aW5hIEludGVybmV0IE5ldHdvcmsgSW5mb3JtYXRpb24gQ2VudGVyIEVWIENlcnRp -ZmljYXRlcyBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm35z -7r07eKpkQ0H1UN+U8i6yjUqORlTSIRLIOTJCBumD1Z9S7eVnAztUwYyZmczpwA// -DdmEEbK40ctb3B75aDFk4Zv6dOtouSCV98YPjUesWgbdYavi7NifFy2cyjw1l1Vx -zUOFsUcW9SxTgHbP0wBkvUCZ3czY28Sf1hNfQYOL+Q2HklY0bBoQCxfVWhyXWIQ8 -hBouXJE0bhlffxdpxWXvayHG1VA6v2G5BY3vbzQ6sm8UY78WO5upKv23KzhmBsUs -4qpnHkWnjQRmQvaPK++IIGmPMowUc9orhpFjIpryp9vOiYurXccUwVswah+xt54u -gQEC7c+WXmPbqOY4twIDAQABo2MwYTAfBgNVHSMEGDAWgBR8cks5x8DbYqVPm6oY -NJKiyoOCWTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E -FgQUfHJLOcfA22KlT5uqGDSSosqDglkwDQYJKoZIhvcNAQEFBQADggEBACrDx0M3 -j92tpLIM7twUbY8opJhJywyA6vPtI2Z1fcXTIWd50XPFtQO3WKwMVC/GVhMPMdoG -52U7HW8228gd+f2ABsqjPWYWqJ1MFn3AlUa1UeTiH9fqBk1jjZaM7+czV0I664zB -echNdn3e9rG3geCg+aF4RhcaVpjwTj2rHO3sOdwHSPdj/gauwqRcalsyiMXHM4Ws -ZkJHwlgkmeHlPuV1LI5D1l08eB6olYIpUNHRFrrvwb562bTYzB5MRuF3sTGrvSrI -zo9uoV1/A3U05K2JRVRevq4opbs/eHnrc7MKDf2+yfdWrPa37S+bISnHOLaVxATy -wy39FCqQmbkHzJ8= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0 -MRMwEQYDVQQDEwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQG -EwJJTDAeFw0wNDAzMjQxMTMyMThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMT -CkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNpZ24xCzAJBgNVBAYTAklMMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49qROR+WCf4C9DklBKK -8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTyP2Q2 -98CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb -2CEJKHxNGGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxC -ejVb7Us6eva1jsz/D3zkYDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7Kpi -Xd3DTKaCQeQzC6zJMw9kglcq/QytNuEMrkvF7zuZ2SOzW120V+x0cAwqTwIDAQAB -o4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2Zl -ZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0PAQH/BAQD -AgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRL -AZs+VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWd -foPPbrxHbvUanlR2QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0M -cXS6hMTXcpuEfDhOZAYnKuGntewImbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq -8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb/627HOkthIDYIb6FUtnUdLlp -hbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VGzT2ouvDzuFYk -Res3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U -AGegcQCCSA== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAw -PDEbMBkGA1UEAxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWdu -MQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwx -GzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBDQTEQMA4GA1UEChMHQ29tU2lnbjEL -MAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGtWhf -HZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs49oh -gHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sW -v+bznkqH7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ue -Mv5WJDmyVIRD9YTC2LxBkMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr -9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d19guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt -6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUwAwEB/zBEBgNVHR8EPTA7 -MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29tU2lnblNl -Y3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58 -ADsAj8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkq -hkiG9w0BAQUFAAOCAQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7p -iL1DRYHjZiM/EoZNGeQFsOY3wo3aBijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtC -dsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtpFhpFfTMDZflScZAmlaxMDPWL -kz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP51qJThRv4zdL -hfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz -OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj @@ -1103,56 +855,6 @@ l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEb -MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow -GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRp -ZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVow -fjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G -A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAiBgNV -BAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPM -cm3ye5drswfxdySRXyWP9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3S -HpR7LZQdqnXXs5jLrLxkU0C8j6ysNstcrbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996 -CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rCoznl2yY4rYsK7hljxxwk -3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3Vp6ea5EQz -6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNV -HQ4EFgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud -EwEB/wQFMAMBAf8wgYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2Rv -Y2EuY29tL1NlY3VyZUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRw -Oi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmww -DQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm4J4oqF7Tt/Q0 -5qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj -Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtI -gKvcnDe4IRRLDXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJ -aD61JlfutuC23bkpgHl9j6PwpCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDl -izeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1HRR3B7Hzs/Sk= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb -MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow -GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0 -aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla -MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO -BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD -VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B -AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW -fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt -TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL -fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW -1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7 -kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G -A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v -ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo -dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu -Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/ -HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32 -pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS -jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+ -xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn -dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE @@ -1225,30 +927,6 @@ xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBb -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3Qx -ETAPBgNVBAsTCERTVCBBQ0VTMRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0w -MzExMjAyMTE5NThaFw0xNzExMjAyMTE5NThaMFsxCzAJBgNVBAYTAlVTMSAwHgYD -VQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UECxMIRFNUIEFDRVMx -FzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPu -ktKe1jzIDZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7 -gLFViYsx+tC3dr5BPTCapCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZH -fAjIgrrep4c9oW24MFbCswKBXy314powGCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4a -ahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPyMjwmR/onJALJfh1biEIT -ajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1UdEwEB/wQF -MAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rk -c3QuY29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjto -dHRwOi8vd3d3LnRydXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMt -aW5kZXguaHRtbDAdBgNVHQ4EFgQUCXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZI -hvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V25FYrnJmQ6AgwbN99Pe7lv7Uk -QIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6tFr8hlxCBPeP/ -h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq -nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpR -rscL9yuwNwXsvFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf2 -9w4LTJxoeHtxMcfrHuBnQfO3oKfN5XozNmr6mis= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow @@ -1313,6 +991,43 @@ H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe +o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD @@ -1335,6 +1050,43 @@ YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j @@ -1358,64 +1110,36 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV -UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL -EwhEU1RDQSBFMTAeFw05ODEyMTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJ -BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x -ETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCg -bIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJENySZ -j9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlV -Sn5JTe2io74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCG -SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx -JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI -RFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMTAxODEw -MjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFGp5 -fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i -+DAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG -SIb3DQEBBQUAA4GBACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lN -QseSJqBcNJo4cvj9axY+IO6CizEqkzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+ -gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4RbyhkwS7hp86W0N6w4pl ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV -UzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQL -EwhEU1RDQSBFMjAeFw05ODEyMDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJ -BgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4x -ETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC/ -k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGODVvso -LeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3o -TQPMx7JSxhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCG -SAGG+EIBAQQEAwIABzBoBgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMx -JDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0dXJlIFRydXN0IENvLjERMA8GA1UECxMI -RFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQwIoAPMTk5ODEyMDkxOTE3 -MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQYMBaAFB6C -TShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5 -WzAMBgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqG -SIb3DQEBBQUAA4GBAEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHR -xdf0CiUPPXiBng+xZ8SQTGPdXqfiup/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVL -B3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1mPnHfxsb1gYgAlihw6ID ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1 -MQswCQYDVQQGEwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxp -Z2kgQS5TLjE8MDoGA1UEAxMzZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZp -a2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3MDEwNDExMzI0OFoXDTE3MDEwNDEx -MzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0cm9uaWsgQmlsZ2kg -R3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9uaWsg -U2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdU -MZTe1RK6UxYC6lhj71vY8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlT -L/jDj/6z/P2douNffb7tC+Bg62nsM+3YjfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H -5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAIJjjcJRFHLfO6IxClv7wC -90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk9Ok0oSy1 -c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoE -VtstxNulMA0GCSqGSIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLP -qk/CaOv/gKlR6D1id4k9CnU58W5dF4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S -/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwqD2fK/A+JYZ1lpTzlvBNbCNvj -/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4Vwpm+Vganf2X -KWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq -fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNV @@ -1454,40 +1178,6 @@ y4Q08ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8d NL/+I5c30jn6PQ0GC7TbO6Orb1wdtn7os4I07QZcJA== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNV -BAMML0VCRyBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx -c8SxMTcwNQYDVQQKDC5FQkcgQmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXpt -ZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAeFw0wNjA4MTcwMDIxMDlaFw0xNjA4 -MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25payBTZXJ0aWZpa2Eg -SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2ltIFRl -a25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h -4fuXd7hxlugTlkaDT7byX3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAk -tiHq6yOU/im/+4mRDGSaBUorzAzu8T2bgmmkTPiab+ci2hC6X5L8GCcKqKpE+i4s -tPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfreYteIAbTdgtsApWjluTL -dlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZTqNGFav4 -c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8Um -TDGyY5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z -+kI2sSXFCjEmN1ZnuqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0O -Lna9XvNRiYuoP1Vzv9s6xiQFlpJIqkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMW -OeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vmExH8nYQKE3vwO9D8owrXieqW -fo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0Nokb+Clsi7n2 -l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB -/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgw -FoAU587GT/wWZ5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+ -8ygjdsZs93/mQJ7ANtyVDR2tFcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI -6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgmzJNSroIBk5DKd8pNSe/iWtkqvTDO -TLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64kXPBfrAowzIpAoHME -wfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqTbCmY -Iai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJn -xk1Gj7sURT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4Q -DgZxGhBM/nV+/x5XOULK1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9q -Kd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11t -hie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQY9iJSrSq3RZj9W6+YKH4 -7ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9AahH3eU7 -QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB 8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 @@ -1568,34 +1258,6 @@ bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er fF6adulZkMV8gzURZVE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC -VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u -ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc -KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u -ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 -MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE -ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j -b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF -bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg -U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA -A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ -I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 -wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC -AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb -oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 -BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p -dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk -MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp -b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu -dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 -MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi -E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa -MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI -hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN -95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd -2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW @@ -1623,70 +1285,79 @@ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV -UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy -dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 -MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx -dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B -AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f -BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A -cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC -AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ -MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm -aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw -ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj -IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF -MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA -A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y -7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh -1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEc -MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBT -ZWN1cmUgR2xvYmFsIGVCdXNpbmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIw -MDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMxHDAaBgNVBAoTE0VxdWlmYXggU2Vj -dXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEdsb2JhbCBlQnVzaW5l -c3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRVPEnC -UdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc -58O/gGzNqfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/ -o5brhTMhHD4ePmBudpxnhcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAH -MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUvqigdHJQa0S3ySPY+6j/s1dr -aGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hsMA0GCSqGSIb3DQEBBAUA -A4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okENI7SS+RkA -Z70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv -8qIYNMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEc -MBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBT -ZWN1cmUgZUJ1c2luZXNzIENBLTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQw -MDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMTRXF1aWZheCBTZWN1cmUgSW5j -LjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENBLTEwgZ8wDQYJ -KoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ1MRo -RvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBu -WqDZQu4aIZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKw -Env+j6YDAgMBAAGjZjBkMBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTAD -AQH/MB8GA1UdIwQYMBaAFEp4MlIR21kWNl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRK -eDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQFAAOBgQB1W6ibAxHm6VZM -zfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5lSE/9dR+ -WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN -/Bf+KpYrtWKmpj29f5JZzVoqgrI3eQ== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYD -VQQKEw9HVEUgQ29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNv -bHV0aW9ucywgSW5jLjEjMCEGA1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJv -b3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEzMjM1OTAwWjB1MQswCQYDVQQGEwJV -UzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQLEx5HVEUgQ3liZXJU -cnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0IEds -b2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrH -iM3dFw4usJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTS -r41tiGeA5u2ylc9yMcqlHHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X4 -04Wqk2kmhXBIgD8SFcd5tB8FLztimQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAG3r -GwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMWM4ETCJ57NE7fQMh017l9 -3PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OFNMQkpw0P -lZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT @@ -1709,27 +1380,6 @@ hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEW -MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFs -IENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQG -EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3Qg -R2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvPE1A -PRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/NTL8 -Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hL -TytCOb1kLUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL -5mkWRxHCJ1kDs6ZgwiFAVvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7 -S4wMcoKK+xfNAGw6EzywhIdLFnopsk/bHdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe -2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE -FHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNHK266ZUap -EBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6td -EPx7srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv -/NgdRN3ggX+d6YvhZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywN -A0ZF66D0f0hExghAzN4bcLUprbqLOzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0 -abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkCx1YAzUm5s2x7UwQa4qjJqhIF -I8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqFH4z1Ir+rzoPz -4iIprn2DQKi6bA== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx @@ -1854,6 +1504,33 @@ OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ +FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F +uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX +kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs +ewv4n4Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw @@ -2006,6 +1683,23 @@ LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI 4uJEvlz36hz1 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p @@ -2031,6 +1725,41 @@ Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI l7WdmplNsDz4SgCbZN2fOUvRJ9e4 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsx FjAUBgNVBAoTDUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3Qg Um9vdCBDQSAxMB4XDTAzMDUxNTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkG @@ -2051,28 +1780,97 @@ fMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilTc4afU9hDDl3WY4JxHYB0yvbi AmvZWg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT -AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ -TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG -9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw -MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM -BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO -MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 -LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI -s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 -xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 -u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b -F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx -Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd -PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV -HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx -NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF -AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ -L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY -YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg -Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a -NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R -0982gaEbeC9xs/FZTEYYKKuF0mBWWg== +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 @@ -2109,76 +1907,37 @@ naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN -AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp -dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw -MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw -CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ -MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB -SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz -ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH -LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP -PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL -2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w -ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC -MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk -AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0 -AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz -AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz -AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f -BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE -FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY -P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi -CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g -kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95 -HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS -na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q -qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z -TbvGRNs2yyqcjg== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAw -cjELMAkGA1UEBhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNy -b3NlYyBMdGQuMRQwEgYDVQQLEwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9z -ZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0MDYxMjI4NDRaFw0xNzA0MDYxMjI4 -NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEWMBQGA1UEChMN -TWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMTGU1p -Y3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2u -uO/TEdyB5s87lozWbxXGd36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+ -LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/NoqdNAoI/gqyFxuEPkEeZlApxcpMqyabA -vjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjcQR/Ji3HWVBTji1R4P770 -Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJPqW+jqpx -62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcB -AQRbMFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3Aw -LQYIKwYBBQUHMAKGIWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAP -BgNVHRMBAf8EBTADAQH/MIIBcwYDVR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIB -AQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3LmUtc3ppZ25vLmh1L1NaU1ov -MIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0AdAB2AOEAbgB5 -ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn -AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABT -AHoAbwBsAGcA4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABh -ACAAcwB6AGUAcgBpAG4AdAAgAGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABo -AHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMAegBpAGcAbgBvAC4AaAB1AC8AUwBa -AFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6Ly93d3cuZS1zemln -bm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NOPU1p -Y3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxP -PU1pY3Jvc2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZv -Y2F0aW9uTGlzdDtiaW5hcnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuB -EGluZm9AZS1zemlnbm8uaHWkdzB1MSMwIQYDVQQDDBpNaWNyb3NlYyBlLVN6aWdu -w7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhTWjEWMBQGA1UEChMNTWlj -cm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhVMIGsBgNV -HSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJI -VTERMA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDAS -BgNVBAsTC2UtU3ppZ25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBS -b290IENBghEAzLjnv04pGv2i3GalHCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS -8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMTnGZjWS7KXHAM/IO8VbH0jgds -ZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FEaGAHQzAxQmHl -7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a -86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfR -hUZLphK3dehKyVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/ -MPMMNz7UwiiAc7EBt51alhQBS6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU= +MIIFwzCCA6ugAwIBAgIUCn6m30tEntpqJIWe5rgV0xZ/u7EwDQYJKoZIhvcNAQEL +BQAwRjELMAkGA1UEBhMCTFUxFjAUBgNVBAoMDUx1eFRydXN0IFMuQS4xHzAdBgNV +BAMMFkx1eFRydXN0IEdsb2JhbCBSb290IDIwHhcNMTUwMzA1MTMyMTU3WhcNMzUw +MzA1MTMyMTU3WjBGMQswCQYDVQQGEwJMVTEWMBQGA1UECgwNTHV4VHJ1c3QgUy5B +LjEfMB0GA1UEAwwWTHV4VHJ1c3QgR2xvYmFsIFJvb3QgMjCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANeFl78RmOnwYoNMPIf5U2o3C/IPPIfOb9wmKb3F +ibrJgz337spbxm1Jc7TJRqMbNBM/wYlFV/TZsfs2ZUv7COJIcRHIbjuend+JZTem +hfY7RBi2xjcwYkSSl2l9QjAk5A0MiWtj3sXh306pFGxT4GHO9hcvHTy95iJMHZP1 +EMShduxq3sVs35a0VkBCwGKSMKEtFZSg0iAGCW5qbeXrt77U8PEVfIvmTroTzEsn +Xpk8F12PgX8zPU/TPxvsXD/wPEx1bvKm1Z3aLQdjAsZy6ZS8TEmVT4hSyNvoaYL4 +zDRbIvCGp4m9SAptZoFtyMhk+wHh9OHe2Z7d21vUKpkmFRseTJIpgp7VkoGSQXAZ +96Tlk0u8d2cx3Rz9MXANF5kM+Qw5GSoXtTBxVdUPrljhPS80m8+f9niFwpN6cj5m +j5wWEWCPnolvZ77gR1o7DJpni89Gxq44o/KnvObWhWszJHAiS8sIm7vI+AIpHb4g +DEa/a4ebsypmQjVGbKq6rfmYe+lQVRQxv7HaLe2ArWgk+2mr2HETMOZns4dA/Yl+ +8kPREd8vZS9kzl8UubG/Mb2HeFpZZYiq/FkySIbWTLkpS5XTdvN3JW1CHDiDTf2j +X5t/Lax5Gw5CMZdjpPuKadUiDTSQMC6otOBttpSsvItO13D8xTiOZCXhTTmQzsmH +hFhxAgMBAAGjgagwgaUwDwYDVR0TAQH/BAUwAwEB/zBCBgNVHSAEOzA5MDcGByuB +KwEBAQowLDAqBggrBgEFBQcCARYeaHR0cHM6Ly9yZXBvc2l0b3J5Lmx1eHRydXN0 +Lmx1MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBT/GCh2+UgFLKGu8SsbK7JT ++Et8szAdBgNVHQ4EFgQU/xgodvlIBSyhrvErGyuyU/hLfLMwDQYJKoZIhvcNAQEL +BQADggIBAGoZFO1uecEsh9QNcH7X9njJCwROxLHOk3D+sFTAMs2ZMGQXvw/l4jP9 +BzZAcg4atmpZ1gDlaCDdLnINH2pkMSCEfUmmWjfrRcmF9dTHF5kH5ptV5AzoqbTO +jFu1EVzPig4N1qx3gf4ynCSecs5U89BvolbW7MM3LGVYvlcAGvI1+ut7MV3CwRI9 +loGIlonBWVx65n9wNOeD4rHh4bhY79SV5GCc8JaXcozrhAIuZY+kt9J/Z93I055c +qqmkoCUUBpvsT34tC38ddfEz2O3OuHVtPlu5mB0xDVbYQw8wkbIEa91WvpWAVWe+ +2M2D2RjuLg+GLZKecBPs3lHJQ3gCpU3I+V/EkVhGFndadKpAvAefMLmx9xIX3eP/ +JEAdemrRTxgKqpAd60Ae36EeRJIQmvKN4dFLRp7oRUKX6kWZ8+xm1QL68qZKJKre +zrnK+T+Tb/mjuuqlPpmt/f97mfVl7vBZKGfXkJWkE4SphMHozs51k2MavDzq1WQf +LSoSOcbDWjLtR5EWDrw4wVDej8oqkDQc7kGUnF4ZLvhFSZl0kbAEb+MEWrGrKqv+ +x9CWttrhSmQGbmBNvUJO/3jaJMobtNeWOWyu8Q6qp31IiyBMz2TWuJdGsE7RKlY6 +oJO9r4Ak4Ap+58rVyuiFVdw2KuGUaJPHZnJED4AhMmwlxyOAgwrr -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD @@ -2229,144 +1988,6 @@ uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUx -ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 -b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQD -EylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikgVGFudXNpdHZhbnlraWFkbzAeFw05 -OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYDVQQGEwJIVTERMA8G -A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh -Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5l -dExvY2sgVXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqG -SIb3DQEBAQUAA4GNADCBiQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xK -gZjupNTKihe5In+DCnVMm8Bp2GQ5o+2So/1bXHQawEfKOml2mrriRBf8TKPV/riX -iK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr1nGTLbO/CVRY7QbrqHvc -Q7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8E -BAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1G -SUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFu -b3MgU3pvbGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBh -bGFwamFuIGtlc3p1bHQuIEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExv -Y2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGln -aXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0 -IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh -c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGph -biBhIGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJo -ZXRvIGF6IGVsbGVub3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBP -UlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmlj -YXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBo -dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNA -bmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06 -sPgzTEdM43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXa -n3BukxowOR0w2y7jfLKRstE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKS -NitjrFgBazMpUIaD8QFI ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUx -ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 -b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQD -EytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBDKSBUYW51c2l0dmFueWtpYWRvMB4X -DTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJBgNVBAYTAkhVMREw -DwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9u -c2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMr -TmV0TG9jayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzAN -BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNA -OoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3ZW3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC -2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63euyucYT2BDMIJTLrdKwW -RMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQwDgYDVR0P -AQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEW -ggJNRklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0 -YWxhbm9zIFN6b2xnYWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFz -b2sgYWxhcGphbiBrZXN6dWx0LiBBIGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBO -ZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1iaXp0b3NpdGFzYSB2ZWRpLiBB -IGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0ZWxlIGF6IGVs -b2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs -ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25s -YXBqYW4gYSBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kg -a2VyaGV0byBheiBlbGxlbm9yemVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4g -SU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5kIHRoZSB1c2Ugb2YgdGhpcyBjZXJ0 -aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQUyBhdmFpbGFibGUg -YXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwgYXQg -Y3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmY -ta3UzbM2xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2g -pO0u9f38vf5NNwgMvOOWgyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4 -Fp1hBWeAyNDYpQcCNJgEjTME1A== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhV -MRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMe -TmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0 -dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFzcyBB -KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oXDTE5MDIxOTIzMTQ0 -N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQHEwhC -dWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQu -MRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBL -b3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSMD7tM9DceqQWC2ObhbHDqeLVu0ThEDaiD -zl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZz+qMkjvN9wfcZnSX9EUi -3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC/tmwqcm8 -WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LY -Oph7tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2Esi -NCubMvJIH5+hCoR64sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCC -ApswDgYDVR0PAQH/BAQDAgAGMBIGA1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4 -QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZRUxFTSEgRXplbiB0 -YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRhdGFz -aSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu -IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtm -ZWxlbG9zc2VnLWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMg -ZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVs -amFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJhc2EgbWVndGFsYWxoYXRv -IGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBzOi8vd3d3 -Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6 -ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1 -YW5jZSBhbmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3Qg -dG8gdGhlIE5ldExvY2sgQ1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRs -b2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBjcHNAbmV0bG9jay5uZXQuMA0G -CSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5ayZrU3/b39/zcT0mwBQO -xmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjPytoUMaFP -0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQ -QeJBCWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxk -f1qbFFgBJ34TUMdrKuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK -8CtmdWOMovsEPoMOmzbwGOQmIMOM8CgHrTwXZoi1/baI ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUx -ETAPBgNVBAcTCEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0 -b25zYWdpIEtmdC4xGjAYBgNVBAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQD -EzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVneXpvaSAoQ2xhc3MgUUEpIFRhbnVz -aXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0bG9jay5odTAeFw0w -MzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTERMA8G -A1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNh -Z2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5l -dExvY2sgTWlub3NpdGV0dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZh -bnlraWFkbzEeMBwGCSqGSIb3DQEJARYPaW5mb0BuZXRsb2NrLmh1MIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRVCacbvWy5FPSKAtt2/Goq -eKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e8ia6AFQe -r7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO5 -3Lhbm+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWd -vLrqOU+L73Sa58XQ0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0l -mT+1fMptsK6ZmfoIYOcZwvK9UdPM0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4IC -wDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNVHQ8BAf8EBAMCAQYwggJ1Bglg -hkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2YW55IGEgTmV0 -TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh -biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQg -ZWxla3Ryb25pa3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywg -dmFsYW1pbnQgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6 -b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwgYXogQWx0YWxhbm9zIFN6ZXJ6b2Rl -c2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kgZWxqYXJhcyBtZWd0 -ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczovL3d3 -dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0Bu -ZXRsb2NrLm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBh -bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRo -ZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMgYXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3 -Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0IGluZm9AbmV0bG9jay5u -ZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3DQEBBQUA -A4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQ -MznNwNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+ -NFAwLvt/MpqNPfMgW/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCR -VCHnpgu0mfVRQdzNo0ci2ccBgcTcR08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY -83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR5qq5aKrN9p2QdRLqOBrKROi3 -macqaJVmlaut74nLYKkGEsaUR+ko ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBi MQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3Jp @@ -2414,57 +2035,104 @@ Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ /L7fCg0= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIJhjCCB26gAwIBAgIBCzANBgkqhkiG9w0BAQsFADCCAR4xPjA8BgNVBAMTNUF1 -dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIFJhaXogZGVsIEVzdGFkbyBWZW5lem9s -YW5vMQswCQYDVQQGEwJWRTEQMA4GA1UEBxMHQ2FyYWNhczEZMBcGA1UECBMQRGlz -dHJpdG8gQ2FwaXRhbDE2MDQGA1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0 -aWZpY2FjaW9uIEVsZWN0cm9uaWNhMUMwQQYDVQQLEzpTdXBlcmludGVuZGVuY2lh -IGRlIFNlcnZpY2lvcyBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9uaWNhMSUwIwYJ -KoZIhvcNAQkBFhZhY3JhaXpAc3VzY2VydGUuZ29iLnZlMB4XDTEwMTIyODE2NTEw -MFoXDTIwMTIyNTIzNTk1OVowgdExJjAkBgkqhkiG9w0BCQEWF2NvbnRhY3RvQHBy -b2NlcnQubmV0LnZlMQ8wDQYDVQQHEwZDaGFjYW8xEDAOBgNVBAgTB01pcmFuZGEx -KjAoBgNVBAsTIVByb3ZlZWRvciBkZSBDZXJ0aWZpY2Fkb3MgUFJPQ0VSVDE2MDQG -A1UEChMtU2lzdGVtYSBOYWNpb25hbCBkZSBDZXJ0aWZpY2FjaW9uIEVsZWN0cm9u -aWNhMQswCQYDVQQGEwJWRTETMBEGA1UEAxMKUFNDUHJvY2VydDCCAiIwDQYJKoZI -hvcNAQEBBQADggIPADCCAgoCggIBANW39KOUM6FGqVVhSQ2oh3NekS1wwQYalNo9 -7BVCwfWMrmoX8Yqt/ICV6oNEolt6Vc5Pp6XVurgfoCfAUFM+jbnADrgV3NZs+J74 -BCXfgI8Qhd19L3uA3VcAZCP4bsm+lU/hdezgfl6VzbHvvnpC2Mks0+saGiKLt38G -ieU89RLAu9MLmV+QfI4tL3czkkohRqipCKzx9hEC2ZUWno0vluYC3XXCFCpa1sl9 -JcLB/KpnheLsvtF8PPqv1W7/U0HU9TI4seJfxPmOEO8GqQKJ/+MMbpfg353bIdD0 -PghpbNjU5Db4g7ayNo+c7zo3Fn2/omnXO1ty0K+qP1xmk6wKImG20qCZyFSTXai2 -0b1dCl53lKItwIKOvMoDKjSuc/HUtQy9vmebVOvh+qBa7Dh+PsHMosdEMXXqP+UH -0quhJZb25uSgXTcYOWEAM11G1ADEtMo88aKjPvM6/2kwLkDd9p+cJsmWN63nOaK/ -6mnbVSKVUyqUtd+tFjiBdWbjxywbk5yqjKPK2Ww8F22c3HxT4CAnQzb5EuE8XL1m -v6JpIzi4mWCZDlZTOpx+FIywBm/xhnaQr/2v/pDGj59/i5IjnOcVdo/Vi5QTcmn7 -K2FjiO/mpF7moxdqWEfLcU8UC17IAggmosvpr2uKGcfLFFb14dq12fy/czja+eev -bqQ34gcnAgMBAAGjggMXMIIDEzASBgNVHRMBAf8ECDAGAQH/AgEBMDcGA1UdEgQw -MC6CD3N1c2NlcnRlLmdvYi52ZaAbBgVghl4CAqASDBBSSUYtRy0yMDAwNDAzNi0w -MB0GA1UdDgQWBBRBDxk4qpl/Qguk1yeYVKIXTC1RVDCCAVAGA1UdIwSCAUcwggFD -gBStuyIdxuDSAaj9dlBSk+2YwU2u06GCASakggEiMIIBHjE+MDwGA1UEAxM1QXV0 -b3JpZGFkIGRlIENlcnRpZmljYWNpb24gUmFpeiBkZWwgRXN0YWRvIFZlbmV6b2xh -bm8xCzAJBgNVBAYTAlZFMRAwDgYDVQQHEwdDYXJhY2FzMRkwFwYDVQQIExBEaXN0 -cml0byBDYXBpdGFsMTYwNAYDVQQKEy1TaXN0ZW1hIE5hY2lvbmFsIGRlIENlcnRp -ZmljYWNpb24gRWxlY3Ryb25pY2ExQzBBBgNVBAsTOlN1cGVyaW50ZW5kZW5jaWEg -ZGUgU2VydmljaW9zIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExJTAjBgkq -hkiG9w0BCQEWFmFjcmFpekBzdXNjZXJ0ZS5nb2IudmWCAQowDgYDVR0PAQH/BAQD -AgEGME0GA1UdEQRGMESCDnByb2NlcnQubmV0LnZloBUGBWCGXgIBoAwMClBTQy0w -MDAwMDKgGwYFYIZeAgKgEgwQUklGLUotMzE2MzUzNzMtNzB2BgNVHR8EbzBtMEag -RKBChkBodHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9sY3IvQ0VSVElGSUNBRE8t -UkFJWi1TSEEzODRDUkxERVIuY3JsMCOgIaAfhh1sZGFwOi8vYWNyYWl6LnN1c2Nl -cnRlLmdvYi52ZTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUHMAGGG2h0dHA6Ly9v -Y3NwLnN1c2NlcnRlLmdvYi52ZTBBBgNVHSAEOjA4MDYGBmCGXgMBAjAsMCoGCCsG -AQUFBwIBFh5odHRwOi8vd3d3LnN1c2NlcnRlLmdvYi52ZS9kcGMwDQYJKoZIhvcN -AQELBQADggIBACtZ6yKZu4SqT96QxtGGcSOeSwORR3C7wJJg7ODU523G0+1ng3dS -1fLld6c2suNUvtm7CpsR72H0xpkzmfWvADmNg7+mvTV+LFwxNG9s2/NkAZiqlCxB -3RWGymspThbASfzXg0gTB1GEMVKIu4YXx2sviiCtxQuPcD4quxtxj7mkoP3Yldmv -Wb8lK5jpY5MvYB7Eqvh39YtsL+1+LrVPQA3uvFd359m21D+VJzog1eWuq2w1n8Gh -HVnchIHuTQfiSLaeS5UtQbHh6N5+LwUeaO6/u5BlOsju6rEYNxxik6SgMexxbJHm -pHmJWhSnFFAFTKQAVzAswbVhltw+HoSvOULP5dAssSS830DD7X9jSr3hTxJkhpXz -sOfIt+FTvZLm8wyWuevo5pLtp4EJFAv8lXrPj9Y0TzYS3F7RNHXGRoAvlQSMx4bE -qCaJqD8Zm4G7UaRKhqsLEQ+xrmNTbSjq3TNWOByyrYDT13K9mmyZY+gAu0F2Bbdb -mRiKw7gSXFbPVgx96OLP7bx0R/vu0xdOIk9W/1DzLuY5poLWccret9W6aAjtmcz9 -opLLabid+Qqkpj5PkygqYWwHJgD/ll9ohri4zspV4KuxPX+Y1zMOWj3YeMLEYC/H -YvBhkdI4sPaeVdtAgAUSM84dkpvRabP/v/GSCmE1P93+hvS84Bpxs2Km +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESCzkFU5fX82bWTCp59rY45nMA0GCSqGSIb3DQEBCwUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcxMB4XDTE0MDUyNjA4NDU1MFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQD4eUbalsUwXopxAy1wpLuwxQjczeY1wICkES3d5oeuXT2R0odsN7faYp6b +wiTXj/HbpqbfRm9RpnHLPhsxZ2L3EVs0J9V5ToybWL0iEA1cJwzdMOWo010hOHQX +/uMftk87ay3bfWAfjH1MBcLrARYVmBSO0ZB3Ij/swjm4eTrwSSTilZHcYTSSjFR0 +77F9jAHiOH3BX2pfJLKOYheteSCtqx234LSWSE9mQxAGFiQD4eCcjsZGT44ameGP +uY4zbGneWK2gDqdkVBFpRGZPTBKnjix9xNRbxQA0MMHZmf4yzgeEtE7NCv82TWLx +p2NX5Ntqp66/K7nJ5rInieV+mhxNaMbBGN4zK1FGSxyO9z0M+Yo0FMT7MzUj8czx +Kselu7Cizv5Ta01BG2Yospb6p64KTrk5M0ScdMGTHPjgniQlQ/GbI4Kq3ywgsNw2 +TgOzfALU5nsaqocTvz6hdLubDuHAk5/XpGbKuxs74zD0M1mKB3IDVedzagMxbm+W +G+Oin6+Sx+31QrclTDsTBM8clq8cIqPQqwWyTBIjUtz9GVsnnB47ev1CI9sjgBPw +vFEVVJSmdz7QdFG9URQIOTfLHzSpMJ1ShC5VkLG631UAC9hWLbFJSXKAqWLXwPYY +EQRVzXR7z2FwefR7LFxckvzluFqrTJOVoSfupb7PcSNCupt2LQIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUl0YhVyE1 +2jZVx/PxN3DlCPaTKbYwHwYDVR0jBBgwFoAUl0YhVyE12jZVx/PxN3DlCPaTKbYw +DQYJKoZIhvcNAQELBQADggIBAB3dAmB84DWn5ph76kTOZ0BP8pNuZtQ5iSas000E +PLuHIT839HEl2ku6q5aCgZG27dmxpGWX4m9kWaSW7mDKHyP7Rbr/jyTwyqkxf3kf +gLMtMrpkZ2CvuVnN35pJ06iCsfmYlIrM4LvgBBuZYLFGZdwIorJGnkSI6pN+VxbS +FXJfLkur1J1juONI5f6ELlgKn0Md/rcYkoZDSw6cMoYsYPXpSOqV7XAp8dUv/TW0 +V8/bhUiZucJvbI/NeJWsZCj9VrDDb8O+WVLhX4SPgPL0DTatdrOjteFkdjpY3H1P +XlZs5VVZV6Xf8YpmMIzUUmI4d7S+KNfKNsSbBfD4Fdvb8e80nR14SohWZ25g/4/I +i+GOvUKpMwpZQhISKvqxnUOOBZuZ2mKtVzazHbYNeS2WuOvyDEsMpZTGMKcmGS3t +TAZQMPH9WD25SxdfGbRqhFS0OE85og2WaMMolP3tLR9Ka0OWLpABEPs4poEL0L91 +09S5zvE/bw4cHjdx5RiHdRk/ULlepEU0rbDK5uUTdg8xFKmOLZTW1YVNcxVPS/Ky +Pu1svf0OnWZzsD2097+o4BGkxK51CUpjAEggpsadCwmKtODmzj7HPiY46SvepghJ +AwSQiumPv+i2tCqjI40cHLI5kqiPAlxAOXXUc0ECd97N4EOH1uS6SsNsEn/+KuYj +1oxx +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESChaRu/vbm9UpaPI+hIvyYRMA0GCSqGSIb3DQEBDQUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcyMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDMtlelM5QQgTJT32F+D3Y5z1zCU3UdSXqWON2ic2rxb95eolq5cSG+Ntmh +/LzubKh8NBpxGuga2F8ORAbtp+Dz0mEL4DKiltE48MLaARf85KxP6O6JHnSrT78e +CbY2albz4e6WiWYkBuTNQjpK3eCasMSCRbP+yatcfD7J6xcvDH1urqWPyKwlCm/6 +1UWY0jUJ9gNDlP7ZvyCVeYCYitmJNbtRG6Q3ffyZO6v/v6wNj0OxmXsWEH4db0fE +FY8ElggGQgT4hNYdvJGmQr5J1WqIP7wtUdGejeBSzFfdNTVY27SPJIjki9/ca1TS +gSuyzpJLHB9G+h3Ykst2Z7UJmQnlrBcUVXDGPKBWCgOz3GIZ38i1MH/1PCZ1Eb3X +G7OHngevZXHloM8apwkQHZOJZlvoPGIytbU6bumFAYueQ4xncyhZW+vj3CzMpSZy +YhK05pyDRPZRpOLAeiRXyg6lPzq1O4vldu5w5pLeFlwoW5cZJ5L+epJUzpM5ChaH +vGOz9bGTXOBut9Dq+WIyiET7vycotjCVXRIouZW+j1MY5aIYFuJWpLIsEPUdN6b4 +t/bQWVyJ98LVtZR00dX+G7bw5tYee9I8y6jj9RjzIR9u701oBnstXW5DiabA+aC/ +gh7PU3+06yzbXfZqfUAkBXKJOAGTy3HCOV0GEfZvePg3DTmEJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUajn6QiL3 +5okATV59M4PLuG53hq8wHwYDVR0jBBgwFoAUajn6QiL35okATV59M4PLuG53hq8w +DQYJKoZIhvcNAQENBQADggIBAJjLq0A85TMCl38th6aP1F5Kr7ge57tx+4BkJamz +Gj5oXScmp7oq4fBXgwpkTx4idBvpkF/wrM//T2h6OKQQbA2xx6R3gBi2oihEdqc0 +nXGEL8pZ0keImUEiyTCYYW49qKgFbdEfwFFEVn8nNQLdXpgKQuswv42hm1GqO+qT +RmTFAHneIWv2V6CG1wZy7HBGS4tz3aAhdT7cHcCP009zHIXZ/n9iyJVvttN7jLpT +wm+bREx50B1ws9efAvSyB7DH5fitIw6mVskpEndI2S9G/Tvw/HRwkqWOOAgfZDC2 +t0v7NqwQjqBSM2OdAzVWxWm9xiNaJ5T2pBL4LTM8oValX9YZ6e18CL13zSdkzJTa +TkZQh+D5wVOAHrut+0dSixv9ovneDiK3PTNZbNTe9ZUGMg1RGUFcPk8G97krgCf2 +o6p6fAbhQ8MTOWIaNr3gKC6UAuQpLmBVrkA9sHSSXvAgZJY/X0VdiLWK2gKgW0VU +3jg9CcCoSmVGFvyqv1ROTVu+OEO3KMqLM6oaJbolXCkvW0pujOotnCr2BXbgd5eA +iN1nE28daCSLT7d0geX0YJ96Vdc+N9oWaz53rK4YcJUIeSkDiv7BO7M/Gg+kO14f +WKGVyasvc0rQLW6aWQ9VGHgtPFGml4vmu7JwqkwR3v98KzfUetF3NI/n+UL3PIEM +S1IK +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICITCCAaagAwIBAgISESDm+Ez8JLC+BUCs2oMbNGA/MAoGCCqGSM49BAMDMEAx +CzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9wZW5U +cnVzdCBSb290IENBIEczMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAwMFow +QDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwUT3Bl +blRydXN0IFJvb3QgQ0EgRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARK7liuTcpm +3gY6oxH84Bjwbhy6LTAMidnW7ptzg6kjFYwvWYpa3RTqnVkrQ7cG7DK2uu5Bta1d +oYXM6h0UZqNnfkbilPPntlahFVmhTzeXuSIevRHr9LIfXsMUmuXZl5mjYzBhMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHd8MUi2I5 +DMlv4VBN0BBY3JWIbTAfBgNVHSMEGDAWgBRHd8MUi2I5DMlv4VBN0BBY3JWIbTAK +BggqhkjOPQQDAwNpADBmAjEAj6jcnboMBBf6Fek9LykBl7+BFjNAk2z8+e2AcG+q +j9uEwov1NcoG3GRvaBbhj5G5AjEA2Euly8LQCGzpGPta3U1fJAuwACEl74+nBCZx +4nxp5V2a+EEfOzmTk51V6s2N8fvB -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJC @@ -2501,6 +2169,37 @@ xFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi5nrQNiOK SnQ2+Q== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV @@ -2534,6 +2233,37 @@ ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y 8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV @@ -2572,141 +2302,156 @@ mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK 4SVhM7JZG+Ju1zdXtg2pEto= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 -IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz -BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y -aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG -9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMjIzM1oXDTE5MDYy -NjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y -azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw -Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl -cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjmFGWHOjVsQaBalfD -cnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td3zZxFJmP3MKS8edgkpfs -2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89HBFx1cQqY -JJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliE -Zwgs3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJ -n0WuPIqpsHEzXcjFV9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/A -PhmcGcwTTYJBtYze4D1gCCAPRX5ron+jjBXu ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6 -MRkwFwYDVQQKExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJp -dHkgMjA0OCBWMzAeFw0wMTAyMjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAX -BgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAbBgNVBAsTFFJTQSBTZWN1cml0eSAy -MDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt49VcdKA3Xtp -eafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7Jylg -/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGl -wSMiuLgbWhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnh -AMFRD0xS+ARaqn1y07iHKrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2 -PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpu -AWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB -BjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4EFgQUB8NR -MKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYc -HnmYv/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/ -Zb5gEydxiKRz44Rj0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+ -f00/FGj1EVDVwfSQpQgdMWD/YIwjVAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVO -rSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395nzIlQnQFgCi/vcEkllgVsRch -6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kApKnXwiJPZ9d3 -7CAFYd4= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJF -UzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJ -R1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcN -MDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3WjBoMQswCQYDVQQGEwJFUzEfMB0G -A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScw -JQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+ -WmmmO3I2F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKj -SgbwJ/BXufjpTjJ3Cj9BZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGl -u6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQD0EbtFpKd71ng+CT516nDOeB0/RSrFOy -A8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXteJajCq+TA81yc477OMUxk -Hl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMBAAGjggM7 -MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBr -aS5ndmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIIC -IwYKKwYBBAG/VQIBADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8A -cgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIA -YQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIAYQBsAGkAdABhAHQAIABWAGEA -bABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQByAGEAYwBpAPMA -bgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA -aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMA -aQBvAG4AYQBtAGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQA -ZQAgAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEA -YwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBuAHQAcgBhACAAZQBuACAAbABhACAA -ZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAAOgAvAC8AdwB3AHcA -LgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0dHA6 -Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+y -eAT8MIGVBgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQsw -CQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0G -A1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVu -Y2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRhTvW1yEICKrNcda3Fbcrn -lD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdzCkj+IHLt -b8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg -9J63NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XF -ducTZnV+ZfsBn5OHiJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmC -IoaZM3Fa6hlXPZHNqcCjbgcTpsnt+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCB -rjELMAkGA1UEBhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcp -MRIwEAYDVQQHEwlTdHV0dGdhcnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fz -c2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVTLVRSVVNUIEF1dGhlbnRpY2F0aW9u -IGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0wNTA2MjIwMDAwMDBa -Fw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFkZW4t -V3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMg -RGV1dHNjaGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJV -U1QgQXV0aGVudGljYXRpb24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBO -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1 -toPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob4QSwI7+Vio5bG0F/WsPo -TUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXLg3KSwlOy -ggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1 -XgqfeN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteF -hy+S8dF2g08LOlk3KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm -7QIDAQABo4GSMIGPMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG -MCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJvbmxpbmUxLTIwNDgtNTAdBgNV -HQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAUD8oeXHngovMp -ttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD -pwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFo -LtU96G7m1R08P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersF -iXOMy6ZNwPv2AtawB6MDwidAnwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0y -h9WUUpY6RsZxlj33mA6ykaqP2vROJAA5VeitF7nTNCtKqUDMFypVZUF0Qn71wK/I -k63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8bHz2eBIPdltkdOpQ= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIGGTCCBAGgAwIBAgIIPtVRGeZNzn4wDQYJKoZIhvcNAQELBQAwajEhMB8GA1UE -AxMYU0cgVFJVU1QgU0VSVklDRVMgUkFDSU5FMRwwGgYDVQQLExMwMDAyIDQzNTI1 -Mjg5NTAwMDIyMRowGAYDVQQKExFTRyBUUlVTVCBTRVJWSUNFUzELMAkGA1UEBhMC -RlIwHhcNMTAwOTA2MTI1MzQyWhcNMzAwOTA1MTI1MzQyWjBqMSEwHwYDVQQDExhT -RyBUUlVTVCBTRVJWSUNFUyBSQUNJTkUxHDAaBgNVBAsTEzAwMDIgNDM1MjUyODk1 -MDAwMjIxGjAYBgNVBAoTEVNHIFRSVVNUIFNFUlZJQ0VTMQswCQYDVQQGEwJGUjCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANqoVgLsfJXwTukK0rcHoyKL -ULO5Lhk9V9sZqtIr5M5C4myh5F0lHjMdtkXRtPpZilZwyW0IdmlwmubHnAgwE/7m -0ZJoYT5MEfJu8rF7V1ZLCb3cD9lxDOiaN94iEByZXtaxFwfTpDktwhpz/cpLKQfC -eSnIyCauLMT8I8hL4oZWDyj9tocbaF85ZEX9aINsdSQePHWZYfrSFPipS7HYfad4 -0hNiZbXWvn5qA7y1svxkMMPQwpk9maTTzdGxxFOHe0wTE2Z/v9VlU2j5XB7ltP82 -mUWjn2LAfxGCAVTeD2WlOa6dSEyJoxA74OaD9bDaLB56HFwfAKzMq6dgZLPGxXvH -VUZ0PJCBDkqOWZ1UsEixUkw7mO6r2jS3U81J2i/rlb4MVxH2lkwEeVyZ1eXkvm/q -R+5RS+8iJq612BGqQ7t4vwt+tN3PdB0lqYljseI0gcSINTjiAg0PE8nVKoIV8IrE -QzJW5FMdHay2z32bll0eZOl0c8RW5BZKUm2SOdPhTQ4/YrnerbUdZbldUv5dCamc -tKQM2S9FdqXPjmqanqqwEaHrYcbrPx78ZrQSnUZ/MhaJvnFFr5Eh2f2Tv7QCkUL/ -SR/tixVo3R+OrJvdggWcRGkWZBdWX0EPSk8ED2VQhpOX7EW/XcIc3M/E2DrmeAXQ -xVVVqV7+qzohu+VyFPcLAgMBAAGjgcIwgb8wHQYDVR0OBBYEFCkgy/HDD9oGjhOT -h/5fYBopu/O2MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUKSDL8cMP2gaO -E5OH/l9gGim787YwEQYDVR0gBAowCDAGBgRVHSAAMEkGA1UdHwRCMEAwPqA8oDqG -OGh0dHA6Ly9jcmwuc2d0cnVzdHNlcnZpY2VzLmNvbS9yYWNpbmUtR3JvdXBlU0cv -TGF0ZXN0Q1JMMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEATEZn -4ERQ9cW2urJRCiUTHbfHiC4fuStkoMuTiFJZqmD1zClSF/8E5ze0MRFGfisebKeL -PEeaXvSqXZA7RT2fSsmKe47A7j55i5KjyJRKuCgRa6YlX129x8j7g09VMeZc8BN8 -471/Kiw3N5RJr4QfFCeiWBCPCjk3GhIgQY8Z9qkfGe2yNLKtfTNEi18KB0PydkVF -La3kjQ4A/QQIqudr+xe9sAhWDjUqcvCz5006Tw3c82ASszhkjNv54SaNL+9O6CRH -PjY0imkPKGuLh8a9hSb50+tpIVZgkdb34GLCqHGuLt5mI7VSRqakSDcsfwEWVxH3 -Jw0O5Q/WkEXhHj8h3NL8FhgTPk1qsiZqQF4leP049KxYejcbmEAEx47J1MRnYbGY -rvDNDty5r2WDewoEij9hqvddQYbmxkzCTzpcVuooO6dEz8hKZPVyYC3jQ7hK4HU8 -MuSqFtcRucFF2ZtmY2blIrc07rrVdC8lZPOBVMt33lfUk+OsBzE6PlwDg1dTx/D+ -aNglUE0SyObhlY1nqzyTPxcCujjXnvcwpT09RAEzGpqfjtCf8e4wiHPvriQZupdz -FcHscQyEZLV77LxpPqRtCRY2yko5isune8YdfucziMm+MG2chZUh6Uc7Bn6B4upG -5nBYgOao8p0LadEziVkw82TTC/bOKwn7fRB2LhA= +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDEr @@ -2774,27 +2519,6 @@ iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDEl -MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMh -U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIz -MloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09N -IFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNlY3VyaXR5IENvbW11 -bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSE -RMqm4miO/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gO -zXppFodEtZDkBp2uoQSXWHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5 -bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4zZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDF -MxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4bepJz11sS6/vmsJWXMY1 -VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK9U2vP9eC -OKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G -CSqGSIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HW -tWS3irO4G8za+6xmiEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZ -q51ihPZRwSzJIxXYKLerJRO1RuGGAv8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDb -EJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnWmHyojf6GPgcWkuF75x3sM3Z+ -Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEWT1MKZPlO9L9O -VL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX @@ -2836,25 +2560,6 @@ JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP -MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAx -MDQwNjEwNDkxM1oXDTIxMDQwNjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNV -BAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJhIENsYXNzMSBDQTCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H887dF+2rDNbS82rDTG -29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9EJUk -oVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk -3w0LBUXl0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBL -qdReLjVQCfOAl/QMF6452F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIIN -nvmLVz5MxxftLItyM19yejhW1ebZrgUaHXVFsculJRwSVzb9IjcCAwEAAaMzMDEw -DwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZTiFIwCwYDVR0PBAQDAgEG -MA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE928Jj2VuX -ZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0H -DjxVyhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VO -TzF2nBBhjrZTOqMRvq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2Uv -kVrCqIexVmiUefkl98HVrhq4uz2PqYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4w -zMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9ZIRlXvVWa ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEP MA0GA1UEChMGU29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAx MDQwNjA3Mjk0MFoXDTIxMDQwNjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNV @@ -2874,26 +2579,36 @@ Tk6ezAyNlNzZRZxe7EJQY670XcSxEtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2 ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLHllpwrN9M -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJO -TDEeMBwGA1UEChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFh -dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEy -MTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4wHAYDVQQKExVTdGFhdCBkZXIgTmVk -ZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxhbmRlbiBSb290IENB -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFtvszn -ExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw71 -9tV2U02PjLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MO -hXeiD+EwR+4A5zN9RGcaC1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+U -tFE5A3+y3qcym7RHjm+0Sq7lr7HcsBthvJly3uSJt3omXdozSVtSnA71iq3DuD3o -BmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn622r+I/q85Ej0ZytqERAh -SQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRVHSAAMDww -OgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMv -cm9vdC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA -7Jbg0zTBLL9s+DANBgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k -/rvuFbQvBgwp8qiSpGEN/KtcCFtREytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzm -eafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbwMVcoEoJz6TMvplW0C5GUR5z6 -u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3ynGQI0DvDKcWy -7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR -iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gRVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0y +MjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5MMR4wHAYDVQQKDBVTdGFhdCBkZXIg +TmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRlcmxhbmRlbiBFViBS +b290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkkSzrS +M4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nC +UiY4iKTWO0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3d +Z//BYY1jTw+bbRcwJu+r0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46p +rfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13l +pJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gVXJrm0w912fxBmJc+qiXb +j5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr08C+eKxC +KFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS +/ZbV0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0X +cgOPvZuM5l5Tnrmd74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH +1vI4gnPah1vlPNOePqc7nvQDs/nxfRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrP +px9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwaivsnuL8wbqg7 +MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u +2dfOWBfoqSmuc0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHS +v4ilf0X8rLiltTMMgsT7B/Zq5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTC +wPTxGfARKbalGAKb12NMcIxHowNDXLldRqANb/9Zjr7dn3LDWyvfjFvO5QxGbJKy +CqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tNf1zuacpzEPuKqf2e +vTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi5Dp6 +Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIa +Gl6I6lD4WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeL +eG9QgkRQP2YGiqtDhFZKDyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8 +FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGyeUN51q1veieQA6TqJIc/2b3Z6fJfUEkc +7uzXLg== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO @@ -2929,6 +2644,38 @@ Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloX +DTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl +ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv +b3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4yolQP +cPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WW +IkYFsO2tx1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqX +xz8ecAgwoNzFs21v0IJyEavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFy +KJLZWyNtZrVtB0LrpjPOktvA9mxjeM3KTj215VKb8b475lRgsGYeCasH/lSJEULR +9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUurmkVLoR9BvUhTFXFkC4az +5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU51nus6+N8 +6U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7 +Ngzp07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHP +bMk7ccHViLVlvMDoFxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXt +BznaqB16nzaeErAMZRKQFWDZJkBE41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTt +XUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleuyjWcLhL75Lpd +INyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwp +LiniyMMB8jPqKqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8 +Ipf3YF3qKS9Ysr1YvY2WTxB1v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixp +gZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA8KCWAg8zxXHzniN9lLf9OtMJgwYh +/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b8KKaa8MFSu1BYBQw +0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0rmj1A +fsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq +4BZ+Extq1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR +1VmiiXTTn74eS9fGbbeIJG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/ +QFH1T/U67cjF68IeHRaVesd+QnGTbksVtzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM +94B7IWcnMFk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw @@ -3000,80 +2747,6 @@ iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn sSi6 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEW -MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg -Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM3WhcNMzYwOTE3MTk0NjM2WjB9 -MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi -U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh -cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk -pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf -OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C -Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT -Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi -HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM -Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w -+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ -Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 -Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B -26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID -AQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD -VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFul -F2mHMMo0aEPQQa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCC -ATgwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5w -ZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL2ludGVybWVk -aWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENvbW1lcmNpYWwgKFN0 -YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0aGUg -c2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0 -aWZpY2F0aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93 -d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgG -CWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1 -dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5fPGFf59Jb2vKXfuM/gTF -wWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWmN3PH/UvS -Ta0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst -0OcNOrg+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNc -pRJvkrKTlMeIFw6Ttn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKl -CcWw0bdT82AUuoVpaiF8H3VhFyAXe2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVF -P0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA2MFrLH9ZXF2RsXAiV+uKa0hK -1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBsHvUwyKMQ5bLm -KhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE -JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ -8dCAWZvLMdibD4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnm -fyWl8kgAwKQB2j8= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEW -MBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlm -aWNhdGlvbiBBdXRob3JpdHkgRzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1 -OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjEsMCoG -A1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRzIwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8Oo1XJ -JZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsD -vfOpL9HG4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnoo -D/Uefyf3lLE3PbfHkffiAez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/ -Q0kGi4xDuFby2X8hQxfqp0iVAXV16iulQ5XqFYSdCI0mblWbq9zSOdIxHWDirMxW -RST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbsO+wmETRIjfaAKxojAuuK -HDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8HvKTlXcxN -nw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM -0D4LnMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/i -UUjXuG+v+E5+M5iSFGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9 -Ha90OrInwMEePnWjFqmveiJdnxMaz6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHg -TuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJKoZIhvcNAQEL -BQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K -2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfX -UfEpY9Z1zRbkJ4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl -6/2o1PXWT6RbdejF0mCy2wl+JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK -9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG/+gyRr61M3Z3qAFdlsHB1b6uJcDJ -HgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTcnIhT76IxW1hPkWLI -wpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/XldblhY -XzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5l -IxKVCCIcl85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoo -hdVddLHRDiBYmxOlsGOm7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulr -so8uBtjRkcfGEvRM/TAXw8HaOFvjqermobp573PYtlNXLfbQ4ddI ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF @@ -3107,39 +2780,6 @@ ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE -BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWdu -IFBsYXRpbnVtIENBIC0gRzIwHhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAw -WjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMSMwIQYDVQQD -ExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu669y -IIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2Htn -IuJpX+UFeNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+ -6ixuEFGSzH7VozPY1kneWCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5ob -jM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIoj5+saCB9bzuohTEJfwvH6GXp43gOCWcw -izSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/68++QHkwFix7qepF6w9fl -+zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34TaNhxKFrY -zt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaP -pZjydomyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtF -KwH3HBqi7Ri6Cr2D+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuW -ae5ogObnmLo2t/5u7Su9IPhlGdpVCX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMB -AAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O -BBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCvzAeHFUdvOMW0 -ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW -IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUA -A4ICAQAIhab1Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0 -uMoI3LQwnkAHFmtllXcBrqS3NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+ -FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4U99REJNi54Av4tHgvI42Rncz7Lj7 -jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8KV2LwUvJ4ooTHbG/ -u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl9x8D -YSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1 -puEa+S1BaYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXa -icYwu+uPyyIIoK6q8QNsOktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbG -DI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSYMdp08YSTcU1f+2BY0fvEwW2JorsgH51x -kcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAciIfNAChs0B0QTwoRqjt8Z -Wr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UE BhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWdu IFNpbHZlciBDQSAtIEcyMB4XDTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0Nlow @@ -3173,108 +2813,6 @@ hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBk -MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 -YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg -Q0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4MTgyMjA2MjBaMGQxCzAJBgNVBAYT -AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp -Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9 -m2BtRsiMMW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdih -FvkcxC7mlSpnzNApbjyFNDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/ -TilftKaNXXsLmREDA/7n29uj/x2lzZAeAR81sH8A25Bvxn570e56eqeqDFdvpG3F -EzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkCb6dJtDZd0KTeByy2dbco -kdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn7uHbHaBu -HYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNF -vJbNcA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo -19AOeCMgkckkKmUpWyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjC -L3UcPX7ape8eYIVpQtPM+GP+HkM5haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJW -bjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNYMUJDLXT5xp6mig/p/r+D5kNX -JLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw -FDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j -BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzc -K6FptWfUjNP9MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzf -ky9NfEBWMXrrpA9gzXrzvsMnjgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7Ik -Vh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQMbFamIp1TpBcahQq4FJHgmDmHtqB -sfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4HVtA4oJVwIHaM190e -3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtlvrsR -ls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ip -mXeascClOS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HH -b6D0jqTsNFFbjCYDcKF31QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksf -rK/7DZBaZmBwXarNeNQk7shBoJMBkpxqnvy5JMWzFYJ+vq6VK+uxwNrjAWALXmms -hFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCyx/yP2FS1k2Kdzs9Z+z0Y -zirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMWNY6E0F/6 -MBr1mmz0DlP5OlvRHA== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIF2TCCA8GgAwIBAgIQHp4o6Ejy5e/DfEoeWhhntjANBgkqhkiG9w0BAQsFADBk -MQswCQYDVQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0 -YWwgQ2VydGlmaWNhdGUgU2VydmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3Qg -Q0EgMjAeFw0xMTA2MjQwODM4MTRaFw0zMTA2MjUwNzM4MTRaMGQxCzAJBgNVBAYT -AmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGlnaXRhbCBDZXJ0aWZp -Y2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAyMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlUJOhJ1R5tMJ6HJaI2nbeHCOFvEr -jw0DzpPMLgAIe6szjPTpQOYXTKueuEcUMncy3SgM3hhLX3af+Dk7/E6J2HzFZ++r -0rk0X2s682Q2zsKwzxNoysjL67XiPS4h3+os1OD5cJZM/2pYmLcX5BtS5X4HAB1f -2uY+lQS3aYg5oUFgJWFLlTloYhyxCwWJwDaCFCE/rtuh/bxvHGCGtlOUSbkrRsVP -ACu/obvLP+DHVxxX6NZp+MEkUp2IVd3Chy50I9AU/SpHWrumnf2U5NGKpV+GY3aF -y6//SSj8gO1MedK75MDvAe5QQQg1I3ArqRa0jG6F6bYRzzHdUyYb3y1aSgJA/MTA -tukxGggo5WDDH8SQjhBiYEQN7Aq+VRhxLKX0srwVYv8c474d2h5Xszx+zYIdkeNL -6yxSNLCK/RJOlrDrcH+eOfdmQrGrrFLadkBXeyq96G4DsguAhYidDMfCd7Camlf0 -uPoTXGiTOmekl9AbmbeGMktg2M7v0Ax/lZ9vh0+Hio5fCHyqW/xavqGRn1V9TrAL -acywlKinh/LTSlDcX3KwFnUey7QYYpqwpzmqm59m2I2mbJYV4+by+PGDYmy7Velh -k6M99bFXi08jsJvllGov34zflVEpYKELKeRcVVi3qPyZ7iVNTA6z00yPhOgpD/0Q -VAKFyPnlw4vP5w8CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0hBBYw -FDASBgdghXQBUwIBBgdghXQBUwIBMBIGA1UdEwEB/wQIMAYBAf8CAQcwHQYDVR0O -BBYEFE0mICKJS9PVpAqhb97iEoHF8TwuMB8GA1UdIwQYMBaAFE0mICKJS9PVpAqh -b97iEoHF8TwuMA0GCSqGSIb3DQEBCwUAA4ICAQAyCrKkG8t9voJXiblqf/P0wS4R -fbgZPnm3qKhyN2abGu2sEzsOv2LwnN+ee6FTSA5BesogpxcbtnjsQJHzQq0Qw1zv -/2BZf82Fo4s9SBwlAjxnffUy6S8w5X2lejjQ82YqZh6NM4OKb3xuqFp1mrjX2lhI -REeoTPpMSQpKwhI3qEAMw8jh0FcNlzKVxzqfl9NX+Ave5XLzo9v/tdhZsnPdTSpx -srpJ9csc1fV5yJmz/MFMdOO0vSk3FQQoHt5FRnDsr7p4DooqzgB53MBfGWcsa0vv -aGgLQ+OswWIJ76bdZWGgr4RVSJFSHMYlkSrQwSIjYVmvRRGFHQEkNI/Ps/8XciAT -woCqISxxOQ7Qj1zB09GOInJGTB2Wrk9xseEFKZZZ9LuedT3PDTcNYtsmjGOpI99n -Bjx8Oto0QuFmtEYE3saWmA9LSHokMnWRn6z3aOkquVVlzl1h0ydw2Df+n7mvoC5W -t6NlUe07qxS/TFED6F+KBZvuim6c779o+sjaC+NCydAXFJy3SuCvkychVSa1ZC+N -8f+mQAWFBVzKBxlcCxMoTFh/wqXvRdpg065lYZ1Tg3TCrvJcwhbtkj6EPnNgiLx2 -9CzP0H1907he0ZESEOnN3col49XtmS++dYFLJPlFRpTJKSFTnCZFqhMX5OfNeOI5 -wSsSnqaeG8XmDtkx2Q== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIF4DCCA8igAwIBAgIRAPL6ZOJ0Y9ON/RAdBB92ylgwDQYJKoZIhvcNAQELBQAw -ZzELMAkGA1UEBhMCY2gxETAPBgNVBAoTCFN3aXNzY29tMSUwIwYDVQQLExxEaWdp -dGFsIENlcnRpZmljYXRlIFNlcnZpY2VzMR4wHAYDVQQDExVTd2lzc2NvbSBSb290 -IEVWIENBIDIwHhcNMTEwNjI0MDk0NTA4WhcNMzEwNjI1MDg0NTA4WjBnMQswCQYD -VQQGEwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2Vy -dGlmaWNhdGUgU2VydmljZXMxHjAcBgNVBAMTFVN3aXNzY29tIFJvb3QgRVYgQ0Eg -MjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMT3HS9X6lds93BdY7Bx -UglgRCgzo3pOCvrY6myLURYaVa5UJsTMRQdBTxB5f3HSek4/OE6zAMaVylvNwSqD -1ycfMQ4jFrclyxy0uYAyXhqdk/HoPGAsp15XGVhRXrwsVgu42O+LgrQ8uMIkqBPH -oCE2G3pXKSinLr9xJZDzRINpUKTk4RtiGZQJo/PDvO/0vezbE53PnUgJUmfANykR -HvvSEaeFGHR55E+FFOtSN+KxRdjMDUN/rhPSays/p8LiqG12W0OfvrSdsyaGOx9/ -5fLoZigWJdBLlzin5M8J0TbDC77aO0RYjb7xnglrPvMyxyuHxuxenPaHZa0zKcQv -idm5y8kDnftslFGXEBuGCxobP/YCfnvUxVFkKJ3106yDgYjTdLRZncHrYTNaRdHL -OdAGalNgHa/2+2m8atwBz735j9m9W8E6X47aD0upm50qKGsaCnw8qyIL5XctcfaC -NYGu+HuB5ur+rPQam3Rc6I8k9l2dRsQs0h4rIWqDJ2dVSqTjyDKXZpBy2uPUZC5f -46Fq9mDU5zXNysRojddxyNMkM3OxbPlq4SjbX8Y96L5V5jcb7STZDxmPX2MYWFCB -UWVv8p9+agTnNCRxunZLWB4ZvRVgRaoMEkABnRDixzgHcgplwLa7JSnaFp6LNYth -7eVxV4O1PHGf40+/fh6Bn0GXAgMBAAGjgYYwgYMwDgYDVR0PAQH/BAQDAgGGMB0G -A1UdIQQWMBQwEgYHYIV0AVMCAgYHYIV0AVMCAjASBgNVHRMBAf8ECDAGAQH/AgED -MB0GA1UdDgQWBBRF2aWBbj2ITY1x0kbBbkUe88SAnTAfBgNVHSMEGDAWgBRF2aWB -bj2ITY1x0kbBbkUe88SAnTANBgkqhkiG9w0BAQsFAAOCAgEAlDpzBp9SSzBc1P6x -XCX5145v9Ydkn+0UjrgEjihLj6p7jjm02Vj2e6E1CqGdivdj5eu9OYLU43otb98T -PLr+flaYC/NUn81ETm484T4VvwYmneTwkLbUwp4wLh/vx3rEUMfqe9pQy3omywC0 -Wqu1kx+AiYQElY2NfwmTv9SoqORjbdlk5LgpWgi/UOGED1V7XwgiG/W9mR4U9s70 -WBCCswo9GcG/W6uqmdjyMb3lOGbcWAXH7WMaLgqXfIeTK7KK4/HsGOV1timH59yL -Gn602MnTihdsfSlEvoqq9X46Lmgxk7lq2prg2+kupYTNHAq4Sgj5nPFhJpiTt3tm -7JFe3VE/23MPrQRYCd0EApUKPtN236YQHoA96M2kZNEzx5LH4k5E4wnJTsJdhw4S -nr8PyQUQ3nqjsTzyP6WqJ3mtMX0f/fwZacXduT98zca0wjAefm6S139hdlqP65VN -vBFuIXxZN5nQBrz5Bm0yFqXZaajh3DyAHmBR3NdUIR7KYndP+tiPsys6DXhyyWhB -WkdKwqPrGtcKqzwyVcgKEZzfdNbwQBUdyLmPtTbFr/giuMod89a2GQ+fYWVq6nTI -fI/DT11lgh/ZDYnadXL77/FHZxOzyNEZiCcmmpl5fx7kLD977vHeTYuWl8PVP3wb -I+2ksx0WckNLIOFZfsLorSa/ovc= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl @@ -3321,180 +2859,30 @@ e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p TpPDpFQUWw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjEL -MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV -BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 -Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYwMTEyMTQzODQzWhcNMjUxMjMxMjI1 -OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i -SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UEAxMc -VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jf -tMjWQ+nEdVl//OEd+DFwIxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKg -uNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2J -XjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQXa7pIXSSTYtZgo+U4+lK -8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7uSNQZu+99 -5OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1Ud -EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3 -kUrL84J6E1wIqzCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy -dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6 -Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz -JTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 -Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u -TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iS -GNn3Bzn1LL4GdXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprt -ZjluS5TmVfwLG4t3wVMTZonZKNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8 -au0WOB9/WIFaGusyiC2y8zl3gK9etmF1KdsjTYjKUCjLhdLTEKJZbtOTVAB6okaV -hgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kPJOzHdiEoZa5X6AeI -dUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfkvQ== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjEL -MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNV -BAsTGVRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0 -Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYwMTEyMTQ0MTU3WhcNMjUxMjMxMjI1 -OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIgR21i -SDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UEAxMc -VEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJW -Ht4bNwcwIi9v8Qbxq63WyKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+Q -Vl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo6SI7dYnWRBpl8huXJh0obazovVkdKyT2 -1oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZuV3bOx4a+9P/FRQI2Alq -ukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk2ZyqBwi1 -Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1Ud -EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NX -XAek0CSnwPIA1DCB7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRy -dXN0Y2VudGVyLmRlL2NybC92Mi90Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6 -Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBUcnVzdENlbnRlciUyMENsYXNz -JTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21iSCxPVT1yb290 -Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u -TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlN -irTzwppVMXzEO2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8 -TtXqluJucsG7Kv5sbviRmEb8yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6 -g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9IJqDnxrcOfHFcqMRA/07QlIp2+gB -95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal092Y+tTmBvTwtiBj -S+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc5A== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTEL -MAkGA1UEBhMCREUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNV -BAsTG1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1 -c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcNMDYwMzIyMTU1NDI4WhcNMjUxMjMx -MjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1c3RDZW50ZXIg -R21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYwJAYD -VQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSR -JJZ4Hgmgm5qVSkr1YnwCqMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3T -fCZdzHd55yx4Oagmcw6iXSVphU9VDprvxrlE4Vc93x9UIuVvZaozhDrzznq+VZeu -jRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtwag+1m7Z3W0hZneTvWq3z -wZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9OgdwZu5GQ -fezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYD -VR0jBBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0G -CSqGSIb3DQEBBQUAA4IBAQAo0uCG1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X1 -7caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/CyvwbZ71q+s2IhtNerNXxTPqYn -8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3ghUJGooWMNjs -ydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT -ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/ -2TYcuiUaUj0a7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEKzCCAxOgAwIBAgIEOsylTDANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJE -SzEVMBMGA1UEChMMVERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQg -Um9vdCBDQTAeFw0wMTA0MDUxNjMzMTdaFw0yMTA0MDUxNzAzMTdaMEMxCzAJBgNV -BAYTAkRLMRUwEwYDVQQKEwxUREMgSW50ZXJuZXQxHTAbBgNVBAsTFFREQyBJbnRl -cm5ldCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxLhA -vJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20jxsNu -Zp+Jpd/gQlBn+h9sHvTQBda/ytZO5GhgbEaqHF1j4QeGDmUApy6mcca8uYGoOn0a -0vnRrEvLznWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc1 -4izbSysseLlJ28TQx5yc5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGN -eGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcD -R0G2l8ktCkEiu7vmpwIDAQABo4IBJTCCASEwEQYJYIZIAYb4QgEBBAQDAgAHMGUG -A1UdHwReMFwwWqBYoFakVDBSMQswCQYDVQQGEwJESzEVMBMGA1UEChMMVERDIElu -dGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTENMAsGA1UEAxME -Q1JMMTArBgNVHRAEJDAigA8yMDAxMDQwNTE2MzMxN1qBDzIwMjEwNDA1MTcwMzE3 -WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUbGQBx/2FbazI2p5QCIUItTxWqFAw -HQYDVR0OBBYEFGxkAcf9hW2syNqeUAiFCLU8VqhQMAwGA1UdEwQFMAMBAf8wHQYJ -KoZIhvZ9B0EABBAwDhsIVjUuMDo0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4IBAQBO -Q8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540mgwV5dOy0uaOX -wTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+ -2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPVT8+PVkuzHu7TmHnaCB4Mb7j4Fifvwm89 -9qNLPg7kbWzbO0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0 -jUNAE4z9mQNUecYu6oah9jrUCbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38 -aQNiuJkFBT1reBK9sG9l ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOc -UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx -c8SxMQswCQYDVQQGDAJUUjEPMA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykg -MjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 -dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMxMDI3MTdaFw0xNTAz -MjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsgU2Vy -dGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYD -VQQHDAZBTktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kg -xLBsZXRpxZ9pbSB2ZSBCaWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEu -xZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7 -XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GXyGl8hMW0kWxsE2qkVa2k -heiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8iSi9BB35J -YbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5C -urKZ8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1 -JuTm5Rh8i27fbMx4W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51 -b0dewQIDAQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV -9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46sWrv7/hg0Uw2ZkUd82YCdAR7 -kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxEq8Sn5RTOPEFh -fEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy -B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdA -aLX/7KfS0zgYnNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKS -RGQDJereW26fyfJOrN3H ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOc -UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx -c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xS -S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg -SGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcNMDUxMTA3MTAwNzU3 -WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVrdHJv -bmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJU -UjEPMA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSw -bGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWe -LiAoYykgS2FzxLFtIDIwMDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqeLCDe2JAOCtFp0if7qnef -J1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKIx+XlZEdh -R3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJ -Qv2gQrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGX -JHpsmxcPbe9TmJEr5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1p -zpwACPI2/z7woQ8arBT9pmAPAgMBAAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58S -Fq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8GA1UdEwEB/wQFMAMBAf8wDQYJ -KoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/nttRbj2hWyfIvwq -ECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4 -Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFz -gw2lGh1uEpJ+hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotH -uFEJjOp9zYhys2AzsfAKRO8P9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LS -y3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5UrbnBEI= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEPTCCAyWgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvzE/MD0GA1UEAww2VMOc -UktUUlVTVCBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx -c8SxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMV4wXAYDVQQKDFVUw5xS -S1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kg -SGl6bWV0bGVyaSBBLsWeLiAoYykgQXJhbMSxayAyMDA3MB4XDTA3MTIyNTE4Mzcx -OVoXDTE3MTIyMjE4MzcxOVowgb8xPzA9BgNVBAMMNlTDnFJLVFJVU1QgRWxla3Ry -b25payBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTELMAkGA1UEBhMC -VFIxDzANBgNVBAcMBkFua2FyYTFeMFwGA1UECgxVVMOcUktUUlVTVCBCaWxnaSDE -sGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkgQS7F -ni4gKGMpIEFyYWzEsWsgMjAwNzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAKu3PgqMyKVYFeaK7yc9SrToJdPNM8Ig3BnuiD9NYvDdE3ePYakqtdTyuTFY -KTsvP2qcb3N2Je40IIDu6rfwxArNK4aUyeNgsURSsloptJGXg9i3phQvKUmi8wUG -+7RP2qFsmmaf8EMJyupyj+sA1zU511YXRxcw9L6/P8JorzZAwan0qafoEGsIiveG -HtyaKhUG9qPw9ODHFNRRf8+0222vR5YXm3dx2KdxnSQM9pQ/hTEST7ruToK4uT6P -IzdezKKqdfcYbwnTrqdUKDT74eA7YH2gvnmJhsifLfkKS8RQouf9eRbHegsYz85M -733WB2+Y8a+xwXrXgTW4qhe04MsCAwEAAaNCMEAwHQYDVR0OBBYEFCnFkKslrxHk -Yb+j/4hhkeYO/pyBMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0G -CSqGSIb3DQEBBQUAA4IBAQAQDdr4Ouwo0RSVgrESLFF6QSU2TJ/sPx+EnWVUXKgW -AkD6bho3hO9ynYYKVZ1WKKxmLNA6VpM0ByWtCLCPyA8JWcqdmBzlVPi5RX9ql2+I -aE1KBiY3iAIOtsbWcpnOa3faYjGkVh+uX4132l32iPwa2Z61gfAyuOOI0JzzaqC5 -mxRZNTZPz/OOXl0XrRWV2N2y1RVuAE6zS89mlOTgzbUF2mNXi+WzqtvALhyQRNsa -XRik7r4EW5nVcV9VZWRi1aKbBFmGyGJ353yCRWo9F7/snXUMrqNvWtMvmDb08PUZ -qxFdyKbjKlhqQgnDvZImZjINXQhVdP+MmNAKpoRq0Tl9 +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx @@ -3611,42 +2999,90 @@ HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx -FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD -VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv -biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy -dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t -MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB -MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG -A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp -b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl -cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv -bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE -VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ -ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR -uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG -9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI -hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM -pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx -FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD -VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv -biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm -MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx -MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT -DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3 -dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl -cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3 -DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD -gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91 -yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX -L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj -EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG -7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e -QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ -qdq5snUb9kLy78fyGPmJvKP/iiMucEc= +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxFzAVBgNVBAMMDlRydXN0Q29y +IEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3MjgwN1owgZwxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3Ig +RUNBLTEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb +3w9U73NjKYKtR8aja+3+XzP4Q1HpGjORMRegdMTUpwHmspI+ap3tDvl0mEDTPwOA +BoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23xFUfJ3zSCNV2HykVh0A5 +3ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmcp0yJF4Ou +owReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/ +wZ0+fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZF +ZtS6mFjBAgMBAAGjYzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAf +BgNVHSMEGDAWgBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEABT41XBVwm8nHc2Fv +civUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u/ukZMjgDfxT2 +AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50 +soIipX1TH0XsJ5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BI +WJZpTdwHjFGTot+fDz2LYLSCjaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1Wi +tJ/X5g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYD +VQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEk +MCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29y +IFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkxMjMxMTcyMzE2WjCB +pDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFuYW1h +IENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUG +A1UECwweVHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZU +cnVzdENvciBSb290Q2VydCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAv463leLCJhJrMxnHQFgKq1mqjQCj/IDHUHuO1CAmujIS2CNUSSUQIpid +RtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4pQa81QBeCQryJ3pS/C3V +seq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0JEsq1pme +9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CV +EY4hgLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorW +hnAbJN7+KIor0Gqw/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/ +DeOxCbeKyKsZn3MzUOcwHwYDVR0jBBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5mDo4Nvu7Zp5I +/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZ +yonnMlo2HD6CqFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djts +L1Ac59v2Z3kf9YKVmgenFK+P3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdN +zl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNV +BAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQw +IgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRy +dXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0eTEfMB0GA1UEAwwWVHJ1c3RDb3Ig +Um9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEyMzExNzI2MzlaMIGk +MQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEg +Q2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYD +VQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRy +dXN0Q29yIFJvb3RDZXJ0IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCnIG7CKqJiJJWQdsg4foDSq8GbZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+ +QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9NkRvRUqdw6VC0xK5mC8tkq +1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1oYxOdqHp +2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nK +DOObXUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hape +az6LMvYHL1cEksr1/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF +3wP+TfSvPd9cW436cOGlfifHhi5qjxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88 +oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQPeSghYA2FFn3XVDjxklb9tTNM +g9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+CtgrKAmrhQhJ8Z3 +mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAd +BgNVHQ4EFgQU2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6U +nrybPZx9mCAZ5YwwYrIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/hOsh80QA9z+LqBrWyOrsGS2h60COX +dKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnpkpfbsEZC89NiqpX+ +MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv2wnL +/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RX +CI/hOWB3S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYa +ZH9bDTMJBzN7Bj8RpFxwPIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW +2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dvDDqPys/cA8GiCcjl/YBeyGBCARsaU1q7 +N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYURpFHmygk71dSTlxCnKr3 +Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANExdqtvArB +As8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp +5KeXRKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu +1uwJ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBF @@ -3670,149 +3106,79 @@ jZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYliB6XzCGcKQEN ZetX2fNXlrtIzYE= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRS -MRgwFgYDVQQHDA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJp -bGltc2VsIHZlIFRla25vbG9qaWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSw -VEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ryb25payB2ZSBLcmlwdG9sb2ppIEFy -YcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNVBAsMGkthbXUgU2Vy -dGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUgS8O2 -ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAe -Fw0wNzA4MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIx -GDAWBgNVBAcMD0dlYnplIC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmls -aW1zZWwgdmUgVGVrbm9sb2ppayBBcmHFn3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBU -QUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZlIEtyaXB0b2xvamkgQXJh -xZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2FtdSBTZXJ0 -aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7Zr -IFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4h -gb46ezzb8R1Sf1n68yJMlaCQvEhOEav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yK -O7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1xnnRFDDtG1hba+818qEhTsXO -fJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR6Oqeyjh1jmKw -lZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL -hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQID -AQABo0IwQDAdBgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/ -BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmP -NOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4N5EY3ATIZJkrGG2AA1nJrvhY0D7t -wyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLTy9LQQfMmNkqblWwM -7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYhLBOh -gLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5n -oN+J1q2MdqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUs -yZyQ2uypQjyttgI= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB -kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug -Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho -dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw -IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG -EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD -VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu -dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 -E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ -D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK -4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq -lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW -bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB -o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT -MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js -LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr -BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB -AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft -Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj -j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH -KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv -2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 -mfnGV/TJVTl4uix5yaaIK/QI ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCB -rjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug -Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho -dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0BgNVBAMTLVVUTi1VU0VSRmlyc3Qt -Q2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05OTA3MDkxNzI4NTBa -Fw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAV -BgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5l -dHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UE -AxMtVVROLVVTRVJGaXJzdC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWls -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3B -YHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIxB8dOtINknS4p1aJkxIW9 -hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8om+rWV6l -L8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLm -SGHGTPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM -1tZUOt4KpLoDd7NlyP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws -6wIDAQABo4G5MIG2MAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNVHR8EUTBPME2gS6BJhkdodHRw -Oi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGllbnRBdXRoZW50 -aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH -AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u -7mFVbwQ+zznexRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0 -xtcgBEXkzYABurorbs6q15L+5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQ -rfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarVNZ1yQAOJujEdxRBoUp7fooXFXAim -eOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZw7JHpsIyYdfHb0gk -USeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB -lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug -Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho -dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt -SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG -A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe -MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v -d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh -cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn -0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ -M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a -MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd -oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI -DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy -oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD -VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 -dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy -bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF -BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM -//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli -CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE -CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t -3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS -KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 -IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz -BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y -aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG -9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIyMjM0OFoXDTE5MDYy -NTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y -azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw -Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl -cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9Y -LqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIiGQj4/xEjm84H9b9pGib+ -TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCmDuJWBQ8Y -TfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0 -LBwGlN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLW -I8sogTLDAHkY7FkXicnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPw -nXS3qT6gpf+2SQMT2iLM7XGCK5nPOrf1LXLI ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0 -IFZhbGlkYXRpb24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAz -BgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9y -aXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG -9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAwMTk1NFoXDTE5MDYy -NjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0d29y -azEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRw -Oi8vd3d3LnZhbGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNl -cnQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOOnHK5avIWZJV16vY -dA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVCCSRrCl6zfN1SLUzm1NZ9 -WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7RfZHM047QS -v4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9v -UJSZSWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTu -IYEZoDJJKPTEjlbVUjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwC -W/POuZ6lcg5Ktz885hZo+L7tdEy8W9ViH0Pd +MIIEJzCCAw+gAwIBAgIHAI4X/iQggTANBgkqhkiG9w0BAQsFADCBsTELMAkGA1UE +BhMCVFIxDzANBgNVBAcMBkFua2FyYTFNMEsGA1UECgxEVMOcUktUUlVTVCBCaWxn +aSDEsGxldGnFn2ltIHZlIEJpbGnFn2ltIEfDvHZlbmxpxJ9pIEhpem1ldGxlcmkg +QS7Fni4xQjBABgNVBAMMOVTDnFJLVFJVU1QgRWxla3Ryb25payBTZXJ0aWZpa2Eg +SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSBINTAeFw0xMzA0MzAwODA3MDFaFw0yMzA0 +MjgwODA3MDFaMIGxMQswCQYDVQQGEwJUUjEPMA0GA1UEBwwGQW5rYXJhMU0wSwYD +VQQKDERUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmlsacWfaW0gR8O8 +dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjFCMEAGA1UEAww5VMOcUktUUlVTVCBF +bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIEg1MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApCUZ4WWe60ghUEoI5RHwWrom +/4NZzkQqL/7hzmAD/I0Dpe3/a6i6zDQGn1k19uwsu537jVJp45wnEFPzpALFp/kR +Gml1bsMdi9GYjZOHp3GXDSHHmflS0yxjXVW86B8BSLlg/kJK9siArs1mep5Fimh3 +4khon6La8eHBEJ/rPCmBp+EyCNSgBbGM+42WAA4+Jd9ThiI7/PS98wl+d+yG6w8z +5UNP9FR1bSmZLmZaQ9/LXMrI5Tjxfjs1nQ/0xVqhzPMggCTTV+wVunUlm+hkS7M0 +hO8EuPbJbKoCPrZV4jI3X/xml1/N1p7HIL9Nxqw/dV8c7TKcfGkAaZHjIxhT6QID +AQABo0IwQDAdBgNVHQ4EFgQUVpkHHtOsDGlktAxQR95DLL4gwPswDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJ5FdnsX +SDLyOIspve6WSk6BGLFRRyDN0GSxDsnZAdkJzsiZ3GglE9Rc8qPoBP5yCccLqh0l +VX6Wmle3usURehnmp349hQ71+S4pL+f5bFgWV1Al9j4uPqrtd3GqqpmWRgqujuwq +URawXs3qZwQcWDD1YIq9pr1N5Za0/EKJAWv2cMhQOQwt1WbZyNKzMrcbGW3LM/nf +peYVhDfwwvJllpKQd/Ct9JDpEXjXk4nAPQu6KfTomZ1yju2dL+6SfaHx/126M2CF +Yv4HAqGEVka+lgqaE9chTLd8B59OTj+RdPsnnRHM3eaxynFNExc5JsUpISuTKWqW ++qtB4Uu2NQvAmxU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL @@ -3892,139 +3258,6 @@ lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 7M2CYfE45k+XmCpajQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG -A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz -cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 -MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV -BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt -YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN -ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f -zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi -TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G -CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW -NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV -Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJ -BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh -c3MgMSBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy -MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp -emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X -DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw -FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMg -UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo -YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 -MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB -AQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgdk4xWArzZbxpvUjZudVYK -VdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIqWpDBucSm -Fc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQID -AQABMA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0J -h9ZrbWB85a7FkCMMXErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2ul -uIncrKTdcu1OofdPvAbT6shkdHvClUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68 -DzFc6PLZ ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl -cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu -LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT -aWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD -VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT -aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ -bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu -IENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2E1Lm0+afY8wR4 -nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/EbRrsC+MO -8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjV -ojYJrKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjb -PG7PoBMAGrgnoeS+Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP2 -6KbqxzcSXKMpHgLZ2x87tNcPVkeBFQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vr -n5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAq2aN17O6x5q25lXQBfGfMY1a -qtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/Ny9Sn2WCVhDr4 -wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3 -ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrs -pSCAaWihT37ha88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4 -E1Z5T21Q6huwtVexN2ZYI/PcD98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0Ns -YXNzIDIgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBH -MjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y -aXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazAe -Fw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJVUzEX -MBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGlj -IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMx -KGMpIDE5OTggVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s -eTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazCBnzANBgkqhkiG9w0B -AQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjxnNuX6Zr8wgQGE75fUsjM -HiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRCwiNPStjw -DqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cC -AwEAATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9ji -nb3/7aHmZuovCfTK1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAX -rXfMSTWqz9iP0b63GJZHc2pUIjRkLbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnIn -jBJ7xUS0rg== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJ -BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVy -aVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24s -IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNp -Z24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 -eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJBgNV -BAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNp -Z24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIElu -Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24g -Q2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt -IEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwoNwtUs22e5LeWU -J92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6tW8UvxDO -JxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUY -wZF7C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9o -koqQHgiBVrKtaaNS0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjN -qWm6o+sdDZykIKbBoMXRRkwXbdKsZj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/E -Srg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0JhU8wI1NQ0kdvekhktdmnLfe -xbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf0xwLRtxyID+u -7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU -sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RI -sH/7NiXaldDxJBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTP -cjnhsUPgKM+351psE2tJs//jGHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG -A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz -cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2 -MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV -BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt -YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN -ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE -BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is -I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G -CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i -2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ -2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ -BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh -c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy -MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp -emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X -DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw -FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg -UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo -YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 -MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB -AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 -pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 -13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID -AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk -U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i -F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY -oJ2daZH9 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu @@ -4049,30 +3282,6 @@ F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl -cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu -LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT -aWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD -VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT -aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ -bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu -IENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK3LpRFpxlmr8Y+1 -GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaStBO3IFsJ -+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0Gbd -U6LM8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLm -NxdLMEYH5IBtptiWLugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XY -ufTsgsbSPZUd5cBPhMnZo0QoBmrXRazwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ -ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAj/ola09b5KROJ1WrIhVZPMq1 -CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXttmhwwjIDLk5Mq -g6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm -fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c -2NU8Qh0XwRJdRTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/ -bLvSHgCwIe34QWKCudiyxLtGUPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBr MQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRl cm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv @@ -4095,34 +3304,6 @@ LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd 398znM/jra6O1I7mT1GvFpLgXPYHDw== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMx -IDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxs -cyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9v -dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDcxMjEzMTcwNzU0WhcNMjIxMjE0 -MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdl -bGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQD -DC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+r -WxxTkqxtnt3CxC5FlAM1iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjU -Dk/41itMpBb570OYj7OeUt9tkTmPOL13i0Nj67eT/DBMHAGTthP796EfvyXhdDcs -HqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8bJVhHlfXBIEyg1J55oNj -z7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiBK0HmOFaf -SZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/Slwxl -AgMBAAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqG -KGh0dHA6Ly9jcmwucGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0P -AQH/BAQDAgHGMB0GA1UdDgQWBBQmlRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0j -BIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGBi6SBiDCBhTELMAkGA1UEBhMC -VVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNX -ZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg -Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEB -ALkVsUSRzCPIK0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd -/ZDJPHV3V3p9+N701NX3leZ0bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pB -A4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSljqHyita04pO2t/caaH/+Xc/77szWn -k4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+esE2fDbbFwRnzVlhE9 -iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJtylv -2G0xffX8oRAHh84vWdw+WNs= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY @@ -4265,4 +3446,5 @@ t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu MdRAGmI0Nj81Aa6sY6A= ------END CERTIFICATE-----`) +-----END CERTIFICATE----- +`) From 6a26430d7e6516cd5e3c80bfa89f8738a8211dd8 Mon Sep 17 00:00:00 2001 From: DmitryStashkevich <34479135+DmitryStashkevich@users.noreply.github.com> Date: Thu, 27 Feb 2020 20:55:57 +0200 Subject: [PATCH 019/381] add admixer adapter (#1195) --- adapters/admixer/admixer.go | 184 ++++++++++++++++++ adapters/admixer/admixer_test.go | 10 + .../exemplary/optional-params.json | 104 ++++++++++ .../exemplary/simple-app-audio.json | 89 +++++++++ .../exemplary/simple-app-banner.json | 101 ++++++++++ .../exemplary/simple-app-native.json | 90 +++++++++ .../exemplary/simple-app-video.json | 111 +++++++++++ .../exemplary/simple-site-audio.json | 89 +++++++++ .../exemplary/simple-site-banner.json | 101 ++++++++++ .../exemplary/simple-site-native.json | 90 +++++++++ .../exemplary/simple-site-video.json | 111 +++++++++++ .../admixertest/params/race/audio.json | 5 + .../admixertest/params/race/banner.json | 5 + .../admixertest/params/race/native.json | 5 + .../admixertest/params/race/video.json | 5 + .../supplemental/bad-dsp-request-example.json | 70 +++++++ .../dsp-server-internal-error-example.json | 70 +++++++ .../supplemental/ext-unmarshall-error.json | 34 ++++ .../unknown-status-code-example.json | 70 +++++++ .../supplemental/wrong-zone-id-error.json | 30 +++ .../supplemental/zero-bid-request-error.json | 19 ++ adapters/admixer/params_test.go | 57 ++++++ adapters/admixer/usersync.go | 11 ++ adapters/admixer/usersync_test.go | 34 ++++ config/config.go | 2 + exchange/adapter_map.go | 2 + go.mod | 15 +- go.sum | 60 +++++- openrtb_ext/bidders.go | 2 + openrtb_ext/imp_admixer.go | 7 + static/bidder-info/admixer.yaml | 15 ++ static/bidder-params/admixer.json | 25 +++ usersync/usersyncers/syncer.go | 5 +- usersync/usersyncers/syncer_test.go | 1 + 34 files changed, 1623 insertions(+), 6 deletions(-) create mode 100644 adapters/admixer/admixer.go create mode 100644 adapters/admixer/admixer_test.go create mode 100644 adapters/admixer/admixertest/exemplary/optional-params.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-app-audio.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-app-banner.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-app-native.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-app-video.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-site-audio.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-site-banner.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-site-native.json create mode 100644 adapters/admixer/admixertest/exemplary/simple-site-video.json create mode 100644 adapters/admixer/admixertest/params/race/audio.json create mode 100644 adapters/admixer/admixertest/params/race/banner.json create mode 100644 adapters/admixer/admixertest/params/race/native.json create mode 100644 adapters/admixer/admixertest/params/race/video.json create mode 100644 adapters/admixer/admixertest/supplemental/bad-dsp-request-example.json create mode 100644 adapters/admixer/admixertest/supplemental/dsp-server-internal-error-example.json create mode 100644 adapters/admixer/admixertest/supplemental/ext-unmarshall-error.json create mode 100644 adapters/admixer/admixertest/supplemental/unknown-status-code-example.json create mode 100644 adapters/admixer/admixertest/supplemental/wrong-zone-id-error.json create mode 100644 adapters/admixer/admixertest/supplemental/zero-bid-request-error.json create mode 100644 adapters/admixer/params_test.go create mode 100644 adapters/admixer/usersync.go create mode 100644 adapters/admixer/usersync_test.go create mode 100644 openrtb_ext/imp_admixer.go create mode 100644 static/bidder-info/admixer.yaml create mode 100644 static/bidder-params/admixer.json diff --git a/adapters/admixer/admixer.go b/adapters/admixer/admixer.go new file mode 100644 index 00000000000..65a94f352ed --- /dev/null +++ b/adapters/admixer/admixer.go @@ -0,0 +1,184 @@ +package admixer + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" +) + +type AdmixerAdapter struct { + endpoint string +} + +func NewAdmixerBidder(endpoint string) *AdmixerAdapter { + return &AdmixerAdapter{endpoint: endpoint} +} + +type admixerImpExt struct { + CustomParams map[string]interface{} `json:"customParams"` +} + +func (a *AdmixerAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) (requests []*adapters.RequestData, errors []error) { + rq, errs := a.makeRequest(request) + + if len(errs) > 0 { + errors = append(errors, errs...) + return + } + + if rq != nil { + requests = append(requests, rq) + } + + return +} + +func (a *AdmixerAdapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, []error) { + var errs []error + var validImps []openrtb.Imp + + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: "No impressions in request", + }} + } + + for _, imp := range request.Imp { + if err := preprocess(&imp); err != nil { + errs = append(errs, err) + continue + } + validImps = append(validImps, imp) + } + + if len(validImps) == 0 { + return nil, errs + } + + request.Imp = validImps + + reqJSON, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }, errs +} + +func preprocess(imp *openrtb.Imp) error { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + var admixerExt openrtb_ext.ExtImpAdmixer + if err := json.Unmarshal(bidderExt.Bidder, &admixerExt); err != nil { + return &errortypes.BadInput{ + Message: "Wrong Admixer bidder ext", + } + } + + //don't use regexp due to possible performance reduce + if len(admixerExt.ZoneId) != 36 { + return &errortypes.BadInput{ + Message: "ZoneId must be UUID/GUID", + } + } + + imp.TagID = admixerExt.ZoneId + imp.BidFloor = admixerExt.CustomBidFloor + + imp.Ext = nil + + if admixerExt.CustomParams != nil { + impExt := admixerImpExt{ + CustomParams: admixerExt.CustomParams, + } + var err error + if imp.Ext, err = json.Marshal(impExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + } + + return nil +} + +func (a *AdmixerAdapter) 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.StatusInternalServerError { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Dsp server internal error", response.StatusCode), + }} + } + + if response.StatusCode >= http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d. Bad request to dsp", response.StatusCode), + }} + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d", response.StatusCode), + }} + } + + var bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + //additional no content check + if len(bidResp.SeatBid) == 0 || len(bidResp.SeatBid[0].Bid) == 0 { + return nil, nil + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp), + }) + } + } + return bidResponse, nil +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) openrtb_ext.BidType { + for _, imp := range imps { + if imp.ID == impID { + if imp.Banner != nil { + return openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + return openrtb_ext.BidTypeVideo + } else if imp.Native != nil { + return openrtb_ext.BidTypeNative + } else if imp.Audio != nil { + return openrtb_ext.BidTypeAudio + } + } + } + return openrtb_ext.BidTypeBanner +} diff --git a/adapters/admixer/admixer_test.go b/adapters/admixer/admixer_test.go new file mode 100644 index 00000000000..b379e8b2910 --- /dev/null +++ b/adapters/admixer/admixer_test.go @@ -0,0 +1,10 @@ +package admixer + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "testing" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "admixertest", NewAdmixerBidder("http://inv-nets.admixer.net/pbs.aspx")) +} diff --git a/adapters/admixer/admixertest/exemplary/optional-params.json b/adapters/admixer/admixertest/exemplary/optional-params.json new file mode 100644 index 00000000000..42a55ec95e8 --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/optional-params.json @@ -0,0 +1,104 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", + "customFloor": 0.1, + "customParams": { + "foo": "bar" + } + } + } + }, + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", + "customFloor": 0.1, + "customParams": { + "foo": [ + "bar", + "baz" + ] + } + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", + "bidfloor": 0.1, + "ext": { + "customParams": { + "foo": "bar" + } + } + }, + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", + "bidfloor": 0.1, + "ext": { + "customParams": { + "foo": [ + "bar", + "baz" + ] + } + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-app-audio.json b/adapters/admixer/admixertest/exemplary/simple-app-audio.json new file mode 100644 index 00000000000..b8c39ead95e --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-app-audio.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + }, + "ext": { + "bidder": { + "zone": "473e443c-43d0-423d-a8d7-a302637a01d8" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + }, + "tagid": "473e443c-43d0-423d-a8d7-a302637a01d8" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "admixer", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid" + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid" + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-app-banner.json b/adapters/admixer/admixertest/exemplary/simple-app-banner.json new file mode 100644 index 00000000000..aff4ccddd64 --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-app-banner.json @@ -0,0 +1,101 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "admixer", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 90, + "w": 728 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-app-native.json b/adapters/admixer/admixertest/exemplary/simple-app-native.json new file mode 100644 index 00000000000..38c005c651c --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-app-native.json @@ -0,0 +1,90 @@ + +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "ext": { + "bidder": { + "zone": "b1fbebfc-7155-4922-bb86-615e7f3d6eef" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "tagid": "b1fbebfc-7155-4922-bb86-615e7f3d6eef" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "admixer", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid" + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid" + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-app-video.json b/adapters/admixer/admixertest/exemplary/simple-app-video.json new file mode 100644 index 00000000000..627023fa1e6 --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-app-video.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "zone": "ac7fa772-d7be-48cc-820b-e21728e434fe" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 576 + }, + "tagid": "ac7fa772-d7be-48cc-820b-e21728e434fe" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "admixer", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 1024, + "h": 576 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-site-audio.json b/adapters/admixer/admixertest/exemplary/simple-site-audio.json new file mode 100644 index 00000000000..5a1d6531a85 --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-site-audio.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + }, + "ext": { + "bidder": { + "zone": "473e443c-43d0-423d-a8d7-a302637a01d8" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + }, + "tagid": "473e443c-43d0-423d-a8d7-a302637a01d8" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "admixer", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid" + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid" + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-site-banner.json b/adapters/admixer/admixertest/exemplary/simple-site-banner.json new file mode 100644 index 00000000000..bd50aba8d1a --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-site-banner.json @@ -0,0 +1,101 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "admixer", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 90, + "w": 728 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-site-native.json b/adapters/admixer/admixertest/exemplary/simple-site-native.json new file mode 100644 index 00000000000..246d02025b1 --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-site-native.json @@ -0,0 +1,90 @@ + +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "ext": { + "bidder": { + "zone": "b1fbebfc-7155-4922-bb86-615e7f3d6eef" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "tagid": "b1fbebfc-7155-4922-bb86-615e7f3d6eef" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "admixer", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid" + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid" + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/exemplary/simple-site-video.json b/adapters/admixer/admixertest/exemplary/simple-site-video.json new file mode 100644 index 00000000000..42d771ce86b --- /dev/null +++ b/adapters/admixer/admixertest/exemplary/simple-site-video.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "zone": "ac7fa772-d7be-48cc-820b-e21728e434fe" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 576 + }, + "tagid": "ac7fa772-d7be-48cc-820b-e21728e434fe" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "admixer", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 1024, + "h": 576 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/admixer/admixertest/params/race/audio.json b/adapters/admixer/admixertest/params/race/audio.json new file mode 100644 index 00000000000..f9aa771e4b1 --- /dev/null +++ b/adapters/admixer/admixertest/params/race/audio.json @@ -0,0 +1,5 @@ +{ + "zone": "473e443c-43d0-423d-a8d7-a302637a01d8", + "customFloor": 0.1, + "customParams": {"foo": "bar"} +} \ No newline at end of file diff --git a/adapters/admixer/admixertest/params/race/banner.json b/adapters/admixer/admixertest/params/race/banner.json new file mode 100644 index 00000000000..03510f7e1ca --- /dev/null +++ b/adapters/admixer/admixertest/params/race/banner.json @@ -0,0 +1,5 @@ +{ + "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", + "customFloor": 0.1, + "customParams": {"foo": "bar"} +} diff --git a/adapters/admixer/admixertest/params/race/native.json b/adapters/admixer/admixertest/params/race/native.json new file mode 100644 index 00000000000..65712a30228 --- /dev/null +++ b/adapters/admixer/admixertest/params/race/native.json @@ -0,0 +1,5 @@ +{ + "zone": "b1fbebfc-7155-4922-bb86-615e7f3d6eef", + "customFloor": 0.1, + "customParams": {"foo": "bar"} +} \ No newline at end of file diff --git a/adapters/admixer/admixertest/params/race/video.json b/adapters/admixer/admixertest/params/race/video.json new file mode 100644 index 00000000000..5e9bc6e59fd --- /dev/null +++ b/adapters/admixer/admixertest/params/race/video.json @@ -0,0 +1,5 @@ +{ + "zone": "ac7fa772-d7be-48cc-820b-e21728e434fe", + "customFloor": 0.1, + "customParams": {"foo": "bar"} +} diff --git a/adapters/admixer/admixertest/supplemental/bad-dsp-request-example.json b/adapters/admixer/admixertest/supplemental/bad-dsp-request-example.json new file mode 100644 index 00000000000..5256c14050b --- /dev/null +++ b/adapters/admixer/admixertest/supplemental/bad-dsp-request-example.json @@ -0,0 +1,70 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "zone": "3e56bd58-865c-47ce-af7f-a918108c3fd2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "tagid": "3e56bd58-865c-47ce-af7f-a918108c3fd2" + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": { + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Bad request to dsp", + "comparison": "literal" + } + ] +} diff --git a/adapters/admixer/admixertest/supplemental/dsp-server-internal-error-example.json b/adapters/admixer/admixertest/supplemental/dsp-server-internal-error-example.json new file mode 100644 index 00000000000..1c06eadce44 --- /dev/null +++ b/adapters/admixer/admixertest/supplemental/dsp-server-internal-error-example.json @@ -0,0 +1,70 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "zone": "3e56bd58-865c-47ce-af7f-a918108c3fd2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "tagid": "3e56bd58-865c-47ce-af7f-a918108c3fd2" + } + ] + } + }, + "mockResponse": { + "status": 500, + "body": { + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Dsp server internal error", + "comparison": "literal" + } + ] +} diff --git a/adapters/admixer/admixertest/supplemental/ext-unmarshall-error.json b/adapters/admixer/admixertest/supplemental/ext-unmarshall-error.json new file mode 100644 index 00000000000..e14a26356f8 --- /dev/null +++ b/adapters/admixer/admixertest/supplemental/ext-unmarshall-error.json @@ -0,0 +1,34 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "format": [ + { + "w": 728, + "h": 90 + } + ], + "ext": { + "bidder": { + "zone": [ + "ec943cb9-61ec-460f-a925-6489c3fcc4e3" + ] + } + } + } + ], + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "ec943cb9-61ec-460f-a925-6489c3fcc4e3" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Wrong Admixer bidder ext", + "comparison": "literal" + } + ] +} diff --git a/adapters/admixer/admixertest/supplemental/unknown-status-code-example.json b/adapters/admixer/admixertest/supplemental/unknown-status-code-example.json new file mode 100644 index 00000000000..972f2f5dd01 --- /dev/null +++ b/adapters/admixer/admixertest/supplemental/unknown-status-code-example.json @@ -0,0 +1,70 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "zone": "3e56bd58-865c-47ce-af7f-a918108c3fd2" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://inv-nets.admixer.net/pbs.aspx", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "tagid": "3e56bd58-865c-47ce-af7f-a918108c3fd2" + } + ] + } + }, + "mockResponse": { + "status": 301, + "body": { + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 301", + "comparison": "literal" + } + ] +} diff --git a/adapters/admixer/admixertest/supplemental/wrong-zone-id-error.json b/adapters/admixer/admixertest/supplemental/wrong-zone-id-error.json new file mode 100644 index 00000000000..2f2dcec1a50 --- /dev/null +++ b/adapters/admixer/admixertest/supplemental/wrong-zone-id-error.json @@ -0,0 +1,30 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "format": [{ + "w": 728, + "h": 90 + }], + "ext": { + "bidder": { + "zone": "12345" + } + } + } + ], + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "ec943cb9-61ec-460f-a925-6489c3fcc4e3" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "ZoneId must be UUID/GUID", + "comparison": "literal" + } + ] +} diff --git a/adapters/admixer/admixertest/supplemental/zero-bid-request-error.json b/adapters/admixer/admixertest/supplemental/zero-bid-request-error.json new file mode 100644 index 00000000000..a43d9b5fa65 --- /dev/null +++ b/adapters/admixer/admixertest/supplemental/zero-bid-request-error.json @@ -0,0 +1,19 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + ], + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impressions in request", + "comparison": "literal" + } + ] +} diff --git a/adapters/admixer/params_test.go b/adapters/admixer/params_test.go new file mode 100644 index 00000000000..71cccb6a3da --- /dev/null +++ b/adapters/admixer/params_test.go @@ -0,0 +1,57 @@ +package admixer + +import ( + "encoding/json" + "github.com/prebid/prebid-server/openrtb_ext" + "testing" +) + +// This file actually intends to test static/bidder-params/admixer.json +// +// These also validate the format of the external API: request.imp[i].ext.admixer + +// TestValidParams makes sure that the admixer 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.BidderAdmixer, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected admixer params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the admixer 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.BidderAdmixer, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21"}`, + `{"zone": "9ff668a2-4122-462e-aaf8-36ea3a54ba21"}`, + `{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21", "customFloor": 0.1}`, + `{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21", "customParams": {"foo": "bar"}}`, + `{"zone": "9ff668a2-4122-462e-aaf8-36ea3a54ba21", "customFloor": 0.1, "customParams": {"foo": ["bar", "baz"]}}`, +} + +var invalidParams = []string{ + `{"zone": "123"}`, + `{"zone": ""}`, + `{"zone": "ZFF668A2-4122-462E-AAF8-36EA3A54BA21"}`, + `{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA211"}`, + `{"zone": "123", "customFloor": "0.1"}`, + `{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21", "customFloor": -0.1}`, + `{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21", "customParams": "foo: bar"}`, +} diff --git a/adapters/admixer/usersync.go b/adapters/admixer/usersync.go new file mode 100644 index 00000000000..0a7f50ab79a --- /dev/null +++ b/adapters/admixer/usersync.go @@ -0,0 +1,11 @@ +package admixer + +import ( + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" + "text/template" +) + +func NewAdmixerSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("admixer", 511, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/admixer/usersync_test.go b/adapters/admixer/usersync_test.go new file mode 100644 index 00000000000..a5715c64a46 --- /dev/null +++ b/adapters/admixer/usersync_test.go @@ -0,0 +1,34 @@ +package admixer + +import ( + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" + "testing" + "text/template" +) + +func TestAdmixerSyncer(t *testing.T) { + syncURL := "https://inv-nets.admixer.net/adxcm.aspx?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=1&rurl=localhost%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewAdmixerSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "A", + Consent: "B", + }, + CCPA: ccpa.Policy{ + Value: "C", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://inv-nets.admixer.net/adxcm.aspx?gdpr=A&gdpr_consent=B&us_privacy=C&redir=1&rurl=localhost%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3DA%26gdpr_consent%3DB%26uid%3D%24%24visitor_cookie%24%24", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 511, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 52686422039..f582201d517 100644 --- a/config/config.go +++ b/config/config.go @@ -491,6 +491,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernelAdn, "https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3DadkernelAdn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdpone, "https://usersync.adpone.com/csync?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdvangelists, "https://nep.advangelists.com/xp/user-sync?acctid={aid}&&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadvangelists%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAppnexus, "https://ib.adnxs.com/getuid?"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadnxs%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeachfront, "https://sync.bfmio.com/sync_s2s?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbeachfront%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bio_cid%5D") @@ -673,6 +674,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.adform.endpoint", "http://adx.adform.net/adx") 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.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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index d169c1204bf..4c6da00f337 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -12,6 +12,7 @@ import ( "github.com/prebid/prebid-server/adapters/adform" "github.com/prebid/prebid-server/adapters/adkernel" "github.com/prebid/prebid-server/adapters/adkernelAdn" + "github.com/prebid/prebid-server/adapters/admixer" "github.com/prebid/prebid-server/adapters/adoppler" "github.com/prebid/prebid-server/adapters/adpone" "github.com/prebid/prebid-server/adapters/adtelligent" @@ -72,6 +73,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderAdform: adform.NewAdformBidder(client, cfg.Adapters[string(openrtb_ext.BidderAdform)].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.BidderAdoppler: adoppler.NewAdopplerBidder(cfg.Adapters[string(openrtb_ext.BidderAdoppler)].Endpoint), openrtb_ext.BidderAdpone: adpone.NewAdponeBidder(cfg.Adapters[string(openrtb_ext.BidderAdpone)].Endpoint), openrtb_ext.BidderAdtelligent: adtelligent.NewAdtelligentBidder(cfg.Adapters[string(openrtb_ext.BidderAdtelligent)].Endpoint), diff --git a/go.mod b/go.mod index 5c837c2ee7b..af4bf5570a5 100644 --- a/go.mod +++ b/go.mod @@ -7,17 +7,23 @@ require ( 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/aerospike/aerospike-client-go v2.7.2+incompatible github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/blang/semver v3.5.1+incompatible + github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 github.com/cespare/xxhash v1.0.0 // indirect github.com/chasex/glog v0.0.0-20160217080310-c62392af379c github.com/coocood/freecache v1.0.1 + github.com/didip/tollbooth v4.0.2+incompatible github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd + github.com/go-redis/redis v6.15.7+incompatible + github.com/gocql/gocql v0.0.0-20200203083758-81b8263d9fe5 github.com/gofrs/uuid v3.2.0+incompatible github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/golang/snappy v0.0.1 github.com/hashicorp/hcl v1.0.0 // indirect github.com/influxdata/influxdb v1.6.1 // indirect github.com/julienschmidt/httprouter v1.1.0 @@ -31,8 +37,10 @@ require ( github.com/mxmCherry/openrtb v11.0.0+incompatible github.com/onsi/ginkgo v1.10.1 // indirect github.com/onsi/gomega v1.7.0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/prebid/go-gdpr v0.6.0 + github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf 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 @@ -40,14 +48,15 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 github.com/rs/cors v1.5.0 github.com/sergi/go-diff v1.0.0 // indirect + github.com/sirupsen/logrus v1.4.2 github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.1.1 // indirect github.com/spf13/cast v1.2.0 // indirect github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 // indirect 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/valyala/fasthttp v1.9.0 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 +64,10 @@ 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 + github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect + golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/text v0.3.0 + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect gopkg.in/yaml.v2 v2.2.1 ) diff --git a/go.sum b/go.sum index 57fcbb76428..06f07b1ece0 100644 --- a/go.sum +++ b/go.sum @@ -6,35 +6,57 @@ 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/aerospike/aerospike-client-go v2.7.2+incompatible h1:bWbRf8trg1FhKF7u43KLGNfOH60RlvIgQjpaS107DZ8= +github.com/aerospike/aerospike-client-go v2.7.2+incompatible/go.mod h1:zj8LBEnWBDOVEIJt8LvaRvDG5ARAoa5dBeHaB472NRc= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 h1:y853v6rXx+zefEcjET3JuKAqvhj+FKflQijjeaSv2iA= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cespare/xxhash v1.0.0 h1:naDmySfoNg0nKS62/ujM6e71ZgM2AoVdaqGwMG0w18A= github.com/cespare/xxhash v1.0.0/go.mod h1:fX/lfQBkSCDXZSUgv6jVIu/EVA3/JNseAX5asI4c4T4= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c h1:eXqCBUHfmjbeDqcuvzjsd+bM6A+bnwo5N9FVbV6m5/s= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c/go.mod h1:omJZNg0Qu76bxJd+ExohVo8uXzNcGOk2bv7vel460xk= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coocood/freecache v1.0.1 h1:oFyo4msX2c0QIKU+kuMJUwsKamJ+AKc2JJrKcMszJ5M= github.com/coocood/freecache v1.0.1/go.mod h1:ePwxCDzOYvARfHdr1pByNct1at3CoKnsipOHwKlNbzI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= +github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd h1:biTJQdqouE5by89AAffXG8++TY+9Fsdrg5rinbt3tHk= github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= +github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/gocql/gocql v0.0.0-20200203083758-81b8263d9fe5 h1:ZZVxQRCm4ewuoqqLBJ7LHpsk4OGx2PkyCsRKLq4oHgE= +github.com/gocql/gocql v0.0.0-20200203083758-81b8263d9fe5/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -45,6 +67,17 @@ github.com/julienschmidt/httprouter v1.1.0 h1:7wLdtIiIpzOkC9u6sXOozpBauPdskj3ru4 github.com/julienschmidt/httprouter v1.1.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= @@ -66,18 +99,20 @@ github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 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/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtWKsUFH5IzdQxHIgP609VSIVe5Hywg2phs= +github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98= 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_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo18YNX3jzl+4G6Bstwt+kqv47GS12uL0= @@ -88,6 +123,8 @@ github.com/rs/cors v1.5.0 h1:dgSHE6+ia18arGOTIYQKKGWLvEbGvmbNE6NfxhoNHUY= github.com/rs/cors v1.5.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.1 h1:Lt3ihYMlE+lreX1GS4Qw4ZsNpYQLxIXKBTEOXm3nt6I= @@ -103,8 +140,14 @@ github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw= +github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 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= @@ -119,21 +162,34 @@ 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= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= +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-20190204203706-41f3e6584952/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= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 6e70ef4b6fa..3ae443410b9 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -25,6 +25,7 @@ const ( BidderAdkernel BidderName = "adkernel" BidderAdkernelAdn BidderName = "adkernelAdn" BidderAdpone BidderName = "adpone" + BidderAdmixer BidderName = "admixer" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderApplogy BidderName = "applogy" @@ -80,6 +81,7 @@ var BidderMap = map[string]BidderName{ "adform": BidderAdform, "adkernel": BidderAdkernel, "adkernelAdn": BidderAdkernelAdn, + "admixer": BidderAdmixer, "adpone": BidderAdpone, "adtelligent": BidderAdtelligent, "advangelists": BidderAdvangelists, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 5447cd28800..7f65c7f476f 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -4,13 +4,13 @@ import ( "strings" "text/template" - "github.com/prebid/prebid-server/adapters/adpone" - "github.com/golang/glog" ttx "github.com/prebid/prebid-server/adapters/33across" "github.com/prebid/prebid-server/adapters/adform" "github.com/prebid/prebid-server/adapters/adkernel" "github.com/prebid/prebid-server/adapters/adkernelAdn" + "github.com/prebid/prebid-server/adapters/admixer" + "github.com/prebid/prebid-server/adapters/adpone" "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" "github.com/prebid/prebid-server/adapters/appnexus" @@ -68,6 +68,7 @@ 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.BidderAdpone, adpone.NewadponeSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdtelligent, adtelligent.NewAdtelligentSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdvangelists, advangelists.NewAdvangelistsSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 87a9caebf96..2a8d1fd1b0b 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -18,6 +18,7 @@ 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.BidderAdpone): syncConfig, string(openrtb_ext.BidderAdtelligent): syncConfig, string(openrtb_ext.BidderAdvangelists): syncConfig, From 2e806517d34fa7bb9a8dfcd819915cf55e96b8a6 Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Tue, 3 Mar 2020 06:59:15 -0800 Subject: [PATCH 020/381] Adding copying of gdpr consent string to openrtb bid request (#1189) * Adding copying of gdpr consent string to openrtb bid request * Updated video request to use OpenRTB Video and User objects * Fixing unit test failure message * Updates from code review comments * Updating unit test initialization * Updated mimes array construction --- endpoints/openrtb2/video_auction.go | 50 +++++++------- endpoints/openrtb2/video_auction_test.go | 66 +++++++++++++++--- openrtb_ext/bid_request_video.go | 87 +----------------------- 3 files changed, 81 insertions(+), 122 deletions(-) diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 7c9651af747..feb8de193e7 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -328,12 +328,12 @@ func max(a, b int) int { return b } -func createImpressionTemplate(imp openrtb.Imp, video openrtb_ext.SimplifiedVideo) openrtb.Imp { +func createImpressionTemplate(imp openrtb.Imp, video *openrtb.Video) 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 + imp.Video.MIMEs = video.MIMEs return imp } @@ -471,14 +471,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 @@ -660,27 +653,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 cd87041055a..dfe2a6a50b8 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -233,8 +233,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 +271,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 +345,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 +418,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 +473,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 +484,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 +670,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 +691,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) { diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go index f7ddf203294..18865108433 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 @@ -225,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"` -} From c6919ee17741b2ed5f3ffbb02d6d24ecefc629be Mon Sep 17 00:00:00 2001 From: johnwier <49074029+johnwier@users.noreply.github.com> Date: Wed, 4 Mar 2020 07:28:54 -0800 Subject: [PATCH 021/381] fix conversant sync pixel (#1208) --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index f582201d517..7c9ffa71672 100644 --- a/config/config.go +++ b/config/config.go @@ -497,7 +497,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeachfront, "https://sync.bfmio.com/sync_s2s?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbeachfront%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bio_cid%5D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBrightroll, "https://pr-bh.ybp.yahoo.com/sync/appnexusprebidserver/?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/prebid/match/bounce/current?rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26networkId%3D72582%26version%3D1%26uid%3D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/match/bounce/current?rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconversant%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26networkId%3D72582%26version%3D1%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderCpmstar, "https://server.cpmstar.com/usersync.aspx?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Demx_digital%26uid%3D%24UID") From f03dfa56d7689502e462ad306eb273075237fae7 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 4 Mar 2020 10:25:38 -0800 Subject: [PATCH 022/381] openx adapter: forward bid response currency in openx adapter if set (#1211) it was always set to the default USD before --- adapters/openx/openx.go | 5 +++++ adapters/openx/openx_test.go | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go index dd176813820..63e8e697869 100644 --- a/adapters/openx/openx.go +++ b/adapters/openx/openx.go @@ -169,6 +169,11 @@ func (a *OpenxAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalReq bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + // overrride default currency + if bidResp.Cur != "" { + bidResponse.Currency = bidResp.Cur + } + for _, sb := range bidResp.SeatBid { for i := range sb.Bid { bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ diff --git a/adapters/openx/openx_test.go b/adapters/openx/openx_test.go index f7765d846ad..f79eb062531 100644 --- a/adapters/openx/openx_test.go +++ b/adapters/openx/openx_test.go @@ -1,11 +1,48 @@ package openx import ( + "encoding/json" "testing" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/stretchr/testify/assert" ) func TestJsonSamples(t *testing.T) { adapterstest.RunJSONBidderTest(t, "openxtest", NewOpenxBidder("http://rtb.openx.net/prebid")) } + +func TestResponseWithCurrencies(t *testing.T) { + assertCurrencyInBidResponse(t, "USD", nil) + + currency := "USD" + assertCurrencyInBidResponse(t, "USD", ¤cy) + + currency = "EUR" + assertCurrencyInBidResponse(t, "EUR", ¤cy) +} + +func assertCurrencyInBidResponse(t *testing.T, expectedCurrency string, currency *string) { + + bidder := NewOpenxBidder("http://rtb.openx.net/prebid") + prebidRequest := &openrtb.BidRequest{ + Imp: []openrtb.Imp{}, + } + mockedBidResponse := &openrtb.BidResponse{} + if currency != nil { + mockedBidResponse.Cur = *currency + } + body, _ := json.Marshal(mockedBidResponse) + responseData := &adapters.ResponseData{ + StatusCode: 200, + Body: body, + } + bidResponse, errs := bidder.MakeBids(prebidRequest, nil, responseData) + + if errs != nil { + t.Fatalf("Failed to make bids %v", errs) + } + assert.Equal(t, expectedCurrency, bidResponse.Currency) +} From 8686f03a46ad51ccc4a96f9756628abdc8e60176 Mon Sep 17 00:00:00 2001 From: guscarreon Date: Thu, 5 Mar 2020 10:41:38 -0500 Subject: [PATCH 023/381] add ucfunnel adapter (#1192) --- adapters/ucfunnel/params_test.go | 47 +++++ adapters/ucfunnel/ucfunnel.go | 150 ++++++++++++++++ adapters/ucfunnel/ucfunnel_test.go | 163 ++++++++++++++++++ .../ucfunneltest/exemplary/ucfunnel.json | 103 +++++++++++ .../ucfunneltest/params/race/banner.json | 5 + .../ucfunneltest/params/race/video.json | 5 + adapters/ucfunnel/usersync.go | 12 ++ adapters/ucfunnel/usersync_test.go | 30 ++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_ucfunnel.go | 7 + static/bidder-info/ucfunnel.yaml | 11 ++ static/bidder-params/ucfunnel.json | 17 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 16 files changed, 559 insertions(+) create mode 100644 adapters/ucfunnel/params_test.go create mode 100644 adapters/ucfunnel/ucfunnel.go create mode 100644 adapters/ucfunnel/ucfunnel_test.go create mode 100644 adapters/ucfunnel/ucfunneltest/exemplary/ucfunnel.json create mode 100644 adapters/ucfunnel/ucfunneltest/params/race/banner.json create mode 100644 adapters/ucfunnel/ucfunneltest/params/race/video.json create mode 100644 adapters/ucfunnel/usersync.go create mode 100644 adapters/ucfunnel/usersync_test.go create mode 100644 openrtb_ext/imp_ucfunnel.go create mode 100644 static/bidder-info/ucfunnel.yaml create mode 100644 static/bidder-params/ucfunnel.json diff --git a/adapters/ucfunnel/params_test.go b/adapters/ucfunnel/params_test.go new file mode 100644 index 00000000000..4faec8739da --- /dev/null +++ b/adapters/ucfunnel/params_test.go @@ -0,0 +1,47 @@ +package ucfunnel + +import ( + "encoding/json" + "github.com/prebid/prebid-server/openrtb_ext" + "testing" +) + +// This file actually intends to test static/bidder-params/ucfunnel.json +// +// These also validate the format of the external API: request.imp[i].ext.ucfunnel + +// TestValidParams makes sure that the ucfunnel 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.BidderUcfunnel, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected ucfunnel params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the ucfunnel 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.BidderUcfunnel, json.RawMessage(invalidParam)); err != nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"adunitid": "ad-83444226E44368D1E32E49EEBE6D29","partnerid": "par-2EDDB423AA24474188B843EE4842932"}`, +} + +var invalidParams = []string{ + `{"adunitid": "","partnerid": ""}`, +} diff --git a/adapters/ucfunnel/ucfunnel.go b/adapters/ucfunnel/ucfunnel.go new file mode 100644 index 00000000000..2c64e81205b --- /dev/null +++ b/adapters/ucfunnel/ucfunnel.go @@ -0,0 +1,150 @@ +package ucfunnel + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" + "net/url" +) + +type UcfunnelAdapter struct { + URI string +} + +func NewUcfunnelBidder(endpoint string) *UcfunnelAdapter { + return &UcfunnelAdapter{ + URI: endpoint} +} + +func (a *UcfunnelAdapter) Name() string { + return "ucfunnel" +} + +func (a *UcfunnelAdapter) SkipNoCookies() bool { + return false +} + +func (a *UcfunnelAdapter) 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 errs []error + var bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + var bidReq openrtb.BidRequest + if err := json.Unmarshal(externalRequest.Body, &bidReq); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + bidType := getBidType(bidReq, sb.Bid[i].ImpID) + if (bidType == openrtb_ext.BidTypeBanner) || (bidType == openrtb_ext.BidTypeVideo) { + b := &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, errs +} + +func (a *UcfunnelAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + errs := make([]error, 0, len(request.Imp)) + + // If all the requests were malformed, don't bother making a server call with no impressions. + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("No impression in the bid request\n"), + }} + } + + partnerId, partnerErr := getPartnerId(request) + if partnerErr != nil { + return nil, partnerErr + } + + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json") + + uri := a.URI + "/" + url.PathEscape(partnerId) + "/request" + return []*adapters.RequestData{{ + Method: "POST", + Uri: uri, + Body: reqJSON, + Headers: headers, + }}, errs +} + +func getPartnerId(request *openrtb.BidRequest) (string, []error) { + var ext ExtBidderUcfunnel + var errs = []error{} + err := json.Unmarshal(request.Imp[0].Ext, &ext) + if err != nil { + errs = append(errs, err) + return "", errs + } + errs = checkBidderParameter(ext) + if errs != nil { + return "", errs + } + return ext.Bidder.PartnerId, nil +} + +func checkBidderParameter(ext ExtBidderUcfunnel) []error { + var errs = []error{} + if len(ext.Bidder.PartnerId) == 0 || len(ext.Bidder.AdUnitId) == 0 { + errs = append(errs, fmt.Errorf("No PartnerId or AdUnitId in the bid request\n")) + return errs + } + return nil +} + +func getBidType(bidReq openrtb.BidRequest, impid string) openrtb_ext.BidType { + for i := range bidReq.Imp { + if bidReq.Imp[i].ID == impid { + if bidReq.Imp[i].Banner != nil { + return openrtb_ext.BidTypeBanner + } else if bidReq.Imp[i].Video != nil { + return openrtb_ext.BidTypeVideo + } else if bidReq.Imp[i].Audio != nil { + return openrtb_ext.BidTypeAudio + } else if bidReq.Imp[i].Native != nil { + return openrtb_ext.BidTypeNative + } + } + } + return openrtb_ext.BidTypeNative +} + +type ExtBidderUcfunnel struct { + Bidder openrtb_ext.ExtImpUcfunnel `json:"bidder"` +} diff --git a/adapters/ucfunnel/ucfunnel_test.go b/adapters/ucfunnel/ucfunnel_test.go new file mode 100644 index 00000000000..60d796fdf99 --- /dev/null +++ b/adapters/ucfunnel/ucfunnel_test.go @@ -0,0 +1,163 @@ +package ucfunnel + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestUcfunnelAdapterNames(t *testing.T) { + adapter := NewUcfunnelBidder("http://localhost/bid") + adapterstest.VerifyStringValue(adapter.Name(), "ucfunnel", t) +} + +func TestSkipNoCookies(t *testing.T) { + adapter := NewUcfunnelBidder("http://localhost/bid") + status := adapter.SkipNoCookies() + if status != false { + t.Errorf("actual = %t expected != %t", status, false) + } +} + +func TestMakeRequests(t *testing.T) { + + imp := openrtb.Imp{ + ID: "1234", + Banner: &openrtb.Banner{}, + } + imp2 := openrtb.Imp{ + ID: "1235", + Video: &openrtb.Video{}, + } + + imp3 := openrtb.Imp{ + ID: "1236", + Audio: &openrtb.Audio{}, + } + + imp4 := openrtb.Imp{ + ID: "1237", + Native: &openrtb.Native{}, + } + imp5 := openrtb.Imp{ + ID: "1237", + Native: &openrtb.Native{}, + } + + internalRequest01 := openrtb.BidRequest{Imp: []openrtb.Imp{}} + internalRequest02 := openrtb.BidRequest{Imp: []openrtb.Imp{imp, imp2, imp3, imp4, imp5}} + internalRequest03 := openrtb.BidRequest{Imp: []openrtb.Imp{imp, imp2, imp3, imp4, imp5}} + + internalRequest03.Imp[0].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[1].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[2].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[3].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[4].Ext = []byte(`{"bidder": {"adunitid": "aa","partnerid": ""}}`) + + adapter := NewUcfunnelBidder("http://localhost/bid") + + var testCases = []struct { + in []openrtb.BidRequest + out1 [](int) + out2 [](bool) + }{ + { + in: []openrtb.BidRequest{internalRequest01, internalRequest02, internalRequest03}, + out1: [](int){1, 1, 0}, + out2: [](bool){false, false, true}, + }, + } + + for idx := range testCases { + for i := range testCases[idx].in { + RequestData, err := adapter.MakeRequests(&testCases[idx].in[i], nil) + if ((RequestData == nil) == testCases[idx].out2[i]) && (len(err) == testCases[idx].out1[i]) { + t.Errorf("actual = %v expected = %v", len(err), testCases[idx].out1[i]) + } + } + } +} + +func TestMakeBids(t *testing.T) { + imp := openrtb.Imp{ + ID: "1234", + Banner: &openrtb.Banner{}, + } + imp2 := openrtb.Imp{ + ID: "1235", + Video: &openrtb.Video{}, + } + + imp3 := openrtb.Imp{ + ID: "1236", + Audio: &openrtb.Audio{}, + } + + imp4 := openrtb.Imp{ + ID: "1237", + Native: &openrtb.Native{}, + } + imp5 := openrtb.Imp{ + ID: "1237", + Native: &openrtb.Native{}, + } + + internalRequest03 := openrtb.BidRequest{Imp: []openrtb.Imp{imp, imp2, imp3, imp4, imp5}} + internalRequest04 := openrtb.BidRequest{Imp: []openrtb.Imp{imp}} + + internalRequest03.Imp[0].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[1].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[2].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[3].Ext = []byte(`{"bidder": {"adunitid": "ad-488663D474E44841E8A293379892348","partnerid": "par-7E6D2DB9A8922AB07B44A444D2BA67"}}`) + internalRequest03.Imp[4].Ext = []byte(`{"bidder": {"adunitid": "aa","partnerid": ""}}`) + internalRequest04.Imp[0].Ext = []byte(`{"bidder": {"adunitid": "0"}}`) + + mockResponse200 := adapters.ResponseData{StatusCode: 200, Body: json.RawMessage(`{"seatbid": [{"bid": [{"impid": "1234"}]},{"bid": [{"impid": "1235"}]},{"bid": [{"impid": "1236"}]},{"bid": [{"impid": "1237"}]}]}`)} + mockResponse203 := adapters.ResponseData{StatusCode: 203, Body: json.RawMessage(`{"seatbid":[{"bid":[{"impid":"1234"}]},{"bid":[{"impid":"1235"}]}]}`)} + mockResponse204 := adapters.ResponseData{StatusCode: 204, Body: json.RawMessage(`{"seatbid":[{"bid":[{"impid":"1234"}]},{"bid":[{"impid":"1235"}]}]}`)} + mockResponse400 := adapters.ResponseData{StatusCode: 400, Body: json.RawMessage(`{"seatbid":[{"bid":[{"impid":"1234"}]},{"bid":[{"impid":"1235"}]}]}`)} + mockResponseError := adapters.ResponseData{StatusCode: 200, Body: json.RawMessage(`{"seatbid":[{"bid":[{"im236"}],{"bid":[{"impid":"1237}]}`)} + + RequestData01 := adapters.RequestData{Method: "POST", Body: []byte(`{"imp":[{"id":"1234","banner":{}},{"id":"1235","video":{}},{"id":"1236","audio":{}},{"id":"1237","native":{}}]}`)} + RequestData02 := adapters.RequestData{Method: "POST", Body: []byte(`{"imp":[{"id":"1234","banne"1235","video":{}},{"id":"1236","audio":{}},{"id":"1237","native":{}}]}`)} + + adapter := NewUcfunnelBidder("http://localhost/bid") + + var testCases = []struct { + in1 []openrtb.BidRequest + in2 []adapters.RequestData + in3 []adapters.ResponseData + out1 [](bool) + out2 [](bool) + }{ + { + in1: []openrtb.BidRequest{internalRequest03, internalRequest03, internalRequest03, internalRequest03, internalRequest03, internalRequest04}, + in2: []adapters.RequestData{RequestData01, RequestData01, RequestData01, RequestData01, RequestData01, RequestData02}, + in3: []adapters.ResponseData{mockResponse200, mockResponse203, mockResponse204, mockResponse400, mockResponseError, mockResponse200}, + out1: [](bool){true, false, false, false, false, false}, + out2: [](bool){false, true, false, true, true, true}, + }, + } + + for idx := range testCases { + for i := range testCases[idx].in1 { + BidderResponse, err := adapter.MakeBids(&testCases[idx].in1[i], &testCases[idx].in2[i], &testCases[idx].in3[i]) + + if (BidderResponse == nil) == testCases[idx].out1[i] { + fmt.Println(i) + fmt.Println("BidderResponse") + t.Errorf("actual = %t expected == %v", (BidderResponse == nil), testCases[idx].out1[i]) + } + + if (err == nil) == testCases[idx].out2[i] { + fmt.Println(i) + fmt.Println("error") + t.Errorf("actual = %t expected == %v", err, testCases[idx].out2[i]) + } + } + } +} diff --git a/adapters/ucfunnel/ucfunneltest/exemplary/ucfunnel.json b/adapters/ucfunnel/ucfunneltest/exemplary/ucfunnel.json new file mode 100644 index 00000000000..2a7e4b2b861 --- /dev/null +++ b/adapters/ucfunnel/ucfunneltest/exemplary/ucfunnel.json @@ -0,0 +1,103 @@ +{ + "imp": [ + { + "id": "1", + "banner": { + "w": 970, + "h": 250, + "mimes": [ + "image/gif", + "image/jpeg", + "image/png", + "text/html", + "text/javascript", + "application/javascript" + ], + "pos": 1, + "battr": [ + 6, + 7 + ], + "topframe": 1 + }, + "instl": 0, + "displaymanager": "aralego.com", + "displaymanagerver": "v1.0.0", + "secure": 0, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "exp": 3600, + "ext":{ + "ucfunnel":{ + "adunitid":"ad-BE7E9EB323E9996218A733887B6E924" + } + } + } + ], + "id": "c901f218-4ca6-480b-97dc-c6fd50e24544", + "at": 2, + "tmax": 300, + "bcat": [ + "IAB11-1" + ], + "badv": [ + "abc.com", + "cbn.com", + "xyz.com" + ], + "regs": { + "coppa": 0, + "ext": { + "gdpr": 0 + }, + "us_privacy": "1--" + }, + "site": { + "id": "5b88fd05beffd764bb0f7a3a", + "name": "test", + "page": "http://127.0.0.1:8000/tmp66.html", + "domain": "127.0.0.1", + "cat": [ + "IAB1" + ], + "publisher": { + "id": "par-7E6D2DB9A8922AB07B44A444D2BA67" + } + }, + "device": { + "w": 1440, + "h": 900, + "dnt": 1, + "ip": "127.0.0.1", + "js": 1, + "os": "MacOS", + "osv": "10.15.1", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36", + "language": "zh-TW", + "devicetype": 2, + "geo": { + "country": "unknown", + "type": 2 + } + }, + "user": { + "id": "e7bf9b85-9554-441c-964e-c8112d35d17b" + }, + "source": { + "fd": 1, + "ext": { + "schain": { + "complete": 1, + "ver": "1.0", + "nodes": [ + { + "asi": "aralego.com", + "sid": "par-7E6D2DB9A8922AB07B44A444D2BA67", + "rid": "c8f800c2-d285-4cb9-8fc9-f95df52f6e0c", + "hp": 1 + } + ] + } + } + } +} \ No newline at end of file diff --git a/adapters/ucfunnel/ucfunneltest/params/race/banner.json b/adapters/ucfunnel/ucfunneltest/params/race/banner.json new file mode 100644 index 00000000000..2c8c2e1e198 --- /dev/null +++ b/adapters/ucfunnel/ucfunneltest/params/race/banner.json @@ -0,0 +1,5 @@ +{ + "adunitid": "ad-83444226E44368D1E32E49EEBE6D29", + "partnerid": "par-2EDDB423AA24474188B843EE4842932" +} + \ No newline at end of file diff --git a/adapters/ucfunnel/ucfunneltest/params/race/video.json b/adapters/ucfunnel/ucfunneltest/params/race/video.json new file mode 100644 index 00000000000..0a562b34aa1 --- /dev/null +++ b/adapters/ucfunnel/ucfunneltest/params/race/video.json @@ -0,0 +1,5 @@ +{ + "adunitid": "ad-E2B22B678D6A664E092824848D26BB2", + "partnerid": "par-2EDDB423AA24474188B843EE4842932" +} + \ No newline at end of file diff --git a/adapters/ucfunnel/usersync.go b/adapters/ucfunnel/usersync.go new file mode 100644 index 00000000000..92eba0d73e0 --- /dev/null +++ b/adapters/ucfunnel/usersync.go @@ -0,0 +1,12 @@ +package ucfunnel + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewUcfunnelSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("ucfunnel", 607, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/ucfunnel/usersync_test.go b/adapters/ucfunnel/usersync_test.go new file mode 100644 index 00000000000..45320b8cac1 --- /dev/null +++ b/adapters/ucfunnel/usersync_test.go @@ -0,0 +1,30 @@ +package ucfunnel + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestUcfunnelSyncer(t *testing.T) { + syncURL := "//sync.aralego.com/idsync?gdpr={{.GDPR}}&redirect=externalURL.com%2Fsetuid%3Fbidder%3Ducfunnel%26uid%3DSspCookieUserId" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewUcfunnelSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "//sync.aralego.com/idsync?gdpr=0&redirect=externalURL.com%2Fsetuid%3Fbidder%3Ducfunnel%26uid%3DSspCookieUserId", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 607, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 7c9ffa71672..b9501cf6355 100644 --- a/config/config.go +++ b/config/config.go @@ -529,6 +529,7 @@ func (cfg *Configuration) setDerivedDefaults() { // openrtb_ext.BidderTappx doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTriplelift, "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ducfunnel%26uid%3DSspCookieUserId") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUnruly, "https://usermatch.targeting.unrulymedia.com/pbsync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dunruly%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dvisx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") // openrtb_ext.BidderVrtcal doesn't have a good default. @@ -719,6 +720,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.ucfunnel.endpoint", "http://apac-hk-adx.aralego.com/prebid") v.SetDefault("adapters.unruly.endpoint", "http://targeting.unrulymedia.com/openrtb/2.2") v.SetDefault("adapters.verizonmedia.disabled", true) v.SetDefault("adapters.visx.endpoint", "https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 4c6da00f337..f0ff9c5b1a7 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -55,6 +55,7 @@ import ( "github.com/prebid/prebid-server/adapters/tappx" "github.com/prebid/prebid-server/adapters/triplelift" "github.com/prebid/prebid-server/adapters/triplelift_native" + "github.com/prebid/prebid-server/adapters/ucfunnel" "github.com/prebid/prebid-server/adapters/unruly" "github.com/prebid/prebid-server/adapters/verizonmedia" "github.com/prebid/prebid-server/adapters/visx" @@ -123,6 +124,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderTappx: tappx.NewTappxBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderTappx))].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.BidderVerizonMedia: verizonmedia.NewVerizonMediaBidder(client, cfg.Adapters[string(openrtb_ext.BidderVerizonMedia)].Endpoint), openrtb_ext.BidderVisx: visx.NewVisxBidder(cfg.Adapters[string(openrtb_ext.BidderVisx)].Endpoint), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 3ae443410b9..d1490603b50 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -68,6 +68,7 @@ const ( BidderTappx BidderName = "tappx" BidderTriplelift BidderName = "triplelift" BidderTripleliftNative BidderName = "triplelift_native" + BidderUcfunnel BidderName = "ucfunnel" BidderUnruly BidderName = "unruly" BidderVerizonMedia BidderName = "verizonmedia" BidderVisx BidderName = "visx" @@ -125,6 +126,7 @@ var BidderMap = map[string]BidderName{ "tappx": BidderTappx, "triplelift": BidderTriplelift, "triplelift_native": BidderTripleliftNative, + "ucfunnel": BidderUcfunnel, "unruly": BidderUnruly, "verizonmedia": BidderVerizonMedia, "visx": BidderVisx, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 7f65c7f476f..eb25171854a 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -48,6 +48,7 @@ import ( "github.com/prebid/prebid-server/adapters/synacormedia" "github.com/prebid/prebid-server/adapters/triplelift" "github.com/prebid/prebid-server/adapters/triplelift_native" + "github.com/prebid/prebid-server/adapters/ucfunnel" "github.com/prebid/prebid-server/adapters/unruly" "github.com/prebid/prebid-server/adapters/verizonmedia" "github.com/prebid/prebid-server/adapters/visx" @@ -107,6 +108,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderSynacormedia, synacormedia.NewSynacorMediaSyncer) 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.BidderVerizonMedia, verizonmedia.NewVerizonMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderVisx, visx.NewVisxSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 2a8d1fd1b0b..dc224fe99bf 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -57,6 +57,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderSynacormedia): syncConfig, string(openrtb_ext.BidderTriplelift): syncConfig, string(openrtb_ext.BidderTripleliftNative): syncConfig, + string(openrtb_ext.BidderUcfunnel): syncConfig, string(openrtb_ext.BidderUnruly): syncConfig, string(openrtb_ext.BidderVerizonMedia): syncConfig, string(openrtb_ext.BidderVisx): syncConfig, From 199d1dcaecc3685596c6a31bf02631fc97bc8a37 Mon Sep 17 00:00:00 2001 From: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Date: Tue, 10 Mar 2020 01:04:30 +0300 Subject: [PATCH 024/381] Update required params for TheMediaGrid adapter (#1188) --- adapters/grid/grid.go | 44 ++++++++++++++++++- .../gridtest/exemplary/simple-banner.json | 8 +++- .../grid/gridtest/exemplary/simple-video.json | 8 +++- .../grid/gridtest/params/race/banner.json | 5 ++- .../supplemental/bad_bidder_request.json | 33 ++++++++++++++ .../supplemental/bad_ext_request.json | 30 +++++++++++++ .../gridtest/supplemental/bad_response.json | 2 + .../supplemental/empty_uid_request.json | 33 ++++++++++++++ .../gridtest/supplemental/no_imp_request.json | 13 ++++++ .../gridtest/supplemental/status_204.json | 2 + .../gridtest/supplemental/status_400.json | 2 + .../gridtest/supplemental/status_418.json | 2 + openrtb_ext/imp_grid.go | 6 +++ static/bidder-params/grid.json | 7 ++- 14 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 adapters/grid/gridtest/supplemental/bad_bidder_request.json create mode 100644 adapters/grid/gridtest/supplemental/bad_ext_request.json create mode 100644 adapters/grid/gridtest/supplemental/empty_uid_request.json create mode 100644 adapters/grid/gridtest/supplemental/no_imp_request.json create mode 100644 openrtb_ext/imp_grid.go diff --git a/adapters/grid/grid.go b/adapters/grid/grid.go index 3e38edd4578..dd18d52d95a 100644 --- a/adapters/grid/grid.go +++ b/adapters/grid/grid.go @@ -15,11 +15,53 @@ type GridAdapter struct { endpoint string } +func processImp(imp *openrtb.Imp) error { + // get the grid extension + var ext adapters.ExtImpBidder + var gridExt openrtb_ext.ExtImpGrid + if err := json.Unmarshal(imp.Ext, &ext); err != nil { + return err + } + if err := json.Unmarshal(ext.Bidder, &gridExt); err != nil { + return err + } + + if gridExt.Uid == 0 { + err := &errortypes.BadInput{ + Message: "uid is empty", + } + return err + } + // no error + return nil +} + // MakeRequests makes the HTTP requests which should be made to fetch bids. func (a *GridAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var errors = make([]error, 0) - reqJSON, err := json.Marshal(request) + // copy the request, because we are going to mutate it + requestCopy := *request + // this will contain all the valid impressions + var validImps []openrtb.Imp + // pre-process the imps + for _, imp := range requestCopy.Imp { + if err := processImp(&imp); err == nil { + validImps = append(validImps, imp) + } else { + errors = append(errors, err) + } + } + if len(validImps) == 0 { + err := &errortypes.BadInput{ + Message: "No valid impressions for grid", + } + errors = append(errors, err) + return nil, errors + } + requestCopy.Imp = validImps + + reqJSON, err := json.Marshal(requestCopy) if err != nil { errors = append(errors, err) return nil, errors diff --git a/adapters/grid/gridtest/exemplary/simple-banner.json b/adapters/grid/gridtest/exemplary/simple-banner.json index b098a94f9ba..1a5ea014d0f 100644 --- a/adapters/grid/gridtest/exemplary/simple-banner.json +++ b/adapters/grid/gridtest/exemplary/simple-banner.json @@ -13,7 +13,9 @@ }] }, "ext": { - "bidder": {} + "bidder": { + "uid": 1 + } } }] }, @@ -35,7 +37,9 @@ }] }, "ext": { - "bidder": {} + "bidder": { + "uid": 1 + } } }] } diff --git a/adapters/grid/gridtest/exemplary/simple-video.json b/adapters/grid/gridtest/exemplary/simple-video.json index fcf783da2a4..12c3771d1b2 100644 --- a/adapters/grid/gridtest/exemplary/simple-video.json +++ b/adapters/grid/gridtest/exemplary/simple-video.json @@ -13,7 +13,9 @@ "h": 250 }, "ext": { - "bidder": {} + "bidder": { + "uid": 1 + } } }] }, @@ -35,7 +37,9 @@ "h": 250 }, "ext": { - "bidder": {} + "bidder": { + "uid": 1 + } } }] } diff --git a/adapters/grid/gridtest/params/race/banner.json b/adapters/grid/gridtest/params/race/banner.json index 0967ef424bc..7e347f11b45 100644 --- a/adapters/grid/gridtest/params/race/banner.json +++ b/adapters/grid/gridtest/params/race/banner.json @@ -1 +1,4 @@ -{} +{ + "uid": 1 +} + diff --git a/adapters/grid/gridtest/supplemental/bad_bidder_request.json b/adapters/grid/gridtest/supplemental/bad_bidder_request.json new file mode 100644 index 00000000000..347a3091a41 --- /dev/null +++ b/adapters/grid/gridtest/supplemental/bad_bidder_request.json @@ -0,0 +1,33 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": "some not exist" + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb_ext.ExtImpGrid", + "comparison": "literal" + }, + { + "value": "No valid impressions for grid", + "comparison": "literal" + } + ], + "httpCalls":[], + "expectedBidResponses": [] +} diff --git a/adapters/grid/gridtest/supplemental/bad_ext_request.json b/adapters/grid/gridtest/supplemental/bad_ext_request.json new file mode 100644 index 00000000000..789db8504f8 --- /dev/null +++ b/adapters/grid/gridtest/supplemental/bad_ext_request.json @@ -0,0 +1,30 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": "any" + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + }, + { + "value": "No valid impressions for grid", + "comparison": "literal" + } + ], + "httpCalls": [] +} diff --git a/adapters/grid/gridtest/supplemental/bad_response.json b/adapters/grid/gridtest/supplemental/bad_response.json index 4ad5c09cf37..87436da7fc1 100644 --- a/adapters/grid/gridtest/supplemental/bad_response.json +++ b/adapters/grid/gridtest/supplemental/bad_response.json @@ -14,6 +14,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } @@ -39,6 +40,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } diff --git a/adapters/grid/gridtest/supplemental/empty_uid_request.json b/adapters/grid/gridtest/supplemental/empty_uid_request.json new file mode 100644 index 00000000000..ff389899788 --- /dev/null +++ b/adapters/grid/gridtest/supplemental/empty_uid_request.json @@ -0,0 +1,33 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": {} + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "uid is empty", + "comparison": "literal" + }, + { + "value": "No valid impressions for grid", + "comparison": "literal" + } + ], + "httpCalls":[], + "expectedBidResponses": [] +} diff --git a/adapters/grid/gridtest/supplemental/no_imp_request.json b/adapters/grid/gridtest/supplemental/no_imp_request.json new file mode 100644 index 00000000000..5e261647fb5 --- /dev/null +++ b/adapters/grid/gridtest/supplemental/no_imp_request.json @@ -0,0 +1,13 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [] + }, + "expectedMakeRequestsErrors": [ + { + "value": "No valid impressions for grid", + "comparison": "literal" + } + ], + "httpCalls":[] +} diff --git a/adapters/grid/gridtest/supplemental/status_204.json b/adapters/grid/gridtest/supplemental/status_204.json index 906d8553bc6..f935cbe85ae 100644 --- a/adapters/grid/gridtest/supplemental/status_204.json +++ b/adapters/grid/gridtest/supplemental/status_204.json @@ -14,6 +14,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } @@ -39,6 +40,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } diff --git a/adapters/grid/gridtest/supplemental/status_400.json b/adapters/grid/gridtest/supplemental/status_400.json index dbf2a4d7b2b..629b1c07bd7 100644 --- a/adapters/grid/gridtest/supplemental/status_400.json +++ b/adapters/grid/gridtest/supplemental/status_400.json @@ -14,6 +14,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } @@ -39,6 +40,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } diff --git a/adapters/grid/gridtest/supplemental/status_418.json b/adapters/grid/gridtest/supplemental/status_418.json index 7619cd6aec1..0ca365c76ce 100644 --- a/adapters/grid/gridtest/supplemental/status_418.json +++ b/adapters/grid/gridtest/supplemental/status_418.json @@ -14,6 +14,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } @@ -39,6 +40,7 @@ }, "ext": { "bidder": { + "uid": 1 } } } 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/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": [] } From 5a9da6672f5c51b4c90b8d0feb4e7649d039bf1e Mon Sep 17 00:00:00 2001 From: htang555 Date: Thu, 12 Mar 2020 10:43:41 -0700 Subject: [PATCH 025/381] add zeroclickfraud adapter (#1207) * add zeroclickfraud adapter * fixes for PR * fix casing of Zeroclickfraud --- adapters/zeroclickfraud/usersync.go | 12 ++ adapters/zeroclickfraud/usersync_test.go | 34 ++++ adapters/zeroclickfraud/zeroclickfraud.go | 187 ++++++++++++++++++ .../zeroclickfraud/zeroclickfraud_test.go | 11 ++ .../exemplary/multi-request.json | 160 +++++++++++++++ .../zeroclickfraudtest/exemplary/native.json | 123 ++++++++++++ .../exemplary/simple-banner.json | 133 +++++++++++++ .../exemplary/simple-video.json | 138 +++++++++++++ .../params/race/banner.json | 4 + .../params/race/native.json | 4 + .../zeroclickfraudtest/params/race/video.json | 4 + .../supplemental/bad-host.json | 33 ++++ .../supplemental/bad-response-body.json | 88 +++++++++ .../supplemental/bad-server-response.json | 88 +++++++++ .../supplemental/bad-sourceId.json | 35 ++++ .../supplemental/missing-ext.json | 27 +++ .../supplemental/missing-extparam.json | 30 +++ .../supplemental/no-content-response.json | 82 ++++++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_zeroclickfraud.go | 7 + static/bidder-info/zeroclickfraud.yaml | 13 ++ static/bidder-params/zeroclickfraud.json | 19 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 26 files changed, 1241 insertions(+) create mode 100644 adapters/zeroclickfraud/usersync.go create mode 100644 adapters/zeroclickfraud/usersync_test.go create mode 100644 adapters/zeroclickfraud/zeroclickfraud.go create mode 100644 adapters/zeroclickfraud/zeroclickfraud_test.go create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/exemplary/multi-request.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/exemplary/native.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-banner.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/exemplary/simple-video.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/params/race/banner.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/params/race/native.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/params/race/video.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-host.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-response-body.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-server-response.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/supplemental/bad-sourceId.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-ext.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/supplemental/missing-extparam.json create mode 100644 adapters/zeroclickfraud/zeroclickfraudtest/supplemental/no-content-response.json create mode 100644 openrtb_ext/imp_zeroclickfraud.go create mode 100644 static/bidder-info/zeroclickfraud.yaml create mode 100644 static/bidder-params/zeroclickfraud.json diff --git a/adapters/zeroclickfraud/usersync.go b/adapters/zeroclickfraud/usersync.go new file mode 100644 index 00000000000..833524e4b3e --- /dev/null +++ b/adapters/zeroclickfraud/usersync.go @@ -0,0 +1,12 @@ +package zeroclickfraud + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/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..30ade771a4c --- /dev/null +++ b/adapters/zeroclickfraud/usersync_test.go @@ -0,0 +1,34 @@ +package zeroclickfraud + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/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..963074bf637 --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraud.go @@ -0,0 +1,187 @@ +package zeroclickfraud + +import ( + "encoding/json" + "fmt" + "github.com/golang/glog" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" + "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..ebe41c19d2e --- /dev/null +++ b/adapters/zeroclickfraud/zeroclickfraud_test.go @@ -0,0 +1,11 @@ +package zeroclickfraud + +import ( + "testing" + + "github.com/prebid/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/config/config.go b/config/config.go index b9501cf6355..953854bf8de 100644 --- a/config/config.go +++ b/config/config.go @@ -534,6 +534,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderVisx, "https://t.visx.net/s2s_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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.BidderYieldmo, "https://ads.yieldmo.com/pbsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldmo%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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) { @@ -726,6 +727,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.yieldmo.endpoint", "https://ads.yieldmo.com/exchange/prebid-server") + v.SetDefault("adapters.zeroclickfraud.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}") v.SetDefault("max_request_size", 1024*256) v.SetDefault("analytics.file.filename", "") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index f0ff9c5b1a7..0354f258158 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -61,6 +61,7 @@ import ( "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" "github.com/prebid/prebid-server/adapters/yieldmo" + "github.com/prebid/prebid-server/adapters/zeroclickfraud" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" ) @@ -130,6 +131,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderYieldmo: yieldmo.NewYieldmoBidder(cfg.Adapters[string(openrtb_ext.BidderYieldmo)].Endpoint), + openrtb_ext.BidderZeroClickFraud: zeroclickfraud.NewZeroClickFraudBidder(cfg.Adapters[string(openrtb_ext.BidderZeroClickFraud)].Endpoint), } legacyBidders := map[openrtb_ext.BidderName]adapters.Adapter{ diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index d1490603b50..ed3d20e06ab 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -74,6 +74,7 @@ const ( BidderVisx BidderName = "visx" BidderVrtcal BidderName = "vrtcal" BidderYieldmo BidderName = "yieldmo" + BidderZeroClickFraud BidderName = "zeroclickfraud" ) // BidderMap stores all the valid OpenRTB 2.x Bidders in the project. This map *must not* be mutated. @@ -132,6 +133,7 @@ var BidderMap = map[string]BidderName{ "visx": BidderVisx, "vrtcal": BidderVrtcal, "yieldmo": BidderYieldmo, + "zeroclickfraud": BidderZeroClickFraud, } // BidderList returns the values of the BidderMap 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/static/bidder-info/zeroclickfraud.yaml b/static/bidder-info/zeroclickfraud.yaml new file mode 100644 index 00000000000..9bf7e780914 --- /dev/null +++ b/static/bidder-info/zeroclickfraud.yaml @@ -0,0 +1,13 @@ +maintainer: + email: "henry@datablocks.net" +capabilities: + app: + mediaTypes: + - banner + - native + - video + site: + mediaTypes: + - banner + - native + - video 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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index eb25171854a..c58d552844d 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -54,6 +54,7 @@ import ( "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" "github.com/prebid/prebid-server/adapters/yieldmo" + "github.com/prebid/prebid-server/adapters/zeroclickfraud" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/usersync" @@ -114,6 +115,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderVisx, visx.NewVisxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderVrtcal, vrtcal.NewVrtcalSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderYieldmo, yieldmo.NewYieldmoSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderZeroClickFraud, zeroclickfraud.NewZeroClickFraudSyncer) return syncers } diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index dc224fe99bf..cc6d4b5870a 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -63,6 +63,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderVisx): syncConfig, string(openrtb_ext.BidderVrtcal): syncConfig, string(openrtb_ext.BidderYieldmo): syncConfig, + string(openrtb_ext.BidderZeroClickFraud): syncConfig, }, } From 8668dfca16538d894355f67d99257a331fdaea45 Mon Sep 17 00:00:00 2001 From: vstatkevich Date: Thu, 12 Mar 2020 20:44:04 +0300 Subject: [PATCH 026/381] Fix Adform's parameters regex (#1214) * Added adform info file * Added Adform adapter and bidder * Updates from master * Removed usersyncInfo from Adform adapter. Inverted Imp type check. * Removed excessive loop * Updated with the last master * Create readme file for adform * Fix Adform's parameters regex Motivation: catastrophic backtracking during regex execution Details: - https://regex101.com/r/NNQrWq/1 - string to check "url_domain:keskustelu.suomi24.fi,url_path:/matkailu/matkakohteet/aasia,layout:lg,categories:Matkailu,main_category:Matkailu" Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich --- static/bidder-params/adform.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From c515816e970fe820cbcf6ce665f67befd3bf7529 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Thu, 12 Mar 2020 11:18:24 -0700 Subject: [PATCH 027/381] If Device.UA is not present in request body, init it with user-agent from header (#1219) * If Device.UA is not present in request body, init it with user-agent from request header if it's present * Moved User-Agent handler to parseVideoRequest func and added unit test * Minor clean up Co-authored-by: Veronika Solovei --- ...o_valid_sample_with_device_user_agent.json | 80 +++++++++++++++++++ ...alid_sample_without_device_user_agent.json | 63 +++++++++++++++ endpoints/openrtb2/video_auction.go | 9 ++- endpoints/openrtb2/video_auction_test.go | 75 +++++++++++++++++ 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json create mode 100644 endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json 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..68c3f4e1c15 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json @@ -0,0 +1,80 @@ + +{ + "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" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "user": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + }, + "gdpr": { + "consentrequired": false, + "consentstring": "something" + }, + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling" + }, + "device": { + "ua": "TestHeaderSample", + "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 + } +} 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..e040a5625ba --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json @@ -0,0 +1,63 @@ + +{ + "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" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "user": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + }, + "gdpr": { + "consentrequired": false, + "consentstring": "something" + }, + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling" + }, + "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 + } +} diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index feb8de193e7..2a8663959a6 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -120,7 +120,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } } //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) return @@ -556,7 +556,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 { @@ -564,6 +564,11 @@ 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 == "" { + req.Device.UA = headers.Get("User-Agent") + } + errL, podErrors := deps.validateVideoRequest(req) if len(errL) > 0 { errs = append(errs, errL...) diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index dfe2a6a50b8..a5ad62c9fa8 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io/ioutil" + "net/http" "net/http/httptest" "strings" "testing" @@ -745,6 +746,80 @@ 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) + req, valErr, podErr := deps.parseVideoRequest(reqData, 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) + req, valErr, podErr := deps.parseVideoRequest(reqData, 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) + req, valErr, podErr := deps.parseVideoRequest(reqData, 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) + req, valErr, podErr := deps.parseVideoRequest(reqData, 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 mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) mockModule := &mockAnalyticsModule{} From f3787be596f55e546f4656d526b1045dced2c614 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Fri, 13 Mar 2020 14:30:34 -0700 Subject: [PATCH 028/381] Queued request timeout (#1217) Co-authored-by: Veronika Solovei --- config/config.go | 10 ++ router/aspects/request_timeout_handler.go | 43 ++++++ .../aspects/request_timeout_handler_test.go | 124 ++++++++++++++++++ router/router.go | 6 + 4 files changed, 183 insertions(+) create mode 100644 router/aspects/request_timeout_handler.go create mode 100644 router/aspects/request_timeout_handler_test.go diff --git a/config/config.go b/config/config.go index 953854bf8de..e3b6c67b651 100644 --- a/config/config.go +++ b/config/config.go @@ -64,6 +64,8 @@ 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"` } const MIN_COOKIE_SIZE_BYTES = 500 @@ -199,6 +201,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 } @@ -748,6 +755,9 @@ 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", "") + // Set environment variable support: v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvPrefix("PBS") diff --git a/router/aspects/request_timeout_handler.go b/router/aspects/request_timeout_handler.go new file mode 100644 index 00000000000..ae11f8c5614 --- /dev/null +++ b/router/aspects/request_timeout_handler.go @@ -0,0 +1,43 @@ +package aspects + +import ( + "github.com/julienschmidt/httprouter" + "github.com/prebid/prebid-server/config" + "net/http" + "strconv" +) + +func QueuedRequestTimeout(f httprouter.Handle, reqTimeoutHeaders config.RequestTimeoutHeaders) 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 + } + + //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")) + return + } + + 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..b6e10fd64bf --- /dev/null +++ b/router/aspects/request_timeout_handler_test.go @@ -0,0 +1,124 @@ +package aspects + +import ( + "github.com/julienschmidt/httprouter" + "github.com/prebid/prebid-server/config" + "net/http" + "net/http/httptest" + "testing" + + "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 + extectedRespCode int + expectedRespCodeMessage string + expectedRespBody string + expectedRespBodyMessage string + }{ + { + //TestQueuedRequestTimeoutWithTimeout + reqTimeInQueue: "6", + reqTimeOut: "5", + setHeaders: true, + extectedRespCode: http.StatusRequestTimeout, + expectedRespCodeMessage: "Http response code is incorrect, should be 408", + expectedRespBody: "Queued request processing time exceeded maximum", + expectedRespBodyMessage: "Body should have error message", + }, + { + //TestQueuedRequestTimeoutNoTimeout + reqTimeInQueue: "0.9", + reqTimeOut: "5", + setHeaders: true, + extectedRespCode: http.StatusOK, + expectedRespCodeMessage: "Http response code is incorrect, should be 200", + expectedRespBody: "Executed", + expectedRespBodyMessage: "Body should be present in response", + }, + { + //TestQueuedRequestNoHeaders + reqTimeInQueue: "", + reqTimeOut: "", + setHeaders: false, + extectedRespCode: http.StatusOK, + expectedRespCodeMessage: "Http response code is incorrect, should be 200", + expectedRespBody: "Executed", + expectedRespBodyMessage: "Body should be present in response", + }, + { + //TestQueuedRequestSomeHeaders + reqTimeInQueue: "2", + reqTimeOut: "", + setHeaders: true, + extectedRespCode: http.StatusOK, + expectedRespCodeMessage: "Http response code is incorrect, should be 200", + expectedRespBody: "Executed", + expectedRespBodyMessage: "Body should be present in response", + }, + { + //TestQueuedRequestAllHeadersIncorrect + reqTimeInQueue: "test1", + reqTimeOut: "test2", + setHeaders: true, + extectedRespCode: http.StatusInternalServerError, + expectedRespCodeMessage: "Http response code is incorrect, should be 400", + expectedRespBody: "Request timeout headers are incorrect (wrong format)", + expectedRespBodyMessage: "Body should have error message", + }, + { + //TestQueuedRequestSomeHeadersIncorrect + reqTimeInQueue: "test1", + reqTimeOut: "123", + setHeaders: true, + extectedRespCode: http.StatusInternalServerError, + expectedRespCodeMessage: "Http response code is incorrect, should be 400", + expectedRespBody: "Request timeout headers are incorrect (wrong format)", + expectedRespBodyMessage: "Body should have error message", + }, + } + + for _, test := range testCases { + result := ExecuteAspectRequest(t, test.reqTimeInQueue, test.reqTimeOut, test.setHeaders) + assert.Equal(t, test.extectedRespCode, 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) *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} + + handler := QueuedRequestTimeout(MockEndpoint(), customHeaders) + + r := httprouter.New() + r.POST("/test", handler) + + r.ServeHTTP(rw, req) + + return rw +} diff --git a/router/router.go b/router/router.go index 449ab65a448..7e713ca637a 100644 --- a/router/router.go +++ b/router/router.go @@ -38,6 +38,7 @@ import ( "github.com/prebid/prebid-server/pbs" metricsConf "github.com/prebid/prebid-server/pbsmetrics/config" pbc "github.com/prebid/prebid-server/prebid_cache_client" + "github.com/prebid/prebid-server/router/aspects" "github.com/prebid/prebid-server/ssl" storedRequestsConf "github.com/prebid/prebid-server/stored_requests/config" "github.com/prebid/prebid-server/usersync/usersyncers" @@ -255,6 +256,11 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r 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.POST("/auction", endpoints.Auction(cfg, syncers, gdprPerms, r.MetricsEngine, dataCache, exchanges)) r.POST("/openrtb2/auction", openrtbEndpoint) r.POST("/openrtb2/video", videoEndpoint) From e94ca8b7e635d599a6e30fb73cc776d704afbf24 Mon Sep 17 00:00:00 2001 From: bretg Date: Mon, 16 Mar 2020 12:53:09 -0400 Subject: [PATCH 029/381] docs: adding currency support section (#1199) --- docs/endpoints/openrtb2/auction.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 9ae6ec78bee..d670b092174 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -403,6 +403,29 @@ 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. + #### 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: From 2bad06903f480e6117a7905f916d59f3aadafab8 Mon Sep 17 00:00:00 2001 From: thuyhq <61451682+thuyhq@users.noreply.github.com> Date: Mon, 16 Mar 2020 23:54:59 +0700 Subject: [PATCH 030/381] Add ValueImpression Adapter (#1204) --- adapters/valueimpression/params_test.go | 52 ++++++ adapters/valueimpression/usersync.go | 12 ++ adapters/valueimpression/usersync_test.go | 35 ++++ adapters/valueimpression/valueimpression.go | 154 ++++++++++++++++++ .../valueimpression/valueimpression_test.go | 11 ++ .../exemplary/banner-and-video.json | 150 +++++++++++++++++ .../valueimpressiontest/exemplary/banner.json | 98 +++++++++++ .../valueimpressiontest/exemplary/video.json | 53 ++++++ .../supplemental/explicit-dimensions.json | 56 +++++++ .../invalid-response-no-bids.json | 50 ++++++ .../invalid-response-unmarshall-error.json | 66 ++++++++ .../supplemental/no-imps-in-request.json | 18 ++ .../supplemental/server-error-code.json | 53 ++++++ .../supplemental/server-no-content.json | 45 +++++ .../supplemental/wrong-impression-ext.json | 26 +++ .../wrong-impression-mapping.json | 75 +++++++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_valueimpression.go | 5 + static/bidder-info/valueimpression.yaml | 11 ++ static/bidder-params/valueimpression.json | 15 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 24 files changed, 994 insertions(+) create mode 100644 adapters/valueimpression/params_test.go create mode 100644 adapters/valueimpression/usersync.go create mode 100644 adapters/valueimpression/usersync_test.go create mode 100644 adapters/valueimpression/valueimpression.go create mode 100644 adapters/valueimpression/valueimpression_test.go create mode 100644 adapters/valueimpression/valueimpressiontest/exemplary/banner-and-video.json create mode 100644 adapters/valueimpression/valueimpressiontest/exemplary/banner.json create mode 100644 adapters/valueimpression/valueimpressiontest/exemplary/video.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/explicit-dimensions.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-no-bids.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-unmarshall-error.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/no-imps-in-request.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/server-error-code.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/server-no-content.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-ext.json create mode 100644 adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-mapping.json create mode 100644 openrtb_ext/imp_valueimpression.go create mode 100644 static/bidder-info/valueimpression.yaml create mode 100644 static/bidder-params/valueimpression.json diff --git a/adapters/valueimpression/params_test.go b/adapters/valueimpression/params_test.go new file mode 100644 index 00000000000..46471de24bb --- /dev/null +++ b/adapters/valueimpression/params_test.go @@ -0,0 +1,52 @@ +package valueimpression + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/valueimpression.json +// These also validate the format of the external API: request.imp[i].ext.valueimpression +// TestValidParams makes sure that the ValueImpression 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.BidderValueImpression, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected ValueImpression params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the ValueImpression 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.BidderValueImpression, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"siteId": "123"}`, +} + +var invalidParams = []string{ + `{}`, + `null`, + `true`, + `154`, + `{"siteId": 123}`, // siteId should be string + `{"invalid_param": "123"}`, +} diff --git a/adapters/valueimpression/usersync.go b/adapters/valueimpression/usersync.go new file mode 100644 index 00000000000..34addbc0e75 --- /dev/null +++ b/adapters/valueimpression/usersync.go @@ -0,0 +1,12 @@ +package valueimpression + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewValueImpressionSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("valueimpression", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/valueimpression/usersync_test.go b/adapters/valueimpression/usersync_test.go new file mode 100644 index 00000000000..63f123055a9 --- /dev/null +++ b/adapters/valueimpression/usersync_test.go @@ -0,0 +1,35 @@ +package valueimpression + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestValueImpressionSyncer(t *testing.T) { + syncURL := "https://rtb.valueimpression.com/usersync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirectUri=http%3A%2F%2Flocalhost:8000%2Fsetuid%3Fbidder%3Dvalueimpression%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewValueImpressionSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", + }, + CCPA: ccpa.Policy{ + Value: "1NYN", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://rtb.valueimpression.com/usersync?gdpr=1&gdpr_consent=BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA&redirectUri=http%3A%2F%2Flocalhost:8000%2Fsetuid%3Fbidder%3Dvalueimpression%26gdpr%3D1%26gdpr_consent%3DBOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA%26uid%3D%24UID", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.False(t, syncInfo.SupportCORS) +} diff --git a/adapters/valueimpression/valueimpression.go b/adapters/valueimpression/valueimpression.go new file mode 100644 index 00000000000..7e0f5f28cb9 --- /dev/null +++ b/adapters/valueimpression/valueimpression.go @@ -0,0 +1,154 @@ +package valueimpression + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type ValueImpressionAdapter struct { + endpoint string +} + +func (a *ValueImpressionAdapter) MakeRequests(request *openrtb.BidRequest, unused *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var adapterRequests []*adapters.RequestData + + if err := preprocess(request); err != nil { + errs = append(errs, err) + return nil, errs + } + + adapterReq, err := a.makeRequest(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + adapterRequests = append(adapterRequests, adapterReq) + + return adapterRequests, errs +} + +func (a *ValueImpressionAdapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, error) { + var err error + + jsonBody, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: jsonBody, + Headers: headers, + }, nil +} + +func preprocess(request *openrtb.BidRequest) error { + if len(request.Imp) == 0 { + return &errortypes.BadInput{ + Message: "No Imps in Bid Request", + } + } + for i := 0; i < len(request.Imp); i++ { + var imp = &request.Imp[i] + var bidderExt adapters.ExtImpBidder + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + var extImp openrtb_ext.ExtImpValueImpression + if err := json.Unmarshal(bidderExt.Bidder, &extImp); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + imp.Ext = bidderExt.Bidder + } + + return nil +} + +// MakeBids based on valueimpression server response +func (a *ValueImpressionAdapter) MakeBids(bidRequest *openrtb.BidRequest, unused *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if responseData.StatusCode == http.StatusNoContent { + return nil, nil + } + + if responseData.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Bad user input: HTTP status %d", responseData.StatusCode), + }} + } + + if responseData.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bad server response: HTTP status %d", responseData.StatusCode), + }} + } + + var bidResponse openrtb.BidResponse + + if err := json.Unmarshal(responseData.Body, &bidResponse); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: err.Error(), + }} + } + + if len(bidResponse.SeatBid) == 0 { + return nil, nil + } + + rv := adapters.NewBidderResponseWithBidsCapacity(len(bidResponse.SeatBid[0].Bid)) + var errors []error + + for _, seatbid := range bidResponse.SeatBid { + for _, bid := range seatbid.Bid { + foundMatchingBid := false + bidType := openrtb_ext.BidTypeBanner + for _, imp := range bidRequest.Imp { + if imp.ID == bid.ImpID { + foundMatchingBid = true + if imp.Banner != nil { + bidType = openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + bidType = openrtb_ext.BidTypeVideo + } + break + } + } + + if foundMatchingBid { + rv.Bids = append(rv.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + }) + } else { + errors = append(errors, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("bid id='%s' could not find valid impid='%s'", bid.ID, bid.ImpID), + }) + } + } + } + return rv, errors +} + +func NewValueImpressionBidder(endpoint string) *ValueImpressionAdapter { + return &ValueImpressionAdapter{ + endpoint: endpoint, + } +} diff --git a/adapters/valueimpression/valueimpression_test.go b/adapters/valueimpression/valueimpression_test.go new file mode 100644 index 00000000000..047521cea41 --- /dev/null +++ b/adapters/valueimpression/valueimpression_test.go @@ -0,0 +1,11 @@ +package valueimpression + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "valueimpressiontest", NewValueImpressionBidder("//host")) +} diff --git a/adapters/valueimpression/valueimpressiontest/exemplary/banner-and-video.json b/adapters/valueimpression/valueimpressiontest/exemplary/banner-and-video.json new file mode 100644 index 00000000000..107c0d84221 --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/exemplary/banner-and-video.json @@ -0,0 +1,150 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + }, + { + "id": "test-video-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ], + "site": { + "id": "123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "siteId": "123" + } + }, + { + "id": "test-video-imp-id", + "video": { + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 640 + }, + "ext": { + "siteId": "123" + } + } + ], + "site": { + "id": "123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "valueimpression", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-video-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "sample.com" + ], + "cid": "958", + "crid": "29681110", + "w": 1024, + "h": 576 + }, + "type": "banner" + }, + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-video-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29484110", + "adomain": [ + "sample.com" + ], + "cid": "958", + "crid": "29484110", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] +} \ No newline at end of file diff --git a/adapters/valueimpression/valueimpressiontest/exemplary/banner.json b/adapters/valueimpression/valueimpressiontest/exemplary/banner.json new file mode 100644 index 00000000000..1ef11ade199 --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/exemplary/banner.json @@ -0,0 +1,98 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ], + "site": { + "id": "fake-site-id" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "siteId": "123" + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "valueimpression", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-banner-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-banner-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} + \ No newline at end of file diff --git a/adapters/valueimpression/valueimpressiontest/exemplary/video.json b/adapters/valueimpression/valueimpressiontest/exemplary/video.json new file mode 100644 index 00000000000..c6e71e7a16f --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/exemplary/video.json @@ -0,0 +1,53 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-video-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-video-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "siteId": "123" + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/explicit-dimensions.json b/adapters/valueimpression/valueimpressiontest/supplemental/explicit-dimensions.json new file mode 100644 index 00000000000..ee23350c9dc --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/explicit-dimensions.json @@ -0,0 +1,56 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "siteId": "123" + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} \ No newline at end of file diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-no-bids.json b/adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-no-bids.json new file mode 100644 index 00000000000..114b27bae07 --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-no-bids.json @@ -0,0 +1,50 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "w": 90, + "h": 728 + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "h": 728, + "w": 90 + }, + "ext": { + "siteId": "123" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some_test_auction", + "seatbid": [ + ], + "cur": "USD" + } + } + } + ] +} \ No newline at end of file diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-unmarshall-error.json b/adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-unmarshall-error.json new file mode 100644 index 00000000000..c854548b78b --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/invalid-response-unmarshall-error.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "w": 90, + "h": 728 + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "some_test_ad", + "banner": { + "h": 728, + "w": 90 + }, + "ext": { + "siteId": "123" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some_test_auction", + "seatbid": [ + { + "bid": [ + { + "id": "uuid", + "impid": "some_test_ad", + "w": "728", + "h": 90 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go struct field Bid(\\.seatbid\\.bid)?\\.w of type uint64", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/no-imps-in-request.json b/adapters/valueimpression/valueimpressiontest/supplemental/no-imps-in-request.json new file mode 100644 index 00000000000..274a34227cf --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/no-imps-in-request.json @@ -0,0 +1,18 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site?with=some¶meters=here" + } + }, + + "expectedMakeRequestsErrors": [ + { + "value": "No Imps in Bid Request", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/server-error-code.json b/adapters/valueimpression/valueimpressiontest/supplemental/server-error-code.json new file mode 100644 index 00000000000..ea31fdc2fe9 --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/server-error-code.json @@ -0,0 +1,53 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 600, + "h": 300 + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 600, + "h": 300 + }, + "ext": { + "siteId": "123" + } + } + ] + } + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Bad server response: HTTP status 500", + "comparison": "literal" + } + ] + } diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/server-no-content.json b/adapters/valueimpression/valueimpressiontest/supplemental/server-no-content.json new file mode 100644 index 00000000000..85633201bc4 --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/server-no-content.json @@ -0,0 +1,45 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "some_test_auction", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "siteId": "123" + } + } + ] + } + }, + "mockResponse": { + "status": 204 + } + } + ] + } diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-ext.json b/adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-ext.json new file mode 100644 index 00000000000..13514ac8ab8 --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-ext.json @@ -0,0 +1,26 @@ +{ + "mockBidRequest": { + "id": "rqid", + "imp": [ + { + "id": "impid", + "video": { + "w": 100, + "h": 200 + }, + "ext": { + "bidder": { + "siteId": 123 + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal number into Go struct field ExtImpValueImpression.siteId of type string", + "comparison": "literal" + } + ] +} diff --git a/adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-mapping.json b/adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..ef4d3a7526b --- /dev/null +++ b/adapters/valueimpression/valueimpressiontest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,75 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "siteId": "123" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "//host", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "siteId": "123" + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "BOGUS-IMPID", + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "bid id='test-bid-id' could not find valid impid='BOGUS-IMPID'", + "comparison": "regex" + } +] +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index e3b6c67b651..2069730b692 100644 --- a/config/config.go +++ b/config/config.go @@ -538,6 +538,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderTripleliftNative, "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ducfunnel%26uid%3DSspCookieUserId") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderUnruly, "https://usermatch.targeting.unrulymedia.com/pbsync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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.BidderYieldmo, "https://ads.yieldmo.com/pbsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldmo%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -730,6 +731,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.triplelift.endpoint", "https://tlx.3lift.com/s2s/auction?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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 0354f258158..7b841a2838e 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -57,6 +57,7 @@ import ( "github.com/prebid/prebid-server/adapters/triplelift_native" "github.com/prebid/prebid-server/adapters/ucfunnel" "github.com/prebid/prebid-server/adapters/unruly" + "github.com/prebid/prebid-server/adapters/valueimpression" "github.com/prebid/prebid-server/adapters/verizonmedia" "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" @@ -127,6 +128,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index ed3d20e06ab..627842f57ff 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -70,6 +70,7 @@ const ( BidderTripleliftNative BidderName = "triplelift_native" BidderUcfunnel BidderName = "ucfunnel" BidderUnruly BidderName = "unruly" + BidderValueImpression BidderName = "valueimpression" BidderVerizonMedia BidderName = "verizonmedia" BidderVisx BidderName = "visx" BidderVrtcal BidderName = "vrtcal" @@ -129,6 +130,7 @@ var BidderMap = map[string]BidderName{ "triplelift_native": BidderTripleliftNative, "ucfunnel": BidderUcfunnel, "unruly": BidderUnruly, + "valueimpression": BidderValueImpression, "verizonmedia": BidderVerizonMedia, "visx": BidderVisx, "vrtcal": BidderVrtcal, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index c58d552844d..c7ad70b7eff 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -50,6 +50,7 @@ import ( "github.com/prebid/prebid-server/adapters/triplelift_native" "github.com/prebid/prebid-server/adapters/ucfunnel" "github.com/prebid/prebid-server/adapters/unruly" + "github.com/prebid/prebid-server/adapters/valueimpression" "github.com/prebid/prebid-server/adapters/verizonmedia" "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" @@ -111,6 +112,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index cc6d4b5870a..7aef9fa8b5a 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -59,6 +59,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderTripleliftNative): syncConfig, string(openrtb_ext.BidderUcfunnel): syncConfig, string(openrtb_ext.BidderUnruly): syncConfig, + string(openrtb_ext.BidderValueImpression): syncConfig, string(openrtb_ext.BidderVerizonMedia): syncConfig, string(openrtb_ext.BidderVisx): syncConfig, string(openrtb_ext.BidderVrtcal): syncConfig, From 95c269f3740c4c0e1bf82b481ca78f52e634cb17 Mon Sep 17 00:00:00 2001 From: rhaksi-kidoz <61601767+rhaksi-kidoz@users.noreply.github.com> Date: Tue, 17 Mar 2020 22:27:52 -0700 Subject: [PATCH 031/381] Kidoz adapter (#1210) Co-authored-by: Ryan Haksi --- .gitignore | 4 + adapters/kidoz/kidoz.go | 188 ++++++++++++++++++ adapters/kidoz/kidoz_test.go | 113 +++++++++++ .../kidoztest/exemplary/simple-banner.json | 71 +++++++ .../kidoztest/exemplary/simple-video.json | 69 +++++++ .../kidoz/kidoztest/supplemental/bad-bid.json | 96 +++++++++ .../supplemental/bidder-marshal.json | 30 +++ .../supplemental/empty-banner-format .json | 19 ++ .../kidoztest/supplemental/ext-marshal.json | 28 +++ .../supplemental/missing-banner-format.json | 18 ++ .../supplemental/missing-bidder.json | 25 +++ .../kidoztest/supplemental/missing-ext.json | 24 +++ .../supplemental/missing-kidoz-info.json | 52 +++++ .../supplemental/only-video-banner.json | 27 +++ .../kidoztest/supplemental/status-204.json | 57 ++++++ .../kidoztest/supplemental/status-400.json | 63 ++++++ .../kidoztest/supplemental/status-403.json | 63 ++++++ .../kidoztest/supplemental/status-408.json | 63 ++++++ .../kidoztest/supplemental/status-500.json | 63 ++++++ .../kidoztest/supplemental/status-502.json | 63 ++++++ .../kidoztest/supplemental/status-503.json | 58 ++++++ .../kidoztest/supplemental/status-504.json | 63 ++++++ adapters/kidoz/params_test.go | 79 ++++++++ analytics/config/testFiles/test-20200303 | 0 config/config.go | 1 + exchange/adapter_map.go | 5 +- go.mod | 1 + go.sum | 2 + openrtb_ext/bid.go | 6 +- openrtb_ext/bidders.go | 2 + openrtb_ext/imp_kidoz.go | 6 + static/bidder-info/kidoz.yaml | 11 + static/bidder-params/kidoz.json | 26 +++ usersync/usersyncers/syncer_test.go | 1 + validate.sh | 4 +- 35 files changed, 1394 insertions(+), 7 deletions(-) create mode 100644 adapters/kidoz/kidoz.go create mode 100644 adapters/kidoz/kidoz_test.go create mode 100644 adapters/kidoz/kidoztest/exemplary/simple-banner.json create mode 100644 adapters/kidoz/kidoztest/exemplary/simple-video.json create mode 100644 adapters/kidoz/kidoztest/supplemental/bad-bid.json create mode 100644 adapters/kidoz/kidoztest/supplemental/bidder-marshal.json create mode 100644 adapters/kidoz/kidoztest/supplemental/empty-banner-format .json create mode 100644 adapters/kidoz/kidoztest/supplemental/ext-marshal.json create mode 100644 adapters/kidoz/kidoztest/supplemental/missing-banner-format.json create mode 100644 adapters/kidoz/kidoztest/supplemental/missing-bidder.json create mode 100644 adapters/kidoz/kidoztest/supplemental/missing-ext.json create mode 100644 adapters/kidoz/kidoztest/supplemental/missing-kidoz-info.json create mode 100644 adapters/kidoz/kidoztest/supplemental/only-video-banner.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-204.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-400.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-403.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-408.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-500.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-502.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-503.json create mode 100644 adapters/kidoz/kidoztest/supplemental/status-504.json create mode 100644 adapters/kidoz/params_test.go create mode 100644 analytics/config/testFiles/test-20200303 create mode 100644 openrtb_ext/imp_kidoz.go create mode 100644 static/bidder-info/kidoz.yaml create mode 100644 static/bidder-params/kidoz.json 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/adapters/kidoz/kidoz.go b/adapters/kidoz/kidoz.go new file mode 100644 index 00000000000..2d04cdffd39 --- /dev/null +++ b/adapters/kidoz/kidoz.go @@ -0,0 +1,188 @@ +package kidoz + +import ( + "encoding/json" + "errors" + "net/http" + "strconv" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type KidozAdapter struct { + endpoint string +} + +func (a *KidozAdapter) MakeRequests(request *openrtb.BidRequest, _ *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("x-openrtb-version", "2.5") + + impressions := request.Imp + result := make([]*adapters.RequestData, 0, len(impressions)) + errs := make([]error, 0, len(impressions)) + + for i, impression := range impressions { + if impression.Banner == nil && impression.Video == nil { + errs = append(errs, &errortypes.BadInput{ + Message: "Kidoz only supports banner or video ads", + }) + continue + } + + if impression.Banner != nil { + banner := impression.Banner + if banner.Format == nil { + errs = append(errs, &errortypes.BadInput{ + Message: "banner format required", + }) + continue + } + if len(banner.Format) == 0 { + errs = append(errs, &errortypes.BadInput{ + Message: "banner format array is empty", + }) + continue + } + } + + if len(impression.Ext) == 0 { + errs = append(errs, errors.New("impression extensions required")) + continue + } + var bidderExt adapters.ExtImpBidder + err := json.Unmarshal(impression.Ext, &bidderExt) + if err != nil { + errs = append(errs, err) + continue + } + if len(bidderExt.Bidder) == 0 { + errs = append(errs, errors.New("bidder required")) + continue + } + var impressionExt openrtb_ext.ExtImpKidoz + err = json.Unmarshal(bidderExt.Bidder, &impressionExt) + if err != nil { + errs = append(errs, err) + continue + } + if impressionExt.AccessToken == "" { + errs = append(errs, errors.New("Kidoz access_token required")) + continue + } + if impressionExt.PublisherID == "" { + errs = append(errs, errors.New("Kidoz publisher_id required")) + continue + } + + request.Imp = impressions[i : i+1] + body, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + continue + } + result = append(result, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: body, + Headers: headers, + }) + } + + request.Imp = impressions + + if len(result) == 0 { + return nil, errs + } + return result, errs +} + +func (a *KidozAdapter) MakeBids(request *openrtb.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + switch responseData.StatusCode { + case http.StatusNoContent: + fallthrough + case http.StatusServiceUnavailable: + return nil, nil + + case http.StatusBadRequest: + fallthrough + case http.StatusUnauthorized: + fallthrough + case http.StatusForbidden: + return nil, []error{&errortypes.BadInput{ + Message: "unexpected status code: " + strconv.Itoa(responseData.StatusCode) + " " + string(responseData.Body), + }} + + case http.StatusOK: + break + + default: + return nil, []error{&errortypes.BadServerResponse{ + Message: "unexpected status code: " + strconv.Itoa(responseData.StatusCode) + " " + string(responseData.Body), + }} + } + + var bidResponse openrtb.BidResponse + err := json.Unmarshal(responseData.Body, &bidResponse) + if err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: err.Error(), + }} + } + + response := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + + for _, seatBid := range bidResponse.SeatBid { + for _, bid := range seatBid.Bid { + thisBid := bid + bidType := GetMediaTypeForImp(bid.ImpID, request.Imp) + if bidType == UndefinedMediaType { + errs = append(errs, &errortypes.BadServerResponse{ + Message: "ignoring bid id=" + bid.ID + ", request doesn't contain any valid impression with id=" + bid.ImpID, + }) + continue + } + response.Bids = append(response.Bids, &adapters.TypedBid{ + Bid: &thisBid, + BidType: bidType, + }) + } + } + + return response, errs +} + +func NewKidozBidder(endpoint string) *KidozAdapter { + return &KidozAdapter{ + endpoint: endpoint, + } +} + +const UndefinedMediaType = openrtb_ext.BidType("") + +func GetMediaTypeForImp(impID string, imps []openrtb.Imp) openrtb_ext.BidType { + var bidType openrtb_ext.BidType = UndefinedMediaType + for _, impression := range imps { + if impression.ID != impID { + continue + } + switch { + case impression.Banner != nil: + bidType = openrtb_ext.BidTypeBanner + case impression.Video != nil: + bidType = openrtb_ext.BidTypeVideo + case impression.Native != nil: + bidType = openrtb_ext.BidTypeNative + case impression.Audio != nil: + bidType = openrtb_ext.BidTypeAudio + } + break + } + return bidType +} diff --git a/adapters/kidoz/kidoz_test.go b/adapters/kidoz/kidoz_test.go new file mode 100644 index 00000000000..55036c08614 --- /dev/null +++ b/adapters/kidoz/kidoz_test.go @@ -0,0 +1,113 @@ +package kidoz + +import ( + "math" + "net/http" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "kidoztest", NewKidozBidder("http://example.com/prebid")) +} + +func makeBidRequest() *openrtb.BidRequest { + request := &openrtb.BidRequest{ + ID: "test-req-id-0", + Imp: []openrtb.Imp{ + { + ID: "test-imp-id-0", + Banner: &openrtb.Banner{ + Format: []openrtb.Format{ + { + W: 320, + H: 50, + }, + }, + }, + Ext: []byte(`{"bidder":{"access_token":"token-0","publisher_id":"pub-0"}}`), + }, + }, + } + return request +} + +func TestMakeRequests(t *testing.T) { + kidoz := NewKidozBidder("http://example.com/prebid") + + t.Run("Handles Request marshal failure", func(t *testing.T) { + request := makeBidRequest() + request.Imp[0].BidFloor = math.Inf(1) // cant be marshalled + extra := &adapters.ExtraRequestInfo{} + reqs, errs := kidoz.MakeRequests(request, extra) + // cant assert message its different on different versions of go + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "json") + assert.Equal(t, 0, len(reqs)) + }) +} + +func TestMakeBids(t *testing.T) { + kidoz := NewKidozBidder("http://example.com/prebid") + + t.Run("Handles response marshal failure", func(t *testing.T) { + request := makeBidRequest() + requestData := &adapters.RequestData{} + responseData := &adapters.ResponseData{ + StatusCode: http.StatusOK, + } + + resp, errs := kidoz.MakeBids(request, requestData, responseData) + // cant assert message its different on different versions of go + assert.Equal(t, 1, len(errs)) + assert.Contains(t, errs[0].Error(), "JSON") + assert.Nil(t, resp) + }) +} + +func TestGetMediaTypeForImp(t *testing.T) { + imps := []openrtb.Imp{ + { + ID: "1", + Banner: &openrtb.Banner{}, + }, + { + ID: "2", + Video: &openrtb.Video{}, + }, + { + ID: "3", + Native: &openrtb.Native{}, + }, + { + ID: "4", + Audio: &openrtb.Audio{}, + }, + } + + t.Run("Bid not found is type empty string", func(t *testing.T) { + actual := GetMediaTypeForImp("ARGLE_BARGLE", imps) + assert.Equal(t, UndefinedMediaType, actual) + }) + t.Run("Can find banner type", func(t *testing.T) { + actual := GetMediaTypeForImp("1", imps) + assert.Equal(t, openrtb_ext.BidTypeBanner, actual) + }) + t.Run("Can find video type", func(t *testing.T) { + actual := GetMediaTypeForImp("2", imps) + assert.Equal(t, openrtb_ext.BidTypeVideo, actual) + }) + t.Run("Can find native type", func(t *testing.T) { + actual := GetMediaTypeForImp("3", imps) + assert.Equal(t, openrtb_ext.BidTypeNative, actual) + }) + t.Run("Can find audio type", func(t *testing.T) { + actual := GetMediaTypeForImp("4", imps) + assert.Equal(t, openrtb_ext.BidTypeAudio, actual) + }) +} diff --git a/adapters/kidoz/kidoztest/exemplary/simple-banner.json b/adapters/kidoz/kidoztest/exemplary/simple-banner.json new file mode 100644 index 00000000000..c44fdba7aeb --- /dev/null +++ b/adapters/kidoz/kidoztest/exemplary/simple-banner.json @@ -0,0 +1,71 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-response-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id-1", + "impid": "test-impression-id-1", + "price": 1 + } + ], + "seat": "kidoz" + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/exemplary/simple-video.json b/adapters/kidoz/kidoztest/exemplary/simple-video.json new file mode 100644 index 00000000000..3b682078cbe --- /dev/null +++ b/adapters/kidoz/kidoztest/exemplary/simple-video.json @@ -0,0 +1,69 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-response-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id-1", + "impid": "test-impression-id-1", + "price": 1 + } + ], + "seat": "kidoz" + } + ] + } + } + } + ] +} diff --git a/adapters/kidoz/kidoztest/supplemental/bad-bid.json b/adapters/kidoz/kidoztest/supplemental/bad-bid.json new file mode 100644 index 00000000000..32b8ec2cf06 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/bad-bid.json @@ -0,0 +1,96 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-0", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-0", + "publisher_id": "test-publisher-0" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-0", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-0", + "publisher_id": "test-publisher-0" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-response-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id-0", + "impid": "test-impression-id-0", + "price": 10 + }, + { + "id": "test-bid-id-bogus", + "impid": "test-impression-id-bogus", + "price": 11 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test-bid-id-0", + "impid": "test-impression-id-0", + "price": 10 + }, + "type": "banner" + } + ] + } + ], + "expectedMakeRequestsErrors": [], + "expectedMakeBidsErrors": [ + { + "value": "ignoring bid id=test-bid-id-bogus, request doesn't contain any valid impression with id=test-impression-id-bogus", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/bidder-marshal.json b/adapters/kidoz/kidoztest/supplemental/bidder-marshal.json new file mode 100644 index 00000000000..8a8a5e76844 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/bidder-marshal.json @@ -0,0 +1,30 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-7", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": "invalid bidder" + } + } + ] + }, + "httpCalls": [], + "expectedBidResponses": [], + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb_ext.ExtImpKidoz", + "comparison": "literal" + } + ], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/empty-banner-format .json b/adapters/kidoz/kidoztest/supplemental/empty-banner-format .json new file mode 100644 index 00000000000..18b62a4e1f4 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/empty-banner-format .json @@ -0,0 +1,19 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [] + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "banner format array is empty", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/ext-marshal.json b/adapters/kidoz/kidoztest/supplemental/ext-marshal.json new file mode 100644 index 00000000000..eaab459461a --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/ext-marshal.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-7", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": "invalid ext" + } + ] + }, + "httpCalls": [], + "expectedBidResponses": [], + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/missing-banner-format.json b/adapters/kidoz/kidoztest/supplemental/missing-banner-format.json new file mode 100644 index 00000000000..3fdb5443c78 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/missing-banner-format.json @@ -0,0 +1,18 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "banner format required", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/missing-bidder.json b/adapters/kidoz/kidoztest/supplemental/missing-bidder.json new file mode 100644 index 00000000000..06ab222e322 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/missing-bidder.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": {} + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "bidder required", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/missing-ext.json b/adapters/kidoz/kidoztest/supplemental/missing-ext.json new file mode 100644 index 00000000000..8424a0b173a --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/missing-ext.json @@ -0,0 +1,24 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "impression extensions required", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/missing-kidoz-info.json b/adapters/kidoz/kidoztest/supplemental/missing-kidoz-info.json new file mode 100644 index 00000000000..bfe67aa7cea --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/missing-kidoz-info.json @@ -0,0 +1,52 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-5", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-5" + } + } + }, + { + "id": "test-impression-id-6", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "publisher_id": "test-publisher-6" + } + } + } + ] + }, + "httpCalls": [], + "expectedBidResponses": [], + "expectedMakeRequestsErrors": [ + { + "value": "Kidoz publisher_id required", + "comparison": "literal" + }, + { + "value": "Kidoz access_token required", + "comparison": "literal" + } + ], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/only-video-banner.json b/adapters/kidoz/kidoztest/supplemental/only-video-banner.json new file mode 100644 index 00000000000..6e87e80806c --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/only-video-banner.json @@ -0,0 +1,27 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-0", + "audio": { + } + }, + { + "id": "test-impression-id-1", + "native": { + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Kidoz only supports banner or video ads", + "comparison": "literal" + }, + { + "value": "Kidoz only supports banner or video ads", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-204.json b/adapters/kidoz/kidoztest/supplemental/status-204.json new file mode 100644 index 00000000000..0bff102259a --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-204.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-400.json b/adapters/kidoz/kidoztest/supplemental/status-400.json new file mode 100644 index 00000000000..ca42aefdca0 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-400.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": "server text here" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 400 \"server text here\"", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-403.json b/adapters/kidoz/kidoztest/supplemental/status-403.json new file mode 100644 index 00000000000..3b6d268ecfe --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-403.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 403, + "body": "server text here" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 403 \"server text here\"", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-408.json b/adapters/kidoz/kidoztest/supplemental/status-408.json new file mode 100644 index 00000000000..8230967f3a8 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-408.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 408, + "body": "server text here" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 408 \"server text here\"", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-500.json b/adapters/kidoz/kidoztest/supplemental/status-500.json new file mode 100644 index 00000000000..f734e6913a7 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-500.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 500, + "body": "server text here" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 500 \"server text here\"", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-502.json b/adapters/kidoz/kidoztest/supplemental/status-502.json new file mode 100644 index 00000000000..b99f52a2e42 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-502.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 502, + "body": "server text here" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 502 \"server text here\"", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-503.json b/adapters/kidoz/kidoztest/supplemental/status-503.json new file mode 100644 index 00000000000..f823372915c --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-503.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 503, + "body": "server text here" + } + } + ], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/kidoz/kidoztest/supplemental/status-504.json b/adapters/kidoz/kidoztest/supplemental/status-504.json new file mode 100644 index 00000000000..b996611eb97 --- /dev/null +++ b/adapters/kidoz/kidoztest/supplemental/status-504.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-id-1", + "banner": { + "format": [ + { + "h": 250, + "w": 300 + } + ] + }, + "ext": { + "bidder": { + "access_token": "test-token-1", + "publisher_id": "test-publisher-1" + } + } + } + ] + } + }, + "mockResponse": { + "status": 504, + "body": "server text here" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 504 \"server text here\"", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kidoz/params_test.go b/adapters/kidoz/params_test.go new file mode 100644 index 00000000000..073d7382d68 --- /dev/null +++ b/adapters/kidoz/params_test.go @@ -0,0 +1,79 @@ +package kidoz + +import ( + "encoding/json" + "testing" + + "github.com/prebid/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.BidderKidoz, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected kidoz params: %s \n Error: %s", validParam, err) + } + } +} + +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.BidderKidoz, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"publisher_id":"pub-valid-0", "access_token":"token-valid-0"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"some_random_field":""}`, + `{"publisher_id":""}`, + `{"publisher_id": 1}`, + `{"publisher_id": 1.2}`, + `{"publisher_id": null}`, + `{"publisher_id": true}`, + `{"publisher_id": []}`, + `{"publisher_id": {}}`, + `{"publisher_id":"", "access_token":"token-valid-0"}`, + `{"publisher_id": 1, "access_token":"token-valid-0"}`, + `{"publisher_id": 1.2, "access_token":"token-valid-0"}`, + `{"publisher_id": null, "access_token":"token-valid-0"}`, + `{"publisher_id": true, "access_token":"token-valid-0"}`, + `{"publisher_id": [], "access_token":"token-valid-0"}`, + `{"publisher_id": {}, "access_token":"token-valid-0"}`, + `{"access_token":""}`, + `{"access_token": 1}`, + `{"access_token": 1.2}`, + `{"access_token": null}`, + `{"access_token": true}`, + `{"access_token": []}`, + `{"access_token": {}}`, + `{"access_token":"", "publisher_id":"pub-valid-0"}`, + `{"access_token": 1, "publisher_id":"pub-valid-0"}`, + `{"access_token": 1.2, "publisher_id":"pub-valid-0"}`, + `{"access_token": null, "publisher_id":"pub-valid-0"}`, + `{"access_token": true, "publisher_id":"pub-valid-0"}`, + `{"access_token": [], "publisher_id":"pub-valid-0"}`, + `{"access_token": {}, "publisher_id":"pub-valid-0"}`, + `{"access_token": 1, "publisher_id":"pub-valid-0"}`, + `{"access_token":"token-valid-0", "publisher_id": 1}`, +} diff --git a/analytics/config/testFiles/test-20200303 b/analytics/config/testFiles/test-20200303 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/config/config.go b/config/config.go index 2069730b692..d4edab2b53f 100644 --- a/config/config.go +++ b/config/config.go @@ -707,6 +707,7 @@ func SetupViper(v *viper.Viper, filename string) { 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 7b841a2838e..05f44e24b66 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -5,8 +5,6 @@ import ( "net/http" "strings" - "github.com/prebid/prebid-server/adapters/kubient" - "github.com/prebid/prebid-server/adapters" ttx "github.com/prebid/prebid-server/adapters/33across" "github.com/prebid/prebid-server/adapters/adform" @@ -35,6 +33,8 @@ import ( "github.com/prebid/prebid-server/adapters/gumgum" "github.com/prebid/prebid-server/adapters/improvedigital" "github.com/prebid/prebid-server/adapters/ix" + "github.com/prebid/prebid-server/adapters/kidoz" + "github.com/prebid/prebid-server/adapters/kubient" "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/lockerdome" "github.com/prebid/prebid-server/adapters/marsmedia" @@ -101,6 +101,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderMarsmedia: marsmedia.NewMarsmediaBidder(cfg.Adapters[string(openrtb_ext.BidderMarsmedia)].Endpoint), diff --git a/go.mod b/go.mod index af4bf5570a5..ea1f65efaa4 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 + github.com/xorcare/pointer v1.1.0 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 diff --git a/go.sum b/go.sum index 06f07b1ece0..6d215da0af5 100644 --- a/go.sum +++ b/go.sum @@ -156,6 +156,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 h1:BcMExZAULPkihVZ7UJXK7t8rwGqisXFw75tILnafhBY= github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xorcare/pointer v1.1.0 h1:sFwXOhRF8QZ0tyVZrtxWGIoVZNEmRzBCaFWdONPQIUM= +github.com/xorcare/pointer v1.1.0/go.mod h1:6KLhkOh6YbuvZkT4YbxIbR/wzLBjyMxOiNzZhJTor2Y= github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M= github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index c9c6f36332b..768128c96d6 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 { diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 627842f57ff..00c25f8a3f0 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -47,6 +47,7 @@ const ( BidderGumGum BidderName = "gumgum" BidderImprovedigital BidderName = "improvedigital" BidderIx BidderName = "ix" + BidderKidoz BidderName = "kidoz" BidderKubient BidderName = "kubient" BidderLifestreet BidderName = "lifestreet" BidderLockerDome BidderName = "lockerdome" @@ -107,6 +108,7 @@ var BidderMap = map[string]BidderName{ "gumgum": BidderGumGum, "improvedigital": BidderImprovedigital, "ix": BidderIx, + "kidoz": BidderKidoz, "kubient": BidderKubient, "lifestreet": BidderLifestreet, "lockerdome": BidderLockerDome, 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/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-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/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 7aef9fa8b5a..3de64ec1eb0 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -74,6 +74,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderTappx: true, openrtb_ext.BidderKubient: true, openrtb_ext.BidderPubnative: true, + openrtb_ext.BidderKidoz: 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` From fb768950a1f625f267c1cdbce65f6668a62e38c8 Mon Sep 17 00:00:00 2001 From: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Date: Wed, 18 Mar 2020 15:14:43 +0000 Subject: [PATCH 032/381] Update auction.md (#1224) Fix type --- docs/endpoints/openrtb2/auction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index d670b092174..02183960791 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -336,7 +336,7 @@ Bids can be temporarily cached on the server by sending the following data as `r } ``` -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. 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 From c3c87971d4dd1b151dec329f59fa00713c9ed95a Mon Sep 17 00:00:00 2001 From: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Date: Wed, 18 Mar 2020 15:15:16 +0000 Subject: [PATCH 033/381] Update auction.md (#1225) Fix typo. --- docs/endpoints/openrtb2/auction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 02183960791..7795ef5afe0 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -174,7 +174,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. From dcc062a84d34f67b42fe34b64cca55e73ef926e4 Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Wed, 18 Mar 2020 08:53:23 -0700 Subject: [PATCH 034/381] Added logging to cache for video endpoint (#1220) * WIP added logging to cache for video endpoint * Updating cache call to use TTL from config * Updates from initial feedback * Log now includes HTTP headers * Fixed caching to use a new cache entry rather than appending to the VAST * Added feature where is query is set, the test flag is set in the request * Updated recorded response and handleError * Updates from code review comments * Changed recorded output to be only the debug ext * Removed extra marhal calls * Changed cache to be an endpoint dependency * Added debugLog struct to hold all debug related info * Numerous smaller changes * Further code cleanup and added unit tests for debug changes * Added missing error checks * Added unit test for error case --- endpoints/openrtb2/amp_auction.go | 5 +- endpoints/openrtb2/amp_auction_test.go | 2 +- endpoints/openrtb2/auction.go | 7 +- endpoints/openrtb2/auction_test.go | 14 +- endpoints/openrtb2/video_auction.go | 83 +++++++-- endpoints/openrtb2/video_auction_test.go | 167 ++++++++++++++++++- exchange/auction.go | 14 +- exchange/auction_test.go | 3 +- exchange/cachetest/debuglog_disabled.json | 54 ++++++ exchange/cachetest/debuglog_enabled.json | 58 +++++++ exchange/exchange.go | 31 +++- exchange/exchange_test.go | 28 +++- exchange/exchangetest/debuglog_disabled.json | 161 ++++++++++++++++++ exchange/exchangetest/debuglog_enabled.json | 161 ++++++++++++++++++ exchange/targeting_test.go | 2 +- router/router.go | 2 +- 16 files changed, 749 insertions(+), 43 deletions(-) create mode 100644 exchange/cachetest/debuglog_disabled.json create mode 100644 exchange/cachetest/debuglog_enabled.json create mode 100644 exchange/exchangetest/debuglog_disabled.json create mode 100644 exchange/exchangetest/debuglog_enabled.json diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index d92f9d0ae61..8edc1e13787 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -71,7 +71,8 @@ func NewAmpEndpoint( disabledBidders, defRequest, defReqJSON, - bidderMap}).AmpAuction), nil + bidderMap, + nil}).AmpAuction), nil } @@ -165,7 +166,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h 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 { diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index c62a6a710d5..39d1e13c50d 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -902,7 +902,7 @@ 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) { +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{ diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index a0ed19e5fa4..d9c31eca98c 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -27,6 +27,7 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/prebid/prebid-server/prebid" + "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/privacy/ccpa" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" @@ -55,7 +56,8 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato disabledBidders, defRequest, defReqJSON, - bidderMap}).Auction), nil + bidderMap, + nil}).Auction), nil } type endpointDeps struct { @@ -71,6 +73,7 @@ type endpointDeps struct { defaultRequest bool defReqJSON []byte bidderMap map[string]openrtb_ext.BidderName + cache prebid_cache_client.Client } func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -137,7 +140,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 { diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 89f0fa255df..74a70c69415 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -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} for i, requestData := range testStoredRequests { newRequest, errList := edep.processStoredRequests(context.Background(), json.RawMessage(requestData)) @@ -638,6 +638,7 @@ func TestOversizedRequest(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -670,6 +671,7 @@ func TestRequestSizeEdgeCase(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -807,6 +809,7 @@ func TestDisabledBidder(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -840,6 +843,7 @@ func TestValidateImpExtDisabledBidder(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, } errs := deps.validateImpExt(imp, nil, 0) assert.JSONEq(t, `{"appnexus":{"placement_id":555}}`, string(imp.Ext)) @@ -878,6 +882,7 @@ func TestCurrencyTrunc(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, } ui := uint64(1) @@ -919,6 +924,7 @@ func TestCCPAInvalidValueWarning(t *testing.T) { false, []byte{}, openrtb_ext.BidderMap, + nil, } ui := uint64(1) @@ -953,7 +959,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 +970,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 +1330,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/video_auction.go b/endpoints/openrtb2/video_auction.go index 2a8663959a6..630a3f5acd3 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -14,6 +14,7 @@ import ( "github.com/buger/jsonparser" jsonpatch "github.com/evanphx/json-patch" + "github.com/gofrs/uuid" "github.com/prebid/prebid-server/errortypes" "github.com/golang/glog" @@ -24,20 +25,21 @@ import ( "github.com/prebid/prebid-server/exchange" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" + "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/usersync" ) 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 + return httprouter.Handle((&endpointDeps{ex, validator, requestsById, videoFetcher, categories, cfg, met, pbsAnalytics, disabledBidders, defRequest, defReqJSON, bidderMap, cache}).VideoAuctionEndpoint), nil } /* @@ -79,7 +81,38 @@ 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{ + EnableDebug: strings.EqualFold(debugQuery, "true"), + CacheType: prebid_cache_client.TypeXML, + TTL: cacheTTL, + } + defer func() { + if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil { + debugLog.Data = fmt.Sprintf("", debugLog.Data) + data, err := json.Marshal(debugLog.Data) + if err == nil { + toCache := []prebid_cache_client.Cacheable{ + { + Type: debugLog.CacheType, + Data: data, + TTLSeconds: debugLog.TTL, + Key: "log_" + debugLog.CacheKey, + }, + } + if deps.cache != nil { + ctx, cancel := context.WithDeadline(context.Background(), start.Add(time.Duration(100)*time.Millisecond)) + defer cancel() + deps.cache.PutJson(ctx, toCache) + } + } + } deps.metricsEngine.RecordRequest(labels) deps.metricsEngine.RecordRequestTime(labels, time.Since(start)) deps.analytics.LogVideoObject(&vo) @@ -91,38 +124,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.EnableDebug { + debugLog.Data = fmt.Sprintf("Request:\n%s", string(requestJson)) + if headerBytes, err := json.Marshal(r.Header); err == nil { + debugLog.Data = fmt.Sprintf("%s\n\nHeaders:\n%s", debugLog.Data, string(headerBytes)) + } else { + debugLog.Data = fmt.Sprintf("%s\n\nUnable to marshal headers data\n", debugLog.Data) + } + } //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, r.Header) if len(errL) > 0 { - handleError(&labels, w, errL, &vo) + handleError(&labels, w, errL, &vo, &debugLog) return } @@ -132,13 +173,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.EnableDebug { + bidReq.Test = 1 + } initialPodNumber := len(videoBidReq.PodConfig.Pods) if len(podErrors) > 0 { @@ -156,7 +201,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 } @@ -168,7 +213,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re errL = deps.validateRequest(bidReq) if len(errL) > 0 { - handleError(&labels, w, errL, &vo) + handleError(&labels, w, errL, &vo, &debugLog) return } @@ -196,16 +241,16 @@ 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) + 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 +258,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 +271,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 } @@ -242,7 +287,13 @@ 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.EnableDebug { + 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 diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index a5ad62c9fa8..0199b43f610 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -17,6 +17,7 @@ import ( "github.com/prebid/prebid-server/exchange" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" + "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" metrics "github.com/rcrowley/go-metrics" @@ -171,6 +172,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 umarshal 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 umarshal 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") @@ -714,7 +821,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") @@ -820,6 +927,41 @@ func TestParseVideoRequestWithoutUserAgentAndEmptyHeader(t *testing.T) { } +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{ + EnableDebug: true, + CacheType: prebid_cache_client.TypeXML, + Data: "test debug data", + TTL: int64(3600), + } + 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 mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) mockModule := &mockAnalyticsModule{} @@ -836,6 +978,7 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *p false, []byte{}, openrtb_ext.BidderMap, + nil, } return edep, theMetrics, mockModule @@ -875,11 +1018,27 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { false, []byte{}, openrtb_ext.BidderMap, + ex.cache, } 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 { } @@ -889,10 +1048,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.EnableDebug { + 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{{ diff --git a/exchange/auction.go b/exchange/auction.go index 2b9a8cb58fc..9909b78dd87 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -60,7 +60,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 +147,18 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, } } + if debugLog != nil && debugLog.EnableDebug { + debugLog.CacheKey = hbCacheID + if jsonBytes, err := json.Marshal(debugLog.Data); 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 ea19732d82b..d23ff03e00a 100644 --- a/exchange/auction_test.go +++ b/exchange/auction_test.go @@ -188,7 +188,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 +232,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/cachetest/debuglog_disabled.json b/exchange/cachetest/debuglog_disabled.json new file mode 100644 index 00000000000..675488c04d1 --- /dev/null +++ b/exchange/cachetest/debuglog_disabled.json @@ -0,0 +1,54 @@ +{ + "debugLog": { + "EnableDebug": false, + "CacheType": "xml", + "TTL": 3600, + "Data": "test debug data" + }, + "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..d4486558a54 --- /dev/null +++ b/exchange/cachetest/debuglog_enabled.json @@ -0,0 +1,58 @@ +{ + "debugLog": { + "EnableDebug": true, + "CacheType": "xml", + "TTL": 3600, + "Data": "test debug data" + }, + "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": "test debug data" + } + ], + "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 ef10180a745..995add3d496 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -27,10 +27,18 @@ import ( "github.com/prebid/prebid-server/prebid_cache_client" ) +type DebugLog struct { + EnableDebug bool + CacheType prebid_cache_client.PayloadType + Data string + TTL int64 + CacheKey string +} + // 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. @@ -78,7 +86,7 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con 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) { // Snapshot of resolved bid request for debug if test request resolvedRequest, err := buildResolvedRequest(bidRequest) if err != nil { @@ -142,6 +150,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions) var auc *auction = nil + var bidResponseExt *openrtb_ext.ExtBidResponse = nil if anyBidsReturned { var bidCategory map[string]string @@ -162,7 +171,15 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque if targData != nil { auc.setRoundedPrices(targData.priceGranularity) - cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &e.defaultTTLs, bidCategory) + if debugLog != nil && debugLog.EnableDebug { + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, errs) + if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil { + debugLog.Data = fmt.Sprintf("", debugLog.Data, string(bidRespExtBytes)) + } else { + errs = append(errs, errors.New("Unable to marshal response ext for debugging")) + } + } + cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &e.defaultTTLs, bidCategory, debugLog) if len(cacheErrs) > 0 { errs = append(errs, cacheErrs...) } @@ -176,7 +193,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque } // Build the response - return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, errs) + return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, bidResponseExt, errs) } type DealTierInfo struct { @@ -394,7 +411,7 @@ func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError { } // 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, 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, errList []error) (*openrtb.BidResponse, error) { bidResponse := new(openrtb.BidResponse) bidResponse.ID = bidRequest.ID @@ -417,7 +434,9 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ bidResponse.SeatBid = seatBids - bidResponseExt := e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, errList) + if bidResponseExt == nil { + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, errList) + } buffer := &bytes.Buffer{} enc := json.NewEncoder(buffer) enc.SetEscapeHTML(false) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 0a64bce0826..7217e609189 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -127,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, errList) + bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, nil, errList) /* 5) Assert we have no errors and one '&' character as we are supposed to */ if err != nil { @@ -279,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, errList) + bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, nil, errList) /* 5) Assert we have no errors and the bid response we expected*/ assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error") @@ -450,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, errList) + actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, nil, 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)) } @@ -489,7 +489,7 @@ func TestRaceIntegration(t *testing.T) { } 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) } @@ -666,7 +666,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) } @@ -732,7 +732,11 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { 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 + } + 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 { @@ -751,6 +755,17 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { } } } + if spec.DebugLog != nil { + if spec.DebugLog.EnableDebug { + if len(debugLog.Data) <= len(spec.DebugLog.Data) { + t.Errorf("%s: DebugLog was not modified when it should have been", filename) + } + } else { + if !strings.EqualFold(spec.DebugLog.Data, debugLog.Data) { + t.Errorf("%s: DebugLog was modified when it shouldn't have been", filename) + } + } + } } func findBiddersInAuction(t *testing.T, context string, req *openrtb.BidRequest) []string { @@ -1588,6 +1603,7 @@ type exchangeSpec struct { OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` Response exchangeResponse `json:"response,omitempty"` EnforceCCPA bool `json:"enforceCcpa"` + DebugLog *DebugLog `json:"debuglog,omitempty"` } type exchangeRequest struct { diff --git a/exchange/exchangetest/debuglog_disabled.json b/exchange/exchangetest/debuglog_disabled.json new file mode 100644 index 00000000000..0c24c121935 --- /dev/null +++ b/exchange/exchangetest/debuglog_disabled.json @@ -0,0 +1,161 @@ +{ + "debugLog": { + "EnableDebug": false, + "CacheType": "xml", + "TTL": 3600, + "Data": "test debug data" + }, + "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 + } + } + } + ], + "ext": { + "prebid": { + "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 + } + ] + } + ] + } + } + } + \ 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..281bf3a1b4e --- /dev/null +++ b/exchange/exchangetest/debuglog_enabled.json @@ -0,0 +1,161 @@ +{ + "debugLog": { + "EnableDebug": true, + "CacheType": "xml", + "TTL": 3600, + "Data": "test debug data" + }, + "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 + } + } + } + ], + "ext": { + "prebid": { + "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 + } + ] + } + ] + } + } + } + \ No newline at end of file diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index 92b338f97fb..f86309684c6 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -106,7 +106,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) diff --git a/router/router.go b/router/router.go index 7e713ca637a..8ac463b85a0 100644 --- a/router/router.go +++ b/router/router.go @@ -251,7 +251,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r 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) + 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) } From c7ead0710a10624a767d28a14ee3f1e3b7278ec7 Mon Sep 17 00:00:00 2001 From: Aadesh Date: Wed, 25 Mar 2020 14:58:02 -0400 Subject: [PATCH 035/381] added VISX vendor ID for usersyncing (#1229) Co-authored-by: Aadesh Patel --- adapters/visx/usersync.go | 2 +- adapters/visx/usersync_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/visx/usersync.go b/adapters/visx/usersync.go index 0ceb58c505f..fe1f5a42a10 100644 --- a/adapters/visx/usersync.go +++ b/adapters/visx/usersync.go @@ -8,5 +8,5 @@ import ( ) func NewVisxSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("visx", 0, temp, adapters.SyncTypeRedirect) + return adapters.NewSyncer("visx", 154, temp, adapters.SyncTypeRedirect) } diff --git a/adapters/visx/usersync_test.go b/adapters/visx/usersync_test.go index 01e80e644c5..a77136c9240 100644 --- a/adapters/visx/usersync_test.go +++ b/adapters/visx/usersync_test.go @@ -30,6 +30,6 @@ func TestVisxSyncer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "https://t.visx.net/s2s_sync?gdpr=A&gdpr_consent=B&us_privacy=C&redir=%2Fsetuid%3Fbidder%3Dvisx%26gdpr%3DA%26gdpr_consent%3DB%26uid%3D%24%7BUUID%7D", syncInfo.URL) assert.Equal(t, "redirect", syncInfo.Type) - assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.EqualValues(t, 154, syncer.GDPRVendorID()) assert.Equal(t, false, syncInfo.SupportCORS) } From 145c5259042a09903a29f7b998d631f1782b92c7 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Wed, 25 Mar 2020 16:59:19 -0400 Subject: [PATCH 036/381] First pass at phase 1 TCF 2.0 support (#1228) * First pass at phase 1 TCF 2.0 support * minor fixes * Update go-gdpr library and fix stuff * Fixes for PR comments --- gdpr/gdpr.go | 14 +++++-- gdpr/impl.go | 28 ++++++++++---- gdpr/impl_test.go | 63 +++++++++++++++++++++++--------- gdpr/vendorlist-fetching.go | 46 ++++++++++++++--------- gdpr/vendorlist-fetching_test.go | 24 ++++++------ go.mod | 2 +- go.sum | 4 +- 7 files changed, 122 insertions(+), 59 deletions(-) diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index bdba008a77a..a6b64203a95 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/prebid/go-gdpr/vendorlist" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" ) @@ -25,6 +26,11 @@ type Permissions interface { PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, error) } +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 +39,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 2fe6a67e99f..615c3a090c9 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -3,7 +3,9 @@ package gdpr import ( "context" - "github.com/prebid/go-gdpr/consentconstants" + "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" "github.com/prebid/go-gdpr/vendorlist" "github.com/prebid/prebid-server/config" @@ -18,7 +20,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) { @@ -71,10 +73,10 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen return false, nil } + // InfoStorageAccess is the same across TCF 1 and TCF 2 if vendor.Purpose(consentconstants.InfoStorageAccess) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && parsedConsent.VendorConsent(vendorID) { return true, nil } - return false, nil } @@ -93,14 +95,20 @@ func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent return 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 { + // Need to add the location special purpose once the library supports it. + 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, 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, nil + } } - return false, nil } -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 +118,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 } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index a1d4af3346d..8b89577d6c8 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -18,8 +18,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 +38,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) @@ -63,9 +69,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") @@ -94,9 +105,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") @@ -125,9 +141,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 +165,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") @@ -169,9 +193,14 @@ 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 diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index d492e9e5e11..987622a6a8a 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -11,12 +11,14 @@ import ( "time" "github.com/golang/glog" + "github.com/prebid/go-gdpr/api" "github.com/prebid/go-gdpr/vendorlist" + "github.com/prebid/go-gdpr/vendorlist2" "github.com/prebid/prebid-server/config" "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 cdde3c46a68..8197fa263bc 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -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) @@ -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) @@ -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) } @@ -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. @@ -201,9 +203,9 @@ func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purpos return string(data) } -func testURLMaker(server *httptest.Server) func(uint16) string { +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)) } } diff --git a/go.mod b/go.mod index ea1f65efaa4..387b8b9815c 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/onsi/gomega v1.7.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.2.0 // indirect - github.com/prebid/go-gdpr v0.6.0 + github.com/prebid/go-gdpr v0.7.0 github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 diff --git a/go.sum b/go.sum index 6d215da0af5..ad9caf5004b 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,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.7.0 h1:m4E/FjUhTBMciDsd3lQlbzFyXLzNK+JQkFmInJpFAwc= +github.com/prebid/go-gdpr v0.7.0/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtWKsUFH5IzdQxHIgP609VSIVe5Hywg2phs= github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98= github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed h1:0dloFFFNNDG7c+8qtkYw2FdADrWy9s5cI8wHp6tK3Mg= From 163f33187cebd4dc87087ffd7f67392d73c89dea Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Thu, 26 Mar 2020 09:30:39 -0700 Subject: [PATCH 037/381] Updated price granularity unmarshal to accept empty values and ranges (#1230) --- openrtb_ext/request.go | 7 ++++--- openrtb_ext/request_test.go | 13 ++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index ee1a0cd0f8b..9d1456c9618 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -148,10 +148,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 } diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index 860334af98f..3291c4f9fb2 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -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}]}`), From 469986402cfcd680687fa3fcc2b1c23885aef31c Mon Sep 17 00:00:00 2001 From: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Date: Thu, 26 Mar 2020 20:33:30 +0300 Subject: [PATCH 038/381] Update vendorID for TheMediaGrid s2s Bid Adapter (#1232) --- adapters/grid/usersync.go | 2 +- adapters/grid/usersync_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/grid/usersync.go b/adapters/grid/usersync.go index ddf7d5db66b..afdc5db763c 100644 --- a/adapters/grid/usersync.go +++ b/adapters/grid/usersync.go @@ -8,5 +8,5 @@ import ( ) func NewGridSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("grid", 0, temp, adapters.SyncTypeRedirect) + return adapters.NewSyncer("grid", 686, temp, adapters.SyncTypeRedirect) } diff --git a/adapters/grid/usersync_test.go b/adapters/grid/usersync_test.go index 9b97f605a41..99730b5deb4 100644 --- a/adapters/grid/usersync_test.go +++ b/adapters/grid/usersync_test.go @@ -25,6 +25,6 @@ func TestGridSyncer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "//not_localhost/synclocalhost%2Fsetuid%3Fbidder%3Dgrid%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, 686, syncer.GDPRVendorID()) assert.Equal(t, false, syncInfo.SupportCORS) } From 7e706f499e66ee8731c23f1b231db3530897c3db Mon Sep 17 00:00:00 2001 From: Aadesh Date: Fri, 27 Mar 2020 10:17:48 -0400 Subject: [PATCH 039/381] treat 204 from FAN as a no bids response (#1233) Co-authored-by: Aadesh Patel --- .../supplemental/no-bid-204.json | 91 +++++++++++++++++++ adapters/audienceNetwork/facebook.go | 6 ++ 2 files changed, 97 insertions(+) create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json 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..042c86bd7fd --- /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-req-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": "b2f9edfd707106adb6b692520081ad7e2a345444af1a895310228297a1b6247e", + "platformid": "test-platform-id" + } + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go index 3ece7bb99e4..db7657f59b7 100644 --- a/adapters/audienceNetwork/facebook.go +++ b/adapters/audienceNetwork/facebook.go @@ -333,6 +333,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{ From e05369b20aaf542b08bb4c1f9c0e61d289789a3d Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 30 Mar 2020 07:48:41 -0700 Subject: [PATCH 040/381] AMP CCPA Fix (#1187) --- analytics/config/testFiles/test-20200303 | 0 endpoints/openrtb2/amp_auction.go | 90 ++- endpoints/openrtb2/amp_auction_test.go | 864 ++++++++++++----------- endpoints/openrtb2/auction.go | 19 +- endpoints/openrtb2/video_auction.go | 6 +- errortypes/code.go | 34 + errortypes/code_test.go | 24 + errortypes/errortypes.go | 101 +-- errortypes/severity.go | 63 ++ errortypes/severity_test.go | 143 ++++ exchange/exchange.go | 14 +- openrtb_ext/bidders.go | 5 + openrtb_ext/bidders_test.go | 20 +- privacy/ccpa/policy.go | 34 +- privacy/ccpa/policy_test.go | 150 +++- privacy/gdpr/policy.go | 7 + privacy/gdpr/policy_test.go | 29 + privacy/policies.go | 25 + privacy/policies_test.go | 42 ++ 19 files changed, 1082 insertions(+), 588 deletions(-) delete mode 100644 analytics/config/testFiles/test-20200303 create mode 100644 errortypes/code.go create mode 100644 errortypes/code_test.go create mode 100644 errortypes/severity.go create mode 100644 errortypes/severity_test.go diff --git a/analytics/config/testFiles/test-20200303 b/analytics/config/testFiles/test-20200303 deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 8edc1e13787..97ac8d0caae 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -23,8 +23,6 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/prebid/prebid-server/privacy" - "github.com/prebid/prebid-server/privacy/ccpa" - "github.com/prebid/prebid-server/privacy/gdpr" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" "github.com/prebid/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 @@ -121,13 +120,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 } @@ -151,18 +150,18 @@ 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 } @@ -206,6 +205,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) @@ -213,10 +213,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 @@ -252,8 +262,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 } @@ -261,18 +271,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 } @@ -287,9 +294,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() @@ -309,7 +313,8 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req return } - if debug { + debugParam := httpRequest.FormValue("debug") + if debugParam == "1" { req.Test = 1 } @@ -336,18 +341,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) @@ -383,16 +385,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 { @@ -533,3 +536,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 39d1e13c50d..b25d5b0cc8f 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,472 @@ 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 := "BONV8oqONXwgmADACHENAO7pqzAAppY" + 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 Resonse + 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 Resonse + 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 Resonse + 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) + // 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) - // 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" - - // 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 Resonse + 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 Resonse + 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) { @@ -739,102 +853,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,6 +920,15 @@ type mockAmpExchange struct { lastRequest *openrtb.BidRequest } +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 @@ -926,39 +953,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 +983,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 +1000,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 d9c31eca98c..4594a4d5f64 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -106,7 +106,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 } @@ -326,7 +326,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 } } @@ -1177,8 +1177,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 @@ -1194,17 +1194,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 { - return true - } - } - return false -} - // Returns the effective publisher ID func effectivePubID(pub *openrtb.Publisher) string { if pub != nil { diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 630a3f5acd3..0215eb4cff2 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -298,12 +298,12 @@ func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error, 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 diff --git a/errortypes/code.go b/errortypes/code.go new file mode 100644 index 00000000000..80a5eb45542 --- /dev/null +++ b/errortypes/code.go @@ -0,0 +1,34 @@ +package errortypes + +// Defines numeric codes for well-known errors. +const ( + UnknownErrorCode = 999 + TimeoutErrorCode = iota + BadInputErrorCode + BlacklistedAppErrorCode + BadServerResponseErrorCode + FailedToRequestBidsErrorCode + BidderTemporarilyDisabledErrorCode + BlacklistedAcctErrorCode + AcctRequiredErrorCode +) + +// 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 4bcea24f737..c953f9b7e08 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -1,28 +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 -) - -// 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. // @@ -36,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. @@ -52,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 @@ -68,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 @@ -84,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 @@ -100,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. @@ -121,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, @@ -138,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 @@ -153,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 } @@ -165,15 +174,27 @@ 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 } -// 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 *InvalidPrivacyConsent) 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/exchange.go b/exchange/exchange.go index 995add3d496..3cab1880456 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -385,14 +385,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 @@ -404,7 +404,7 @@ 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 @@ -672,7 +672,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/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 00c25f8a3f0..e3f186db333 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -17,8 +17,12 @@ 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" @@ -80,6 +84,7 @@ const ( ) // 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, 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/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index e34a35717a4..8b50e1112a9 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -3,6 +3,7 @@ package ccpa import ( "encoding/json" "errors" + "fmt" "github.com/buger/jsonparser" "github.com/mxmCherry/openrtb" @@ -49,35 +50,44 @@ func (p Policy) Write(req *openrtb.BidRequest) error { 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 a70874ebbec..54613c89880 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -154,74 +154,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/gdpr/policy.go b/privacy/gdpr/policy.go index 61f95ac99c6..a4e1bc6aac7 100644 --- a/privacy/gdpr/policy.go +++ b/privacy/gdpr/policy.go @@ -5,6 +5,7 @@ import ( "github.com/buger/jsonparser" "github.com/mxmCherry/openrtb" + "github.com/prebid/go-gdpr/vendorconsent" ) // Policy represents the GDPR regulation for an OpenRTB bid request. @@ -32,3 +33,9 @@ func (p Policy) Write(req *openrtb.BidRequest) error { req.User.Ext, err = jsonparser.Set(req.User.Ext, []byte(`"`+p.Consent+`"`), "consent") return err } + +// 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 80bd882dada..00b97644971 100644 --- a/privacy/gdpr/policy_test.go +++ b/privacy/gdpr/policy_test.go @@ -72,3 +72,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/policies.go b/privacy/policies.go index ebe34ef5c3d..cb11c6d03a6 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 697942521fc..34fbe52d0e9 100644 --- a/privacy/policies_test.go +++ b/privacy/policies_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/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") + } +} From 7c009bac4f43b9c3b94671ccce58fe2f96024478 Mon Sep 17 00:00:00 2001 From: bretg Date: Mon, 30 Mar 2020 16:29:14 -0400 Subject: [PATCH 041/381] Update rubicon.md (#1234) --- docs/bidders/rubicon.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/bidders/rubicon.md b/docs/bidders/rubicon.md index 564acd9a3c4..ea376da427d 100644 --- a/docs/bidders/rubicon.md +++ b/docs/bidders/rubicon.md @@ -1,6 +1,6 @@ # 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. From 4b1f3e707ff9396517e87f2a488ba54b9c75c098 Mon Sep 17 00:00:00 2001 From: bretg Date: Wed, 1 Apr 2020 15:35:52 -0400 Subject: [PATCH 042/381] adding schain interface (#1203) --- docs/endpoints/openrtb2/auction.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 7795ef5afe0..0f03960190d 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -403,28 +403,21 @@ 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 +#### Supply Chain 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"] -``` +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. -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) +Bidder-specific schains (PBS-Java only): ``` -"ext": { - "prebid": { - "currency": { - "rates": { - "USD": { "UAH": 24.47, "ETB": 32.04 } - } - } - } -} +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 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. +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. #### Stored Responses (PBS-Java only) From 40f433bfc30b47ae32fc302de1b64ef0cbcab086 Mon Sep 17 00:00:00 2001 From: bretg Date: Wed, 1 Apr 2020 17:13:21 -0400 Subject: [PATCH 043/381] added Rewarded Video section (#1200) also edited all examples so they include the full openRTB context --- docs/endpoints/openrtb2/auction.md | 179 ++++++++++++++++++++--------- 1 file changed, 125 insertions(+), 54 deletions(-) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 0f03960190d..bd421850d1f 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -95,8 +95,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 +120,21 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta ``` { - "pricegranularity": { - "precision": 2, - "ranges": [ - { + "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 + }] + }, + "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. @@ -159,9 +169,20 @@ MediaType PriceGranularity (PBS-Java only) - when a single OpenRTB request conta ``` { - "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." + } + } + } + }] + }] } ``` @@ -183,8 +204,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" + } + } + } + } } ``` @@ -245,11 +274,9 @@ 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. @@ -273,19 +300,17 @@ For example, a request may return this in `response.ext` ``` { - "errors": { - "appnexus": [ - { - "code": 2, - "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio." - } - ], - "rubicon": [ - { - "code": 1, - "message": "The request exceeded the timeout allocated" - } - ] + "ext": { + "errors": { + "appnexus": [{ + "code": 2, + "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio." + }], + "rubicon": [{ + "code": 1, + "message": "The request exceeded the timeout allocated" + }] + } } } ``` @@ -319,7 +344,15 @@ A typical `storedrequest` value looks like this: ``` { - "id": "some-id" + "imp": [{ + "ext": { + "prebid": { + "storedrequest": { + "id": "some-id" + } + } + } + }] } ``` @@ -331,12 +364,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. This 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 @@ -403,8 +442,35 @@ 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): @@ -419,6 +485,11 @@ In this scenario, Prebid Server sends the first schain object to `bidderA` and t 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: @@ -583,33 +654,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 } } ] From 3665275ff778b6a95fe583053dedf4c0a1365529 Mon Sep 17 00:00:00 2001 From: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> Date: Thu, 2 Apr 2020 23:40:42 +0200 Subject: [PATCH 044/381] nanointeractive adapter (#1213) * nanointeractive adapter * nanointeractive adapter, changes after review * nanointeractive adapter * nanointeractive adapter, changes after review * formatting --- adapters/nanointeractive/nanointeractive.go | 172 ++++++++++++++++++ .../nanointeractive/nanointeractive_test.go | 10 + .../exemplary/simple-banner.json | 90 +++++++++ .../params/race/banner.json | 3 + .../supplemental/bad_response.json | 63 +++++++ .../supplemental/invalid-params.json | 81 +++++++++ .../supplemental/multi-param.json | 151 +++++++++++++++ .../supplemental/status_204.json | 58 ++++++ .../supplemental/status_400.json | 63 +++++++ .../supplemental/status_418.json | 63 +++++++ adapters/nanointeractive/params_test.go | 63 +++++++ adapters/nanointeractive/usersync.go | 12 ++ adapters/nanointeractive/usersync_test.go | 36 ++++ config/config.go | 2 + exchange/adapter_map.go | 32 ++-- openrtb_ext/bidders.go | 2 + openrtb_ext/imp_nanointeractive.go | 10 + static/bidder-info/nanointeractive.yaml | 9 + static/bidder-params/nanointeractive.json | 32 ++++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 21 files changed, 940 insertions(+), 15 deletions(-) create mode 100644 adapters/nanointeractive/nanointeractive.go create mode 100644 adapters/nanointeractive/nanointeractive_test.go create mode 100644 adapters/nanointeractive/nanointeractivetest/exemplary/simple-banner.json create mode 100644 adapters/nanointeractive/nanointeractivetest/params/race/banner.json create mode 100644 adapters/nanointeractive/nanointeractivetest/supplemental/bad_response.json create mode 100644 adapters/nanointeractive/nanointeractivetest/supplemental/invalid-params.json create mode 100644 adapters/nanointeractive/nanointeractivetest/supplemental/multi-param.json create mode 100644 adapters/nanointeractive/nanointeractivetest/supplemental/status_204.json create mode 100644 adapters/nanointeractive/nanointeractivetest/supplemental/status_400.json create mode 100644 adapters/nanointeractive/nanointeractivetest/supplemental/status_418.json create mode 100644 adapters/nanointeractive/params_test.go create mode 100644 adapters/nanointeractive/usersync.go create mode 100644 adapters/nanointeractive/usersync_test.go create mode 100644 openrtb_ext/imp_nanointeractive.go create mode 100644 static/bidder-info/nanointeractive.yaml create mode 100644 static/bidder-params/nanointeractive.json diff --git a/adapters/nanointeractive/nanointeractive.go b/adapters/nanointeractive/nanointeractive.go new file mode 100644 index 00000000000..72832893af1 --- /dev/null +++ b/adapters/nanointeractive/nanointeractive.go @@ -0,0 +1,172 @@ +package nanointeractive + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" +) + +type NanoInteractiveAdapter struct { + endpoint string +} + +func (a *NanoInteractiveAdapter) Name() string { + return "Nano" +} + +func (a *NanoInteractiveAdapter) SkipNoCookies() bool { + return false +} + +func (a *NanoInteractiveAdapter) MakeRequests(bidRequest *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + var errs []error + var validImps []openrtb.Imp + + var adapterRequests []*adapters.RequestData + var referer string = "" + + for i := 0; i < len(bidRequest.Imp); i++ { + + ref, err := checkImp(&bidRequest.Imp[i]) + + // If the parsing is failed, remove imp and add the error. + if err != nil { + errs = append(errs, err) + continue + } + if referer == "" && ref != "" { + referer = ref + } + validImps = append(validImps, bidRequest.Imp[i]) + } + + if len(validImps) == 0 { + errs = append(errs, fmt.Errorf("no impressions in the bid request")) + return nil, errs + } + + // set referer origin + if referer != "" { + if bidRequest.Site == nil { + bidRequest.Site = &openrtb.Site{} + } + bidRequest.Site.Ref = referer + } + + bidRequest.Imp = validImps + + reqJSON, err := json.Marshal(bidRequest) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("x-openrtb-version", "2.5") + if bidRequest.Device != nil { + headers.Add("User-Agent", bidRequest.Device.UA) + headers.Add("X-Forwarded-For", bidRequest.Device.IP) + } + if bidRequest.Site != nil { + headers.Add("Referer", bidRequest.Site.Page) + } + + // set user's cookie + if bidRequest.User != nil && bidRequest.User.BuyerUID != "" { + headers.Add("Cookie", "Nano="+bidRequest.User.BuyerUID) + } + + adapterRequests = append(adapterRequests, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }) + + return adapterRequests, errs +} + +func (a *NanoInteractiveAdapter) MakeBids( + internalRequest *openrtb.BidRequest, + externalRequest *adapters.RequestData, + response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } else if response.StatusCode == http.StatusBadRequest { + return nil, []error{adapters.BadInput("Invalid request.")} + } else if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("unexpected HTTP status %d.", response.StatusCode), + }} + } + + var openRtbBidResponse openrtb.BidResponse + + if err := json.Unmarshal(response.Body, &openRtbBidResponse); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("bad server body response"), + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(openRtbBidResponse.SeatBid[0].Bid)) + bidResponse.Currency = openRtbBidResponse.Cur + + sb := openRtbBidResponse.SeatBid[0] + for i := 0; i < len(sb.Bid); i++ { + if !(sb.Bid[i].Price > 0) { + continue + } + bid := sb.Bid[i] + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: openrtb_ext.BidTypeBanner, + }) + } + return bidResponse, nil +} + +func checkImp(imp *openrtb.Imp) (string, error) { + // We support only banner impression + if imp.Banner == nil { + return "", fmt.Errorf("invalid MediaType. NanoInteractive only supports Banner type. ImpID=%s", imp.ID) + } + + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return "", fmt.Errorf("ext not provided; ImpID=%s", imp.ID) + } + + var nanoExt openrtb_ext.ExtImpNanoInteractive + if err := json.Unmarshal(bidderExt.Bidder, &nanoExt); err != nil { + return "", fmt.Errorf("ext.bidder not provided; ImpID=%s", imp.ID) + } + if nanoExt.Pid == "" { + return "", fmt.Errorf("pid is empty; ImpID=%s", imp.ID) + } + + if nanoExt.Ref != "" { + return string(nanoExt.Ref), nil + } + + return "", nil +} + +func NewNanoIneractiveBidder(endpoint string) *NanoInteractiveAdapter { + return &NanoInteractiveAdapter{ + endpoint: endpoint, + } +} + +func NewNanoInteractiveAdapter(uri string) *NanoInteractiveAdapter { + return &NanoInteractiveAdapter{ + endpoint: uri, + } +} diff --git a/adapters/nanointeractive/nanointeractive_test.go b/adapters/nanointeractive/nanointeractive_test.go new file mode 100644 index 00000000000..fa7069a5da3 --- /dev/null +++ b/adapters/nanointeractive/nanointeractive_test.go @@ -0,0 +1,10 @@ +package nanointeractive + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "testing" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "nanointeractivetest", NewNanoIneractiveBidder("https://ad.audiencemanager.de/hbs")) +} diff --git a/adapters/nanointeractive/nanointeractivetest/exemplary/simple-banner.json b/adapters/nanointeractive/nanointeractivetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..20cc70b6785 --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/exemplary/simple-banner.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "pid": "58bfec94eb0a1916fa380163" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ad.audiencemanager.de/hbs", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{ "w": 300,"h": 250} + ] + }, + "ext": { + "bidder": { + "pid": "58bfec94eb0a1916fa380163" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "nanointeractive", + "bid": [{ + "id": "1", + "impid": "test-imp-id", + "price": 0.4580126, + "adm": "", + "adid": "test_ad_id", + "adomain": ["audiencemanager.de"], + "cid": "test_cid", + "crid": "test_banner_crid", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5a7789eg2662b524d8d7264a96", + "cur": "EUR" + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.4580126, + "adm": "", + "adid": "test_ad_id", + "adomain": ["yahoo.com"], + "cid": "test_cid", + "crid": "test_banner_crid", + "h": 250, + "w": 300 + }, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] +} diff --git a/adapters/nanointeractive/nanointeractivetest/params/race/banner.json b/adapters/nanointeractive/nanointeractivetest/params/race/banner.json new file mode 100644 index 00000000000..bb35ea8488a --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "pid": "58bfec94eb0a1916fa380163" +} diff --git a/adapters/nanointeractive/nanointeractivetest/supplemental/bad_response.json b/adapters/nanointeractive/nanointeractivetest/supplemental/bad_response.json new file mode 100644 index 00000000000..587c952a042 --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/supplemental/bad_response.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "213" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ad.audiencemanager.de/hbs", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "213" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": "{\"id\"data.lost" + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "bad server body response", + "comparison": "literal" + } + ] +} diff --git a/adapters/nanointeractive/nanointeractivetest/supplemental/invalid-params.json b/adapters/nanointeractive/nanointeractivetest/supplemental/invalid-params.json new file mode 100644 index 00000000000..631dc99e5a8 --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/supplemental/invalid-params.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": {}, + "ext": { + "bidder": {} + } + }, + { + "id": "test-imp-id-2", + "banner": { + "format": [{"w": 300, "h": 250}] + }, + "ext": { + + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [{"w": 300, "h": 250}] + } + }, + { + "id": "test-imp-id-4", + "video": {}, + "ext": { + "bidder": {} + } + }, + { + "id": "test-imp-id-5", + "audio": { + "startdelay": 0, + "api": [] + }, + "ext": { + "bidder": {} + } + } + ], + "site": { + "id": "siteID", + "publisher": { + "id": "1234" + } + }, + "device": { + "os": "android" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "pid is empty; ImpID=test-imp-id-1", + "comparison": "literal" + }, + { + "value": "ext.bidder not provided; ImpID=test-imp-id-2", + "comparison": "literal" + }, + { + "value": "ext not provided; ImpID=test-imp-id-3", + "comparison": "literal" + }, + { + "value": "invalid MediaType. NanoInteractive only supports Banner type. ImpID=test-imp-id-4", + "comparison": "literal" + }, + { + "value": "invalid MediaType. NanoInteractive only supports Banner type. ImpID=test-imp-id-5", + "comparison": "literal" + }, + { + "value": "no impressions in the bid request", + "comparison": "literal" + } + ] +} diff --git a/adapters/nanointeractive/nanointeractivetest/supplemental/multi-param.json b/adapters/nanointeractive/nanointeractivetest/supplemental/multi-param.json new file mode 100644 index 00000000000..27e7bec1f5f --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/supplemental/multi-param.json @@ -0,0 +1,151 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "pid": "58bfec94eb0a1916fa380163", + "ref": "https://nanointeractive.com" + } + } + }, + { + "id": "test-imp-id2", + "banner": { + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "pid": "58bfec94eb0a1916fa380163", + "nq": ["search query"], + "category": "Automotive", + "subId": "a23", + "ref": "https://nanointeractive.com" + } + } + } + ], + "device": { + "ip": "127.0.0.1", + "ua": "user_agent" + }, + "user": { + "buyeruid": "userId" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ad.audiencemanager.de/hbs", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{ "w": 300,"h": 250} + ] + }, + "ext": { + "bidder": { + "pid": "58bfec94eb0a1916fa380163", + "ref": "https://nanointeractive.com" + } + } + }, + { + "id": "test-imp-id2", + "banner": { + "format": [{ "w": 300,"h": 250} + ] + }, + "ext": { + "bidder": { + "pid": "58bfec94eb0a1916fa380163", + "nq": ["search query"], + "category": "Automotive", + "subId": "a23", + "ref": "https://nanointeractive.com" + } + } + } + ], + "site": { + "ref": "https://nanointeractive.com" + }, + "device": { + "ip": "127.0.0.1", + "ua": "user_agent" + }, + "user": { + "buyeruid": "userId" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "nanointeractive", + "bid": [{ + "id": "1", + "impid": "test-imp-id", + "price": 0.4580126, + "adm": "", + "adid": "test_ad_id", + "adomain": ["audiencemanager.de"], + "cid": "test_cid", + "crid": "test_banner_crid", + "h": 250, + "w": 300 + },{ + "id": "2", + "impid": "test-imp-id2", + "price": 0, + "adm": "", + "adid": "test_ad_id", + "adomain": ["audiencemanager.de"], + "cid": "test_cid", + "crid": "test_banner_crid", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5a7789eg2662b524d8d7264a96", + "cur": "EUR" + } + } + } + ], + + "expectedBids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.4580126, + "adm": "", + "adid": "test_ad_id", + "adomain": ["yahoo.com"], + "cid": "test_cid", + "crid": "test_banner_crid", + "h": 250, + "w": 300 + }, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] +} diff --git a/adapters/nanointeractive/nanointeractivetest/supplemental/status_204.json b/adapters/nanointeractive/nanointeractivetest/supplemental/status_204.json new file mode 100644 index 00000000000..ed4d8ff38b8 --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/supplemental/status_204.json @@ -0,0 +1,58 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "123" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ad.audiencemanager.de/hbs", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "123" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + + "expectedBidResponses": [] +} diff --git a/adapters/nanointeractive/nanointeractivetest/supplemental/status_400.json b/adapters/nanointeractive/nanointeractivetest/supplemental/status_400.json new file mode 100644 index 00000000000..f02bd478656 --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/supplemental/status_400.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "123" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ad.audiencemanager.de/hbs", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "123" + } + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Invalid request.", + "comparison": "literal" + } + ] +} diff --git a/adapters/nanointeractive/nanointeractivetest/supplemental/status_418.json b/adapters/nanointeractive/nanointeractivetest/supplemental/status_418.json new file mode 100644 index 00000000000..b7ed65da2af --- /dev/null +++ b/adapters/nanointeractive/nanointeractivetest/supplemental/status_418.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "123" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ad.audiencemanager.de/hbs", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "pid": "123" + } + } + } + ] + } + }, + "mockResponse": { + "status": 418, + "body": {} + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "unexpected HTTP status 418.", + "comparison": "literal" + } + ] +} diff --git a/adapters/nanointeractive/params_test.go b/adapters/nanointeractive/params_test.go new file mode 100644 index 00000000000..b290f3d94b1 --- /dev/null +++ b/adapters/nanointeractive/params_test.go @@ -0,0 +1,63 @@ +package nanointeractive + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/nanointeractive.json +// +// These also validate the format of the external API: request.imp[i].ext.nanointeracive + +// TestValidParams makes sure that the NanoInteractive 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.BidderNanoInteractive, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected NanoInteractive params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the Marsmedia 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.BidderNanoInteractive, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"pid": "dafad098"}`, + `{"pid":"dfasfda","nq":["search query"]}`, + `{"pid":"dfasfda","nq":["search query"],"subId":"any string value","category":"any string value"}`, +} + +var invalidParams = []string{ + `{"pid":123}`, + `{"pid":"12323","nq":"search query not an array"}`, + `{"pid":"12323","category":1}`, + `{"pid":"12323","subId":23}`, + ``, + `null`, + `true`, + `9`, + `1.2`, + `[]`, + `{}`, + `placementId`, + `zone`, + `zoneId`, +} diff --git a/adapters/nanointeractive/usersync.go b/adapters/nanointeractive/usersync.go new file mode 100644 index 00000000000..e6227436fb2 --- /dev/null +++ b/adapters/nanointeractive/usersync.go @@ -0,0 +1,12 @@ +package nanointeractive + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewNanoInteractiveSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("nanointeractive", 72, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/nanointeractive/usersync_test.go b/adapters/nanointeractive/usersync_test.go new file mode 100644 index 00000000000..ec9787bc20d --- /dev/null +++ b/adapters/nanointeractive/usersync_test.go @@ -0,0 +1,36 @@ +package nanointeractive + +import ( + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/stretchr/testify/assert" +) + +func TestNewNanoInteractiveSyncer(t *testing.T) { + syncURL := "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri=http%3A%2F%2Flocalhost%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + userSync := NewNanoInteractiveSyncer(syncURLTemplate) + syncInfo, err := userSync.GetUsersyncInfo( + privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", + }, + CCPA: ccpa.Policy{ + Value: "1NYN", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr=1&consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw&us_privacy=1NYN&redirectUri=http%3A%2F%2Flocalhost%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D1%26gdpr_consent%3DBONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw%26uid%3D%24UID", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 72, userSync.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index d4edab2b53f..999b1870b54 100644 --- a/config/config.go +++ b/config/config.go @@ -521,6 +521,7 @@ func (cfg *Configuration) setDerivedDefaults() { 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlockerdome%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7B%7Buid%7D%7D") 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNanoInteractive, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOpenx, "https://rtb.openx.net/sync/prebid?r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dpulsepoint%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25VGUID%25%25") @@ -713,6 +714,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.lockerdome.endpoint", "https://lockerdome.com/ladbid/prebidserver/openrtb2") 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.nanointeractive.endpoint", "https://ad.audiencemanager.de/hbs") v.SetDefault("adapters.openx.endpoint", "http://rtb.openx.net/prebid") 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 05f44e24b66..f7b970c571b 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -39,6 +39,7 @@ import ( "github.com/prebid/prebid-server/adapters/lockerdome" "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/mgid" + "github.com/prebid/prebid-server/adapters/nanointeractive" "github.com/prebid/prebid-server/adapters/openx" "github.com/prebid/prebid-server/adapters/pubmatic" "github.com/prebid/prebid-server/adapters/pubnative" @@ -96,21 +97,22 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.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.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.BidderMarsmedia: marsmedia.NewMarsmediaBidder(cfg.Adapters[string(openrtb_ext.BidderMarsmedia)].Endpoint), + openrtb_ext.BidderMgid: mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].Endpoint), + openrtb_ext.BidderNanoInteractive: nanointeractive.NewNanoIneractiveBidder(cfg.Adapters[string(openrtb_ext.BidderNanoInteractive)].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, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index e3f186db333..ec9745563ef 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -57,6 +57,7 @@ const ( BidderLockerDome BidderName = "lockerdome" BidderMarsmedia BidderName = "marsmedia" BidderMgid BidderName = "mgid" + BidderNanoInteractive BidderName = "nanointeractive" BidderOpenx BidderName = "openx" BidderPubmatic BidderName = "pubmatic" BidderPubnative BidderName = "pubnative" @@ -119,6 +120,7 @@ var BidderMap = map[string]BidderName{ "lockerdome": BidderLockerDome, "marsmedia": BidderMarsmedia, "mgid": BidderMgid, + "nanointeractive": BidderNanoInteractive, "openx": BidderOpenx, "pubmatic": BidderPubmatic, "pubnative": BidderPubnative, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index c7ad70b7eff..be0392f2dbb 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -34,6 +34,7 @@ import ( "github.com/prebid/prebid-server/adapters/lockerdome" "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/mgid" + "github.com/prebid/prebid-server/adapters/nanointeractive" "github.com/prebid/prebid-server/adapters/openx" "github.com/prebid/prebid-server/adapters/pubmatic" "github.com/prebid/prebid-server/adapters/pulsepoint" @@ -96,6 +97,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderLockerDome, lockerdome.NewLockerDomeSyncer) 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.BidderOpenx, openx.NewOpenxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderPubmatic, pubmatic.NewPubmaticSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderPulsepoint, pulsepoint.NewPulsepointSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 3de64ec1eb0..383e24d82cf 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -43,6 +43,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderLockerDome): syncConfig, string(openrtb_ext.BidderMarsmedia): syncConfig, string(openrtb_ext.BidderMgid): syncConfig, + string(openrtb_ext.BidderNanoInteractive): syncConfig, string(openrtb_ext.BidderOpenx): syncConfig, string(openrtb_ext.BidderPubmatic): syncConfig, string(openrtb_ext.BidderPulsepoint): syncConfig, From fb386190f4491648bb1e8d1b0345a333be1c0393 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 6 Apr 2020 18:53:03 -0400 Subject: [PATCH 045/381] Typos Fix (#1236) * Fix Typo * Fixed More Typos --- adapters/adapterstest/test_json.go | 2 +- analytics/config/config_test.go | 6 +++--- config/config.go | 2 +- config/config_test.go | 4 ++-- config/stored_requests.go | 2 +- docs/bidders/appnexus.md | 2 +- docs/bidders/audienceNetwork.md | 2 +- docs/bidders/sovrn.md | 2 +- docs/developers/automated-tests.md | 2 +- docs/developers/cookie-syncs.md | 2 +- docs/developers/default-request.md | 6 +++--- docs/endpoints/openrtb2/amp.md | 2 +- docs/endpoints/openrtb2/auction.md | 8 +++---- endpoints/openrtb2/amp_auction_test.go | 10 ++++----- endpoints/openrtb2/auction_test.go | 10 ++++----- endpoints/openrtb2/video_auction_test.go | 6 +++--- exchange/bidder.go | 2 +- exchange/exchange_test.go | 2 +- gdpr/gdpr.go | 6 +++--- main.go | 4 +++- openrtb_ext/bid.go | 2 +- openrtb_ext/request.go | 4 ++-- openrtb_ext/request_test.go | 8 +++---- pbsmetrics/metrics.go | 2 +- pbsmetrics/prometheus/prometheus.go | 4 ++-- pbsmetrics/prometheus/prometheus_test.go | 6 +++--- .../aspects/request_timeout_handler_test.go | 21 ++++++++++--------- ssl/ssl_test.go | 2 +- .../backends/db_fetcher/fetcher.go | 2 +- stored_requests/events/events_test.go | 2 +- stored_requests/events/http/http.go | 2 +- stored_requests/fetcher.go | 2 +- 32 files changed, 71 insertions(+), 68 deletions(-) diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go index a0d1954894a..7602ab16e41 100644 --- a/adapters/adapterstest/test_json.go +++ b/adapters/adapterstest/test_json.go @@ -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/analytics/config/config_test.go b/analytics/config/config_test.go index 0fd3ec2019e..7d97fb5f1be 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 index 999b1870b54..2cb5f8f2e66 100644 --- a/config/config.go +++ b/config/config.go @@ -221,7 +221,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. // diff --git a/config/config_test.go b/config/config_test.go index 78630e071d9..9677ce2aaba 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -417,9 +417,9 @@ 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)) } } } 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/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/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/automated-tests.md b/docs/developers/automated-tests.md index 12532237e08..0dff9b04212 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 bd421850d1f..67430e51481 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -228,7 +228,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 @@ -265,7 +265,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. @@ -280,7 +280,7 @@ For example, if the Request defines an alias like this: ``` 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 @@ -495,7 +495,7 @@ client can declare a given adunit as eligible for rewards by declaring `imp.ext. 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** diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index b25d5b0cc8f..9dc81eb1b9d 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -219,7 +219,7 @@ func TestGDPRConsent(t *testing.T) { responseRecorder := httptest.NewRecorder() endpoint(responseRecorder, request, nil) - // Parse Resonse + // Parse Response var response AmpResponse if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { t.Fatalf("Error unmarshalling response: %s", err.Error()) @@ -372,7 +372,7 @@ func TestCCPAConsent(t *testing.T) { responseRecorder := httptest.NewRecorder() endpoint(responseRecorder, request, nil) - // Parse Resonse + // Parse Response var response AmpResponse if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { t.Fatalf("Error unmarshalling response: %s", err.Error()) @@ -431,7 +431,7 @@ func TestNoConsent(t *testing.T) { responseRecorder := httptest.NewRecorder() endpoint(responseRecorder, request, nil) - // Parse Resonse + // Parse Response var response AmpResponse if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { t.Fatalf("Error unmarshalling response: %s", err.Error()) @@ -478,7 +478,7 @@ func TestInvalidConsent(t *testing.T) { responseRecorder := httptest.NewRecorder() endpoint(responseRecorder, request, nil) - // Parse Resonse + // Parse Response var response AmpResponse if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { t.Fatalf("Error unmarshalling response: %s", err.Error()) @@ -561,7 +561,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { responseRecorder := httptest.NewRecorder() endpoint(responseRecorder, request, nil) - // Parse Resonse + // Parse Response var response AmpResponse if err := json.Unmarshal(responseRecorder.Body.Bytes(), &response); err != nil { t.Fatalf("Error unmarshalling response: %s", err.Error()) diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 74a70c69415..98dfa66d6a4 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) } @@ -805,7 +805,7 @@ 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, @@ -839,7 +839,7 @@ 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, @@ -847,7 +847,7 @@ func TestValidateImpExtDisabledBidder(t *testing.T) { } 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) { diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 0199b43f610..d0ce33de1c4 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -44,7 +44,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") @@ -197,7 +197,7 @@ func TestVideoEndpointDebugQueryTrue(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") @@ -239,7 +239,7 @@ func TestVideoEndpointDebugQueryFalse(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") diff --git a/exchange/bidder.go b/exchange/bidder.go index 97f64e74bb5..8e95835ffba 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -208,7 +208,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 } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 7217e609189..2f115ca4f93 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -1762,7 +1762,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/gdpr/gdpr.go b/gdpr/gdpr.go index a6b64203a95..4e36e22fdb9 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -12,17 +12,17 @@ import ( 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. + // 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, error) } diff --git a/main.go b/main.go index ae3b7fd5705..d6ba430f059 100644 --- a/main.go +++ b/main.go @@ -42,9 +42,11 @@ func main() { } } +const configFileName = "pbs" + func loadConfig() (*config.Configuration, error) { v := viper.New() - config.SetupViper(v, "pbs") // filke = filename + config.SetupViper(v, configFileName) return config.New(v) } diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index 768128c96d6..3b297c7ab5d 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -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/request.go b/openrtb_ext/request.go index 9d1456c9618..25b5c881408 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -160,7 +160,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 +175,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 3291c4f9fb2..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") diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index aea9735c276..cc836011efa 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -248,7 +248,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. diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index 7cb80643542..e2b646d5238 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -76,7 +76,7 @@ const ( // 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} metrics := Metrics{} @@ -112,7 +112,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", diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index 4cf9676e1d4..f76480f0852 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") } } diff --git a/router/aspects/request_timeout_handler_test.go b/router/aspects/request_timeout_handler_test.go index b6e10fd64bf..5283d5d51e7 100644 --- a/router/aspects/request_timeout_handler_test.go +++ b/router/aspects/request_timeout_handler_test.go @@ -1,12 +1,13 @@ package aspects import ( - "github.com/julienschmidt/httprouter" - "github.com/prebid/prebid-server/config" "net/http" "net/http/httptest" "testing" + "github.com/julienschmidt/httprouter" + "github.com/prebid/prebid-server/config" + "github.com/stretchr/testify/assert" ) @@ -18,7 +19,7 @@ func TestAny(t *testing.T) { reqTimeInQueue string reqTimeOut string setHeaders bool - extectedRespCode int + expectedRespCode int expectedRespCodeMessage string expectedRespBody string expectedRespBodyMessage string @@ -28,7 +29,7 @@ func TestAny(t *testing.T) { reqTimeInQueue: "6", reqTimeOut: "5", setHeaders: true, - extectedRespCode: http.StatusRequestTimeout, + 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", @@ -38,7 +39,7 @@ func TestAny(t *testing.T) { reqTimeInQueue: "0.9", reqTimeOut: "5", setHeaders: true, - extectedRespCode: http.StatusOK, + expectedRespCode: http.StatusOK, expectedRespCodeMessage: "Http response code is incorrect, should be 200", expectedRespBody: "Executed", expectedRespBodyMessage: "Body should be present in response", @@ -48,7 +49,7 @@ func TestAny(t *testing.T) { reqTimeInQueue: "", reqTimeOut: "", setHeaders: false, - extectedRespCode: http.StatusOK, + expectedRespCode: http.StatusOK, expectedRespCodeMessage: "Http response code is incorrect, should be 200", expectedRespBody: "Executed", expectedRespBodyMessage: "Body should be present in response", @@ -58,7 +59,7 @@ func TestAny(t *testing.T) { reqTimeInQueue: "2", reqTimeOut: "", setHeaders: true, - extectedRespCode: http.StatusOK, + expectedRespCode: http.StatusOK, expectedRespCodeMessage: "Http response code is incorrect, should be 200", expectedRespBody: "Executed", expectedRespBodyMessage: "Body should be present in response", @@ -68,7 +69,7 @@ func TestAny(t *testing.T) { reqTimeInQueue: "test1", reqTimeOut: "test2", setHeaders: true, - extectedRespCode: http.StatusInternalServerError, + expectedRespCode: http.StatusInternalServerError, expectedRespCodeMessage: "Http response code is incorrect, should be 400", expectedRespBody: "Request timeout headers are incorrect (wrong format)", expectedRespBodyMessage: "Body should have error message", @@ -78,7 +79,7 @@ func TestAny(t *testing.T) { reqTimeInQueue: "test1", reqTimeOut: "123", setHeaders: true, - extectedRespCode: http.StatusInternalServerError, + expectedRespCode: http.StatusInternalServerError, expectedRespCodeMessage: "Http response code is incorrect, should be 400", expectedRespBody: "Request timeout headers are incorrect (wrong format)", expectedRespBodyMessage: "Body should have error message", @@ -87,7 +88,7 @@ func TestAny(t *testing.T) { for _, test := range testCases { result := ExecuteAspectRequest(t, test.reqTimeInQueue, test.reqTimeOut, test.setHeaders) - assert.Equal(t, test.extectedRespCode, result.Code, test.expectedRespCodeMessage) + assert.Equal(t, test.expectedRespCode, result.Code, test.expectedRespCodeMessage) assert.Equal(t, test.expectedRespBody, string(result.Body.Bytes()), test.expectedRespBodyMessage) } } 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/stored_requests/backends/db_fetcher/fetcher.go b/stored_requests/backends/db_fetcher/fetcher.go index a8232fd5173..223067c917e 100644 --- a/stored_requests/backends/db_fetcher/fetcher.go +++ b/stored_requests/backends/db_fetcher/fetcher.go @@ -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/events/events_test.go b/stored_requests/events/events_test.go index aaece692bd2..240a697592a 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 4f141dac5cd..a6a129eed42 100644 --- a/stored_requests/events/http/http.go +++ b/stored_requests/events/http/http.go @@ -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/fetcher.go b/stored_requests/fetcher.go index 808495e4584..23fdb6b4925 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) From 1af4a6af23d2b808c383f5a193b8809a1b7cb016 Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Thu, 9 Apr 2020 07:44:38 -0700 Subject: [PATCH 046/381] Moved hb_pc_cat_dur modification to be before caching (#1250) --- exchange/exchange.go | 20 +++++++++++--------- exchange/exchange_test.go | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/exchange/exchange.go b/exchange/exchange.go index 3cab1880456..e625e5ca8f3 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -179,6 +179,12 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque errs = append(errs, errors.New("Unable to marshal response ext for debugging")) } } + + if requestExt.Prebid.SupportDeals { + dealErrs := applyDealSupport(bidRequest, auc, bidCategory) + errs = append(errs, dealErrs...) + } + cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &e.defaultTTLs, bidCategory, debugLog) if len(cacheErrs) > 0 { errs = append(errs, cacheErrs...) @@ -186,10 +192,6 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque targData.setTargeting(auc, bidRequest.App != nil, bidCategory) } - if requestExt.Prebid.SupportDeals { - dealErrs := applyDealSupport(bidRequest, auc) - errs = append(errs, dealErrs...) - } } // Build the response @@ -210,7 +212,7 @@ type BidderDealTier struct { } // applyDealSupport updates targeting keys with deal prefixes if minimum deal tier exceeded -func applyDealSupport(bidRequest *openrtb.BidRequest, auc *auction) []error { +func applyDealSupport(bidRequest *openrtb.BidRequest, auc *auction, bidCategory map[string]string) []error { errs := []error{} impDealMap := getDealTiers(bidRequest) @@ -221,7 +223,7 @@ func applyDealSupport(bidRequest *openrtb.BidRequest, auc *auction) []error { if topBidPerBidder.dealPriority > 0 { if validateAndNormalizeDealTier(impDeal[bidderString]) { - updateHbPbCatDur(topBidPerBidder, impDeal[bidderString].Info) + updateHbPbCatDur(topBidPerBidder, impDeal[bidderString].Info, bidCategory) } else { errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", bidderString, impID)) } @@ -258,16 +260,16 @@ func validateAndNormalizeDealTier(impDeal *DealTier) bool { return len(impDeal.Info.Prefix) > 0 && impDeal.Info.MinDealTier > 0 } -func updateHbPbCatDur(bid *pbsOrtbBid, dealTierInfo *DealTierInfo) { +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 := bid.bidTargets["hb_pb_cat_dur"]; ok { + if oldCatDur, ok := bidCategory[bid.bid.ID]; ok { oldCatDurSplit := strings.SplitAfterN(oldCatDur, "_", 2) oldCatDurSplit[0] = prefixTier newCatDur := strings.Join(oldCatDurSplit, "") - bid.bidTargets["hb_pb_cat_dur"] = newCatDur + bidCategory[bid.bid.ID] = newCatDur } } } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 2f115ca4f93..f263eea8569 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -1403,7 +1403,10 @@ func TestApplyDealSupport(t *testing.T) { }, } - bid := pbsOrtbBid{&openrtb.Bid{}, "video", test.targ, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority} + 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{ @@ -1413,9 +1416,9 @@ func TestApplyDealSupport(t *testing.T) { }, } - dealErrs := applyDealSupport(bidRequest, auc) + dealErrs := applyDealSupport(bidRequest, auc, bidCategory) - assert.Equal(t, test.expectedHbPbCatDur, auc.winningBidsByBidder["imp_id1"][bidderName].bidTargets["hb_pb_cat_dur"], test.description) + 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") } @@ -1590,11 +1593,14 @@ func TestUpdateHbPbCatDur(t *testing.T) { } for _, test := range testCases { - bid := pbsOrtbBid{&openrtb.Bid{}, "video", test.targ, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority} + 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) + updateHbPbCatDur(&bid, test.dealTier, bidCategory) - assert.Equal(t, test.expectedHbPbCatDur, bid.bidTargets["hb_pb_cat_dur"], test.description) + assert.Equal(t, test.expectedHbPbCatDur, bidCategory[bid.bid.ID], test.description) } } From 733b40d71f38d8b386b3d2bbce02e49476aaa91a Mon Sep 17 00:00:00 2001 From: bretg Date: Mon, 13 Apr 2020 11:21:52 -0400 Subject: [PATCH 047/381] replacing info@prebid.org maintainer email addrs (#1256) --- static/bidder-info/appnexus.yaml | 2 +- static/bidder-info/audienceNetwork.yaml | 2 +- static/bidder-info/ix.yaml | 2 +- static/bidder-info/pulsepoint.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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/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: From 2b334afb690c297f9e72d6d7f466a7fb5fe75518 Mon Sep 17 00:00:00 2001 From: bretg Date: Tue, 14 Apr 2020 12:26:39 -0400 Subject: [PATCH 048/381] aligning maintainer info (#1258) --- static/bidder-info/33across.yaml | 4 ++-- static/bidder-info/adoppler.yaml | 2 +- static/bidder-info/advangelists.yaml | 2 +- static/bidder-info/brightroll.yaml | 2 +- static/bidder-info/conversant.yaml | 2 +- static/bidder-info/datablocks.yaml | 2 +- static/bidder-info/engagebdr.yaml | 2 +- static/bidder-info/gamoshi.yaml | 2 +- static/bidder-info/gumgum.yaml | 2 +- static/bidder-info/openx.yaml | 2 +- static/bidder-info/rtbhouse.yaml | 2 +- static/bidder-info/sonobi.yaml | 2 +- static/bidder-info/verizonmedia.yaml | 4 ++-- static/bidder-info/visx.yaml | 2 +- static/bidder-info/yieldmo.yaml | 2 +- static/bidder-info/zeroclickfraud.yaml | 2 +- 16 files changed, 18 insertions(+), 18 deletions(-) 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/adoppler.yaml b/static/bidder-info/adoppler.yaml index 7fa79eda163..1b10103923e 100644 --- a/static/bidder-info/adoppler.yaml +++ b/static/bidder-info/adoppler.yaml @@ -1,5 +1,5 @@ maintainer: - email: info@adoppler.com + email: pbs@adoppler.com capabilities: app: mediaTypes: 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/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/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/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/openx.yaml b/static/bidder-info/openx.yaml index ce2b67db7da..e3062b54fba 100644 --- a/static/bidder-info/openx.yaml +++ b/static/bidder-info/openx.yaml @@ -1,5 +1,5 @@ maintainer: - email: "team-openx@openx.com" + email: "prebid@openx.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/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/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 f404a013337..b6a16e4c2d0 100644 --- a/static/bidder-info/visx.yaml +++ b/static/bidder-info/visx.yaml @@ -1,5 +1,5 @@ maintainer: - email: "service@yoc.com" + email: "supply.partners@yoc.com" capabilities: site: mediaTypes: 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/zeroclickfraud.yaml b/static/bidder-info/zeroclickfraud.yaml index 9bf7e780914..527c0065600 100644 --- a/static/bidder-info/zeroclickfraud.yaml +++ b/static/bidder-info/zeroclickfraud.yaml @@ -1,5 +1,5 @@ maintainer: - email: "henry@datablocks.net" + email: "support@datablocks.net" capabilities: app: mediaTypes: From fb59f73a044e5f15d31e9ae8e9cc81eea62e6fa9 Mon Sep 17 00:00:00 2001 From: bretg Date: Tue, 14 Apr 2020 12:32:07 -0400 Subject: [PATCH 049/381] Add kidoz bidder info (#1257) got this info from email communication with kidoz --- docs/bidders/kidoz.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/bidders/kidoz.md 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. From c027bac4f780a33d3bd0645949e88d06e6fd3c6e Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 14 Apr 2020 21:49:38 +0300 Subject: [PATCH 050/381] Add Cropping of BAdv for Rubicon Adapter (#1254) * Add Cropping of BAdv for Rubicon Adapter BAdv size is limited to 50 * Fix after review Co-authored-by: Harbar Dmytro --- adapters/rubicon/rubicon.go | 9 +++++++ adapters/rubicon/rubicon_test.go | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 46caf262108..dad85ee1184 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -21,6 +21,8 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) +const badvLimitSize = 50 + type RubiconAdapter struct { http *adapters.HTTPAdapter URI string @@ -740,6 +742,13 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap request.App = &appCopy } + reqBadv := request.BAdv + if reqBadv != nil { + if len(reqBadv) > badvLimitSize { + request.BAdv = reqBadv[:badvLimitSize] + } + } + request.Imp = []openrtb.Imp{thisImp} request.Cur = nil request.Ext = nil diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index d386daed5b1..96623659d08 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "strconv" "testing" "time" @@ -1133,6 +1134,48 @@ func TestOpenRTBRequestWithImpAndAdSlotIncluded(t *testing.T) { "Unexpected dfp_ad_unit_code: %s", rubiconExtInventory["dfp_ad_unit_code"]) } +func TestOpenRTBRequestWithBadvOverflowed(t *testing.T) { + SIZE_ID := getTestSizes() + bidder := new(RubiconAdapter) + + badvOverflowed := make([]string, 100) + for i := range badvOverflowed { + badvOverflowed[i] = strconv.Itoa(i) + } + + request := &openrtb.BidRequest{ + ID: "test-request-id", + BAdv: badvOverflowed, + Imp: []openrtb.Imp{{ + ID: "test-imp-id", + Banner: &openrtb.Banner{ + Format: []openrtb.Format{ + SIZE_ID[15], + }, + }, + Ext: json.RawMessage(`{ + "bidder": { + "zoneId": 8394, + "siteId": 283282, + "accountId": 7891, + "inventory": {"key1" : "val1"}, + "visitor": {"key2" : "val2"} + } + }`), + }}, + } + + reqs, _ := bidder.MakeRequests(request, &adapters.ExtraRequestInfo{}) + + rubiconReq := &openrtb.BidRequest{} + if err := json.Unmarshal(reqs[0].Body, rubiconReq); err != nil { + t.Fatalf("Unexpected error while decoding request: %s", err) + } + + badvRequest := rubiconReq.BAdv + assert.Equal(t, badvOverflowed[:50], badvRequest, "Unexpected dfp_ad_unit_code: %s") +} + func TestOpenRTBRequestWithSpecificExtUserEids(t *testing.T) { SIZE_ID := getTestSizes() bidder := new(RubiconAdapter) From d416035355bd7293f54f195a78f386a827b95239 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Wed, 15 Apr 2020 08:33:43 -0700 Subject: [PATCH 051/381] Added metrics support to endpoint aspect (#1226) Co-authored-by: Veronika Solovei --- pbsmetrics/config/metrics.go | 11 ++++ pbsmetrics/config/metrics_test.go | 6 ++ pbsmetrics/go_metrics.go | 18 ++++++ pbsmetrics/go_metrics_test.go | 3 + pbsmetrics/metrics.go | 13 ++-- pbsmetrics/metrics_mock.go | 5 ++ pbsmetrics/prometheus/preload.go | 8 +++ pbsmetrics/prometheus/prometheus.go | 24 ++++++++ pbsmetrics/prometheus/prometheus_test.go | 60 +++++++++++++++++++ router/aspects/request_timeout_handler.go | 8 ++- .../aspects/request_timeout_handler_test.go | 44 ++++++-------- router/router.go | 3 +- 12 files changed, 170 insertions(+), 33 deletions(-) diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index 81cfbfd0798..e1cdaceb0e5 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -181,6 +181,13 @@ 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) + } +} + // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests) type DummyMetricsEngine struct{} @@ -251,3 +258,7 @@ 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) { +} diff --git a/pbsmetrics/config/metrics_test.go b/pbsmetrics/config/metrics_test.go index ad817ba75a9..d2374f95195 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 9b3dd65ff4e..ff3d9681fb1 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 @@ -111,6 +112,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), @@ -146,6 +148,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,11 +198,15 @@ 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) return newMetrics @@ -526,6 +537,13 @@ 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 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 b403733dcc7..253ff69e3c2 100644 --- a/pbsmetrics/go_metrics_test.go +++ b/pbsmetrics/go_metrics_test.go @@ -50,6 +50,9 @@ 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]) } func TestRecordBidType(t *testing.T) { diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index cc836011efa..611692c9c01 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, } } @@ -272,4 +274,5 @@ 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) } diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go index 6d57f9fcfaa..1f5b84b1e0f 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -96,3 +96,8 @@ 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) +} diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go index 7654dd54f82..11e6bdc14d8 100644 --- a/pbsmetrics/prometheus/preload.go +++ b/pbsmetrics/prometheus/preload.go @@ -1,6 +1,7 @@ package prometheusmetrics import ( + "github.com/prebid/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 e2b646d5238..d66defea4cd 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -24,6 +24,7 @@ type Metrics struct { prebidCacheWriteTimer *prometheus.HistogramVec requests *prometheus.CounterVec requestsTimer *prometheus.HistogramVec + requestsQueueTimer *prometheus.HistogramVec requestsWithoutCookie *prometheus.CounterVec storedImpressionsCacheResult *prometheus.CounterVec storedRequestCacheResult *prometheus.CounterVec @@ -73,11 +74,17 @@ const ( markupDeliveryNurl = "nurl" ) +const ( + requestSuccessLabel = "requestAcceptedLabel" + requestRejectLabel = "requestRejectedLabel" +) + // 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} 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() @@ -187,6 +194,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 +387,14 @@ 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()) +} diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index f76480f0852..e4d6a4f78d1 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -881,6 +881,48 @@ 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 assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) { m := dto.Metric{} counter.Write(&m) @@ -906,6 +948,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/router/aspects/request_timeout_handler.go b/router/aspects/request_timeout_handler.go index ae11f8c5614..23d6cef9faf 100644 --- a/router/aspects/request_timeout_handler.go +++ b/router/aspects/request_timeout_handler.go @@ -3,11 +3,13 @@ package aspects import ( "github.com/julienschmidt/httprouter" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/pbsmetrics" "net/http" "strconv" + "time" ) -func QueuedRequestTimeout(f httprouter.Handle, reqTimeoutHeaders config.RequestTimeoutHeaders) httprouter.Handle { +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) { @@ -30,13 +32,17 @@ func QueuedRequestTimeout(f httprouter.Handle, reqTimeoutHeaders config.RequestT 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 index 5283d5d51e7..cdc920c4263 100644 --- a/router/aspects/request_timeout_handler_test.go +++ b/router/aspects/request_timeout_handler_test.go @@ -1,12 +1,14 @@ package aspects import ( + "github.com/julienschmidt/httprouter" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/pbsmetrics" "net/http" "net/http/httptest" + "strconv" "testing" - - "github.com/julienschmidt/httprouter" - "github.com/prebid/prebid-server/config" + "time" "github.com/stretchr/testify/assert" ) @@ -23,6 +25,7 @@ func TestAny(t *testing.T) { expectedRespCodeMessage string expectedRespBody string expectedRespBodyMessage string + requestStatusMetrics bool }{ { //TestQueuedRequestTimeoutWithTimeout @@ -33,6 +36,7 @@ func TestAny(t *testing.T) { 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 @@ -43,6 +47,7 @@ func TestAny(t *testing.T) { expectedRespCodeMessage: "Http response code is incorrect, should be 200", expectedRespBody: "Executed", expectedRespBodyMessage: "Body should be present in response", + requestStatusMetrics: true, }, { //TestQueuedRequestNoHeaders @@ -53,6 +58,7 @@ func TestAny(t *testing.T) { expectedRespCodeMessage: "Http response code is incorrect, should be 200", expectedRespBody: "Executed", expectedRespBodyMessage: "Body should be present in response", + requestStatusMetrics: true, }, { //TestQueuedRequestSomeHeaders @@ -63,31 +69,13 @@ func TestAny(t *testing.T) { expectedRespCodeMessage: "Http response code is incorrect, should be 200", expectedRespBody: "Executed", expectedRespBodyMessage: "Body should be present in response", - }, - { - //TestQueuedRequestAllHeadersIncorrect - reqTimeInQueue: "test1", - reqTimeOut: "test2", - setHeaders: true, - expectedRespCode: http.StatusInternalServerError, - expectedRespCodeMessage: "Http response code is incorrect, should be 400", - expectedRespBody: "Request timeout headers are incorrect (wrong format)", - expectedRespBodyMessage: "Body should have error message", - }, - { - //TestQueuedRequestSomeHeadersIncorrect - reqTimeInQueue: "test1", - reqTimeOut: "123", - setHeaders: true, - expectedRespCode: http.StatusInternalServerError, - expectedRespCodeMessage: "Http response code is incorrect, should be 400", - expectedRespBody: "Request timeout headers are incorrect (wrong format)", - expectedRespBodyMessage: "Body should have error message", + requestStatusMetrics: true, }, } for _, test := range testCases { - result := ExecuteAspectRequest(t, test.reqTimeInQueue, test.reqTimeOut, test.setHeaders) + 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) } @@ -101,7 +89,7 @@ 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) *httptest.ResponseRecorder { +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 { @@ -114,7 +102,11 @@ func ExecuteAspectRequest(t *testing.T, timeInQueue string, reqTimeout string, s customHeaders := config.RequestTimeoutHeaders{reqTimeInQueueHeaderName, reqTimeoutHeaderName} - handler := QueuedRequestTimeout(MockEndpoint(), customHeaders) + 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) diff --git a/router/router.go b/router/router.go index 8ac463b85a0..68627f937dd 100644 --- a/router/router.go +++ b/router/router.go @@ -6,6 +6,7 @@ import ( "database/sql" "encoding/json" "fmt" + "github.com/prebid/prebid-server/pbsmetrics" "io/ioutil" "net/http" "path/filepath" @@ -258,7 +259,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r requestTimeoutHeaders := config.RequestTimeoutHeaders{} if cfg.RequestTimeoutHeaders != requestTimeoutHeaders { - videoEndpoint = aspects.QueuedRequestTimeout(videoEndpoint, cfg.RequestTimeoutHeaders) + videoEndpoint = aspects.QueuedRequestTimeout(videoEndpoint, cfg.RequestTimeoutHeaders, r.MetricsEngine, pbsmetrics.ReqTypeVideo) } r.POST("/auction", endpoints.Auction(cfg, syncers, gdprPerms, r.MetricsEngine, dataCache, exchanges)) From cc7a247bee26b46c53d6bf7473b9146ce8f8ef57 Mon Sep 17 00:00:00 2001 From: Telaria Engineering <36203956+telariaEng@users.noreply.github.com> Date: Wed, 15 Apr 2020 10:21:16 -0700 Subject: [PATCH 052/381] Prebid Server adapter for Telaria (#1231) * TELARIA adapter. First Pass * Some refactoring * added the json files * fixed some tests and added the bidder info * fixed some tests and added the bidder info * added default user sync ur; * - Handling gzipped responses from our server * - more refactoring. * added the proper user sync default URL * changed the urls from dev to prod * changed up the required fields. Now AdCode in the Imp.Ext isn't required but Bid.SeatCode is required * change in the return type after decompressing * some refactoring * change in our config url * using pbs.yml to switch between our production and test URLs * setting default endpoint * - fixed the issue that was preventing telaria test cases to run. - added more test cases * - Modifications as per the changes requested by the maintainers. * Moved the seat code to imp.ext * Moved the seat code to imp.ext * Added 'Telaria: ' prefix for error messages * - Fixes for race conditions. Was modifying the original request object instead of a copy * cosmetic changes. * added params_test.go Co-authored-by: Vinay Prasad --- adapters/telaria/params_test.go | 50 +++ adapters/telaria/telaria.go | 330 ++++++++++++++++++ adapters/telaria/telaria_test.go | 34 ++ .../telariatest/exemplary/video-app.json | 157 +++++++++ .../telariatest/exemplary/video-web.json | 145 ++++++++ .../telariatest/params/race/video.json | 4 + .../supplemental/banner-unsupported.json | 42 +++ .../supplemental/invalid-response.json | 105 ++++++ .../invalid-telaria-ext-object.json | 29 ++ .../supplemental/requires-imp-object.json | 16 + .../supplemental/requires-seat-code.json | 30 ++ .../supplemental/requires-video-object.json | 26 ++ .../supplemental/status-code-bad-request.json | 80 +++++ .../supplemental/status-code-no-content.json | 83 +++++ .../supplemental/status-code-other-error.json | 83 +++++ .../status-code-service-unavailable.json | 83 +++++ adapters/telaria/usersync.go | 12 + adapters/telaria/usersync_test.go | 33 ++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_telaria.go | 6 + static/bidder-info/telaria.yaml | 9 + static/bidder-params/telaria.json | 22 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 26 files changed, 1388 insertions(+) create mode 100644 adapters/telaria/params_test.go create mode 100644 adapters/telaria/telaria.go create mode 100644 adapters/telaria/telaria_test.go create mode 100644 adapters/telaria/telariatest/exemplary/video-app.json create mode 100644 adapters/telaria/telariatest/exemplary/video-web.json create mode 100644 adapters/telaria/telariatest/params/race/video.json create mode 100644 adapters/telaria/telariatest/supplemental/banner-unsupported.json create mode 100644 adapters/telaria/telariatest/supplemental/invalid-response.json create mode 100644 adapters/telaria/telariatest/supplemental/invalid-telaria-ext-object.json create mode 100644 adapters/telaria/telariatest/supplemental/requires-imp-object.json create mode 100644 adapters/telaria/telariatest/supplemental/requires-seat-code.json create mode 100644 adapters/telaria/telariatest/supplemental/requires-video-object.json create mode 100644 adapters/telaria/telariatest/supplemental/status-code-bad-request.json create mode 100644 adapters/telaria/telariatest/supplemental/status-code-no-content.json create mode 100644 adapters/telaria/telariatest/supplemental/status-code-other-error.json create mode 100644 adapters/telaria/telariatest/supplemental/status-code-service-unavailable.json create mode 100644 adapters/telaria/usersync.go create mode 100644 adapters/telaria/usersync_test.go create mode 100644 openrtb_ext/imp_telaria.go create mode 100644 static/bidder-info/telaria.yaml create mode 100644 static/bidder-params/telaria.json diff --git a/adapters/telaria/params_test.go b/adapters/telaria/params_test.go new file mode 100644 index 00000000000..efa3fba1be9 --- /dev/null +++ b/adapters/telaria/params_test.go @@ -0,0 +1,50 @@ +package telaria + +import ( + "encoding/json" + "github.com/prebid/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.BidderTelaria, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected Telaria params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the Telaria 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.BidderTelaria, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"adCode": "string", "seatCode": "string", "originalPublisherid": "string"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"adCode": "string", "originalPublisherid": "string"}`, + `{"adCode": "string", "seatCode": 5, "originalPublisherid": "string"}`, +} diff --git a/adapters/telaria/telaria.go b/adapters/telaria/telaria.go new file mode 100644 index 00000000000..9edafa86a32 --- /dev/null +++ b/adapters/telaria/telaria.go @@ -0,0 +1,330 @@ +package telaria + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" + "strconv" +) + +const Endpoint = "https://ads.tremorhub.com/ad/rtb/prebid" + +type TelariaAdapter struct { + URI string +} + +// This will be part of Imp[i].Ext when this adapter calls out the Telaria Ad Server +type ImpressionExtOut struct { + OriginalTagID string `json:"originalTagid"` + OriginalPublisherID string `json:"originalPublisherid"` +} + +// used for cookies and such +func (a *TelariaAdapter) Name() string { + return "telaria" +} + +func (a *TelariaAdapter) SkipNoCookies() bool { + return false +} + +// Endpoint for Telaria Ad server +func (a *TelariaAdapter) FetchEndpoint() string { + return a.URI +} + +// Checker method to ensure len(request.Imp) > 0 +func (a *TelariaAdapter) CheckHasImps(request *openrtb.BidRequest) error { + if len(request.Imp) == 0 { + err := &errortypes.BadInput{ + Message: "Telaria: Missing Imp Object", + } + return err + } + return nil +} + +// Checking if Imp[i].Video exists and Imp[i].Banner doesn't exist +func (a *TelariaAdapter) CheckHasVideoObject(request *openrtb.BidRequest) error { + hasVideoObject := false + + for _, imp := range request.Imp { + if imp.Banner != nil { + return &errortypes.BadInput{ + Message: "Telaria: Banner not supported", + } + } + + hasVideoObject = hasVideoObject || imp.Video != nil + } + + if !hasVideoObject { + return &errortypes.BadInput{ + Message: "Telaria: Only Supports Video", + } + } + + return nil +} + +// Fetches the populated header object +func GetHeaders(request *openrtb.BidRequest) *http.Header { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + headers.Add("Accept-Encoding", "gzip") + + if request.Device != nil { + if len(request.Device.UA) > 0 { + headers.Add("User-Agent", request.Device.UA) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Forwarded-For", request.Device.IP) + } + + if len(request.Device.Language) > 0 { + headers.Add("Accept-Language", request.Device.Language) + } + + if request.Device.DNT != nil { + headers.Add("Dnt", strconv.Itoa(int(*request.Device.DNT))) + } + } + + return &headers +} + +// Checks the imp[i].ext object and returns a imp.ext object as per ExtImpTelaria format +func (a *TelariaAdapter) FetchTelariaExtImpParams(imp *openrtb.Imp) (*openrtb_ext.ExtImpTelaria, error) { + var bidderExt adapters.ExtImpBidder + err := json.Unmarshal(imp.Ext, &bidderExt) + + if err != nil { + err = &errortypes.BadInput{ + Message: "Telaria: ext.bidder not provided", + } + + return nil, err + } + + var telariaExt openrtb_ext.ExtImpTelaria + err = json.Unmarshal(bidderExt.Bidder, &telariaExt) + + if err != nil { + return nil, err + } + + if telariaExt.SeatCode == "" { + return nil, &errortypes.BadInput{Message: "Telaria: Seat Code required"} + } + + return &telariaExt, nil +} + +// Method to fetch the original publisher ID. Note that this method must be called +// before we replace publisher.ID with seatCode +func (a *TelariaAdapter) FetchOriginalPublisherID(request *openrtb.BidRequest) string { + + if request.Site != nil && request.Site.Publisher != nil { + return request.Site.Publisher.ID + } else if request.App != nil && request.App.Publisher != nil { + return request.App.Publisher.ID + } + + return "" +} + +// Method to do a deep copy of the publisher object. It also adds the seatCode as publisher.ID +func (a *TelariaAdapter) MakePublisherObject(seatCode string, publisher *openrtb.Publisher) *openrtb.Publisher { + var pub = &openrtb.Publisher{ID: seatCode} + + if publisher != nil { + pub.Domain = publisher.Domain + pub.Name = publisher.Name + pub.Cat = publisher.Cat + pub.Ext = publisher.Ext + } + + return pub +} + +// This method changes .publisher.id to the seatCode +func (a *TelariaAdapter) PopulatePublisherId(request *openrtb.BidRequest, seatCode string) (*openrtb.Site, *openrtb.App) { + if request.Site != nil { + siteCopy := *request.Site + siteCopy.Publisher = a.MakePublisherObject(seatCode, request.Site.Publisher) + return &siteCopy, nil + } else if request.App != nil { + appCopy := *request.App + appCopy.Publisher = a.MakePublisherObject(seatCode, request.App.Publisher) + return nil, &appCopy + } + return nil, nil +} + +func (a *TelariaAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + // make a copy of the incoming request + request := *requestIn + + // ensure that the request has Impressions + if noImps := a.CheckHasImps(&request); noImps != nil { + return nil, []error{noImps} + } + + // ensure that the request has a Video object + if noVideoObjectError := a.CheckHasVideoObject(&request); noVideoObjectError != nil { + return nil, []error{noVideoObjectError} + } + + var seatCode string + originalPublisherID := a.FetchOriginalPublisherID(&request) + + var errors []error + for i, imp := range request.Imp { + // fetch adCode & seatCode from Imp[i].Ext + telariaExt, err := a.FetchTelariaExtImpParams(&imp) + if err != nil { + errors = append(errors, err) + break + } + + seatCode = telariaExt.SeatCode + + // move the original tagId and the original publisher.id into the Imp[i].Ext object + request.Imp[i].Ext, err = json.Marshal(&ImpressionExtOut{request.Imp[i].TagID, originalPublisherID}) + if err != nil { + errors = append(errors, err) + break + } + + // Swap the tagID with adCode + request.Imp[i].TagID = telariaExt.AdCode + } + + if len(errors) > 0 { + return nil, errors + } + + // Add seatCode to .Publisher.ID + siteObject, appObject := a.PopulatePublisherId(&request, seatCode) + + request.Site = siteObject + request.App = appObject + + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + return []*adapters.RequestData{{ + Method: "POST", + Uri: a.FetchEndpoint(), + Body: reqJSON, + Headers: *GetHeaders(&request), + }}, nil +} + +// response isn't automatically decompressed. This method unzips the response if Content-Encoding is gzip +func GetResponseBody(response *adapters.ResponseData) ([]byte, error) { + + if "gzip" == response.Headers.Get("Content-Encoding") { + body := bytes.NewBuffer(response.Body) + r, readerErr := gzip.NewReader(body) + if readerErr != nil { + return nil, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Error while trying to unzip data [ %d ]", response.StatusCode), + } + } + var resB bytes.Buffer + var err error + _, err = resB.ReadFrom(r) + if err != nil { + return nil, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Error while trying to unzip data [ %d ]", response.StatusCode), + } + } + + response.Headers.Del("Content-Encoding") + + return resB.Bytes(), nil + } else { + return response.Body, nil + } +} + +func (a *TelariaAdapter) CheckResponseStatusCodes(response *adapters.ResponseData) error { + if response.StatusCode == http.StatusNoContent { + return &errortypes.BadInput{Message: "Telaria: Invalid Bid Request received by the server"} + } + + if response.StatusCode == http.StatusBadRequest { + return &errortypes.BadInput{ + Message: fmt.Sprintf("Telaria: Unexpected status code: [ %d ] ", response.StatusCode), + } + } + + if response.StatusCode == http.StatusServiceUnavailable { + return &errortypes.BadInput{ + Message: fmt.Sprintf("Telaria: Something went wrong, please contact your Account Manager. Status Code: [ %d ] ", response.StatusCode), + } + } + + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + return &errortypes.BadInput{ + Message: fmt.Sprintf("Telaria: Something went wrong, please contact your Account Manager. Status Code: [ %d ] ", response.StatusCode), + } + } + + return nil +} + +func (a *TelariaAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + httpStatusError := a.CheckResponseStatusCodes(response) + if httpStatusError != nil { + return nil, []error{httpStatusError} + } + + responseBody, err := GetResponseBody(response) + + if err != nil { + return nil, []error{err} + } + + var bidResp openrtb.BidResponse + if err := json.Unmarshal(responseBody, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Telaria: Bad Server Response", + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + sb := bidResp.SeatBid[0] + + for _, bid := range sb.Bid { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: openrtb_ext.BidTypeVideo, + }) + } + return bidResponse, nil +} + +func NewTelariaBidder(endpoint string) *TelariaAdapter { + if endpoint == "" { + endpoint = Endpoint + } + + return &TelariaAdapter{ + URI: endpoint, + } +} diff --git a/adapters/telaria/telaria_test.go b/adapters/telaria/telaria_test.go new file mode 100644 index 00000000000..7ad96b9307b --- /dev/null +++ b/adapters/telaria/telaria_test.go @@ -0,0 +1,34 @@ +package telaria + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "testing" +) + +/** + * Verify adapter names are setup correctly. + */ +func TestTelariaAdapterNames(t *testing.T) { + adapter := NewTelariaBidder("") + adapterstest.VerifyStringValue(adapter.Name(), "telaria", t) +} + +/** + * Verify adapter SkipNoCookie is correct. + */ +func TestTelariaAdapterSkipNoCookiesFlag(t *testing.T) { + adapter := NewTelariaBidder("") + adapterstest.VerifyBoolValue(adapter.SkipNoCookies(), false, t) +} + +/** + * Verify bidder has the proper URL + */ +func TestTelariaAdapterEndpoint(t *testing.T) { + adapter := NewTelariaBidder("") + adapterstest.VerifyStringValue(adapter.URI, "https://ads.tremorhub.com/ad/rtb/prebid", t) +} + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "telariatest", NewTelariaBidder("")) +} diff --git a/adapters/telaria/telariatest/exemplary/video-app.json b/adapters/telaria/telariatest/exemplary/video-app.json new file mode 100644 index 00000000000..09bcf998454 --- /dev/null +++ b/adapters/telaria/telariatest/exemplary/video-app.json @@ -0,0 +1,157 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": ["2.5"], + "Accept-Encoding": ["gzip"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["123.123.123.123"], + "Accept-Language": ["en"], + "Dnt": ["0"] + }, + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "my-adcode", + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [{ + "bid": [{ + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + }], + "seat": "telaria" + }], + "cur": "USD", + "ext": { + "responsetimemillis": { + "telaria": 154 + }, + "tmaxrequest": 1000 + } + } + } + }], + "expectedBids": [{ + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + }] +} diff --git a/adapters/telaria/telariatest/exemplary/video-web.json b/adapters/telaria/telariatest/exemplary/video-web.json new file mode 100644 index 00000000000..5dc26b0f018 --- /dev/null +++ b/adapters/telaria/telariatest/exemplary/video-web.json @@ -0,0 +1,145 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": ["2.5"], + "Accept-Encoding": ["gzip"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["123.123.123.123"], + "Accept-Language": ["en"], + "Dnt": ["0"] + }, + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [{ + "bid": [{ + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + }], + "seat": "telaria" + }], + "cur": "USD", + "ext": { + "responsetimemillis": { + "telaria": 154 + }, + "tmaxrequest": 1000 + } + } + } + }], + "expectedBids": [{ + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + }] +} diff --git a/adapters/telaria/telariatest/params/race/video.json b/adapters/telaria/telariatest/params/race/video.json new file mode 100644 index 00000000000..e3b67ec8c20 --- /dev/null +++ b/adapters/telaria/telariatest/params/race/video.json @@ -0,0 +1,4 @@ +{ + "adCode": "my-adcode", + "seatCode": "my-seatcode" +} diff --git a/adapters/telaria/telariatest/supplemental/banner-unsupported.json b/adapters/telaria/telariatest/supplemental/banner-unsupported.json new file mode 100644 index 00000000000..1e75371dd22 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/banner-unsupported.json @@ -0,0 +1,42 @@ + +{ + "expectedMakeRequestsErrors": [ + { + "value": "Telaria: Banner not supported", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.com", + "publisher": { + "id": "someother-publisher-id" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + } +} diff --git a/adapters/telaria/telariatest/supplemental/invalid-response.json b/adapters/telaria/telariatest/supplemental/invalid-response.json new file mode 100644 index 00000000000..8e4f1ee8cb5 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/invalid-response.json @@ -0,0 +1,105 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"], + "X-Openrtb-Version": ["2.5"], + "Accept-Encoding": ["gzip"], + "User-Agent": ["test-user-agent"], + "X-Forwarded-For": ["123.123.123.123"], + "Accept-Language": ["en"], + "Dnt": ["0"] + }, + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": "invalid response" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Telaria: Bad Server Response", + "comparison": "literal" + } + ] +} diff --git a/adapters/telaria/telariatest/supplemental/invalid-telaria-ext-object.json b/adapters/telaria/telariatest/supplemental/invalid-telaria-ext-object.json new file mode 100644 index 00000000000..efdec8ad61b --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/invalid-telaria-ext-object.json @@ -0,0 +1,29 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Telaria: ext.bidder not provided", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": "Awesome" + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} diff --git a/adapters/telaria/telariatest/supplemental/requires-imp-object.json b/adapters/telaria/telariatest/supplemental/requires-imp-object.json new file mode 100644 index 00000000000..5933dc4ee08 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/requires-imp-object.json @@ -0,0 +1,16 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Telaria: Missing Imp Object", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} diff --git a/adapters/telaria/telariatest/supplemental/requires-seat-code.json b/adapters/telaria/telariatest/supplemental/requires-seat-code.json new file mode 100644 index 00000000000..5c05e529772 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/requires-seat-code.json @@ -0,0 +1,30 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Telaria: Seat Code required", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-1", + "video": { + "w": 640, + "h": 480, + "linearity": 1 + }, + "ext": { + "bidder": { + "adCode": "my-adcode" + } + } + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} diff --git a/adapters/telaria/telariatest/supplemental/requires-video-object.json b/adapters/telaria/telariatest/supplemental/requires-video-object.json new file mode 100644 index 00000000000..3f797c9e1de --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/requires-video-object.json @@ -0,0 +1,26 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "Telaria: Only Supports Video", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-impression-1", + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} diff --git a/adapters/telaria/telariatest/supplemental/status-code-bad-request.json b/adapters/telaria/telariatest/supplemental/status-code-bad-request.json new file mode 100644 index 00000000000..0b5d8a85982 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/status-code-bad-request.json @@ -0,0 +1,80 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "" + } + } + ], + "app": { + "id": "123456789", + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 400 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Telaria: Unexpected status code: [ 400 ] ", + "comparison": "literal" + } + ] +} diff --git a/adapters/telaria/telariatest/supplemental/status-code-no-content.json b/adapters/telaria/telariatest/supplemental/status-code-no-content.json new file mode 100644 index 00000000000..ffb183f4121 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/status-code-no-content.json @@ -0,0 +1,83 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 204 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Telaria: Invalid Bid Request received by the server", + "comparison": "literal" + } + ] +} diff --git a/adapters/telaria/telariatest/supplemental/status-code-other-error.json b/adapters/telaria/telariatest/supplemental/status-code-other-error.json new file mode 100644 index 00000000000..15e4b7f87d8 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/status-code-other-error.json @@ -0,0 +1,83 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 306 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Telaria: Something went wrong, please contact your Account Manager. Status Code: [ 306 ] ", + "comparison": "literal" + } + ] +} diff --git a/adapters/telaria/telariatest/supplemental/status-code-service-unavailable.json b/adapters/telaria/telariatest/supplemental/status-code-service-unavailable.json new file mode 100644 index 00000000000..b92d4ea8ba1 --- /dev/null +++ b/adapters/telaria/telariatest/supplemental/status-code-service-unavailable.json @@ -0,0 +1,83 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "adCode": "my-adcode", + "seatCode": "my-seatcode" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 503 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Telaria: Something went wrong, please contact your Account Manager. Status Code: [ 503 ] ", + "comparison": "literal" + } + ] +} diff --git a/adapters/telaria/usersync.go b/adapters/telaria/usersync.go new file mode 100644 index 00000000000..e3f76f6e9b4 --- /dev/null +++ b/adapters/telaria/usersync.go @@ -0,0 +1,12 @@ +package telaria + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewTelariaSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("telaria", 202, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/telaria/usersync_test.go b/adapters/telaria/usersync_test.go new file mode 100644 index 00000000000..4896b253d2f --- /dev/null +++ b/adapters/telaria/usersync_test.go @@ -0,0 +1,33 @@ +package telaria + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestTelariaSyncer(t *testing.T) { + + syncURL := "https://pbs.publishers.tremorhub.com/pubsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewTelariaSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://pbs.publishers.tremorhub.com/pubsync?gdpr=0&gdpr_consent=", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 202, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) + assert.Equal(t, "telaria", syncer.FamilyName()) + +} diff --git a/config/config.go b/config/config.go index 2cb5f8f2e66..652ad28cd87 100644 --- a/config/config.go +++ b/config/config.go @@ -535,6 +535,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSovrn, "https://ap.lijit.com/pixel?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%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?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ducfunnel%26uid%3DSspCookieUserId") @@ -729,6 +730,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.sovrn.endpoint", "http://ap.lijit.com/rtb/bid?src=prebid_server") v.SetDefault("adapters.synacormedia.endpoint", "http://{{.Host}}.technoratimedia.com/openrtb/bids/{{.Host}}") v.SetDefault("adapters.tappx.endpoint", "https://{{.Host}}") + 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index f7b970c571b..8e779822cae 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -54,6 +54,7 @@ import ( "github.com/prebid/prebid-server/adapters/sovrn" "github.com/prebid/prebid-server/adapters/synacormedia" "github.com/prebid/prebid-server/adapters/tappx" + "github.com/prebid/prebid-server/adapters/telaria" "github.com/prebid/prebid-server/adapters/triplelift" "github.com/prebid/prebid-server/adapters/triplelift_native" "github.com/prebid/prebid-server/adapters/ucfunnel" @@ -127,6 +128,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderSovrn: sovrn.NewSovrnBidder(client, cfg.Adapters[string(openrtb_ext.BidderSovrn)].Endpoint), openrtb_ext.BidderSynacormedia: synacormedia.NewSynacorMediaBidder(cfg.Adapters[string(openrtb_ext.BidderSynacormedia)].Endpoint), openrtb_ext.BidderTappx: tappx.NewTappxBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderTappx))].Endpoint), + 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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index ec9745563ef..43d9b894ea8 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -72,6 +72,7 @@ const ( BidderSovrn BidderName = "sovrn" BidderSynacormedia BidderName = "synacormedia" BidderTappx BidderName = "tappx" + BidderTelaria BidderName = "telaria" BidderTriplelift BidderName = "triplelift" BidderTripleliftNative BidderName = "triplelift_native" BidderUcfunnel BidderName = "ucfunnel" @@ -135,6 +136,7 @@ var BidderMap = map[string]BidderName{ "sovrn": BidderSovrn, "synacormedia": BidderSynacormedia, "tappx": BidderTappx, + "telaria": BidderTelaria, "triplelift": BidderTriplelift, "triplelift_native": BidderTripleliftNative, "ucfunnel": BidderUcfunnel, diff --git a/openrtb_ext/imp_telaria.go b/openrtb_ext/imp_telaria.go new file mode 100644 index 00000000000..8ea371a8ad0 --- /dev/null +++ b/openrtb_ext/imp_telaria.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpTelaria struct { + AdCode string `json:"adCode,omitempty"` + SeatCode string `json:"seatCode"` +} diff --git a/static/bidder-info/telaria.yaml b/static/bidder-info/telaria.yaml new file mode 100644 index 00000000000..43e8707a17b --- /dev/null +++ b/static/bidder-info/telaria.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "github@telaria.com" +capabilities: + app: + mediaTypes: + - video + site: + mediaTypes: + - video diff --git a/static/bidder-params/telaria.json b/static/bidder-params/telaria.json new file mode 100644 index 00000000000..b4121967351 --- /dev/null +++ b/static/bidder-params/telaria.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Telaria Adapter Params", + "description": "A schema which validates params accepted by the Telaria adapter", + + "type": "object", + "properties": { + "adCode": { + "type": "string", + "description": "The Ad Unit Code." + }, + "seatCode": { + "type": "string", + "description": "Your Seat Code." + }, + "originalPublisherid": { + "type": "string", + "description": "publisher ID from the original request" + } + }, + "required": ["seatCode"] +} diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index be0392f2dbb..da235402bf0 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -47,6 +47,7 @@ import ( "github.com/prebid/prebid-server/adapters/sonobi" "github.com/prebid/prebid-server/adapters/sovrn" "github.com/prebid/prebid-server/adapters/synacormedia" + "github.com/prebid/prebid-server/adapters/telaria" "github.com/prebid/prebid-server/adapters/triplelift" "github.com/prebid/prebid-server/adapters/triplelift_native" "github.com/prebid/prebid-server/adapters/ucfunnel" @@ -110,6 +111,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 383e24d82cf..637f590e25f 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -56,6 +56,7 @@ func TestNewSyncerMap(t *testing.T) { 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, From f07b1c335893b7c80a2c58844031885aece8b85b Mon Sep 17 00:00:00 2001 From: Krzysztof Desput Date: Wed, 15 Apr 2020 21:03:10 +0200 Subject: [PATCH 053/381] #615 Beachfront URLs from config (#1238) --- adapters/beachfront/beachfront.go | 80 ++++++++++++------- adapters/beachfront/beachfront_test.go | 2 +- .../exemplary/minimal-banner.json | 2 +- .../beachfronttest/exemplary/simple-mix.json | 2 +- .../minimal-banner-empty_array-200.json | 2 +- .../supplemental/minimal-site-banner.json | 2 +- .../supplemental/mobile-banner.json | 2 +- .../supplemental/multi-banner.json | 2 +- config/config.go | 1 + config/config_test.go | 2 + exchange/adapter_map.go | 17 ++-- exchange/exchange_test.go | 5 ++ 12 files changed, 74 insertions(+), 45 deletions(-) diff --git a/adapters/beachfront/beachfront.go b/adapters/beachfront/beachfront.go index e2eb31b3577..34a198e93a2 100644 --- a/adapters/beachfront/beachfront.go +++ b/adapters/beachfront/beachfront.go @@ -4,26 +4,27 @@ import ( "encoding/json" "errors" "fmt" - "github.com/mxmCherry/openrtb" - "github.com/prebid/prebid-server/adapters" - "github.com/prebid/prebid-server/errortypes" - "github.com/prebid/prebid-server/openrtb_ext" "net/http" "reflect" "strconv" "strings" + + "github.com/golang/glog" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" ) 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 4e82deaf3d8..c220cebf9b0 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/config/config.go b/config/config.go index 652ad28cd87..1c686591ef2 100644 --- a/config/config.go +++ b/config/config.go @@ -695,6 +695,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.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.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") diff --git a/config/config_test.go b/config/config_test.go index 9677ce2aaba..92794d7941e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -270,6 +270,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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 8e779822cae..20805fb7898 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -85,15 +85,14 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderAdvangelists: advangelists.NewAdvangelistsBidder(cfg.Adapters[string(openrtb_ext.BidderAdvangelists)].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.BidderCpmstar: cpmstar.NewCpmstarBidder(cfg.Adapters[string(openrtb_ext.BidderCpmstar)].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.BidderBeachfront: beachfront.NewBeachfrontBidder(cfg.Adapters[string(openrtb_ext.BidderBeachfront)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderBeachfront)].ExtraAdapterInfo), + 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.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, diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index f263eea8569..e7df5e85733 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -483,6 +483,11 @@ 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) From 76747ef3b2cb0cce51c624b2f3cb8161021952ab Mon Sep 17 00:00:00 2001 From: Mansi Nahar Date: Thu, 16 Apr 2020 10:35:27 -0400 Subject: [PATCH 054/381] Add nil check errors when setting native asset types (#1260) --- exchange/bidder.go | 39 ++++++++--- exchange/bidder_test.go | 144 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 11 deletions(-) diff --git a/exchange/bidder.go b/exchange/bidder.go index 8e95835ffba..7a53db5ee97 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -224,26 +224,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) { @@ -255,13 +272,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. diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 46f63cc66c4..f20b431c13a 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -15,6 +15,9 @@ import ( "github.com/prebid/prebid-server/currencies" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" + + nativeRequests "github.com/mxmCherry/openrtb/native/request" + nativeResponse "github.com/mxmCherry/openrtb/native/response" ) // TestSingleBidder makes sure that the following things work if the Bidder needs only one request. @@ -1083,6 +1086,147 @@ 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) + } + } + } +} + type goodSingleBidder struct { bidRequest *openrtb.BidRequest httpRequest *adapters.RequestData From 79bb4dc745ae2220fe8baae04fa12a4c12b1827c Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Thu, 16 Apr 2020 07:55:47 -0700 Subject: [PATCH 055/381] Bugfix: no bids from bidder handling (#1252) Co-authored-by: Veronika Solovei --- exchange/exchange.go | 30 ++- exchange/exchange_test.go | 19 +- .../request-multi-bidders-debug-info.json | 227 ++++++++++++++++++ .../request-multi-bidders-one-no-resp.json | 122 ++++++++++ 4 files changed, 386 insertions(+), 12 deletions(-) create mode 100644 exchange/exchangetest/request-multi-bidders-debug-info.json create mode 100644 exchange/exchangetest/request-multi-bidders-one-no-resp.json diff --git a/exchange/exchange.go b/exchange/exchange.go index e625e5ca8f3..48903dd98c7 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -63,6 +63,9 @@ type exchange struct { 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 { @@ -324,6 +327,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) @@ -346,7 +353,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 { @@ -639,20 +651,20 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb } } - for a, b := range adapterBids { - if b != nil && req.Test == 1 { - // Fill debug info - bidResponseExt.Debug.HttpCalls[a] = b.httpCalls + for bidderName, responseExtra := range adapterExtra { + + if req.Test == 1 { + 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 diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index e7df5e85733..3c1c2f3bc72 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -771,6 +771,11 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { } } } + 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 { @@ -1625,6 +1630,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 { @@ -1638,8 +1644,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. @@ -1696,7 +1703,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, } } 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..ec174f75b36 --- /dev/null +++ b/exchange/exchangetest/request-multi-bidders-debug-info.json @@ -0,0 +1,227 @@ +{ + "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": { + "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": { + "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" + } + } + } + }] + } + ] + } + } +} + + From 8657ae9b0113855e7dd15f921b3cf6f0387a63c3 Mon Sep 17 00:00:00 2001 From: jmaynardxandr <46759873+jmaynardxandr@users.noreply.github.com> Date: Tue, 21 Apr 2020 12:17:38 -0700 Subject: [PATCH 056/381] Add missing categories to AppNexus -> IAB mapping file. (#1264) * Add missing categories to AppNexus -> IAB mapping file. * Remove entry for category 38 which was set to a primary IAB category instead of a sub-category. * Fix order of category 22 --- static/adapter/appnexus/opts.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/adapter/appnexus/opts.json b/static/adapter/appnexus/opts.json index bd6f8af3e8b..3ea849735de 100644 --- a/static/adapter/appnexus/opts.json +++ b/static/adapter/appnexus/opts.json @@ -20,8 +20,11 @@ "19": "IAB18-4", "20": "IAB1-5", "21": "IAB1-6", + "22": "IAB19-28", "23": "IAB19-13", "24": "IAB22-2", + "25": "IAB3-9", + "26": "IAB17-26", "27": "IAB19-6", "28": "IAB1-7", "29": "IAB9-5", @@ -31,7 +34,6 @@ "33": "IAB16-5", "34": "IAB19-34", "37": "IAB11-4", - "38": "IAB23", "39": "IAB9-30", "41": "IAB7-44", "51": "IAB17-12", From dc9335ca0a343d4ccc89def2307562a6576c86df Mon Sep 17 00:00:00 2001 From: hbanalytics <55453525+hbanalytics@users.noreply.github.com> Date: Wed, 22 Apr 2020 17:11:38 +0300 Subject: [PATCH 057/381] Yieldone s2s Bid Adapter (#1242) * Added new Yieldone Bid s2s Adapter * Update endpoint for yieldone bid adapter * Fixes after review for Yieldone Bid s2s Adapter * Fix typeo in Yieldone s2s Bid Adapter --- adapters/yieldone/params_test.go | 48 ++++++ adapters/yieldone/usersync.go | 12 ++ adapters/yieldone/usersync_test.go | 30 ++++ adapters/yieldone/yieldone.go | 144 ++++++++++++++++++ adapters/yieldone/yieldone_test.go | 11 ++ .../yieldonetest/exemplary/simple-banner.json | 89 +++++++++++ .../yieldonetest/exemplary/simple-video.json | 87 +++++++++++ .../yieldonetest/params/race/banner.json | 4 + .../supplemental/bad_response.json | 65 ++++++++ .../yieldonetest/supplemental/status_204.json | 60 ++++++++ .../yieldonetest/supplemental/status_400.json | 65 ++++++++ .../yieldonetest/supplemental/status_418.json | 65 ++++++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_yieldone.go | 6 + static/bidder-info/yieldone.yaml | 11 ++ static/bidder-params/yieldone.json | 15 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 20 files changed, 721 insertions(+) create mode 100644 adapters/yieldone/params_test.go create mode 100644 adapters/yieldone/usersync.go create mode 100644 adapters/yieldone/usersync_test.go create mode 100644 adapters/yieldone/yieldone.go create mode 100644 adapters/yieldone/yieldone_test.go create mode 100644 adapters/yieldone/yieldonetest/exemplary/simple-banner.json create mode 100644 adapters/yieldone/yieldonetest/exemplary/simple-video.json create mode 100644 adapters/yieldone/yieldonetest/params/race/banner.json create mode 100644 adapters/yieldone/yieldonetest/supplemental/bad_response.json create mode 100644 adapters/yieldone/yieldonetest/supplemental/status_204.json create mode 100644 adapters/yieldone/yieldonetest/supplemental/status_400.json create mode 100644 adapters/yieldone/yieldonetest/supplemental/status_418.json create mode 100644 openrtb_ext/imp_yieldone.go create mode 100644 static/bidder-info/yieldone.yaml create mode 100644 static/bidder-params/yieldone.json diff --git a/adapters/yieldone/params_test.go b/adapters/yieldone/params_test.go new file mode 100644 index 00000000000..6048ea5d7dc --- /dev/null +++ b/adapters/yieldone/params_test.go @@ -0,0 +1,48 @@ +package yieldone + +import ( + "encoding/json" + "github.com/prebid/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..bc9d1b3235b --- /dev/null +++ b/adapters/yieldone/usersync.go @@ -0,0 +1,12 @@ +package yieldone + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/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..902f3b66b34 --- /dev/null +++ b/adapters/yieldone/usersync_test.go @@ -0,0 +1,30 @@ +package yieldone + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/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..f02d1a0b088 --- /dev/null +++ b/adapters/yieldone/yieldone.go @@ -0,0 +1,144 @@ +package yieldone + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..34d58bafbd7 --- /dev/null +++ b/adapters/yieldone/yieldone_test.go @@ -0,0 +1,11 @@ +package yieldone + +import ( + "testing" + + "github.com/prebid/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/config/config.go b/config/config.go index 1c686591ef2..c944153a6b0 100644 --- a/config/config.go +++ b/config/config.go @@ -544,6 +544,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderVisx, "https://t.visx.net/s2s_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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.BidderYieldmo, "https://ads.yieldmo.com/pbsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldmo%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldone, "https://y.one.impact-ad.jp/hbs_sc?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dzeroclickfraud%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D") } @@ -742,6 +743,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.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) diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 20805fb7898..585f89beb19 100644 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -64,6 +64,7 @@ import ( "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" "github.com/prebid/prebid-server/adapters/yieldmo" + "github.com/prebid/prebid-server/adapters/yieldone" "github.com/prebid/prebid-server/adapters/zeroclickfraud" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -137,6 +138,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.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), } diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 43d9b894ea8..508379b5df9 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -82,6 +82,7 @@ const ( BidderVisx BidderName = "visx" BidderVrtcal BidderName = "vrtcal" BidderYieldmo BidderName = "yieldmo" + BidderYieldone BidderName = "yieldone" BidderZeroClickFraud BidderName = "zeroclickfraud" ) @@ -146,6 +147,7 @@ var BidderMap = map[string]BidderName{ "visx": BidderVisx, "vrtcal": BidderVrtcal, "yieldmo": BidderYieldmo, + "yieldone": BidderYieldone, "zeroclickfraud": BidderZeroClickFraud, } 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index da235402bf0..995f573adca 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -57,6 +57,7 @@ import ( "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" "github.com/prebid/prebid-server/adapters/yieldmo" + "github.com/prebid/prebid-server/adapters/yieldone" "github.com/prebid/prebid-server/adapters/zeroclickfraud" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -121,6 +122,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderVisx, visx.NewVisxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderVrtcal, vrtcal.NewVrtcalSyncer) 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 index 637f590e25f..c25a91bc6b4 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -66,6 +66,7 @@ func TestNewSyncerMap(t *testing.T) { 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, }, } From b44980c8e53af42881ba7160b606c641c1730059 Mon Sep 17 00:00:00 2001 From: chino117 Date: Wed, 22 Apr 2020 11:15:24 -0300 Subject: [PATCH 058/381] Fix: URL de sync (#1261) --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index c944153a6b0..22c5b96222c 100644 --- a/config/config.go +++ b/config/config.go @@ -509,7 +509,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderDatablocks, "https://sync.v5prebid.datablocks.net/s2ssync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ddatablocks%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Deplanning%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") // openrtb_ext.BidderFacebook doesn't have a good default. // 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgamoshi%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bgusr%5D") From 99dc46bc7eef9bf50b45ce34ad275b02425c0784 Mon Sep 17 00:00:00 2001 From: Aadesh Date: Wed, 22 Apr 2020 10:29:25 -0400 Subject: [PATCH 059/381] populate the app ID in the FAN timeout notif url with the publisher ID (#1265) and the auction with the request ID Co-authored-by: Aadesh Patel --- .../audienceNetworktest/exemplary/banner.json | 6 ++-- .../exemplary/interstitial.json | 6 ++-- .../exemplary/native-1.1.json | 6 ++-- .../audienceNetworktest/exemplary/video.json | 6 ++-- .../supplemental/banner-format-only.json | 6 ++-- .../supplemental/multi-imp.json | 12 +++---- .../supplemental/no-bid-204.json | 4 +-- .../supplemental/split-placementId.json | 6 ++-- adapters/audienceNetwork/facebook.go | 32 +++++++++++++++++-- adapters/audienceNetwork/facebook_test.go | 22 +++++++++++-- 10 files changed, 74 insertions(+), 32 deletions(-) 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 index 042c86bd7fd..bb192aad76f 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.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" } } 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 db7657f59b7..9edb9a7d57e 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), @@ -455,18 +460,39 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string) } func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { - // Note, facebook creates one request per imp, so all these requests will only have one imp in them - auction_id, err := jsonparser.GetString(req.Body, "imp", "[0]", "id") + 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} } - uri := fmt.Sprintf("https://www.facebook.com/audiencenetwork/nurl/?partner=%s&app=%s&auction=%s&ortb_loss_code=2", fa.platformID, fa.platformID, auction_id) + // 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 1edaabd45d7..784a540e596 100644 --- a/adapters/audienceNetwork/facebook_test.go +++ b/adapters/audienceNetwork/facebook_test.go @@ -43,9 +43,9 @@ func TestJsonSamples(t *testing.T) { adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret")) } -func TestMakeTimeoutNotice(t *testing.T) { +func TestMakeTimeoutNoticeApp(t *testing.T) { req := adapters.RequestData{ - Body: []byte(`{"imp":[{"id":"1234"}]}}`), + Body: []byte(`{"id":"1234","imp":[{"id":"1234"}],"app":{"publisher":{"id":"5678"}}}`), } fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") @@ -56,9 +56,25 @@ func TestMakeTimeoutNotice(t *testing.T) { 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=test-platform-id&auction=1234&ortb_loss_code=2" + 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) { From f4905e8fa74a6681091751d15c9c0e9de0252133 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Wed, 22 Apr 2020 07:30:58 -0700 Subject: [PATCH 060/381] Added header User Agent decoding (#1268) * Added header User Agent decoding * Added header User Agent decoding: unit tests * Added header User Agent decoding: unit tests * Added check UA is encoded to avoid `+` converted to space Co-authored-by: Veronika Solovei --- endpoints/openrtb2/video_auction.go | 16 +++++++-- endpoints/openrtb2/video_auction_test.go | 43 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 0215eb4cff2..c7316604d73 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -8,12 +8,13 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "strconv" "strings" "time" "github.com/buger/jsonparser" - jsonpatch "github.com/evanphx/json-patch" + "github.com/evanphx/json-patch" "github.com/gofrs/uuid" "github.com/prebid/prebid-server/errortypes" @@ -617,7 +618,18 @@ func (deps *endpointDeps) parseVideoRequest(request []byte, headers http.Header) //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 == "" { - req.Device.UA = headers.Get("User-Agent") + 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) diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index d0ce33de1c4..ec525c6ff08 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -927,6 +927,49 @@ func TestParseVideoRequestWithoutUserAgentAndEmptyHeader(t *testing.T) { } +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) + req, valErr, podErr := deps.parseVideoRequest(reqData, 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) + req, valErr, podErr := deps.parseVideoRequest(reqData, 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, From 62a135779b88ada41ac9872c2455a8d092fb02c4 Mon Sep 17 00:00:00 2001 From: Ad Generation Date: Fri, 24 Apr 2020 00:45:45 +0900 Subject: [PATCH 061/381] Ad Generation Adapter Integration. (#1253) * AdGeneration Integration. * update AdGeneration adapter. fix: some methods of the adgAdapter replace to functions. fix: unmarshal functions return a pointer. fix: header is defined once. fix: return when imps is appended * update AdGeneration Adapter. add: Added a comment in usersync. add: Added a test for parameters whose ID does not exist in params_test. change: Change to query creation by net/url. Added getRawQuery Test. fix: Changed variable names related to bidRequest. --- adapters/adgeneration/adgeneration.go | 260 ++++++++++++++++++ adapters/adgeneration/adgeneration_test.go | 176 ++++++++++++ .../exemplary/single-banner.json | 151 ++++++++++ .../adgenerationtest/params/race/banner.json | 3 + .../supplemental/204-bid-response.json | 72 +++++ .../supplemental/400-bid-response.json | 77 ++++++ .../supplemental/invalid-adg-param.json | 31 +++ .../supplemental/no-bid-response.json | 89 ++++++ adapters/adgeneration/params_test.go | 47 ++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adgeneration.go | 5 + static/bidder-info/adgeneration.yaml | 10 + static/bidder-params/adgeneration.json | 15 + usersync/usersyncers/syncer_test.go | 13 +- 16 files changed, 949 insertions(+), 6 deletions(-) create mode 100644 adapters/adgeneration/adgeneration.go create mode 100644 adapters/adgeneration/adgeneration_test.go create mode 100644 adapters/adgeneration/adgenerationtest/exemplary/single-banner.json create mode 100644 adapters/adgeneration/adgenerationtest/params/race/banner.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/invalid-adg-param.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json create mode 100644 adapters/adgeneration/params_test.go create mode 100644 openrtb_ext/imp_adgeneration.go create mode 100644 static/bidder-info/adgeneration.yaml create mode 100644 static/bidder-params/adgeneration.json diff --git a/adapters/adgeneration/adgeneration.go b/adapters/adgeneration/adgeneration.go new file mode 100644 index 00000000000..4b1215dea9d --- /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/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..e76995fc5e4 --- /dev/null +++ b/adapters/adgeneration/adgeneration_test.go @@ -0,0 +1,176 @@ +package adgeneration + +import ( + "encoding/json" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/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"` +} + +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 httpRequests []*adapters.RequestData + var errors []error + + for _, auction := range request.Imp { + newHttpRequest, err := a.makeRequest(httpRequests, &auction, request, consentString) + if err != nil { + errors = append(errors, err) + } else if newHttpRequest != nil { + httpRequests = append(httpRequests, newHttpRequest) + } + } + + return httpRequests, errors +} + +func (a *AdOceanAdapter) makeRequest(existingRequests []*adapters.RequestData, imp *openrtb.Imp, request *openrtb.BidRequest, consentString string) (*adapters.RequestData, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing bidderExt object", + } + } + + var adOceanExt openrtb_ext.ExtImpAdOcean + if err := json.Unmarshal(bidderExt.Bidder, &adOceanExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "Error parsing adOceanExt parameters", + } + } + + addedToExistingRequest := addToExistingRequest(existingRequests, &adOceanExt, imp.ID) + if addedToExistingRequest { + return nil, nil + } + + url, err := a.makeURL(&adOceanExt, imp.ID, request, consentString) + if err != nil { + return nil, 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) + } + + return &adapters.RequestData{ + Method: "GET", + Uri: url, + Headers: headers, + }, nil +} + +func addToExistingRequest(existingRequests []*adapters.RequestData, newParams *openrtb_ext.ExtImpAdOcean, auctionID string) bool { +requestsLoop: + for _, request := range existingRequests { + endpointURL, _ := url.Parse(request.Uri) + queryParams := endpointURL.Query() + masterID := queryParams["id"][0] + + if masterID == newParams.MasterID { + aids := queryParams["aid"] + for _, aid := range aids { + slaveID := strings.SplitN(aid, ":", 2)[0] + if slaveID == newParams.SlaveID { + continue requestsLoop + } + } + + queryParams.Add("aid", newParams.SlaveID+":"+auctionID) + endpointURL.RawQuery = queryParams.Encode() + newUri := endpointURL.String() + if len(newUri) < maxUriLength { + request.Uri = newUri + return true + } + } + } + + return false +} + +func (a *AdOceanAdapter) makeURL(params *openrtb_ext.ExtImpAdOcean, auctionID string, request *openrtb.BidRequest, consentString string) (string, error) { + endpointParams := macros.EndpointTemplateParams{Host: params.EmitterDomain} + host, err := macros.ResolveMacros(a.endpointTemplate, endpointParams) + if err != nil { + return "", &errortypes.BadInput{ + Message: "Unable to parse endpoint url template: " + err.Error(), + } + } + + endpointURL, err := url.Parse(host) + if err != nil { + return "", &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" + + 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) + } + endpointURL.RawQuery = queryParams.Encode() + + return endpointURL.String(), nil +} + +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..5713b02da27 --- /dev/null +++ b/adapters/adocean/adocean_test.go @@ -0,0 +1,12 @@ +package adocean + +import ( + "net/http" + "testing" + + "github.com/prebid/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..007a530621a --- /dev/null +++ b/adapters/adocean/adoceantest/exemplary/multi-banner-impression.json @@ -0,0 +1,130 @@ +{ + "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": "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&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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..b938a042a80 --- /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&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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..514262d5d2e --- /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&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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..2f775f98748 --- /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&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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..7a5fa8fd18e --- /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&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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..ed81bb35114 --- /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&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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/requests-merge.json b/adapters/adocean/adoceantest/supplemental/requests-merge.json new file mode 100644 index 00000000000..9b5eb39aee2 --- /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&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.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..1a88c4716e0 --- /dev/null +++ b/adapters/adocean/params_test.go @@ -0,0 +1,50 @@ +package adocean + +import ( + "encoding/json" + "testing" + + "github.com/prebid/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..650e517a578 --- /dev/null +++ b/adapters/adocean/usersync.go @@ -0,0 +1,12 @@ +package adocean + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/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..9ca81b98cb4 --- /dev/null +++ b/adapters/adocean/usersync_test.go @@ -0,0 +1,34 @@ +package adocean + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/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/config/config.go b/config/config.go index 2d49b0605a1..7e4e4196cd9 100755 --- a/config/config.go +++ b/config/config.go @@ -500,6 +500,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdpone, "https://usersync.adpone.com/csync?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Daja%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25s") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAppnexus, "https://ib.adnxs.com/getuid?"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadnxs%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -692,6 +693,7 @@ func SetupViper(v *viper.Viper, filename string) { 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 3bf1b6a5c82..e6cce7a643b 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -12,6 +12,7 @@ import ( "github.com/prebid/prebid-server/adapters/adkernel" "github.com/prebid/prebid-server/adapters/adkernelAdn" "github.com/prebid/prebid-server/adapters/admixer" + "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adoppler" "github.com/prebid/prebid-server/adapters/adpone" "github.com/prebid/prebid-server/adapters/adtelligent" @@ -84,6 +85,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderAdtelligent: adtelligent.NewAdtelligentBidder(cfg.Adapters[string(openrtb_ext.BidderAdtelligent)].Endpoint), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 6660ddac946..aa8e959f7a5 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -31,6 +31,7 @@ const ( BidderAdkernelAdn BidderName = "adkernelAdn" BidderAdpone BidderName = "adpone" BidderAdmixer BidderName = "admixer" + BidderAdOcean BidderName = "adocean" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderAJA BidderName = "aja" @@ -98,6 +99,7 @@ var BidderMap = map[string]BidderName{ "adkernel": BidderAdkernel, "adkernelAdn": BidderAdkernelAdn, "admixer": BidderAdmixer, + "adocean": BidderAdOcean, "adpone": BidderAdpone, "adtelligent": BidderAdtelligent, "advangelists": BidderAdvangelists, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 2c9cf59781b..1d8c8a0794c 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -10,6 +10,7 @@ import ( "github.com/prebid/prebid-server/adapters/adkernel" "github.com/prebid/prebid-server/adapters/adkernelAdn" "github.com/prebid/prebid-server/adapters/admixer" + "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adpone" "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" @@ -77,6 +78,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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.BidderAdtelligent, adtelligent.NewAdtelligentSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdvangelists, advangelists.NewAdvangelistsSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 221302dd333..050e1039000 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -19,6 +19,7 @@ func TestNewSyncerMap(t *testing.T) { 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.BidderAdtelligent): syncConfig, string(openrtb_ext.BidderAdvangelists): syncConfig, From 8db5479aecd52455facbcc9484a8bb8f128984e5 Mon Sep 17 00:00:00 2001 From: trchandraprakash <47793448+trchandraprakash@users.noreply.github.com> Date: Wed, 6 May 2020 07:29:16 -0700 Subject: [PATCH 072/381] LunaMedia Adapter (#1285) Co-authored-by: Chandra Prakash --- adapters/lunamedia/lunamedia.go | 236 ++++++++++++++++++ adapters/lunamedia/lunamedia_test.go | 10 + .../lunamediatest/exemplary/banner.json | 95 +++++++ .../lunamediatest/exemplary/video.json | 83 ++++++ .../lunamediatest/params/race/banner.json | 4 + .../lunamediatest/params/race/video.json | 4 + .../lunamediatest/supplemental/checkImp.json | 14 ++ .../lunamediatest/supplemental/compat.json | 80 ++++++ .../lunamediatest/supplemental/ext.json | 33 +++ .../supplemental/missingpub.json | 35 +++ .../supplemental/responseCode.json | 78 ++++++ .../supplemental/responsebid.json | 79 ++++++ .../lunamediatest/supplemental/site.json | 103 ++++++++ .../lunamediatest/supplemental/size.json | 28 +++ adapters/lunamedia/params_test.go | 45 ++++ adapters/lunamedia/usersync.go | 12 + adapters/lunamedia/usersync_test.go | 31 +++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_lunamedia.go | 6 + static/bidder-info/lunamedia.yaml | 13 + static/bidder-params/lunamedia.json | 18 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 25 files changed, 1016 insertions(+) create mode 100644 adapters/lunamedia/lunamedia.go create mode 100644 adapters/lunamedia/lunamedia_test.go create mode 100644 adapters/lunamedia/lunamediatest/exemplary/banner.json create mode 100644 adapters/lunamedia/lunamediatest/exemplary/video.json create mode 100644 adapters/lunamedia/lunamediatest/params/race/banner.json create mode 100644 adapters/lunamedia/lunamediatest/params/race/video.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/checkImp.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/compat.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/ext.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/missingpub.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/responseCode.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/responsebid.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/site.json create mode 100644 adapters/lunamedia/lunamediatest/supplemental/size.json create mode 100644 adapters/lunamedia/params_test.go create mode 100644 adapters/lunamedia/usersync.go create mode 100644 adapters/lunamedia/usersync_test.go create mode 100755 openrtb_ext/imp_lunamedia.go create mode 100644 static/bidder-info/lunamedia.yaml create mode 100644 static/bidder-params/lunamedia.json diff --git a/adapters/lunamedia/lunamedia.go b/adapters/lunamedia/lunamedia.go new file mode 100644 index 00000000000..51906884331 --- /dev/null +++ b/adapters/lunamedia/lunamedia.go @@ -0,0 +1,236 @@ +package lunamedia + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type LunaMediaAdapter struct { + EndpointTemplate template.Template +} + +//MakeRequests prepares request information for prebid-server core +func (adapter *LunaMediaAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + errs := make([]error, 0, len(request.Imp)) + if len(request.Imp) == 0 { + errs = append(errs, &errortypes.BadInput{Message: "No impression in the bid request"}) + return nil, errs + } + pub2impressions, imps, err := getImpressionsInfo(request.Imp) + if len(imps) == 0 { + return nil, err + } + errs = append(errs, err...) + + if len(pub2impressions) == 0 { + return nil, errs + } + + result := make([]*adapters.RequestData, 0, len(pub2impressions)) + for k, imps := range pub2impressions { + bidRequest, err := adapter.buildAdapterRequest(request, &k, imps) + if err != nil { + errs = append(errs, err) + return nil, errs + } else { + result = append(result, bidRequest) + } + } + return result, errs +} + +// getImpressionsInfo checks each impression for validity and returns impressions copy with corresponding exts +func getImpressionsInfo(imps []openrtb.Imp) (map[openrtb_ext.ExtImpLunaMedia][]openrtb.Imp, []openrtb.Imp, []error) { + errors := make([]error, 0, len(imps)) + resImps := make([]openrtb.Imp, 0, len(imps)) + res := make(map[openrtb_ext.ExtImpLunaMedia][]openrtb.Imp) + + for _, imp := range imps { + impExt, err := getImpressionExt(&imp) + if err != nil { + errors = append(errors, err) + continue + } + if err := validateImpression(impExt); err != nil { + errors = append(errors, err) + continue + } + //dispatchImpressions + //Group impressions by LunaMedia-specific parameters `pubid + if err := compatImpression(&imp); err != nil { + errors = append(errors, err) + continue + } + if res[*impExt] == nil { + res[*impExt] = make([]openrtb.Imp, 0) + } + res[*impExt] = append(res[*impExt], imp) + resImps = append(resImps, imp) + } + return res, resImps, errors +} + +func validateImpression(impExt *openrtb_ext.ExtImpLunaMedia) error { + if impExt.PublisherID == "" { + return &errortypes.BadInput{Message: "No pubid value provided"} + } + return nil +} + +//Alter impression info to comply with LunaMedia platform requirements +func compatImpression(imp *openrtb.Imp) error { + imp.Ext = nil //do not forward ext to LunaMedia platform + if imp.Banner != nil { + return compatBannerImpression(imp) + } + return nil +} + +func compatBannerImpression(imp *openrtb.Imp) error { + // Create a copy of the banner, since imp is a shallow copy of the original. + + bannerCopy := *imp.Banner + banner := &bannerCopy + //As banner.w/h are required fields for LunaMedia platform - take the first format entry + if banner.W == nil || banner.H == nil { + if len(banner.Format) == 0 { + return &errortypes.BadInput{Message: "Expected at least one banner.format entry or explicit w/h"} + } + format := banner.Format[0] + banner.Format = banner.Format[1:] + banner.W = &format.W + banner.H = &format.H + imp.Banner = banner + } + return nil +} + +func getImpressionExt(imp *openrtb.Imp) (*openrtb_ext.ExtImpLunaMedia, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: err.Error(), + } + } + var LunaMediaExt openrtb_ext.ExtImpLunaMedia + if err := json.Unmarshal(bidderExt.Bidder, &LunaMediaExt); err != nil { + return nil, &errortypes.BadInput{ + Message: err.Error(), + } + } + return &LunaMediaExt, nil +} + +func (adapter *LunaMediaAdapter) buildAdapterRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpLunaMedia, imps []openrtb.Imp) (*adapters.RequestData, error) { + newBidRequest := createBidRequest(prebidBidRequest, params, imps) + reqJSON, err := json.Marshal(newBidRequest) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("x-openrtb-version", "2.5") + + url, err := adapter.buildEndpointURL(params) + if err != nil { + return nil, err + } + + return &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: reqJSON, + Headers: headers}, nil +} + +func createBidRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpLunaMedia, imps []openrtb.Imp) *openrtb.BidRequest { + bidRequest := *prebidBidRequest + bidRequest.Imp = imps + for idx := range bidRequest.Imp { + imp := &bidRequest.Imp[idx] + imp.TagID = params.Placement + } + if bidRequest.Site != nil { + // Need to copy Site as Request is a shallow copy + siteCopy := *bidRequest.Site + bidRequest.Site = &siteCopy + bidRequest.Site.Publisher = nil + bidRequest.Site.Domain = "" + } + if bidRequest.App != nil { + // Need to copy App as Request is a shallow copy + appCopy := *bidRequest.App + bidRequest.App = &appCopy + bidRequest.App.Publisher = nil + } + return &bidRequest +} + +// Builds enpoint url based on adapter-specific pub settings from imp.ext +func (adapter *LunaMediaAdapter) buildEndpointURL(params *openrtb_ext.ExtImpLunaMedia) (string, error) { + endpointParams := macros.EndpointTemplateParams{PublisherID: params.PublisherID} + return macros.ResolveMacros(adapter.EndpointTemplate, endpointParams) +} + +//MakeBids translates LunaMedia bid response to prebid-server specific format +func (adapter *LunaMediaAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var msg = "" + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + if response.StatusCode != http.StatusOK { + msg = fmt.Sprintf("Unexpected http status code: %d", response.StatusCode) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + + } + var bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + msg = fmt.Sprintf("Bad server response: %d", err) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + } + if len(bidResp.SeatBid) != 1 { + var msg = fmt.Sprintf("Invalid SeatBids count: %d", len(bidResp.SeatBid)) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + } + + seatBid := bidResp.SeatBid[0] + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + + for i := 0; i < len(seatBid.Bid); i++ { + bid := seatBid.Bid[i] + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: getMediaTypeForImpID(bid.ImpID, internalRequest.Imp), + }) + } + return bidResponse, nil +} + +// getMediaTypeForImp figures out which media type this bid is for +func getMediaTypeForImpID(impID string, imps []openrtb.Imp) openrtb_ext.BidType { + for _, imp := range imps { + if imp.ID == impID && imp.Video != nil { + return openrtb_ext.BidTypeVideo + } + } + return openrtb_ext.BidTypeBanner +} + +// NewLunaMediaAdapter to be called in prebid-server core to create LunaMedia adapter instance +func NewLunaMediaBidder(endpointTemplate string) adapters.Bidder { + template, err := template.New("endpointTemplate").Parse(endpointTemplate) + if err != nil { + return nil + } + return &LunaMediaAdapter{EndpointTemplate: *template} +} diff --git a/adapters/lunamedia/lunamedia_test.go b/adapters/lunamedia/lunamedia_test.go new file mode 100644 index 00000000000..924e6a774b1 --- /dev/null +++ b/adapters/lunamedia/lunamedia_test.go @@ -0,0 +1,10 @@ +package lunamedia + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "testing" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "lunamediatest", NewLunaMediaBidder("http://api.lunamedia.io/xp/get?pubid={{.PublisherID}}")) +} diff --git a/adapters/lunamedia/lunamediatest/exemplary/banner.json b/adapters/lunamedia/lunamediatest/exemplary/banner.json new file mode 100644 index 00000000000..3b5c417f169 --- /dev/null +++ b/adapters/lunamedia/lunamediatest/exemplary/banner.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + }, + { + "w": 320, + "h": 300 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "bidder": { + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "body":{ + "id": "testid", + "imp": [{ + "id": "testimpid", + "tagid": "dummyplacement", + "banner": { + "format": [{ + "w": 320, + "h": 250 + }, { + "w": 320, + "h": 300 + }], + "w": 320, + "h": 250 + } + + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/lunamedia/lunamediatest/exemplary/video.json b/adapters/lunamedia/lunamediatest/exemplary/video.json new file mode 100644 index 00000000000..82217373e2e --- /dev/null +++ b/adapters/lunamedia/lunamediatest/exemplary/video.json @@ -0,0 +1,83 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "body":{ + "id": "testid", + "imp": [{ + "id": "testimpid", + "tagid": "dummyplacement", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/params/race/banner.json b/adapters/lunamedia/lunamediatest/params/race/banner.json new file mode 100644 index 00000000000..2eed8f2ec4e --- /dev/null +++ b/adapters/lunamedia/lunamediatest/params/race/banner.json @@ -0,0 +1,4 @@ +{ + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" +} \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/params/race/video.json b/adapters/lunamedia/lunamediatest/params/race/video.json new file mode 100644 index 00000000000..2eed8f2ec4e --- /dev/null +++ b/adapters/lunamedia/lunamediatest/params/race/video.json @@ -0,0 +1,4 @@ +{ + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" +} \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/supplemental/checkImp.json b/adapters/lunamedia/lunamediatest/supplemental/checkImp.json new file mode 100644 index 00000000000..ca48812b4df --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/checkImp.json @@ -0,0 +1,14 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test", + "domain": "test.com" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the bid request", + "comparison": "literal" + }] + } \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/supplemental/compat.json b/adapters/lunamedia/lunamediatest/supplemental/compat.json new file mode 100644 index 00000000000..5b84d3a5a39 --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/compat.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [{ + "w": 320, + "h": 250 + }] + }, + "ext": { + "bidder": { + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "body":{ + "id": "testid", + "imp": [{ + "banner": { + "h": 250, + "w": 320 + }, + "id": "testimpid", + "tagid": "dummyplacement" + + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/lunamedia/lunamediatest/supplemental/ext.json b/adapters/lunamedia/lunamediatest/supplemental/ext.json new file mode 100644 index 00000000000..3cfb878bd47 --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/ext.json @@ -0,0 +1,33 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + }, + { + "w": 320, + "h": 300 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" + } + } + ] + }, +"expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/supplemental/missingpub.json b/adapters/lunamedia/lunamediatest/supplemental/missingpub.json new file mode 100644 index 00000000000..b088917afa3 --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/missingpub.json @@ -0,0 +1,35 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + }, + { + "w": 320, + "h": 300 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "bidder": { + "pubid": "", + "placement": "dummyplacement" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "No pubid value provided", + "comparison": "literal" + }] + } \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/supplemental/responseCode.json b/adapters/lunamedia/lunamediatest/supplemental/responseCode.json new file mode 100644 index 00000000000..739af044b29 --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/responseCode.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test" + }, + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + }, + { + "w": 320, + "h": 300 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "bidder": { + "pubid": "yu", + "placement": "dummyplacement" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://api.lunamedia.io/xp/get?pubid=yu", + "body": { + "id": "testid", + "imp": [ + { + "banner": { + "format": [ + { + "h": 250, + "w": 320 + }, + { + "h": 300, + "w": 320 + } + ], + "h": 250, + "w": 320 + }, + "id": "testimpid", + "tagid": "dummyplacement" + } + ], + "site": { + "id": "test" + } + } + }, + "mockResponse": { + "body": { + "seatbid": [] + } + } + } + + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected http status code: 0", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/supplemental/responsebid.json b/adapters/lunamedia/lunamediatest/supplemental/responsebid.json new file mode 100644 index 00000000000..e9d8c3c543d --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/responsebid.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test" + }, + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + }, + { + "w": 320, + "h": 300 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "bidder": { + "pubid": "yu", + "placement": "dummyplacement" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://api.lunamedia.io/xp/get?pubid=yu", + "body": { + "id": "testid", + "imp": [ + { + "banner": { + "format": [ + { + "h": 250, + "w": 320 + }, + { + "h": 300, + "w": 320 + } + ], + "h": 250, + "w": 320 + }, + "id": "testimpid", + "tagid": "dummyplacement" + } + ], + "site": { + "id": "test" + } + } + }, + "mockResponse": { + "status":200, + "body": { + "seatbid": [] + } + } + } + + ], + "expectedMakeBidsErrors": [ + { + "value": "Invalid SeatBids count: 0", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/supplemental/site.json b/adapters/lunamedia/lunamediatest/supplemental/site.json new file mode 100644 index 00000000000..81d71554f38 --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/site.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test" + }, + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + }, + { + "w": 320, + "h": 300 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "bidder": { + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://api.lunamedia.io/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688", + "body": { + "id": "testid", + "imp": [ + { + "banner": { + "format": [ + { + "h": 250, + "w": 320 + }, + { + "h": 300, + "w": 320 + } + ], + "h": 250, + "w": 320 + }, + "id": "testimpid", + "tagid": "dummyplacement" + } + ], + "site": { + "id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "testid", + "impid": "testimpid", + "cid": "8048" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/lunamedia/lunamediatest/supplemental/size.json b/adapters/lunamedia/lunamediatest/supplemental/size.json new file mode 100644 index 00000000000..77228559eee --- /dev/null +++ b/adapters/lunamedia/lunamediatest/supplemental/size.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test", + "domain": "test.com" + }, + "imp": [ + { + "id": "testimpid", + "banner": { + + }, + "ext": { + "bidder": { + "pubid": "19f1b372c7548ec1fe734d2c9f8dc688", + "placement": "dummyplacement" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Expected at least one banner.format entry or explicit w/h", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/lunamedia/params_test.go b/adapters/lunamedia/params_test.go new file mode 100644 index 00000000000..b4faeea1f77 --- /dev/null +++ b/adapters/lunamedia/params_test.go @@ -0,0 +1,45 @@ +package lunamedia + +import ( + "encoding/json" + "github.com/prebid/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.BidderLunaMedia, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected LunaMedia 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.BidderLunaMedia, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"pubid": "19f1b372c7548ec1fe734d2c9f8dc688"}`, +} + +var invalidParams = []string{ + `{"publisher": "19f1b372c7548ec1fe734d2c9f8dc688"}`, + `nil`, + ``, + `[]`, + `true`, +} diff --git a/adapters/lunamedia/usersync.go b/adapters/lunamedia/usersync.go new file mode 100644 index 00000000000..7ad54e384a1 --- /dev/null +++ b/adapters/lunamedia/usersync.go @@ -0,0 +1,12 @@ +package lunamedia + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewLunaMediaSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("lunamedia", 0, temp, adapters.SyncTypeIframe) +} diff --git a/adapters/lunamedia/usersync_test.go b/adapters/lunamedia/usersync_test.go new file mode 100644 index 00000000000..c9fe2032d2c --- /dev/null +++ b/adapters/lunamedia/usersync_test.go @@ -0,0 +1,31 @@ +package lunamedia + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestLunaMediaSyncer(t *testing.T) { + syncURL := "https://api.lunamedia.io/xp/user-sync?acctid={aid}&&redirect=localhost/setuid?bidder=lunamedia&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&uid=$UID" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewLunaMediaSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "A", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://api.lunamedia.io/xp/user-sync?acctid={aid}&&redirect=localhost/setuid?bidder=lunamedia&gdpr=1&gdpr_consent=A&uid=$UID", syncInfo.URL) + assert.Equal(t, "iframe", syncInfo.Type) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 7e4e4196cd9..a7132edbc81 100755 --- a/config/config.go +++ b/config/config.go @@ -522,6 +522,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=184932&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNanoInteractive, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -722,6 +723,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.nanointeractive.endpoint", "https://ad.audiencemanager.de/hbs") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index e6cce7a643b..17814b3639a 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -40,6 +40,7 @@ import ( "github.com/prebid/prebid-server/adapters/kubient" "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/lockerdome" + "github.com/prebid/prebid-server/adapters/lunamedia" "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/mgid" "github.com/prebid/prebid-server/adapters/nanointeractive" @@ -113,6 +114,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderNanoInteractive: nanointeractive.NewNanoIneractiveBidder(cfg.Adapters[string(openrtb_ext.BidderNanoInteractive)].Endpoint), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index aa8e959f7a5..c9b7f7a0519 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -58,6 +58,7 @@ const ( BidderKubient BidderName = "kubient" BidderLifestreet BidderName = "lifestreet" BidderLockerDome BidderName = "lockerdome" + BidderLunaMedia BidderName = "lunamedia" BidderMarsmedia BidderName = "marsmedia" BidderMgid BidderName = "mgid" BidderNanoInteractive BidderName = "nanointeractive" @@ -127,6 +128,7 @@ var BidderMap = map[string]BidderName{ "kubient": BidderKubient, "lifestreet": BidderLifestreet, "lockerdome": BidderLockerDome, + "lunamedia": BidderLunaMedia, "marsmedia": BidderMarsmedia, "mgid": BidderMgid, "nanointeractive": BidderNanoInteractive, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 1d8c8a0794c..42c93d652b3 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -34,6 +34,7 @@ import ( "github.com/prebid/prebid-server/adapters/ix" "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/lockerdome" + "github.com/prebid/prebid-server/adapters/lunamedia" "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/mgid" "github.com/prebid/prebid-server/adapters/nanointeractive" @@ -102,6 +103,7 @@ 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 050e1039000..44ff15bd5fe 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -43,6 +43,7 @@ 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, From 42d52814780de6cecadcdca84aaefd1f594688b7 Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Wed, 6 May 2020 08:38:11 -0700 Subject: [PATCH 073/381] [Sharethrough] Add CCPA support (#1263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handle gzip responses from ad server correctly * Bump to version 8 * [Go Modules] Add proxy (#1079) * Add SSL cert for accessing stored request API (#1087) * [misspell] fix a misspell (#1102) * update static bidder params for rubicon video to follow the json marshalling names (#1100) * Switching yieldmo auction endpoint from http to https (#1103) * Add Datablocks Adapter (#1095) * datablocks bid adapter * ttx * add test json * add coverage * redo ttx * formatted * better error handling * additional tests and recomended fixes * Adding translatecategories flag to includebrandcategory (#1098) * Making IAB category translation optional with translatecategories boolean in request * Updating exchange unit tests to remove extra bids * Updates from code review comments * Removed comment about default TranslateCategories value * Changed translateCat to translateCategories in tests * Combined helper functions in exchange_test related to TranslateCategories * Bid floor (#1085) * Currency handling fix (#1097) * facebook adapter refactor (#1064) * Kubient adapter (#1094) * [synacormedia] Update user sync url to be https (#1115) This detail was missed while setting up the adapter, but we would like to use https for the user sync. * Remove Go 1.11 Build Target (#1109) * Set "Secure" on Same SIte cookies (#1119) * TripleliftNative Adapter (#1114) * ignore swp files * start small * start really small * add a user sync * justify * triplelift adapter * add our endpoint * fix syntax * config stuff * compiler fixes * more config * add params * making progress * make our ext more exty * start making responses * more logic * fix compilation errors * can we just nil this out? * augment our json * radically simplify our json * fix errs * infer the bid type * fix syntax * fix comilation errors * rename * fix compilation error * config stuff * simplify params * more config stuff * fixes * revert this * fix up the extension * getting closer * add a test * update config * update bidder params * add the floor here, too * add a usersync test * validation, ws, and a test * update tests * fix test * update email * why not * change email * preprocess requests * do some parsing * take care of some errors * floor is optional * ws * remove native * everything is either banner or video * this should be a float * floor to floor * fix compilation errors * add some tests * more tests * more tests * simplify * more progress * format * ws * rm * don't need this * fix test * fix test * don't ignore swap * change line back * report an error if there are no valid impressions for triplelift * check for either a Banner or Video object on the impression * more tests * mv * more tests * update triplelift end point * send native * ws * start changing tests * fix more tests * update config * add redirect to triplelift usersync * fix supplier id in triplelift_test * update tl usersync endpoint and test * fix tl supplier id in test json * update usersync test template * adjust inconsistency with test and sync url * mv * update packages * mv * mv * update * fix compilation errors * rename * rename some stuff * rename * rename * fix some compilation errors * ws * ws * add the extra info * add some extra info * add some files back * ws and such * updates * ws * fix compilation error * mv * rename * Revert "rename" This reverts commit 1b77c72e1eeee580148540fbdd880e70bf699709. * Revert "mv" This reverts commit 52a134ddfaf531fe6235e4751935d4266a36e78f. * it builds * cp a file * cp another file * fix a test * fix test * add the extra info * ws * add some logic * edit comment * it compiles * this is now public * call this * add the function * return nil * seems to be working * ws * seems to be working * ws * mv * starting to work * ws * add a new function * ws * fix tests * bug fix * update some stuff * revert * take out prints * fix up diff * fix up diff * update ws * fix * ws * omit the triplelift endppint * Revert "omit the triplelift endppint" This reverts commit 7abc3e46f0fbba39041da6fff7bb2335adc1fece. * populate the endpoint through the extinfo * ws * set disabled to be default * ws * update types * fixing tests * making progres * fix tests * fix tests * more fixes for tests * fixed tests * just use a comment * get rid of endpoint * restore endpoint * add some errors around unmarshalling * ws * ws * use the literal * ws * ws * update json * simplify * ws * restore tests * fail fast when grabbing invcode * use the right type * use a different error type * bump code coverage * add a new test * change error type * ws * break out test into its own function * JSON block that has a full data-center specific URL cache info (#1104) * Update Dockerfile and Makefile (#1099) * Add option for running tests as part of the docker image building * Update Makefile - Add ability to execute adapter specific tests - Execute targets for "all" rather than just printing the target name and usage - Remove use of non-existing "install" target from .PHONY targets - Remove "build" as a dependency for "image" * enable app requests for audience network (#1122) * [docs] fix markdown title (#1124) * Prometheus Refactor (#1108) * update default sync url (#1127) * Update sync url for BidderGrid adapter (#1120) * [SonarCloud] Legacy auction endpoint (#1017) * [currency converter] allow to deduce reverse rate (#1126) This CL allows the currency rate currency to deduce a currency rate even if not directly defined in the table but the reverse rate is present. E.q. USD => EUR is 1.0897 EUR => USD is not set Old behavior when asking rate from EUR to USD will not be found, New behavior is using the known reverse rate to deduce the rate. Rate for 2 USD will be 2 * (1 / 1.0897) * Updated handleError arguments to be pointers for video endpoint (#1128) * Updated handleError arguments to be pointers for video endpoint * Removing unneeded pointer to http.ResponseWriter * Adding units test for update to handleError * Revert changes to GetExtCacheData() made in #1104 (#1130) (#1131) * Better native request validation (#1132) * require the caller to define native assets[...].ID (#1123) * require the caller to define native assets[...].ID * Update assets-with-partial-ids.json * CCPA Phase 1: AMP Endpoint (#1125) * facebook: removed Auth-Token from header (replaced by authentication_id in the request body) (#1113) * Setuid Fix (#1121) * Update http refresh to use url builder. Fixes #1065 (#1133) * Add mapping of user.ext.eids[] for LiveIntent in Rubicon bidder (#1089) * support facebook app_secret config param (#1139) * CCPA Phase 1: Cookie Sync (#1135) * null check banner.h (#1142) * Add Pubnative Adapter (#1134) * Adding the passing of CCPA value to the bid request for video endpoint (#1143) * first draft (#1137) * CCPA Phase 2: Enforcement (#1138) * Gamoshi Adapter: Update cookie sync (#1146) * Simplify static/bidder-params/triplelift_native.json (#1152) * Added US Privacy support in TheMediaGrid server adapter (#1147) * Add TheMediaGrid server adapter * Add video support in TheMediaGrid s2s adapter * Update sync url for TheMediaGrid s2s adapter * Added CCPA support for TheMediaGrid s2s adapter * Fix sync url for TheMediaGrid adapter * CCPA User Sync Updates (#1153) * Marsmedia - add new bidder (#1118) * Add Applogy adapter (#1151) * enforce video.size_id for video imps in rubicon adapter (#1101) * Updated PubMatic endpoint to use https (#1155) * Update Example AppNexus Placement ID (#1160) * Fix Currency Converter Doesn't Output CUR (#1154) * Add custom JSON req/resp data to the analytics logging… (#1145) * Add custom JSON req/resp data to the analytics logging for the /openrtb2/video endpoint. * Add calls in unit tests to cover logging and jsonify of video object. * CCPA User Sync URL Updates (#1157) * Fixes audienceNetwork adapter ignoring banner.format sizes. (#1164) * adding yieldmo vendor id to usersync (#1166) * Add SmartRTB adapter (#1071) * Added new adapter for CPMStar ad network banners and video (#1159) * Update the Conversant sync pixel (#1161) * Add imp.ext.is_rewarded_inventory flag for rewarded video in Rubicon (#1170) * [currencies] fix GetInfo() null ref issue (#1169) This CL fixes the null ref on `RateConverter.GetInfo()` when rates are nil. Issue: #1136 * Fix triplelift User Sync (#1173) * Enhance Message For Cache Errors (#1175) * Fix PubMatic Usersync URL (#1178) Co-authored-by: pm-isha-bharti * [Synacormedia] Add tagId bidder parameter (#1165) * Remove all non-secure calls from eplanning adapter (#1179) * Expose Cache HTTP Settings (#1184) * Adding bid rejection messages to debug response (#1181) * Adds timeout notifications for Facebook (#1182) * VIS.X: added app type support (#1194) * Add Adoppler bidder support. (#1186) * Add Adoppler bidder support. * Address code review comments. Use JSON-templates for testing. * Fix misprint; Add url.PathEscape call for adunit URL parameter. * Adding support for deal prefixes (#1183) * updating default hard-coded list of certs (#1201) Co-authored-by: Shalmali Patil * add admixer adapter (#1195) * Adding copying of gdpr consent string to openrtb bid request (#1189) * Adding copying of gdpr consent string to openrtb bid request * Updated video request to use OpenRTB Video and User objects * Fixing unit test failure message * Updates from code review comments * Updating unit test initialization * Updated mimes array construction * fix conversant sync pixel (#1208) * openx adapter: forward bid response currency in openx adapter if set (#1211) it was always set to the default USD before * add ucfunnel adapter (#1192) * Update required params for TheMediaGrid adapter (#1188) * add zeroclickfraud adapter (#1207) * add zeroclickfraud adapter * fixes for PR * fix casing of Zeroclickfraud * Fix Adform's parameters regex (#1214) * Added adform info file * Added Adform adapter and bidder * Updates from master * Removed usersyncInfo from Adform adapter. Inverted Imp type check. * Removed excessive loop * Updated with the last master * Create readme file for adform * Fix Adform's parameters regex Motivation: catastrophic backtracking during regex execution Details: - https://regex101.com/r/NNQrWq/1 - string to check "url_domain:keskustelu.suomi24.fi,url_path:/matkailu/matkakohteet/aasia,layout:lg,categories:Matkailu,main_category:Matkailu" Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich * If Device.UA is not present in request body, init it with user-agent from header (#1219) * If Device.UA is not present in request body, init it with user-agent from request header if it's present * Moved User-Agent handler to parseVideoRequest func and added unit test * Minor clean up Co-authored-by: Veronika Solovei * Queued request timeout (#1217) Co-authored-by: Veronika Solovei * docs: adding currency support section (#1199) * Add ValueImpression Adapter (#1204) * Kidoz adapter (#1210) Co-authored-by: Ryan Haksi * Update auction.md (#1224) Fix type * Update auction.md (#1225) Fix typo. * Added logging to cache for video endpoint (#1220) * WIP added logging to cache for video endpoint * Updating cache call to use TTL from config * Updates from initial feedback * Log now includes HTTP headers * Fixed caching to use a new cache entry rather than appending to the VAST * Added feature where is query is set, the test flag is set in the request * Updated recorded response and handleError * Updates from code review comments * Changed recorded output to be only the debug ext * Removed extra marhal calls * Changed cache to be an endpoint dependency * Added debugLog struct to hold all debug related info * Numerous smaller changes * Further code cleanup and added unit tests for debug changes * Added missing error checks * Added unit test for error case * added VISX vendor ID for usersyncing (#1229) Co-authored-by: Aadesh Patel * First pass at phase 1 TCF 2.0 support (#1228) * First pass at phase 1 TCF 2.0 support * minor fixes * Update go-gdpr library and fix stuff * Fixes for PR comments * Updated price granularity unmarshal to accept empty values and ranges (#1230) * Update vendorID for TheMediaGrid s2s Bid Adapter (#1232) * treat 204 from FAN as a no bids response (#1233) Co-authored-by: Aadesh Patel * AMP CCPA Fix (#1187) * Update rubicon.md (#1234) * adding schain interface (#1203) * added Rewarded Video section (#1200) also edited all examples so they include the full openRTB context * nanointeractive adapter (#1213) * nanointeractive adapter * nanointeractive adapter, changes after review * nanointeractive adapter * nanointeractive adapter, changes after review * formatting * Typos Fix (#1236) * Fix Typo * Fixed More Typos * Moved hb_pc_cat_dur modification to be before caching (#1250) * Handle CCPA + enable gzip response [#169984259] * Addressing review (#273) [#169984259] * Remove custom gzip logic (#280) * Getting rid of custom gzip logic [#169984259] * Restore prod ad server url [#169984259] Co-authored-by: Benjamin Co-authored-by: guscarreon Co-authored-by: Aadesh Co-authored-by: Winston-Yieldmo <46379634+Winston-Yieldmo@users.noreply.github.com> Co-authored-by: htang555 Co-authored-by: Cameron Rice <37162584+camrice@users.noreply.github.com> Co-authored-by: ah-tappx <46002207+ah-tappx@users.noreply.github.com> Co-authored-by: hhhjort <31041505+hhhjort@users.noreply.github.com> Co-authored-by: Marsel Co-authored-by: Corey Kress Co-authored-by: Scott Kay Co-authored-by: Kevin Kerr Co-authored-by: Mansi Nahar Co-authored-by: Benjamin Co-authored-by: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Co-authored-by: Austin Bischoff Co-authored-by: rpanchyk Co-authored-by: Florian Hartwig Co-authored-by: Salomon Rada Co-authored-by: vladi-mmg Co-authored-by: Aleksei Lin Co-authored-by: PubMatic-OpenWrap Co-authored-by: jmaynardxandr <46759873+jmaynardxandr@users.noreply.github.com> Co-authored-by: evanmsmrtb Co-authored-by: CPMStar Co-authored-by: johnwier <49074029+johnwier@users.noreply.github.com> Co-authored-by: pm-isha-bharti Co-authored-by: Seba Perez Co-authored-by: Michael Kuryshev Co-authored-by: Viacheslav Chimishuk Co-authored-by: Shalmali Patil Co-authored-by: DmitryStashkevich <34479135+DmitryStashkevich@users.noreply.github.com> Co-authored-by: vstatkevich Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich Co-authored-by: Veronika Solovei Co-authored-by: Veronika Solovei Co-authored-by: bretg Co-authored-by: thuyhq <61451682+thuyhq@users.noreply.github.com> Co-authored-by: rhaksi-kidoz <61601767+rhaksi-kidoz@users.noreply.github.com> Co-authored-by: Ryan Haksi Co-authored-by: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Co-authored-by: Aadesh Patel Co-authored-by: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> --- adapters/sharethrough/butler.go | 11 +++++++++++ adapters/sharethrough/butler_test.go | 2 ++ adapters/sharethrough/sharethrough.go | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/adapters/sharethrough/butler.go b/adapters/sharethrough/butler.go index 61081aaa3ff..522bbc4967e 100644 --- a/adapters/sharethrough/butler.go +++ b/adapters/sharethrough/butler.go @@ -7,6 +7,7 @@ import ( "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/privacy/ccpa" "net/http" "net/url" "regexp" @@ -21,6 +22,7 @@ type StrAdSeverParams struct { BidID string ConsentRequired bool ConsentString string + USPrivacySignal string InstantPlayCapable bool Iframe bool Height uint64 @@ -94,6 +96,11 @@ func (s StrOpenRTBTranslator) requestFromOpenRTB(imp openrtb.Imp, request *openr return nil, err } + usPolicySignal := "" + if usPolicy, err := ccpa.ReadPolicy(request); err == nil { + usPolicySignal = usPolicy.Value + } + return &adapters.RequestData{ Method: "POST", Uri: s.UriHelper.buildUri(StrAdSeverParams{ @@ -101,6 +108,7 @@ func (s StrOpenRTBTranslator) requestFromOpenRTB(imp openrtb.Imp, request *openr BidID: imp.ID, ConsentRequired: s.Util.gdprApplies(request), ConsentString: userInfo.Consent, + USPrivacySignal: usPolicySignal, Iframe: strImpParams.Iframe, Height: height, Width: width, @@ -184,6 +192,9 @@ func (h StrUriHelper) buildUri(params StrAdSeverParams) string { v.Set("bidId", params.BidID) v.Set("consent_required", fmt.Sprintf("%t", params.ConsentRequired)) v.Set("consent_string", params.ConsentString) + if params.USPrivacySignal != "" { + v.Set("us_privacy", params.USPrivacySignal) + } if params.TheTradeDeskUserId != "" { v.Set("ttduid", params.TheTradeDeskUserId) } diff --git a/adapters/sharethrough/butler_test.go b/adapters/sharethrough/butler_test.go index 40c59b50442..402e8365dd0 100644 --- a/adapters/sharethrough/butler_test.go +++ b/adapters/sharethrough/butler_test.go @@ -437,6 +437,7 @@ func TestBuildUri(t *testing.T) { BidID: "bid", ConsentRequired: true, ConsentString: "consent", + USPrivacySignal: "ccpa", InstantPlayCapable: true, Iframe: false, Height: 20, @@ -450,6 +451,7 @@ func TestBuildUri(t *testing.T) { "bidId=bid", "consent_required=true", "consent_string=consent", + "us_privacy=ccpa", "instant_play_capable=true", "stayInIframe=false", "height=20", diff --git a/adapters/sharethrough/sharethrough.go b/adapters/sharethrough/sharethrough.go index d1b2408ce66..5e0377ab27a 100644 --- a/adapters/sharethrough/sharethrough.go +++ b/adapters/sharethrough/sharethrough.go @@ -10,7 +10,7 @@ import ( ) const supplyId = "FGMrCMMc" -const strVersion = 7 +const strVersion = 8 func NewSharethroughBidder(endpoint string) *SharethroughAdapter { return &SharethroughAdapter{ From cd909915eeee8b1796243c4f5d1a8d5f46a6737b Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 6 May 2020 11:46:03 -0400 Subject: [PATCH 074/381] Remove Outdated GDPR AMP Special Case (#1283) --- exchange/utils.go | 3 +-- privacy/enforcement.go | 17 ++++--------- privacy/enforcement_test.go | 51 ++----------------------------------- 3 files changed, 8 insertions(+), 63 deletions(-) diff --git a/exchange/utils.go b/exchange/utils.go index d1c95b88b86..f09b11513f1 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -43,7 +43,6 @@ func cleanOpenRTBRequests(ctx context.Context, gdpr := extractGDPR(orig, usersyncIfAmbiguous) consent := extractConsent(orig) - isAMP := labels.RType == pbsmetrics.ReqTypeAMP privacyEnforcement := privacy.Enforcement{ COPPA: orig.Regs != nil && orig.Regs.COPPA == 1, @@ -66,7 +65,7 @@ func cleanOpenRTBRequests(ctx context.Context, privacyEnforcement.GDPR = false } - privacyEnforcement.Apply(bidReq, isAMP) + privacyEnforcement.Apply(bidReq) } return diff --git a/privacy/enforcement.go b/privacy/enforcement.go index caea396c0f6..592b6fb6937 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -17,14 +17,14 @@ func (e Enforcement) Any() bool { } // 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) { + e.apply(bidRequest, NewScrubber()) } -func (e Enforcement) apply(bidRequest *openrtb.BidRequest, isAMP bool, scrubber Scrubber) { +func (e Enforcement) apply(bidRequest *openrtb.BidRequest, 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.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy()) } } @@ -56,18 +56,11 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { return ScrubStrategyGeoNone } -func (e Enforcement) getUserScrubStrategy(isAMP bool) ScrubStrategyUser { +func (e Enforcement) getUserScrubStrategy() ScrubStrategyUser { if e.COPPA { return ScrubStrategyUserFull } - // 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 { - return ScrubStrategyUserNone - } - if e.GDPR || e.CCPA { return ScrubStrategyUserBuyerIDOnly } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index ffc9da5d30c..3bc716b38d2 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -52,7 +52,6 @@ func TestAny(t *testing.T) { func TestApply(t *testing.T) { testCases := []struct { enforcement Enforcement - isAMP bool expectedDeviceMacAndIFA bool expectedDeviceIPv6 ScrubStrategyIPV6 expectedDeviceGeo ScrubStrategyGeo @@ -66,7 +65,6 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: true, }, - isAMP: true, expectedDeviceMacAndIFA: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, @@ -80,7 +78,6 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: false, }, - isAMP: false, expectedDeviceMacAndIFA: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, @@ -94,7 +91,6 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: true, }, - isAMP: false, expectedDeviceMacAndIFA: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, @@ -102,27 +98,12 @@ func TestApply(t *testing.T) { expectedUserGeo: ScrubStrategyGeoReducedPrecision, description: "GDPR", }, - { - enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: true, - }, - isAMP: true, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserNone, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "GDPR For AMP", - }, { enforcement: Enforcement{ CCPA: true, COPPA: false, GDPR: false, }, - isAMP: false, expectedDeviceMacAndIFA: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, @@ -130,34 +111,6 @@ func TestApply(t *testing.T) { expectedUserGeo: ScrubStrategyGeoReducedPrecision, description: "CCPA", }, - { - enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, - }, - isAMP: true, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserBuyerIDOnly, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "CCPA For AMP", - }, - { - enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: true, - }, - isAMP: true, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserNone, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "GDPR And CCPA For AMP", - }, } for _, test := range testCases { @@ -172,7 +125,7 @@ func TestApply(t *testing.T) { 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() - test.enforcement.apply(req, test.isAMP, m) + test.enforcement.apply(req, m) m.AssertExpectations(t) assert.Equal(t, device, req.Device, "Device Set Correctly") @@ -191,7 +144,7 @@ func TestApplyNoneApplicable(t *testing.T) { m := &mockScrubber{} - enforcement.apply(req, true, m) + enforcement.apply(req, m) m.AssertNotCalled(t, "ScrubDevice") m.AssertNotCalled(t, "ScrubUser") From cc3d2daa4a3a71161b153c912ebc621e3f5c0c17 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 6 May 2020 14:59:32 -0400 Subject: [PATCH 075/381] Stricter Privacy Scrubbing (#1286) * Stricter Privacy Scrubbing * Update Unit Test Style * Fixed Whitespace --- go.mod | 4 +- go.sum | 4 + privacy/enforcement.go | 18 +-- privacy/enforcement_test.go | 106 ++++++++--------- privacy/scrubber.go | 51 +++------ privacy/scrubber_test.go | 220 ++++++++++++++++++++++++++---------- 6 files changed, 244 insertions(+), 159 deletions(-) diff --git a/go.mod b/go.mod index 387b8b9815c..89cc69e4519 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 // indirect github.com/spf13/pflag v1.0.2 // indirect github.com/spf13/viper v1.1.0 - github.com/stretchr/testify v1.3.0 + github.com/stretchr/testify v1.5.1 github.com/valyala/fasthttp v1.9.0 github.com/vrischmann/go-metrics-influxdb v0.0.0-20160917065939-43af8332c303 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect @@ -70,5 +70,5 @@ require ( golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/text v0.3.0 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect - gopkg.in/yaml.v2 v2.2.1 + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index ad9caf5004b..f929408f0f3 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw= @@ -196,3 +198,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/privacy/enforcement.go b/privacy/enforcement.go index 592b6fb6937..0230ca6b9af 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -23,15 +23,11 @@ func (e Enforcement) Apply(bidRequest *openrtb.BidRequest) { func (e Enforcement) apply(bidRequest *openrtb.BidRequest, 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(), e.getGeoScrubStrategy()) + bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) + bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getDemographicScrubStrategy(), e.getGeoScrubStrategy()) } } -func (e Enforcement) getDeviceMacAndIFA() bool { - return e.COPPA -} - func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 { if e.COPPA { return ScrubStrategyIPV6Lowest32 @@ -56,14 +52,10 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { return ScrubStrategyGeoNone } -func (e Enforcement) getUserScrubStrategy() ScrubStrategyUser { +func (e Enforcement) getDemographicScrubStrategy() ScrubStrategyDemographic { if e.COPPA { - return ScrubStrategyUserFull - } - - if e.GDPR || e.CCPA { - return ScrubStrategyUserBuyerIDOnly + return ScrubStrategyDemographicAgeAndGender } - return ScrubStrategyUserNone + return ScrubStrategyDemographicNone } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 3bc716b38d2..c7433f8b271 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -15,31 +15,31 @@ func TestAny(t *testing.T) { description string }{ { + description: "All False", enforcement: Enforcement{ CCPA: false, COPPA: false, GDPR: false, }, - expected: false, - description: "All False", + expected: false, }, { + description: "All True", enforcement: Enforcement{ CCPA: true, COPPA: true, GDPR: true, }, - expected: true, - description: "All True", + expected: true, }, { + description: "Mixed", enforcement: Enforcement{ CCPA: false, COPPA: true, GDPR: false, }, - expected: true, - description: "Mixed", + expected: true, }, } @@ -51,117 +51,119 @@ func TestAny(t *testing.T) { func TestApply(t *testing.T) { testCases := []struct { + description string enforcement Enforcement - expectedDeviceMacAndIFA bool expectedDeviceIPv6 ScrubStrategyIPV6 expectedDeviceGeo ScrubStrategyGeo - expectedUser ScrubStrategyUser + expectedUserDemographic ScrubStrategyDemographic expectedUserGeo ScrubStrategyGeo - description string }{ { + description: "All Enforced", enforcement: Enforcement{ CCPA: true, COPPA: true, GDPR: true, }, - expectedDeviceMacAndIFA: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, - expectedUser: ScrubStrategyUserFull, + expectedUserDemographic: ScrubStrategyDemographicAgeAndGender, expectedUserGeo: ScrubStrategyGeoFull, - description: "All Enforced - Most Strict", }, { + description: "CCPA Only", + enforcement: Enforcement{ + CCPA: true, + COPPA: false, + GDPR: false, + }, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUserDemographic: ScrubStrategyDemographicNone, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "COPPA Only", enforcement: Enforcement{ CCPA: false, COPPA: true, GDPR: false, }, - expectedDeviceMacAndIFA: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, - expectedUser: ScrubStrategyUserFull, + expectedUserDemographic: ScrubStrategyDemographicAgeAndGender, expectedUserGeo: ScrubStrategyGeoFull, - description: "COPPA", }, { + description: "GDPR Only", enforcement: Enforcement{ CCPA: false, COPPA: false, GDPR: true, }, - expectedDeviceMacAndIFA: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserBuyerIDOnly, + expectedUserDemographic: ScrubStrategyDemographicNone, expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "GDPR", - }, - { - enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, - }, - expectedDeviceMacAndIFA: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserBuyerIDOnly, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - description: "CCPA", }, } 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.expectedUserDemographic, test.expectedUserGeo).Return(replacedUser).Once() test.enforcement.apply(req, 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, + } enforcement.apply(req, 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") +} + +func TestApplyNil(t *testing.T) { + m := &mockScrubber{} + + enforcement := Enforcement{} + enforcement.apply(nil, m) + + m.AssertNotCalled(t, "ScrubDevice") + m.AssertNotCalled(t, "ScrubUser") } 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) } -func (m *mockScrubber) ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User { - args := m.Called(user, strategy, geo) +func (m *mockScrubber) ScrubUser(user *openrtb.User, demographic ScrubStrategyDemographic, geo ScrubStrategyGeo) *openrtb.User { + args := m.Called(user, demographic, geo) return args.Get(0).(*openrtb.User) } diff --git a/privacy/scrubber.go b/privacy/scrubber.go index 916b660dcc5..45b79c20a4e 100644 --- a/privacy/scrubber.go +++ b/privacy/scrubber.go @@ -34,24 +34,21 @@ const ( ScrubStrategyGeoReducedPrecision ) -// ScrubStrategyUser defines the approach to scrub PII from user data. -type ScrubStrategyUser int +// ScrubStrategyDemographic defines the approach to non-location demographic data. +type ScrubStrategyDemographic int const ( - // ScrubStrategyUserNone does not remove user data. - ScrubStrategyUserNone ScrubStrategyUser = iota + // ScrubStrategyDemographicNone does not remove non-location demographic data. + ScrubStrategyDemographicNone ScrubStrategyDemographic = iota - // ScrubStrategyUserFull removes the user's buyer id, exchange id year of birth, and gender. - ScrubStrategyUserFull - - // ScrubStrategyUserBuyerIDOnly removes the user's buyer id. - ScrubStrategyUserBuyerIDOnly + // ScrubStrategyDemographicAgeAndGender removes age and gender data. + ScrubStrategyDemographicAgeAndGender ) // Scrubber removes PII from parts of an OpenRTB request. type Scrubber interface { - ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device - ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User + ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device + ScrubUser(user *openrtb.User, demographic ScrubStrategyDemographic, geo ScrubStrategyGeo) *openrtb.User } type scrubber struct{} @@ -61,25 +58,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) @@ -97,21 +90,19 @@ func (scrubber) ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubSt return &deviceCopy } -func (scrubber) ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User { +func (scrubber) ScrubUser(user *openrtb.User, demographic ScrubStrategyDemographic, geo ScrubStrategyGeo) *openrtb.User { if user == nil { return nil } userCopy := *user + userCopy.BuyerUID = "" + userCopy.ID = "" - switch strategy { - case ScrubStrategyUserFull: - userCopy.BuyerUID = "" - userCopy.ID = "" + switch demographic { + case ScrubStrategyDemographicAgeAndGender: userCopy.Yob = 0 userCopy.Gender = "" - case ScrubStrategyUserBuyerIDOnly: - userCopy.BuyerUID = "" } switch geo { @@ -169,13 +160,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 { diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go index 168fb5fb23e..2d5ee667538 100644 --- a/privacy/scrubber_test.go +++ b/privacy/scrubber_test.go @@ -28,13 +28,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 +47,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 +64,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 +81,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 +104,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,41 +173,72 @@ 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", + }, }, - isMacAndIFA: false, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoFull, - description: "Without MAC Address And IFA Scrubbing", + 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", + }, + }, + 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", Geo: &openrtb.Geo{ @@ -179,52 +252,77 @@ func TestScrubUser(t *testing.T) { testCases := []struct { expected *openrtb.User - strategy ScrubStrategyUser + demographic ScrubStrategyDemographic geo ScrubStrategyGeo description string }{ { + description: "Demographic Age And Gender & Geo Full", expected: &openrtb.User{ - BuyerUID: "", ID: "", + BuyerUID: "", Yob: 0, Gender: "", Geo: &openrtb.Geo{}, }, - strategy: ScrubStrategyUserFull, + demographic: ScrubStrategyDemographicAgeAndGender, geo: ScrubStrategyGeoFull, - description: "Full Scrubbing", }, { + description: "Demographic Age And Gender & Geo Reduced", expected: &openrtb.User{ + ID: "", BuyerUID: "", - ID: "anyID", - Yob: 42, - Gender: "anyGender", - Geo: &openrtb.Geo{}, + Yob: 0, + Gender: "", + Geo: &openrtb.Geo{ + Lat: 123.46, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, }, - strategy: ScrubStrategyUserBuyerIDOnly, - geo: ScrubStrategyGeoFull, - description: "User Buyer ID Only", + demographic: ScrubStrategyDemographicAgeAndGender, + geo: ScrubStrategyGeoReducedPrecision, + }, + { + description: "Demographic Age And Gender & Geo None", + expected: &openrtb.User{ + ID: "", + BuyerUID: "", + Yob: 0, + Gender: "", + Geo: &openrtb.Geo{ + Lat: 123.456, + Lon: 678.89, + Metro: "some metro", + City: "some city", + ZIP: "some zip", + }, + }, + demographic: ScrubStrategyDemographicAgeAndGender, + geo: ScrubStrategyGeoNone, }, { + description: "Demographic None & Geo Full", expected: &openrtb.User{ - BuyerUID: "anyBuyerUID", - ID: "anyID", + ID: "", + BuyerUID: "", Yob: 42, Gender: "anyGender", Geo: &openrtb.Geo{}, }, - strategy: ScrubStrategyUserNone, + demographic: ScrubStrategyDemographicNone, geo: ScrubStrategyGeoFull, - description: "User None", }, { + description: "Demographic None & Geo Reduced", expected: &openrtb.User{ - BuyerUID: "", ID: "", - Yob: 0, - Gender: "", + BuyerUID: "", + Yob: 42, + Gender: "anyGender", Geo: &openrtb.Geo{ Lat: 123.46, Lon: 678.89, @@ -233,16 +331,16 @@ func TestScrubUser(t *testing.T) { ZIP: "some zip", }, }, - strategy: ScrubStrategyUserFull, + demographic: ScrubStrategyDemographicNone, geo: ScrubStrategyGeoReducedPrecision, - description: "Geo Reduced Precision", }, { + description: "Demographic None & Geo None", expected: &openrtb.User{ - BuyerUID: "", ID: "", - Yob: 0, - Gender: "", + BuyerUID: "", + Yob: 42, + Gender: "anyGender", Geo: &openrtb.Geo{ Lat: 123.456, Lon: 678.89, @@ -251,18 +349,22 @@ func TestScrubUser(t *testing.T) { ZIP: "some zip", }, }, - strategy: ScrubStrategyUserFull, + demographic: ScrubStrategyDemographicNone, geo: ScrubStrategyGeoNone, - description: "Geo None", }, } for _, test := range testCases { - result := NewScrubber().ScrubUser(user, test.strategy, test.geo) + result := NewScrubber().ScrubUser(user, test.demographic, test.geo) assert.Equal(t, test.expected, result, test.description) } } +func TestScrubUserNil(t *testing.T) { + result := NewScrubber().ScrubUser(nil, ScrubStrategyDemographicNone, ScrubStrategyGeoNone) + assert.Nil(t, result) +} + func TestScrubIPV4(t *testing.T) { testCases := []struct { IP string From 2481fea01f9b339f5455d93a370e2550c58ac087 Mon Sep 17 00:00:00 2001 From: Arne Schulz Date: Mon, 11 May 2020 17:09:33 +0200 Subject: [PATCH 076/381] Add Adapter Orbidder (#1275) Co-authored-by: Volk, Rainer Co-authored-by: RainerVolk4014 <53347752+RainerVolk4014@users.noreply.github.com> Co-authored-by: rvolk <> Co-authored-by: Hendrik Iseke Co-authored-by: hendrikiseke1979 <53309111+hendrikiseke1979@users.noreply.github.com> --- adapters/orbidder/orbidder.go | 127 ++++++++++++++++++ adapters/orbidder/orbidder_test.go | 25 ++++ .../exemplary/simple-app-banner.json | 111 +++++++++++++++ .../orbiddertest/params/race/banner.json | 5 + .../supplemental/dsp-bad-request-example.json | 78 +++++++++++ .../dsp-bad-response-example.json | 78 +++++++++++ .../dsp-internal-server-error-example.json | 78 +++++++++++ .../dsp-invalid-accountid-example.json | 78 +++++++++++ .../supplemental/empty-imp-request-error.json | 19 +++ .../supplemental/ext-unmarshall-error.json | 32 +++++ .../supplemental/no-content-response.json | 73 ++++++++++ .../supplemental/valid-and-invalid-imps.json | 123 +++++++++++++++++ adapters/orbidder/params_test.go | 65 +++++++++ config/config.go | 1 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_orbidder.go | 8 ++ static/bidder-info/orbidder.yaml | 9 ++ static/bidder-params/orbidder.json | 24 ++++ usersync/usersyncers/syncer_test.go | 1 + 20 files changed, 939 insertions(+) create mode 100644 adapters/orbidder/orbidder.go create mode 100644 adapters/orbidder/orbidder_test.go create mode 100644 adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json create mode 100644 adapters/orbidder/orbiddertest/params/race/banner.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/empty-imp-request-error.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/ext-unmarshall-error.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/no-content-response.json create mode 100644 adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json create mode 100644 adapters/orbidder/params_test.go create mode 100644 openrtb_ext/imp_orbidder.go create mode 100644 static/bidder-info/orbidder.yaml create mode 100644 static/bidder-params/orbidder.json diff --git a/adapters/orbidder/orbidder.go b/adapters/orbidder/orbidder.go new file mode 100644 index 00000000000..27b41c89857 --- /dev/null +++ b/adapters/orbidder/orbidder.go @@ -0,0 +1,127 @@ +package orbidder + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" +) + +type OrbidderAdapter struct { + endpoint string +} + +// MakeRequests makes the HTTP requests which should be made to fetch bids from orbidder. +func (rcv *OrbidderAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var validImps []openrtb.Imp + + // check if imps exists, if not return error and do send request to orbidder. + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: "No impressions in request", + }} + } + + // validate imps + for _, imp := range request.Imp { + if err := preprocess(&imp); err != nil { + errs = append(errs, err) + continue + } + validImps = append(validImps, imp) + } + + if len(validImps) == 0 { + return nil, errs + } + + //set imp array to only valid imps + request.Imp = validImps + + requestBodyJSON, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: rcv.endpoint, + Body: requestBodyJSON, + Headers: headers, + }}, errs +} + +func preprocess(imp *openrtb.Imp) error { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + var orbidderExt openrtb_ext.ExtImpOrbidder + if err := json.Unmarshal(bidderExt.Bidder, &orbidderExt); err != nil { + return &errortypes.BadInput{ + Message: "Wrong orbidder bidder ext: " + err.Error(), + } + } + + return nil +} + +// MakeBids unpacks server response into Bids. +func (rcv OrbidderAdapter) 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.StatusInternalServerError { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Dsp server internal error.", response.StatusCode), + }} + } + + if response.StatusCode >= http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d. Bad request to dsp.", response.StatusCode), + }} + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Bad response from dsp.", response.StatusCode), + }} + } + + var bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + + for _, seatBid := range bidResp.SeatBid { + for _, bid := range seatBid.Bid { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: openrtb_ext.BidTypeBanner, + }) + } + } + return bidResponse, nil +} + +func NewOrbidderBidder(endpoint string) *OrbidderAdapter { + return &OrbidderAdapter{ + endpoint: endpoint, + } +} diff --git a/adapters/orbidder/orbidder_test.go b/adapters/orbidder/orbidder_test.go new file mode 100644 index 00000000000..e0f7a6b4265 --- /dev/null +++ b/adapters/orbidder/orbidder_test.go @@ -0,0 +1,25 @@ +package orbidder + +import ( + "encoding/json" + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUnmarshalOrbidderExtImp(t *testing.T) { + ext := json.RawMessage(`{"accountId":"orbidder-test", "placementId":"center-banner", "bidfloor": 0.1}`) + impExt := new(openrtb_ext.ExtImpOrbidder) + + assert.NoError(t, json.Unmarshal(ext, impExt)) + assert.Equal(t, &openrtb_ext.ExtImpOrbidder{ + AccountId: "orbidder-test", + PlacementId: "center-banner", + BidFloor: 0.1, + }, impExt) +} + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "orbiddertest", NewOrbidderBidder("https://orbidder-test")) +} diff --git a/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json b/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json new file mode 100644 index 00000000000..8697bff3a92 --- /dev/null +++ b/adapters/orbidder/orbiddertest/exemplary/simple-app-banner.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/orbidder/orbiddertest/params/race/banner.json b/adapters/orbidder/orbiddertest/params/race/banner.json new file mode 100644 index 00000000000..bdb0e010e05 --- /dev/null +++ b/adapters/orbidder/orbiddertest/params/race/banner.json @@ -0,0 +1,5 @@ +{ + "accountId": "orbidder-test", + "placementId": "center-banner", + "bidfloor": 0.1 +} \ No newline at end of file diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json new file mode 100644 index 00000000000..69496c4ff3f --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-request-example.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": { + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Bad request to dsp.", + "comparison": "literal" + } + ] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json new file mode 100644 index 00000000000..d1c57a54a9e --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-bad-response-example.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 300, + "body": { + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 300. Bad response from dsp.", + "comparison": "literal" + } + ] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json new file mode 100644 index 00000000000..20ea36ab38c --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-internal-server-error-example.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 500, + "body": { + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Dsp server internal error.", + "comparison": "literal" + } + ] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json b/adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json new file mode 100644 index 00000000000..6bc0482dd0c --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/dsp-invalid-accountid-example.json @@ -0,0 +1,78 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 403, + "body": { + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 403. Bad request to dsp.", + "comparison": "literal" + } + ] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/empty-imp-request-error.json b/adapters/orbidder/orbiddertest/supplemental/empty-imp-request-error.json new file mode 100644 index 00000000000..0c5cf6d2faa --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/empty-imp-request-error.json @@ -0,0 +1,19 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + ], + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impressions in request", + "comparison": "literal" + } + ] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/ext-unmarshall-error.json b/adapters/orbidder/orbiddertest/supplemental/ext-unmarshall-error.json new file mode 100644 index 00000000000..447e2985f92 --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/ext-unmarshall-error.json @@ -0,0 +1,32 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "ext": { + "bidder": { + "accountId": [] + } + } + } + ], + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa": "87857b31-8942-4646-ae80-ab9c95bf3fab" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Wrong orbidder bidder ext: json: cannot unmarshal array into Go struct field ExtImpOrbidder.accountId of type string", + "comparison": "literal" + } + ] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/no-content-response.json b/adapters/orbidder/orbiddertest/supplemental/no-content-response.json new file mode 100644 index 00000000000..f3b1b287da7 --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/no-content-response.json @@ -0,0 +1,73 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": { + } + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json b/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json new file mode 100644 index 00000000000..b6db9f48ee3 --- /dev/null +++ b/adapters/orbidder/orbiddertest/supplemental/valid-and-invalid-imps.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + }, + { + "id": "test-imp-id-INVALID", + "banner": { + "format": [{}] + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://orbidder-test", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "accountId": "orbidder-test", + "placementId": "test-placement", + "bidfloor": 0.1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat-id", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "adid": "11110126", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ], + "expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/orbidder/params_test.go b/adapters/orbidder/params_test.go new file mode 100644 index 00000000000..19c4ed8d9d4 --- /dev/null +++ b/adapters/orbidder/params_test.go @@ -0,0 +1,65 @@ +package orbidder + +import ( + "encoding/json" + "github.com/prebid/prebid-server/openrtb_ext" + "testing" +) + +// This file actually intends to test static/bidder-params/orbidder.json +// +// These also validate the format of the external API: request.imp[i].ext.orbidder + +// TestValidParams makes sure that the orbidder 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.BidderOrbidder, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected orbidder params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the orbidder 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.BidderOrbidder, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"placementId":"123","accountId":"orbidder-test"}`, + `{"placementId":"123","accountId":"orbidder-test","bidfloor":0.5}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"placement_id":"123"}`, + `{"placementId":123}`, + `{"placementId":"123"}`, + `{"account_id":"orbidder-test"}`, + `{"accountId":123}`, + `{"accountId":"orbidder-test"}`, + `{"placementId":123,"account_id":"orbidder-test"}`, + `{"placementId":"123","account_id":123}`, + `{"placementId":"123","accountId":"orbidder-test","bidfloor":"0.5"}`, + `{"placementId":"123","bidfloor":"0.5"}`, + `{"accountId":"orbidder-test","bidfloor":"0.5"}`, +} diff --git a/config/config.go b/config/config.go index a7132edbc81..ccdb5c0625b 100755 --- a/config/config.go +++ b/config/config.go @@ -729,6 +729,7 @@ func SetupViper(v *viper.Viper, filename string) { 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 17814b3639a..2029d1a7553 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -46,6 +46,7 @@ import ( "github.com/prebid/prebid-server/adapters/nanointeractive" "github.com/prebid/prebid-server/adapters/ninthdecimal" "github.com/prebid/prebid-server/adapters/openx" + "github.com/prebid/prebid-server/adapters/orbidder" "github.com/prebid/prebid-server/adapters/pubmatic" "github.com/prebid/prebid-server/adapters/pubnative" "github.com/prebid/prebid-server/adapters/pulsepoint" @@ -119,6 +120,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter openrtb_ext.BidderMgid: mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index c9b7f7a0519..01112091cdf 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -64,6 +64,7 @@ const ( BidderNanoInteractive BidderName = "nanointeractive" BidderNinthDecimal BidderName = "ninthdecimal" BidderOpenx BidderName = "openx" + BidderOrbidder BidderName = "orbidder" BidderPubmatic BidderName = "pubmatic" BidderPubnative BidderName = "pubnative" BidderPulsepoint BidderName = "pulsepoint" @@ -134,6 +135,7 @@ var BidderMap = map[string]BidderName{ "nanointeractive": BidderNanoInteractive, "ninthdecimal": BidderNinthDecimal, "openx": BidderOpenx, + "orbidder": BidderOrbidder, "pubmatic": BidderPubmatic, "pubnative": BidderPubnative, "pulsepoint": BidderPulsepoint, 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/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-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/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 44ff15bd5fe..88c1b9467a6 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -83,6 +83,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderKubient: true, openrtb_ext.BidderPubnative: true, openrtb_ext.BidderKidoz: true, + openrtb_ext.BidderOrbidder: true, } for bidder, config := range cfg.Adapters { From 4257bf1ed9dc1dd2647c4f463890b95a775147ab Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Tue, 12 May 2020 07:27:03 -0700 Subject: [PATCH 077/381] Added OpenX Bidder adapter documentation (#1291) --- docs/bidders/openx.md | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/bidders/openx.md 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 From 93b8a0edb8e418f4e360469d6d136995ddf45dc1 Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Wed, 13 May 2020 10:12:14 -0700 Subject: [PATCH 078/381] OpenX adapter: Pass rewarded video flag (#1290) --- adapters/openx/openx.go | 8 ++ .../openxtest/exemplary/video-rewarded.json | 102 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 adapters/openx/openxtest/exemplary/video-rewarded.json diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go index 63e8e697869..63297d0a4ee 100644 --- a/adapters/openx/openx.go +++ b/adapters/openx/openx.go @@ -142,6 +142,14 @@ func preprocess(imp *openrtb.Imp, reqExt *openxReqExt) error { } } + if imp.Video != nil { + if bidderExt.Prebid != nil && bidderExt.Prebid.IsRewardedInventory == 1 { + imp.Video.Ext = json.RawMessage(`{"rewarded":1}`) + } else { + imp.Video.Ext = nil + } + } + return nil } diff --git a/adapters/openx/openxtest/exemplary/video-rewarded.json b/adapters/openx/openxtest/exemplary/video-rewarded.json new file mode 100644 index 00000000000..b16a92f23ac --- /dev/null +++ b/adapters/openx/openxtest/exemplary/video-rewarded.json @@ -0,0 +1,102 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576, + "ext": { + "foo": "bar" + } + }, + "instl": 1, + "ext": { + "bidder": { + "unit": "539439964", + "delDomain": "se-demo-d.openx.net" + }, + "prebid": { + "is_rewarded_inventory": 1 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.openx.net/prebid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576, + "ext": { + "rewarded": 1 + } + }, + "tagid": "539439964", + "instl": 1 + } + ], + "ext": { + "bc": "hb_pbs_1.0.0", + "delDomain": "se-demo-d.openx.net" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "openx", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576 + }] + } + ] + } + } + } + ], + + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + } + ] +} From c18a2d86c5bad5987008b3df485072ebf471683e Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Thu, 14 May 2020 07:35:49 -0700 Subject: [PATCH 079/381] Bugfix for missing fields in imp.video (#1297) Co-authored-by: Veronika Solovei --- endpoints/openrtb2/video_auction.go | 8 +++----- endpoints/openrtb2/video_auction_test.go | 25 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index c7316604d73..cf764bc9d2d 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -381,11 +381,9 @@ func max(a, b int) int { } func createImpressionTemplate(imp openrtb.Imp, video *openrtb.Video) 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 + //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 } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index ec525c6ff08..38c9dc3f685 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1005,6 +1005,31 @@ func TestHandleErrorDebugLog(t *testing.T) { 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 mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) mockModule := &mockAnalyticsModule{} From 9f7ed209b685c3135eee56a64f963476268716a9 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 14 May 2020 17:44:06 +0300 Subject: [PATCH 080/381] Add cpmOverride (#1289) * Add cpmOverride Enabled `request.ext.rubicon.debug.cpmOverride` and `request.imp[].ext.rubicon.debug.cpmOverride` processing. Updates tests * Remove unnecessary error checks and add shallow copy * Fixed same pointer --- adapters/rubicon/rubicon.go | 71 ++++++++++++++++++++--- adapters/rubicon/rubicon_test.go | 97 +++++++++++++++++++++++++++++++- openrtb_ext/imp_rubicon.go | 6 ++ 3 files changed, 163 insertions(+), 11 deletions(-) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index dad85ee1184..ee737bd05ea 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -48,6 +48,18 @@ type rubiconParams struct { Video rubiconVideoParams `json:"video"` } +type bidRequestExt struct { + Rubicon bidRequestExtRubicon `json:"rubicon,omitempty"` +} + +type bidRequestExtRubicon struct { + Debug bidRequestExtRubiconDebug `json:"debug,omitempty"` +} + +type bidRequestExtRubiconDebug struct { + CpmOverride float64 `json:"cpmOverride,omitempty"` +} + type rubiconImpExtRPTrack struct { Mint string `json:"mint"` MintVersion string `json:"mint_version"` @@ -578,6 +590,7 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap requestImpCopy := request.Imp + rubiconRequest := *request for i := 0; i < numRequests; i++ { thisImp := requestImpCopy[i] @@ -677,14 +690,14 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap errs = append(errs, err) continue } - request.User = &userCopy + rubiconRequest.User = &userCopy } if request.Device != nil { deviceCopy := *request.Device deviceExt := rubiconDeviceExt{RP: rubiconDeviceExtRP{PixelRatio: request.Device.PxRatio}} deviceCopy.Ext, err = json.Marshal(&deviceExt) - request.Device = &deviceCopy + rubiconRequest.Device = &deviceCopy } isVideo := isVideo(thisImp) @@ -732,28 +745,28 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap siteCopy.Ext, err = json.Marshal(&siteExt) siteCopy.Publisher = &openrtb.Publisher{} siteCopy.Publisher.Ext, err = json.Marshal(&pubExt) - request.Site = &siteCopy + rubiconRequest.Site = &siteCopy } if request.App != nil { appCopy := *request.App appCopy.Ext, err = json.Marshal(&siteExt) appCopy.Publisher = &openrtb.Publisher{} appCopy.Publisher.Ext, err = json.Marshal(&pubExt) - request.App = &appCopy + rubiconRequest.App = &appCopy } reqBadv := request.BAdv if reqBadv != nil { if len(reqBadv) > badvLimitSize { - request.BAdv = reqBadv[:badvLimitSize] + rubiconRequest.BAdv = reqBadv[:badvLimitSize] } } - request.Imp = []openrtb.Imp{thisImp} - request.Cur = nil - request.Ext = nil + rubiconRequest.Imp = []openrtb.Imp{thisImp} + rubiconRequest.Cur = nil + rubiconRequest.Ext = nil - reqJSON, err := json.Marshal(request) + reqJSON, err := json.Marshal(rubiconRequest) if err != nil { errs = append(errs, err) continue @@ -900,9 +913,22 @@ func (a *RubiconAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalR bidType = openrtb_ext.BidTypeVideo } + impToCpmOverride := mapImpIdToCpmOverride(internalRequest.Imp) + cmpOverride := cmpOverrideFromBidRequest(internalRequest) + for _, sb := range bidResp.SeatBid { for i := 0; i < len(sb.Bid); i++ { bid := sb.Bid[i] + + bidCmpOverride, ok := impToCpmOverride[bid.ImpID] + if !ok || bidCmpOverride == 0 { + bidCmpOverride = cmpOverride + } + + if bidCmpOverride > 0 { + bid.Price = bidCmpOverride + } + if bid.Price != 0 { // Since Rubicon XAPI returns only one bid per response // copy response.bidid to openrtb_response.seatbid.bid.bidid @@ -919,3 +945,30 @@ func (a *RubiconAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalR return bidResponse, nil } + +func cmpOverrideFromBidRequest(bidRequest *openrtb.BidRequest) float64 { + var bidRequestExt bidRequestExt + if err := json.Unmarshal(bidRequest.Ext, &bidRequestExt); err != nil { + return 0 + } + + return bidRequestExt.Rubicon.Debug.CpmOverride +} + +func mapImpIdToCpmOverride(imps []openrtb.Imp) map[string]float64 { + impIdToCmpOverride := make(map[string]float64) + for _, imp := range imps { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + continue + } + + var rubiconExt openrtb_ext.ExtImpRubicon + if err := json.Unmarshal(bidderExt.Bidder, &rubiconExt); err != nil { + continue + } + + impIdToCmpOverride[imp.ID] = rubiconExt.Debug.CpmOverride + } + return impIdToCmpOverride +} diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index 96623659d08..7a2cc28896b 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -973,9 +973,9 @@ func TestOpenRTBRequest(t *testing.T) { } assert.Equal(t, request.ID, rpRequest.ID, "Bad Request ID. Expected %s, Got %s", request.ID, rpRequest.ID) - assert.Equal(t, len(request.Imp), len(rpRequest.Imp), "Wrong len(request.Imp). Expected %d, Got %d", len(request.Imp), len(rpRequest.Imp)) + assert.Equal(t, 1, len(rpRequest.Imp), "Wrong len(request.Imp). Expected %d, Got %d", len(request.Imp), len(rpRequest.Imp)) assert.Nil(t, rpRequest.Cur, "Wrong request.Cur. Expected nil, Got %s", rpRequest.Cur) - assert.Nil(t, request.Ext, "Wrong request.ext. Expected nil, Got %v", request.Ext) + assert.Nil(t, rpRequest.Ext, "Wrong request.ext. Expected nil, Got %v", rpRequest.Ext) if rpRequest.Imp[0].ID == "test-imp-banner-id" { var rpExt rubiconBannerExt @@ -1425,6 +1425,99 @@ func TestOpenRTBStandardResponse(t *testing.T) { assert.Equal(t, "1234567890", theBid.ID, "Bad bid ID. Expected %s, got %s", "1234567890", theBid.ID) } +func TestOpenRTBResponseOverridePriceFromBidRequest(t *testing.T) { + request := &openrtb.BidRequest{ + ID: "test-request-id", + Imp: []openrtb.Imp{{ + ID: "test-imp-id", + Banner: &openrtb.Banner{ + Format: []openrtb.Format{{ + W: 320, + H: 50, + }}, + }, + Ext: json.RawMessage(`{"bidder": { + "accountId": 2763, + "siteId": 68780, + "zoneId": 327642 + }}`), + }}, + Ext: json.RawMessage(`{"rubicon": { + "debug": { + "cpmOverride" : 10 + }}}`), + } + + requestJson, _ := json.Marshal(request) + reqData := &adapters.RequestData{ + Method: "POST", + Uri: "test-uri", + Body: requestJson, + Headers: nil, + } + + httpResp := &adapters.ResponseData{ + StatusCode: http.StatusOK, + Body: []byte(`{"id":"test-request-id","seatbid":[{"bid":[{"id":"1234567890","impid":"test-imp-id","price": 2,"crid":"4122982","adm":"some ad","h": 50,"w": 320,"ext":{"bidder":{"rp":{"targeting": {"key": "rpfl_2763", "values":["43_tier0100"]},"mime": "text/html","size_id": 43}}}}]}]}`), + } + + bidder := new(RubiconAdapter) + bidResponse, errs := bidder.MakeBids(request, reqData, httpResp) + + assert.Empty(t, errs, "Expected 0 errors. Got %d", len(errs)) + + assert.Equal(t, float64(10), bidResponse.Bids[0].Bid.Price, + "Expected Price 10. Got: %s", bidResponse.Bids[0].Bid.Price) +} + +func TestOpenRTBResponseOverridePriceFromCorrespondingImp(t *testing.T) { + request := &openrtb.BidRequest{ + ID: "test-request-id", + Imp: []openrtb.Imp{{ + ID: "test-imp-id", + Banner: &openrtb.Banner{ + Format: []openrtb.Format{{ + W: 320, + H: 50, + }}, + }, + Ext: json.RawMessage(`{"bidder": { + "accountId": 2763, + "siteId": 68780, + "zoneId": 327642, + "debug": { + "cpmOverride" : 20 + } + }}`), + }}, + Ext: json.RawMessage(`{"rubicon": { + "debug": { + "cpmOverride" : 10 + }}}`), + } + + requestJson, _ := json.Marshal(request) + reqData := &adapters.RequestData{ + Method: "POST", + Uri: "test-uri", + Body: requestJson, + Headers: nil, + } + + httpResp := &adapters.ResponseData{ + StatusCode: http.StatusOK, + Body: []byte(`{"id":"test-request-id","seatbid":[{"bid":[{"id":"1234567890","impid":"test-imp-id","price": 2,"crid":"4122982","adm":"some ad","h": 50,"w": 320,"ext":{"bidder":{"rp":{"targeting": {"key": "rpfl_2763", "values":["43_tier0100"]},"mime": "text/html","size_id": 43}}}}]}]}`), + } + + bidder := new(RubiconAdapter) + bidResponse, errs := bidder.MakeBids(request, reqData, httpResp) + + assert.Empty(t, errs, "Expected 0 errors. Got %d", len(errs)) + + assert.Equal(t, float64(20), bidResponse.Bids[0].Bid.Price, + "Expected Price 20. Got: %s", bidResponse.Bids[0].Bid.Price) +} + func TestOpenRTBCopyBidIdFromResponseIfZero(t *testing.T) { request := &openrtb.BidRequest{ ID: "test-request-id", diff --git a/openrtb_ext/imp_rubicon.go b/openrtb_ext/imp_rubicon.go index d588af82184..17585a8ee93 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"` +} From 6e5d0445ad29a9af1259a175843cb3f531cacd5a Mon Sep 17 00:00:00 2001 From: ddantuonobeintoo <58686785+ddantuonobeintoo@users.noreply.github.com> Date: Thu, 14 May 2020 16:59:44 +0200 Subject: [PATCH 081/381] Add Beintoo adapter (#1274) * Add Beintoo adapter --- adapters/beintoo/beintoo.go | 222 ++++++++++++++++++ adapters/beintoo/beintoo_test.go | 12 + .../beintootest/exemplary/minimal-banner.json | 117 +++++++++ .../beintootest/params/race/banner.json | 4 + .../supplemental/add-bidfloor.json | 42 ++++ .../bad-imp-banner-missing-sizes.json | 32 +++ .../supplemental/bad-imp-ext-tagid-value.json | 33 +++ .../supplemental/build-banner-object.json | 61 +++++ .../invalid-request-no-banner.json | 26 ++ .../invalid-response-no-bids.json | 45 ++++ .../invalid-response-unmarshall-error.json | 63 +++++ .../supplemental/no-imps-in-request.json | 18 ++ .../supplemental/server-error-code.json | 52 ++++ .../supplemental/server-no-content.json | 44 ++++ .../site-domain-and-url-correctly-parsed.json | 61 +++++ adapters/beintoo/params_test.go | 53 +++++ adapters/beintoo/usersync.go | 12 + adapters/beintoo/usersync_test.go | 35 +++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_beintoo.go | 6 + static/bidder-info/beintoo.yaml | 6 + static/bidder-params/beintoo.json | 18 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 26 files changed, 971 insertions(+) create mode 100644 adapters/beintoo/beintoo.go create mode 100644 adapters/beintoo/beintoo_test.go create mode 100644 adapters/beintoo/beintootest/exemplary/minimal-banner.json create mode 100644 adapters/beintoo/beintootest/params/race/banner.json create mode 100644 adapters/beintoo/beintootest/supplemental/add-bidfloor.json create mode 100644 adapters/beintoo/beintootest/supplemental/bad-imp-banner-missing-sizes.json create mode 100644 adapters/beintoo/beintootest/supplemental/bad-imp-ext-tagid-value.json create mode 100644 adapters/beintoo/beintootest/supplemental/build-banner-object.json create mode 100644 adapters/beintoo/beintootest/supplemental/invalid-request-no-banner.json create mode 100644 adapters/beintoo/beintootest/supplemental/invalid-response-no-bids.json create mode 100644 adapters/beintoo/beintootest/supplemental/invalid-response-unmarshall-error.json create mode 100644 adapters/beintoo/beintootest/supplemental/no-imps-in-request.json create mode 100644 adapters/beintoo/beintootest/supplemental/server-error-code.json create mode 100644 adapters/beintoo/beintootest/supplemental/server-no-content.json create mode 100644 adapters/beintoo/beintootest/supplemental/site-domain-and-url-correctly-parsed.json create mode 100644 adapters/beintoo/params_test.go create mode 100644 adapters/beintoo/usersync.go create mode 100644 adapters/beintoo/usersync_test.go create mode 100644 openrtb_ext/imp_beintoo.go create mode 100644 static/bidder-info/beintoo.yaml create mode 100644 static/bidder-params/beintoo.json diff --git a/adapters/beintoo/beintoo.go b/adapters/beintoo/beintoo.go new file mode 100644 index 00000000000..fb511f12075 --- /dev/null +++ b/adapters/beintoo/beintoo.go @@ -0,0 +1,222 @@ +package beintoo + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..863da1513e5 --- /dev/null +++ b/adapters/beintoo/beintoo_test.go @@ -0,0 +1,12 @@ +package beintoo + +import ( + "testing" + + "github.com/prebid/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..8c230c15b15 --- /dev/null +++ b/adapters/yieldlab/params_test.go @@ -0,0 +1,63 @@ +package yieldlab + +import ( + "encoding/json" + "testing" + + "github.com/prebid/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..3ee9a3fdfb5 --- /dev/null +++ b/adapters/yieldlab/usersync.go @@ -0,0 +1,12 @@ +package yieldlab + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/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..3892c16bf05 --- /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/prebid/prebid-server/privacy" + "github.com/prebid/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..20f3674797d --- /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/mxmCherry/openrtb" + "golang.org/x/text/currency" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..b6ca0507ab8 --- /dev/null +++ b/adapters/yieldlab/yieldlab_test.go @@ -0,0 +1,128 @@ +package yieldlab + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/prebid/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/config/config.go b/config/config.go index 5c66f4cdf02..86f2e9a98a0 100755 --- a/config/config.go +++ b/config/config.go @@ -551,6 +551,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderValueImpression, "https://rtb.valueimpression.com/usersync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldmo%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldone, "https://y.one.impact-ad.jp/hbs_sc?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dzeroclickfraud%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D") @@ -760,6 +761,7 @@ func SetupViper(v *viper.Viper, filename string) { 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}}") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index b91f01a7e9a..b69b5b50e13 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -73,6 +73,7 @@ import ( "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" "github.com/prebid/prebid-server/adapters/yeahmobi" + "github.com/prebid/prebid-server/adapters/yieldlab" "github.com/prebid/prebid-server/adapters/yieldmo" "github.com/prebid/prebid-server/adapters/yieldone" "github.com/prebid/prebid-server/adapters/zeroclickfraud" @@ -153,6 +154,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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), diff --git a/go.mod b/go.mod index 89cc69e4519..8de6f10e4b9 100644 --- a/go.mod +++ b/go.mod @@ -7,23 +7,17 @@ require ( 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/aerospike/aerospike-client-go v2.7.2+incompatible github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/blang/semver v3.5.1+incompatible - github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 github.com/cespare/xxhash v1.0.0 // indirect github.com/chasex/glog v0.0.0-20160217080310-c62392af379c github.com/coocood/freecache v1.0.1 - github.com/didip/tollbooth v4.0.2+incompatible github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd - github.com/go-redis/redis v6.15.7+incompatible - github.com/gocql/gocql v0.0.0-20200203083758-81b8263d9fe5 github.com/gofrs/uuid v3.2.0+incompatible github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/golang/snappy v0.0.1 github.com/hashicorp/hcl v1.0.0 // indirect github.com/influxdata/influxdb v1.6.1 // indirect github.com/julienschmidt/httprouter v1.1.0 @@ -37,10 +31,8 @@ require ( github.com/mxmCherry/openrtb v11.0.0+incompatible github.com/onsi/ginkgo v1.10.1 // indirect github.com/onsi/gomega v1.7.0 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/prebid/go-gdpr v0.7.0 - github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf 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 @@ -48,27 +40,24 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 github.com/rs/cors v1.5.0 github.com/sergi/go-diff v1.0.0 // indirect - github.com/sirupsen/logrus v1.4.2 github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.1.1 // indirect github.com/spf13/cast v1.2.0 // indirect github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 // indirect 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.5.1 - github.com/valyala/fasthttp v1.9.0 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 github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 - github.com/xorcare/pointer v1.1.0 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 - github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb // indirect 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 - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index f929408f0f3..176bacfc20a 100644 --- a/go.sum +++ b/go.sum @@ -6,57 +6,35 @@ 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/aerospike/aerospike-client-go v2.7.2+incompatible h1:bWbRf8trg1FhKF7u43KLGNfOH60RlvIgQjpaS107DZ8= -github.com/aerospike/aerospike-client-go v2.7.2+incompatible/go.mod h1:zj8LBEnWBDOVEIJt8LvaRvDG5ARAoa5dBeHaB472NRc= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= -github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 h1:y853v6rXx+zefEcjET3JuKAqvhj+FKflQijjeaSv2iA= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cespare/xxhash v1.0.0 h1:naDmySfoNg0nKS62/ujM6e71ZgM2AoVdaqGwMG0w18A= github.com/cespare/xxhash v1.0.0/go.mod h1:fX/lfQBkSCDXZSUgv6jVIu/EVA3/JNseAX5asI4c4T4= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c h1:eXqCBUHfmjbeDqcuvzjsd+bM6A+bnwo5N9FVbV6m5/s= github.com/chasex/glog v0.0.0-20160217080310-c62392af379c/go.mod h1:omJZNg0Qu76bxJd+ExohVo8uXzNcGOk2bv7vel460xk= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coocood/freecache v1.0.1 h1:oFyo4msX2c0QIKU+kuMJUwsKamJ+AKc2JJrKcMszJ5M= github.com/coocood/freecache v1.0.1/go.mod h1:ePwxCDzOYvARfHdr1pByNct1at3CoKnsipOHwKlNbzI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= -github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd h1:biTJQdqouE5by89AAffXG8++TY+9Fsdrg5rinbt3tHk= github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= -github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/gocql/gocql v0.0.0-20200203083758-81b8263d9fe5 h1:ZZVxQRCm4ewuoqqLBJ7LHpsk4OGx2PkyCsRKLq4oHgE= -github.com/gocql/gocql v0.0.0-20200203083758-81b8263d9fe5/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -67,17 +45,6 @@ github.com/julienschmidt/httprouter v1.1.0 h1:7wLdtIiIpzOkC9u6sXOozpBauPdskj3ru4 github.com/julienschmidt/httprouter v1.1.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= -github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= @@ -99,16 +66,12 @@ github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 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.7.0 h1:m4E/FjUhTBMciDsd3lQlbzFyXLzNK+JQkFmInJpFAwc= github.com/prebid/go-gdpr v0.7.0/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= -github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtWKsUFH5IzdQxHIgP609VSIVe5Hywg2phs= -github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98= 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= @@ -123,8 +86,6 @@ github.com/rs/cors v1.5.0 h1:dgSHE6+ia18arGOTIYQKKGWLvEbGvmbNE6NfxhoNHUY= github.com/rs/cors v1.5.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.1 h1:Lt3ihYMlE+lreX1GS4Qw4ZsNpYQLxIXKBTEOXm3nt6I= @@ -140,16 +101,10 @@ github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw= -github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 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= @@ -158,16 +113,12 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 h1:BcMExZAULPkihVZ7UJXK7t8rwGqisXFw75tILnafhBY= github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xorcare/pointer v1.1.0 h1:sFwXOhRF8QZ0tyVZrtxWGIoVZNEmRzBCaFWdONPQIUM= -github.com/xorcare/pointer v1.1.0/go.mod h1:6KLhkOh6YbuvZkT4YbxIbR/wzLBjyMxOiNzZhJTor2Y= github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M= github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= 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= -github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= -github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= 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= @@ -178,7 +129,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4 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-20190204203706-41f3e6584952/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= @@ -186,14 +136,10 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/p 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= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index f2f8e7c67ab..8a53e4adcf2 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -91,6 +91,7 @@ const ( BidderVisx BidderName = "visx" BidderVrtcal BidderName = "vrtcal" BidderYeahmobi BidderName = "yeahmobi" + BidderYieldlab BidderName = "yieldlab" BidderYieldmo BidderName = "yieldmo" BidderYieldone BidderName = "yieldone" BidderZeroClickFraud BidderName = "zeroclickfraud" @@ -166,6 +167,7 @@ var BidderMap = map[string]BidderName{ "visx": BidderVisx, "vrtcal": BidderVrtcal, "yeahmobi": BidderYeahmobi, + "yieldlab": BidderYieldlab, "yieldmo": BidderYieldmo, "yieldone": BidderYieldone, "zeroclickfraud": BidderZeroClickFraud, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 3f12ee7f728..791a00de0a9 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -61,6 +61,7 @@ import ( "github.com/prebid/prebid-server/adapters/verizonmedia" "github.com/prebid/prebid-server/adapters/visx" "github.com/prebid/prebid-server/adapters/vrtcal" + "github.com/prebid/prebid-server/adapters/yieldlab" "github.com/prebid/prebid-server/adapters/yieldmo" "github.com/prebid/prebid-server/adapters/yieldone" "github.com/prebid/prebid-server/adapters/zeroclickfraud" @@ -131,6 +132,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 20fce80c83a..9aae284da2a 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -67,6 +67,7 @@ func TestNewSyncerMap(t *testing.T) { 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, From d29a749f6835ef521a6ddfaa2818ef4fa66aabf0 Mon Sep 17 00:00:00 2001 From: Gena Date: Tue, 2 Jun 2020 21:59:01 +0300 Subject: [PATCH 101/381] Update adtelligent ortb endpoint (#1318) --- adapters/adtelligent/adtelligent.go | 1 - adapters/adtelligent/adtelligent_test.go | 2 +- .../adtelligenttest/exemplary/media-type-mapping.json | 2 +- .../adtelligent/adtelligenttest/exemplary/simple-banner.json | 2 +- .../adtelligent/adtelligenttest/exemplary/simple-video.json | 2 +- .../adtelligenttest/supplemental/explicit-dimensions.json | 2 +- .../supplemental/wrong-impression-mapping.json | 2 +- config/config.go | 4 ++-- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/adapters/adtelligent/adtelligent.go b/adapters/adtelligent/adtelligent.go index 7f0262fdc92..78a71dcf9cd 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 63655da677e..b8894c5e4d9 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/config/config.go b/config/config.go index 86f2e9a98a0..3b34d3a4815 100755 --- a/config/config.go +++ b/config/config.go @@ -500,7 +500,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernel, "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3DadkernelAdn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdpone, "https://usersync.adpone.com/csync?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadtelligent%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadvangelists%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -702,7 +702,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.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") From b5993cd0e5912638b03cea039466673325dee40b Mon Sep 17 00:00:00 2001 From: Seba Perez Date: Tue, 2 Jun 2020 16:05:03 -0300 Subject: [PATCH 102/381] Change on eplanning endpoint (#1327) --- adapters/eplanning/eplanning.go | 1 - adapters/eplanning/eplanning_test.go | 2 +- adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json | 2 +- adapters/eplanning/eplanningtest/exemplary/simple-banner.json | 2 +- adapters/eplanning/eplanningtest/exemplary/two-banners.json | 2 +- .../supplemental/app-domain-and-url-correctly-parsed.json | 2 +- .../eplanningtest/supplemental/banner-no-size-sends-1x1.json | 2 +- .../eplanningtest/supplemental/invalid-response-no-bids.json | 2 +- .../supplemental/invalid-response-unmarshall-error.json | 2 +- .../eplanningtest/supplemental/server-bad-request.json | 2 +- .../eplanning/eplanningtest/supplemental/server-error-code.json | 2 +- .../eplanning/eplanningtest/supplemental/server-no-content.json | 2 +- .../supplemental/site-domain-and-url-correctly-parsed.json | 2 +- config/config.go | 2 +- 14 files changed, 13 insertions(+), 14 deletions(-) diff --git a/adapters/eplanning/eplanning.go b/adapters/eplanning/eplanning.go index 5fb9ccf27c2..2a46b5469e0 100644 --- a/adapters/eplanning/eplanning.go +++ b/adapters/eplanning/eplanning.go @@ -133,7 +133,6 @@ func (adapter *EPlanningAdapter) MakeRequests(request *openrtb.BidRequest, reqIn uriObj.Path = uriObj.Path + fmt.Sprintf("/%s/%s/%s/%s", clientID, dfpClientID, requestTarget, sec) query := url.Values{} - query.Set("r", "pbs") query.Set("ncb", "1") if request.App == nil { query.Set("ur", pageURL) diff --git a/adapters/eplanning/eplanning_test.go b/adapters/eplanning/eplanning_test.go index d2c331d456d..461fb849ced 100644 --- a/adapters/eplanning/eplanning_test.go +++ b/adapters/eplanning/eplanning_test.go @@ -8,7 +8,7 @@ import ( ) func TestJsonSamples(t *testing.T) { - eplanningAdapter := NewEPlanningBidder(new(http.Client), "https://ads.us.e-planning.net/hb/1") + eplanningAdapter := NewEPlanningBidder(new(http.Client), "https://ads.us.e-planning.net/pbs/1") eplanningAdapter.testing = true adapterstest.RunJSONBidderTest(t, "eplanningtest", eplanningAdapter) } diff --git a/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json b/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json index e602877f27f..a67a86d18e6 100644 --- a/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json +++ b/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json @@ -28,7 +28,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=300x250%3A300x250&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=300x250%3A300x250&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/exemplary/simple-banner.json b/adapters/eplanning/eplanningtest/exemplary/simple-banner.json index 0403e59a763..9146bb4afb5 100644 --- a/adapters/eplanning/eplanningtest/exemplary/simple-banner.json +++ b/adapters/eplanning/eplanningtest/exemplary/simple-banner.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadun_itco_de%3A600x300&ip=123.123.123.123&ncb=1&r=pbs&uid=2154987&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadun_itco_de%3A600x300&ip=123.123.123.123&ncb=1&uid=2154987&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/exemplary/two-banners.json b/adapters/eplanning/eplanningtest/exemplary/two-banners.json index 8c4ca0214db..174c8ce3fc6 100644 --- a/adapters/eplanning/eplanningtest/exemplary/two-banners.json +++ b/adapters/eplanning/eplanningtest/exemplary/two-banners.json @@ -39,7 +39,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300%2B300x250%3A300x250&ip=123.123.123.123&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300%2B300x250%3A300x250&ip=123.123.123.123&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json b/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json index e6e25566b6a..04df82f6668 100644 --- a/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json +++ b/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json @@ -39,7 +39,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/mx.com.xeu/ROS?app=1&appid=%5Ba-f0-9%5D%7B16%7D&appn=MobileExchange&e=testadunitcode%3A600x300&ifa=3B8E2335-Z049&ip=123.123.123.123&ncb=1&r=pbs", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/mx.com.xeu/ROS?app=1&appid=%5Ba-f0-9%5D%7B16%7D&appn=MobileExchange&e=testadunitcode%3A600x300&ifa=3B8E2335-Z049&ip=123.123.123.123&ncb=1", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json b/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json index f1bc29e1afc..f02eb80fe41 100644 --- a/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json +++ b/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json @@ -19,7 +19,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadunitcodenosize%3A1x1&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcodenosize%3A1x1&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json index 978989e295f..8bdcfddd733 100644 --- a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json +++ b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json b/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json index 7198f4ee117..9f5b2d7fc03 100644 --- a/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json +++ b/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json b/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json index 938ede62664..2ef03648884 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/server-error-code.json b/adapters/eplanning/eplanningtest/supplemental/server-error-code.json index eaa2a677f93..76e75a5c203 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-error-code.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-error-code.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/server-no-content.json b/adapters/eplanning/eplanningtest/supplemental/server-no-content.json index d1feb865f0d..02f1fa46d33 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-no-content.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-no-content.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&r=pbs&ur=FILE", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json b/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json index 2f43b9aac2f..581cb1d5b46 100644 --- a/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json +++ b/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json @@ -25,7 +25,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/hb/1/12345/1/www.publisher.com/ROS?e=testadunitcode%3A600x300&ncb=1&r=pbs&ur=http%3A%2F%2Fwww.publisher.com%2Fawesome%2Fsite%3Fwith%3Dsome%26parameters%3Dhere", + "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/www.publisher.com/ROS?e=testadunitcode%3A600x300&ncb=1&ur=http%3A%2F%2Fwww.publisher.com%2Fawesome%2Fsite%3Fwith%3Dsome%26parameters%3Dhere", "body": {} }, "mockResponse": { diff --git a/config/config.go b/config/config.go index 3b34d3a4815..79a31db154a 100755 --- a/config/config.go +++ b/config/config.go @@ -718,7 +718,7 @@ func SetupViper(v *viper.Viper, filename string) { 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", "https://ads.us.e-planning.net/hb/1") + v.SetDefault("adapters.eplanning.endpoint", "https://ads.us.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") From cd9116e80c514a846600a4c14f57e608ffaedf32 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Wed, 3 Jun 2020 14:33:07 -0400 Subject: [PATCH 103/381] Enable full TCF2 support (#1302) * New config options * Enble TCF2 fields and logic * Resolves some PR comments * More tests * gofmt * Added enforcement tests for split GDPR/GDPRGeo * Testing tweaks * No longer ignore enforce purpose 1 on allowSync() * Removes Purpose 4 --- config/config.go | 29 +++ endpoints/auction_test.go | 5 +- endpoints/cookie_sync_test.go | 4 +- endpoints/setuid_test.go | 4 +- exchange/utils.go | 4 +- exchange/utils_test.go | 6 +- gdpr/gdpr.go | 2 +- gdpr/impl.go | 96 ++++++-- gdpr/impl_test.go | 390 ++++++++++++++++++++++++++++++- gdpr/vendorlist-fetching_test.go | 86 ++++++- go.mod | 3 +- go.sum | 6 +- privacy/enforcement.go | 19 +- privacy/enforcement_test.go | 108 ++++++--- 14 files changed, 677 insertions(+), 85 deletions(-) diff --git a/config/config.go b/config/config.go index 79a31db154a..5f19629d2db 100755 --- a/config/config.go +++ b/config/config.go @@ -142,6 +142,7 @@ type GDPR struct { 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"` } @@ -165,6 +166,26 @@ 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"` } @@ -774,6 +795,14 @@ 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("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json") diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index 3035a6d45fb..5e9e9639a9c 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,8 @@ 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 { diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index bb766aa92e7..824e32f1957 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -254,8 +254,8 @@ 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 { diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 8499ac1ca5d..3f47b257d2e 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -437,8 +437,8 @@ 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 { diff --git a/exchange/utils.go b/exchange/utils.go index d961089c4cb..f602d1e8fba 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -60,10 +60,12 @@ func cleanOpenRTBRequests(ctx context.Context, 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, ampGDPRException) diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 53d6b85c243..acbf25ff691 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -24,11 +24,11 @@ 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 { diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 9390d942f80..0dfa12f5ebd 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -23,7 +23,7 @@ type Permissions interface { // Determines whether or not to send PI information to a bidder, or mask it out. // // 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, error) + PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) // Exposes the AMP execption flag AMPException() bool diff --git a/gdpr/impl.go b/gdpr/impl.go index 8743d7f2778..60db804aec6 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -2,11 +2,13 @@ package gdpr import ( "context" + "fmt" "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" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -40,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] @@ -52,10 +54,10 @@ 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 { @@ -78,38 +80,104 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen } // 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 parsedConsent.Version() == 2 { - // Need to add the location special purpose once the library supports it. + 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, nil + 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, nil + return true, true, nil } } - return false, 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 +} + +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 api.VendorConsents, vendor api.Vendor, err error) { @@ -146,8 +214,8 @@ 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 { diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 8b89577d6c8..f05f25e87ea 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -10,6 +10,9 @@ import ( "github.com/prebid/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) { @@ -55,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{ @@ -91,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{ @@ -127,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{ @@ -179,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{ @@ -204,21 +207,377 @@ func TestAllowPersonalInfo(t *testing.T) { } // 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)) @@ -228,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_test.go b/gdpr/vendorlist-fetching_test.go index 8197fa263bc..824f9178faa 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{ @@ -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{ @@ -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{ @@ -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{ @@ -174,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 { @@ -203,6 +203,72 @@ func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purpos return string(data) } +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, TCFVer uint8) string { @@ -220,5 +286,5 @@ func testConfig() config.GDPR { } type purposes struct { - purposes []uint8 + purposes []int } diff --git a/go.mod b/go.mod index 8de6f10e4b9..0224057e464 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,8 @@ 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.7.0 + github.com/prebid/go-gdpr v0.8.2 + github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf 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 diff --git a/go.sum b/go.sum index 176bacfc20a..5d941b89e90 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,10 @@ 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.7.0 h1:m4E/FjUhTBMciDsd3lQlbzFyXLzNK+JQkFmInJpFAwc= -github.com/prebid/go-gdpr v0.7.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/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtWKsUFH5IzdQxHIgP609VSIVe5Hywg2phs= +github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98= 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= diff --git a/privacy/enforcement.go b/privacy/enforcement.go index 96d03ef4433..d302192ec3f 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -6,14 +6,15 @@ 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 } // 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 } // Apply cleans personally identifiable information from an OpenRTB bid request. @@ -45,7 +46,7 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { return ScrubStrategyGeoFull } - if e.GDPR || e.CCPA { + if e.GDPRGeo || e.CCPA { return ScrubStrategyGeoReducedPrecision } @@ -60,5 +61,11 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs if e.GDPR && ampGDPRException { return ScrubStrategyUserNone } - return ScrubStrategyUserID + + // If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above) + if e.CCPA || e.GDPR { + return ScrubStrategyUserID + } + + return ScrubStrategyUserNone } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 25e08b5e80d..0e82648d4b9 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -17,27 +17,40 @@ func TestAny(t *testing.T) { { description: "All False", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: false, + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: false, }, expected: false, }, { description: "All True", enforcement: Enforcement{ - CCPA: true, - COPPA: true, - GDPR: true, + CCPA: true, + COPPA: true, + GDPR: true, + GDPRGeo: true, }, expected: true, }, { description: "Mixed", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: false, + CCPA: false, + COPPA: true, + GDPR: false, + GDPRGeo: false, + }, + expected: true, + }, + { + description: "GDPRGeo only", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: true, }, expected: true, }, @@ -62,9 +75,10 @@ func TestApply(t *testing.T) { { description: "All Enforced", enforcement: Enforcement{ - CCPA: true, - COPPA: true, - GDPR: true, + CCPA: true, + COPPA: true, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -75,9 +89,10 @@ func TestApply(t *testing.T) { { description: "CCPA Only", enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, + CCPA: true, + COPPA: false, + GDPR: false, + GDPRGeo: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -88,9 +103,10 @@ func TestApply(t *testing.T) { { description: "COPPA Only", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: false, + CCPA: false, + COPPA: true, + GDPR: false, + GDPRGeo: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -101,9 +117,10 @@ func TestApply(t *testing.T) { { description: "GDPR Only", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: true, + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -114,9 +131,10 @@ func TestApply(t *testing.T) { { description: "GDPR Only, ampGDPRException", enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: true, + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -127,9 +145,10 @@ func TestApply(t *testing.T) { { description: "CCPA Only, ampGDPRException", enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, + CCPA: true, + COPPA: false, + GDPR: false, + GDPRGeo: false, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -140,9 +159,10 @@ func TestApply(t *testing.T) { { description: "COPPA and GDPR, ampGDPRException", enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: true, + CCPA: false, + COPPA: true, + GDPR: true, + GDPRGeo: true, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -150,6 +170,34 @@ func TestApply(t *testing.T) { expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoFull, }, + { + description: "GDPR Only, no Geo", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: false, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoNone, + expectedUser: ScrubStrategyUserID, + expectedUserGeo: ScrubStrategyGeoNone, + }, + { + description: "GDPR Only, Geo only", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: true, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6None, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserNone, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, + }, } for _, test := range testCases { From 23c684c5a6c4189ca172a83453b2cfa8de2bf0a5 Mon Sep 17 00:00:00 2001 From: Seba Perez Date: Wed, 3 Jun 2020 15:40:46 -0300 Subject: [PATCH 104/381] Change on eplanning endpoint (hostname) (#1328) --- adapters/eplanning/eplanning_test.go | 2 +- adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json | 2 +- adapters/eplanning/eplanningtest/exemplary/simple-banner.json | 2 +- adapters/eplanning/eplanningtest/exemplary/two-banners.json | 2 +- .../supplemental/app-domain-and-url-correctly-parsed.json | 2 +- .../eplanningtest/supplemental/banner-no-size-sends-1x1.json | 2 +- .../eplanningtest/supplemental/invalid-response-no-bids.json | 2 +- .../supplemental/invalid-response-unmarshall-error.json | 2 +- .../eplanningtest/supplemental/server-bad-request.json | 2 +- .../eplanning/eplanningtest/supplemental/server-error-code.json | 2 +- .../eplanning/eplanningtest/supplemental/server-no-content.json | 2 +- .../supplemental/site-domain-and-url-correctly-parsed.json | 2 +- config/config.go | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/adapters/eplanning/eplanning_test.go b/adapters/eplanning/eplanning_test.go index 461fb849ced..28fdf6c45c2 100644 --- a/adapters/eplanning/eplanning_test.go +++ b/adapters/eplanning/eplanning_test.go @@ -8,7 +8,7 @@ import ( ) func TestJsonSamples(t *testing.T) { - eplanningAdapter := NewEPlanningBidder(new(http.Client), "https://ads.us.e-planning.net/pbs/1") + eplanningAdapter := NewEPlanningBidder(new(http.Client), "http://rtb.e-planning.net/pbs/1") eplanningAdapter.testing = true adapterstest.RunJSONBidderTest(t, "eplanningtest", eplanningAdapter) } diff --git a/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json b/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json index a67a86d18e6..556831217ec 100644 --- a/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json +++ b/adapters/eplanning/eplanningtest/exemplary/simple-banner-2.json @@ -28,7 +28,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=300x250%3A300x250&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=300x250%3A300x250&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/exemplary/simple-banner.json b/adapters/eplanning/eplanningtest/exemplary/simple-banner.json index 9146bb4afb5..04eca985340 100644 --- a/adapters/eplanning/eplanningtest/exemplary/simple-banner.json +++ b/adapters/eplanning/eplanningtest/exemplary/simple-banner.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadun_itco_de%3A600x300&ip=123.123.123.123&ncb=1&uid=2154987&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadun_itco_de%3A600x300&ip=123.123.123.123&ncb=1&uid=2154987&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/exemplary/two-banners.json b/adapters/eplanning/eplanningtest/exemplary/two-banners.json index 174c8ce3fc6..72aeb64b3a9 100644 --- a/adapters/eplanning/eplanningtest/exemplary/two-banners.json +++ b/adapters/eplanning/eplanningtest/exemplary/two-banners.json @@ -39,7 +39,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300%2B300x250%3A300x250&ip=123.123.123.123&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300%2B300x250%3A300x250&ip=123.123.123.123&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json b/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json index 04df82f6668..413c973dfa2 100644 --- a/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json +++ b/adapters/eplanning/eplanningtest/supplemental/app-domain-and-url-correctly-parsed.json @@ -39,7 +39,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/mx.com.xeu/ROS?app=1&appid=%5Ba-f0-9%5D%7B16%7D&appn=MobileExchange&e=testadunitcode%3A600x300&ifa=3B8E2335-Z049&ip=123.123.123.123&ncb=1", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/mx.com.xeu/ROS?app=1&appid=%5Ba-f0-9%5D%7B16%7D&appn=MobileExchange&e=testadunitcode%3A600x300&ifa=3B8E2335-Z049&ip=123.123.123.123&ncb=1", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json b/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json index f02eb80fe41..05547d81707 100644 --- a/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json +++ b/adapters/eplanning/eplanningtest/supplemental/banner-no-size-sends-1x1.json @@ -19,7 +19,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcodenosize%3A1x1&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcodenosize%3A1x1&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json index 8bdcfddd733..570488825e2 100644 --- a/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json +++ b/adapters/eplanning/eplanningtest/supplemental/invalid-response-no-bids.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json b/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json index 9f5b2d7fc03..4ba2d44bf3a 100644 --- a/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json +++ b/adapters/eplanning/eplanningtest/supplemental/invalid-response-unmarshall-error.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json b/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json index 2ef03648884..9e8eae8c080 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-bad-request.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/server-error-code.json b/adapters/eplanning/eplanningtest/supplemental/server-error-code.json index 76e75a5c203..08f46d9e6c2 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-error-code.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-error-code.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/server-no-content.json b/adapters/eplanning/eplanningtest/supplemental/server-no-content.json index 02f1fa46d33..20a2b1cf456 100644 --- a/adapters/eplanning/eplanningtest/supplemental/server-no-content.json +++ b/adapters/eplanning/eplanningtest/supplemental/server-no-content.json @@ -21,7 +21,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/FILE/ROS?e=testadunitcode%3A600x300&ncb=1&ur=FILE", "body": {} }, "mockResponse": { diff --git a/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json b/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json index 581cb1d5b46..62890d914ff 100644 --- a/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json +++ b/adapters/eplanning/eplanningtest/supplemental/site-domain-and-url-correctly-parsed.json @@ -25,7 +25,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "https://ads.us.e-planning.net/pbs/1/12345/1/www.publisher.com/ROS?e=testadunitcode%3A600x300&ncb=1&ur=http%3A%2F%2Fwww.publisher.com%2Fawesome%2Fsite%3Fwith%3Dsome%26parameters%3Dhere", + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/www.publisher.com/ROS?e=testadunitcode%3A600x300&ncb=1&ur=http%3A%2F%2Fwww.publisher.com%2Fawesome%2Fsite%3Fwith%3Dsome%26parameters%3Dhere", "body": {} }, "mockResponse": { diff --git a/config/config.go b/config/config.go index 5f19629d2db..9652ae141f5 100755 --- a/config/config.go +++ b/config/config.go @@ -739,7 +739,7 @@ func SetupViper(v *viper.Viper, filename string) { 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", "https://ads.us.e-planning.net/pbs/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") From 352784573cbdb29d725c45328dda7fa335096116 Mon Sep 17 00:00:00 2001 From: Steve Alliance Date: Wed, 3 Jun 2020 14:43:31 -0400 Subject: [PATCH 105/381] Districtm Dmx: new adapter (#1209) Co-authored-by: steve-a-districtm --- adapters/dmx/dmx.go | 296 +++++++ adapters/dmx/dmx_test.go | 782 ++++++++++++++++++ .../dmx/dmxtest/exemplary/simple-app.json | 138 ++++ .../dmx/dmxtest/exemplary/simple-banner.json | 126 +++ .../dmx/dmxtest/exemplary/simple-video.json | 112 +++ adapters/dmx/dmxtest/params/race/banner.json | 4 + adapters/dmx/dmxtest/params/race/video.json | 4 + adapters/dmx/usersync.go | 12 + adapters/dmx/usersync_test.go | 20 + config/config.go | 4 +- exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + static/bidder-info/dmx.yaml | 11 + static/bidder-params/dmx.json | 22 + usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 16 files changed, 1537 insertions(+), 1 deletion(-) create mode 100644 adapters/dmx/dmx.go create mode 100644 adapters/dmx/dmx_test.go create mode 100644 adapters/dmx/dmxtest/exemplary/simple-app.json create mode 100644 adapters/dmx/dmxtest/exemplary/simple-banner.json create mode 100644 adapters/dmx/dmxtest/exemplary/simple-video.json create mode 100644 adapters/dmx/dmxtest/params/race/banner.json create mode 100644 adapters/dmx/dmxtest/params/race/video.json create mode 100644 adapters/dmx/usersync.go create mode 100644 adapters/dmx/usersync_test.go create mode 100644 static/bidder-info/dmx.yaml create mode 100644 static/bidder-params/dmx.json diff --git a/adapters/dmx/dmx.go b/adapters/dmx/dmx.go new file mode 100644 index 00000000000..6b4f698d4b1 --- /dev/null +++ b/adapters/dmx/dmx.go @@ -0,0 +1,296 @@ +package dmx + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" + "net/url" + "strings" +) + +type DmxAdapter struct { + endpoint string +} + +func NewDmxBidder(endpoint string) *DmxAdapter { + return &DmxAdapter{endpoint: endpoint} +} + +type dmxExt struct { + Bidder dmxParams `json:"bidder"` +} + +type dmxParams struct { + TagId string `json:"tagid,omitempty"` + DmxId string `json:"dmxid,omitempty"` + MemberId string `json:"memberid,omitempty"` + PublisherId string `json:"publisher_id,omitempty"` + SellerId string `json:"seller_id,omitempty"` +} + +func UserSellerOrPubId(str1, str2 string) string { + if str1 != "" { + return str1 + } + return str2 +} + +func (adapter *DmxAdapter) MakeRequests(request *openrtb.BidRequest, req *adapters.ExtraRequestInfo) (reqsBidder []*adapters.RequestData, errs []error) { + var imps []openrtb.Imp + var rootExtInfo dmxExt + var publisherId string + var sellerId string + var userExt openrtb_ext.ExtUser + var anyHasId = false + var reqCopy openrtb.BidRequest = *request + var dmxReq *openrtb.BidRequest = &reqCopy + + if request.User == nil { + if request.App == nil { + return nil, []error{errors.New("No user id or app id found. Could not send request to DMX.")} + } + } + + if len(request.Imp) >= 1 { + err := json.Unmarshal(request.Imp[0].Ext, &rootExtInfo) + if err != nil { + errs = append(errs, err) + } else { + publisherId = UserSellerOrPubId(rootExtInfo.Bidder.PublisherId, rootExtInfo.Bidder.MemberId) + sellerId = rootExtInfo.Bidder.SellerId + } + } + + if request.App != nil { + appCopy := *request.App + appPublisherCopy := *request.App.Publisher + dmxReq.App = &appCopy + dmxReq.App.Publisher = &appPublisherCopy + if dmxReq.App.ID != "" { + anyHasId = true + } + } else { + dmxReq.App = nil + } + + if request.Site != nil { + siteCopy := *request.Site + sitePublisherCopy := *request.Site.Publisher + dmxReq.Site = &siteCopy + dmxReq.Site.Publisher = &sitePublisherCopy + if dmxReq.Site.Publisher != nil { + dmxReq.Site.Publisher.ID = publisherId + } else { + dmxReq.Site.Publisher = &openrtb.Publisher{ID: publisherId} + } + } else { + dmxReq.Site = nil + } + + if request.User != nil { + userCopy := *request.User + dmxReq.User = &userCopy + } else { + dmxReq.User = nil + } + + if dmxReq.User != nil { + if dmxReq.User.ID != "" { + anyHasId = true + } + if dmxReq.User.Ext != nil { + if err := json.Unmarshal(dmxReq.User.Ext, &userExt); err == nil { + if len(userExt.Eids) > 0 || (userExt.DigiTrust != nil && userExt.DigiTrust.ID != "") { + anyHasId = true + } + } + } + } + + if anyHasId == false { + return nil, []error{errors.New("This request contained no identifier")} + } + + for _, inst := range dmxReq.Imp { + var banner *openrtb.Banner + var video *openrtb.Video + var ins openrtb.Imp + var params dmxExt + const intVal int8 = 1 + source := (*json.RawMessage)(&inst.Ext) + if err := json.Unmarshal(*source, ¶ms); err != nil { + errs = append(errs, err) + } + if isDmxParams(params.Bidder) { + if inst.Banner != nil { + if len(inst.Banner.Format) != 0 { + banner = inst.Banner + if params.Bidder.PublisherId != "" || params.Bidder.MemberId != "" { + imps = fetchParams(params, inst, ins, imps, banner, nil, intVal) + } else { + return nil, []error{errors.New("Missing Params for auction to be send")} + } + } + } + + if inst.Video != nil { + video = inst.Video + if params.Bidder.PublisherId != "" || params.Bidder.MemberId != "" { + imps = fetchParams(params, inst, ins, imps, nil, video, intVal) + } else { + return nil, []error{errors.New("Missing Params for auction to be send")} + } + } + } + + } + + dmxReq.Imp = imps + + oJson, err := json.Marshal(dmxReq) + + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "Application/json;charset=utf-8") + reqBidder := &adapters.RequestData{ + Method: "POST", + Uri: adapter.endpoint + addParams(sellerId), //adapter.endpoint, + Body: oJson, + Headers: headers, + } + reqsBidder = append(reqsBidder, reqBidder) + return +} + +func (adapter *DmxAdapter) MakeBids(request *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + if http.StatusNoContent == response.StatusCode { + return nil, nil + } + + if http.StatusBadRequest == response.StatusCode { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code 400"), + }} + } + + if http.StatusOK != response.StatusCode { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected response no status code"), + }} + } + + var bidResp openrtb.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, request.Imp) + if err != nil { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + } + if b.BidType == openrtb_ext.BidTypeVideo { + b.Bid.AdM = videoImpInsertion(b.Bid) + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, errs + +} + +func fetchParams(params dmxExt, inst openrtb.Imp, ins openrtb.Imp, imps []openrtb.Imp, banner *openrtb.Banner, video *openrtb.Video, intVal int8) []openrtb.Imp { + if params.Bidder.TagId != "" { + ins = openrtb.Imp{ + ID: inst.ID, + TagID: params.Bidder.TagId, + Ext: inst.Ext, + Secure: &intVal, + } + } + + if params.Bidder.DmxId != "" { + ins = openrtb.Imp{ + ID: inst.ID, + TagID: params.Bidder.DmxId, + Ext: inst.Ext, + Secure: &intVal, + } + } + if banner != nil { + ins.Banner = banner + } + + if video != nil { + ins.Video = video + } + + if ins.TagID == "" { + return imps + } + imps = append(imps, ins) + return imps +} + +func addParams(str string) string { + if str != "" { + return "?sellerid=" + url.QueryEscape(str) + } + return "" +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) (openrtb_ext.BidType, error) { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impID { + if imp.Banner == nil && imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } + return mediaType, nil + } + } + + // This shouldnt happen. Lets handle it just incase by returning an error. + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression \"%s\" ", impID), + } +} + +func videoImpInsertion(bid *openrtb.Bid) string { + adm := bid.AdM + nurl := bid.NURL + search := "" + imp := "" + wrapped_nurl := fmt.Sprintf(imp, nurl) + results := strings.Replace(adm, search, wrapped_nurl, 1) + return results +} + +func isDmxParams(t interface{}) bool { + switch t.(type) { + case dmxParams: + return true + default: + return false + } +} diff --git a/adapters/dmx/dmx_test.go b/adapters/dmx/dmx_test.go new file mode 100644 index 00000000000..e9f195eb61d --- /dev/null +++ b/adapters/dmx/dmx_test.go @@ -0,0 +1,782 @@ +package dmx + +import ( + "encoding/json" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "strings" + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +var ( + bidRequest string +) + +func TestFetchParams(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + var arrImp []openrtb.Imp + var imps = fetchParams( + dmxExt{Bidder: dmxParams{ + TagId: "222", + PublisherId: "5555", + }}, + openrtb.Imp{ID: "32"}, + openrtb.Imp{ID: "32"}, + arrImp, + &openrtb.Banner{W: &width, H: &height, Format: []openrtb.Format{ + {W: 300, H: 250}, + }}, + nil, + 1) + var imps2 = fetchParams( + dmxExt{Bidder: dmxParams{ + DmxId: "222", + MemberId: "5555", + }}, + openrtb.Imp{ID: "32"}, + openrtb.Imp{ID: "32"}, + arrImp, + &openrtb.Banner{W: &width, H: &height, Format: []openrtb.Format{ + {W: 300, H: 250}, + }}, + nil, + 1) + if len(imps) == 0 { + t.Errorf("should increment the length by one") + } + + if len(imps2) == 0 { + t.Errorf("should increment the length by one") + } + +} +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "dmxtest", new(DmxAdapter)) +} + +func TestMakeRequestsOtherPlacement(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + User: &openrtb.User{ID: "bscakucbkasucbkasunscancasuin"}, + Imp: []openrtb.Imp{imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + ID: "1234", + } + + actualAdapterRequests, err := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + if actualAdapterRequests == nil { + t.Errorf("request should be nil") + } + if len(err) != 0 { + t.Errorf("We should have no error") + } + +} + +func TestMakeRequestsInvalid(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + ID: "1234", + } + + actualAdapterRequests, err := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + if len(actualAdapterRequests) != 0 { + t.Errorf("request should be nil") + } + if len(err) == 0 { + t.Errorf("We should have no error") + } + +} + +func TestMakeRequestNoSite(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1}, + App: &openrtb.App{ID: "cansanuabnua", Publisher: &openrtb.Publisher{ID: "whatever"}}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + if len(actualAdapterRequests) != 1 { + t.Errorf("openrtb type should be an Array when it's an App") + } + var the_body openrtb.BidRequest + if err := json.Unmarshal(actualAdapterRequests[0].Body, &the_body); err != nil { + t.Errorf("failed to read bid request") + } + + if the_body.App == nil { + t.Errorf("app property should be populated") + } + + if the_body.App.Publisher.ID == "" { + t.Errorf("Missing publisher ID must be in") + } +} + +func TestMakeRequestsApp(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + App: &openrtb.App{ID: "cansanuabnua", Publisher: &openrtb.Publisher{ID: "whatever"}}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + if len(actualAdapterRequests) != 1 { + t.Errorf("openrtb type should be an Array when it's an App") + } + var the_body openrtb.BidRequest + if err := json.Unmarshal(actualAdapterRequests[0].Body, &the_body); err != nil { + t.Errorf("failed to read bid request") + } + + if the_body.App == nil { + t.Errorf("app property should be populated") + } + +} + +func TestMakeRequestsNoUser(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + if actualAdapterRequests != nil { + t.Errorf("openrtb type should be empty") + } + +} + +func TestMakeRequests(t *testing.T) { + //server := httptest.NewServer(http.HandlerFunc(DummyDmxServer)) + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + imp2 := openrtb.Imp{ + ID: "imp2", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + imp3 := openrtb.Imp{ + ID: "imp3", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1, imp2, imp3}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + User: &openrtb.User{ID: "districtmID"}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + if len(actualAdapterRequests) != 1 { + t.Errorf("should have 1 request") + } + var the_body openrtb.BidRequest + if err := json.Unmarshal(actualAdapterRequests[0].Body, &the_body); err != nil { + t.Errorf("failed to read bid request") + } + + if len(the_body.Imp) != 3 { + t.Errorf("must have 3 bids") + } + +} + +func TestMakeBidVideo(t *testing.T) { + var w, h int = 640, 480 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Video: &openrtb.Video{ + W: width, + H: height, + MIMEs: []string{"video/mp4"}, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + User: &openrtb.User{ID: "districtmID"}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + if len(actualAdapterRequests) != 1 { + t.Errorf("should have 1 request") + } + var the_body openrtb.BidRequest + if err := json.Unmarshal(actualAdapterRequests[0].Body, &the_body); err != nil { + t.Errorf("failed to read bid request") + } + + if len(the_body.Imp) != 1 { + t.Errorf("must have 1 bids") + } +} + +func TestMakeBidsNoContent(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + User: &openrtb.User{ID: "districtmID"}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + + _, err204 := adapter.MakeBids(&inputRequest, actualAdapterRequests[0], &adapters.ResponseData{StatusCode: 204}) + + if err204 != nil { + t.Errorf("Was expecting nil") + } + + _, err400 := adapter.MakeBids(&inputRequest, actualAdapterRequests[0], &adapters.ResponseData{StatusCode: 400}) + + if err400 == nil { + t.Errorf("Was expecting error") + } + + _, err500 := adapter.MakeBids(&inputRequest, actualAdapterRequests[0], &adapters.ResponseData{StatusCode: 500}) + + if err500 == nil { + t.Errorf("Was expecting error") + } + + bidResponse := &adapters.ResponseData{ + StatusCode: 200, + Body: []byte(`{ + "id": "JdSgvXjee0UZ", + "seatbid": [ + { + "bid": [ + { + "id": "16-40dbf1ef_0gKywr9JnzPAW4bE-1", + "impid": "imp1", + "price": 2.3456, + "adm": "", + "nurl": "dmxnotificationurlhere", + "adomain": [ + "brand.com", + "advertiser.net" + ], + "cid": "12345", + "crid": "232303", + "cat": [ + "IAB20-3" + ], + "attr": [ + 2 + ], + "w": 300, + "h": 600, + "language": "en" + } + ], + "seat": "10001" + } + ], + "cur": "USD" +}`), + } + + bidResponseNoMatch := &adapters.ResponseData{ + StatusCode: 200, + Body: []byte(`{ + "id": "JdSgvXjee0UZ", + "seatbid": [ + { + "bid": [ + { + "id": "16-40dbf1ef_0gKywr9JnzPAW4bE-1", + "impid": "djvnsvns", + "price": 2.3456, + "adm": "", + "nurl": "dmxnotificationurlhere", + "adomain": [ + "brand.com", + "advertiser.net" + ], + "cid": "12345", + "crid": "232303", + "cat": [ + "IAB20-3" + ], + "attr": [ + 2 + ], + "w": 300, + "h": 600, + "language": "en" + } + ], + "seat": "10001" + } + ], + "cur": "USD" +}`), + } + + bids, _ := adapter.MakeBids(&inputRequest, actualAdapterRequests[0], bidResponse) + if bids == nil { + t.Errorf("ads not parse") + } + bidsNoMatching, _ := adapter.MakeBids(&inputRequest, actualAdapterRequests[0], bidResponseNoMatch) + if bidsNoMatching == nil { + t.Errorf("ads not parse") + } + +} +func TestUserExtEmptyObject(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1, imp1, imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + User: &openrtb.User{Ext: json.RawMessage(`{}`)}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + if len(actualAdapterRequests) != 0 { + t.Errorf("should have 0 request") + } +} +func TestUserEidsOnly(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1, imp1, imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + User: &openrtb.User{Ext: json.RawMessage(`{"eids": [{ + "source": "adserver.org", + "uids": [{ + "id": "111111111111", + "ext": { + "rtiPartner": "TDID" + } + }] + },{ + "source": "netid.de", + "uids": [{ + "id": "11111111" + }] + }] + }`)}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + if len(actualAdapterRequests) != 1 { + t.Errorf("should have 1 request") + } +} + +func TestUserDigitrustOnly(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1, imp1, imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + User: &openrtb.User{Ext: json.RawMessage(`{ + "digitrust": { + "id": "11111111111", + "keyv": 4 + }}`)}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + if len(actualAdapterRequests) != 1 { + t.Errorf("should have 1 request") + } +} + +func TestUsersEids(t *testing.T) { + var w, h int = 300, 250 + + var width, height uint64 = uint64(w), uint64(h) + + adapter := NewDmxBidder("https://dmx.districtm.io/b/v2") + imp1 := openrtb.Imp{ + ID: "imp1", + Ext: json.RawMessage("{\"bidder\":{\"dmxid\": \"1007\", \"memberid\": \"123456\", \"seller_id\":\"1008\"}}"), + Banner: &openrtb.Banner{ + W: &width, + H: &height, + Format: []openrtb.Format{ + {W: 300, H: 250}, + }, + }} + + inputRequest := openrtb.BidRequest{ + Imp: []openrtb.Imp{imp1, imp1, imp1}, + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "10007", + }, + }, + User: &openrtb.User{ID: "districtmID", Ext: json.RawMessage(`{"eids": [{ + "source": "adserver.org", + "uids": [{ + "id": "111111111111", + "ext": { + "rtiPartner": "TDID" + } + }] + },{ + "source": "pubcid.org", + "uids": [{ + "id":"11111111" + }] + }, + { + "source": "id5-sync.com", + "uids": [{ + "id": "ID5-12345" + }] + }, + { + "source": "parrable.com", + "uids": [{ + "id": "01.1563917337.test-eid" + }] + },{ + "source": "identityLink", + "uids": [{ + "id": "11111111" + }] + },{ + "source": "criteo", + "uids": [{ + "id": "11111111" + }] + },{ + "source": "britepool.com", + "uids": [{ + "id": "11111111" + }] + },{ + "source": "liveintent.com", + "uids": [{ + "id": "11111111" + }] + },{ + "source": "netid.de", + "uids": [{ + "id": "11111111" + }] + }], + "digitrust": { + "id": "11111111111", + "keyv": 4 + }}`)}, + ID: "1234", + } + + actualAdapterRequests, _ := adapter.MakeRequests(&inputRequest, &adapters.ExtraRequestInfo{}) + if len(actualAdapterRequests) != 1 { + t.Errorf("should have 1 request") + } + var the_body openrtb.BidRequest + if err := json.Unmarshal(actualAdapterRequests[0].Body, &the_body); err != nil { + t.Errorf("failed to read bid request") + } + + if len(the_body.Imp) != 3 { + t.Errorf("must have 3 bids") + } +} +func TestVideoImpInsertion(t *testing.T) { + var bidResp openrtb.BidResponse + var bid openrtb.Bid + payload := []byte(`{ + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "video1", + "impid": "video1", + "price": 5.01, + "nurl": "https://demo.arripiblik.com/359585167267151", + "adm": "BidSwitch", + "crid": "76575664756", + "dealid": "dmx-deal-hp-24", + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + }, + { + "id": "some-impression-id", + "impid": "some-impression-id", + "price": 5.01, + "adm": "", + "crid": "1346943998", + "dealid": "dmx-deal-hp-24", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + { + "id": "some-impression-id2", + "impid": "some-impression-id2", + "price": 5.01, + "adm": "", + "crid": "1424798162", + "dealid": "dmx-deal-hp-24", + "w": 728, + "h": 90, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "dmx" + } + ] +}`) + + err := json.Unmarshal(payload, &bidResp) + if err != nil { + t.Errorf("Payload is invalid") + } + bid = openrtb.Bid(bidResp.SeatBid[0].Bid[0]) + data := videoImpInsertion(&bid) + find := strings.Index(data, "demo.arripiblik.com") + if find == -1 { + t.Errorf("String was not found") + } + +} diff --git a/adapters/dmx/dmxtest/exemplary/simple-app.json b/adapters/dmx/dmxtest/exemplary/simple-app.json new file mode 100644 index 00000000000..a2d57163f3c --- /dev/null +++ b/adapters/dmx/dmxtest/exemplary/simple-app.json @@ -0,0 +1,138 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app":{ + "bundle":"302324249", + "id":"ed6207cefff74c14878963566683c070", + "name":"Skout - iOS Match Buy", + "publisher":{ + "id":"10400" + }, + "storeurl":"https://itunes.apple.com/app/id302324249" + }, + "imp": [ + { + + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "dmxid": "123454", + "publisher_id": "10400" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "", + "body": { + "id": "test-request-id", + "app":{ + "bundle":"302324249", + "id":"ed6207cefff74c14878963566683c070", + "name":"Skout - iOS Match Buy", + "publisher":{ + "id":"10400" + }, + "storeurl":"https://itunes.apple.com/app/id302324249" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123454", + "secure": 1, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisher_id": "10400", + "dmxid": "123454" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.75, + "adid": "29681110", + "adm": "
banner-ads
", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.75, + "adm": "
banner-ads
", + "adid": "29681110", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250 + + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/dmx/dmxtest/exemplary/simple-banner.json b/adapters/dmx/dmxtest/exemplary/simple-banner.json new file mode 100644 index 00000000000..03ea6246ee4 --- /dev/null +++ b/adapters/dmx/dmxtest/exemplary/simple-banner.json @@ -0,0 +1,126 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "fhacacnasicnaic" + }, + "imp": [ + { + + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "dmxid": "123454", + "publisher_id": "10400" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "", + "body": { + "id": "test-request-id", + "user": { + "id": "fhacacnasicnaic" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "123454", + "secure": 1, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisher_id": "10400", + "dmxid": "123454" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.75, + "adid": "29681110", + "adm": "
banner-ads
", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.75, + "adm": "
banner-ads
", + "adid": "29681110", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250 + + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/dmx/dmxtest/exemplary/simple-video.json b/adapters/dmx/dmxtest/exemplary/simple-video.json new file mode 100644 index 00000000000..b4c53188119 --- /dev/null +++ b/adapters/dmx/dmxtest/exemplary/simple-video.json @@ -0,0 +1,112 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "whateveryouwant" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 15, + "maxduration": 30, + "protocols": [2, 3, 5, 6, 7, 8], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "tagid": "12345", + "publisher_id": "10400" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "", + "body": { + "id": "test-request-id", + "user": { + "id": "whateveryouwant" + }, + "imp": [ + { + "ext": { + "bidder": { + "tagid": "12345", + "publisher_id": "10400" + } + }, + "id": "test-imp-id", + "tagid": "12345", + "secure": 1, + "video": { + "mimes": ["video/mp4"], + "minduration": 15, + "maxduration": 30, + "protocols": [2, 3, 5, 6, 7, 8], + "w": 940, + "h": 560 + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.90, + "adid": "29681110", + "adm": "ads", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.90, + "adm": "ads", + "adid": "29681110", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/dmx/dmxtest/params/race/banner.json b/adapters/dmx/dmxtest/params/race/banner.json new file mode 100644 index 00000000000..1c0adff78ac --- /dev/null +++ b/adapters/dmx/dmxtest/params/race/banner.json @@ -0,0 +1,4 @@ +{ + "tagid": "25251", + "publisher_id": "100152" +} \ No newline at end of file diff --git a/adapters/dmx/dmxtest/params/race/video.json b/adapters/dmx/dmxtest/params/race/video.json new file mode 100644 index 00000000000..3bbd83bd3b0 --- /dev/null +++ b/adapters/dmx/dmxtest/params/race/video.json @@ -0,0 +1,4 @@ +{ + "tagid": "25255", + "publisher_id": "100151" +} \ No newline at end of file diff --git a/adapters/dmx/usersync.go b/adapters/dmx/usersync.go new file mode 100644 index 00000000000..98e56234fa6 --- /dev/null +++ b/adapters/dmx/usersync.go @@ -0,0 +1,12 @@ +package dmx + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewDmxSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("dmx", 144, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/dmx/usersync_test.go b/adapters/dmx/usersync_test.go new file mode 100644 index 00000000000..e4e3c7d8e55 --- /dev/null +++ b/adapters/dmx/usersync_test.go @@ -0,0 +1,20 @@ +package dmx + +import ( + "github.com/prebid/prebid-server/privacy" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestDmxSyncer(t *testing.T) { + temp := template.Must(template.New("sync-template").Parse("https://dmx.districtm.io/s/v1/img/s/10007")) + syncer := NewDmxSyncer(temp) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{}) + assert.NoError(t, err) + assert.Equal(t, "https://dmx.districtm.io/s/v1/img/s/10007", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 144, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 9652ae141f5..e93aed46eab 100755 --- a/config/config.go +++ b/config/config.go @@ -534,6 +534,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/match/bounce/current?version=1&networkId=72582&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Deplanning%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -711,9 +712,10 @@ func SetupViper(v *viper.Viper, filename string) { // 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index b69b5b50e13..390016117fb 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -29,6 +29,7 @@ import ( "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/cpmstar" "github.com/prebid/prebid-server/adapters/datablocks" + "github.com/prebid/prebid-server/adapters/dmx" "github.com/prebid/prebid-server/adapters/emx_digital" "github.com/prebid/prebid-server/adapters/engagebdr" "github.com/prebid/prebid-server/adapters/eplanning" @@ -107,6 +108,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 8a53e4adcf2..b3ecddb06cd 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -46,6 +46,7 @@ const ( BidderConversant BidderName = "conversant" BidderCpmstar BidderName = "cpmstar" BidderDatablocks BidderName = "datablocks" + BidderDmx BidderName = "dmx" BidderEmxDigital BidderName = "emx_digital" BidderEngageBDR BidderName = "engagebdr" BidderEPlanning BidderName = "eplanning" @@ -122,6 +123,7 @@ var BidderMap = map[string]BidderName{ "conversant": BidderConversant, "cpmstar": BidderCpmstar, "datablocks": BidderDatablocks, + "dmx": BidderDmx, "emx_digital": BidderEmxDigital, "engagebdr": BidderEngageBDR, "eplanning": BidderEPlanning, 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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 791a00de0a9..5dccf855add 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -24,6 +24,7 @@ import ( "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/cpmstar" "github.com/prebid/prebid-server/adapters/datablocks" + "github.com/prebid/prebid-server/adapters/dmx" "github.com/prebid/prebid-server/adapters/emx_digital" "github.com/prebid/prebid-server/adapters/engagebdr" "github.com/prebid/prebid-server/adapters/eplanning" @@ -94,6 +95,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 9aae284da2a..ddd067e8be7 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -32,6 +32,7 @@ func TestNewSyncerMap(t *testing.T) { 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, From b10b55ce107f1f217b5476367129aaf528350990 Mon Sep 17 00:00:00 2001 From: hbanalytics <55453525+hbanalytics@users.noreply.github.com> Date: Thu, 4 Jun 2020 20:18:34 +0300 Subject: [PATCH 106/381] Fix sync url for Yieldone s2s Bid Adapter (#1336) * Fix typo in Yieldone sync url --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index e93aed46eab..4e54bc712a2 100755 --- a/config/config.go +++ b/config/config.go @@ -575,7 +575,7 @@ func (cfg *Configuration) setDerivedDefaults() { // 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldmo%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") - setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldone, "https://y.one.impact-ad.jp/hbs_sc?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldone%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dzeroclickfraud%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D") } From dc9d246285fea7b5fe13ebdb4a5e2992b0cbbead Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 8 Jun 2020 17:34:21 -0400 Subject: [PATCH 107/381] CCPA Video Bug (#1333) --- endpoints/openrtb2/amp_auction_test.go | 3 +- endpoints/openrtb2/auction.go | 7 +- endpoints/openrtb2/auction_test.go | 12 +- .../video/video_invalid_sample.json | 125 +++++++------- .../video/video_valid_sample.json | 155 ++++++++--------- .../video_valid_sample_ccpa_malformed.json | 88 ++++++++++ .../video/video_valid_sample_ccpa_valid.json | 88 ++++++++++ ...ideo_valid_sample_different_durations.json | 159 +++++++++--------- ...o_valid_sample_with_device_user_agent.json | 155 ++++++++--------- ...alid_sample_without_device_user_agent.json | 122 +++++++------- endpoints/openrtb2/video_auction.go | 4 +- endpoints/openrtb2/video_auction_test.go | 101 ++++++++++- privacy/ccpa/policy.go | 37 +++- privacy/ccpa/policy_test.go | 42 +++++ 14 files changed, 729 insertions(+), 369 deletions(-) create mode 100644 endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_malformed.json create mode 100644 endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_valid.json diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 9dc81eb1b9d..289db3f48cb 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -122,9 +122,8 @@ func TestAMPPageInfo(t *testing.T) { } func TestGDPRConsent(t *testing.T) { - consent := "BONV8oqONXwgmADACHENAO7pqzAAppY" + consent := "BOu5On0Ou5On0ADACHENAO7pqzAAppY" existingConsent := "BONV8oqONXwgmADACHENAO7pqzAAppY" - digitrust := &openrtb_ext.ExtUserDigiTrust{ ID: "anyDigitrustID", KeyV: 1, diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index bcb13724519..bd50fca9149 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -315,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)) diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index fdd6b3a47cf..c3b9267bf8b 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -915,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), @@ -943,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". 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 index 68c3f4e1c15..6a9dc605ea2 100644 --- 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 @@ -1,80 +1,85 @@ - { - "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" + "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 } - ] - }, - "site": { - "page": "prebid.com" - }, - "user": { - "buyeruids": { - "appnexus": "unique_id_an", - "rubicon": "unique_id_rubi" }, - "gdpr": { - "consentrequired": false, - "consentstring": "something" + "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 }, - "yob": 1991, - "gender": "F", - "keywords": "Hotels, Travelling" - }, - "device": { - "ua": "TestHeaderSample", - "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_without_device_user_agent.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json index e040a5625ba..199391865b2 100644 --- 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 @@ -1,63 +1,69 @@ - { - "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" + "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 } - ] - }, - "site": { - "page": "prebid.com" - }, - "user": { - "buyeruids": { - "appnexus": "unique_id_an", - "rubicon": "unique_id_rubi" }, - "gdpr": { - "consentrequired": false, - "consentstring": "something" + "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 }, - "yob": 1991, - "gender": "F", - "keywords": "Hotels, Travelling" - }, - "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/video_auction.go b/endpoints/openrtb2/video_auction.go index 020a5196333..64c99fa5a3e 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -204,7 +204,7 @@ 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 { + if errortypes.ContainsFatalError(errL) { handleError(&labels, w, errL, &vo, &debugLog) return } @@ -232,7 +232,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL = append(errL, acctIdErr) + errL := []error{err} handleError(&labels, w, errL, &vo, &debugLog) return } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 5ba34068f7b..631cb277f7f 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1,6 +1,7 @@ package openrtb2 import ( + "bytes" "context" "encoding/json" "errors" @@ -860,12 +861,12 @@ func TestParseVideoRequestWithUserAgentAndHeader(t *testing.T) { 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) - req, valErr, podErr := deps.parseVideoRequest(reqData, headers) + 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") @@ -883,7 +884,8 @@ func TestParseVideoRequestWithUserAgentAndEmptyHeader(t *testing.T) { headers := http.Header{} deps := mockDeps(t, ex) - req, valErr, podErr := deps.parseVideoRequest(reqData, headers) + 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") @@ -902,7 +904,8 @@ func TestParseVideoRequestWithoutUserAgentWithHeader(t *testing.T) { headers.Add("User-Agent", "TestHeader") deps := mockDeps(t, ex) - req, valErr, podErr := deps.parseVideoRequest(reqData, headers) + 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") @@ -920,7 +923,8 @@ func TestParseVideoRequestWithoutUserAgentAndEmptyHeader(t *testing.T) { headers := http.Header{} deps := mockDeps(t, ex) - req, valErr, podErr := deps.parseVideoRequest(reqData, headers) + 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") @@ -942,7 +946,8 @@ func TestParseVideoRequestWithEncodedUserAgentInHeader(t *testing.T) { headers.Add("User-Agent", uaEncoded) deps := mockDeps(t, ex) - req, valErr, podErr := deps.parseVideoRequest(reqData, headers) + 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") @@ -963,7 +968,8 @@ func TestParseVideoRequestWithDecodedUserAgentInHeader(t *testing.T) { headers.Add("User-Agent", uaDecoded) deps := mockDeps(t, ex) - req, valErr, podErr := deps.parseVideoRequest(reqData, headers) + 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") @@ -1036,6 +1042,71 @@ func TestCreateImpressionTemplate(t *testing.T) { 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{} @@ -1166,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/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index 64579f2a2f6..11ac434595a 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -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 } @@ -59,6 +63,37 @@ func (p Policy) Write(req *openrtb.BidRequest) error { 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 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 policy does not adhere to the IAB spec. func (p Policy) Validate() error { if err := ValidateConsent(p.Value); err != nil { diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go index 740f95a8a6a..e9b4c4525b1 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -112,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"}, From 47bed2a1a2ab043113391c2ebe25338ecbd83446 Mon Sep 17 00:00:00 2001 From: Artur Aleksanyan Date: Tue, 9 Jun 2020 18:48:04 +0400 Subject: [PATCH 108/381] Add Pubnative bidder documentation (#1340) --- docs/bidders/pubnative.md | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/bidders/pubnative.md 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 From c628f1a83238d9e0ec5b430dd196d597145e1d11 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Tue, 9 Jun 2020 11:15:01 -0400 Subject: [PATCH 109/381] Timeout notification monitoring and debugging (#1322) --- config/config.go | 31 ++++++++++++++++++++ config/config_test.go | 8 ++++++ config/util/loggers.go | 24 ++++++++++++++++ config/util/loggers_test.go | 32 +++++++++++++++++++++ docs/developers/add-new-bidder.md | 10 +++++++ exchange/adapter_map.go | 6 ++-- exchange/adapter_map_test.go | 5 ++-- exchange/bidder.go | 36 +++++++++++++++++++----- exchange/bidder_test.go | 18 ++++++------ exchange/exchange.go | 2 +- exchange/targeting_test.go | 4 ++- pbsmetrics/config/metrics.go | 11 ++++++++ pbsmetrics/go_metrics.go | 18 ++++++++++++ pbsmetrics/go_metrics_test.go | 3 ++ pbsmetrics/metrics.go | 1 + pbsmetrics/metrics_mock.go | 5 ++++ pbsmetrics/prometheus/prometheus.go | 23 +++++++++++++++ pbsmetrics/prometheus/prometheus_test.go | 21 ++++++++++++++ 18 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 config/util/loggers.go create mode 100644 config/util/loggers_test.go diff --git a/config/config.go b/config/config.go index 4e54bc712a2..559fc5dac19 100755 --- a/config/config.go +++ b/config/config.go @@ -66,6 +66,8 @@ type Configuration struct { 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 @@ -104,6 +106,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 } @@ -450,6 +453,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 @@ -820,6 +847,10 @@ func SetupViper(v *viper.Viper, filename string) { 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 92794d7941e..ee8e68e7025 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -426,6 +426,14 @@ func TestCookieSizeError(t *testing.T) { } } +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/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/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/exchange/adapter_map.go b/exchange/adapter_map.go index 390016117fb..c8fbb775a21 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -5,6 +5,8 @@ import ( "net/http" "strings" + "github.com/prebid/prebid-server/pbsmetrics" + "github.com/prebid/prebid-server/adapters" ttx "github.com/prebid/prebid-server/adapters/33across" "github.com/prebid/prebid-server/adapters/adform" @@ -85,7 +87,7 @@ 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), @@ -190,7 +192,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 a732f357897..f472ab1d988 100644 --- a/exchange/adapter_map_test.go +++ b/exchange/adapter_map_test.go @@ -7,11 +7,12 @@ import ( "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" + metricsConfig "github.com/prebid/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/bidder.go b/exchange/bidder.go index 7a53db5ee97..f9b4a522343 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -10,13 +10,18 @@ import ( "net/http" "time" + "github.com/golang/glog" + "github.com/prebid/prebid-server/config/util" + "github.com/mxmCherry/openrtb" nativeRequests "github.com/mxmCherry/openrtb/native/request" nativeResponse "github.com/mxmCherry/openrtb/native/response" "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currencies" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/pbsmetrics" "golang.org/x/net/context/ctxhttp" ) @@ -82,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) (*pbsOrtbSeatBid, []error) { @@ -365,8 +374,21 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou httpReq, err := http.NewRequest(toReq.Method, toReq.Uri, bytes.NewBuffer(toReq.Body)) if err == nil { httpReq.Header = req.Headers - ctxhttp.Do(ctx, bidder.Client, httpReq) - // No validation yet on sending notifications + 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) } } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index f20b431c13a..fa04e6a4771 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -12,8 +12,10 @@ import ( "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currencies" "github.com/prebid/prebid-server/openrtb_ext" + metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config" "github.com/stretchr/testify/assert" nativeRequests "github.com/mxmCherry/openrtb/native/request" @@ -64,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{}) @@ -152,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{}) @@ -510,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, @@ -658,7 +660,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(), @@ -824,7 +826,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, @@ -939,7 +941,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( @@ -1051,7 +1053,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( @@ -1072,7 +1074,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{}) if bids != nil { diff --git a/exchange/exchange.go b/exchange/exchange.go index 6d51b87de4a..660beb641ef 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -69,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 diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index f86309684c6..72de1d4261f 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -8,12 +8,14 @@ import ( "testing" "time" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currencies" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/pbsmetrics" metricsConf "github.com/prebid/prebid-server/pbsmetrics/config" + metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config" "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" @@ -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/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index e1cdaceb0e5..4e249785ba6 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -188,6 +188,13 @@ func (me *MultiMetricsEngine) RecordRequestQueueTime(success bool, requestType p } } +// 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{} @@ -262,3 +269,7 @@ func (me *DummyMetricsEngine) RecordPrebidCacheRequestTime(success bool, length // 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/go_metrics.go b/pbsmetrics/go_metrics.go index ff3d9681fb1..1ced4d57269 100644 --- a/pbsmetrics/go_metrics.go +++ b/pbsmetrics/go_metrics.go @@ -48,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 @@ -131,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, @@ -209,6 +215,9 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d 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 } @@ -544,6 +553,15 @@ func (me *Metrics) RecordRequestQueueTime(success bool, requestType RequestType, } +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 253ff69e3c2..25f75e77758 100644 --- a/pbsmetrics/go_metrics_test.go +++ b/pbsmetrics/go_metrics_test.go @@ -53,6 +53,9 @@ func TestNewMetrics(t *testing.T) { 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 611692c9c01..e65ba313338 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -275,4 +275,5 @@ type MetricsEngine interface { 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 1f5b84b1e0f..482cbf24fae 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -101,3 +101,8 @@ func (me *MetricsEngineMock) RecordPrebidCacheRequestTime(success bool, length t 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/prometheus.go b/pbsmetrics/prometheus/prometheus.go index d66defea4cd..e385b044981 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -28,6 +28,7 @@ type Metrics struct { requestsWithoutCookie *prometheus.CounterVec storedImpressionsCacheResult *prometheus.CounterVec storedRequestCacheResult *prometheus.CounterVec + timeout_notifications *prometheus.CounterVec // Adapter Metrics adapterBids *prometheus.CounterVec @@ -79,6 +80,11 @@ const ( 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} @@ -147,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).", @@ -398,3 +409,15 @@ func (m *Metrics) RecordRequestQueueTime(success bool, requestType pbsmetrics.Re 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 e4d6a4f78d1..24c50492139 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -923,6 +923,27 @@ func TestRecordRequestQueueTimeMetric(t *testing.T) { } } +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) From 4361bf64f83a085227ac97781b43f0f30e60a053 Mon Sep 17 00:00:00 2001 From: Gena Date: Tue, 9 Jun 2020 19:32:38 +0300 Subject: [PATCH 110/381] Add Adtarget server adapter (#1319) * Add Adtarget server adapter * Suggested changes for Adtarget --- adapters/adtarget/adtarget.go | 189 ++++++++++++++++++ adapters/adtarget/adtarget_test.go | 11 + .../exemplary/media-type-mapping.json | 88 ++++++++ .../adtargettest/exemplary/simple-banner.json | 62 ++++++ .../adtargettest/exemplary/simple-video.json | 55 +++++ .../adtargettest/params/race/banner.json | 3 + .../adtargettest/params/race/video.json | 3 + .../adtargettest/supplemental/audio.json | 25 +++ .../supplemental/explicit-dimensions.json | 58 ++++++ .../adtargettest/supplemental/native.json | 25 +++ .../supplemental/wrong-impression-ext.json | 26 +++ .../wrong-impression-mapping.json | 77 +++++++ adapters/adtarget/params_test.go | 60 ++++++ adapters/adtarget/usersync.go | 12 ++ adapters/adtarget/usersync_test.go | 37 ++++ config/config.go | 2 + docs/bidders/adtarget.md | 5 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adtarget.go | 9 + static/bidder-info/adtarget.yaml | 11 + static/bidder-params/adtarget.json | 26 +++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 24 files changed, 791 insertions(+) create mode 100644 adapters/adtarget/adtarget.go create mode 100644 adapters/adtarget/adtarget_test.go create mode 100644 adapters/adtarget/adtargettest/exemplary/media-type-mapping.json create mode 100644 adapters/adtarget/adtargettest/exemplary/simple-banner.json create mode 100644 adapters/adtarget/adtargettest/exemplary/simple-video.json create mode 100644 adapters/adtarget/adtargettest/params/race/banner.json create mode 100644 adapters/adtarget/adtargettest/params/race/video.json create mode 100644 adapters/adtarget/adtargettest/supplemental/audio.json create mode 100644 adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json create mode 100644 adapters/adtarget/adtargettest/supplemental/native.json create mode 100644 adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json create mode 100644 adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json create mode 100644 adapters/adtarget/params_test.go create mode 100644 adapters/adtarget/usersync.go create mode 100644 adapters/adtarget/usersync_test.go create mode 100644 docs/bidders/adtarget.md create mode 100644 openrtb_ext/imp_adtarget.go create mode 100644 static/bidder-info/adtarget.yaml create mode 100644 static/bidder-params/adtarget.json diff --git a/adapters/adtarget/adtarget.go b/adapters/adtarget/adtarget.go new file mode 100644 index 00000000000..77622d458a4 --- /dev/null +++ b/adapters/adtarget/adtarget.go @@ -0,0 +1,189 @@ +package adtarget + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..93732988120 --- /dev/null +++ b/adapters/adtarget/adtarget_test.go @@ -0,0 +1,11 @@ +package adtarget + +import ( + "testing" + + "github.com/prebid/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..b128d11c9cf --- /dev/null +++ b/adapters/adtarget/params_test.go @@ -0,0 +1,60 @@ +package adtarget + +import ( + "encoding/json" + "testing" + + "github.com/prebid/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..20bced25c72 --- /dev/null +++ b/adapters/adtarget/usersync.go @@ -0,0 +1,12 @@ +package adtarget + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/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..3ab2ed5b5df --- /dev/null +++ b/adapters/adtarget/usersync_test.go @@ -0,0 +1,37 @@ +package adtarget + +import ( + "fmt" + "github.com/prebid/prebid-server/privacy/ccpa" + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/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/config/config.go b/config/config.go index 559fc5dac19..07384f9d2d3 100755 --- a/config/config.go +++ b/config/config.go @@ -548,6 +548,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernel, "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3DadkernelAdn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdpone, "https://usersync.adpone.com/csync?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadpone%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24") // openrtb_ext.BidderAdOcean doesn't have a good default. @@ -752,6 +753,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.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") 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/exchange/adapter_map.go b/exchange/adapter_map.go index c8fbb775a21..2ea8f7fb648 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -18,6 +18,7 @@ import ( "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adoppler" "github.com/prebid/prebid-server/adapters/adpone" + "github.com/prebid/prebid-server/adapters/adtarget" "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" "github.com/prebid/prebid-server/adapters/aja" @@ -99,6 +100,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index b3ecddb06cd..659c6616fea 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -33,6 +33,7 @@ const ( BidderAdpone BidderName = "adpone" BidderAdmixer BidderName = "admixer" BidderAdOcean BidderName = "adocean" + BidderAdtarget BidderName = "adtarget" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderAJA BidderName = "aja" @@ -110,6 +111,7 @@ var BidderMap = map[string]BidderName{ "admixer": BidderAdmixer, "adocean": BidderAdOcean, "adpone": BidderAdpone, + "adtarget": BidderAdtarget, "adtelligent": BidderAdtelligent, "advangelists": BidderAdvangelists, "aja": BidderAJA, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 5dccf855add..751d2aabfbe 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -12,6 +12,7 @@ import ( "github.com/prebid/prebid-server/adapters/admixer" "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adpone" + "github.com/prebid/prebid-server/adapters/adtarget" "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" "github.com/prebid/prebid-server/adapters/aja" @@ -84,6 +85,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index ddd067e8be7..c9ef382fc92 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -21,6 +21,7 @@ func TestNewSyncerMap(t *testing.T) { 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, From 86fa52b19e92348d070d97fefd83d21974bbf25d Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Tue, 9 Jun 2020 13:23:57 -0400 Subject: [PATCH 111/381] Update Auction OpenRTB Sample (#1342) * Update Auction OpenRTB Sample * Removed Extra "Or" --- docs/endpoints/openrtb2/auction.md | 209 +++++++++++++++++------------ 1 file changed, 126 insertions(+), 83 deletions(-) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index 67430e51481..d09216188b8 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 @@ -98,7 +139,7 @@ If you find that some bidders use Gross bids, publishers can adjust for it with "ext": { "prebid": { "bidadjustmentfactors": { - "appnexus: 0.8, + "appnexus": 0.8, "rubicon": 0.7 } } @@ -126,8 +167,8 @@ 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" + "max": 20.00, + "increment": 0.10 // This is equivalent to the deprecated "pricegranularity": "medium" }] }, "includewinners": false, // Optional param defaulting to true @@ -146,23 +187,29 @@ 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`) @@ -238,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": { @@ -303,12 +348,12 @@ For example, a request may return this in `response.ext` "ext": { "errors": { "appnexus": [{ - "code": 2, - "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio." + "code": 2, + "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio." }], "rubicon": [{ - "code": 1, - "message": "The request exceeded the timeout allocated" + "code": 1, + "message": "The request exceeded the timeout allocated" }] } } @@ -413,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, From 24665e8341ce985de7b7524e35a63962ffe5146d Mon Sep 17 00:00:00 2001 From: Brandon Ling <51931757+blingster7@users.noreply.github.com> Date: Thu, 11 Jun 2020 14:10:50 -0400 Subject: [PATCH 112/381] Triplelift: Add SRA Support (#1347) --- adapters/triplelift/triplelift_test.go | 2 +- .../triplelifttest/exemplary/optional-params.json | 2 +- .../triplelift/triplelifttest/exemplary/simple-banner.json | 2 +- .../triplelift/triplelifttest/exemplary/simple-video.json | 6 +++--- .../triplelifttest/supplemental/badresponseext.json | 2 +- .../triplelifttest/supplemental/badstatuscode.json | 2 +- .../triplelifttest/supplemental/notgoodstatuscode.json | 2 +- config/config.go | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/adapters/triplelift/triplelift_test.go b/adapters/triplelift/triplelift_test.go index 2d7ed04f51b..6fd2b506f8a 100644 --- a/adapters/triplelift/triplelift_test.go +++ b/adapters/triplelift/triplelift_test.go @@ -6,5 +6,5 @@ import ( ) func TestJsonSamples(t *testing.T) { - adapterstest.RunJSONBidderTest(t, "triplelifttest", NewTripleliftBidder(nil, "http://tlx.3lift.net/s2s/auction?supplier_id=20")) + adapterstest.RunJSONBidderTest(t, "triplelifttest", NewTripleliftBidder(nil, "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20")) } diff --git a/adapters/triplelift/triplelifttest/exemplary/optional-params.json b/adapters/triplelift/triplelifttest/exemplary/optional-params.json index 0851bc096d7..90c8da5b3c1 100644 --- a/adapters/triplelift/triplelifttest/exemplary/optional-params.json +++ b/adapters/triplelift/triplelifttest/exemplary/optional-params.json @@ -28,7 +28,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/triplelift/triplelifttest/exemplary/simple-banner.json b/adapters/triplelift/triplelifttest/exemplary/simple-banner.json index ff680037a7e..156e07e37eb 100644 --- a/adapters/triplelift/triplelifttest/exemplary/simple-banner.json +++ b/adapters/triplelift/triplelifttest/exemplary/simple-banner.json @@ -27,7 +27,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/triplelift/triplelifttest/exemplary/simple-video.json b/adapters/triplelift/triplelifttest/exemplary/simple-video.json index 185446bd243..846c62b4d37 100644 --- a/adapters/triplelift/triplelifttest/exemplary/simple-video.json +++ b/adapters/triplelift/triplelifttest/exemplary/simple-video.json @@ -33,7 +33,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "body": { "id": "test-request-id", "imp": [ @@ -85,7 +85,7 @@ "adomain": [ "foo.com" ], - "iurl": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "iurl": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "cid": "958", "crid": "29681110", "h": 250, @@ -122,7 +122,7 @@ "adomain": [ "foo.com" ], - "iurl": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "iurl": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "cid": "958", "crid": "29681110", "w": 300, diff --git a/adapters/triplelift/triplelifttest/supplemental/badresponseext.json b/adapters/triplelift/triplelifttest/supplemental/badresponseext.json index 324c05825c9..6c09448fc4a 100644 --- a/adapters/triplelift/triplelifttest/supplemental/badresponseext.json +++ b/adapters/triplelift/triplelifttest/supplemental/badresponseext.json @@ -27,7 +27,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json b/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json index 15799616933..f24eb7998ed 100644 --- a/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json +++ b/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json @@ -27,7 +27,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json b/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json index 963db593776..bdcc0e3a666 100644 --- a/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json +++ b/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json @@ -27,7 +27,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20", + "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20", "body": { "id": "test-request-id", "imp": [ diff --git a/config/config.go b/config/config.go index 07384f9d2d3..56b6a1ba88d 100755 --- a/config/config.go +++ b/config/config.go @@ -805,7 +805,7 @@ 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") From eb77b170618b581833a1029264a7b39027644e1a Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 15 Jun 2020 10:24:36 -0400 Subject: [PATCH 113/381] Privacy: Limit Ad Tracking (#1334) --- config/config.go | 13 ++ config/config_test.go | 3 + exchange/exchange.go | 10 +- exchange/exchange_test.go | 17 ++- .../exchangetest/lmt-featureflag-off.json | 63 +++++++++ exchange/exchangetest/lmt-featureflag-on.json | 61 +++++++++ exchange/utils.go | 25 +++- exchange/utils_test.go | 94 +++++++++++-- privacy/enforcement.go | 9 +- privacy/enforcement_test.go | 53 ++++++-- privacy/lmt/policy.go | 33 +++++ privacy/lmt/policy_test.go | 128 ++++++++++++++++++ 12 files changed, 473 insertions(+), 36 deletions(-) create mode 100644 exchange/exchangetest/lmt-featureflag-off.json create mode 100644 exchange/exchangetest/lmt-featureflag-on.json create mode 100644 privacy/lmt/policy.go create mode 100644 privacy/lmt/policy_test.go diff --git a/config/config.go b/config/config.go index 56b6a1ba88d..0f470c6a611 100755 --- a/config/config.go +++ b/config/config.go @@ -49,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"` @@ -139,6 +140,13 @@ 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"` @@ -193,6 +201,10 @@ type CCPA struct { Enforce bool `mapstructure:"enforce"` } +type LMT struct { + Enforce bool `mapstructure:"enforce"` +} + type Analytics struct { File FileLogs `mapstructure:"file"` } @@ -836,6 +848,7 @@ func SetupViper(v *viper.Viper, filename string) { 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", "") diff --git a/config/config_test.go b/config/config_test.go index ee8e68e7025..2b291fe978d 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 @@ -240,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") diff --git a/exchange/exchange.go b/exchange/exchange.go index 660beb641ef..84ae35d644c 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -48,7 +48,7 @@ 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 @@ -77,7 +77,11 @@ 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 } @@ -100,7 +104,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) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index e9b2127e18b..4f329962a53 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -731,7 +731,17 @@ 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 { @@ -816,7 +826,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 { @@ -854,7 +864,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] gDPR: gdpr.AlwaysAllow{}, currencyConverter: currencies.NewRateConverterDefault(), UsersyncIfAmbiguous: false, - enforceCCPA: enforceCCPA, + privacyConfig: privacyConfig, } } @@ -1620,6 +1630,7 @@ type exchangeSpec struct { OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` Response exchangeResponse `json:"response,omitempty"` EnforceCCPA bool `json:"enforceCcpa"` + EnforceLMT bool `json:"enforceLmt"` DebugLog *DebugLog `json:"debuglog,omitempty"` } 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/utils.go b/exchange/utils.go index f602d1e8fba..54122d13c09 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -8,11 +8,13 @@ import ( "github.com/buger/jsonparser" "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/lmt" ) // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is: @@ -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 { @@ -45,15 +47,24 @@ func cleanOpenRTBRequests(ctx context.Context, consent := extractConsent(orig) 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 { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index acbf25ff691..4dad3f54648 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/stretchr/testify/assert" @@ -69,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 { @@ -99,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-"}`), + } - results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, test.enforceCCPA) + 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, 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) @@ -163,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", @@ -178,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{ @@ -189,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/privacy/enforcement.go b/privacy/enforcement.go index d302192ec3f..8a5d201fc95 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -10,11 +10,12 @@ type Enforcement struct { 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 || e.GDPRGeo + return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo || e.LMT } // Apply cleans personally identifiable information from an OpenRTB bid request. @@ -34,7 +35,7 @@ func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 { return ScrubStrategyIPV6Lowest32 } - if e.GDPR || e.CCPA { + if e.GDPR || e.CCPA || e.LMT { return ScrubStrategyIPV6Lowest16 } @@ -46,7 +47,7 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { return ScrubStrategyGeoFull } - if e.GDPRGeo || e.CCPA { + if e.GDPRGeo || e.CCPA || e.LMT { return ScrubStrategyGeoReducedPrecision } @@ -63,7 +64,7 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs } // If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above) - if e.CCPA || e.GDPR { + if e.CCPA || e.GDPR || e.LMT { return ScrubStrategyUserID } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 0e82648d4b9..968c6354710 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -21,6 +21,7 @@ func TestAny(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + LMT: false, }, expected: false, }, @@ -31,6 +32,7 @@ func TestAny(t *testing.T) { COPPA: true, GDPR: true, GDPRGeo: true, + LMT: true, }, expected: true, }, @@ -41,16 +43,7 @@ func TestAny(t *testing.T) { COPPA: true, GDPR: false, GDPRGeo: false, - }, - expected: true, - }, - { - description: "GDPRGeo only", - enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPR: false, - GDPRGeo: true, + LMT: true, }, expected: true, }, @@ -79,6 +72,7 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: true, GDPRGeo: true, + LMT: true, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -93,6 +87,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + LMT: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -107,6 +102,7 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: false, GDPRGeo: false, + LMT: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -121,6 +117,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: true, GDPRGeo: true, + LMT: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -135,6 +132,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: true, GDPRGeo: true, + LMT: false, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -149,6 +147,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + LMT: false, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -163,6 +162,7 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: true, GDPRGeo: true, + LMT: false, }, ampGDPRException: true, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -177,6 +177,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: true, GDPRGeo: false, + LMT: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -191,6 +192,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: true, + LMT: false, }, ampGDPRException: false, expectedDeviceIPv6: ScrubStrategyIPV6None, @@ -198,6 +200,36 @@ func TestApply(t *testing.T) { 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 { @@ -229,6 +261,7 @@ func TestApplyNoneApplicable(t *testing.T) { CCPA: false, COPPA: false, GDPR: false, + LMT: false, } enforcement.apply(req, false, m) diff --git a/privacy/lmt/policy.go b/privacy/lmt/policy.go new file mode 100644 index 00000000000..79425bf59f7 --- /dev/null +++ b/privacy/lmt/policy.go @@ -0,0 +1,33 @@ +package lmt + +import ( + "github.com/mxmCherry/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..45de219a9bf --- /dev/null +++ b/privacy/lmt/policy_test.go @@ -0,0 +1,128 @@ +package lmt + +import ( + "testing" + + "github.com/mxmCherry/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) + } +} From dd05c38f5b698441b8f5c07908506093b2290745 Mon Sep 17 00:00:00 2001 From: Richard Lee <14349+dlackty@users.noreply.github.com> Date: Mon, 15 Jun 2020 23:21:03 +0800 Subject: [PATCH 114/381] Avoid overriding AMP request original size with mutli-size (#1352) --- endpoints/openrtb2/amp_auction.go | 17 ++++++++++------- endpoints/openrtb2/amp_auction_test.go | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 586481ddfc5..2dcd572c63c 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -407,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) { diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 289db3f48cb..731fd55e196 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -832,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, From 62fe413dad59ee0c6c95e410ae6ad26d8af304ea Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Wed, 17 Jun 2020 10:25:02 -0400 Subject: [PATCH 115/381] Extra logging for timeout notifications (#1349) --- exchange/bidder.go | 13 ++++++++ exchange/bidder_test.go | 74 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/exchange/bidder.go b/exchange/bidder.go index f9b4a522343..df9f0a3bf1b 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -389,7 +389,20 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou } } 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) } } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index fa04e6a4771..fff397f0084 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -1229,6 +1229,64 @@ func TestSetAssetTypes(t *testing.T) { } } +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 @@ -1302,3 +1360,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 +} From 2d2ed0c6dcd984769d1a65edc15a96bfc4c69482 Mon Sep 17 00:00:00 2001 From: Daniel Cassidy Date: Wed, 17 Jun 2020 18:32:47 +0100 Subject: [PATCH 116/381] Consumable: Correct bid type, should always be "banner". (#1359) --- adapters/consumable/consumable.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/adapters/consumable/consumable.go b/adapters/consumable/consumable.go index 1fa23377319..243f1b8000b 100644 --- a/adapters/consumable/consumable.go +++ b/adapters/consumable/consumable.go @@ -268,8 +268,11 @@ func (a *ConsumableAdapter) MakeBids( //bid.referrer = utils.getTopWindowUrl(); bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ - Bid: &bid, - BidType: getMediaTypeForImp(getImp(bid.ImpID, internalRequest.Imp)), + Bid: &bid, + // Consumable units are always HTML, never VAST. + // From Prebid's point of view, this means that Consumable units + // are always "banners". + BidType: openrtb_ext.BidTypeBanner, }) } } @@ -303,16 +306,6 @@ func extractExtensions(impression openrtb.Imp) (*adapters.ExtImpBidder, *openrtb return &bidderExt, &consumableExt, nil } -func getMediaTypeForImp(imp *openrtb.Imp) openrtb_ext.BidType { - // TODO: Whatever logic we need here possibly as follows - may always be Video when we bid - if imp.Banner != nil { - return openrtb_ext.BidTypeBanner - } else if imp.Video != nil { - return openrtb_ext.BidTypeVideo - } - return openrtb_ext.BidTypeVideo -} - func testConsumableBidder(testClock instant, endpoint string) *ConsumableAdapter { return &ConsumableAdapter{testClock, endpoint} } From 98417cb13f3181a1cfd1d1b8089ab17de3423467 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 17 Jun 2020 13:33:22 -0400 Subject: [PATCH 117/381] Build With Go 1.14 (#1350) --- .travis.yml | 3 +-- Dockerfile | 4 ++-- README.md | 5 +++-- go.mod | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea2c46c4374..692141f716c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: go go: - - '1.12' - '1.13' - - '1.14' + - '1.14.2' go_import_path: github.com/prebid/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 a59bf5f6aa3..b69e7e76db4 100644 --- a/README.md +++ b/README.md @@ -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/go.mod b/go.mod index 0224057e464..72bb9b74886 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/prebid/prebid-server -go 1.12 +go 1.13 require ( github.com/BurntSushi/toml v0.3.1 // indirect From d1c81294a7d05bb626c9b8f8181d845b8cee7fe9 Mon Sep 17 00:00:00 2001 From: jmaynardxandr <46759873+jmaynardxandr@users.noreply.github.com> Date: Wed, 17 Jun 2020 11:24:33 -0700 Subject: [PATCH 118/381] Category mapping changes from product team. (#1348) --- static/adapter/appnexus/opts.json | 69 +- .../category-mapping/freewheel/freewheel.json | 2348 +++++++++-------- 2 files changed, 1234 insertions(+), 1183 deletions(-) diff --git a/static/adapter/appnexus/opts.json b/static/adapter/appnexus/opts.json index 7bb297e0b41..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,33 +21,79 @@ "19": "IAB18-4", "20": "IAB1-5", "21": "IAB1-6", - "22": "IAB19-28", + "22": "IAB3-4", "23": "IAB19-13", "24": "IAB22-2", "25": "IAB3-9", - "26": "IAB17-26", + "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": "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", + "68": "IAB9-5", + "69": "IAB7-44", + "71": "IAB22-3", + "73": "IAB19-30", "74": "IAB8-5", - "87": "IAB3-7" - } + "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/category-mapping/freewheel/freewheel.json b/static/category-mapping/freewheel/freewheel.json index 7eebcce0c98..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": "413", - "name": "Gaming" - }, - "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 From 6eed87311b4a5a2f05bcceb758296a6b798f4dfb Mon Sep 17 00:00:00 2001 From: Simon Critchley Date: Thu, 18 Jun 2020 15:08:06 +0100 Subject: [PATCH 119/381] Adds Avocet adapter (#1354) --- adapters/avocet/avocet.go | 124 ++++++++ adapters/avocet/avocet/exemplary/banner.json | 106 +++++++ adapters/avocet/avocet/exemplary/video.json | 104 +++++++ adapters/avocet/avocet_test.go | 301 +++++++++++++++++++ adapters/avocet/usersync.go | 12 + adapters/avocet/usersync_test.go | 35 +++ config/config.go | 2 + docs/bidders/avocet.md | 5 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_avocet.go | 7 + static/bidder-info/avocet.yaml | 11 + static/bidder-params/avocet.json | 24 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 15 files changed, 738 insertions(+) create mode 100644 adapters/avocet/avocet.go create mode 100644 adapters/avocet/avocet/exemplary/banner.json create mode 100644 adapters/avocet/avocet/exemplary/video.json create mode 100644 adapters/avocet/avocet_test.go create mode 100644 adapters/avocet/usersync.go create mode 100644 adapters/avocet/usersync_test.go create mode 100644 docs/bidders/avocet.md create mode 100644 openrtb_ext/imp_avocet.go create mode 100644 static/bidder-info/avocet.yaml create mode 100644 static/bidder-params/avocet.json diff --git a/adapters/avocet/avocet.go b/adapters/avocet/avocet.go new file mode 100644 index 00000000000..918fc23e894 --- /dev/null +++ b/adapters/avocet/avocet.go @@ -0,0 +1,124 @@ +package avocet + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..ff2159bf406 --- /dev/null +++ b/adapters/avocet/avocet_test.go @@ -0,0 +1,301 @@ +package avocet + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/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..f1075ab3c52 --- /dev/null +++ b/adapters/avocet/usersync.go @@ -0,0 +1,12 @@ +package avocet + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/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..8fba403f1b1 --- /dev/null +++ b/adapters/avocet/usersync_test.go @@ -0,0 +1,35 @@ +package avocet + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/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/config/config.go b/config/config.go index 0f470c6a611..01de9b1ab2e 100755 --- a/config/config.go +++ b/config/config.go @@ -567,6 +567,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdvangelists, "https://nep.advangelists.com/xp/user-sync?acctid={aid}&&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Daja%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25s") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAppnexus, "https://ib.adnxs.com/getuid?"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadnxs%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAvocet, "https://ads.avct.cloud/getuid?&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbrightroll%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -772,6 +773,7 @@ func SetupViper(v *viper.Viper, filename string) { 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") 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/exchange/adapter_map.go b/exchange/adapter_map.go index 2ea8f7fb648..6e771236fb7 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -25,6 +25,7 @@ import ( "github.com/prebid/prebid-server/adapters/applogy" "github.com/prebid/prebid-server/adapters/appnexus" "github.com/prebid/prebid-server/adapters/audienceNetwork" + "github.com/prebid/prebid-server/adapters/avocet" "github.com/prebid/prebid-server/adapters/beachfront" "github.com/prebid/prebid-server/adapters/beintoo" "github.com/prebid/prebid-server/adapters/brightroll" @@ -106,6 +107,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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), + 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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 659c6616fea..416f36d135f 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -40,6 +40,7 @@ const ( BidderApplogy BidderName = "applogy" BidderAppnexus BidderName = "appnexus" BidderAdoppler BidderName = "adoppler" + BidderAvocet BidderName = "avocet" BidderBeachfront BidderName = "beachfront" BidderBeintoo BidderName = "beintoo" BidderBrightroll BidderName = "brightroll" @@ -118,6 +119,7 @@ var BidderMap = map[string]BidderName{ "applogy": BidderApplogy, "appnexus": BidderAppnexus, "adoppler": BidderAdoppler, + "avocet": BidderAvocet, "beachfront": BidderBeachfront, "beintoo": BidderBeintoo, "brightroll": BidderBrightroll, 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/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-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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 751d2aabfbe..1beb9d586df 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -18,6 +18,7 @@ import ( "github.com/prebid/prebid-server/adapters/aja" "github.com/prebid/prebid-server/adapters/appnexus" "github.com/prebid/prebid-server/adapters/audienceNetwork" + "github.com/prebid/prebid-server/adapters/avocet" "github.com/prebid/prebid-server/adapters/beachfront" "github.com/prebid/prebid-server/adapters/beintoo" "github.com/prebid/prebid-server/adapters/brightroll" @@ -90,6 +91,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index c9ef382fc92..69751dd55f4 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -26,6 +26,7 @@ func TestNewSyncerMap(t *testing.T) { 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, From a8feeca97c88cd6ca41483fdba02123f5d40b691 Mon Sep 17 00:00:00 2001 From: Marcin Muras <47107445+mmuras@users.noreply.github.com> Date: Thu, 18 Jun 2020 17:27:42 +0200 Subject: [PATCH 120/381] AdOcean adapter - Support for sizes defined in prebid configuration. (#1339) support for multiple sizes bump version to 1.1.0 --- adapters/adocean/adocean.go | 155 ++++++++++++---- .../exemplary/multi-banner-impression.json | 5 +- .../exemplary/single-banner-impression.json | 2 +- .../supplemental/bad-response.json | 2 +- .../supplemental/encode-error.json | 2 +- .../supplemental/network-error.json | 2 +- .../adoceantest/supplemental/no-bid.json | 4 +- .../adoceantest/supplemental/no-sizes.json | 168 ++++++++++++++++++ .../supplemental/requests-merge.json | 4 +- 9 files changed, 298 insertions(+), 46 deletions(-) create mode 100644 adapters/adocean/adoceantest/supplemental/no-sizes.json diff --git a/adapters/adocean/adocean.go b/adapters/adocean/adocean.go index 514aeb38424..8740712b6a0 100644 --- a/adapters/adocean/adocean.go +++ b/adapters/adocean/adocean.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "regexp" + "sort" "strconv" "strings" "text/template" @@ -19,7 +20,7 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -const adapterVersion = "1.0.0" +const adapterVersion = "1.1.0" const maxUriLength = 8000 const measurementCode = ` ", + "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 index 9b5eb39aee2..e0736ec918f 100644 --- a/adapters/adocean/adoceantest/supplemental/requests-merge.json +++ b/adapters/adocean/adoceantest/supplemental/requests-merge.json @@ -83,7 +83,7 @@ }, "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.0.0" + "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, @@ -117,7 +117,7 @@ } }, { "expectedRequest": { - "uri": "https://myao.adocean.pl/_10000000/ad.json?aid=adoceanmyaowafpdwlrks%3Aao-test-three&gdpr=1&gdpr_consent=COwK6gaOwK6gaFmAAAENAPCAAAAAAAAAAAAAAAAAAAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&id=tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7&nc=1&nosecure=1&pbsrv_v=1.0.0" + "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, From 9c79ee485422e4e8383df8e288e5e20b95ec6841 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Thu, 18 Jun 2020 11:59:13 -0400 Subject: [PATCH 121/381] =?UTF-8?q?Log=20account=20id=20and=20all=20bidder?= =?UTF-8?q?=20names=20when=20recovering=20from=20OpenRTB=20auction=20bidde?= =?UTF-8?q?r=E2=80=A6=20(#1358)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- exchange/exchange.go | 19 ++++++++++++++++--- exchange/exchange_test.go | 10 +++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/exchange/exchange.go b/exchange/exchange.go index 84ae35d644c..d7eab0f4475 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -303,7 +303,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) @@ -373,11 +373,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) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 4f329962a53..93cb60fb5af 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -582,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, From 5f39344c43d56331c69a855c979483a5e3d84086 Mon Sep 17 00:00:00 2001 From: tadam75 Date: Sat, 20 Jun 2020 19:22:25 +0200 Subject: [PATCH 122/381] Adding Smartadserver adapter (#1346) Co-authored-by: tadam --- adapters/smartadserver/params_test.go | 61 ++++++ adapters/smartadserver/smartadserver.go | 179 ++++++++++++++++++ adapters/smartadserver/smartadserver_test.go | 11 ++ .../exemplary/multi-banner.json | 175 +++++++++++++++++ .../exemplary/simple-banner.json | 94 +++++++++ .../exemplary/simple-video.json | 100 ++++++++++ .../smartadservertest/params/race/banner.json | 7 + .../smartadservertest/params/race/video.json | 7 + .../request-no-bidder-object.json | 21 ++ .../supplemental/request-no-ext-object.json | 19 ++ .../supplemental/request-no-imp.json | 13 ++ .../supplemental/request-site-recreated.json | 99 ++++++++++ .../response-200-without-body.json | 62 ++++++ .../supplemental/response-204.json | 56 ++++++ .../supplemental/response-400.json | 62 ++++++ .../supplemental/response-500.json | 62 ++++++ adapters/smartadserver/usersync.go | 12 ++ adapters/smartadserver/usersync_test.go | 35 ++++ config/config.go | 2 + docs/bidders/smartAdserver.md | 59 ++++++ exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_smartadserver.go | 9 + static/bidder-info/smartadserver.yaml | 11 ++ static/bidder-params/smartadserver.json | 35 ++++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 27 files changed, 1198 insertions(+) create mode 100644 adapters/smartadserver/params_test.go create mode 100644 adapters/smartadserver/smartadserver.go create mode 100644 adapters/smartadserver/smartadserver_test.go create mode 100644 adapters/smartadserver/smartadservertest/exemplary/multi-banner.json create mode 100644 adapters/smartadserver/smartadservertest/exemplary/simple-banner.json create mode 100644 adapters/smartadserver/smartadservertest/exemplary/simple-video.json create mode 100644 adapters/smartadserver/smartadservertest/params/race/banner.json create mode 100644 adapters/smartadserver/smartadservertest/params/race/video.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/request-no-bidder-object.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/request-no-ext-object.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/request-no-imp.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/request-site-recreated.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/response-200-without-body.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/response-204.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/response-400.json create mode 100644 adapters/smartadserver/smartadservertest/supplemental/response-500.json create mode 100644 adapters/smartadserver/usersync.go create mode 100644 adapters/smartadserver/usersync_test.go create mode 100644 docs/bidders/smartAdserver.md create mode 100644 openrtb_ext/imp_smartadserver.go create mode 100644 static/bidder-info/smartadserver.yaml create mode 100644 static/bidder-params/smartadserver.json diff --git a/adapters/smartadserver/params_test.go b/adapters/smartadserver/params_test.go new file mode 100644 index 00000000000..6e45bb1d046 --- /dev/null +++ b/adapters/smartadserver/params_test.go @@ -0,0 +1,61 @@ +package smartadserver + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/smartadserver.json +// +// These also validate the format of the external API: request.imp[i].ext.smartadserver + +// TestValidParams makes sure that the smartadserver 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.BidderSmartadserver, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected smartadserver params: %s \n Error: %s", validParam, err) + } + } +} + +// TestInvalidParams makes sure that the smartadserver 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.BidderSmartadserver, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"networkId":73}`, + `{"networkId":73,"siteId":1,"pageId":2,"formatId":3}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"networkId":"73"}`, + `{"networkId":"73","siteId":"1","pageId":"2","formatId":"3"}`, + `{"siteId":1,"pageId":2,"formatId":3}`, + `{"networkId":73,"pageId":2,"formatId":3}`, + `{"networkId":73,"siteId":1,"formatId":3}`, + `{"networkId":73,"siteId":1,"pageId":2}`, +} diff --git a/adapters/smartadserver/smartadserver.go b/adapters/smartadserver/smartadserver.go new file mode 100644 index 00000000000..c35b749c51c --- /dev/null +++ b/adapters/smartadserver/smartadserver.go @@ -0,0 +1,179 @@ +package smartadserver + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strconv" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type SmartAdserverAdapter struct { + host string +} + +func NewSmartadserverBidder(host string) *SmartAdserverAdapter { + return &SmartAdserverAdapter{ + host: host, + } +} + +// MakeRequests makes the HTTP requests which should be made to fetch bids. +func (a *SmartAdserverAdapter) 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", + }} + } + + var adapterRequests []*adapters.RequestData + var errs []error + + // We copy the original request. + smartRequest := *request + + // We create or copy the Site object. + if smartRequest.Site == nil { + smartRequest.Site = &openrtb.Site{} + } else { + site := *smartRequest.Site + smartRequest.Site = &site + } + + // We create or copy the Publisher object. + if smartRequest.Site.Publisher == nil { + smartRequest.Site.Publisher = &openrtb.Publisher{} + } else { + publisher := *smartRequest.Site.Publisher + smartRequest.Site.Publisher = &publisher + } + + // We send one serialized "smartRequest" per impression of the original request. + for _, imp := range request.Imp { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errs = append(errs, &errortypes.BadInput{ + Message: "Error parsing bidderExt object", + }) + continue + } + + var smartadserverExt openrtb_ext.ExtImpSmartadserver + if err := json.Unmarshal(bidderExt.Bidder, &smartadserverExt); err != nil { + errs = append(errs, &errortypes.BadInput{ + Message: "Error parsing smartadserverExt parameters", + }) + continue + } + + // Adding publisher id. + smartRequest.Site.Publisher.ID = strconv.Itoa(smartadserverExt.NetworkID) + + // We send one request for each impression. + smartRequest.Imp = []openrtb.Imp{imp} + + var errMarshal error + if imp.Ext, errMarshal = json.Marshal(smartadserverExt); errMarshal != nil { + errs = append(errs, &errortypes.BadInput{ + Message: errMarshal.Error(), + }) + continue + } + + reqJSON, err := json.Marshal(smartRequest) + if err != nil { + errs = append(errs, &errortypes.BadInput{ + Message: "Error parsing reqJSON object", + }) + continue + } + + url, err := a.BuildEndpointURL(&smartadserverExt) + if url == "" { + errs = append(errs, err) + continue + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + adapterRequests = append(adapterRequests, &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: reqJSON, + Headers: headers, + }) + } + return adapterRequests, errs +} + +// MakeBids unpacks the server's response into Bids. +func (a *SmartAdserverAdapter) 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: "Unexpected status code: " + strconv.Itoa(response.StatusCode) + ". Run with request.debug = 1 for more info", + }} + } + + var bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + + for _, sb := range bidResp.SeatBid { + for i := 0; i < len(sb.Bid); i++ { + bid := sb.Bid[i] + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: getMediaTypeForImp(bid.ImpID, internalRequest.Imp), + }) + + } + } + return bidResponse, []error{} +} + +// BuildEndpointURL : Builds endpoint url +func (a *SmartAdserverAdapter) BuildEndpointURL(params *openrtb_ext.ExtImpSmartadserver) (string, error) { + uri, err := url.Parse(a.host) + if err != nil || uri.Scheme == "" || uri.Host == "" { + return "", &errortypes.BadInput{ + Message: "Malformed URL: " + a.host + ".", + } + } + + uri.Path = path.Join(uri.Path, "api/bid") + uri.RawQuery = "callerId=5" + + return uri.String(), nil +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) openrtb_ext.BidType { + for _, imp := range imps { + if imp.ID == impID { + if imp.Video != nil { + return openrtb_ext.BidTypeVideo + } + return openrtb_ext.BidTypeBanner + } + } + return openrtb_ext.BidTypeBanner +} diff --git a/adapters/smartadserver/smartadserver_test.go b/adapters/smartadserver/smartadserver_test.go new file mode 100644 index 00000000000..7e4cff678cc --- /dev/null +++ b/adapters/smartadserver/smartadserver_test.go @@ -0,0 +1,11 @@ +package smartadserver + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "smartadservertest", NewSmartadserverBidder("https://ssb.smartadserver.com")) +} diff --git a/adapters/smartadserver/smartadservertest/exemplary/multi-banner.json b/adapters/smartadserver/smartadservertest/exemplary/multi-banner.json new file mode 100644 index 00000000000..b7cf27c37e2 --- /dev/null +++ b/adapters/smartadserver/smartadservertest/exemplary/multi-banner.json @@ -0,0 +1,175 @@ +{ + "mockBidRequest": { + "id": "test-request-multi-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + }, + { + "id": "test-imp-id-2", + "banner": { + "format": [{"w": 300, "h": 150}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 4, + "networkId": 73 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-multi-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-multi-id", + "seatbid": [ + { + "seat": "smartadserver", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-1", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 90, + "w": 728 + }] + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-multi-id", + "imp": [ + { + "id": "test-imp-id-2", + "banner": { + "format": [{"w": 300, "h": 150}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 4, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-multi-id", + "seatbid": [ + { + "seat": "smartadserver", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e801", + "impid": "test-imp-id-2", + "price": 0.800000, + "adm": "some-test-ad", + "crid": "crid_11", + "h": 150, + "w": 300 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e801", + "impid": "test-imp-id-2", + "price": 0.8, + "adm": "some-test-ad", + "crid": "crid_11", + "w": 300, + "h": 150 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/exemplary/simple-banner.json b/adapters/smartadserver/smartadservertest/exemplary/simple-banner.json new file mode 100644 index 00000000000..e8faab141cd --- /dev/null +++ b/adapters/smartadserver/smartadservertest/exemplary/simple-banner.json @@ -0,0 +1,94 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "smartadserver", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 90, + "w": 728 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/exemplary/simple-video.json b/adapters/smartadserver/smartadservertest/exemplary/simple-video.json new file mode 100644 index 00000000000..86f9361a807 --- /dev/null +++ b/adapters/smartadserver/smartadservertest/exemplary/simple-video.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "test-request-id-video", + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "protocols": [1], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-id-video", + "imp": [ + { + "id": "test-imp-id-video", + "video": { + "mimes": ["video/mp4"], + "protocols": [1], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id-video", + "seatbid": [ + { + "seat": "smartadserver", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-video", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 576, + "w": 1024 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id-video", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 1024, + "h": 576 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/params/race/banner.json b/adapters/smartadserver/smartadservertest/params/race/banner.json new file mode 100644 index 00000000000..b34088307d4 --- /dev/null +++ b/adapters/smartadserver/smartadservertest/params/race/banner.json @@ -0,0 +1,7 @@ +{ + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + \ No newline at end of file diff --git a/adapters/smartadserver/smartadservertest/params/race/video.json b/adapters/smartadserver/smartadservertest/params/race/video.json new file mode 100644 index 00000000000..b34088307d4 --- /dev/null +++ b/adapters/smartadserver/smartadservertest/params/race/video.json @@ -0,0 +1,7 @@ +{ + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + \ No newline at end of file diff --git a/adapters/smartadserver/smartadservertest/supplemental/request-no-bidder-object.json b/adapters/smartadserver/smartadservertest/supplemental/request-no-bidder-object.json new file mode 100644 index 00000000000..48664a66073 --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/request-no-bidder-object.json @@ -0,0 +1,21 @@ +{ + "mockBidRequest": { + "id": "test-no-bidder", + "imp": [ + { + "id": "test-imp-id-no-bidder", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing smartadserverExt parameters", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/supplemental/request-no-ext-object.json b/adapters/smartadserver/smartadservertest/supplemental/request-no-ext-object.json new file mode 100644 index 00000000000..d6637d0ebc3 --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/request-no-ext-object.json @@ -0,0 +1,19 @@ +{ + "mockBidRequest": { + "id": "test-no-ext", + "imp": [ + { + "id": "test-imp-id-no-ext", + "banner": { + "format": [{"w": 728, "h": 90}] + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing bidderExt object", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/supplemental/request-no-imp.json b/adapters/smartadserver/smartadservertest/supplemental/request-no-imp.json new file mode 100644 index 00000000000..50e00a8a969 --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/request-no-imp.json @@ -0,0 +1,13 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the bid request", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/supplemental/request-site-recreated.json b/adapters/smartadserver/smartadservertest/supplemental/request-site-recreated.json new file mode 100644 index 00000000000..4a402674abf --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/request-site-recreated.json @@ -0,0 +1,99 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "1" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "smartadserver", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 90, + "w": 728 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/supplemental/response-200-without-body.json b/adapters/smartadserver/smartadservertest/supplemental/response-200-without-body.json new file mode 100644 index 00000000000..3e27569491c --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/response-200-without-body.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 200 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/supplemental/response-204.json b/adapters/smartadserver/smartadservertest/supplemental/response-204.json new file mode 100644 index 00000000000..32aa2642f0a --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/response-204.json @@ -0,0 +1,56 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 204 + } + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/supplemental/response-400.json b/adapters/smartadserver/smartadservertest/supplemental/response-400.json new file mode 100644 index 00000000000..b7d5a95475d --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/response-400.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartadserver/smartadservertest/supplemental/response-500.json b/adapters/smartadserver/smartadservertest/supplemental/response-500.json new file mode 100644 index 00000000000..727e8f0843b --- /dev/null +++ b/adapters/smartadserver/smartadservertest/supplemental/response-500.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssb.smartadserver.com/api/bid?callerId=5", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": 1, + "pageId": 2, + "formatId": 3, + "networkId": 73 + } + } + } + ], + "site": { + "publisher": { + "id": "73" + } + } + } + }, + "mockResponse": { + "status": 500 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/smartadserver/usersync.go b/adapters/smartadserver/usersync.go new file mode 100644 index 00000000000..95b305ff227 --- /dev/null +++ b/adapters/smartadserver/usersync.go @@ -0,0 +1,12 @@ +package smartadserver + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewSmartadserverSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("smartadserver", 45, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/smartadserver/usersync_test.go b/adapters/smartadserver/usersync_test.go new file mode 100644 index 00000000000..e279b49e017 --- /dev/null +++ b/adapters/smartadserver/usersync_test.go @@ -0,0 +1,35 @@ +package smartadserver + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestSmartadserverSyncer(t *testing.T) { + syncURL := "//ssbsync.smartadserver.com/getuid?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url=localhost%2Fsetuid%3Fbidder%3Dsmartadserver%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bsas_uid%5D%22" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewSmartadserverSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "COyASAoOyASAoAfAAAENAfCAAAAAAAAAAAAAAAAAAAAA", + }, + CCPA: ccpa.Policy{ + Value: "1YNN", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "//ssbsync.smartadserver.com/getuid?gdpr=1&gdpr_consent=COyASAoOyASAoAfAAAENAfCAAAAAAAAAAAAAAAAAAAAA&us_privacy=1YNN&url=localhost%2Fsetuid%3Fbidder%3Dsmartadserver%26gdpr%3D1%26gdpr_consent%3DCOyASAoOyASAoAfAAAENAfCAAAAAAAAAAAAAAAAAAAAA%26uid%3D%5Bsas_uid%5D%22", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 45, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 01de9b1ab2e..c50118e2008 100755 --- a/config/config.go +++ b/config/config.go @@ -600,6 +600,7 @@ func (cfg *Configuration) setDerivedDefaults() { // 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsharethrough%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartadserver, "https://ssbsync.smartadserver.com/api/sync?callerId=5&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsmartadserver%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bssb_sync_pid%5D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartRTB, "https://market-global.smrtb.com/sync/all?nid=smartrtb&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&rr="+url.QueryEscape(externalURL)+"%252Fsetuid%253Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsonobi%26consent_string%3D{{.GDPR}}%26gdpr%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D") @@ -810,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.smartadserver.endpoint", "https://ssb.smartadserver.com") 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") diff --git a/docs/bidders/smartAdserver.md b/docs/bidders/smartAdserver.md new file mode 100644 index 00000000000..4d2663f8a3b --- /dev/null +++ b/docs/bidders/smartAdserver.md @@ -0,0 +1,59 @@ +# Smart Adserver Bidder + +## Parameters +The `ext.smartadserver` object of impression bid requests supports the following parameters : +- "networkId" - Required. The network identifier you have been provided with. +- "siteId" - Optional. The site identifier from your campaign configuration. +- "pageId" - Optional. The page identifier from your campaign configuration. +- "formatId" - Optional. The format identifier from your campaign configuration. + +The network identifier is provided by your Account Manager. +**Note:** The site, page and format identifiers have to all be provided or all empty. + +## Examples + +Without site/page/format : +``` + "imp": [{ + "id": "some-impression-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "smartadserver": { + "networkId": 73 + } + } + }] +``` + +With site/page/format : + +``` + "imp": [{ + "id": "some-impression-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "smartadserver": { + "networkId": 73 + "siteId": 1, + "pageId": 2, + "formatId": 3 + } + } + }] +``` \ No newline at end of file diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 6e771236fb7..44054df06fd 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -62,6 +62,7 @@ import ( "github.com/prebid/prebid-server/adapters/rtbhouse" "github.com/prebid/prebid-server/adapters/rubicon" "github.com/prebid/prebid-server/adapters/sharethrough" + "github.com/prebid/prebid-server/adapters/smartadserver" "github.com/prebid/prebid-server/adapters/smartrtb" "github.com/prebid/prebid-server/adapters/somoaudience" "github.com/prebid/prebid-server/adapters/sonobi" @@ -150,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.BidderSmartadserver: smartadserver.NewSmartadserverBidder(cfg.Adapters[string(openrtb_ext.BidderSmartadserver)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 416f36d135f..1f9cffb9938 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -78,6 +78,7 @@ const ( BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSharethrough BidderName = "sharethrough" + BidderSmartadserver BidderName = "smartadserver" BidderSmartRTB BidderName = "smartrtb" BidderSomoaudience BidderName = "somoaudience" BidderSonobi BidderName = "sonobi" @@ -157,6 +158,7 @@ var BidderMap = map[string]BidderName{ "rtbhouse": BidderRTBHouse, "rubicon": BidderRubicon, "sharethrough": BidderSharethrough, + "smartadserver": BidderSmartadserver, "smartrtb": BidderSmartRTB, "somoaudience": BidderSomoaudience, "sonobi": BidderSonobi, diff --git a/openrtb_ext/imp_smartadserver.go b/openrtb_ext/imp_smartadserver.go new file mode 100644 index 00000000000..d542e0ffd27 --- /dev/null +++ b/openrtb_ext/imp_smartadserver.go @@ -0,0 +1,9 @@ +package openrtb_ext + +// ExtImpSmartadserver defines the contract for bidrequest.imp[i].ext.smartadserver +type ExtImpSmartadserver struct { + SiteID int `json:"siteId"` + PageID int `json:"pageId"` + FormatID int `json:"formatId"` + NetworkID int `json:"networkId"` +} diff --git a/static/bidder-info/smartadserver.yaml b/static/bidder-info/smartadserver.yaml new file mode 100644 index 00000000000..626b7dac00d --- /dev/null +++ b/static/bidder-info/smartadserver.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "support@smartadserver.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-params/smartadserver.json b/static/bidder-params/smartadserver.json new file mode 100644 index 00000000000..b76a3bd6ac9 --- /dev/null +++ b/static/bidder-params/smartadserver.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smartadserver Adapter Params", + "description": "A schema which validates params accepted by the Smartadserver adapter", + + "type": "object", + "properties": { + "siteId": { + "type": "integer", + "description": "The site id.", + "minimum": 1 + }, + "pageId": { + "type": "integer", + "description": "The page id.", + "minimum": 1 + }, + "formatId": { + "type": "integer", + "description": "The format id.", + "minimum": 1 + }, + "networkId": { + "type": "integer", + "description": "The network id.", + "minimum": 1 + } + }, + "dependencies": { + "siteId": { "required": ["pageId", "formatId"] }, + "pageId": { "required": ["siteId", "formatId"] }, + "formatId": { "required": ["siteId", "pageId"] } + }, + "required": ["networkId"] +} \ No newline at end of file diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 1beb9d586df..5657c8b7010 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -50,6 +50,7 @@ import ( "github.com/prebid/prebid-server/adapters/rtbhouse" "github.com/prebid/prebid-server/adapters/rubicon" "github.com/prebid/prebid-server/adapters/sharethrough" + "github.com/prebid/prebid-server/adapters/smartadserver" "github.com/prebid/prebid-server/adapters/smartrtb" "github.com/prebid/prebid-server/adapters/somoaudience" "github.com/prebid/prebid-server/adapters/sonobi" @@ -127,6 +128,7 @@ 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.BidderSmartadserver, smartadserver.NewSmartadserverSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderSmartRTB, smartrtb.NewSmartRTBSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderSynacormedia, synacormedia.NewSynacorMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderTelaria, telaria.NewTelariaSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 69751dd55f4..363cd491648 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -62,6 +62,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderSomoaudience): syncConfig, string(openrtb_ext.BidderSonobi): syncConfig, string(openrtb_ext.BidderSovrn): syncConfig, + string(openrtb_ext.BidderSmartadserver): syncConfig, string(openrtb_ext.BidderSmartRTB): syncConfig, string(openrtb_ext.BidderSynacormedia): syncConfig, string(openrtb_ext.BidderTelaria): syncConfig, From 379492dced7d01da04c4762cb2d38ad9d02a37e4 Mon Sep 17 00:00:00 2001 From: Telaria Engineering <36203956+telariaEng@users.noreply.github.com> Date: Sat, 20 Jun 2020 13:26:09 -0700 Subject: [PATCH 123/381] Added additional Ext Param (#1357) Co-authored-by: Vinay Prasad --- adapters/telaria/telaria.go | 20 +- .../telariatest/exemplary/video-app.json | 226 ++++++++++-------- .../telariatest/exemplary/video-web.json | 224 +++++++++-------- openrtb_ext/imp_telaria.go | 7 +- 4 files changed, 277 insertions(+), 200 deletions(-) diff --git a/adapters/telaria/telaria.go b/adapters/telaria/telaria.go index 294d0d100a9..6bed043152e 100644 --- a/adapters/telaria/telaria.go +++ b/adapters/telaria/telaria.go @@ -23,6 +23,10 @@ type ImpressionExtOut struct { OriginalPublisherID string `json:"originalPublisherid"` } +type telariaBidExt struct { + Extra json.RawMessage `json:"extra,omitempty"` +} + // used for cookies and such func (a *TelariaAdapter) Name() string { return "telaria" @@ -186,15 +190,17 @@ func (a *TelariaAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo *ad originalPublisherID := a.FetchOriginalPublisherID(&request) var errors []error + var telariaImpExt *openrtb_ext.ExtImpTelaria + var err error for i, imp := range request.Imp { // fetch adCode & seatCode from Imp[i].Ext - telariaExt, err := a.FetchTelariaExtImpParams(&imp) + telariaImpExt, err = a.FetchTelariaExtImpParams(&imp) if err != nil { errors = append(errors, err) break } - seatCode = telariaExt.SeatCode + seatCode = telariaImpExt.SeatCode // move the original tagId and the original publisher.id into the Imp[i].Ext object request.Imp[i].Ext, err = json.Marshal(&ImpressionExtOut{request.Imp[i].TagID, originalPublisherID}) @@ -204,7 +210,15 @@ func (a *TelariaAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo *ad } // Swap the tagID with adCode - request.Imp[i].TagID = telariaExt.AdCode + request.Imp[i].TagID = telariaImpExt.AdCode + } + + // Add the Extra from Imp to the top level Ext + if telariaImpExt != nil && telariaImpExt.Extra != nil { + request.Ext, err = json.Marshal(&telariaBidExt{Extra: telariaImpExt.Extra}) + if err != nil { + errors = append(errors, err) + } } if len(errors) > 0 { diff --git a/adapters/telaria/telariatest/exemplary/video-app.json b/adapters/telaria/telariatest/exemplary/video-app.json index fa755cc93d3..6450509c8e1 100644 --- a/adapters/telaria/telariatest/exemplary/video-app.json +++ b/adapters/telaria/telariatest/exemplary/video-app.json @@ -39,118 +39,148 @@ "ext": { "bidder": { "adCode": "my-adcode", - "seatCode": "my-seatcode" + "seatCode": "my-seatcode", + "extra": { + "custom": "1234" + } } } } ] }, - "httpCalls": [{ - "expectedRequest": { - "headers": { - "Content-Type": ["application/json;charset=utf-8"], - "Accept": ["application/json"], - "X-Openrtb-Version": ["2.5"], - "User-Agent": ["test-user-agent"], - "X-Forwarded-For": ["123.123.123.123"], - "Accept-Language": ["en"], - "Dnt": ["0"] - }, - "uri": "https://ads.tremorhub.com/ad/rtb/prebid", - "body": { - "id": "some-request-id", - "device": { - "ua": "test-user-agent", - "ip": "123.123.123.123", - "language": "en", - "dnt": 0 + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Accept-Language": [ + "en" + ], + "Dnt": [ + "0" + ] }, - "imp": [ - { - "id": "some-impression-id", - "video": { - "mimes": [ - "video/mp4" - ], - "minduration": 120, - "maxduration": 150, - "w": 640, - "h": 480 - }, - "tagid": "my-adcode", - "ext": { - "originalTagid": "ogTAGID", - "originalPublisherid": "123456789" + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "ext": { + "extra": { + "custom": "1234" + } + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "my-adcode", + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" + } } - } - ], - "app": { - "id": "123456789", - "name": "Awesome App", - "bundle": "com.app.awesome", - "domain": "awesomeapp.com", - "cat": [ - "IAB22-1" ], - "publisher": { - "id": "my-seatcode" - } - }, - "user": { - "buyeruid": "awesome-user" - }, - "tmax": 1000 - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "awesome-resp-id", - "seatbid": [{ - "bid": [{ - "id": "a3ae1b4e2fc24a4fb45540082e98e161", - "impid": "1", - "price": 3.5, - "adm": "asesome-markup", - "adomain": [ - "awesome.com" + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" ], - "crid": "20", - "w": 1280, - "h": 720, - "ext": { - "prebid": { - "type": "video" - } + "publisher": { + "id": "my-seatcode" } - }], - "seat": "telaria" - }], - "cur": "USD", - "ext": { - "responsetimemillis": { - "telaria": 154 }, - "tmaxrequest": 1000 + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "telaria" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "telaria": 154 + }, + "tmaxrequest": 1000 + } } } } - }], - "expectedBids": [{ - "id": "a3ae1b4e2fc24a4fb45540082e98e161", - "impid": "1", - "price": 3.5, - "adm": "awesome-markup", - "adomain": [ - "awesome.com" - ], - "crid": "20", - "w": 1280, - "h": 720, - "ext": { - "prebid": { - "type": "video" + ], + "expectedBids": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } } } - }] + ] } diff --git a/adapters/telaria/telariatest/exemplary/video-web.json b/adapters/telaria/telariatest/exemplary/video-web.json index c671d2f5f30..f3a3a56928c 100644 --- a/adapters/telaria/telariatest/exemplary/video-web.json +++ b/adapters/telaria/telariatest/exemplary/video-web.json @@ -1,4 +1,3 @@ - { "mockBidRequest": { "id": "some-request-id", @@ -23,7 +22,9 @@ "id": "some-impression-id", "tagid": "ogTAGID", "video": { - "mimes": ["video/mp4"], + "mimes": [ + "video/mp4" + ], "w": 640, "h": 480, "minduration": 120, @@ -32,113 +33,142 @@ "ext": { "bidder": { "adCode": "my-adcode", - "seatCode": "my-seatcode" + "seatCode": "my-seatcode", + "extra": { + "custom": "1234" + } } } } ] }, - - "httpCalls": [{ - "expectedRequest": { - "headers": { - "Content-Type": ["application/json;charset=utf-8"], - "Accept": ["application/json"], - "X-Openrtb-Version": ["2.5"], - "User-Agent": ["test-user-agent"], - "X-Forwarded-For": ["123.123.123.123"], - "Accept-Language": ["en"], - "Dnt": ["0"] - }, - "uri": "https://ads.tremorhub.com/ad/rtb/prebid", - "body": { - "id": "some-request-id", - "device": { - "ua": "test-user-agent", - "ip": "123.123.123.123", - "language": "en", - "dnt": 0 - }, - "imp": [ - { - "id": "some-impression-id", - "tagid": "my-adcode", - "video": { - "mimes": [ - "video/mp4" - ], - "minduration": 120, - "maxduration": 150, - "w": 640, - "h": 480 - }, - "ext": { - "originalTagid": "ogTAGID", - "originalPublisherid": "123456789" - } - } - ], - "site": { - "page": "test.com", - "publisher": { - "id": "my-seatcode" - } - }, - "user": { - "buyeruid": "awesome-user" + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Accept-Language": [ + "en" + ], + "Dnt": [ + "0" + ] }, - "tmax": 1000 - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "awesome-resp-id", - "seatbid": [{ - "bid": [{ - "id": "a3ae1b4e2fc24a4fb45540082e98e161", - "impid": "1", - "price": 3.5, - "adm": "asesome-markup", - "adomain": [ - "awesome.com" - ], - "crid": "20", - "w": 1280, - "h": 720, - "ext": { - "prebid": { - "type": "video" + "uri": "https://ads.tremorhub.com/ad/rtb/prebid", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "originalTagid": "ogTAGID", + "originalPublisherid": "123456789" } } - }], - "seat": "telaria" - }], - "cur": "USD", - "ext": { - "responsetimemillis": { - "telaria": 154 + ], + "site": { + "page": "test.com", + "publisher": { + "id": "my-seatcode" + } + }, + "user": { + "buyeruid": "awesome-user" }, - "tmaxrequest": 1000 + "tmax": 1000, + "ext": { + "extra": { + "custom": "1234" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "telaria" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "telaria": 154 + }, + "tmaxrequest": 1000 + } } } } - }], - "expectedBids": [{ - "id": "a3ae1b4e2fc24a4fb45540082e98e161", - "impid": "1", - "price": 3.5, - "adm": "asesome-markup", - "adomain": [ - "awesome.com" - ], - "crid": "20", - "w": 1280, - "h": 720, - "ext": { - "prebid": { - "type": "video" + ], + "expectedBids": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "ext": { + "prebid": { + "type": "video" + } } } - }] + ] } diff --git a/openrtb_ext/imp_telaria.go b/openrtb_ext/imp_telaria.go index 8ea371a8ad0..19a025c0b15 100644 --- a/openrtb_ext/imp_telaria.go +++ b/openrtb_ext/imp_telaria.go @@ -1,6 +1,9 @@ package openrtb_ext +import "encoding/json" + type ExtImpTelaria struct { - AdCode string `json:"adCode,omitempty"` - SeatCode string `json:"seatCode"` + AdCode string `json:"adCode,omitempty"` + SeatCode string `json:"seatCode"` + Extra json.RawMessage `json:"extra,omitempty"` } From aaff1568c385f2e9b1c7bda32f4c037d81db5199 Mon Sep 17 00:00:00 2001 From: SmartyAdman <59048845+SmartyAdman@users.noreply.github.com> Date: Wed, 24 Jun 2020 00:57:38 +0300 Subject: [PATCH 124/381] Adman adapter (#1356) Co-authored-by: Aiholkin --- adapters/adman/adman.go | 140 ++++++++++++++++++ adapters/adman/adman_test.go | 12 ++ .../admantest/exemplary/simple-banner.json | 134 +++++++++++++++++ .../admantest/exemplary/simple-video.json | 119 +++++++++++++++ .../exemplary/simple-web-banner.json | 133 +++++++++++++++++ adapters/adman/admantest/params/banner.json | 3 + .../adman/admantest/params/race/banner.json | 3 + .../adman/admantest/params/race/video.json | 3 + adapters/adman/admantest/params/video.json | 3 + .../admantest/supplemental/bad-imp-ext.json | 42 ++++++ .../admantest/supplemental/bad_response.json | 85 +++++++++++ .../admantest/supplemental/no-imp-ext-1.json | 39 +++++ .../admantest/supplemental/no-imp-ext-2.json | 39 +++++ .../admantest/supplemental/status-204.json | 79 ++++++++++ .../admantest/supplemental/status-404.json | 85 +++++++++++ adapters/adman/params_test.go | 46 ++++++ adapters/adman/usersync.go | 13 ++ adapters/adman/usersync_test.go | 35 +++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adman.go | 6 + static/bidder-info/adman.yaml | 11 ++ static/bidder-params/adman.json | 15 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 26 files changed, 1054 insertions(+) create mode 100644 adapters/adman/adman.go create mode 100644 adapters/adman/adman_test.go create mode 100644 adapters/adman/admantest/exemplary/simple-banner.json create mode 100644 adapters/adman/admantest/exemplary/simple-video.json create mode 100644 adapters/adman/admantest/exemplary/simple-web-banner.json create mode 100644 adapters/adman/admantest/params/banner.json create mode 100644 adapters/adman/admantest/params/race/banner.json create mode 100644 adapters/adman/admantest/params/race/video.json create mode 100644 adapters/adman/admantest/params/video.json create mode 100644 adapters/adman/admantest/supplemental/bad-imp-ext.json create mode 100644 adapters/adman/admantest/supplemental/bad_response.json create mode 100644 adapters/adman/admantest/supplemental/no-imp-ext-1.json create mode 100644 adapters/adman/admantest/supplemental/no-imp-ext-2.json create mode 100644 adapters/adman/admantest/supplemental/status-204.json create mode 100644 adapters/adman/admantest/supplemental/status-404.json create mode 100644 adapters/adman/params_test.go create mode 100644 adapters/adman/usersync.go create mode 100644 adapters/adman/usersync_test.go create mode 100644 openrtb_ext/imp_adman.go create mode 100644 static/bidder-info/adman.yaml create mode 100644 static/bidder-params/adman.json diff --git a/adapters/adman/adman.go b/adapters/adman/adman.go new file mode 100644 index 00000000000..aa8d0dc6e74 --- /dev/null +++ b/adapters/adman/adman.go @@ -0,0 +1,140 @@ +package adman + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// AdmanAdapter struct +type AdmanAdapter struct { + URI string +} + +// NewAdmanBidder Initializes the Bidder +func NewAdmanBidder(endpoint string) *AdmanAdapter { + return &AdmanAdapter{ + URI: endpoint, + } +} + +type admanParams struct { + TagID string `json:"TagID"` +} + +// MakeRequests create bid request for adman demand +func (a *AdmanAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var admanExt openrtb_ext.ExtImpAdman + var err error + + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb.Imp{imp} + + var bidderExt adapters.ExtImpBidder + if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil { + errs = append(errs, err) + continue + } + + if err = json.Unmarshal(bidderExt.Bidder, &admanExt); err != nil { + errs = append(errs, err) + continue + } + + reqCopy.Imp[0].TagID = admanExt.TagID + + adapterReq, errors := a.makeRequest(&reqCopy) + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + errs = append(errs, errors...) + } + return adapterRequests, errs +} + +func (a *AdmanAdapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, []error) { + + var errs []error + + reqJSON, err := json.Marshal(request) + + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.URI, + Body: reqJSON, + Headers: headers, + }, errs +} + +// MakeBids makes the bids +func (a *AdmanAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode == http.StatusNotFound { + 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 { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, errs +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) (openrtb_ext.BidType, error) { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impID { + if imp.Banner == nil && imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } + return mediaType, nil + } + } + + // This shouldnt happen. Lets handle it just incase by returning an error. + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression \"%s\" ", impID), + } +} diff --git a/adapters/adman/adman_test.go b/adapters/adman/adman_test.go new file mode 100644 index 00000000000..da0f37e9a48 --- /dev/null +++ b/adapters/adman/adman_test.go @@ -0,0 +1,12 @@ +package adman + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + admanAdapter := NewAdmanBidder("http://eu-ams-1.admanmedia.com/?c=o&m=ortb") + adapterstest.RunJSONBidderTest(t, "admantest", admanAdapter) +} diff --git a/adapters/adman/admantest/exemplary/simple-banner.json b/adapters/adman/admantest/exemplary/simple-banner.json new file mode 100644 index 00000000000..41f76e00645 --- /dev/null +++ b/adapters/adman/admantest/exemplary/simple-banner.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "16", + "ext": { + "bidder": { + "TagID": "16" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } +}, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "16", + "ext": { + "bidder": { + "TagID": "16" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adman" + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/adman/admantest/exemplary/simple-video.json b/adapters/adman/admantest/exemplary/simple-video.json new file mode 100644 index 00000000000..d7fa82d274d --- /dev/null +++ b/adapters/adman/admantest/exemplary/simple-video.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "TagID": "22" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "tagid": "22", + "ext": { + "bidder": { + "TagID": "22" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "adman" + } + ], + "cur": "USD" + } + } + } + ], + + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adman/admantest/exemplary/simple-web-banner.json b/adapters/adman/admantest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..ce872bff52b --- /dev/null +++ b/adapters/adman/admantest/exemplary/simple-web-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adman" + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/adman/admantest/params/banner.json b/adapters/adman/admantest/params/banner.json new file mode 100644 index 00000000000..03fa8f3f2d8 --- /dev/null +++ b/adapters/adman/admantest/params/banner.json @@ -0,0 +1,3 @@ +{ + "TagID": "16" +} \ No newline at end of file diff --git a/adapters/adman/admantest/params/race/banner.json b/adapters/adman/admantest/params/race/banner.json new file mode 100644 index 00000000000..03fa8f3f2d8 --- /dev/null +++ b/adapters/adman/admantest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "TagID": "16" +} \ No newline at end of file diff --git a/adapters/adman/admantest/params/race/video.json b/adapters/adman/admantest/params/race/video.json new file mode 100644 index 00000000000..e776c928a7e --- /dev/null +++ b/adapters/adman/admantest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "TagID": "22" +} \ No newline at end of file diff --git a/adapters/adman/admantest/params/video.json b/adapters/adman/admantest/params/video.json new file mode 100644 index 00000000000..e776c928a7e --- /dev/null +++ b/adapters/adman/admantest/params/video.json @@ -0,0 +1,3 @@ +{ + "TagID": "22" +} \ No newline at end of file diff --git a/adapters/adman/admantest/supplemental/bad-imp-ext.json b/adapters/adman/admantest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..db3c8de5767 --- /dev/null +++ b/adapters/adman/admantest/supplemental/bad-imp-ext.json @@ -0,0 +1,42 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "16", + "ext": { + "adman": { + "TagID": "16" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } +}, +"expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } +] +} diff --git a/adapters/adman/admantest/supplemental/bad_response.json b/adapters/adman/admantest/supplemental/bad_response.json new file mode 100644 index 00000000000..8c349297e73 --- /dev/null +++ b/adapters/adman/admantest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/adman/admantest/supplemental/no-imp-ext-1.json b/adapters/adman/admantest/supplemental/no-imp-ext-1.json new file mode 100644 index 00000000000..8fad5ba5ef0 --- /dev/null +++ b/adapters/adman/admantest/supplemental/no-imp-ext-1.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "16", + "ext": "" + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/adman/admantest/supplemental/no-imp-ext-2.json b/adapters/adman/admantest/supplemental/no-imp-ext-2.json new file mode 100644 index 00000000000..337dfd044b3 --- /dev/null +++ b/adapters/adman/admantest/supplemental/no-imp-ext-2.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "16", + "ext": {} + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/adman/admantest/supplemental/status-204.json b/adapters/adman/admantest/supplemental/status-204.json new file mode 100644 index 00000000000..7f9a12dec29 --- /dev/null +++ b/adapters/adman/admantest/supplemental/status-204.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }] +} diff --git a/adapters/adman/admantest/supplemental/status-404.json b/adapters/adman/admantest/supplemental/status-404.json new file mode 100644 index 00000000000..560878342f0 --- /dev/null +++ b/adapters/adman/admantest/supplemental/status-404.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/adman/params_test.go b/adapters/adman/params_test.go new file mode 100644 index 00000000000..a80c2a44b8b --- /dev/null +++ b/adapters/adman/params_test.go @@ -0,0 +1,46 @@ +package adman + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// TestValidParams makes sure that the adman 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.BidderAdman, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected adman params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the adman 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.BidderAdman, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"TagID": "16"}`, +} + +var invalidParams = []string{ + `{"id": "123"}`, + `{"tagid": "123"}`, + `{"TagID": 16}`, +} diff --git a/adapters/adman/usersync.go b/adapters/adman/usersync.go new file mode 100644 index 00000000000..aae6afcdfcd --- /dev/null +++ b/adapters/adman/usersync.go @@ -0,0 +1,13 @@ +package adman + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +// NewAdmanSyncer returns adman syncer +func NewAdmanSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("adman", 149, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/adman/usersync_test.go b/adapters/adman/usersync_test.go new file mode 100644 index 00000000000..55a6e2cec97 --- /dev/null +++ b/adapters/adman/usersync_test.go @@ -0,0 +1,35 @@ +package adman + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestAdmanSyncer(t *testing.T) { + syncURL := "https://sync.admanmedia.com/pbs.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dadman%26uid%3D%5BUID%5D" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewAdmanSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + Consent: "ANDFJDS", + }, + CCPA: ccpa.Policy{ + Value: "1-YY", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://sync.admanmedia.com/pbs.gif?gdpr=0&gdpr_consent=ANDFJDS&us_privacy=1-YY&redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dadman%26uid%3D%5BUID%5D", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 149, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index c50118e2008..bb2c3191491 100755 --- a/config/config.go +++ b/config/config.go @@ -563,6 +563,7 @@ func (cfg *Configuration) setDerivedDefaults() { 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdman, "https://sync.admanmedia.com/pbs.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadman%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D") // 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Daja%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25s") @@ -763,6 +764,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.adman.endpoint", "http://eu-ams-1.admanmedia.com/?c=o&m=ortb") 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 44054df06fd..c30bb0c622e 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -14,6 +14,7 @@ import ( "github.com/prebid/prebid-server/adapters/adhese" "github.com/prebid/prebid-server/adapters/adkernel" "github.com/prebid/prebid-server/adapters/adkernelAdn" + "github.com/prebid/prebid-server/adapters/adman" "github.com/prebid/prebid-server/adapters/admixer" "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adoppler" @@ -98,6 +99,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderAdman: adman.NewAdmanBidder(cfg.Adapters[string(openrtb_ext.BidderAdman)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 1f9cffb9938..49d7b09d671 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -31,6 +31,7 @@ const ( BidderAdkernel BidderName = "adkernel" BidderAdkernelAdn BidderName = "adkernelAdn" BidderAdpone BidderName = "adpone" + BidderAdman BidderName = "adman" BidderAdmixer BidderName = "admixer" BidderAdOcean BidderName = "adocean" BidderAdtarget BidderName = "adtarget" @@ -110,6 +111,7 @@ var BidderMap = map[string]BidderName{ "adhese": BidderAdhese, "adkernel": BidderAdkernel, "adkernelAdn": BidderAdkernelAdn, + "adman": BidderAdman, "admixer": BidderAdmixer, "adocean": BidderAdOcean, "adpone": BidderAdpone, diff --git a/openrtb_ext/imp_adman.go b/openrtb_ext/imp_adman.go new file mode 100644 index 00000000000..bc79415452c --- /dev/null +++ b/openrtb_ext/imp_adman.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpAdman defines adman specifiec param +type ExtImpAdman struct { + TagID string `json:"TagID"` +} diff --git a/static/bidder-info/adman.yaml b/static/bidder-info/adman.yaml new file mode 100644 index 00000000000..932ef2e4242 --- /dev/null +++ b/static/bidder-info/adman.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "prebid@admanmedia.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-params/adman.json b/static/bidder-params/adman.json new file mode 100644 index 00000000000..90021e2cdfd --- /dev/null +++ b/static/bidder-params/adman.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adman Adapter Params", + "description": "A schema which validates params accepted by the Adman adapter", + + "type": "object", + "properties": { + "TagID": { + "type": "string", + "description": "An ID which identifies the adman ad tag" + } + }, + "required" : [ "TagID" ] + } + \ No newline at end of file diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 5657c8b7010..f1f643afb74 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -9,6 +9,7 @@ import ( "github.com/prebid/prebid-server/adapters/adform" "github.com/prebid/prebid-server/adapters/adkernel" "github.com/prebid/prebid-server/adapters/adkernelAdn" + "github.com/prebid/prebid-server/adapters/adman" "github.com/prebid/prebid-server/adapters/admixer" "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adpone" @@ -84,6 +85,7 @@ 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.BidderAdman, adman.NewAdmanSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdmixer, admixer.NewAdmixerSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdOcean, adocean.NewAdOceanSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdpone, adpone.NewadponeSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 363cd491648..b23541eaf8a 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -18,6 +18,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderAdform): syncConfig, string(openrtb_ext.BidderAdkernel): syncConfig, string(openrtb_ext.BidderAdkernelAdn): syncConfig, + string(openrtb_ext.BidderAdman): syncConfig, string(openrtb_ext.BidderAdmixer): syncConfig, string(openrtb_ext.BidderAdOcean): syncConfig, string(openrtb_ext.BidderAdpone): syncConfig, From e376a8bbfcf513d65821f9c97547913d9a9c0d93 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 24 Jun 2020 14:08:14 -0400 Subject: [PATCH 125/381] =?UTF-8?q?PBS-632=20add=20max=20connections=20per?= =?UTF-8?q?=20host=20config=20setting=20to=20general=20http=20a=E2=80=A6?= =?UTF-8?q?=20(#1366)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.go | 3 +++ config/config_test.go | 4 ++++ router/router.go | 2 ++ 3 files changed, 9 insertions(+) diff --git a/config/config.go b/config/config.go index bb2c3191491..7d34954583f 100755 --- a/config/config.go +++ b/config/config.go @@ -74,6 +74,7 @@ type Configuration struct { const MIN_COOKIE_SIZE_BYTES = 500 type HTTPClient struct { + MaxConnsPerHost int `mapstructure:"max_connections_per_host"` MaxIdleConns int `mapstructure:"max_idle_connections"` MaxIdleConnsPerHost int `mapstructure:"max_idle_connections_per_host"` IdleConnTimeout int `mapstructure:"idle_connection_timeout_seconds"` @@ -669,9 +670,11 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("host_cookie.value", "") v.SetDefault("host_cookie.ttl_days", 90) v.SetDefault("host_cookie.max_cookie_size_bytes", 0) + v.SetDefault("http_client.max_connections_per_host", 0) // unlimited 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_connections_per_host", 0) // unlimited 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) diff --git a/config/config_test.go b/config/config_test.go index 2b291fe978d..c7d406cc8cc 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -67,10 +67,12 @@ external_cache: host: www.externalprebidcache.net path: endpoints/cache http_client: + max_connections_per_host: 10 max_idle_connections: 500 max_idle_connections_per_host: 20 idle_connection_timeout_seconds: 30 http_client_cache: + max_connections_per_host: 5 max_idle_connections: 1 max_idle_connections_per_host: 2 idle_connection_timeout_seconds: 3 @@ -217,9 +219,11 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "cache.query", cfg.CacheURL.Query, "uuid=%PBS_CACHE_UUID%") cmpStrings(t, "external_cache.host", cfg.ExtCacheURL.Host, "www.externalprebidcache.net") cmpStrings(t, "external_cache.path", cfg.ExtCacheURL.Path, "endpoints/cache") + cmpInts(t, "http_client.max_connections_per_host", cfg.Client.MaxConnsPerHost, 10) 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_connections_per_host", cfg.CacheClient.MaxConnsPerHost, 5) 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) diff --git a/router/router.go b/router/router.go index 045c86ef25f..30936705a22 100644 --- a/router/router.go +++ b/router/router.go @@ -188,6 +188,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r generalHttpClient := &http.Client{ Transport: &http.Transport{ + MaxConnsPerHost: cfg.Client.MaxConnsPerHost, MaxIdleConns: cfg.Client.MaxIdleConns, MaxIdleConnsPerHost: cfg.Client.MaxIdleConnsPerHost, IdleConnTimeout: time.Duration(cfg.Client.IdleConnTimeout) * time.Second, @@ -197,6 +198,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r cacheHttpClient := &http.Client{ Transport: &http.Transport{ + MaxConnsPerHost: cfg.CacheClient.MaxConnsPerHost, MaxIdleConns: cfg.CacheClient.MaxIdleConns, MaxIdleConnsPerHost: cfg.CacheClient.MaxIdleConnsPerHost, IdleConnTimeout: time.Duration(cfg.CacheClient.IdleConnTimeout) * time.Second, From 16676360160b9a53286e8c8bcacf3454d923457d Mon Sep 17 00:00:00 2001 From: Marsel Date: Thu, 25 Jun 2020 17:29:25 +0300 Subject: [PATCH 126/381] Add ext.bidder.zoneid for Kubient adapater (#1367) * Add ext.bidder.zoneid for Kubient adapater * Check the number of Imps. zoneid is optional. --- adapters/kubient/kubient.go | 49 ++++++++++++++++--- .../kubient/kubienttest/exemplary/banner.json | 8 ++- .../kubient/kubienttest/exemplary/video.json | 8 ++- .../supplemental/bad_response.json | 8 ++- .../supplemental/missing-zoneid.json | 31 ++++++++++++ .../kubienttest/supplemental/no-imps.json | 12 +++++ .../kubienttest/supplemental/status_204.json | 2 + .../kubienttest/supplemental/status_400.json | 8 ++- openrtb_ext/imp_kubient.go | 6 +++ static/bidder-params/kubient.json | 8 ++- 10 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 adapters/kubient/kubienttest/supplemental/missing-zoneid.json create mode 100644 adapters/kubient/kubienttest/supplemental/no-imps.json create mode 100644 openrtb_ext/imp_kubient.go diff --git a/adapters/kubient/kubient.go b/adapters/kubient/kubient.go index cb1fe93ff82..acfaa44b6af 100644 --- a/adapters/kubient/kubient.go +++ b/adapters/kubient/kubient.go @@ -24,10 +24,24 @@ type KubientAdapter struct { func (adapter *KubientAdapter) MakeRequests( openRTBRequest *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo, -) ( - requestsToBidder []*adapters.RequestData, - errs []error, -) { +) ([]*adapters.RequestData, []error) { + if len(openRTBRequest.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: "No impression in the bid request", + }} + } + errs := make([]error, 0, len(openRTBRequest.Imp)) + hasErrors := false + for _, impObj := range openRTBRequest.Imp { + err := checkImpExt(impObj) + if err != nil { + errs = append(errs, err) + hasErrors = true + } + } + if hasErrors { + return nil, errs + } openRTBRequestJSON, err := json.Marshal(openRTBRequest) if err != nil { errs = append(errs, err) @@ -36,17 +50,36 @@ func (adapter *KubientAdapter) MakeRequests( headers := http.Header{} headers.Add("Content-Type", "application/json;charset=utf-8") - requestToBidder := &adapters.RequestData{ + requestsToBidder := []*adapters.RequestData{{ Method: "POST", Uri: adapter.endpoint, Body: openRTBRequestJSON, Headers: headers, - } - requestsToBidder = append(requestsToBidder, requestToBidder) - + }} return requestsToBidder, errs } +func checkImpExt(impObj openrtb.Imp) error { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(impObj.Ext, &bidderExt); err != nil { + return &errortypes.BadInput{ + Message: "ext.bidder not provided", + } + } + var kubientExt openrtb_ext.ExtImpKubient + if err := json.Unmarshal(bidderExt.Bidder, &kubientExt); err != nil { + return &errortypes.BadInput{ + Message: "ext.bidder.zoneid is not provided", + } + } + if kubientExt.ZoneID == "" { + return &errortypes.BadInput{ + Message: "zoneid is empty", + } + } + return nil +} + // MakeBids makes the bids func (adapter *KubientAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { var errs []error diff --git a/adapters/kubient/kubienttest/exemplary/banner.json b/adapters/kubient/kubienttest/exemplary/banner.json index a32c761a7d0..9af4f9f8cfa 100644 --- a/adapters/kubient/kubienttest/exemplary/banner.json +++ b/adapters/kubient/kubienttest/exemplary/banner.json @@ -17,7 +17,9 @@ ] }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "9042" + } } } ] @@ -44,7 +46,9 @@ ] }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "9042" + } } } ] diff --git a/adapters/kubient/kubienttest/exemplary/video.json b/adapters/kubient/kubienttest/exemplary/video.json index 59d32874cec..d9346c3fa46 100644 --- a/adapters/kubient/kubienttest/exemplary/video.json +++ b/adapters/kubient/kubienttest/exemplary/video.json @@ -11,7 +11,9 @@ "h": 576 }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "9010" + } } } ] @@ -32,7 +34,9 @@ "h": 576 }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "9010" + } } } ] diff --git a/adapters/kubient/kubienttest/supplemental/bad_response.json b/adapters/kubient/kubienttest/supplemental/bad_response.json index 166743cf497..076acf29058 100644 --- a/adapters/kubient/kubienttest/supplemental/bad_response.json +++ b/adapters/kubient/kubienttest/supplemental/bad_response.json @@ -13,7 +13,9 @@ ] }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "23" + } } } ] @@ -36,7 +38,9 @@ ] }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "23" + } } } ] diff --git a/adapters/kubient/kubienttest/supplemental/missing-zoneid.json b/adapters/kubient/kubienttest/supplemental/missing-zoneid.json new file mode 100644 index 00000000000..cfd616621e2 --- /dev/null +++ b/adapters/kubient/kubienttest/supplemental/missing-zoneid.json @@ -0,0 +1,31 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-missing-req-param-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": {} + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "zoneid is empty", + "comparison": "literal" + } + ] +} diff --git a/adapters/kubient/kubienttest/supplemental/no-imps.json b/adapters/kubient/kubienttest/supplemental/no-imps.json new file mode 100644 index 00000000000..189adf9a932 --- /dev/null +++ b/adapters/kubient/kubienttest/supplemental/no-imps.json @@ -0,0 +1,12 @@ +{ + "mockBidRequest": { + "id": "test-no-imp-request-id", + "imp": [] + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the bid request", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/kubient/kubienttest/supplemental/status_204.json b/adapters/kubient/kubienttest/supplemental/status_204.json index 58bb2629a5e..6794d58be6c 100644 --- a/adapters/kubient/kubienttest/supplemental/status_204.json +++ b/adapters/kubient/kubienttest/supplemental/status_204.json @@ -14,6 +14,7 @@ }, "ext": { "bidder": { + "zoneid": "203" } } } @@ -39,6 +40,7 @@ }, "ext": { "bidder": { + "zoneid": "203" } } } diff --git a/adapters/kubient/kubienttest/supplemental/status_400.json b/adapters/kubient/kubienttest/supplemental/status_400.json index e895f793dc1..29438cc3b8b 100644 --- a/adapters/kubient/kubienttest/supplemental/status_400.json +++ b/adapters/kubient/kubienttest/supplemental/status_400.json @@ -13,7 +13,9 @@ ] }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "102" + } } } ] @@ -37,7 +39,9 @@ ] }, "ext": { - "bidder": {} + "bidder": { + "zoneid": "102" + } } } ] diff --git a/openrtb_ext/imp_kubient.go b/openrtb_ext/imp_kubient.go new file mode 100644 index 00000000000..fafd2a0eb8f --- /dev/null +++ b/openrtb_ext/imp_kubient.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpKubient defines the contract for bidrequest.imp[i].ext.kubient +type ExtImpKubient struct { + ZoneID string `json:"zoneid"` +} diff --git a/static/bidder-params/kubient.json b/static/bidder-params/kubient.json index a75dd734ff2..9b975289a7b 100644 --- a/static/bidder-params/kubient.json +++ b/static/bidder-params/kubient.json @@ -3,5 +3,11 @@ "title": "Kubient Adapter Params", "description": "A schema which validates params accepted by the Kubient adapter", "type": "object", - "properties": { } + "properties": { + "zoneid": { + "type": "string", + "description": "Zone ID identifies Kubient placement ID.", + "minLength": 1 + } + } } From 8378a4529e66dc389167984607b8b4adf2a31dbf Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 29 Jun 2020 14:21:20 -0400 Subject: [PATCH 127/381] Improved IPv6 Support + Private Network Filtering (#1362) --- config/config.go | 41 ++- config/config_test.go | 74 +++- config/requestvalidation.go | 55 +++ config/requestvalidation_test.go | 145 ++++++++ endpoints/openrtb2/amp_auction.go | 9 +- endpoints/openrtb2/auction.go | 96 +++-- endpoints/openrtb2/auction_test.go | 196 ++++++++++- .../supplementary/site-has-ipv4.json | 38 ++ .../supplementary/site-has-ipv6.json | 38 ++ endpoints/openrtb2/video_auction.go | 24 +- endpoints/openrtb2/video_auction_test.go | 10 +- exchange/exchange_test.go | 15 +- main_test.go | 14 +- pbs/pbsrequest.go | 11 +- prebid/prebid.go | 82 ----- util/httputil/httputil.go | 99 ++++++ util/httputil/httputil_test.go | 327 ++++++++++++++++++ util/iputil/parse.go | 27 ++ util/iputil/parse_test.go | 30 ++ util/iputil/validator.go | 48 +++ util/iputil/validator_test.go | 222 ++++++++++++ 21 files changed, 1420 insertions(+), 181 deletions(-) create mode 100644 config/requestvalidation.go create mode 100644 config/requestvalidation_test.go create mode 100644 endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json create mode 100644 endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json delete mode 100644 prebid/prebid.go create mode 100644 util/httputil/httputil.go create mode 100644 util/httputil/httputil_test.go create mode 100644 util/iputil/parse.go create mode 100644 util/iputil/parse_test.go create mode 100644 util/iputil/validator.go create mode 100644 util/iputil/validator_test.go diff --git a/config/config.go b/config/config.go index 7d34954583f..50cfbb1c170 100755 --- a/config/config.go +++ b/config/config.go @@ -17,7 +17,7 @@ import ( validator "github.com/asaskevich/govalidator" ) -// Configuration +// Configuration specifies the static application config. type Configuration struct { ExternalURL string `mapstructure:"external_url"` Host string `mapstructure:"host"` @@ -69,6 +69,8 @@ type Configuration struct { RequestTimeoutHeaders RequestTimeoutHeaders `mapstructure:"request_timeout_headers"` // Debug/logging flags go here Debug Debug `mapstructure:"debug"` + // RequestValidation specifies the request validation options. + RequestValidation RequestValidation `mapstructure:"request_validation"` } const MIN_COOKIE_SIZE_BYTES = 500 @@ -239,15 +241,15 @@ type HostCookie struct { TTL int64 `mapstructure:"ttl_days"` } +func (cfg *HostCookie) TTLDuration() time.Duration { + return time.Duration(cfg.TTL) * time.Hour * 24 +} + 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 -} - const ( dummyHost string = "dummyhost.com" dummyPublisherID string = "12" @@ -498,6 +500,15 @@ func New(v *viper.Viper) (*Configuration, error) { } c.setDerivedDefaults() + if err := c.RequestValidation.Parse(); err != nil { + return nil, err + } + + if err := isValidCookieSize(c.HostCookie.MaxCookieSizeBytes); err != nil { + glog.Fatal(fmt.Printf("Max cookie size %d cannot be less than %d \n", c.HostCookie.MaxCookieSizeBytes, MIN_COOKIE_SIZE_BYTES)) + return nil, err + } + // To look for a request's publisher_id in the NonStandardPublishers list in // O(1) time, we fill this hash table located in the NonStandardPublisherMap field of GDPR c.GDPR.NonStandardPublisherMap = make(map[string]int) @@ -519,11 +530,6 @@ func New(v *viper.Viper) (*Configuration, error) { c.BlacklistedAcctMap[c.BlacklistedAccts[i]] = true } - if err := isValidCookieSize(c.HostCookie.MaxCookieSizeBytes); err != nil { - glog.Fatal(fmt.Printf("Max cookie size %d cannot be less than %d \n", c.HostCookie.MaxCookieSizeBytes, MIN_COOKIE_SIZE_BYTES)) - return nil, err - } - glog.Info("Logging the resolved configuration:") logGeneral(reflect.ValueOf(c), " \t") if errs := c.validate(); len(errs) > 0 { @@ -875,8 +881,23 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("debug.timeout_notification.sampling_rate", 0.0) v.SetDefault("debug.timeout_notification.fail_only", false) + /* IPv4 + /* Site Local: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + /* Link Local: 169.254.0.0/16 + /* Loopback: 127.0.0.0/8 + /* + /* IPv6 + /* Loopback: ::1/128 + /* Unique Local: fc00::/7 + /* Link Local: fe80::/10 + /* Multicast: ff00::/8 + */ + v.SetDefault("request_validation.ipv4_private_networks", []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"}) + v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8"}) + // Set environment variable support: v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.SetTypeByDefaultValue(true) v.SetEnvPrefix("PBS") v.AutomaticEnv() v.ReadInConfig() diff --git a/config/config_test.go b/config/config_test.go index c7d406cc8cc..3456694db5c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,7 +2,7 @@ package config import ( "bytes" - "fmt" + "net" "strings" "testing" "time" @@ -119,6 +119,9 @@ adapters: blacklisted_apps: ["spamAppID","sketchy-app-id"] account_required: true certificates_file: /etc/ssl/cert.pem +request_validation: + ipv4_private_networks: ["1.1.1.0/24"] + ipv6_private_networks: ["1111::/16", "2222::/16"] `) var adapterExtraInfoConfig = []byte(` @@ -292,6 +295,9 @@ func TestFullConfig(t *testing.T) { cmpBools(t, "account_required", cfg.AccountRequired, true) cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true) cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem") + cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24") + cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16") + cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[1], "2222::/16") } func TestUnmarshalAdapterExtraInfo(t *testing.T) { @@ -412,23 +418,63 @@ func TestLimitTimeout(t *testing.T) { } func TestCookieSizeError(t *testing.T) { - type aTest struct { - cookieHost *HostCookie + testCases := []struct { + description string + cookieSize int expectError bool + }{ + {"MIN_COOKIE_SIZE_BYTES + 1", MIN_COOKIE_SIZE_BYTES + 1, false}, + {"MIN_COOKIE_SIZE_BYTES", MIN_COOKIE_SIZE_BYTES, false}, + {"MIN_COOKIE_SIZE_BYTES - 1", MIN_COOKIE_SIZE_BYTES - 1, true}, + {"Zero", 0, false}, + {"Negative", -100, true}, } - testCases := []aTest{ - {cookieHost: &HostCookie{MaxCookieSizeBytes: 1 << 15}, expectError: false}, //32 KB, no error - {cookieHost: &HostCookie{MaxCookieSizeBytes: 800}, expectError: false}, - {cookieHost: &HostCookie{MaxCookieSizeBytes: 500}, expectError: false}, - {cookieHost: &HostCookie{MaxCookieSizeBytes: 0}, expectError: false}, - {cookieHost: &HostCookie{MaxCookieSizeBytes: 200}, expectError: true}, - {cookieHost: &HostCookie{MaxCookieSizeBytes: -100}, expectError: true}, + + for _, test := range testCases { + resultErr := isValidCookieSize(test.cookieSize) + + if test.expectError { + assert.Error(t, resultErr, test.description) + } else { + assert.NoError(t, resultErr, test.description) + } + } +} + +func TestNewCallsRequestValidation(t *testing.T) { + testCases := []struct { + description string + privateIPNetworks string + expectedError string + expectedIPs []net.IPNet + }{ + { + description: "Valid", + privateIPNetworks: `["1.1.1.0/24"]`, + expectedIPs: []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: net.CIDRMask(24, 32)}}, + }, + { + description: "Invalid", + privateIPNetworks: `["1"]`, + expectedError: "Invalid private IPv4 networks: '1'", + }, } - for i := range testCases { - if testCases[i].expectError { - 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)) + + for _, test := range testCases { + v := viper.New() + SetupViper(v, "") + v.SetConfigType("yaml") + v.ReadConfig(bytes.NewBuffer([]byte( + `request_validation: + ipv4_private_networks: ` + test.privateIPNetworks))) + + result, resultErr := New(v) + + if test.expectedError == "" { + assert.NoError(t, resultErr, test.description+":err") + assert.ElementsMatch(t, test.expectedIPs, result.RequestValidation.IPv4PrivateNetworksParsed, test.description+":parsed") } else { - 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)) + assert.Error(t, resultErr, test.description+":err") } } } diff --git a/config/requestvalidation.go b/config/requestvalidation.go new file mode 100644 index 00000000000..0824f4da880 --- /dev/null +++ b/config/requestvalidation.go @@ -0,0 +1,55 @@ +package config + +import ( + "errors" + "fmt" + "net" + "strings" +) + +// RequestValidation specifies the request validation options. +type RequestValidation struct { + IPv4PrivateNetworks []string `mapstructure:"ipv4_private_networks,flow"` + IPv4PrivateNetworksParsed []net.IPNet + + IPv6PrivateNetworks []string `mapstructure:"ipv6_private_networks,flow"` + IPv6PrivateNetworksParsed []net.IPNet +} + +// Parse converts the CIDR representation of the IPv4 and IPv6 private networks as net.IPNet structs, or returns an error if at least one is invalid. +func (r *RequestValidation) Parse() error { + ipv4Nets, err := parseNetworks(r.IPv4PrivateNetworks, net.IPv4len) + if err != nil { + return errors.New("Invalid private IPv4 network: " + err.Error()) + } + + ipv6Nets, err := parseNetworks(r.IPv6PrivateNetworks, net.IPv6len) + if err != nil { + return errors.New("Invalid private IPv6 network: " + err.Error()) + } + + r.IPv4PrivateNetworksParsed = ipv4Nets + r.IPv6PrivateNetworksParsed = ipv6Nets + return nil +} + +func parseNetworks(networks []string, networksLen int) ([]net.IPNet, error) { + ipNetworks := make([]net.IPNet, 0, len(networks)) + errMsg := strings.Builder{} + + for _, v := range networks { + v := strings.TrimSpace(v) + + if _, ipNet, err := net.ParseCIDR(v); err != nil || len(ipNet.IP) != networksLen { + fmt.Fprintf(&errMsg, "'%s',", v) + } else { + ipNetworks = append(ipNetworks, *ipNet) + } + } + + if errMsg.Len() > 0 { + return nil, errors.New(errMsg.String()[:errMsg.Len()-1]) + } + + return ipNetworks, nil +} diff --git a/config/requestvalidation_test.go b/config/requestvalidation_test.go new file mode 100644 index 00000000000..cacb4f2d140 --- /dev/null +++ b/config/requestvalidation_test.go @@ -0,0 +1,145 @@ +package config + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParse(t *testing.T) { + ipv4Mask16 := net.CIDRMask(16, 32) + ipv4Mask24 := net.CIDRMask(24, 32) + + ipv6Mask16 := net.CIDRMask(16, 128) + ipv6Mask32 := net.CIDRMask(32, 128) + + testCases := []struct { + description string + ipv4 []string + ipv4Expected []net.IPNet + ipv6 []string + ipv6Expected []net.IPNet + expectedErr string + }{ + { + description: "Empty", + ipv4: []string{}, + ipv4Expected: []net.IPNet{}, + ipv6: []string{}, + ipv6Expected: []net.IPNet{}, + }, + { + description: "One", + ipv4: []string{"1.1.1.1/24"}, + ipv4Expected: []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: ipv4Mask24}}, + ipv6: []string{"1111:2222::/16"}, + ipv6Expected: []net.IPNet{{IP: net.ParseIP("1111::"), Mask: ipv6Mask16}}, + }, + { + description: "One - Ignore Whitespace", + ipv4: []string{" 1.1.1.1/24 "}, + ipv4Expected: []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: ipv4Mask24}}, + ipv6: []string{" 1111:2222::/16 "}, + ipv6Expected: []net.IPNet{{IP: net.ParseIP("1111::"), Mask: ipv6Mask16}}, + }, + { + description: "Many", + ipv4: []string{"1.1.1.1/24", "2.2.2.2/16"}, + ipv4Expected: []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: ipv4Mask24}, {IP: net.IP{2, 2, 0, 0}, Mask: ipv4Mask16}}, + ipv6: []string{"1111:2222::/16", "1111:2222:3333::/32"}, + ipv6Expected: []net.IPNet{{IP: net.ParseIP("1111::"), Mask: ipv6Mask16}, {IP: net.ParseIP("1111:2222::"), Mask: ipv6Mask32}}, + }, + { + description: "Malformed - IPv4 - One", + ipv4: []string{"malformed1"}, + ipv6: []string{}, + expectedErr: "Invalid private IPv4 network: 'malformed1'", + }, + { + description: "Malformed - IPv4 - Many", + ipv4: []string{"malformed1", "malformed2"}, + ipv6: []string{}, + expectedErr: "Invalid private IPv4 network: 'malformed1','malformed2'", + }, + { + description: "Malformed - IPv6 - One", + ipv4: []string{}, + ipv6: []string{"malformed2"}, + expectedErr: "Invalid private IPv6 network: 'malformed2'", + }, + { + description: "Malformed - IPv6 - Many", + ipv4: []string{}, + ipv6: []string{"malformed1", "malformed2"}, + expectedErr: "Invalid private IPv6 network: 'malformed1','malformed2'", + }, + { + description: "Malformed - Mixed", + ipv4: []string{"malformed1"}, + ipv6: []string{"malformed2"}, + expectedErr: "Invalid private IPv4 network: 'malformed1'", + }, + { + description: "Malformed - IPv4 - Ignore Whitespace", + ipv4: []string{" malformed1 "}, + ipv6: []string{}, + expectedErr: "Invalid private IPv4 network: 'malformed1'", + }, + { + description: "Malformed - IPv6 - Ignore Whitespace", + ipv4: []string{}, + ipv6: []string{" malformed2 "}, + expectedErr: "Invalid private IPv6 network: 'malformed2'", + }, + { + description: "Malformed - IPv4 - Missing Network Mask", + ipv4: []string{"1.1.1.1"}, + ipv6: []string{}, + expectedErr: "Invalid private IPv4 network: '1.1.1.1'", + }, + { + description: "Malformed - IPv6 - Missing Network Mask", + ipv4: []string{}, + ipv6: []string{"1111::"}, + expectedErr: "Invalid private IPv6 network: '1111::'", + }, + { + description: "Malformed - IPv4 - Wrong IP Version", + ipv4: []string{"1111::/16"}, + ipv6: []string{}, + expectedErr: "Invalid private IPv4 network: '1111::/16'", + }, + { + description: "Malformed - IPv6 - Wrong IP Version", + ipv4: []string{}, + ipv6: []string{"1.1.1.1/16"}, + expectedErr: "Invalid private IPv6 network: '1.1.1.1/16'", + }, + { + description: "Malformed - IPv6 Mapped IPv4", + ipv4: []string{"::FFFF:1.1.1.1"}, + ipv6: []string{}, + expectedErr: "Invalid private IPv4 network: '::FFFF:1.1.1.1'", + }, + } + + for _, test := range testCases { + requestValidation := &RequestValidation{ + IPv4PrivateNetworks: test.ipv4, + IPv6PrivateNetworks: test.ipv6, + } + + err := requestValidation.Parse() + + if test.expectedErr == "" { + assert.NoError(t, err, test.description+":err") + } else { + assert.Error(t, err, test.description+":err") + assert.Equal(t, test.expectedErr, err.Error(), test.description+":err_msg") + } + + assert.ElementsMatch(t, requestValidation.IPv4PrivateNetworksParsed, test.ipv4Expected, test.description+":ipv4") + assert.ElementsMatch(t, requestValidation.IPv6PrivateNetworksParsed, test.ipv6Expected, test.description+":ipv6") + } +} diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 2dcd572c63c..e8b5d3ecc76 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -26,6 +26,7 @@ import ( "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/util/iputil" ) const defaultAmpRequestTimeoutMillis = 900 @@ -58,6 +59,11 @@ func NewAmpEndpoint( defRequest := defReqJSON != nil && len(defReqJSON) > 0 + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + return httprouter.Handle((&endpointDeps{ ex, validator, @@ -72,7 +78,8 @@ func NewAmpEndpoint( defReqJSON, bidderMap, nil, - nil}).AmpAuction), nil + nil, + ipValidator}).AmpAuction), nil } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index bd50fca9149..20acc2aedd3 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -27,12 +27,13 @@ import ( "github.com/prebid/prebid-server/exchange" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" - "github.com/prebid/prebid-server/prebid" "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/privacy/ccpa" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/util/httputil" + "github.com/prebid/prebid-server/util/iputil" "golang.org/x/net/publicsuffix" ) @@ -43,8 +44,14 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { return nil, errors.New("NewEndpoint requires non-nil arguments.") } + defRequest := defReqJSON != nil && len(defReqJSON) > 0 + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + return httprouter.Handle((&endpointDeps{ ex, validator, @@ -59,24 +66,26 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato defReqJSON, bidderMap, nil, - nil}).Auction), nil + nil, + ipValidator}).Auction), nil } type endpointDeps struct { - ex exchange.Exchange - paramsValidator openrtb_ext.BidderParamValidator - storedReqFetcher stored_requests.Fetcher - videoFetcher stored_requests.Fetcher - categories stored_requests.CategoryFetcher - cfg *config.Configuration - metricsEngine pbsmetrics.MetricsEngine - analytics analytics.PBSAnalyticsModule - disabledBidders map[string]string - defaultRequest bool - defReqJSON []byte - bidderMap map[string]openrtb_ext.BidderName - cache prebid_cache_client.Client - debugLogRegexp *regexp.Regexp + ex exchange.Exchange + paramsValidator openrtb_ext.BidderParamValidator + storedReqFetcher stored_requests.Fetcher + videoFetcher stored_requests.Fetcher + categories stored_requests.CategoryFetcher + cfg *config.Configuration + metricsEngine pbsmetrics.MetricsEngine + analytics analytics.PBSAnalyticsModule + disabledBidders map[string]string + defaultRequest bool + defReqJSON []byte + bidderMap map[string]openrtb_ext.BidderName + cache prebid_cache_client.Client + debugLogRegexp *regexp.Regexp + privateNetworkIPValidator iputil.IPValidator } func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -308,17 +317,14 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { return errL } - ccpaPolicy, ccpaPolicyErr := ccpa.ReadPolicy(req) - if ccpaPolicyErr != nil { - errL = append(errL, ccpaPolicyErr) + if policy, err := ccpa.ReadPolicy(req); err != nil { + errL = append(errL, errL...) return errL - } - - if err := ccpaPolicy.Validate(); err != nil { + } else if err := policy.Validate(); err != nil { 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 { + policy.Value = "" + if err := policy.Write(req); err != nil { errL = append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err)) } } @@ -914,13 +920,27 @@ func validateRegs(regs *openrtb.Regs) error { return nil } +func sanitizeRequest(r *openrtb.BidRequest, ipValidator iputil.IPValidator) { + if r.Device != nil { + if ip, ver := iputil.ParseIP(r.Device.IP); ip == nil || ver != iputil.IPv4 || !ipValidator.IsValid(ip, ver) { + r.Device.IP = "" + } + + if ip, ver := iputil.ParseIP(r.Device.IPv6); ip == nil || ver != iputil.IPv6 || !ipValidator.IsValid(ip, ver) { + r.Device.IPv6 = "" + } + } +} + // setFieldsImplicitly uses _implicit_ information from the httpReq to set values on bidReq. // This function does not consume the request body, which was set explicitly, but infers certain // OpenRTB properties from the headers and other implicit info. // // This function _should not_ override any fields which were defined explicitly by the caller in the request. func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { - setDeviceImplicitly(httpReq, bidReq) + sanitizeRequest(bidReq, deps.privateNetworkIPValidator) + + setDeviceImplicitly(httpReq, bidReq, deps.privateNetworkIPValidator) // Per the OpenRTB spec: A bid request must not contain both a Site and an App object. if bidReq.App == nil { @@ -932,8 +952,8 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *ope } // setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device -func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { - setIPImplicitly(httpReq, bidReq) // Fixes #230 +func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidtor iputil.IPValidator) { + setIPImplicitly(httpReq, bidReq, ipValidtor) setUAImplicitly(httpReq, bidReq) } @@ -975,7 +995,7 @@ func setSiteImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { func setImpsImplicitly(httpReq *http.Request, imps []openrtb.Imp) { secure := int8(1) for i := 0; i < len(imps); i++ { - if imps[i].Secure == nil && prebid.IsSecure(httpReq) { + if imps[i].Secure == nil && httputil.IsSecure(httpReq) { imps[i].Secure = &secure } } @@ -1132,13 +1152,21 @@ func getStoredRequestId(data []byte) (string, bool, error) { } // setIPImplicitly sets the IP address on bidReq, if it's not explicitly defined and we can figure it out. -func setIPImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { - if bidReq.Device == nil || bidReq.Device.IP == "" { - if ip := prebid.GetIP(httpReq); ip != "" { - if bidReq.Device == nil { - bidReq.Device = &openrtb.Device{} +func setIPImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidator iputil.IPValidator) { + if bidReq.Device == nil || (bidReq.Device.IP == "" && bidReq.Device.IPv6 == "") { + if ip, ver := httputil.FindIP(httpReq, ipValidator); ip != nil { + switch ver { + case iputil.IPv4: + if bidReq.Device == nil { + bidReq.Device = &openrtb.Device{} + } + bidReq.Device.IP = ip.String() + case iputil.IPv6: + if bidReq.Device == nil { + bidReq.Device = &openrtb.Device{} + } + bidReq.Device.IPv6 = ip.String() } - bidReq.Device.IP = ip } } } diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index c3b9267bf8b..97f0038a392 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "net/http/httptest" "os" @@ -29,6 +30,7 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" + "github.com/prebid/prebid-server/util/iputil" "github.com/stretchr/testify/assert" ) @@ -526,26 +528,79 @@ func TestAuctionTypeDefault(t *testing.T) { } } -// TestImplicitIPs prevents #230 -func TestImplicitIPs(t *testing.T) { - ex := &nobidExchange{} - // 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{}) - endpoint, _ := NewEndpoint(ex, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) +func TestImplicitIPsEndToEnd(t *testing.T) { + testCases := []struct { + description string + reqJSONFile string + xForwardedForHeader string + privateNetworksIPv4 []net.IPNet + privateNetworksIPv6 []net.IPNet + expectedDeviceIPv4 string + expectedDeviceIPv6 string + }{ + { + description: "IPv4", + reqJSONFile: "site.json", + xForwardedForHeader: "1.1.1.1", + expectedDeviceIPv4: "1.1.1.1", + }, + { + description: "IPv6", + reqJSONFile: "site.json", + xForwardedForHeader: "1111::", + expectedDeviceIPv6: "1111::", + }, + { + description: "IPv4 - Defined In Request", + reqJSONFile: "site-has-ipv4.json", + xForwardedForHeader: "1.1.1.1", + expectedDeviceIPv4: "8.8.8.8", // Hardcoded value in test file. + }, + { + description: "IPv6 - Defined In Request", + reqJSONFile: "site-has-ipv6.json", + xForwardedForHeader: "1111::", + expectedDeviceIPv6: "8888::", // Hardcoded value in test file. + }, + { + description: "IPv4 - Defined In Request - Private Network", + reqJSONFile: "site-has-ipv4.json", + xForwardedForHeader: "1.1.1.1", + privateNetworksIPv4: []net.IPNet{{IP: net.IP{8, 8, 8, 0}, Mask: net.CIDRMask(24, 32)}}, // Hardcoded value in test file. + expectedDeviceIPv4: "1.1.1.1", + }, + { + description: "IPv6 - Defined In Request - Private Network", + reqJSONFile: "site-has-ipv6.json", + xForwardedForHeader: "1111::", + privateNetworksIPv6: []net.IPNet{{IP: net.ParseIP("8800::"), Mask: net.CIDRMask(8, 128)}}, // Hardcoded value in test file. + expectedDeviceIPv6: "1111::", + }, + } - httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) - httpReq.Header.Set("X-Forwarded-For", "123.456.78.90") - recorder := httptest.NewRecorder() + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + for _, test := range testCases { + exchange := &nobidExchange{} + cfg := &config.Configuration{ + MaxRequestSize: maxSize, + RequestValidation: config.RequestValidation{ + IPv4PrivateNetworksParsed: test.privateNetworksIPv4, + IPv6PrivateNetworksParsed: test.privateNetworksIPv6, + }, + } + endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) - endpoint(recorder, httpReq, nil) + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) + httpReq.Header.Set("X-Forwarded-For", test.xForwardedForHeader) - if ex.gotRequest == nil { - t.Fatalf("The request never made it into the Exchange.") - } + endpoint(httptest.NewRecorder(), httpReq, nil) - if ex.gotRequest.Device.IP != "123.456.78.90" { - t.Errorf("Bad device IP. Expected 123.456.78.90, got %s", ex.gotRequest.Device.IP) + result := exchange.gotRequest + if !assert.NotEmpty(t, result, test.description+"Request received by the exchange.") { + t.FailNow() + } + assert.Equal(t, test.expectedDeviceIPv4, result.Device.IP, test.description+":ipv4") + assert.Equal(t, test.expectedDeviceIPv6, result.Device.IPv6, test.description+":ipv6") } } @@ -602,10 +657,26 @@ 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, nil, nil} + deps := &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, + hardcodedResponseIPValidator{response: true}, + } for i, requestData := range testStoredRequests { - newRequest, errList := edep.processStoredRequests(context.Background(), json.RawMessage(requestData)) + newRequest, errList := deps.processStoredRequests(context.Background(), json.RawMessage(requestData)) if len(errList) != 0 { for _, err := range errList { if err != nil { @@ -640,6 +711,7 @@ func TestOversizedRequest(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -674,6 +746,7 @@ func TestRequestSizeEdgeCase(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -813,6 +886,7 @@ func TestDisabledBidder(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -848,6 +922,7 @@ func TestValidateImpExtDisabledBidder(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } errs := deps.validateImpExt(imp, nil, 0) assert.JSONEq(t, `{"appnexus":{"placement_id":555}}`, string(imp.Ext)) @@ -888,6 +963,7 @@ func TestCurrencyTrunc(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } ui := uint64(1) @@ -931,6 +1007,7 @@ func TestCCPAInvalid(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } ui := uint64(1) @@ -962,6 +1039,81 @@ func TestCCPAInvalid(t *testing.T) { assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") } +func TestSanitizeRequest(t *testing.T) { + testCases := []struct { + description string + req *openrtb.BidRequest + ipValidator iputil.IPValidator + expectedIPv4 string + expectedIPv6 string + }{ + { + description: "Empty", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "", + IPv6: "", + }, + }, + expectedIPv4: "", + expectedIPv6: "", + }, + { + description: "Valid", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "1.1.1.1", + IPv6: "1111::", + }, + }, + ipValidator: hardcodedResponseIPValidator{response: true}, + expectedIPv4: "1.1.1.1", + expectedIPv6: "1111::", + }, + { + description: "Invalid", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "1.1.1.1", + IPv6: "1111::", + }, + }, + ipValidator: hardcodedResponseIPValidator{response: false}, + expectedIPv4: "", + expectedIPv6: "", + }, + { + description: "Invalid - Wrong IP Types", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "1111::", + IPv6: "1.1.1.1", + }, + }, + ipValidator: hardcodedResponseIPValidator{response: true}, + expectedIPv4: "", + expectedIPv6: "", + }, + { + description: "Malformed", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "malformed", + IPv6: "malformed", + }, + }, + expectedIPv4: "", + expectedIPv6: "", + }, + } + + for _, test := range testCases { + sanitizeRequest(test.req, test.ipValidator) + assert.Equal(t, test.expectedIPv4, test.req.Device.IP, test.description+":ipv4") + assert.Equal(t, test.expectedIPv6, test.req.Device.IPv6, test.description+":ipv6") + } +} + // nobidExchange is a well-behaved exchange which always bids "no bid". type nobidExchange struct { gotRequest *openrtb.BidRequest @@ -1385,3 +1537,11 @@ func newBidderInfo(cfg config.Adapter) adapters.BidderInfo { Status: status, } } + +type hardcodedResponseIPValidator struct { + response bool +} + +func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool { + return v.response +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json new file mode 100644 index 00000000000..feade898833 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json @@ -0,0 +1,38 @@ +{ + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 600 + }] + }, + "pmp": { + "deals": [{ + "id": "some-deal-id" + }] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + }], + "device": { + "ip": "8.8.8.8" + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } + } + } +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json new file mode 100644 index 00000000000..42d8d37b1cb --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json @@ -0,0 +1,38 @@ +{ + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 600 + }] + }, + "pmp": { + "deals": [{ + "id": "some-deal-id" + }] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + }], + "device": { + "ipv6": "8888::" + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } + } + } + } \ No newline at end of file diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 64c99fa5a3e..18678be541c 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -18,6 +18,7 @@ import ( jsonpatch "github.com/evanphx/json-patch" "github.com/gofrs/uuid" "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/util/iputil" "github.com/golang/glog" "github.com/julienschmidt/httprouter" @@ -39,11 +40,32 @@ func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamVal 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 + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + videoEndpointRegexp := regexp.MustCompile(`[<>]`) - return httprouter.Handle((&endpointDeps{ex, validator, requestsById, videoFetcher, categories, cfg, met, pbsAnalytics, disabledBidders, defRequest, defReqJSON, bidderMap, cache, videoEndpointRegexp}).VideoAuctionEndpoint), nil + return httprouter.Handle((&endpointDeps{ + ex, + validator, + requestsById, + videoFetcher, + categories, + cfg, + met, + pbsAnalytics, + disabledBidders, + defRequest, + defReqJSON, + bidderMap, + cache, + videoEndpointRegexp, + ipValidator}).VideoAuctionEndpoint), nil } /* diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 631cb277f7f..f29ac3bfed9 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1110,7 +1110,7 @@ func TestCCPA(t *testing.T) { func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) mockModule := &mockAnalyticsModule{} - edep := &endpointDeps{ + deps := &endpointDeps{ ex, newParamsValidator(t), &mockVideoStoredReqFetcher{}, @@ -1125,9 +1125,10 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *p openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } - return edep, theMetrics, mockModule + return deps, theMetrics, mockModule } type mockAnalyticsModule struct { @@ -1151,7 +1152,7 @@ func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { return } func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - edep := &endpointDeps{ + deps := &endpointDeps{ ex, newParamsValidator(t), &mockVideoStoredReqFetcher{}, @@ -1166,9 +1167,10 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { openrtb_ext.BidderMap, ex.cache, regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, } - return edep + return deps } type mockCacheClient struct { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 93cb60fb5af..161b24fd1c1 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -16,19 +16,18 @@ import ( "time" "github.com/prebid/prebid-server/adapters" - "github.com/prebid/prebid-server/currencies" - "github.com/prebid/prebid-server/prebid_cache_client" - "github.com/prebid/prebid-server/stored_requests" - "github.com/prebid/prebid-server/stored_requests/backends/file_fetcher" - - "github.com/buger/jsonparser" - "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/currencies" "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" metricsConf "github.com/prebid/prebid-server/pbsmetrics/config" pbc "github.com/prebid/prebid-server/prebid_cache_client" + "github.com/prebid/prebid-server/stored_requests" + "github.com/prebid/prebid-server/stored_requests/backends/file_fetcher" + + "github.com/buger/jsonparser" + "github.com/mxmCherry/openrtb" "github.com/rcrowley/go-metrics" "github.com/stretchr/testify/assert" "github.com/yudai/gojsondiff" @@ -1837,7 +1836,7 @@ func (c *wellBehavedCache) GetExtCacheData() (string, string) { return "www.pbcserver.com", "/pbcache/endpoint" } -func (c *wellBehavedCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) { +func (c *wellBehavedCache) PutJson(ctx context.Context, values []pbc.Cacheable) ([]string, []error) { ids := make([]string, len(values)) for i := 0; i < len(values); i++ { ids[i] = strconv.Itoa(i) diff --git a/main_test.go b/main_test.go index d7dc9dd24a0..70eea2825f0 100644 --- a/main_test.go +++ b/main_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/prebid/prebid-server/config" + "github.com/stretchr/testify/assert" "github.com/spf13/viper" ) @@ -56,10 +57,11 @@ func TestViperEnv(t *testing.T) { ttl := forceEnv(t, "PBS_HOST_COOKIE_TTL_DAYS", "60") defer ttl() - // Basic config set - compareStrings(t, "Viper error: port expected to be %s, found %s", "7777", v.Get("port").(string)) - // Nested config set - compareStrings(t, "Viper error: adapters.pubmatic.endpoint expected to be %s, found %s", "not_an_endpoint", v.Get("adapters.pubmatic.endpoint").(string)) - // Config set with underscores - compareStrings(t, "Viper error: host_cookie.ttl_days expected to be %s, found %s", "60", v.Get("host_cookie.ttl_days").(string)) + ipv4Networks := forceEnv(t, "PBS_REQUEST_VALIDATION_IPV4_PRIVATE_NETWORKS", "1.1.1.1/24 2.2.2.2/24") + defer ipv4Networks() + + assert.Equal(t, 7777, v.Get("port"), "Basic Config") + assert.Equal(t, "not_an_endpoint", v.Get("adapters.pubmatic.endpoint"), "Nested Config") + assert.Equal(t, 60, v.Get("host_cookie.ttl_days"), "Config With Underscores") + assert.ElementsMatch(t, []string{"1.1.1.1/24", "2.2.2.2/24"}, v.Get("request_validation.ipv4_private_networks"), "Arrays") } diff --git a/pbs/pbsrequest.go b/pbs/pbsrequest.go index 9e79b62b38b..30f8bd25c0d 100644 --- a/pbs/pbsrequest.go +++ b/pbs/pbsrequest.go @@ -12,9 +12,10 @@ import ( "github.com/prebid/prebid-server/cache" "github.com/prebid/prebid-server/config" - "github.com/prebid/prebid-server/prebid" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/usersync" + "github.com/prebid/prebid-server/util/httputil" + "github.com/prebid/prebid-server/util/iputil" "github.com/blang/semver" "github.com/buger/jsonparser" @@ -216,6 +217,8 @@ func ParseMediaTypes(types []string) []MediaType { return mtypes } +var ipv4Validator iputil.IPValidator = iputil.VersionIPValidator{iputil.IPv4} + func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.Cache, hostCookieConfig *config.HostCookie) (*PBSRequest, error) { defer r.Body.Close() @@ -235,7 +238,9 @@ func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.C if pbsReq.Device == nil { pbsReq.Device = &openrtb.Device{} } - pbsReq.Device.IP = prebid.GetIP(r) + if ip, _ := httputil.FindIP(r, ipv4Validator); ip != nil { + pbsReq.Device.IP = ip.String() + } if pbsReq.SDK == nil { pbsReq.SDK = &SDK{} @@ -291,7 +296,7 @@ func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.C pbsReq.IsDebug = true } - if prebid.IsSecure(r) { + if httputil.IsSecure(r) { pbsReq.Secure = 1 } diff --git a/prebid/prebid.go b/prebid/prebid.go deleted file mode 100644 index 68c4a48c2c8..00000000000 --- a/prebid/prebid.go +++ /dev/null @@ -1,82 +0,0 @@ -package prebid - -import ( - "net" - "net/http" - "strings" -) - -var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") -var xRealIP = http.CanonicalHeaderKey("X-Real-IP") -var xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") - -// IsSecure attempts to detect whether the request is https -func IsSecure(r *http.Request) bool { - // lowercase for case-insensitive match for X-Forwarded-Proto header - if strings.ToLower(r.Header.Get(xForwardedProto)) == "https" { - return true - } - // ensure that URL.Scheme is lowercase (it should be "https") - if strings.ToLower(r.URL.Scheme) == "https" { - return true - } - // use strings.HasPrefix because a valid example is "HTTP/1.0" - if strings.HasPrefix(r.Proto, "HTTPS") { - return true - } - // check if TLS is not-nil as a final fallback - if r.TLS != nil { - return true - } - return false -} - -// GetIP will attempt to get the IP Address by first checking headers -// and then falling back on the RemoteAddr -func GetIP(r *http.Request) string { - // first check headers - if ip := GetForwardedIP(r); ip != "" { - return ip - } - // next try to parse the RemoteAddr. - // if err is not nil then weird hosts might appear as the ip: https://github.com/golang/go/issues/14827 - if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { - return ip - } - return "" -} - -// GetForwardedIP will return back X-Forwarded-For or X-Real-IP (if set) -func GetForwardedIP(r *http.Request) string { - // first attempt to parse X-Forwarded-For - if ip := getForwardedFor(r); ip != "" { - return ip - } - // if we don't have X-Forwarded-For then try X-Real-IP - if ip := getRealIP(r); ip != "" { - return ip - } - return "" -} - -// getForwardedFor will attempt to parse the X-Forwarded-For header -func getForwardedFor(r *http.Request) string { - if xff := r.Header.Get(xForwardedFor); xff != "" { - // X-Forwarded-For: client1, proxy1, proxy2 - i := strings.Index(xff, ", ") - if i == -1 { - i = len(xff) - } - return xff[:i] - } - return "" -} - -// getRealIP will attempt to parse the X-Real-IP header -// Header.Get is case-insensitive -func getRealIP(r *http.Request) string { - if xrip := r.Header.Get(xRealIP); xrip != "" { - return xrip - } - return "" -} diff --git a/util/httputil/httputil.go b/util/httputil/httputil.go new file mode 100644 index 00000000000..461512771b3 --- /dev/null +++ b/util/httputil/httputil.go @@ -0,0 +1,99 @@ +package httputil + +import ( + "net" + "net/http" + "strings" + + "github.com/prebid/prebid-server/util/iputil" +) + +var ( + trueClientIP = http.CanonicalHeaderKey("True-Client-IP") + xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") + xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") + xRealIP = http.CanonicalHeaderKey("X-Real-IP") +) + +const ( + https = "https" +) + +// IsSecure determines if a http request uses https. +func IsSecure(r *http.Request) bool { + if strings.EqualFold(r.Header.Get(xForwardedProto), https) { + return true + } + + if strings.EqualFold(r.URL.Scheme, https) { + return true + } + + if r.TLS != nil { + return true + } + + return false +} + +// FindIP returns the first ip address found in the http request matching the predicate v. +func FindIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if ip, ver := findTrueClientIP(r, v); ip != nil { + return ip, ver + } + + if ip, ver := findForwardedFor(r, v); ip != nil { + return ip, ver + } + + if ip, ver := findRealIP(r, v); ip != nil { + return ip, ver + } + + if ip, ver := findRemoteAddr(r, v); ip != nil { + return ip, ver + } + + return nil, iputil.IPvUnknown +} + +func findTrueClientIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if value := r.Header.Get(trueClientIP); value != "" { + value = strings.TrimSpace(value) + if ip, ver := iputil.ParseIP(value); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + return nil, iputil.IPvUnknown +} + +func findForwardedFor(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if value := r.Header.Get(xForwardedFor); value != "" { + for _, p := range strings.Split(value, ",") { + p = strings.TrimSpace(p) + if ip, ver := iputil.ParseIP(p); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + } + return nil, iputil.IPvUnknown +} + +func findRealIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if value := r.Header.Get(xRealIP); value != "" { + value = strings.TrimSpace(value) + if ip, ver := iputil.ParseIP(value); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + return nil, iputil.IPvUnknown +} + +func findRemoteAddr(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + if ip, ver := iputil.ParseIP(host); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + return nil, iputil.IPvUnknown +} diff --git a/util/httputil/httputil_test.go b/util/httputil/httputil_test.go new file mode 100644 index 00000000000..f7166740fe5 --- /dev/null +++ b/util/httputil/httputil_test.go @@ -0,0 +1,327 @@ +package httputil + +import ( + "crypto/tls" + "net" + "net/http" + "testing" + + "github.com/prebid/prebid-server/util/iputil" + "github.com/stretchr/testify/assert" +) + +func TestIsSecure(t *testing.T) { + testCases := []struct { + description string + url string + xForwardedProto string + tls bool + expectIsSecure bool + }{ + { + description: "HTTP", + url: "http://host.com", + expectIsSecure: false, + }, + { + description: "HTTPS - Forwarded Protocol", + url: "http://host.com", + xForwardedProto: "https", + expectIsSecure: true, + }, + { + description: "HTTPS - Forwarded Protocol - Case Insensitive", + url: "http://host.com", + xForwardedProto: "HTTPS", + expectIsSecure: true, + }, + { + description: "HTTPS - Protocol", + url: "https://host.com", + expectIsSecure: true, + }, + { + description: "HTTPS - Protocol - Case Insensitive", + url: "HTTPS://host.com", + expectIsSecure: true, + }, + { + description: "HTTPS - TLS", + url: "http://host.com", + tls: true, + expectIsSecure: true, + }, + } + + for _, test := range testCases { + request, err := http.NewRequest("GET", test.url, nil) + if err != nil { + t.Fatalf("Unable to create test http request. Err: %v", err) + } + if test.xForwardedProto != "" { + request.Header.Add("X-Forwarded-Proto", test.xForwardedProto) + } + if test.tls { + request.TLS = &tls.ConnectionState{} + } + + result := IsSecure(request) + + assert.Equal(t, test.expectIsSecure, result, test.description) + } +} + +func TestFindIP(t *testing.T) { + alwaysTrue := hardcodedResponseIPValidator{response: true} + alwaysFalse := hardcodedResponseIPValidator{response: false} + + testCases := []struct { + description string + trueClientIP string + xForwardedFor string + xRealIP string + remoteAddr string + validator iputil.IPValidator + expectedIP net.IP + expectedVer iputil.IPVersion + }{ + { + description: "No Address", + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "False Validator - IPv4", + trueClientIP: "1.1.1.1", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysFalse, + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "False Validator - IPv6", + trueClientIP: "1111::", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5]", + validator: alwaysFalse, + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "True Validator - IPv4 - True Client IP", + trueClientIP: "1.1.1.1", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1.1.1.1"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - True Client IP - Ignore Whitespace", + trueClientIP: " 1.1.1.1 ", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1.1.1.1"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Forwarded For", + trueClientIP: "", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2.2.2.2"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Forwarded For - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: " 2.2.2.2, 3.3.3.3 ", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2.2.2.2"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Real IP", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Real IP - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: "", + xRealIP: " 4.4.4.4 ", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - Remote Address", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "", + remoteAddr: "5.5.5.5:80", + validator: alwaysTrue, + expectedIP: net.ParseIP("5.5.5.5"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv6 - True Client IP", + trueClientIP: "1111::", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1111::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - True Client IP - Ignore Whitespace", + trueClientIP: " 1111:: ", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1111::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Forwarded For", + trueClientIP: "", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2222::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Forwarded For - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: " 2222::, 3333:: ", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2222::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Real IP", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4444::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Real IP - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: "", + xRealIP: " 4444:: ", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4444::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - Remote Address", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("5555::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - Malformed - All", + trueClientIP: "malformed", + xForwardedFor: "malformed", + xRealIP: "malformed", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "True Validator - Malformed - Some", + trueClientIP: "malformed", + xForwardedFor: "malformed", + xRealIP: "4.4.4.4", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - Malformed - X Forwarded For - IPv4", + trueClientIP: "malformed", + xForwardedFor: "malformed, 4.4.4.4, 3333::, malformed", + xRealIP: "malformed", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - Malformed - X Forwarded For - IPv6", + trueClientIP: "malformed", + xForwardedFor: "malformed, 3333::, 4.4.4.4, malformed", + xRealIP: "malformed", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: net.ParseIP("3333::"), + expectedVer: iputil.IPv6, + }, + } + + for _, test := range testCases { + // Build Request + request, err := http.NewRequest("GET", "http://anyurl.com", nil) + if err != nil { + t.Fatalf("Unable to create test http request. Err: %v", err) + } + if test.trueClientIP != "" { + request.Header.Add("True-Client-IP", test.trueClientIP) + } + if test.xForwardedFor != "" { + request.Header.Add("X-Forwarded-For", test.xForwardedFor) + } + if test.xRealIP != "" { + request.Header.Add("X-Real-IP", test.xRealIP) + } + request.RemoteAddr = test.remoteAddr + + // Run Test + ip, ver := FindIP(request, test.validator) + + // Assertions + assert.Equal(t, test.expectedIP, ip, test.description+":ip") + assert.Equal(t, test.expectedVer, ver, test.description+":ver") + } +} + +type hardcodedResponseIPValidator struct { + response bool +} + +func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool { + return v.response +} diff --git a/util/iputil/parse.go b/util/iputil/parse.go new file mode 100644 index 00000000000..bcb00760e22 --- /dev/null +++ b/util/iputil/parse.go @@ -0,0 +1,27 @@ +package iputil + +import ( + "net" + "strings" +) + +// IPVersion is the numerical version of the IP address spec (4 or 6). +type IPVersion int + +// IP address versions. +const ( + IPvUnknown IPVersion = 0 + IPv4 IPVersion = 4 + IPv6 IPVersion = 6 +) + +// ParseIP parses v as an ip address returning the result and version, or nil and unknown if invalid. +func ParseIP(v string) (net.IP, IPVersion) { + if ip := net.ParseIP(v); ip != nil { + if strings.ContainsRune(v, ':') { + return ip, IPv6 + } + return ip, IPv4 + } + return nil, IPvUnknown +} diff --git a/util/iputil/parse_test.go b/util/iputil/parse_test.go new file mode 100644 index 00000000000..53431b0f2a9 --- /dev/null +++ b/util/iputil/parse_test.go @@ -0,0 +1,30 @@ +package iputil + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseIP(t *testing.T) { + testCases := []struct { + input string + expectedVer IPVersion + expectedIP net.IP + }{ + {"", IPvUnknown, nil}, + {"1.1.1.1", IPv4, net.IPv4(1, 1, 1, 1)}, + {"-1.-1.-1.-1", IPvUnknown, nil}, + {"256.256.256.256", IPvUnknown, nil}, + {"::ffff:1.1.1.1", IPv6, net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 1, 1, 1, 1}}, + {"0101::", IPv6, net.IP{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, + {"zzzz::", IPvUnknown, nil}, + } + + for _, test := range testCases { + ip, ver := ParseIP(test.input) + assert.Equal(t, test.expectedVer, ver) + assert.Equal(t, test.expectedIP, ip) + } +} diff --git a/util/iputil/validator.go b/util/iputil/validator.go new file mode 100644 index 00000000000..e4b822f0c7c --- /dev/null +++ b/util/iputil/validator.go @@ -0,0 +1,48 @@ +package iputil + +import ( + "net" +) + +// IPValidator is the interface for validating an ip address and version. +type IPValidator interface { + // IsValid returns true when an IP address is determined to be valid. + IsValid(net.IP, IPVersion) bool +} + +// PublicNetworkIPValidator validates an ip address which is not contained in the list of known private networks. +type PublicNetworkIPValidator struct { + IPv4PrivateNetworks []net.IPNet + IPv6PrivateNetworks []net.IPNet +} + +// IsValid implements the IPValidator interface. +func (v PublicNetworkIPValidator) IsValid(ip net.IP, ver IPVersion) bool { + var privateNetworks []net.IPNet + switch ver { + case IPv4: + privateNetworks = v.IPv4PrivateNetworks + case IPv6: + privateNetworks = v.IPv6PrivateNetworks + default: + return false + } + + for _, ipNet := range privateNetworks { + if ipNet.Contains(ip) { + return false + } + } + + return true +} + +// VersionIPValidator validates an ip address based on the desired ip version. +type VersionIPValidator struct { + Version IPVersion +} + +// IsValid implements the IPValidator interface. +func (v VersionIPValidator) IsValid(ip net.IP, ver IPVersion) bool { + return ver == v.Version +} diff --git a/util/iputil/validator_test.go b/util/iputil/validator_test.go new file mode 100644 index 00000000000..4419af22c04 --- /dev/null +++ b/util/iputil/validator_test.go @@ -0,0 +1,222 @@ +package iputil + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPublicNetworkIPValidator(t *testing.T) { + ipv4Network1 := net.IPNet{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)} + ipv4Network2 := net.IPNet{IP: net.ParseIP("2.0.0.0"), Mask: net.CIDRMask(8, 32)} + + ipv6Network1 := net.IPNet{IP: net.ParseIP("3300::"), Mask: net.CIDRMask(8, 128)} + ipv6Network2 := net.IPNet{IP: net.ParseIP("4400::"), Mask: net.CIDRMask(8, 128)} + + testCases := []struct { + description string + ip net.IP + ver IPVersion + ipv4PrivateNetworks []net.IPNet + ipv6PrivateNetworks []net.IPNet + expected bool + }{ + { + description: "IPv4 - Public - None", + ip: net.ParseIP("1.1.1.1"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv4 - Public - One", + ip: net.ParseIP("2.2.2.2"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv4 - Public - Many", + ip: net.ParseIP("3.3.3.3"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network2}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv4 - Private - One", + ip: net.ParseIP("1.1.1.1"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: false, + }, + { + description: "IPv4 - Private - Many", + ip: net.ParseIP("2.2.2.2"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network2}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: false, + }, + { + description: "IPv6 - Public - None", + ip: net.ParseIP("3333::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv6 - Public - One", + ip: net.ParseIP("4444::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1}, + expected: true, + }, + { + description: "IPv6 - Public - Many", + ip: net.ParseIP("5555::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: true, + }, + { + description: "IPv6 - Private - One", + ip: net.ParseIP("3333::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1}, + expected: false, + }, + { + description: "IPv6 - Private - Many", + ip: net.ParseIP("4444::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Unknown", + ip: net.ParseIP("3.3.3.3"), + ver: IPvUnknown, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Public - IPv4", + ip: net.ParseIP("3.3.3.3"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: true, + }, + { + description: "Mixed - Public - IPv6", + ip: net.ParseIP("5555::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: true, + }, + { + description: "Mixed - Private - IPv4", + ip: net.ParseIP("1.1.1.1"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Private - IPv6", + ip: net.ParseIP("3333::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Public - IPv6 Encoded IPv4", + ip: net.ParseIP("::FFFF:1.1.1.1"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)}}, + ipv6PrivateNetworks: []net.IPNet{{IP: net.ParseIP("::FFFF:2.0.0.0"), Mask: net.CIDRMask(108, 128)}}, + expected: true, + }, + { + description: "Mixed - Private - IPv6 Encoded IPv4", + ip: net.ParseIP("::FFFF:2.2.2.2"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)}}, + ipv6PrivateNetworks: []net.IPNet{{IP: net.ParseIP("::FFFF:2.0.0.0"), Mask: net.CIDRMask(108, 128)}}, + expected: false, + }, + } + + for _, test := range testCases { + requestValidation := PublicNetworkIPValidator{ + IPv4PrivateNetworks: test.ipv4PrivateNetworks, + IPv6PrivateNetworks: test.ipv6PrivateNetworks, + } + + result := requestValidation.IsValid(test.ip, test.ver) + + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestVersionIPValidator(t *testing.T) { + testCases := []struct { + description string + validatorVersion IPVersion + ip net.IP + ipVer IPVersion + expected bool + }{ + { + description: "IPv4", + validatorVersion: IPv4, + ip: net.ParseIP("1.1.1.1"), + ipVer: IPv4, + expected: true, + }, + { + description: "IPv4 - Given Unknown", + validatorVersion: IPv4, + ip: nil, + ipVer: IPvUnknown, + expected: false, + }, + { + description: "IPv6", + validatorVersion: IPv6, + ip: net.ParseIP("1111::"), + ipVer: IPv6, + expected: true, + }, + { + description: "IPv6 - Given Unknown", + validatorVersion: IPv6, + ip: nil, + ipVer: IPvUnknown, + expected: false, + }, + } + + for _, test := range testCases { + m := VersionIPValidator{ + Version: test.validatorVersion, + } + + result := m.IsValid(test.ip, test.ipVer) + + assert.Equal(t, test.expected, result) + } +} From 9b96f50afeb81a665668525ff1804d04c3b64ea2 Mon Sep 17 00:00:00 2001 From: SmartyAdman <59048845+SmartyAdman@users.noreply.github.com> Date: Mon, 29 Jun 2020 22:45:43 +0300 Subject: [PATCH 128/381] Change endpont address (#1370) * Adman adapter * add adman line to syner test * add tests * fix issues * fix web banner test * add 404 banner * fmt * rase coverage * del redundant files * change endpont address * change config endpoint Co-authored-by: Aiholkin --- adapters/adman/adman_test.go | 2 +- adapters/adman/admantest/exemplary/simple-banner.json | 6 +++--- adapters/adman/admantest/exemplary/simple-video.json | 2 +- adapters/adman/admantest/exemplary/simple-web-banner.json | 6 +++--- adapters/adman/admantest/supplemental/bad_response.json | 2 +- adapters/adman/admantest/supplemental/status-204.json | 2 +- adapters/adman/admantest/supplemental/status-404.json | 2 +- config/config.go | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/adapters/adman/adman_test.go b/adapters/adman/adman_test.go index da0f37e9a48..4ef88933759 100644 --- a/adapters/adman/adman_test.go +++ b/adapters/adman/adman_test.go @@ -7,6 +7,6 @@ import ( ) func TestJsonSamples(t *testing.T) { - admanAdapter := NewAdmanBidder("http://eu-ams-1.admanmedia.com/?c=o&m=ortb") + admanAdapter := NewAdmanBidder("http://pub.admanmedia.com/?c=o&m=ortb") adapterstest.RunJSONBidderTest(t, "admantest", admanAdapter) } diff --git a/adapters/adman/admantest/exemplary/simple-banner.json b/adapters/adman/admantest/exemplary/simple-banner.json index 41f76e00645..8bbe16aa0fe 100644 --- a/adapters/adman/admantest/exemplary/simple-banner.json +++ b/adapters/adman/admantest/exemplary/simple-banner.json @@ -37,7 +37,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "uri": "http://pub.admanmedia.com/?c=o&m=ortb", "body": { "id": "test-request-id", "imp": [ @@ -84,7 +84,7 @@ "id": "test_bid_id", "impid": "test-imp-id", "price": 0.27543, - "adm": "", + "adm": "", "cid": "test_cid", "crid": "test_crid", "dealid": "test_dealid", @@ -114,7 +114,7 @@ "id": "test_bid_id", "impid": "test-imp-id", "price": 0.27543, - "adm": "", + "adm": "", "cid": "test_cid", "crid": "test_crid", "dealid": "test_dealid", diff --git a/adapters/adman/admantest/exemplary/simple-video.json b/adapters/adman/admantest/exemplary/simple-video.json index d7fa82d274d..159a30a93e0 100644 --- a/adapters/adman/admantest/exemplary/simple-video.json +++ b/adapters/adman/admantest/exemplary/simple-video.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "uri": "http://pub.admanmedia.com/?c=o&m=ortb", "body": { "id": "test-request-id", "device": { diff --git a/adapters/adman/admantest/exemplary/simple-web-banner.json b/adapters/adman/admantest/exemplary/simple-web-banner.json index ce872bff52b..0ceaac7c6d5 100644 --- a/adapters/adman/admantest/exemplary/simple-web-banner.json +++ b/adapters/adman/admantest/exemplary/simple-web-banner.json @@ -36,7 +36,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "uri": "http://pub.admanmedia.com/?c=o&m=ortb", "body": { "id": "test-request-id", "imp": [ @@ -82,7 +82,7 @@ "id": "test_bid_id", "impid": "test-imp-id", "price": 0.27543, - "adm": "", + "adm": "", "cid": "test_cid", "crid": "test_crid", "dealid": "test_dealid", @@ -112,7 +112,7 @@ "id": "test_bid_id", "impid": "test-imp-id", "price": 0.27543, - "adm": "", + "adm": "", "cid": "test_cid", "crid": "test_crid", "dealid": "test_dealid", diff --git a/adapters/adman/admantest/supplemental/bad_response.json b/adapters/adman/admantest/supplemental/bad_response.json index 8c349297e73..d5a28c74256 100644 --- a/adapters/adman/admantest/supplemental/bad_response.json +++ b/adapters/adman/admantest/supplemental/bad_response.json @@ -35,7 +35,7 @@ }, "httpCalls": [{ "expectedRequest": { - "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "uri": "http://pub.admanmedia.com/?c=o&m=ortb", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/adman/admantest/supplemental/status-204.json b/adapters/adman/admantest/supplemental/status-204.json index 7f9a12dec29..72b28bffdcf 100644 --- a/adapters/adman/admantest/supplemental/status-204.json +++ b/adapters/adman/admantest/supplemental/status-204.json @@ -35,7 +35,7 @@ }, "httpCalls": [{ "expectedRequest": { - "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "uri": "http://pub.admanmedia.com/?c=o&m=ortb", "body": { "id": "test-request-id", "imp": [ diff --git a/adapters/adman/admantest/supplemental/status-404.json b/adapters/adman/admantest/supplemental/status-404.json index 560878342f0..043afbdc1dc 100644 --- a/adapters/adman/admantest/supplemental/status-404.json +++ b/adapters/adman/admantest/supplemental/status-404.json @@ -35,7 +35,7 @@ }, "httpCalls": [{ "expectedRequest": { - "uri": "http://eu-ams-1.admanmedia.com/?c=o&m=ortb", + "uri": "http://pub.admanmedia.com/?c=o&m=ortb", "body": { "id": "test-request-id", "imp": [ diff --git a/config/config.go b/config/config.go index 50cfbb1c170..16bab2996be 100755 --- a/config/config.go +++ b/config/config.go @@ -773,7 +773,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.adman.endpoint", "http://eu-ams-1.admanmedia.com/?c=o&m=ortb") + v.SetDefault("adapters.adman.endpoint", "http://pub.admanmedia.com/?c=o&m=ortb") 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") From 919d29ac3a6a29a55baa392d0dbbb88872ddd3c4 Mon Sep 17 00:00:00 2001 From: Florian Hartwig Date: Tue, 30 Jun 2020 16:29:43 +0200 Subject: [PATCH 129/381] Don't override test parameter (#1373) --- adapters/pubnative/pubnative.go | 1 - adapters/pubnative/pubnativetest/exemplary/simple-banner.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/pubnative/pubnative.go b/adapters/pubnative/pubnative.go index 4dc92920d8e..777ac4a05ed 100644 --- a/adapters/pubnative/pubnative.go +++ b/adapters/pubnative/pubnative.go @@ -83,7 +83,6 @@ func checkRequest(request *openrtb.BidRequest) error { } } - request.Test = 0 // don't forward test flag to PN adserver return nil } diff --git a/adapters/pubnative/pubnativetest/exemplary/simple-banner.json b/adapters/pubnative/pubnativetest/exemplary/simple-banner.json index 7c7d1319a50..5297cd3284d 100644 --- a/adapters/pubnative/pubnativetest/exemplary/simple-banner.json +++ b/adapters/pubnative/pubnativetest/exemplary/simple-banner.json @@ -111,6 +111,7 @@ }, "at": 1, "tmax": 200, + "test": 1, "source": { "tid": "283746293874293" }, From e430c629b140d263f0c38195862fae5661bb785f Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Tue, 30 Jun 2020 12:44:01 -0400 Subject: [PATCH 130/381] OpenX + Facebook Hardening (#1368) --- adapters/adapterstest/test_json.go | 6 +- .../exemplary/banner-app.json | 116 +++++++++++++++ .../{banner.json => banner-site.json} | 6 - .../exemplary/interstitial.json | 6 - .../exemplary/native-1.1.json | 6 - .../audienceNetworktest/exemplary/video.json | 6 - .../supplemental/banner-format-only.json | 6 - .../supplemental/invalid-adm.json | 103 ++++++++++++++ .../supplemental/invalid-interstitial.json | 40 ++++++ .../supplemental/missing-adm-bidid.json | 107 ++++++++++++++ .../supplemental/missing-adm.json | 106 ++++++++++++++ .../supplemental/multi-imp.json | 12 -- .../supplemental/no-bid-204.json | 6 - .../supplemental/no-imps.json | 22 +++ .../supplemental/server-error-500.json | 87 ++++++++++++ .../supplemental/split-placementId.json | 6 - adapters/audienceNetwork/facebook.go | 132 +++++++++--------- adapters/audienceNetwork/facebook_test.go | 31 +++- adapters/openx/openx.go | 6 +- exchange/adapter_map.go | 1 - util/maputil/maputil.go | 21 +++ util/maputil/maputil_test.go | 113 +++++++++++++++ 22 files changed, 813 insertions(+), 132 deletions(-) create mode 100644 adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json rename adapters/audienceNetwork/audienceNetworktest/exemplary/{banner.json => banner-site.json} (96%) create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json create mode 100644 util/maputil/maputil.go create mode 100644 util/maputil/maputil_test.go diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go index 8fdb9c5d9d6..a25a4f1905a 100644 --- a/adapters/adapterstest/test_json.go +++ b/adapters/adapterstest/test_json.go @@ -164,14 +164,16 @@ type httpRequest struct { } type httpResponse struct { - Status int `json:"status"` - Body json.RawMessage `json:"body"` + Status int `json:"status"` + Body json.RawMessage `json:"body"` + Headers http.Header `json:"headers"` } func (resp *httpResponse) ToResponseData(t *testing.T) *adapters.ResponseData { return &adapters.ResponseData{ StatusCode: resp.Status, Body: resp.Body, + Headers: resp.Headers, } } diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json new file mode 100644 index 00000000000..3ac62d90cd4 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json @@ -0,0 +1,116 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisherid": "123", + "placementid": "456" + } + } + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid", + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "987", + "impid": "test-imp-id", + "price": 1, + "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", + "adid": "987", + "crid": "987", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }, + "type": "banner" + }] + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json similarity index 96% rename from adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json rename to adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json index f5f92515e26..01bab3dfd71 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json @@ -62,12 +62,6 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json index bad228d5f18..9f563f11948 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json @@ -64,12 +64,6 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json index 9090d80d099..16bed344767 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json @@ -56,12 +56,6 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json index 22c62f8b821..5ece0f08530 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json @@ -66,12 +66,6 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json index 3edd6569258..5469fefbd65 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json @@ -64,12 +64,6 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json new file mode 100644 index 00000000000..f145f5fe4ce --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "adm": "malformed", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [{ + "value": "invalid character 'm' looking for beginning of value", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json new file mode 100644 index 00000000000..ad19d94c6e9 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json @@ -0,0 +1,40 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 15, + "maxduration": 30, + "protocols": [2, 3, 5, 6, 7, 8], + "linearity": 1, + "w": 940, + "h": 560 + }, + "instl": 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 + }, + "expectedMakeRequestsErrors": [{ + "value": "imp #test-imp-id: interstitial imps are only supported for banner", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json new file mode 100644 index 00000000000..b57c900104e --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json @@ -0,0 +1,107 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "adm": "{\"type\":\"ID\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [] + }], + "expectedMakeBidsErrors": [{ + "value": "bid 987 missing 'bid_id' in 'adm'", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json new file mode 100644 index 00000000000..23227aab959 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [] + }], + "expectedMakeBidsErrors": [{ + "value": "Bid 987 missing 'adm'", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json index 16e8aede10c..231c2826548 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json @@ -81,12 +81,6 @@ "tagid": "pub1_plmt1" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", @@ -158,12 +152,6 @@ "tagid": "pub2_plmt2" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json index bb192aad76f..45b35e05dd9 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json @@ -56,12 +56,6 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json new file mode 100644 index 00000000000..7420f7e8fb2 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json @@ -0,0 +1,22 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [], + "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 + }, + "expectedMakeRequestsErrors": [{ + "value": "No impressions provided", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json new file mode 100644 index 00000000000..7ff8886139a --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json @@ -0,0 +1,87 @@ +{ + "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" + }], + "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": { + "headers": { + "X-Fb-An-Errors": [ + "someError" + ]}, + "status": 500 + } + }], + "expectedMakeBidsErrors": [{ + "value": "Unexpected status code 500 with error message 'someError'", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json index 4c561c55276..34c1eccc58e 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json @@ -50,12 +50,6 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, "site": { "domain": "prebid.org", "page": "prebid.org", diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go index 9edb9a7d57e..f4091e4e23c 100644 --- a/adapters/audienceNetwork/facebook.go +++ b/adapters/audienceNetwork/facebook.go @@ -10,16 +10,17 @@ import ( "net/http" "strings" - "github.com/buger/jsonparser" - "github.com/golang/glog" - "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/util/maputil" + + "github.com/buger/jsonparser" + "github.com/golang/glog" + "github.com/mxmCherry/openrtb" ) type FacebookAdapter struct { - http *adapters.HTTPAdapter URI string nonSecureUri string platformID string @@ -35,15 +36,6 @@ var supportedBannerHeights = map[uint64]bool{ 250: true, } -// used for cookies and such -func (a *FacebookAdapter) Name() string { - return "audienceNetwork" -} - -func (a *FacebookAdapter) SkipNoCookies() bool { - return false -} - type facebookReqExt struct { PlatformID string `json:"platformid"` AuthID string `json:"authentication_id"` @@ -178,8 +170,10 @@ func (this *FacebookAdapter) modifyImp(out *openrtb.Imp) error { } } - switch impType { - case openrtb_ext.BidTypeBanner: + if impType == openrtb_ext.BidTypeBanner { + bannerCopy := *out.Banner + out.Banner = &bannerCopy + if out.Instl == 1 { out.Banner.W = openrtb.Uint64Ptr(0) out.Banner.H = openrtb.Uint64Ptr(0) @@ -212,7 +206,6 @@ func (this *FacebookAdapter) modifyImp(out *openrtb.Imp) error { /* This will get overwritten post-serialization */ out.Banner.W = openrtb.Uint64Ptr(0) out.Banner.Format = nil - break } return nil @@ -239,102 +232,106 @@ func (this *FacebookAdapter) extractPlacementAndPublisher(out *openrtb.Imp) (str } } - placementId := fbExt.PlacementId - publisherId := fbExt.PublisherId + placementID := fbExt.PlacementId + publisherID := fbExt.PublisherId // Support the legacy path with the caller was expected to pass in just placementId // which was an underscore concantenated string with the publisherId and placementId. // The new path for callers is to pass in the placementId and publisherId independently // and the below code will prefix the placementId that we pass to FAN with the publsiherId // so that we can abstract the implementation details from the caller - toks := strings.Split(placementId, "_") + toks := strings.Split(placementID, "_") if len(toks) == 1 { - if publisherId == "" { + if publisherID == "" { return "", "", &errortypes.BadInput{ Message: "Missing publisherId param", } } - return placementId, publisherId, nil + return placementID, publisherID, nil } else if len(toks) == 2 { - publisherId = toks[0] - placementId = toks[1] + publisherID = toks[0] + placementID = toks[1] } else { return "", "", &errortypes.BadInput{ - Message: fmt.Sprintf("Invalid placementId param '%s' and publisherId param '%s'", placementId, publisherId), + Message: fmt.Sprintf("Invalid placementId param '%s' and publisherId param '%s'", placementID, publisherID), } } - return placementId, publisherId, nil + return placementID, publisherID, nil } // XXX: This entire function is just a hack to get around mxmCherry 11.0.0 limitations, without // having to fork the library and maintain our own branch -func modifyImpCustom(json []byte, imp *openrtb.Imp) ([]byte, error) { +func modifyImpCustom(jsonData []byte, imp *openrtb.Imp) ([]byte, error) { impType, ok := resolveImpType(imp) if ok == false { panic("processing an invalid impression") } - var err error + var jsonMap map[string]interface{} + err := json.Unmarshal(jsonData, &jsonMap) + if err != nil { + return jsonData, err + } + + var impMap map[string]interface{} + if impSlice, ok := maputil.ReadEmbeddedSlice(jsonMap, "imp"); !ok { + return jsonData, errors.New("unable to find imp in json data") + } else if len(impSlice) == 0 { + return jsonData, errors.New("unable to find imp[0] in json data") + } else if impMap, ok = impSlice[0].(map[string]interface{}); !ok { + return jsonData, errors.New("unexpected type for imp[0] found in json data") + } switch impType { case openrtb_ext.BidTypeBanner: - // The current version of mxmCherry (11.0.0) repesents banner.w as unsigned - // integers, so setting a value of -1 is not possible which is why we have to do it + // The current version of mxmCherry (11.0.0) represents banner.w as an unsigned + // integer, so setting a value of -1 is not possible which is why we have to do it // post-serialization - - // The above does not apply to interstitial impressions - if imp.Instl == 1 { - break - } - - json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "banner", "w") - if err != nil { - return json, err + isInterstitial := imp.Instl == 1 + if !isInterstitial { + if bannerMap, ok := maputil.ReadEmbeddedMap(impMap, "banner"); ok { + bannerMap["w"] = json.RawMessage("-1") + } else { + return jsonData, errors.New("unable to find imp[0].banner in json data") + } } - break - case openrtb_ext.BidTypeVideo: // mxmCherry omits video.w/h if set to zero, so we need to force set those // fields to zero post-serialization for the time being - json, err = jsonparser.Set(json, []byte("0"), "imp", "[0]", "video", "w") - if err != nil { - return json, err + if videoMap, ok := maputil.ReadEmbeddedMap(impMap, "video"); ok { + videoMap["w"] = json.RawMessage("0") + videoMap["h"] = json.RawMessage("0") + } else { + return jsonData, errors.New("unable to find imp[0].video in json data") } - json, err = jsonparser.Set(json, []byte("0"), "imp", "[0]", "video", "h") - if err != nil { - return json, err + case openrtb_ext.BidTypeNative: + nativeMap, ok := maputil.ReadEmbeddedMap(impMap, "native") + if !ok { + return jsonData, errors.New("unable to find imp[0].video in json data") } - break - - case openrtb_ext.BidTypeNative: // Set w/h to -1 for native impressions based on the facebook native spec. // We have to set this post-serialization since the OpenRTB protocol doesn't - // actaully support w/h in the native object - json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "native", "w") - if err != nil { - return json, err - } - - json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "native", "h") - if err != nil { - return json, err - } + // actually support w/h in the native object + nativeMap["w"] = json.RawMessage("-1") + nativeMap["h"] = json.RawMessage("-1") // The FAN adserver does not expect the native request payload, all that information // is derived server side based on the placement ID. We need to remove these pieces of // information manually since OpenRTB (and thus mxmCherry) never omit native.request - json = jsonparser.Delete(json, "imp", "[0]", "native", "ver") - json = jsonparser.Delete(json, "imp", "[0]", "native", "request") - - break + delete(nativeMap, "ver") + delete(nativeMap, "request") } - return json, nil + if jsonReEncoded, err := json.Marshal(jsonMap); err == nil { + return jsonReEncoded, nil + } else { + return nil, fmt.Errorf("unable to encode json data (%v)", err) + } } func (this *FacebookAdapter) MakeBids(request *openrtb.BidRequest, adapterRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { @@ -430,7 +427,7 @@ func resolveImpType(imp *openrtb.Imp) (openrtb_ext.BidType, bool) { return openrtb_ext.BidTypeBanner, false } -func NewFacebookBidder(client *http.Client, platformID string, appSecret string) adapters.Bidder { +func NewFacebookBidder(platformID string, appSecret string) adapters.Bidder { if platformID == "" { glog.Errorf("No facebook partnerID specified. Calls to the Audience Network will fail. Did you set adapters.facebook.platform_id in the app config?") return &adapters.MisconfiguredBidder{ @@ -447,11 +444,8 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string) } } - a := &adapters.HTTPAdapter{Client: client} - return &FacebookAdapter{ - http: a, - URI: "https://an.facebook.com/placementbid.ortb", + URI: "https://an.facebook.com/placementbid.ortb", //for AB test nonSecureUri: "http://an.facebook.com/placementbid.ortb", platformID: platformID, diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go index 784a540e596..7f567d6137b 100644 --- a/adapters/audienceNetwork/facebook_test.go +++ b/adapters/audienceNetwork/facebook_test.go @@ -1,6 +1,7 @@ package audienceNetwork import ( + "errors" "testing" "time" @@ -40,14 +41,14 @@ type FacebookExt struct { } func TestJsonSamples(t *testing.T) { - adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret")) + adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder("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") + fba := NewFacebookBidder("test-platform-id", "test-app-secret") tb, ok := fba.(adapters.TimeoutBidder) if !ok { @@ -64,7 +65,7 @@ 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") + fba := NewFacebookBidder("test-platform-id", "test-app-secret") tb, ok := fba.(adapters.TimeoutBidder) if !ok { @@ -81,7 +82,7 @@ func TestMakeTimeoutNoticeBadRequest(t *testing.T) { req := adapters.RequestData{ Body: []byte(`{"imp":[{{"id":"1234"}}`), } - fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") + fba := NewFacebookBidder("test-platform-id", "test-app-secret") tb, ok := fba.(adapters.TimeoutBidder) if !ok { @@ -93,3 +94,25 @@ func TestMakeTimeoutNoticeBadRequest(t *testing.T) { assert.NotNil(t, err, "Facebook MakeTimeoutNotification() did not return an error") } + +func TestNewFacebookBidderMissingPlatformID(t *testing.T) { + result := NewFacebookBidder("", "anyAppSecret") + + expected := &adapters.MisconfiguredBidder{ + Name: "audienceNetwork", + Error: errors.New("Audience Network is not configured properly on this Prebid Server deploy. If you believe this should work, contact the company hosting the service and tell them to check their configuration."), + } + + assert.Equal(t, expected, result) +} + +func TestNewFacebookBidderMissingAppSecret(t *testing.T) { + result := NewFacebookBidder("anyPlatformID", "") + + expected := &adapters.MisconfiguredBidder{ + Name: "audienceNetwork", + Error: errors.New("Audience Network is not configured properly on this Prebid Server deploy. If you believe this should work, contact the company hosting the service and tell them to check their configuration."), + } + + assert.Equal(t, expected, result) +} diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go index 63297d0a4ee..ca88b18bdb8 100644 --- a/adapters/openx/openx.go +++ b/adapters/openx/openx.go @@ -143,11 +143,13 @@ func preprocess(imp *openrtb.Imp, reqExt *openxReqExt) error { } if imp.Video != nil { + videoCopy := *imp.Video if bidderExt.Prebid != nil && bidderExt.Prebid.IsRewardedInventory == 1 { - imp.Video.Ext = json.RawMessage(`{"rewarded":1}`) + videoCopy.Ext = json.RawMessage(`{"rewarded":1}`) } else { - imp.Video.Ext = nil + videoCopy.Ext = nil } + imp.Video = &videoCopy } return nil diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index c30bb0c622e..1f62d232233 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -122,7 +122,6 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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), diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go new file mode 100644 index 00000000000..0d1d7dbb51c --- /dev/null +++ b/util/maputil/maputil.go @@ -0,0 +1,21 @@ +package maputil + +// ReadEmbeddedMap reads element k from the map m as a map[string]interface{}. +func ReadEmbeddedMap(m map[string]interface{}, k string) (map[string]interface{}, bool) { + if v, ok := m[k]; ok { + vCasted, ok := v.(map[string]interface{}) + return vCasted, ok + } + + return nil, false +} + +// ReadEmbeddedSlice reads element k from the map m as a []interface{}. +func ReadEmbeddedSlice(m map[string]interface{}, k string) ([]interface{}, bool) { + if v, ok := m[k]; ok { + vCasted, ok := v.([]interface{}) + return vCasted, ok + } + + return nil, false +} diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go new file mode 100644 index 00000000000..2e6955cec9b --- /dev/null +++ b/util/maputil/maputil_test.go @@ -0,0 +1,113 @@ +package maputil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadEmbeddedMap(t *testing.T) { + testCases := []struct { + description string + value map[string]interface{} + key string + expectedMap map[string]interface{} + expectedOK bool + }{ + { + description: "Nil", + value: nil, + key: "", + expectedMap: nil, + expectedOK: false, + }, + { + description: "Empty", + value: map[string]interface{}{}, + key: "foo", + expectedMap: nil, + expectedOK: false, + }, + { + description: "Success", + value: map[string]interface{}{"foo": map[string]interface{}{"bar": 42}}, + key: "foo", + expectedMap: map[string]interface{}{"bar": 42}, + expectedOK: true, + }, + { + description: "Not Found", + value: map[string]interface{}{"foo": map[string]interface{}{"bar": 42}}, + key: "notFound", + expectedMap: nil, + expectedOK: false, + }, + { + description: "Wrong Type", + value: map[string]interface{}{"foo": 42}, + key: "foo", + expectedMap: nil, + expectedOK: false, + }, + } + + for _, test := range testCases { + resultMap, resultOK := ReadEmbeddedMap(test.value, test.key) + + assert.Equal(t, test.expectedMap, resultMap, test.description+":map") + assert.Equal(t, test.expectedOK, resultOK, test.description+":ok") + } +} + +func TestReadEmbeddedSlice(t *testing.T) { + testCases := []struct { + description string + value map[string]interface{} + key string + expectedSlice []interface{} + expectedOK bool + }{ + { + description: "Nil", + value: nil, + key: "", + expectedSlice: nil, + expectedOK: false, + }, + { + description: "Empty", + value: map[string]interface{}{}, + key: "foo", + expectedSlice: nil, + expectedOK: false, + }, + { + description: "Success", + value: map[string]interface{}{"foo": []interface{}{42}}, + key: "foo", + expectedSlice: []interface{}{42}, + expectedOK: true, + }, + { + description: "Not Found", + value: map[string]interface{}{"foo": []interface{}{42}}, + key: "notFound", + expectedSlice: nil, + expectedOK: false, + }, + { + description: "Wrong Type", + value: map[string]interface{}{"foo": 42}, + key: "foo", + expectedSlice: nil, + expectedOK: false, + }, + } + + for _, test := range testCases { + resultSlice, resultOK := ReadEmbeddedSlice(test.value, test.key) + + assert.Equal(t, test.expectedSlice, resultSlice, test.description+":slicd") + assert.Equal(t, test.expectedOK, resultOK, test.description+":ok") + } +} From 74af63b88166afd5bdcb2f388908a1f908855ff1 Mon Sep 17 00:00:00 2001 From: AaronColbyPrice <67345931+AaronColbyPrice@users.noreply.github.com> Date: Thu, 2 Jul 2020 07:58:50 -0700 Subject: [PATCH 131/381] Updating Conversant endpoint url (#1376) --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 16bab2996be..bb2b909f5de 100755 --- a/config/config.go +++ b/config/config.go @@ -791,7 +791,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25") 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") From 47c7a6b71d9f9cc8e2b66d218e5d896d0b614430 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:08:51 -0400 Subject: [PATCH 132/381] Metrics for TCF 2 adoption (#1360) --- exchange/exchange.go | 5 +++- exchange/utils.go | 20 +++++++++++++- exchange/utils_test.go | 6 ++--- go.sum | 3 +++ pbsmetrics/config/metrics.go | 11 ++++++++ pbsmetrics/go_metrics.go | 30 +++++++++++++++++++++ pbsmetrics/go_metrics_test.go | 4 +++ pbsmetrics/metrics.go | 30 +++++++++++++++++++++ pbsmetrics/metrics_mock.go | 5 ++++ pbsmetrics/prometheus/preload.go | 5 ++++ pbsmetrics/prometheus/prometheus.go | 27 ++++++++++++++++--- pbsmetrics/prometheus/prometheus_test.go | 34 ++++++++++++++++++++++-- pbsmetrics/prometheus/type_conversion.go | 9 +++++++ 13 files changed, 178 insertions(+), 11 deletions(-) diff --git a/exchange/exchange.go b/exchange/exchange.go index d7eab0f4475..174a0b3e0fc 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -104,8 +104,11 @@ 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.privacyConfig) + cleanRequests, aliases, cleanMetrics, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) + if cleanMetrics.gdprEnforced { + e.me.RecordTCFReq(pbsmetrics.TCFVersionToValue(cleanMetrics.gdprTcfVersion)) + } // List of bidders we have requests for. liveAdapters := listBiddersWithRequests(cleanRequests) diff --git a/exchange/utils.go b/exchange/utils.go index 54122d13c09..96c00ec0e36 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -6,6 +6,8 @@ import ( "fmt" "math/rand" + "github.com/prebid/go-gdpr/vendorconsent" + "github.com/buger/jsonparser" "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/config" @@ -17,6 +19,15 @@ import ( "github.com/prebid/prebid-server/privacy/lmt" ) +// cleanMetrics is a struct to export any metrics data resulting from cleanOpenRTBRequests(). It starts with just +// the TCF version, but made a struct to facilitate future expansion +type cleanMetrics struct { + // A simple flag if GDPR is being enforced on this request. + gdprEnforced bool + // a zero value means a missing or invalid GDPR string + gdprTcfVersion int +} + // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is: // // 1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder. @@ -29,7 +40,7 @@ func cleanOpenRTBRequests(ctx context.Context, labels pbsmetrics.Labels, gDPR gdpr.Permissions, usersyncIfAmbiguous bool, - privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) { + privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, cleanMetrics cleanMetrics, errs []error) { impsByBidder, errs := splitImps(orig.Imp) if len(errs) > 0 { @@ -64,6 +75,13 @@ func cleanOpenRTBRequests(ctx context.Context, LMT: lmtPolicy.ShouldEnforce(), } + if gdpr == 1 { + cleanMetrics.gdprEnforced = true + parsedConsent, err := vendorconsent.ParseString(consent) + if err == nil { + cleanMetrics.gdprTcfVersion = int(parsedConsent.Version()) + } + } // bidder level privacy policies for bidder, bidReq := range requestsByBidder { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 4dad3f54648..e50d0f777f0 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -80,7 +80,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { } for _, test := range testCases { - reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + 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 { @@ -120,7 +120,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { }, } - results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + 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) @@ -182,7 +182,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { }, } - results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + 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) diff --git a/go.sum b/go.sum index 5d941b89e90..35b2b76591d 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtW github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98= 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_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= +github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54= @@ -126,6 +128,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9 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/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= 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= diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index 4e249785ba6..3d105dead44 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -195,6 +195,13 @@ func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) { } } +// RecordTCFReq across all engines +func (me *MultiMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) { + for _, thisME := range *me { + thisME.RecordTCFReq(version) + } +} + // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests) type DummyMetricsEngine struct{} @@ -273,3 +280,7 @@ func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType p // RecordTimeoutNotice as a noop func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) { } + +// RecordReq as a noop +func (me *DummyMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) { +} diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go index 1ced4d57269..73eb30a1504 100644 --- a/pbsmetrics/go_metrics.go +++ b/pbsmetrics/go_metrics.go @@ -48,9 +48,13 @@ type Metrics struct { ImpsTypeAudio metrics.Meter ImpsTypeNative metrics.Meter + // Notification timeout metrics TimeoutNotificationSuccess metrics.Meter TimeoutNotificationFailure metrics.Meter + // TCF adaption metrics + TCFReqVersion map[TCFVersionValue]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 @@ -137,6 +141,8 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa TimeoutNotificationSuccess: blankMeter, TimeoutNotificationFailure: blankMeter, + TCFReqVersion: make(map[TCFVersionValue]metrics.Meter, len(TCFVersions())), + AdapterMetrics: make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)), accountMetrics: make(map[string]*accountMetrics), MetricsDisabled: disableMetrics, @@ -154,6 +160,15 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa } } + for _, c := range CacheResults() { + newMetrics.StoredReqCacheMeter[c] = blankMeter + newMetrics.StoredImpCacheMeter[c] = blankMeter + } + + for _, v := range TCFVersions() { + newMetrics.TCFReqVersion[v] = blankMeter + } + //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) @@ -218,6 +233,11 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry) newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry) + + for _, version := range TCFVersions() { + newMetrics.TCFReqVersion[version] = metrics.GetOrRegisterMeter(fmt.Sprintf("privacy.request.tcf.%s", string(version)), registry) + } + return newMetrics } @@ -562,6 +582,16 @@ func (me *Metrics) RecordTimeoutNotice(success bool) { return } +func (me *Metrics) RecordTCFReq(version TCFVersionValue) { + met, ok := me.TCFReqVersion[version] + if ok { + met.Mark(1) + } else { + me.TCFReqVersion[TCFVersionErr].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 25f75e77758..6d9eaf9f0e9 100644 --- a/pbsmetrics/go_metrics_test.go +++ b/pbsmetrics/go_metrics_test.go @@ -56,6 +56,10 @@ func TestNewMetrics(t *testing.T) { ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess) ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure) + ensureContains(t, registry, "privacy.request.tcf.v1", m.TCFReqVersion[TCFVersionV1]) + ensureContains(t, registry, "privacy.request.tcf.v2", m.TCFReqVersion[TCFVersionV2]) + ensureContains(t, registry, "privacy.request.tcf.err", m.TCFReqVersion[TCFVersionErr]) + } func TestRecordBidType(t *testing.T) { diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index e65ba313338..0e94fe71e90 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -248,6 +248,35 @@ func RequestActions() []RequestAction { } } +// TCFVersionValue : The possible values for TCF versions +type TCFVersionValue string + +const ( + TCFVersionErr TCFVersionValue = "err" + TCFVersionV1 TCFVersionValue = "v1" + TCFVersionV2 TCFVersionValue = "v2" +) + +// TCFVersions rtuens the possible values for the TCF version +func TCFVersions() []TCFVersionValue { + return []TCFVersionValue{ + TCFVersionErr, + TCFVersionV1, + TCFVersionV2, + } +} + +// TCFVersionToValue takes an integer TCF version and returns the corresponding TCFVersionValue +func TCFVersionToValue(version int) TCFVersionValue { + switch { + case version == 1: + return TCFVersionV1 + case version == 2: + return TCFVersionV2 + } + return TCFVersionErr +} + // 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 number of incoming requests. The remaining 5 fire off per outgoing @@ -276,4 +305,5 @@ type MetricsEngine interface { RecordPrebidCacheRequestTime(success bool, length time.Duration) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) RecordTimeoutNotice(sucess bool) + RecordTCFReq(version TCFVersionValue) } diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go index 482cbf24fae..a6d36a72401 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -106,3 +106,8 @@ func (me *MetricsEngineMock) RecordRequestQueueTime(success bool, requestType Re func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) { me.Called(success) } + +// RecordTCFReq mock +func (me *MetricsEngineMock) RecordTCFReq(version TCFVersionValue) { + me.Called(version) +} diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go index 11e6bdc14d8..19f4f225af9 100644 --- a/pbsmetrics/prometheus/preload.go +++ b/pbsmetrics/prometheus/preload.go @@ -99,6 +99,11 @@ func preloadLabelValues(m *Metrics) { requestTypeLabel: {string(pbsmetrics.ReqTypeVideo)}, requestStatusLabel: {requestSuccessLabel, requestRejectLabel}, }) + + preloadLabelValuesForCounter(m.tcfVersion, map[string][]string{ + versionLabel: tcfVersionsAsString(), + sourceLabel: {string(sourceRequest)}, + }) } func preloadLabelValuesForCounter(counter *prometheus.CounterVec, labelsWithValues map[string][]string) { diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index e385b044981..bf854746fd2 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -28,7 +28,8 @@ type Metrics struct { requestsWithoutCookie *prometheus.CounterVec storedImpressionsCacheResult *prometheus.CounterVec storedRequestCacheResult *prometheus.CounterVec - timeout_notifications *prometheus.CounterVec + timeoutNotifications *prometheus.CounterVec + tcfVersion *prometheus.CounterVec // Adapter Metrics adapterBids *prometheus.CounterVec @@ -63,6 +64,7 @@ const ( requestStatusLabel = "request_status" requestTypeLabel = "request_type" successLabel = "success" + versionLabel = "version" ) const ( @@ -85,6 +87,11 @@ const ( requestFailed = "failed" ) +const ( + sourceLabel = "source" + sourceRequest = "request" +) + // 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} @@ -153,11 +160,16 @@ 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, + metrics.timeoutNotifications = newCounter(cfg, metrics.Registry, "timeout_notification", "Count of timeout notifications triggered, and if they were successfully sent.", []string{successLabel}) + metrics.tcfVersion = newCounter(cfg, metrics.Registry, + "privacy_tcf", + "Count of TCF versions for requests where GDPR was enforced.", + []string{versionLabel, sourceLabel}) + metrics.adapterBids = newCounter(cfg, metrics.Registry, "adapter_bids", "Count of bids labeled by adapter and markup delivery type (adm or nurl).", @@ -412,12 +424,19 @@ func (m *Metrics) RecordRequestQueueTime(success bool, requestType pbsmetrics.Re func (m *Metrics) RecordTimeoutNotice(success bool) { if success { - m.timeout_notifications.With(prometheus.Labels{ + m.timeoutNotifications.With(prometheus.Labels{ successLabel: requestSuccessful, }).Inc() } else { - m.timeout_notifications.With(prometheus.Labels{ + m.timeoutNotifications.With(prometheus.Labels{ successLabel: requestFailed, }).Inc() } } + +func (m *Metrics) RecordTCFReq(version pbsmetrics.TCFVersionValue) { + m.tcfVersion.With(prometheus.Labels{ + versionLabel: string(version), + sourceLabel: sourceRequest, + }).Inc() +} diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index 24c50492139..03daff0d56b 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -930,13 +930,13 @@ func TestTimeoutNotifications(t *testing.T) { m.RecordTimeoutNotice(true) m.RecordTimeoutNotice(false) - assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeout_notifications, + assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeoutNotifications, float64(2), prometheus.Labels{ successLabel: requestSuccessful, }) - assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeout_notifications, + assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeoutNotifications, float64(1), prometheus.Labels{ successLabel: requestFailed, @@ -944,6 +944,36 @@ func TestTimeoutNotifications(t *testing.T) { } +func TestTCFMetrics(t *testing.T) { + m := createMetricsForTesting() + + m.RecordTCFReq(pbsmetrics.TCFVersionToValue(0)) + m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1)) + m.RecordTCFReq(pbsmetrics.TCFVersionToValue(2)) + m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1)) + + assertCounterVecValue(t, "", "privacy_tcf:err", m.tcfVersion, + float64(1), + prometheus.Labels{ + versionLabel: "err", + sourceLabel: sourceRequest, + }) + + assertCounterVecValue(t, "", "privacy_tcf:v1", m.tcfVersion, + float64(2), + prometheus.Labels{ + versionLabel: "v1", + sourceLabel: sourceRequest, + }) + + assertCounterVecValue(t, "", "privacy_tcf:v2", m.tcfVersion, + float64(1), + prometheus.Labels{ + versionLabel: "v2", + sourceLabel: sourceRequest, + }) +} + func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) { m := dto.Metric{} counter.Write(&m) diff --git a/pbsmetrics/prometheus/type_conversion.go b/pbsmetrics/prometheus/type_conversion.go index 8294ede0617..55a7092ed6d 100644 --- a/pbsmetrics/prometheus/type_conversion.go +++ b/pbsmetrics/prometheus/type_conversion.go @@ -76,3 +76,12 @@ func requestTypesAsString() []string { } return valuesAsString } + +func tcfVersionsAsString() []string { + values := pbsmetrics.TCFVersions() + valuesAsString := make([]string, len(values)) + for i, v := range values { + valuesAsString[i] = string(v) + } + return valuesAsString +} From 1d276d5d9ad24dddee29d2cd5dc709645e572eb5 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:19:58 -0400 Subject: [PATCH 133/381] =?UTF-8?q?Fall=20back=20to=20constant=20rates=20w?= =?UTF-8?q?hen=20the=20currency=20rates=20endpoint=20i=E2=80=A6=20(#1364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.go | 2 + currencies/rate_converter.go | 77 ++++++--- currencies/rate_converter_test.go | 265 +++++++++++++++++++++--------- exchange/bidder_test.go | 2 + main.go | 4 +- 5 files changed, 248 insertions(+), 102 deletions(-) diff --git a/config/config.go b/config/config.go index bb2b909f5de..cc1d4a0ab4e 100755 --- a/config/config.go +++ b/config/config.go @@ -215,6 +215,7 @@ type Analytics struct { type CurrencyConverter struct { FetchURL string `mapstructure:"fetch_url"` FetchIntervalSeconds int `mapstructure:"fetch_interval_seconds"` + StaleRatesSeconds int `mapstructure:"stale_rates_seconds"` } func (cfg *CurrencyConverter) validate(errs configErrors) configErrors { @@ -866,6 +867,7 @@ func SetupViper(v *viper.Viper, filename string) { 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("currency_converter.stale_rates_seconds", 0) v.SetDefault("default_request.type", "") v.SetDefault("default_request.file.name", "") v.SetDefault("default_request.alias_info", false) diff --git a/currencies/rate_converter.go b/currencies/rate_converter.go index 6c6ed172652..d22f347b17c 100644 --- a/currencies/rate_converter.go +++ b/currencies/rate_converter.go @@ -12,14 +12,15 @@ import ( // RateConverter holds the currencies conversion rates dictionary type RateConverter struct { - httpClient httpClient - done chan bool - updateNotifier chan<- int - fetchingInterval time.Duration - syncSourceURL string - rates atomic.Value // Should only hold Rates struct - lastUpdated atomic.Value // Should only hold time.Time - constantRates Conversions + httpClient httpClient + done chan bool + updateNotifier chan<- int + fetchingInterval time.Duration + staleRatesThreshold time.Duration + syncSourceURL string + rates atomic.Value // Should only hold Rates struct + lastUpdated atomic.Value // Should only hold time.Time + constantRates Conversions } // NewRateConverter returns a new RateConverter @@ -27,11 +28,13 @@ func NewRateConverter( httpClient httpClient, syncSourceURL string, fetchingInterval time.Duration, + staleRatesThreshold time.Duration, ) *RateConverter { return NewRateConverterWithNotifier( httpClient, syncSourceURL, fetchingInterval, + staleRatesThreshold, nil, // no notifier channel specified, won't send any notifications ) } @@ -40,7 +43,7 @@ func NewRateConverter( // By default there will be no currencies conversions done. // `currencies.ConstantRate` will be used. func NewRateConverterDefault() *RateConverter { - return NewRateConverter(&http.Client{}, "", time.Duration(0)) + return NewRateConverter(&http.Client{}, "", time.Duration(0), time.Duration(0)) } // NewRateConverterWithNotifier returns a new RateConverter @@ -51,22 +54,24 @@ func NewRateConverterWithNotifier( httpClient httpClient, syncSourceURL string, fetchingInterval time.Duration, + staleRatesThreshold time.Duration, updateNotifier chan<- int, ) *RateConverter { rc := &RateConverter{ - httpClient: httpClient, - done: make(chan bool), - updateNotifier: updateNotifier, - fetchingInterval: fetchingInterval, - syncSourceURL: syncSourceURL, - rates: atomic.Value{}, - lastUpdated: atomic.Value{}, + httpClient: httpClient, + done: make(chan bool), + updateNotifier: updateNotifier, + fetchingInterval: fetchingInterval, + staleRatesThreshold: staleRatesThreshold, + syncSourceURL: syncSourceURL, + rates: atomic.Value{}, + lastUpdated: atomic.Value{}, + constantRates: NewConstantRates(), } // In case host do not want to support currency lookup // we just stop here and do nothing if rc.fetchingInterval == time.Duration(0) { - rc.constantRates = NewConstantRates() return rc } @@ -111,7 +116,12 @@ func (rc *RateConverter) Update() error { rc.rates.Store(rates) rc.lastUpdated.Store(time.Now()) } else { - glog.Errorf("Error updating conversion rates: %v", err) + if rc.CheckStaleRates() { + rc.ClearRates() + glog.Errorf("Error updating conversion rates, falling back to constant rates: %v", err) + } else { + glog.Errorf("Error updating conversion rates: %v", err) + } } return err @@ -160,14 +170,33 @@ func (rc *RateConverter) LastUpdated() time.Time { // Rates returns current conversions rates func (rc *RateConverter) Rates() Conversions { - if rc.constantRates != nil { - // Converter is not active, returning the constant rates - return rc.constantRates - } - if rates := rc.rates.Load(); rates != nil { + // atomic.Value field rates is an empty interface and will be of type *Rates the first time rates are stored + // or nil if the rates have never been stored + if rates := rc.rates.Load(); rates != (*Rates)(nil) && rates != nil { return rates.(*Rates) } - return nil + return rc.constantRates +} + +// ClearRates sets the rates to nil +func (rc *RateConverter) ClearRates() { + // atomic.Value field rates must be of type *Rates so we cast nil to that type + rc.rates.Store((*Rates)(nil)) +} + +// CheckStaleRates checks if loaded third party conversion rates are stale +func (rc *RateConverter) CheckStaleRates() bool { + if rc.staleRatesThreshold <= 0 { + return false + } + currentTime := time.Now().UTC() + if lastUpdated := rc.lastUpdated.Load(); lastUpdated != nil { + delta := currentTime.Sub(lastUpdated.(time.Time).UTC()) + if delta.Seconds() > rc.staleRatesThreshold.Seconds() { + return true + } + } + return false } // GetInfo returns setup information about the converter diff --git a/currencies/rate_converter_test.go b/currencies/rate_converter_test.go index cb5e2a0be54..d717d1a3f9c 100644 --- a/currencies/rate_converter_test.go +++ b/currencies/rate_converter_test.go @@ -13,6 +13,20 @@ import ( "github.com/stretchr/testify/assert" ) +func getMockRates() []byte { + return []byte(`{ + "dataAsOf":"2018-09-12", + "conversions":{ + "USD":{ + "GBP":0.77208 + }, + "GBP":{ + "USD":1.2952 + } + } + }`) +} + func TestFetch_Success(t *testing.T) { // Setup: @@ -21,19 +35,7 @@ func TestFetch_Success(t *testing.T) { func(rw http.ResponseWriter, req *http.Request) { calledURLs = append(calledURLs, req.RequestURI) rw.WriteHeader(http.StatusOK) - rw.Write([]byte( - `{ - "dataAsOf":"2018-09-12", - "conversions":{ - "USD":{ - "GBP":0.77208 - }, - "GBP":{ - "USD":1.2952 - } - } - }`, - )) + rw.Write([]byte(getMockRates())) }), ) @@ -57,6 +59,7 @@ func TestFetch_Success(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(24)*time.Hour, + time.Duration(24)*time.Hour, ) // Verify: @@ -87,12 +90,13 @@ func TestFetch_Fail404(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(24)*time.Hour, + time.Duration(24)*time.Hour, ) // Verify: 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } @@ -114,12 +118,13 @@ func TestFetch_FailErrorHttpClient(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(24)*time.Hour, + time.Duration(24)*time.Hour, ) // Verify: 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } @@ -132,11 +137,12 @@ func TestFetch_FailBadSyncURL(t *testing.T) { &http.Client{}, "justaweirdurl", time.Duration(24)*time.Hour, + time.Duration(24)*time.Hour, ) // 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } @@ -172,12 +178,13 @@ func TestFetch_FailBadJSON(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(24)*time.Hour, + time.Duration(24)*time.Hour, ) // Verify: 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } @@ -200,12 +207,13 @@ func TestFetch_InvalidRemoteResponseContent(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(24)*time.Hour, + time.Duration(24)*time.Hour, ) // Verify: 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } @@ -215,19 +223,7 @@ func TestInit(t *testing.T) { mockedHttpServer := httptest.NewServer(http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte( - `{ - "dataAsOf":"2018-09-12", - "conversions":{ - "USD":{ - "GBP":0.77208 - }, - "GBP":{ - "USD":1.2952 - } - } - }`, - )) + rw.Write([]byte(getMockRates())) }), ) @@ -239,6 +235,7 @@ func TestInit(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(100)*time.Millisecond, + time.Duration(24)*time.Hour, ticks, ) @@ -266,10 +263,8 @@ func TestInit(t *testing.T) { assert.False(t, intervalDiff > float64(errorMargin*100), "Interval between ticks should be: %d but was: %d", expectedIntervalDuration, intervalDuration) } - assert.NotNil(t, currencyConverter.Rates(), "Rates shouldn't be nil") 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.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones") assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") if ticksCount == expectedTicks { @@ -287,19 +282,7 @@ func TestStop(t *testing.T) { func(rw http.ResponseWriter, req *http.Request) { calledURLs = append(calledURLs, req.RequestURI) rw.WriteHeader(http.StatusOK) - rw.Write([]byte( - `{ - "dataAsOf":"2018-09-12", - "conversions":{ - "USD":{ - "GBP":0.77208 - }, - "GBP":{ - "USD":1.2952 - } - } - }`, - )) + rw.Write([]byte(getMockRates())) }), ) @@ -310,6 +293,7 @@ func TestStop(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(100)*time.Millisecond, + time.Duration(24)*time.Hour, ticks, ) @@ -337,19 +321,7 @@ func TestInitWithZeroDuration(t *testing.T) { func(rw http.ResponseWriter, req *http.Request) { calledURLs = append(calledURLs, req.RequestURI) rw.WriteHeader(http.StatusOK) - rw.Write([]byte( - `{ - "dataAsOf":"2018-09-12", - "conversions":{ - "USD":{ - "GBP":0.77208 - }, - "GBP":{ - "USD":1.2952 - } - } - }`, - )) + rw.Write([]byte(getMockRates())) }), ) @@ -358,6 +330,7 @@ func TestInitWithZeroDuration(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(0), + time.Duration(24)*time.Hour, ) // Verify: @@ -366,8 +339,7 @@ func TestInitWithZeroDuration(t *testing.T) { assert.Equal(t, 0, len(calledURLs), "sync URL shouldn't have been called but was called %d times", 0, len(calledURLs)) 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") } @@ -393,19 +365,7 @@ func TestRates(t *testing.T) { mockedHttpServer := httptest.NewServer(http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte( - `{ - "dataAsOf":"2018-09-12", - "conversions":{ - "USD":{ - "GBP":0.77208 - }, - "GBP":{ - "USD":1.2952 - } - } - }`, - )) + rw.Write([]byte(getMockRates())) }), ) @@ -415,6 +375,7 @@ func TestRates(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(100)*time.Millisecond, + time.Duration(24)*time.Hour, ticks, ) rates := currencyConverter.Rates() @@ -456,12 +417,161 @@ func TestRates_EmptyRates(t *testing.T) { &http.Client{}, mockedHttpServer.URL, time.Duration(100)*time.Millisecond, + time.Duration(24)*time.Hour, ) defer currencyConverter.StopPeriodicFetching() - rates := currencyConverter.Rates() // Verify: - assert.Nil(t, rates, "rates should be nil") + assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") +} + +func TestSelectRatesBasedOnStaleness(t *testing.T) { + calledURLs := []string{} + callCnt := 0 + mockedHttpServer := httptest.NewServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + calledURLs = append(calledURLs, req.RequestURI) + if callCnt == 0 || callCnt >= 5 { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(getMockRates())) + } else { + rw.WriteHeader(http.StatusNotFound) + } + callCnt++ + }), + ) + + defer mockedHttpServer.Close() + + expectedRates := ¤cies.Rates{ + DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), + Conversions: map[string]map[string]float64{ + "USD": { + "GBP": 0.77208, + }, + "GBP": { + "USD": 1.2952, + }, + }, + } + + // Execute: + currencyConverter := currencies.NewRateConverter( + &http.Client{}, + mockedHttpServer.URL, + time.Duration(100)*time.Millisecond, + time.Duration(200)*time.Millisecond, + ) + + // Verify: + // Rates are valid at t=0, then invalid for 500ms before being valid again + assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + + time.Sleep(100 * time.Millisecond) + // Rates have been invalid for ~100ms, rates not stale yet + assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + + time.Sleep(200 * time.Millisecond) + // Rates have been invalid for ~300ms, rates are stale + assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") + + time.Sleep(300 * time.Millisecond) + // Rates have been valid again for ~100ms + assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") +} + +func TestUseConstantRatesUntilFetchIsSuccessful(t *testing.T) { + callCnt := 0 + mockedHttpServer := httptest.NewServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + if callCnt >= 5 { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(getMockRates())) + } else { + rw.WriteHeader(http.StatusNotFound) + } + callCnt++ + }), + ) + + defer mockedHttpServer.Close() + + expectedRates := ¤cies.Rates{ + DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), + Conversions: map[string]map[string]float64{ + "USD": { + "GBP": 0.77208, + }, + "GBP": { + "USD": 1.2952, + }, + }, + } + + // Execute: + currencyConverter := currencies.NewRateConverter( + &http.Client{}, + mockedHttpServer.URL, + time.Duration(100)*time.Millisecond, + time.Duration(1)*time.Second, + ) + + // Verify: + // Rates are invalid at t=0 and remain invalid until 500ms have elapsed + assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") + + time.Sleep(400 * time.Millisecond) + // Rates have been invalid for ~400ms + assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") + + time.Sleep(200 * time.Millisecond) + // Rates have been valid for ~100ms + assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") +} + +func TestRatesAreNeverStale(t *testing.T) { + callCnt := 0 + mockedHttpServer := httptest.NewServer(http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + if callCnt == 0 { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(getMockRates())) + } else { + rw.WriteHeader(http.StatusNotFound) + } + callCnt++ + }), + ) + + defer mockedHttpServer.Close() + + expectedRates := ¤cies.Rates{ + DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), + Conversions: map[string]map[string]float64{ + "USD": { + "GBP": 0.77208, + }, + "GBP": { + "USD": 1.2952, + }, + }, + } + + // Execute: + currencyConverter := currencies.NewRateConverter( + &http.Client{}, + mockedHttpServer.URL, + time.Duration(100)*time.Millisecond, + time.Duration(0)*time.Millisecond, + ) + + // Verify: + // Rates are valid at t=0 and are then invalid at 100ms + assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + + time.Sleep(500 * time.Millisecond) + // Rates have been invalid for ~400ms + assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") } func TestRace(t *testing.T) { @@ -495,6 +605,7 @@ func TestRace(t *testing.T) { mockedHttpClient, "currency.fake.com", time.Duration(10)*time.Millisecond, + time.Duration(24)*time.Hour, ) defer currencyConverter.StopPeriodicFetching() diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index fff397f0084..b776715adaf 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -517,6 +517,7 @@ func TestMultiCurrencies(t *testing.T) { &http.Client{}, mockedHTTPServer.URL, time.Duration(10)*time.Second, + time.Duration(24)*time.Hour, ) seatBid, errs := bidder.requestBid( context.Background(), @@ -831,6 +832,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { &http.Client{}, mockedHTTPServer.URL, time.Duration(10)*time.Second, + time.Duration(24)*time.Hour, ) seatBid, errs := bidder.requestBid( context.Background(), diff --git a/main.go b/main.go index d6ba430f059..9a835f42a4c 100644 --- a/main.go +++ b/main.go @@ -52,7 +52,9 @@ func loadConfig() (*config.Configuration, error) { func serve(revision string, cfg *config.Configuration) error { fetchingInterval := time.Duration(cfg.CurrencyConverter.FetchIntervalSeconds) * time.Second - currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL, fetchingInterval) + staleRatesThreshold := time.Duration(cfg.CurrencyConverter.StaleRatesSeconds) * time.Second + currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL, + fetchingInterval, staleRatesThreshold) r, err := router.New(cfg, currencyConverter) if err != nil { From 33f36b6be002e8993fe66be8985c6e95938512fb Mon Sep 17 00:00:00 2001 From: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Date: Mon, 6 Jul 2020 17:12:07 +0300 Subject: [PATCH 134/381] TheMediaGrid: added app type support (#1377) --- static/bidder-info/grid.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/bidder-info/grid.yaml b/static/bidder-info/grid.yaml index 9594830c0d0..325421a2c05 100644 --- a/static/bidder-info/grid.yaml +++ b/static/bidder-info/grid.yaml @@ -1,7 +1,11 @@ maintainer: email: "grid-tech@themediagrid.com" capabilities: - site: + app: mediaTypes: - banner - video + site: + mediaTypes: + - banner + - video \ No newline at end of file From 0f2dc5f7bec5a13a65792dbc33c4fa759f2b6899 Mon Sep 17 00:00:00 2001 From: Jurij Sinickij Date: Wed, 8 Jul 2020 01:39:27 +0300 Subject: [PATCH 135/381] user.ext.eids support in adform adapter (#1381) --- adapters/adform/adform.go | 31 +++++++++++++++++++++++++++++++ adapters/adform/adform_test.go | 31 ++++++++++++++++++++++++++++++- openrtb_ext/user.go | 5 +++-- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/adapters/adform/adform.go b/adapters/adform/adform.go index 3aeea62ebde..69f1c12f073 100644 --- a/adapters/adform/adform.go +++ b/adapters/adform/adform.go @@ -42,6 +42,7 @@ type adformRequest struct { consent string digitrust *adformDigitrust currency string + eids string } type adformDigitrust struct { @@ -279,6 +280,9 @@ func (r *adformRequest) buildAdformUrl(a *AdformAdapter) string { parameters.Add("gdpr", r.gdprApplies) parameters.Add("gdpr_consent", r.consent) + if r.eids != "" { + parameters.Add("eids", r.eids) + } URL := *a.URL URL.RawQuery = parameters.Encode() @@ -465,6 +469,7 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro } } + eids := "" consent := "" var digitrustData *openrtb_ext.ExtUserDigiTrust if request.User != nil { @@ -472,6 +477,7 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro if err := json.Unmarshal(request.User.Ext, &extUser); err == nil { consent = extUser.Consent digitrustData = extUser.DigiTrust + eids = encodeEids(extUser.Eids) } } @@ -513,9 +519,34 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro consent: consent, digitrust: digitrust, currency: requestCurrency, + eids: eids, }, errors } +func encodeEids(eids []openrtb_ext.ExtUserEid) string { + if eids == nil { + return "" + } + + eidsMap := make(map[string]map[string][]int) + for _, eid := range eids { + _, ok := eidsMap[eid.Source] + if !ok { + eidsMap[eid.Source] = make(map[string][]int) + } + for _, uid := range eid.Uids { + eidsMap[eid.Source][uid.ID] = append(eidsMap[eid.Source][uid.ID], uid.Atype) + } + } + + encodedEids := "" + if eidsString, err := json.Marshal(eidsMap); err == nil { + encodedEids = base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(eidsString) + } + + return encodedEids +} + func getIPSafely(device *openrtb.Device) string { if device == nil { return "" diff --git a/adapters/adform/adform_test.go b/adapters/adform/adform_test.go index 63646f5f7f5..2fca7d1722d 100644 --- a/adapters/adform/adform_test.go +++ b/adapters/adform/adform_test.go @@ -480,7 +480,33 @@ func getUserExt() []byte { KeyV: 1, Pref: 0, } + + eids := []openrtb_ext.ExtUserEid{ + { + Source: "test.com", + Uids: []openrtb_ext.ExtUserEidUid{ + { + ID: "some_user_id", + Atype: 1, + }, + { + ID: "other_user_id", + }, + }, + }, + { + Source: "test2.org", + Uids: []openrtb_ext.ExtUserEidUid{ + { + ID: "other_user_id", + Atype: 2, + }, + }, + }, + } + userExt := openrtb_ext.ExtUser{ + Eids: eids, Consent: "abc", DigiTrust: &digitrust, } @@ -519,13 +545,16 @@ func assertAdformServerRequest(testData aBidInfo, r *http.Request, isOpenRtb boo } var midsWithCurrency = "" + var queryString = "" if isOpenRtb { midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9RVVSJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZQ&bWlkPTMyMzQ1JnJjdXI9RVVS&bWlkPTMyMzQ2JnJjdXI9RVVS" + queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&eids=eyJ0ZXN0LmNvbSI6eyJvdGhlcl91c2VyX2lkIjpbMF0sInNvbWVfdXNlcl9pZCI6WzFdfSwidGVzdDIub3JnIjp7Im90aGVyX3VzZXJfaWQiOlsyXX19&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&" + midsWithCurrency } else { midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZQ&bWlkPTMyMzQ1JnJjdXI9VVNE&bWlkPTMyMzQ2JnJjdXI9VVNE" // no way to pass currency in legacy adapter + queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&" + midsWithCurrency } - if ok, err := equal("CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&"+midsWithCurrency, r.URL.RawQuery, "Query string"); !ok { + if ok, err := equal(queryString, r.URL.RawQuery, "Query string"); !ok { return err } if ok, err := equal("application/json;charset=utf-8", r.Header.Get("Content-Type"), "Content type"); !ok { diff --git a/openrtb_ext/user.go b/openrtb_ext/user.go index 520d73a6ed1..b83f82330db 100644 --- a/openrtb_ext/user.go +++ b/openrtb_ext/user.go @@ -43,6 +43,7 @@ type ExtUserEid struct { // ExtUserEidUid defines the contract for bidrequest.user.ext.eids[i].uids[j] type ExtUserEidUid struct { - ID string `json:"id"` - Ext json.RawMessage `json:"ext,omitempty"` + ID string `json:"id"` + Atype int `json:"atype,omitempty"` + Ext json.RawMessage `json:"ext,omitempty"` } From 034928ebb64b0aecab6c935188027fd45614f2a8 Mon Sep 17 00:00:00 2001 From: logicad Date: Fri, 10 Jul 2020 01:23:53 +0900 Subject: [PATCH 136/381] Add Logicad adapter (#1382) --- adapters/logicad/logicad.go | 155 ++++++++++++++++++ adapters/logicad/logicad_test.go | 10 ++ .../logicad/logicadtest/exemplary/banner.json | 92 +++++++++++ .../logicadtest/params/race/banner.json | 3 + .../logicadtest/supplemental/checkImp.json | 15 ++ .../logicad/logicadtest/supplemental/ext.json | 31 ++++ .../logicadtest/supplemental/missingtid.json | 33 ++++ .../supplemental/multiImpSameTid.json | 112 +++++++++++++ .../supplemental/responseCode.json | 72 ++++++++ .../supplemental/responseNoBid.json | 66 ++++++++ .../logicadtest/supplemental/responsebid.json | 73 +++++++++ .../logicadtest/supplemental/site.json | 98 +++++++++++ adapters/logicad/params_test.go | 45 +++++ adapters/logicad/usersync.go | 12 ++ adapters/logicad/usersync_test.go | 31 ++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_logicad.go | 5 + static/bidder-info/logicad.yaml | 10 ++ static/bidder-params/logicad.json | 13 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 23 files changed, 885 insertions(+) create mode 100644 adapters/logicad/logicad.go create mode 100644 adapters/logicad/logicad_test.go create mode 100644 adapters/logicad/logicadtest/exemplary/banner.json create mode 100644 adapters/logicad/logicadtest/params/race/banner.json create mode 100644 adapters/logicad/logicadtest/supplemental/checkImp.json create mode 100644 adapters/logicad/logicadtest/supplemental/ext.json create mode 100644 adapters/logicad/logicadtest/supplemental/missingtid.json create mode 100644 adapters/logicad/logicadtest/supplemental/multiImpSameTid.json create mode 100644 adapters/logicad/logicadtest/supplemental/responseCode.json create mode 100644 adapters/logicad/logicadtest/supplemental/responseNoBid.json create mode 100644 adapters/logicad/logicadtest/supplemental/responsebid.json create mode 100644 adapters/logicad/logicadtest/supplemental/site.json create mode 100644 adapters/logicad/params_test.go create mode 100644 adapters/logicad/usersync.go create mode 100644 adapters/logicad/usersync_test.go create mode 100644 openrtb_ext/imp_logicad.go create mode 100644 static/bidder-info/logicad.yaml create mode 100644 static/bidder-params/logicad.json diff --git a/adapters/logicad/logicad.go b/adapters/logicad/logicad.go new file mode 100644 index 00000000000..e757705a7bd --- /dev/null +++ b/adapters/logicad/logicad.go @@ -0,0 +1,155 @@ +package logicad + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type LogicadAdapter struct { + endpoint string +} + +func (adapter *LogicadAdapter) 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"}} + } + + pub2impressions, imps, errs := getImpressionsInfo(request.Imp) + if len(pub2impressions) == 0 || len(imps) == 0 { + return nil, errs + } + + result := make([]*adapters.RequestData, 0, len(pub2impressions)) + for k, imps := range pub2impressions { + bidRequest, err := adapter.buildAdapterRequest(request, &k, imps) + if err != nil { + errs = append(errs, err) + } else { + result = append(result, bidRequest) + } + } + return result, errs +} + +func getImpressionsInfo(imps []openrtb.Imp) (map[openrtb_ext.ExtImpLogicad][]openrtb.Imp, []openrtb.Imp, []error) { + errors := make([]error, 0, len(imps)) + resImps := make([]openrtb.Imp, 0, len(imps)) + res := make(map[openrtb_ext.ExtImpLogicad][]openrtb.Imp) + + for _, imp := range imps { + impExt, err := getImpressionExt(&imp) + if err != nil { + errors = append(errors, err) + continue + } + if err := validateImpression(&impExt); err != nil { + errors = append(errors, err) + continue + } + + if res[impExt] == nil { + res[impExt] = make([]openrtb.Imp, 0) + } + res[impExt] = append(res[impExt], imp) + resImps = append(resImps, imp) + } + return res, resImps, errors +} + +func validateImpression(impExt *openrtb_ext.ExtImpLogicad) error { + if impExt.Tid == "" { + return &errortypes.BadInput{Message: "No tid value provided"} + } + return nil +} + +func getImpressionExt(imp *openrtb.Imp) (openrtb_ext.ExtImpLogicad, error) { + var bidderExt adapters.ExtImpBidder + var logicadExt openrtb_ext.ExtImpLogicad + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return logicadExt, &errortypes.BadInput{ + Message: err.Error(), + } + } + if err := json.Unmarshal(bidderExt.Bidder, &logicadExt); err != nil { + return logicadExt, &errortypes.BadInput{ + Message: err.Error(), + } + } + return logicadExt, nil +} + +func (adapter *LogicadAdapter) buildAdapterRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpLogicad, imps []openrtb.Imp) (*adapters.RequestData, error) { + newBidRequest := createBidRequest(prebidBidRequest, params, imps) + reqJSON, err := json.Marshal(newBidRequest) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + return &adapters.RequestData{ + Method: "POST", + Uri: adapter.endpoint, + Body: reqJSON, + Headers: headers}, nil +} + +func createBidRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpLogicad, imps []openrtb.Imp) *openrtb.BidRequest { + bidRequest := *prebidBidRequest + bidRequest.Imp = imps + for idx := range bidRequest.Imp { + imp := &bidRequest.Imp[idx] + imp.TagID = params.Tid + imp.Ext = nil + } + return &bidRequest +} + +//MakeBids translates Logicad bid response to prebid-server specific format +func (adapter *LogicadAdapter) 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 { + msg := fmt.Sprintf("Unexpected http status code: %d", response.StatusCode) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + + } + var bidResp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + msg := fmt.Sprintf("Bad server response: %d", err) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + } + if len(bidResp.SeatBid) != 1 { + msg := fmt.Sprintf("Invalid SeatBids count: %d", len(bidResp.SeatBid)) + return nil, []error{&errortypes.BadServerResponse{Message: msg}} + } + + seatBid := bidResp.SeatBid[0] + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seatBid.Bid)) + + for i := 0; i < len(seatBid.Bid); i++ { + bid := seatBid.Bid[i] + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: openrtb_ext.BidTypeBanner, + }) + } + return bidResponse, nil +} + +func NewLogicadBidder(endpoint string) adapters.Bidder { + return &LogicadAdapter{ + endpoint: endpoint, + } +} diff --git a/adapters/logicad/logicad_test.go b/adapters/logicad/logicad_test.go new file mode 100644 index 00000000000..adf20e4ed33 --- /dev/null +++ b/adapters/logicad/logicad_test.go @@ -0,0 +1,10 @@ +package logicad + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "testing" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "logicadtest", NewLogicadBidder("https://localhost/adrequest/prebidserver")) +} diff --git a/adapters/logicad/logicadtest/exemplary/banner.json b/adapters/logicad/logicadtest/exemplary/banner.json new file mode 100644 index 00000000000..f782cc2b9f8 --- /dev/null +++ b/adapters/logicad/logicadtest/exemplary/banner.json @@ -0,0 +1,92 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "tid": "testtid" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/adrequest/prebidserver", + "body": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "tagid": "testtid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "crid": "123", + "adid": "456", + "price": 0.12, + "id": "testid", + "impid": "testimpid", + "cid": "789" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "crid": "123", + "adid": "456", + "price": 0.12, + "id": "testid", + "impid": "testimpid", + "cid": "789" + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/logicad/logicadtest/params/race/banner.json b/adapters/logicad/logicadtest/params/race/banner.json new file mode 100644 index 00000000000..7cb3de5a1ef --- /dev/null +++ b/adapters/logicad/logicadtest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "tid": "testtid" +} \ No newline at end of file diff --git a/adapters/logicad/logicadtest/supplemental/checkImp.json b/adapters/logicad/logicadtest/supplemental/checkImp.json new file mode 100644 index 00000000000..62c6e3e8f9e --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/checkImp.json @@ -0,0 +1,15 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test", + "domain": "test.com" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the bid request", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/logicad/logicadtest/supplemental/ext.json b/adapters/logicad/logicadtest/supplemental/ext.json new file mode 100644 index 00000000000..ad35892086b --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/ext.json @@ -0,0 +1,31 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "tid": "testtid" + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/logicad/logicadtest/supplemental/missingtid.json b/adapters/logicad/logicadtest/supplemental/missingtid.json new file mode 100644 index 00000000000..5ed84cef65e --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/missingtid.json @@ -0,0 +1,33 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "tid": "" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "No tid value provided", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/logicad/logicadtest/supplemental/multiImpSameTid.json b/adapters/logicad/logicadtest/supplemental/multiImpSameTid.json new file mode 100644 index 00000000000..848733cdf35 --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/multiImpSameTid.json @@ -0,0 +1,112 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "tid": "testtid" + } + } + }, + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "tid": "testtid" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/adrequest/prebidserver", + "body": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "tagid": "testtid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "id": "testimpid", + "tagid": "testtid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "crid": "123", + "adid": "456", + "price": 0.12, + "id": "testid", + "impid": "testimpid", + "cid": "789" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "crid": "123", + "adid": "456", + "price": 0.12, + "id": "testid", + "impid": "testimpid", + "cid": "789" + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/logicad/logicadtest/supplemental/responseCode.json b/adapters/logicad/logicadtest/supplemental/responseCode.json new file mode 100644 index 00000000000..471993ad8f2 --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/responseCode.json @@ -0,0 +1,72 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test" + }, + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "tid": "testtid" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/adrequest/prebidserver", + "body": { + "id": "testid", + "imp": [ + { + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "id": "testimpid", + "tagid": "testtid" + } + ], + "site": { + "id": "test" + } + } + }, + "mockResponse": { + "body": { + "seatbid": [] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected http status code: 0", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/logicad/logicadtest/supplemental/responseNoBid.json b/adapters/logicad/logicadtest/supplemental/responseNoBid.json new file mode 100644 index 00000000000..6ddab2ab6bd --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/responseNoBid.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "tid": "testtid" + } + } + } + ], + "site": { + "id": "test" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/adrequest/prebidserver", + "body": { + "id": "testid", + "imp": [ + { + "id": "testimpid", + "tagid": "testtid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + } + } + ], + "site": { + "id": "test" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/logicad/logicadtest/supplemental/responsebid.json b/adapters/logicad/logicadtest/supplemental/responsebid.json new file mode 100644 index 00000000000..59d1c2ac21e --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/responsebid.json @@ -0,0 +1,73 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test" + }, + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "tid": "testtid" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/adrequest/prebidserver", + "body": { + "id": "testid", + "imp": [ + { + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "id": "testimpid", + "tagid": "testtid" + } + ], + "site": { + "id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [] + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Invalid SeatBids count: 0", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/logicad/logicadtest/supplemental/site.json b/adapters/logicad/logicadtest/supplemental/site.json new file mode 100644 index 00000000000..c747413f91c --- /dev/null +++ b/adapters/logicad/logicadtest/supplemental/site.json @@ -0,0 +1,98 @@ +{ + "mockBidRequest": { + "id": "testid", + "site": { + "id": "test" + }, + "imp": [ + { + "id": "testimpid", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "tid": "testtid" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://localhost/adrequest/prebidserver", + "body": { + "id": "testid", + "imp": [ + { + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 320, + "h": 50 + } + ] + }, + "id": "testimpid", + "tagid": "testtid" + } + ], + "site": { + "id": "test" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "crid": "123", + "adid": "456", + "price": 0.12, + "id": "testid", + "impid": "testimpid", + "cid": "789" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "crid": "123", + "adid": "456", + "price": 0.12, + "id": "testid", + "impid": "testimpid", + "cid": "789" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/logicad/params_test.go b/adapters/logicad/params_test.go new file mode 100644 index 00000000000..eb34452811b --- /dev/null +++ b/adapters/logicad/params_test.go @@ -0,0 +1,45 @@ +package logicad + +import ( + "encoding/json" + "github.com/prebid/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.BidderLogicad, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected LunaMedia 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.BidderLogicad, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"tid": "testtid"}`, +} + +var invalidParams = []string{ + `nil`, + ``, + `[]`, + `true`, + `{"tid": 42}`, +} diff --git a/adapters/logicad/usersync.go b/adapters/logicad/usersync.go new file mode 100644 index 00000000000..d26a197b0a1 --- /dev/null +++ b/adapters/logicad/usersync.go @@ -0,0 +1,12 @@ +package logicad + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewLogicadSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("logicad", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/logicad/usersync_test.go b/adapters/logicad/usersync_test.go new file mode 100644 index 00000000000..89d6207d348 --- /dev/null +++ b/adapters/logicad/usersync_test.go @@ -0,0 +1,31 @@ +package logicad + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestLogicadSyncer(t *testing.T) { + syncURL := "https://localhost/cookiesender?r=true&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ru=localhost%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewLogicadSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "A", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://localhost/cookiesender?r=true&gdpr=1&gdpr_consent=A&ru=localhost%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D1%26gdpr_consent%3DA%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/config/config.go b/config/config.go index cc1d4a0ab4e..65ad352c938 100755 --- a/config/config.go +++ b/config/config.go @@ -597,6 +597,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=184932&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlockerdome%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7B%7Buid%7D%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLogicad, "https://cr-p31.ladsp.jp/cookiesender/31?r=true&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ru="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLunaMedia, "https://api.lunamedia.io/xp/user-sync?redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D") @@ -808,6 +809,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.logicad.endpoint", "https://pbs.ladsp.com/adrequest/prebidserver") 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/") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 1f62d232233..53607ac57d8 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -48,6 +48,7 @@ import ( "github.com/prebid/prebid-server/adapters/kubient" "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/lockerdome" + "github.com/prebid/prebid-server/adapters/logicad" "github.com/prebid/prebid-server/adapters/lunamedia" "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/mgid" @@ -133,6 +134,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderLogicad: logicad.NewLogicadBidder(cfg.Adapters[string(openrtb_ext.BidderLogicad)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 49d7b09d671..62fb9750616 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -64,6 +64,7 @@ const ( BidderKubient BidderName = "kubient" BidderLifestreet BidderName = "lifestreet" BidderLockerDome BidderName = "lockerdome" + BidderLogicad BidderName = "logicad" BidderLunaMedia BidderName = "lunamedia" BidderMarsmedia BidderName = "marsmedia" BidderMgid BidderName = "mgid" @@ -145,6 +146,7 @@ var BidderMap = map[string]BidderName{ "kubient": BidderKubient, "lifestreet": BidderLifestreet, "lockerdome": BidderLockerDome, + "logicad": BidderLogicad, "lunamedia": BidderLunaMedia, "marsmedia": BidderMarsmedia, "mgid": BidderMgid, diff --git a/openrtb_ext/imp_logicad.go b/openrtb_ext/imp_logicad.go new file mode 100644 index 00000000000..e4e3c3b091c --- /dev/null +++ b/openrtb_ext/imp_logicad.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpLogicad struct { + Tid string `json:"tid"` +} diff --git a/static/bidder-info/logicad.yaml b/static/bidder-info/logicad.yaml new file mode 100644 index 00000000000..c087516c061 --- /dev/null +++ b/static/bidder-info/logicad.yaml @@ -0,0 +1,10 @@ +maintainer: + email: "prebid@so-netmedia.jp" +capabilities: + site: + mediaTypes: + - banner + app: + mediaTypes: + - banner + diff --git a/static/bidder-params/logicad.json b/static/bidder-params/logicad.json new file mode 100644 index 00000000000..2a892f91266 --- /dev/null +++ b/static/bidder-params/logicad.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Logicad Adapter Params", + "description": "A schema which validates params accepted by the Logicad adapter", + "type": "object", + "properties": { + "tid": { + "type": "string", + "description": "Logicad for Publishers placement ID" + } + }, + "required": ["tid"] +} diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index f1f643afb74..89540ea205b 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -39,6 +39,7 @@ import ( "github.com/prebid/prebid-server/adapters/ix" "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/lockerdome" + "github.com/prebid/prebid-server/adapters/logicad" "github.com/prebid/prebid-server/adapters/lunamedia" "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/mgid" @@ -115,6 +116,7 @@ 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.BidderLogicad, logicad.NewLogicadSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderLunaMedia, lunamedia.NewLunaMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderMarsmedia, marsmedia.NewMarsmediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderMgid, mgid.NewMgidSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index b23541eaf8a..32ab2e730eb 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -48,6 +48,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderIx): syncConfig, string(openrtb_ext.BidderLifestreet): syncConfig, string(openrtb_ext.BidderLockerDome): syncConfig, + string(openrtb_ext.BidderLogicad): syncConfig, string(openrtb_ext.BidderLunaMedia): syncConfig, string(openrtb_ext.BidderMarsmedia): syncConfig, string(openrtb_ext.BidderMgid): syncConfig, From 7c3521b2c8e1bec25341cb54072e8770becea820 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 13 Jul 2020 23:35:29 -0400 Subject: [PATCH 137/381] Fix Previous Merge Conflict (#1392) --- config/config.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 65ad352c938..f33dba69b60 100755 --- a/config/config.go +++ b/config/config.go @@ -764,12 +764,8 @@ func SetupViper(v *viper.Viper, filename string) { // Disabling adapters by default that require some specific config params. // If you're using one of these, make sure you check out the documentation (https://github.com/prebid/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") @@ -787,6 +783,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.audiencenetwork.disabled", true) 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\"}") @@ -796,6 +793,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25") v.SetDefault("adapters.cpmstar.endpoint", "https://server.cpmstar.com/openrtbbidrq.aspx") v.SetDefault("adapters.datablocks.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}") + v.SetDefault("adapters.dmx.endpoint", "https://dmx.districtm.io/b/v2") 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://rtb.e-planning.net/pbs/1") @@ -823,6 +821,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.pulsepoint.endpoint", "http://bid.contextweb.com/header/s/ortb/prebid-s2s") v.SetDefault("adapters.rhythmone.endpoint", "http://tag.1rx.io/rmp") v.SetDefault("adapters.rtbhouse.endpoint", "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids") + v.SetDefault("adapters.rubicon.disabled", true) 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.smartadserver.endpoint", "https://ssb.smartadserver.com") From bb2b03748ee9a7a83c09243762bfaf50b91dea77 Mon Sep 17 00:00:00 2001 From: Marsel Date: Wed, 15 Jul 2020 08:49:58 +0300 Subject: [PATCH 138/381] Kubient: Change default endpont address (#1398) --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index f33dba69b60..5d538f38523 100755 --- a/config/config.go +++ b/config/config.go @@ -804,7 +804,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.kubient.endpoint", "https://kssp.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.logicad.endpoint", "https://pbs.ladsp.com/adrequest/prebidserver") From e6d159e71dd479338207f4eb7f7746b71a0e3d9d Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 15 Jul 2020 10:07:53 -0400 Subject: [PATCH 139/381] Add support for multiple root schain nodes (#1374) --- endpoints/openrtb2/auction.go | 9 ++ endpoints/openrtb2/auction_test.go | 47 ++++++++ exchange/utils.go | 94 ++++++++++++++- exchange/utils_test.go | 180 +++++++++++++++++++++++++++++ openrtb_ext/request.go | 43 ++++++- 5 files changed, 366 insertions(+), 7 deletions(-) diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 20acc2aedd3..3fd2132143e 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -290,6 +290,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { if err := validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil { return []error{err} } + + if err := validateSChains(bidExt); err != nil { + return []error{err} + } } if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) { @@ -362,6 +366,11 @@ func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases return nil } +func validateSChains(req *openrtb_ext.ExtRequest) error { + _, err := exchange.BidderToPrebidSChains(req) + return err +} + func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]string, index int) []error { if imp.ID == "" { return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)} diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 97f0038a392..c697c206483 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1039,6 +1039,53 @@ func TestCCPAInvalid(t *testing.T) { assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") } +func TestSChainInvalid(t *testing.T) { + deps := &endpointDeps{ + &nobidExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{}, + pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + nil, + nil, + hardcodedResponseIPValidator{response: true}, + } + + ui := uint64(1) + req := openrtb.BidRequest{ + ID: "someID", + Imp: []openrtb.Imp{ + { + ID: "imp-ID", + Banner: &openrtb.Banner{ + W: &ui, + H: &ui, + }, + Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`), + }, + }, + Site: &openrtb.Site{ + ID: "myID", + }, + Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"abcd"}`), + }, + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), + } + + errL := deps.validateRequest(&req) + + expectedError := fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.") + assert.ElementsMatch(t, errL, []error{expectedError}) +} + func TestSanitizeRequest(t *testing.T) { testCases := []struct { description string diff --git a/exchange/utils.go b/exchange/utils.go index 96c00ec0e36..4de985eca40 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -28,6 +28,29 @@ type cleanMetrics struct { gdprTcfVersion int } +func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { + bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) + + if len(req.Prebid.SChains) == 0 { + return bidderToSChains, nil + } + + for _, schainWrapper := range req.Prebid.SChains { + if schainWrapper != nil && len(schainWrapper.Bidders) > 0 { + for _, bidder := range schainWrapper.Bidders { + if _, present := bidderToSChains[bidder]; present { + return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+ + "it must contain no more than one per bidder.", bidder) + } else { + bidderToSChains[bidder] = &schainWrapper.SChain + } + } + } + } + + return bidderToSChains, nil +} + // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is: // // 1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder. @@ -103,12 +126,35 @@ func cleanOpenRTBRequests(ctx context.Context, return } -func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.Imp, aliases map[string]string, usersyncs IdFetcher, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) { +func splitBidRequest(req *openrtb.BidRequest, + impsByBidder map[string][]openrtb.Imp, + aliases map[string]string, + usersyncs IdFetcher, + blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, + labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) { + requestsByBidder := make(map[openrtb_ext.BidderName]*openrtb.BidRequest, len(impsByBidder)) explicitBuyerUIDs, err := extractBuyerUIDs(req.User) if err != nil { return nil, []error{err} } + + var requestExt openrtb_ext.ExtRequest + var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain + if len(req.Ext) > 0 { + err := json.Unmarshal(req.Ext, &requestExt) + if err != nil { + return nil, []error{err} + } + + sChainsByBidder, err = BidderToPrebidSChains(&requestExt) + if err != nil { + return nil, []error{err} + } + } else { + sChainsByBidder = make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) + } + for bidder, imps := range impsByBidder { reqCopy := *req coreBidder := resolveBidder(bidder, aliases) @@ -128,11 +174,57 @@ func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb. blabels[coreBidder].CookieFlag = pbsmetrics.CookieFlagYes } reqCopy.Imp = imps + + prepareSource(&reqCopy, bidder, sChainsByBidder) + prepareExt(&reqCopy, &requestExt) + requestsByBidder[openrtb_ext.BidderName(bidder)] = &reqCopy } return requestsByBidder, nil } +func prepareExt(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) { + if len(req.Ext) == 0 { + return + } + extCopy := *unpackedExt + extCopy.Prebid.SChains = nil + reqExt, err := json.Marshal(extCopy) + if err == nil { + req.Ext = reqExt + } +} + +func prepareSource(req *openrtb.BidRequest, bidder string, sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) { + const sChainWildCard = "*" + var selectedSChain *openrtb_ext.ExtRequestPrebidSChainSChain + + wildCardSChain := sChainsByBidder[sChainWildCard] + bidderSChain := sChainsByBidder[bidder] + + // source should not be modified + if bidderSChain == nil && wildCardSChain == nil { + return + } + + if bidderSChain != nil { + selectedSChain = bidderSChain + } else { + selectedSChain = wildCardSChain + } + + // set source + var source openrtb.Source + schain := openrtb_ext.ExtRequestPrebidSChain{ + SChain: *selectedSChain, + } + sourceExt, err := json.Marshal(schain) + if err == nil { + source.Ext = sourceExt + req.Source = &source + } +} + // extractBuyerUIDs parses the values from user.ext.prebid.buyeruids, and then deletes those values from the ext. // This prevents a Bidder from using these values to figure out who else is involved in the Auction. func extractBuyerUIDs(user *openrtb.User) (map[string]string, error) { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index e50d0f777f0..6d66e816e7b 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -135,6 +135,92 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { } } +func TestCleanOpenRTBRequestsSChain(t *testing.T) { + testCases := []struct { + description string + inSourceExt json.RawMessage + inExt json.RawMessage + outSourceExt json.RawMessage + outExt json.RawMessage + hasError bool + }{ + { + description: "Empty root ext and source ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(``), + outSourceExt: json.RawMessage(``), + outExt: json.RawMessage(``), + hasError: false, + }, + { + description: "No schains in root ext and empty source ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[]}}`), + outSourceExt: json.RawMessage(``), + outExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains", + inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + outExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use schain for bidder in ext.prebid.schains", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use wildcard schain in ext.prebid.schains", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use schain for bidder in ext.prebid.schains instead of wildcard", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`), + hasError: false, + }, + { + description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains", + inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: nil, + outExt: nil, + hasError: true, + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.Source.Ext = test.inSourceExt + req.Ext = test.inExt + + results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{}) + result := results["appnexus"] + + if test.hasError == true { + assert.NotNil(t, errs) + assert.Nil(t, result) + } else { + assert.Nil(t, errs) + assert.Equal(t, test.outSourceExt, result.Source.Ext, test.description+":Source.Ext") + assert.Equal(t, test.outExt, result.Ext, test.description+":Ext") + } + } +} + func TestCleanOpenRTBRequestsLMT(t *testing.T) { var ( enabled int8 = 1 @@ -302,5 +388,99 @@ func TestRandomizeList(t *testing.T) { if len(adapters) != 1 { t.Errorf("RandomizeList, expected a list of 1, found %d", len(adapters)) } +} + +func TestBidderToPrebidChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{ + { + Bidders: []string{"Bidder1", "Bidder2"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{ + Complete: 1, + Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{ + { + ASI: "asi1", + SID: "sid1", + Name: "name1", + RID: "rid1", + Domain: "domain1", + HP: 1, + }, + { + ASI: "asi2", + SID: "sid2", + Name: "name2", + RID: "rid2", + Domain: "domain2", + HP: 2, + }, + }, + Ver: "version1", + }, + }, + { + Bidders: []string{"Bidder3", "Bidder4"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + }, + }, + } + + output, err := BidderToPrebidSChains(&input) + + assert.Nil(t, err) + assert.Equal(t, len(output), 4) + assert.Same(t, output["Bidder1"], &input.Prebid.SChains[0].SChain) + assert.Same(t, output["Bidder2"], &input.Prebid.SChains[0].SChain) + assert.Same(t, output["Bidder3"], &input.Prebid.SChains[1].SChain) + assert.Same(t, output["Bidder4"], &input.Prebid.SChains[1].SChain) +} + +func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{ + { + Bidders: []string{"Bidder1"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + { + Bidders: []string{"Bidder1", "Bidder2"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + }, + }, + } + + output, err := BidderToPrebidSChains(&input) + + assert.NotNil(t, err) + assert.Nil(t, output) +} + +func TestBidderToPrebidChainsNilSChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: nil, + }, + } + + output, err := BidderToPrebidSChains(&input) + + assert.Nil(t, err) + assert.Equal(t, len(output), 0) +} + +func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{}, + }, + } + + output, err := BidderToPrebidSChains(&input) + assert.Nil(t, err) + assert.Equal(t, len(output), 0) } diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 25b5c881408..86388f60cf4 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -12,12 +12,43 @@ type ExtRequest struct { // ExtRequestPrebid defines the contract for bidrequest.ext.prebid type ExtRequestPrebid struct { - Aliases map[string]string `json:"aliases,omitempty"` - BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"` - Cache *ExtRequestPrebidCache `json:"cache,omitempty"` - StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` - Targeting *ExtRequestTargeting `json:"targeting,omitempty"` - SupportDeals bool `json:"supportdeals,omitempty"` + Aliases map[string]string `json:"aliases,omitempty"` + BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"` + Cache *ExtRequestPrebidCache `json:"cache,omitempty"` + SChains []*ExtRequestPrebidSChain `json:"schains,omitempty"` + StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` + Targeting *ExtRequestTargeting `json:"targeting,omitempty"` + SupportDeals bool `json:"supportdeals,omitempty"` +} + +// ExtRequestPrebid defines the contract for bidrequest.ext.prebid.schains +type ExtRequestPrebidSChain struct { + Bidders []string `json:"bidders,omitempty"` + SChain ExtRequestPrebidSChainSChain `json:"schain"` +} + +// ExtRequestPrebidSChainSChain defines the contract for bidrequest.ext.prebid.schains[i].schain +type ExtRequestPrebidSChainSChain struct { + Complete int `json:"complete"` + Nodes []*ExtRequestPrebidSChainSChainNode `json:"nodes"` + Ver string `json:"ver"` + Ext json.RawMessage `json:"ext,omitempty"` +} + +// ExtRequestPrebidSChainSChainNode defines the contract for bidrequest.ext.prebid.schains[i].schain[i].nodes +type ExtRequestPrebidSChainSChainNode struct { + ASI string `json:"asi"` + SID string `json:"sid"` + RID string `json:"rid,omitempty"` + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + HP int `json:"hp"` + Ext json.RawMessage `json:"ext,omitempty"` +} + +// SourceExt defines the contract for bidrequest.source.ext +type SourceExt struct { + SChain ExtRequestPrebidSChainSChain `json:"schain"` } // ExtRequestPrebidCache defines the contract for bidrequest.ext.prebid.cache From e6fe57e058a0dd44273916d5c919829937ec13dc Mon Sep 17 00:00:00 2001 From: Steve Alliance Date: Wed, 15 Jul 2020 22:05:49 -0400 Subject: [PATCH 140/381] Update endpoint for latest release by districtm (#1401) Co-authored-by: steve-a-districtm --- adapters/dmx/dmx.go | 2 +- config/config.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/dmx/dmx.go b/adapters/dmx/dmx.go index 6b4f698d4b1..de33bd390e5 100644 --- a/adapters/dmx/dmx.go +++ b/adapters/dmx/dmx.go @@ -160,7 +160,7 @@ func (adapter *DmxAdapter) MakeRequests(request *openrtb.BidRequest, req *adapte } headers := http.Header{} - headers.Add("Content-Type", "Application/json;charset=utf-8") + headers.Add("Content-Type", "application/json;charset=utf-8") reqBidder := &adapters.RequestData{ Method: "POST", Uri: adapter.endpoint + addParams(sellerId), //adapter.endpoint, diff --git a/config/config.go b/config/config.go index 5d538f38523..2e7f875b023 100755 --- a/config/config.go +++ b/config/config.go @@ -793,7 +793,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25") v.SetDefault("adapters.cpmstar.endpoint", "https://server.cpmstar.com/openrtbbidrq.aspx") v.SetDefault("adapters.datablocks.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}") - v.SetDefault("adapters.dmx.endpoint", "https://dmx.districtm.io/b/v2") + v.SetDefault("adapters.dmx.endpoint", "https://dmx-direct.districtm.io/b/v2") 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://rtb.e-planning.net/pbs/1") From ea348e3fd3fa4fcc04149e761676051539d92aa1 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 15 Jul 2020 23:01:29 -0400 Subject: [PATCH 141/381] Set OpenRTB DNT From HTTP Header (#1397) --- endpoints/openrtb2/auction.go | 26 +++ endpoints/openrtb2/auction_test.go | 183 ++++++++++++++++++ .../supplementary/site-has-dnt.json | 45 +++++ 3 files changed, 254 insertions(+) create mode 100644 endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 3fd2132143e..86186fa8373 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -39,6 +39,12 @@ import ( const storedRequestTimeoutMillis = 50 +var ( + dntKey string = http.CanonicalHeaderKey("DNT") + dntDisabled int8 = 0 + dntEnabled int8 = 1 +) + func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById 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) { if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { @@ -964,6 +970,8 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *ope func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidtor iputil.IPValidator) { setIPImplicitly(httpReq, bidReq, ipValidtor) setUAImplicitly(httpReq, bidReq) + setDoNotTrackImplicitly(httpReq, bidReq) + } // setAuctionTypeImplicitly sets the auction type to 1 if it wasn't on the request, @@ -1192,6 +1200,24 @@ func setUAImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { } } +func setDoNotTrackImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { + if bidReq.Device == nil || bidReq.Device.DNT == nil { + dnt := httpReq.Header.Get(dntKey) + if dnt == "0" || dnt == "1" { + if bidReq.Device == nil { + bidReq.Device = &openrtb.Device{} + } + + switch dnt { + case "0": + bidReq.Device.DNT = &dntDisabled + case "1": + bidReq.Device.DNT = &dntEnabled + } + } + } +} + // parseUserID gets this user's ID for the host machine, if it exists. func parseUserID(cfg *config.Configuration, httpReq *http.Request) (string, bool) { if hostCookie, err := httpReq.Cookie(cfg.HostCookie.CookieName); hostCookie != nil && err == nil { diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index c697c206483..957760c61c9 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -604,6 +604,189 @@ func TestImplicitIPsEndToEnd(t *testing.T) { } } +func TestImplicitDNT(t *testing.T) { + var ( + disabled int8 = 0 + enabled int8 = 1 + ) + testCases := []struct { + description string + dntHeader string + request openrtb.BidRequest + expectedRequest openrtb.BidRequest + }{ + { + description: "Device Missing - Not Set In Header", + dntHeader: "", + request: openrtb.BidRequest{}, + expectedRequest: openrtb.BidRequest{}, + }, + { + description: "Device Missing - Set To 0 In Header", + dntHeader: "0", + request: openrtb.BidRequest{}, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &disabled, + }, + }, + }, + { + description: "Device Missing - Set To 1 In Header", + dntHeader: "1", + request: openrtb.BidRequest{}, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Not Set In Request - Not Set In Header", + dntHeader: "", + request: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + }, + { + description: "Not Set In Request - Set To 0 In Header", + dntHeader: "0", + request: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &disabled, + }, + }, + }, + { + description: "Not Set In Request - Set To 1 In Header", + dntHeader: "1", + request: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Set In Request - Not Set In Header", + dntHeader: "", + request: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Set In Request - Set To 0 In Header", + dntHeader: "0", + request: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Set In Request - Set To 1 In Header", + dntHeader: "1", + request: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + } + + for _, test := range testCases { + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", nil) + httpReq.Header.Set("DNT", test.dntHeader) + setDoNotTrackImplicitly(httpReq, &test.request) + assert.Equal(t, test.expectedRequest, test.request) + } +} + +func TestImplicitDNTEndToEnd(t *testing.T) { + var ( + disabled int8 = 0 + enabled int8 = 1 + ) + testCases := []struct { + description string + reqJSONFile string + dntHeader string + expectedDNT *int8 + }{ + { + description: "Not Set In Request - Not Set In Header", + reqJSONFile: "site.json", + dntHeader: "", + expectedDNT: nil, + }, + { + description: "Not Set In Request - Set To 0 In Header", + reqJSONFile: "site.json", + dntHeader: "0", + expectedDNT: &disabled, + }, + { + description: "Not Set In Request - Set To 1 In Header", + reqJSONFile: "site.json", + dntHeader: "1", + expectedDNT: &enabled, + }, + { + description: "Set In Request - Not Set In Header", + reqJSONFile: "site-has-dnt.json", + dntHeader: "", + expectedDNT: &enabled, // Hardcoded value in test file. + }, + { + description: "Set In Request - Not Overwritten By Header", + reqJSONFile: "site-has-dnt.json", + dntHeader: "0", + expectedDNT: &enabled, // Hardcoded value in test file. + }, + } + + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + for _, test := range testCases { + exchange := &nobidExchange{} + endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) + httpReq.Header.Set("DNT", test.dntHeader) + + endpoint(httptest.NewRecorder(), httpReq, nil) + + result := exchange.gotRequest + if !assert.NotEmpty(t, result, test.description+"Request received by the exchange.") { + t.FailNow() + } + assert.Equal(t, test.expectedDNT, result.Device.DNT, test.description+":dnt") + } +} func TestImplicitSecure(t *testing.T) { httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) httpReq.Header.Set(http.CanonicalHeaderKey("X-Forwarded-Proto"), "https") diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json new file mode 100644 index 00000000000..b1fae20afe4 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json @@ -0,0 +1,45 @@ +{ + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "device": { + "dnt": 1 + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } + } + } + } + \ No newline at end of file From 55f4c453668d0b7233331067be7232b0e89f0626 Mon Sep 17 00:00:00 2001 From: Gena Date: Thu, 16 Jul 2020 17:22:23 +0300 Subject: [PATCH 142/381] Add video for InApp support (#1399) --- static/bidder-info/adtelligent.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/static/bidder-info/adtelligent.yaml b/static/bidder-info/adtelligent.yaml index fe791343daf..7a20d52b266 100644 --- a/static/bidder-info/adtelligent.yaml +++ b/static/bidder-info/adtelligent.yaml @@ -4,6 +4,7 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner From 62a72e23621b20ec3e5241ba955d36c775dfb394 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Thu, 16 Jul 2020 16:36:32 -0400 Subject: [PATCH 143/381] Timeout fix (#1390) --- config/util/loggers.go | 6 +-- exchange/bidder.go | 22 +++++++--- exchange/bidder_test.go | 91 +++++++++++++++++++++++++++++---------- exchange/exchange_test.go | 9 ++++ 4 files changed, 97 insertions(+), 31 deletions(-) diff --git a/config/util/loggers.go b/config/util/loggers.go index 88702e68763..d9aad43a7fb 100644 --- a/config/util/loggers.go +++ b/config/util/loggers.go @@ -4,18 +4,18 @@ import ( "math/rand" ) -type logMsg func(string, ...interface{}) +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) { +func LogRandomSample(msg string, logger LogMsg, chance float32) { logRandomSampleImpl(msg, logger, chance, rand.Float32) } -func logRandomSampleImpl(msg string, logger logMsg, chance float32, randGenerator randomGenerator) { +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 diff --git a/exchange/bidder.go b/exchange/bidder.go index df9f0a3bf1b..ee6a4942147 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -312,6 +312,10 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall { // doRequest makes a request, handles the response, and returns the data needed by the // Bidder interface. func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData) *httpCallInfo { + return bidder.doRequestImpl(ctx, req, glog.Warningf) +} + +func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg) *httpCallInfo { httpReq, err := http.NewRequest(req.Method, req.Uri, bytes.NewBuffer(req.Body)) if err != nil { return &httpCallInfo{ @@ -325,12 +329,18 @@ 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 { + var corebidder adapters.Bidder = bidder.Bidder + // The bidder adapter normally stores an info-aware bidder (a bidder wrapper) + // rather than the actual bidder. So we need to unpack that first. + if b, ok := corebidder.(*adapters.InfoAwareBidder); ok { + corebidder = b.Bidder + } + if tb, ok := corebidder.(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) + go bidder.doTimeoutNotification(tb, req, logger) } } @@ -366,7 +376,7 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques } } -func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData) { +func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData, logger util.LogMsg) { ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() toReq, errL := timeoutBidder.MakeTimeoutNotification(req) @@ -385,13 +395,13 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou 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) + util.LogRandomSample(msg, logger, 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) + util.LogRandomSample(msg, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate) } } } else if bidder.DebugConfig.TimeoutNotification.Log { @@ -402,7 +412,7 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou } 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) + util.LogRandomSample(msg, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate) } } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index b776715adaf..d4fc0cf7cd3 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -1,6 +1,7 @@ package exchange import ( + "bytes" "context" "encoding/json" "errors" @@ -10,6 +11,7 @@ import ( "testing" "time" + "github.com/golang/glog" "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/config" @@ -1237,8 +1239,8 @@ func TestTimeoutNotificationOff(t *testing.T) { server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) defer server.Close() - bidderImpl := ¬ifingBidder{ - notiRequest: adapters.RequestData{ + bidderImpl := ¬ifyingBidder{ + notifyRequest: adapters.RequestData{ Method: "GET", Uri: server.URL + "/notify/me", Body: nil, @@ -1254,39 +1256,83 @@ func TestTimeoutNotificationOff(t *testing.T) { if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok { t.Error("Failed to cast bidder to a TimeoutBidder") } else { - bidder.doTimeoutNotification(tb, &adapters.RequestData{}) + bidder.doTimeoutNotification(tb, &adapters.RequestData{}, glog.Warningf) } } func TestTimeoutNotificationOn(t *testing.T) { - respBody := "{\"bid\":false}" - respStatus := 200 - server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + // Expire context immediately to force timeout handler. + ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now()) + cancelFunc() + + // Notification logic is hardcoded for 200ms. We need to wait for a little longer than that. + server := httptest.NewServer(mockSlowHandler(205*time.Millisecond, 200, `{"bid":false}`)) defer server.Close() - bidderImpl := ¬ifingBidder{ - notiRequest: adapters.RequestData{ + bidder := ¬ifyingBidder{ + notifyRequest: adapters.RequestData{ Method: "GET", Uri: server.URL + "/notify/me", Body: nil, Headers: http.Header{}, }, } - bidder := &bidderAdapter{ - Bidder: bidderImpl, + + // Wrap with BidderInfo to mimic exchange.go flow. + bidderWrappedWithInfo := wrapWithBidderInfo(bidder) + + bidderAdapter := &bidderAdapter{ + Bidder: bidderWrappedWithInfo, Client: server.Client(), DebugConfig: config.Debug{ TimeoutNotification: config.TimeoutNotification{ - Log: true, + Log: true, + SamplingRate: 1.0, }, }, 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{}) + + // Unwrap To Mimic exchange.go Casting Code + var coreBidder adapters.Bidder = bidderAdapter.Bidder + if b, ok := coreBidder.(*adapters.InfoAwareBidder); ok { + coreBidder = b.Bidder + } + if _, ok := coreBidder.(adapters.TimeoutBidder); !ok { + t.Fatal("Failed to cast bidder to a TimeoutBidder") + } + + bidRequest := adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), + } + + var loggerBuffer bytes.Buffer + logger := func(msg string, args ...interface{}) { + loggerBuffer.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...)) + } + + bidderAdapter.doRequestImpl(ctx, &bidRequest, logger) + + // Wait a little longer than the 205ms mock server sleep. + time.Sleep(210 * time.Millisecond) + + logExpected := "TimeoutNotification: error:(context deadline exceeded) body:\n" + logActual := loggerBuffer.String() + assert.EqualValues(t, logExpected, logActual) +} + +func wrapWithBidderInfo(bidder adapters.Bidder) adapters.Bidder { + bidderInfo := adapters.BidderInfo{ + Status: adapters.StatusActive, + Capabilities: &adapters.CapabilitiesInfo{ + App: &adapters.PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}, + }, + }, } + return adapters.EnforceBidderInfo(bidder, bidderInfo) } type goodSingleBidder struct { @@ -1363,18 +1409,19 @@ func (bidder *bidRejector) MakeBids(internalRequest *openrtb.BidRequest, externa return nil, []error{errors.New("Can't make a response.")} } -type notifingBidder struct { - notiRequest adapters.RequestData +type notifyingBidder struct { + requests []*adapters.RequestData + notifyRequest adapters.RequestData } -func (bidder *notifingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - return nil, nil +func (bidder *notifyingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + return bidder.requests, nil } -func (bidder *notifingBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (bidder *notifyingBidder) 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 +func (bidder *notifyingBidder) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { + return &bidder.notifyRequest, nil } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 161b24fd1c1..96f740de23a 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -1830,6 +1830,15 @@ func mockHandler(statusCode int, getBody string, postBody string) http.Handler { }) } +func mockSlowHandler(delay time.Duration, statusCode int, body string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(delay) + + w.WriteHeader(statusCode) + w.Write([]byte(body)) + }) +} + type wellBehavedCache struct{} func (c *wellBehavedCache) GetExtCacheData() (string, string) { From 5a7a2cf17b12f6ad78889c31cf1bdf7d7c71c904 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Fri, 17 Jul 2020 09:59:59 -0400 Subject: [PATCH 144/381] Privacy Request Metrics (#1400) * Privacy Request Metrics * Fix Bug + Add Unit Tests * Fixed Tests * Fix Typo --- exchange/exchange.go | 7 +- exchange/utils.go | 22 ++- exchange/utils_test.go | 198 ++++++++++++++++++++--- gdpr/impl_test.go | 2 +- pbsmetrics/config/metrics.go | 10 +- pbsmetrics/go_metrics.go | 49 ++++-- pbsmetrics/go_metrics_test.go | 65 +++++++- pbsmetrics/metrics.go | 14 +- pbsmetrics/metrics_mock.go | 6 +- pbsmetrics/prometheus/preload.go | 22 ++- pbsmetrics/prometheus/prometheus.go | 56 ++++++- pbsmetrics/prometheus/prometheus_test.go | 85 ++++++++-- 12 files changed, 454 insertions(+), 82 deletions(-) diff --git a/exchange/exchange.go b/exchange/exchange.go index 174a0b3e0fc..3f0258dd3c1 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -104,11 +104,10 @@ 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, cleanMetrics, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) + cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) + + e.me.RecordRequestPrivacy(privacyLabels) - if cleanMetrics.gdprEnforced { - e.me.RecordTCFReq(pbsmetrics.TCFVersionToValue(cleanMetrics.gdprTcfVersion)) - } // List of bidders we have requests for. liveAdapters := listBiddersWithRequests(cleanRequests) diff --git a/exchange/utils.go b/exchange/utils.go index 4de985eca40..bc1b555e507 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -19,15 +19,6 @@ import ( "github.com/prebid/prebid-server/privacy/lmt" ) -// cleanMetrics is a struct to export any metrics data resulting from cleanOpenRTBRequests(). It starts with just -// the TCF version, but made a struct to facilitate future expansion -type cleanMetrics struct { - // A simple flag if GDPR is being enforced on this request. - gdprEnforced bool - // a zero value means a missing or invalid GDPR string - gdprTcfVersion int -} - func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) @@ -63,7 +54,7 @@ func cleanOpenRTBRequests(ctx context.Context, labels pbsmetrics.Labels, gDPR gdpr.Permissions, usersyncIfAmbiguous bool, - privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, cleanMetrics cleanMetrics, errs []error) { + privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, privacyLabels pbsmetrics.PrivacyLabels, errs []error) { impsByBidder, errs := splitImps(orig.Imp) if len(errs) > 0 { @@ -98,13 +89,20 @@ func cleanOpenRTBRequests(ctx context.Context, LMT: lmtPolicy.ShouldEnforce(), } + privacyLabels.CCPAProvided = ccpaPolicy.Value != "" + privacyLabels.CCPAEnforced = privacyEnforcement.CCPA + privacyLabels.COPPAEnforced = privacyEnforcement.COPPA + privacyLabels.LMTEnforced = privacyEnforcement.LMT + if gdpr == 1 { - cleanMetrics.gdprEnforced = true + privacyLabels.GDPREnforced = true parsedConsent, err := vendorconsent.ParseString(consent) if err == nil { - cleanMetrics.gdprTcfVersion = int(parsedConsent.Version()) + version := int(parsedConsent.Version()) + privacyLabels.GDPRTCFVersion = pbsmetrics.TCFVersionToValue(version) } } + // bidder level privacy policies for bidder, bidReq := range requestsByBidder { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 6d66e816e7b..608e6a17a10 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -15,7 +15,9 @@ import ( // permissionsMock mocks the Permissions interface for tests // // It only allows appnexus for GDPR consent -type permissionsMock struct{} +type permissionsMock struct { + personalInfoAllowed bool +} func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { return true, nil @@ -26,10 +28,7 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_ } func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, error) { - if bidder == "appnexus" { - return true, true, nil - } - return false, false, nil + return p.personalInfoAllowed, p.personalInfoAllowed, nil } func (p *permissionsMock) AMPException() bool { @@ -80,7 +79,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { } for _, test := range testCases { - reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -92,26 +91,48 @@ func TestCleanOpenRTBRequests(t *testing.T) { func TestCleanOpenRTBRequestsCCPA(t *testing.T) { testCases := []struct { - description string - enforceCCPA bool - expectDataScrub bool + description string + ccpaConsent string + enforceCCPA bool + expectDataScrub bool + expectPrivacyLabels pbsmetrics.PrivacyLabels }{ { - description: "Feature Flag Enabled", + description: "Feature Flag Enabled - Opt Out", + ccpaConsent: "1-Y-", enforceCCPA: true, expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, + }, + { + description: "Feature Flag Enabled - Opt In", + ccpaConsent: "1-N-", + enforceCCPA: true, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: false, + }, }, { description: "Feature Flag Disabled", + ccpaConsent: "1-Y-", enforceCCPA: false, expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: false, + CCPAEnforced: false, + }, }, } for _, test := range testCases { req := newBidRequest(t) req.Regs = &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`), + Ext: json.RawMessage(`{"us_privacy":"` + test.ccpaConsent + `"}`), } privacyConfig := config.Privacy{ @@ -120,11 +141,10 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { }, } - results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, 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") @@ -132,6 +152,51 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5") } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") + } +} + +func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { + testCases := []struct { + description string + coppa int8 + expectDataScrub bool + expectPrivacyLabels pbsmetrics.PrivacyLabels + }{ + { + description: "Enabled", + coppa: 1, + expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + COPPAEnforced: true, + }, + }, + { + description: "Disabled", + coppa: 0, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + COPPAEnforced: false, + }, + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.Regs = &openrtb.Regs{COPPA: test.coppa} + + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}) + result := results["appnexus"] + + assert.Nil(t, errs) + if test.expectDataScrub { + assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") + assert.Equal(t, result.User.Yob, int64(0), test.description+":User.Yob") + } else { + assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") + assert.NotEqual(t, result.User.Yob, int64(0), test.description+":User.Yob") + } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") } } @@ -227,34 +292,47 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { disabled int8 = 0 ) testCases := []struct { - description string - lmt *int8 - enforceLMT bool - expectDataScrub bool + description string + lmt *int8 + enforceLMT bool + expectDataScrub bool + expectPrivacyLabels pbsmetrics.PrivacyLabels }{ { description: "Feature Flag Enabled - OpenTRB Enabled", lmt: &enabled, enforceLMT: true, expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: true, + }, }, { description: "Feature Flag Disabled - OpenTRB Enabled", lmt: &enabled, enforceLMT: false, expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: false, + }, }, { description: "Feature Flag Enabled - OpenTRB Disabled", lmt: &disabled, enforceLMT: true, expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: false, + }, }, { description: "Feature Flag Disabled - OpenTRB Disabled", lmt: &disabled, enforceLMT: false, expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: false, + }, }, } @@ -268,11 +346,10 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { }, } - results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, 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") @@ -280,6 +357,88 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5") } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") + } +} + +func TestCleanOpenRTBRequestsGDPR(t *testing.T) { + testCases := []struct { + description string + gdpr string + gdprConsent string + gdprScrub bool + enforceGDPR bool + expectPrivacyLabels pbsmetrics.PrivacyLabels + }{ + { + description: "Enforce - TCF Invalid", + gdpr: "1", + gdprConsent: "malformed", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: "", + }, + }, + { + description: "Enforce - TCF 1", + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }, + }, + { + description: "Enforce - TCF 2", + gdpr: "1", + gdprConsent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV2, + }, + }, + { + description: "Not Enforce - TCF 1", + gdpr: "0", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.User.Ext = json.RawMessage(`{"consent":"` + test.gdprConsent + `"}`) + req.Regs = &openrtb.Regs{ + Ext: json.RawMessage(`{"gdpr":` + test.gdpr + `}`), + } + + privacyConfig := config.Privacy{ + GDPR: config.GDPR{ + TCF2: config.TCF2{ + Enabled: true, + }, + }, + } + + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: !test.gdprScrub}, true, privacyConfig) + result := results["appnexus"] + + assert.Nil(t, errs) + if test.gdprScrub { + 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") + } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") } } @@ -352,6 +511,7 @@ func newBidRequest(t *testing.T) *openrtb.BidRequest { User: &openrtb.User{ ID: "our-id", BuyerUID: "their-id", + Yob: 1982, Ext: json.RawMessage(`{"digitrust":{"id":"digi-id","keyv":1,"pref":1}}`), }, Imp: []openrtb.Imp{{ diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index f05f25e87ea..05b2fb6d98e 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -276,7 +276,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { }, } - // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes and vendors 2, 6, 8 // PI needs all purposes to succeed testDefs := []tcf2TestDef{ { diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index 3d105dead44..0dbe9a69d9f 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -195,10 +195,10 @@ func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) { } } -// RecordTCFReq across all engines -func (me *MultiMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) { +// RecordRequestPrivacy across all engines +func (me *MultiMetricsEngine) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) { for _, thisME := range *me { - thisME.RecordTCFReq(version) + thisME.RecordRequestPrivacy(privacy) } } @@ -281,6 +281,6 @@ func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType p func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) { } -// RecordReq as a noop -func (me *DummyMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) { +// RecordRequestPrivacy as a noop +func (me *DummyMetricsEngine) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) { } diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go index 73eb30a1504..836434bf25e 100644 --- a/pbsmetrics/go_metrics.go +++ b/pbsmetrics/go_metrics.go @@ -53,7 +53,11 @@ type Metrics struct { TimeoutNotificationFailure metrics.Meter // TCF adaption metrics - TCFReqVersion map[TCFVersionValue]metrics.Meter + PrivacyCCPARequest metrics.Meter + PrivacyCCPARequestOptOut metrics.Meter + PrivacyCOPPARequest metrics.Meter + PrivacyLMTRequest metrics.Meter + PrivacyTCFRequestVersion map[TCFVersionValue]metrics.Meter AdapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics // Don't export accountMetrics because we need helper functions here to insure its properly populated dynamically @@ -141,7 +145,11 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa TimeoutNotificationSuccess: blankMeter, TimeoutNotificationFailure: blankMeter, - TCFReqVersion: make(map[TCFVersionValue]metrics.Meter, len(TCFVersions())), + PrivacyCCPARequest: blankMeter, + PrivacyCCPARequestOptOut: blankMeter, + PrivacyCOPPARequest: blankMeter, + PrivacyLMTRequest: blankMeter, + PrivacyTCFRequestVersion: make(map[TCFVersionValue]metrics.Meter, len(TCFVersions())), AdapterMetrics: make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)), accountMetrics: make(map[string]*accountMetrics), @@ -149,6 +157,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa exchanges: exchanges, } + for _, a := range exchanges { newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics() } @@ -166,7 +175,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa } for _, v := range TCFVersions() { - newMetrics.TCFReqVersion[v] = blankMeter + newMetrics.PrivacyTCFRequestVersion[v] = blankMeter } //to minimize memory usage, queuedTimeout metric is now supported for video endpoint only @@ -234,8 +243,12 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry) newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry) + newMetrics.PrivacyCCPARequest = metrics.GetOrRegisterMeter("privacy.request.ccpa.specified", registry) + newMetrics.PrivacyCCPARequestOptOut = metrics.GetOrRegisterMeter("privacy.request.ccpa.opt-out", registry) + newMetrics.PrivacyCOPPARequest = metrics.GetOrRegisterMeter("privacy.request.coppa", registry) + newMetrics.PrivacyLMTRequest = metrics.GetOrRegisterMeter("privacy.request.lmt", registry) for _, version := range TCFVersions() { - newMetrics.TCFReqVersion[version] = metrics.GetOrRegisterMeter(fmt.Sprintf("privacy.request.tcf.%s", string(version)), registry) + newMetrics.PrivacyTCFRequestVersion[version] = metrics.GetOrRegisterMeter(fmt.Sprintf("privacy.request.tcf.%s", string(version)), registry) } return newMetrics @@ -582,12 +595,28 @@ func (me *Metrics) RecordTimeoutNotice(success bool) { return } -func (me *Metrics) RecordTCFReq(version TCFVersionValue) { - met, ok := me.TCFReqVersion[version] - if ok { - met.Mark(1) - } else { - me.TCFReqVersion[TCFVersionErr].Mark(1) +func (me *Metrics) RecordRequestPrivacy(privacy PrivacyLabels) { + if privacy.CCPAProvided { + me.PrivacyCCPARequest.Mark(1) + if privacy.CCPAEnforced { + me.PrivacyCCPARequestOptOut.Mark(1) + } + } + + if privacy.COPPAEnforced { + me.PrivacyCOPPARequest.Mark(1) + } + + if privacy.GDPREnforced { + if metric, ok := me.PrivacyTCFRequestVersion[privacy.GDPRTCFVersion]; ok { + metric.Mark(1) + } else { + me.PrivacyTCFRequestVersion[TCFVersionErr].Mark(1) + } + } + + if privacy.LMTEnforced { + me.PrivacyLMTRequest.Mark(1) } return } diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go index 6d9eaf9f0e9..2faa08491e0 100644 --- a/pbsmetrics/go_metrics_test.go +++ b/pbsmetrics/go_metrics_test.go @@ -56,10 +56,14 @@ func TestNewMetrics(t *testing.T) { ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess) ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure) - ensureContains(t, registry, "privacy.request.tcf.v1", m.TCFReqVersion[TCFVersionV1]) - ensureContains(t, registry, "privacy.request.tcf.v2", m.TCFReqVersion[TCFVersionV2]) - ensureContains(t, registry, "privacy.request.tcf.err", m.TCFReqVersion[TCFVersionErr]) + ensureContains(t, registry, "privacy.request.ccpa.specified", m.PrivacyCCPARequest) + ensureContains(t, registry, "privacy.request.ccpa.opt-out", m.PrivacyCCPARequestOptOut) + ensureContains(t, registry, "privacy.request.coppa", m.PrivacyCOPPARequest) + ensureContains(t, registry, "privacy.request.lmt", m.PrivacyLMTRequest) + ensureContains(t, registry, "privacy.request.tcf.v1", m.PrivacyTCFRequestVersion[TCFVersionV1]) + ensureContains(t, registry, "privacy.request.tcf.v2", m.PrivacyTCFRequestVersion[TCFVersionV2]) + ensureContains(t, registry, "privacy.request.tcf.err", m.PrivacyTCFRequestVersion[TCFVersionErr]) } func TestRecordBidType(t *testing.T) { @@ -202,6 +206,61 @@ func TestRecordPrebidCacheRequestTimeWithNotSuccess(t *testing.T) { assert.Equal(t, m.PrebidCacheRequestTimerError.Count(), int64(1)) } +func TestRecordRequestPrivacy(t *testing.T) { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true}) + + // CCPA + m.RecordRequestPrivacy(PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: true, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: false, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + CCPAEnforced: false, + CCPAProvided: true, + }) + + // COPPA + m.RecordRequestPrivacy(PrivacyLabels{ + COPPAEnforced: true, + }) + + // LMT + m.RecordRequestPrivacy(PrivacyLabels{ + LMTEnforced: true, + }) + + // GDPR + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionErr, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionV1, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionV2, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionV1, + }) + + assert.Equal(t, m.PrivacyCCPARequest.Count(), int64(2), "CCPA") + assert.Equal(t, m.PrivacyCCPARequestOptOut.Count(), int64(1), "CCPA Opt Out") + assert.Equal(t, m.PrivacyCOPPARequest.Count(), int64(1), "COPPA") + assert.Equal(t, m.PrivacyLMTRequest.Count(), int64(1), "LMT") + assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionErr].Count(), int64(1), "TCF Err") + assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionV1].Count(), int64(2), "TCF V1") + assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionV2].Count(), int64(1), "TCF V2") +} + func ensureContainsBidTypeMetrics(t *testing.T, registry metrics.Registry, prefix string, mdm map[openrtb_ext.BidType]*MarkupDeliveryMetrics) { ensureContains(t, registry, prefix+".banner.adm_bids_received", mdm[openrtb_ext.BidTypeBanner].AdmMeter) ensureContains(t, registry, prefix+".banner.nurl_bids_received", mdm[openrtb_ext.BidTypeBanner].NurlMeter) diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index 0e94fe71e90..514fbac1015 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -41,6 +41,16 @@ type RequestLabels struct { RequestStatus RequestStatus } +// PrivacyLabels defines metrics describing the result of privacy enforcement. +type PrivacyLabels struct { + CCPAEnforced bool + CCPAProvided bool + COPPAEnforced bool + GDPREnforced bool + GDPRTCFVersion TCFVersionValue + LMTEnforced bool +} + // Label typecasting. Se below the type definitions for possible values // DemandSource : Demand source enumeration @@ -257,7 +267,7 @@ const ( TCFVersionV2 TCFVersionValue = "v2" ) -// TCFVersions rtuens the possible values for the TCF version +// TCFVersions returns the possible values for the TCF version func TCFVersions() []TCFVersionValue { return []TCFVersionValue{ TCFVersionErr, @@ -305,5 +315,5 @@ type MetricsEngine interface { RecordPrebidCacheRequestTime(success bool, length time.Duration) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) RecordTimeoutNotice(sucess bool) - RecordTCFReq(version TCFVersionValue) + RecordRequestPrivacy(privacy PrivacyLabels) } diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go index a6d36a72401..6c263f0af4d 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -107,7 +107,7 @@ func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) { me.Called(success) } -// RecordTCFReq mock -func (me *MetricsEngineMock) RecordTCFReq(version TCFVersionValue) { - me.Called(version) +// RecordRequestPrivacy mock +func (me *MetricsEngineMock) RecordRequestPrivacy(privacy PrivacyLabels) { + me.Called(privacy) } diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go index 19f4f225af9..ef1d300c4df 100644 --- a/pbsmetrics/prometheus/preload.go +++ b/pbsmetrics/prometheus/preload.go @@ -8,15 +8,16 @@ import ( func preloadLabelValues(m *Metrics) { var ( actionValues = actionsAsString() - adapterValues = adaptersAsString() adapterErrorValues = adapterErrorsAsString() + adapterValues = adaptersAsString() bidTypeValues = []string{markupDeliveryAdm, markupDeliveryNurl} boolValues = boolValuesAsString() cacheResultValues = cacheResultsAsString() - cookieValues = cookieTypesAsString() connectionErrorValues = []string{connectionAcceptError, connectionCloseError} + cookieValues = cookieTypesAsString() requestStatusValues = requestStatusesAsString() requestTypeValues = requestTypesAsString() + sourceValues = []string{sourceRequest} ) preloadLabelValuesForCounter(m.connectionsError, map[string][]string{ @@ -100,9 +101,22 @@ func preloadLabelValues(m *Metrics) { requestStatusLabel: {requestSuccessLabel, requestRejectLabel}, }) - preloadLabelValuesForCounter(m.tcfVersion, map[string][]string{ + preloadLabelValuesForCounter(m.privacyCCPA, map[string][]string{ + sourceLabel: sourceValues, + optOutLabel: boolValues, + }) + + preloadLabelValuesForCounter(m.privacyCOPPA, map[string][]string{ + sourceLabel: sourceValues, + }) + + preloadLabelValuesForCounter(m.privacyLMT, map[string][]string{ + sourceLabel: sourceValues, + }) + + preloadLabelValuesForCounter(m.privacyTCF, map[string][]string{ + sourceLabel: sourceValues, versionLabel: tcfVersionsAsString(), - sourceLabel: {string(sourceRequest)}, }) } diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index bf854746fd2..d94c4d78f62 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -29,7 +29,10 @@ type Metrics struct { storedImpressionsCacheResult *prometheus.CounterVec storedRequestCacheResult *prometheus.CounterVec timeoutNotifications *prometheus.CounterVec - tcfVersion *prometheus.CounterVec + privacyCCPA *prometheus.CounterVec + privacyCOPPA *prometheus.CounterVec + privacyLMT *prometheus.CounterVec + privacyTCF *prometheus.CounterVec // Adapter Metrics adapterBids *prometheus.CounterVec @@ -60,6 +63,7 @@ const ( isNativeLabel = "native" isVideoLabel = "video" markupDeliveryLabel = "delivery" + optOutLabel = "opt_out" privacyBlockedLabel = "privacy_blocked" requestStatusLabel = "request_status" requestTypeLabel = "request_type" @@ -165,11 +169,26 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of timeout notifications triggered, and if they were successfully sent.", []string{successLabel}) - metrics.tcfVersion = newCounter(cfg, metrics.Registry, + metrics.privacyCCPA = newCounter(cfg, metrics.Registry, + "privacy_ccpa", + "Count of total requests to Prebid Server where CCPA was provided by source and opt-out .", + []string{sourceLabel, optOutLabel}) + + metrics.privacyCOPPA = newCounter(cfg, metrics.Registry, + "privacy_coppa", + "Count of total requests to Prebid Server where the COPPA flag was set by source", + []string{sourceLabel}) + + metrics.privacyTCF = newCounter(cfg, metrics.Registry, "privacy_tcf", - "Count of TCF versions for requests where GDPR was enforced.", + "Count of TCF versions for requests where GDPR was enforced by source and version.", []string{versionLabel, sourceLabel}) + metrics.privacyLMT = newCounter(cfg, metrics.Registry, + "privacy_lmt", + "Count of total requests to Prebid Server where the LMT flag was set by source", + []string{sourceLabel}) + metrics.adapterBids = newCounter(cfg, metrics.Registry, "adapter_bids", "Count of bids labeled by adapter and markup delivery type (adm or nurl).", @@ -434,9 +453,30 @@ func (m *Metrics) RecordTimeoutNotice(success bool) { } } -func (m *Metrics) RecordTCFReq(version pbsmetrics.TCFVersionValue) { - m.tcfVersion.With(prometheus.Labels{ - versionLabel: string(version), - sourceLabel: sourceRequest, - }).Inc() +func (m *Metrics) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) { + if privacy.CCPAProvided { + m.privacyCCPA.With(prometheus.Labels{ + sourceLabel: sourceRequest, + optOutLabel: strconv.FormatBool(privacy.CCPAEnforced), + }).Inc() + } + + if privacy.COPPAEnforced { + m.privacyCOPPA.With(prometheus.Labels{ + sourceLabel: sourceRequest, + }).Inc() + } + + if privacy.GDPREnforced { + m.privacyTCF.With(prometheus.Labels{ + versionLabel: string(privacy.GDPRTCFVersion), + sourceLabel: sourceRequest, + }).Inc() + } + + if privacy.LMTEnforced { + m.privacyLMT.With(prometheus.Labels{ + sourceLabel: sourceRequest, + }).Inc() + } } diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index 03daff0d56b..b722ab28b5c 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -944,33 +944,96 @@ func TestTimeoutNotifications(t *testing.T) { } -func TestTCFMetrics(t *testing.T) { +func TestRecordRequestPrivacy(t *testing.T) { m := createMetricsForTesting() - m.RecordTCFReq(pbsmetrics.TCFVersionToValue(0)) - m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1)) - m.RecordTCFReq(pbsmetrics.TCFVersionToValue(2)) - m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1)) + // CCPA + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: true, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: false, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + CCPAEnforced: false, + CCPAProvided: true, + }) + + // COPPA + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + COPPAEnforced: true, + }) - assertCounterVecValue(t, "", "privacy_tcf:err", m.tcfVersion, + // LMT + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + LMTEnforced: true, + }) + + // GDPR + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionErr, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV2, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }) + + assertCounterVecValue(t, "", "privacy_ccpa", m.privacyCCPA, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + optOutLabel: "true", + }) + + assertCounterVecValue(t, "", "privacy_ccpa", m.privacyCCPA, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + optOutLabel: "false", + }) + + assertCounterVecValue(t, "", "privacy_coppa", m.privacyCOPPA, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + }) + + assertCounterVecValue(t, "", "privacy_lmt", m.privacyLMT, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + }) + + assertCounterVecValue(t, "", "privacy_tcf:err", m.privacyTCF, float64(1), prometheus.Labels{ - versionLabel: "err", sourceLabel: sourceRequest, + versionLabel: "err", }) - assertCounterVecValue(t, "", "privacy_tcf:v1", m.tcfVersion, + assertCounterVecValue(t, "", "privacy_tcf:v1", m.privacyTCF, float64(2), prometheus.Labels{ - versionLabel: "v1", sourceLabel: sourceRequest, + versionLabel: "v1", }) - assertCounterVecValue(t, "", "privacy_tcf:v2", m.tcfVersion, + assertCounterVecValue(t, "", "privacy_tcf:v2", m.privacyTCF, float64(1), prometheus.Labels{ - versionLabel: "v2", sourceLabel: sourceRequest, + versionLabel: "v2", }) } From 0ccb77388da8d96fe6e1ee512d17fa415e607c70 Mon Sep 17 00:00:00 2001 From: Daniel Barrigas Date: Fri, 17 Jul 2020 16:32:22 +0100 Subject: [PATCH 145/381] Parse Site.Publisher.ID from Amp Auction HTTP Req Query Parameter "account" (#1403) --- endpoints/openrtb2/amp_auction.go | 8 ++++++++ endpoints/openrtb2/amp_auction_test.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index e8b5d3ecc76..8efba5a926c 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -388,6 +388,14 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope setAmpExt(req.Site, "1") + account := httpRequest.FormValue("account") + if account != "" { + if req.Site.Publisher == nil { + req.Site.Publisher = &openrtb.Publisher{} + } + req.Site.Publisher.ID = account + } + slot := httpRequest.FormValue("slot") if slot != "" { req.Imp[0].TagID = slot diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 731fd55e196..692d3fb0c5d 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -755,8 +755,9 @@ func TestQueryParamOverrides(t *testing.T) { curl := "http://example.com" slot := "1234" timeout := int64(500) + account := "12345" - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&debug=1&curl=%s&slot=%s&timeout=%d", requestID, curl, slot, timeout), nil) + request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&debug=1&curl=%s&slot=%s&timeout=%d&account=%s", requestID, curl, slot, timeout, account), nil) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -784,6 +785,10 @@ func TestQueryParamOverrides(t *testing.T) { if resolvedRequest.Site == nil || resolvedRequest.Site.Page != curl { t.Errorf("Expected Site.Page to equal curl (%s), got: %s", curl, resolvedRequest.Site.Page) } + + if resolvedRequest.Site == nil || resolvedRequest.Site.Publisher == nil || resolvedRequest.Site.Publisher.ID != account { + t.Errorf("Expected Site.Publisher.ID to equal (%s), got: %s", account, resolvedRequest.Site.Publisher.ID) + } } func TestOverrideDimensions(t *testing.T) { @@ -876,6 +881,7 @@ type formatOverrideSpec struct { overrideWidth uint64 overrideHeight uint64 multisize string + account string expect []openrtb.Format } @@ -897,7 +903,7 @@ func (s formatOverrideSpec) execute(t *testing.T) { openrtb_ext.BidderMap, ) - url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize) + url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s&account=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize, s.account) request := httptest.NewRequest("GET", url, nil) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) From bfcfefe2d225ad7b09a80567f3a19e4be5f4305a Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Fri, 17 Jul 2020 13:05:19 -0400 Subject: [PATCH 146/381] Facebook Only Supports App Impressions (#1396) --- .../exemplary/banner-site.json | 132 ------------------ .../exemplary/interstitial.json | 12 +- .../exemplary/native-1.1.json | 12 +- .../audienceNetworktest/exemplary/video.json | 12 +- .../supplemental/banner-format-only.json | 12 +- .../supplemental/invalid-adm.json | 12 +- .../supplemental/invalid-banner-height.json | 6 +- .../supplemental/invalid-interstitial.json | 6 +- .../supplemental/missing-adm-bidid.json | 12 +- .../supplemental/missing-adm.json | 12 +- .../supplemental/missing-banner-height.json | 6 +- .../supplemental/multi-imp.json | 18 +-- .../supplemental/no-bid-204.json | 12 +- .../supplemental/no-imps.json | 6 +- .../supplemental/required-buyeruid.json | 6 +- .../required-param-placementId.json | 6 +- .../required-param-publisherId.json | 6 +- .../supplemental/server-error-500.json | 12 +- .../supplemental/site-not-supported.json | 38 +++++ .../supplemental/split-placementId.json | 12 +- adapters/audienceNetwork/facebook.go | 20 ++- adapters/audienceNetwork/facebook_test.go | 17 --- static/bidder-info/audienceNetwork.yaml | 5 - 23 files changed, 137 insertions(+), 255 deletions(-) delete mode 100644 adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json create mode 100644 adapters/audienceNetwork/audienceNetworktest/supplemental/site-not-supported.json diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json deleted file mode 100644 index 01bab3dfd71..00000000000 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-req-id", - "imp": [ - { - "id": "test-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "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", - "banner": { - "w": -1, - "h": 250 - }, - "tagid": "123_456" - } - ], - "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": 200, - "body": { - "id": "test-imp-id", - "seatbid": [ - { - "bid": [ - { - "id": "987", - "impid": "test-imp-id", - "price": 1.000000, - "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", - "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" - } - ] - } - ], - "bidid": "654", - "cur": "USD" - } - } - } - ], - "expectedBidResponses": [ - { - "currency": "USD", - "bids": [ - { - "bid": { - "id": "987", - "impid": "test-imp-id", - "price": 1, - "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", - "adid": "987", - "crid": "987", - "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" - }, - "type": "banner" - } - ] - } - ] -} diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json index 9f563f11948..573032c81e1 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json @@ -23,9 +23,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -64,9 +64,9 @@ "tagid": "123_456" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json index 16bed344767..08639bee013 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json @@ -16,9 +16,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -56,9 +56,9 @@ "tagid": "123_456" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json index 5ece0f08530..35bdf9a443e 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json @@ -21,9 +21,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -66,9 +66,9 @@ "tagid": "123_456" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json index 5469fefbd65..450e0d9e45b 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json @@ -24,9 +24,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -64,9 +64,9 @@ "tagid": "123_456" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json index f145f5fe4ce..c33807bda74 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json @@ -18,9 +18,9 @@ } } }], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -55,9 +55,9 @@ }, "tagid": "123_456" }], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json index fa9fd9132b8..b229d41a27a 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json @@ -22,9 +22,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json index ad19d94c6e9..68ca8044812 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json @@ -20,9 +20,9 @@ } } }], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json index b57c900104e..50212155752 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json @@ -18,9 +18,9 @@ } } }], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -55,9 +55,9 @@ }, "tagid": "123_456" }], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json index 23227aab959..832b16dca22 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json @@ -18,9 +18,9 @@ } } }], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -55,9 +55,9 @@ }, "tagid": "123_456" }], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json index 016e8de0ef0..0793f990049 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json @@ -20,9 +20,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json index 231c2826548..682c33e46b8 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json @@ -41,9 +41,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -81,9 +81,9 @@ "tagid": "pub1_plmt1" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "pub1" } @@ -152,9 +152,9 @@ "tagid": "pub2_plmt2" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "pub2" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json index 45b35e05dd9..642e495810a 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json @@ -16,9 +16,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -56,9 +56,9 @@ "tagid": "123_456" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json index 7420f7e8fb2..fccdf71ca4a 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json @@ -2,9 +2,9 @@ "mockBidRequest": { "id": "test-req-id", "imp": [], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json index 964dcb48b48..72b4fbacdd1 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json @@ -26,9 +26,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json index a9c3c23d298..f13b70e1be2 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json @@ -25,9 +25,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json index c50f3d36378..a80a1e09b65 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json @@ -25,9 +25,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json index 7ff8886139a..f0a11905cf8 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json @@ -14,9 +14,9 @@ } } }], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -51,9 +51,9 @@ }, "tagid": "123_456" }], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/site-not-supported.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/site-not-supported.json new file mode 100644 index 00000000000..9155352a192 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/site-not-supported.json @@ -0,0 +1,38 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "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 + }, + "expectedMakeRequestsErrors": [{ + "value": "Site impressions are not supported.", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json index 34c1eccc58e..45c34192ea2 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json @@ -21,9 +21,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -50,9 +50,9 @@ "tagid": "123_456" } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go index f4091e4e23c..d9f6719fd17 100644 --- a/adapters/audienceNetwork/facebook.go +++ b/adapters/audienceNetwork/facebook.go @@ -54,6 +54,12 @@ func (this *FacebookAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo * }} } + if request.Site != nil { + return nil, []error{&errortypes.BadInput{ + Message: "Site impressions are not supported.", + }} + } + return this.buildRequests(request) } @@ -143,10 +149,6 @@ func (this *FacebookAdapter) modifyRequest(out *openrtb.BidRequest) error { app := *out.App app.Publisher = &openrtb.Publisher{ID: pubId} out.App = &app - } else { - site := *out.Site - site.Publisher = &openrtb.Publisher{ID: pubId} - out.Site = &site } if err = this.modifyImp(imp); err != nil { @@ -468,15 +470,11 @@ func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (* 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 + // The publisher ID is expected in the app object 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"), - } + return &adapters.RequestData{}, []error{ + errors.New("path app.publisher.id not found in the request"), } } diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go index 7f567d6137b..912f12223f8 100644 --- a/adapters/audienceNetwork/facebook_test.go +++ b/adapters/audienceNetwork/facebook_test.go @@ -61,23 +61,6 @@ func TestMakeTimeoutNoticeApp(t *testing.T) { 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("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"}}`), diff --git a/static/bidder-info/audienceNetwork.yaml b/static/bidder-info/audienceNetwork.yaml index 56230bf3f9a..324e5c6dff8 100644 --- a/static/bidder-info/audienceNetwork.yaml +++ b/static/bidder-info/audienceNetwork.yaml @@ -1,11 +1,6 @@ maintainer: email: "none" capabilities: - site: - mediaTypes: - - banner - - video - - native app: mediaTypes: - banner From c889570b14dac808f95fd46d7254d124e7b0c226 Mon Sep 17 00:00:00 2001 From: Ad Generation Date: Sat, 18 Jul 2020 02:39:31 +0900 Subject: [PATCH 147/381] fix: Change currency of ad-generation's bidResponse according to bidRequest (#1383) --- adapters/adgeneration/adgeneration.go | 3 +- adapters/adgeneration/adgeneration_test.go | 63 +++++++++++++++++++ .../exemplary/single-banner.json | 2 +- .../supplemental/204-bid-response.json | 2 +- .../supplemental/400-bid-response.json | 2 +- .../supplemental/no-bid-response.json | 2 +- 6 files changed, 69 insertions(+), 5 deletions(-) diff --git a/adapters/adgeneration/adgeneration.go b/adapters/adgeneration/adgeneration.go index 4b1215dea9d..054fa7f6df3 100644 --- a/adapters/adgeneration/adgeneration.go +++ b/adapters/adgeneration/adgeneration.go @@ -210,6 +210,7 @@ func (adg *AdgenerationAdapter) MakeBids(internalRequest *openrtb.BidRequest, ex Bid: &bid, BidType: bitType, }) + bidResponse.Currency = adg.getCurrency(internalRequest) return bidResponse, nil } } @@ -254,7 +255,7 @@ func removeWrapper(ad string) string { func NewAdgenerationAdapter(endpoint string) *AdgenerationAdapter { return &AdgenerationAdapter{ endpoint, - "1.0.0", + "1.0.1", "JPY", } } diff --git a/adapters/adgeneration/adgeneration_test.go b/adapters/adgeneration/adgeneration_test.go index e76995fc5e4..3c795ea28a8 100644 --- a/adapters/adgeneration/adgeneration_test.go +++ b/adapters/adgeneration/adgeneration_test.go @@ -5,7 +5,9 @@ import ( "testing" "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/stretchr/testify/assert" ) func TestJsonSamples(t *testing.T) { @@ -174,3 +176,64 @@ func TestCreateAd(t *testing.T) { t.Errorf("%v does not match createAd.", adgVastResponse) } } + +func TestMakeBids(t *testing.T) { + bidder := NewAdgenerationAdapter("https://d.socdm.com/adsv/v1") + internalRequest := &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"}, + } + externalRequest := adapters.RequestData{} + response := adapters.ResponseData{ + StatusCode: 200, + Body: ([]byte)("{\n \"ad\": \"testAd\",\n \"cpm\": 30,\n \"creativeid\": \"Dummy_supership.jp\",\n \"h\": 250,\n \"locationid\": \"58278\",\n \"results\": [{}],\n \"dealid\": \"test-deal-id\",\n \"w\": 300\n }"), + } + // default Currency InternalRequest + defaultCurBidderResponse, errs := bidder.MakeBids(internalRequest, &externalRequest, &response) + if len(errs) > 0 { + t.Errorf("MakeBids return errors. errors: %v", errs) + } + checkBidResponse(t, defaultCurBidderResponse, bidder.defaultCurrency) + + // Specified Currency InternalRequest + usdCur := "USD" + internalRequest.Cur = []string{usdCur} + specifiedCurBidderResponse, errs := bidder.MakeBids(internalRequest, &externalRequest, &response) + if len(errs) > 0 { + t.Errorf("MakeBids return errors. errors: %v", errs) + } + checkBidResponse(t, specifiedCurBidderResponse, usdCur) + +} + +func checkBidResponse(t *testing.T, bidderResponse *adapters.BidderResponse, expectedCurrency string) { + if bidderResponse == nil { + t.Errorf("actual bidResponse is nil.") + } + + // AdM is assured by TestCreateAd and JSON tests + var expectedAdM string = "testAd" + var expectedID string = "58278" + var expectedImpID = "bidRequest-success-test" + var expectedPrice float64 = 30.0 + var expectedW uint64 = 300 + var expectedH uint64 = 250 + var expectedCrID string = "Dummy_supership.jp" + var extectedDealID string = "test-deal-id" + + assert.Equal(t, expectedCurrency, bidderResponse.Currency) + assert.Equal(t, 1, len(bidderResponse.Bids)) + assert.Equal(t, expectedID, bidderResponse.Bids[0].Bid.ID) + assert.Equal(t, expectedImpID, bidderResponse.Bids[0].Bid.ImpID) + assert.Equal(t, expectedAdM, bidderResponse.Bids[0].Bid.AdM) + assert.Equal(t, expectedPrice, bidderResponse.Bids[0].Bid.Price) + assert.Equal(t, expectedW, bidderResponse.Bids[0].Bid.W) + assert.Equal(t, expectedH, bidderResponse.Bids[0].Bid.H) + assert.Equal(t, expectedCrID, bidderResponse.Bids[0].Bid.CrID) + assert.Equal(t, extectedDealID, bidderResponse.Bids[0].Bid.DealID) +} diff --git a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json index d23a510bee5..10bf1c4a0c0 100644 --- a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json +++ b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json @@ -52,7 +52,7 @@ "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", + "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1¤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" diff --git a/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json index cf8635bbc3d..bc469a1e3a9 100644 --- a/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json +++ b/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json @@ -52,7 +52,7 @@ "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", + "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1¤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" diff --git a/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json index f5dc7fe0af5..6ac92d9a38b 100644 --- a/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json +++ b/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json @@ -52,7 +52,7 @@ "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", + "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1¤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" diff --git a/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json index 399f85a5856..a0abb66d039 100644 --- a/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json +++ b/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json @@ -52,7 +52,7 @@ "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", + "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1¤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" From 6b7c113b76ed202c9022d063bc5b713ae53ae0a6 Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Fri, 17 Jul 2020 22:50:22 -0400 Subject: [PATCH 148/381] Adding primary categories to freewheel mapping (#1407) --- .../category-mapping/freewheel/freewheel.json | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/static/category-mapping/freewheel/freewheel.json b/static/category-mapping/freewheel/freewheel.json index 1c4a4fa2471..11529206087 100644 --- a/static/category-mapping/freewheel/freewheel.json +++ b/static/category-mapping/freewheel/freewheel.json @@ -1178,5 +1178,81 @@ "IAB22-3": { "id": "410", "name": "Product" + }, + "IAB1": { + "id": "392", + "name": "Entertainment" + }, + "IAB2": { + "id": "399", + "name": "Automotive" + }, + "IAB3": { + "id": "393", + "name": "Business Services" + }, + "IAB4": { + "id": "405", + "name": "Educational Services" + }, + "IAB5": { + "id": "405", + "name": "Educational Services" + }, + "IAB7": { + "id": "406", + "name": "Health Care Services" + }, + "IAB8": { + "id": "394", + "name": "Food" + }, + "IAB9": { + "id": "392", + "name": "Entertainment" + }, + "IAB10": { + "id": "434", + "name": "Home Furnishings" + }, + "IAB11": { + "id": "398", + "name": "Government/Municipal" + }, + "IAB12": { + "id": "438", + "name": "News" + }, + "IAB13": { + "id": "393", + "name": "Business Services" + }, + "IAB16": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB17": { + "id": "425", + "name": "Professional Sports" + }, + "IAB18": { + "id": "397", + "name": "Apparel" + }, + "IAB19": { + "id": "409", + "name": "Computing Product" + }, + "IAB20": { + "id": "395", + "name": "Travel/Hotel/Airlines" + }, + "IAB21": { + "id": "416", + "name": "Real Estate" + }, + "IAB22": { + "id": "403", + "name": "Retail Stores/Chains" } } \ No newline at end of file From a5962de9a5900f3b205dfac1263f53d7daf96eec Mon Sep 17 00:00:00 2001 From: guscarreon Date: Wed, 22 Jul 2020 13:11:25 -0400 Subject: [PATCH 149/381] Add Outgoing Connection Metrics (#1343) --- config/config.go | 6 + config/config_test.go | 3 + exchange/adapter_map.go | 2 +- exchange/bidder.go | 77 ++++++++-- exchange/bidder_test.go | 130 ++++++++++++++--- exchange/targeting_test.go | 2 +- go.mod | 1 + go.sum | 5 + pbsmetrics/config/metrics.go | 25 +++- pbsmetrics/go_metrics.go | 52 ++++++- pbsmetrics/go_metrics_test.go | 139 ++++++++++++++++++ pbsmetrics/metrics.go | 2 + pbsmetrics/metrics_mock.go | 10 ++ pbsmetrics/prometheus/preload.go | 14 ++ pbsmetrics/prometheus/prometheus.go | 105 +++++++++++--- pbsmetrics/prometheus/prometheus_test.go | 174 ++++++++++++++++++++++- 16 files changed, 683 insertions(+), 64 deletions(-) diff --git a/config/config.go b/config/config.go index 2e7f875b023..a82dbb5edf7 100755 --- a/config/config.go +++ b/config/config.go @@ -379,6 +379,11 @@ type Metrics struct { type DisabledMetrics struct { // True if we want to stop collecting account-to-adapter metrics AccountAdapterDetails bool `mapstructure:"account_adapter_details"` + + // True if we don't want to collect metrics about the connections prebid + // server establishes with bidder servers such as the number of connections + // that were created or reused. + AdapterConnectionMetrics bool `mapstructure:"adapter_connections_metrics"` } func (cfg *Metrics) validate(errs configErrors) configErrors { @@ -688,6 +693,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.disabled_metrics.adapter_connections_metrics", true) v.SetDefault("metrics.influxdb.host", "") v.SetDefault("metrics.influxdb.database", "") v.SetDefault("metrics.influxdb.username", "") diff --git a/config/config_test.go b/config/config_test.go index 3456694db5c..4774d9d6e46 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -33,6 +33,7 @@ func TestDefaults(t *testing.T) { cmpBools(t, "account_required", cfg.AccountRequired, false) cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20) cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false) + cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true) cmpStrings(t, "certificates_file", cfg.PemCertsFile, "") } @@ -89,6 +90,7 @@ metrics: metric_send_interval: 30 disabled_metrics: account_adapter_details: true + adapter_connections_metrics: true datacache: type: postgres filename: /usr/db/db.db @@ -294,6 +296,7 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "adapters.rhythmone.usersync_url", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].UserSyncURL, "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=http%3A%2F%2Fprebid-server.prebid.org%2F%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BRX_UUID%5D") cmpBools(t, "account_required", cfg.AccountRequired, true) cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true) + cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true) cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem") cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24") cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 53607ac57d8..2ecddb83cfc 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -201,7 +201,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, cfg, me) + allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client, cfg, me, name) } } diff --git a/exchange/bidder.go b/exchange/bidder.go index ee6a4942147..7c39b72b348 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/http/httptrace" "time" "github.com/golang/glog" @@ -87,20 +88,30 @@ 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, cfg *config.Configuration, me pbsmetrics.MetricsEngine) adaptedBidder { +func adaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me pbsmetrics.MetricsEngine, name openrtb_ext.BidderName) adaptedBidder { return &bidderAdapter{ - Bidder: bidder, - Client: client, - DebugConfig: cfg.Debug, - me: me, + Bidder: bidder, + BidderName: name, + Client: client, + me: me, + config: bidderAdapterConfig{ + Debug: cfg.Debug, + DisableConnMetrics: cfg.Metrics.Disabled.AdapterConnectionMetrics, + }, } } type bidderAdapter struct { - Bidder adapters.Bidder - Client *http.Client - DebugConfig config.Debug - me pbsmetrics.MetricsEngine + Bidder adapters.Bidder + BidderName openrtb_ext.BidderName + Client *http.Client + me pbsmetrics.MetricsEngine + config bidderAdapterConfig +} + +type bidderAdapterConfig struct { + Debug config.Debug + DisableConnMetrics bool } func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) { @@ -325,6 +336,11 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re } httpReq.Header = req.Headers + // If adapter connection metrics are not disabled, add the client trace + // to get complete connection info into our metrics + if !bidder.config.DisableConnMetrics { + ctx = bidder.addClientTrace(ctx) + } httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq) if err != nil { if err == context.DeadlineExceeded { @@ -387,7 +403,7 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou 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) { + if bidder.config.Debug.TimeoutNotification.Log && !(bidder.config.Debug.TimeoutNotification.FailOnly && success) { var msg string if err == nil { msg = fmt.Sprintf("TimeoutNotification: status:(%d) body:%s", httpResp.StatusCode, string(toReq.Body)) @@ -395,16 +411,16 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou 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, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate) + util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate) } } else { bidder.me.RecordTimeoutNotice(false) - if bidder.DebugConfig.TimeoutNotification.Log { + if bidder.config.Debug.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, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate) + util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate) } } - } else if bidder.DebugConfig.TimeoutNotification.Log { + } else if bidder.config.Debug.TimeoutNotification.Log { reqJSON, err := json.Marshal(req) var msg string if err == nil { @@ -412,7 +428,7 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou } 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, logger, bidder.DebugConfig.TimeoutNotification.SamplingRate) + util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate) } } @@ -422,3 +438,34 @@ type httpCallInfo struct { response *adapters.ResponseData err error } + +// This function adds an httptrace.ClientTrace object to the context so, if connection with the bidder +// endpoint is established, we can keep track of whether the connection was newly created, reused, and +// the time from the connection request, to the connection creation. +func (bidder *bidderAdapter) addClientTrace(ctx context.Context) context.Context { + var connStart, dnsStart time.Time + + trace := &httptrace.ClientTrace{ + // GetConn is called before a connection is created or retrieved from an idle pool + GetConn: func(hostPort string) { + connStart = time.Now() + }, + // GotConn is called after a successful connection is obtained + GotConn: func(info httptrace.GotConnInfo) { + connWaitTime := time.Now().Sub(connStart) + + bidder.me.RecordAdapterConnections(bidder.BidderName, info.Reused, connWaitTime) + }, + // DNSStart is called when a DNS lookup begins. + DNSStart: func(info httptrace.DNSStartInfo) { + dnsStart = time.Now() + }, + // DNSDone is called when a DNS lookup ends. + DNSDone: func(info httptrace.DNSDoneInfo) { + dnsLookupTime := time.Now().Sub(dnsStart) + + bidder.me.RecordDNSTime(dnsLookupTime) + }, + } + return httptrace.WithClientTrace(ctx, trace) +} diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index d4fc0cf7cd3..1a27b72aa12 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -6,8 +6,11 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" "net/http" "net/http/httptest" + "net/http/httptrace" + "strings" "testing" "time" @@ -17,8 +20,11 @@ import ( "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/currencies" "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/pbsmetrics" + metricsConf "github.com/prebid/prebid-server/pbsmetrics/config" metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" nativeRequests "github.com/mxmCherry/openrtb/native/request" nativeResponse "github.com/mxmCherry/openrtb/native/response" @@ -68,7 +74,7 @@ func TestSingleBidder(t *testing.T) { }, bidResponse: mockBidderResponse, } - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverterDefault() seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) @@ -156,7 +162,7 @@ func TestMultiBidder(t *testing.T) { }}, bidResponse: mockBidderResponse, } - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverterDefault() seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) @@ -194,8 +200,10 @@ func TestBidderTimeout(t *testing.T) { defer server.Close() bidder := &bidderAdapter{ - Bidder: &mixedMultiBidder{}, - Client: server.Client(), + Bidder: &mixedMultiBidder{}, + BidderName: openrtb_ext.BidderAppnexus, + Client: server.Client(), + me: &metricsConf.DummyMetricsEngine{}, } callInfo := bidder.doRequest(ctx, &adapters.RequestData{ @@ -235,8 +243,10 @@ func TestConnectionClose(t *testing.T) { server = httptest.NewServer(handler) bidder := &bidderAdapter{ - Bidder: &mixedMultiBidder{}, - Client: server.Client(), + Bidder: &mixedMultiBidder{}, + Client: server.Client(), + BidderName: openrtb_ext.BidderAppnexus, + me: &metricsConf.DummyMetricsEngine{}, } callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{ @@ -514,7 +524,7 @@ func TestMultiCurrencies(t *testing.T) { ) // Execute: - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, @@ -663,7 +673,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { } // Execute: - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverterDefault() seatBid, errs := bidder.requestBid( context.Background(), @@ -829,7 +839,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { } // Execute: - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, @@ -945,7 +955,7 @@ func TestServerCallDebugging(t *testing.T) { Headers: http.Header{}, }, } - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverterDefault() bids, _ := bidder.requestBid( @@ -1057,7 +1067,7 @@ func TestMobileNativeTypes(t *testing.T) { }, bidResponse: tc.mockBidderResponse, } - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverterDefault() seatBids, _ := bidder.requestBid( @@ -1078,7 +1088,7 @@ func TestMobileNativeTypes(t *testing.T) { } func TestErrorReporting(t *testing.T) { - bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverterDefault() bids, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if bids != nil { @@ -1233,6 +1243,82 @@ func TestSetAssetTypes(t *testing.T) { } } +func TestCallRecordAdapterConnections(t *testing.T) { + // Setup mock server + respStatus := 200 + respBody := "{\"bid\":false}" + server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + defer server.Close() + + // declare requestBid parameters + bidAdjustment := 2.0 + + bidderImpl := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{}, + } + + // setup a mock metrics engine and its expectation + metrics := &pbsmetrics.MetricsEngineMock{} + expectedAdapterName := openrtb_ext.BidderAppnexus + compareConnWaitTime := func(dur time.Duration) bool { return dur.Nanoseconds() > 0 } + + metrics.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once() + + // Run requestBid using an http.Client with a mock handler + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, metrics, openrtb_ext.BidderAppnexus) + _, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencies.NewRateConverterDefault().Rates(), &adapters.ExtraRequestInfo{}) + + // Assert no errors + assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs) + + // Assert RecordAdapterConnections() was called with the parameters we expected + metrics.AssertExpectations(t) +} + +type DNSDoneTripper struct{} + +func (DNSDoneTripper) RoundTrip(req *http.Request) (*http.Response, error) { + //Access the httptrace.ClientTrace + trace := httptrace.ContextClientTrace(req.Context()) + + //Force DNSDone call defined in exchange/bidder.go + trace.DNSDone(httptrace.DNSDoneInfo{}) + + resp := &http.Response{ + StatusCode: 200, + Header: map[string][]string{"Location": {"http://www.example.com/"}}, + Body: ioutil.NopCloser(strings.NewReader("postBody")), + } + return resp, nil +} + +func TestCallRecordRecordDNSTime(t *testing.T) { + // setup a mock metrics engine and its expectation + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return() + + // Instantiate the bidder that will send the request. We'll make sure to use an + // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) + // gets called + bidder := &bidderAdapter{ + Bidder: &mixedMultiBidder{}, + Client: &http.Client{Transport: DNSDoneTripper{}}, + me: metricsMock, + } + + // Run test + bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}) + + // Tried one or another, none seem to work without panicking + metricsMock.AssertExpectations(t) +} + func TestTimeoutNotificationOff(t *testing.T) { respBody := "{\"bid\":false}" respStatus := 200 @@ -1248,10 +1334,10 @@ func TestTimeoutNotificationOff(t *testing.T) { }, } bidder := &bidderAdapter{ - Bidder: bidderImpl, - Client: server.Client(), - DebugConfig: config.Debug{}, - me: &metricsConfig.DummyMetricsEngine{}, + Bidder: bidderImpl, + Client: server.Client(), + config: bidderAdapterConfig{Debug: config.Debug{}}, + me: &metricsConf.DummyMetricsEngine{}, } if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok { t.Error("Failed to cast bidder to a TimeoutBidder") @@ -1284,13 +1370,15 @@ func TestTimeoutNotificationOn(t *testing.T) { bidderAdapter := &bidderAdapter{ Bidder: bidderWrappedWithInfo, Client: server.Client(), - DebugConfig: config.Debug{ - TimeoutNotification: config.TimeoutNotification{ - Log: true, - SamplingRate: 1.0, + config: bidderAdapterConfig{ + Debug: config.Debug{ + TimeoutNotification: config.TimeoutNotification{ + Log: true, + SamplingRate: 1.0, + }, }, }, - me: &metricsConfig.DummyMetricsEngine{}, + me: &metricsConf.DummyMetricsEngine{}, } // Unwrap To Mimic exchange.go Casting Code diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index 72de1d4261f..16955e97c5b 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -134,7 +134,7 @@ func buildAdapterMap(bids map[openrtb_ext.BidderName][]*openrtb.Bid, mockServerU adapterMap[bidder] = adaptBidder(&mockTargetingBidder{ mockServerURL: mockServerURL, bids: bids, - }, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + }, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) } return adapterMap } diff --git a/go.mod b/go.mod index 72bb9b74886..00cadd31ce1 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/influxdata/influxdb v1.6.1 // indirect github.com/julienschmidt/httprouter v1.1.0 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect + github.com/kr/pretty v0.2.0 // indirect github.com/lib/pq v1.0.0 github.com/magiconair/properties v1.8.0 github.com/mattn/go-colorable v0.1.2 // indirect diff --git a/go.sum b/go.sum index 35b2b76591d..5eaf37cad9f 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,11 @@ github.com/julienschmidt/httprouter v1.1.0 h1:7wLdtIiIpzOkC9u6sXOozpBauPdskj3ru4 github.com/julienschmidt/httprouter v1.1.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index 0dbe9a69d9f..6a36f9e71c0 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -37,7 +37,7 @@ func NewMetricsEngine(cfg *config.Configuration, adapterList []openrtb_ext.Bidde } if cfg.Metrics.Prometheus.Port != 0 { // Set up the Prometheus metrics. - returnEngine.PrometheusMetrics = prometheusmetrics.NewMetrics(cfg.Metrics.Prometheus) + returnEngine.PrometheusMetrics = prometheusmetrics.NewMetrics(cfg.Metrics.Prometheus, cfg.Metrics.Disabled) engineList = append(engineList, returnEngine.PrometheusMetrics) } @@ -118,6 +118,21 @@ func (me *MultiMetricsEngine) RecordAdapterRequest(labels pbsmetrics.AdapterLabe } } +// Keeps track of created and reused connections to adapter bidders and the time from the +// connection request, to the connection creation, or reuse from the pool across all engines +func (me *MultiMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { + for _, thisME := range *me { + thisME.RecordAdapterConnections(bidderName, connWasReused, connWaitTime) + } +} + +// Times the DNS resolution process +func (me *MultiMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { + for _, thisME := range *me { + thisME.RecordDNSTime(dnsLookupTime) + } +} + // RecordAdapterBidReceived across all engines func (me *MultiMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { for _, thisME := range *me { @@ -237,6 +252,14 @@ func (me *DummyMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels func (me *DummyMetricsEngine) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) { } +// RecordAdapterConnections as a noop +func (me *DummyMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { +} + +// RecordDNSTime as a noop +func (me *DummyMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { +} + // RecordAdapterBidReceived as a noop func (me *DummyMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { } diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go index 836434bf25e..26f6ce07b29 100644 --- a/pbsmetrics/go_metrics.go +++ b/pbsmetrics/go_metrics.go @@ -29,6 +29,7 @@ type Metrics struct { PrebidCacheRequestTimerError metrics.Timer StoredReqCacheMeter map[CacheResult]metrics.Meter StoredImpCacheMeter map[CacheResult]metrics.Meter + DNSLookupTimer metrics.Timer // Metrics for OpenRTB requests specifically. So we can track what % of RequestsMeter are OpenRTB // and know when legacy requests have been abandoned. @@ -81,6 +82,9 @@ type AdapterMetrics struct { BidsReceivedMeter metrics.Meter PanicMeter metrics.Meter MarkupMetrics map[openrtb_ext.BidType]*MarkupDeliveryMetrics + ConnCreated metrics.Counter + ConnReused metrics.Counter + ConnWaitTime metrics.Timer } type MarkupDeliveryMetrics struct { @@ -106,7 +110,7 @@ const unknownBidder openrtb_ext.BidderName = "unknown" // rather than loading legacy metrics that never get filled. // This will also eventually let us configure metrics, such as setting a limited set of metrics // for a production instance, and then expanding again when we need more debugging. -func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disableMetrics config.DisabledMetrics) *Metrics { +func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disabledMetrics config.DisabledMetrics) *Metrics { blankMeter := &metrics.NilMeter{} blankTimer := &metrics.NilTimer{} @@ -123,6 +127,7 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa SafariRequestMeter: blankMeter, SafariNoCookieMeter: blankMeter, RequestTimer: blankTimer, + DNSLookupTimer: blankTimer, RequestsQueueTimer: make(map[RequestType]map[bool]metrics.Timer), PrebidCacheRequestTimerSuccess: blankTimer, PrebidCacheRequestTimerError: blankTimer, @@ -153,13 +158,13 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa AdapterMetrics: make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)), accountMetrics: make(map[string]*accountMetrics), - MetricsDisabled: disableMetrics, + MetricsDisabled: disabledMetrics, exchanges: exchanges, } for _, a := range exchanges { - newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics() + newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics(newMetrics.MetricsDisabled) } for _, t := range RequestTypes() { @@ -209,6 +214,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d newMetrics.AppRequestMeter = metrics.GetOrRegisterMeter("app_requests", registry) newMetrics.SafariNoCookieMeter = metrics.GetOrRegisterMeter("safari_no_cookie_requests", registry) newMetrics.RequestTimer = metrics.GetOrRegisterTimer("request_time", registry) + newMetrics.DNSLookupTimer = metrics.GetOrRegisterTimer("dns_lookup_time", registry) newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry) newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry) @@ -255,7 +261,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d } // Part of setting up blank metrics, the adapter metrics. -func makeBlankAdapterMetrics() *AdapterMetrics { +func makeBlankAdapterMetrics(disabledMetrics config.DisabledMetrics) *AdapterMetrics { blankMeter := &metrics.NilMeter{} newAdapter := &AdapterMetrics{ NoCookieMeter: blankMeter, @@ -268,6 +274,11 @@ func makeBlankAdapterMetrics() *AdapterMetrics { PanicMeter: blankMeter, MarkupMetrics: makeBlankBidMarkupMetrics(), } + if !disabledMetrics.AdapterConnectionMetrics { + newAdapter.ConnCreated = metrics.NilCounter{} + newAdapter.ConnReused = metrics.NilCounter{} + newAdapter.ConnWaitTime = &metrics.NilTimer{} + } for _, err := range AdapterErrors() { newAdapter.ErrorMeters[err] = blankMeter } @@ -302,6 +313,9 @@ func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string, openrtb_ext.BidTypeAudio: makeDeliveryMetrics(registry, adapterOrAccount+"."+exchange, openrtb_ext.BidTypeAudio), openrtb_ext.BidTypeNative: makeDeliveryMetrics(registry, adapterOrAccount+"."+exchange, openrtb_ext.BidTypeNative), } + am.ConnCreated = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_created", adapterOrAccount, exchange), registry) + am.ConnReused = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_reused", adapterOrAccount, exchange), registry) + am.ConnWaitTime = metrics.GetOrRegisterTimer(fmt.Sprintf("%[1]s.%[2]s.connection_wait_time", adapterOrAccount, exchange), registry) for err := range am.ErrorMeters { am.ErrorMeters[err] = metrics.GetOrRegisterMeter(fmt.Sprintf("%s.%s.requests.%s", adapterOrAccount, exchange, err), registry) } @@ -348,7 +362,7 @@ func (me *Metrics) getAccountMetrics(id string) *accountMetrics { am.adapterMetrics = make(map[openrtb_ext.BidderName]*AdapterMetrics, len(me.exchanges)) if !me.MetricsDisabled.AccountAdapterDetails { for _, a := range me.exchanges { - am.adapterMetrics[a] = makeBlankAdapterMetrics() + am.adapterMetrics[a] = makeBlankAdapterMetrics(me.MetricsDisabled) registerAdapterMetrics(me.MetricsRegistry, fmt.Sprintf("account.%s", id), string(a), am.adapterMetrics[a]) } } @@ -472,6 +486,34 @@ func (me *Metrics) RecordAdapterRequest(labels AdapterLabels) { } } +// Keeps track of created and reused connections to adapter bidders and the time from the +// connection request, to the connection creation, or reuse from the pool across all engines +func (me *Metrics) RecordAdapterConnections(adapterName openrtb_ext.BidderName, + connWasReused bool, + connWaitTime time.Duration) { + + if me.MetricsDisabled.AdapterConnectionMetrics { + return + } + + am, ok := me.AdapterMetrics[adapterName] + if !ok { + glog.Errorf("Trying to log adapter connection metrics for %s: adapter not found", string(adapterName)) + return + } + + if connWasReused { + am.ConnReused.Inc(1) + } else { + am.ConnCreated.Inc(1) + } + am.ConnWaitTime.Update(connWaitTime) +} + +func (me *Metrics) RecordDNSTime(dnsLookupTime time.Duration) { + me.DNSLookupTimer.Update(dnsLookupTime) +} + // RecordAdapterBidReceived implements a part of the MetricsEngine interface. // This tracks how many bids from each Bidder use `adm` vs. `nurl. func (me *Metrics) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go index 2faa08491e0..f676991649d 100644 --- a/pbsmetrics/go_metrics_test.go +++ b/pbsmetrics/go_metrics_test.go @@ -2,6 +2,7 @@ package pbsmetrics import ( "testing" + "time" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" @@ -115,6 +116,10 @@ func ensureContainsAdapterMetrics(t *testing.T, registry metrics.Registry, name ensureContains(t, registry, name+".request_time", adapterMetrics.RequestTimer) ensureContains(t, registry, name+".prices", adapterMetrics.PriceHistogram) ensureContainsBidTypeMetrics(t, registry, name, adapterMetrics.MarkupMetrics) + + ensureContains(t, registry, name+".connections_created", adapterMetrics.ConnCreated) + ensureContains(t, registry, name+".connections_reused", adapterMetrics.ConnReused) + ensureContains(t, registry, name+".connection_wait_time", adapterMetrics.ConnWaitTime) } func TestRecordBidTypeDisabledConfig(t *testing.T) { @@ -179,6 +184,140 @@ func TestRecordBidTypeDisabledConfig(t *testing.T) { } } +func TestRecordDNSTime(t *testing.T) { + testCases := []struct { + description string + inDnsLookupDuration time.Duration + outExpDuration time.Duration + }{ + { + description: "Five second DNS lookup time", + inDnsLookupDuration: time.Second * 5, + outExpDuration: time.Second * 5, + }, + { + description: "Zero DNS lookup time", + inDnsLookupDuration: time.Duration(0), + outExpDuration: time.Duration(0), + }, + } + for _, test := range testCases { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true}) + + m.RecordDNSTime(test.inDnsLookupDuration) + + assert.Equal(t, test.outExpDuration.Nanoseconds(), m.DNSLookupTimer.Sum(), test.description) + } +} + +func TestRecordAdapterConnections(t *testing.T) { + var fakeBidder openrtb_ext.BidderName = "fooAdvertising" + + type testIn struct { + adapterName openrtb_ext.BidderName + connWasReused bool + connWait time.Duration + connMetricsDisabled bool + } + + type testOut struct { + expectedConnReusedCount int64 + expectedConnCreatedCount int64 + expectedConnWaitTime time.Duration + } + + testCases := []struct { + description string + in testIn + out testOut + }{ + { + description: "Successful, new connection created, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 5, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnReusedCount: 0, + expectedConnCreatedCount: 1, + expectedConnWaitTime: time.Second * 5, + }, + }, + { + description: "Successful, new connection created, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 4, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnCreatedCount: 1, + expectedConnWaitTime: time.Second * 4, + }, + }, + { + description: "Successful, was reused, no connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnWaitTime: 0, + }, + }, + { + description: "Successful, was reused, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + connWait: time.Second * 5, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnWaitTime: time.Second * 5, + }, + }, + { + description: "Fake bidder, nothing gets updated", + in: testIn{ + adapterName: fakeBidder, + connWasReused: false, + connWait: 0, + connMetricsDisabled: false, + }, + out: testOut{}, + }, + { + description: "Adapter connection metrics are disabled, nothing gets updated", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 5, + connMetricsDisabled: true, + }, + out: testOut{}, + }, + } + + for i, test := range testCases { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AdapterConnectionMetrics: test.in.connMetricsDisabled}) + + m.RecordAdapterConnections(test.in.adapterName, test.in.connWasReused, test.in.connWait) + + assert.Equal(t, test.out.expectedConnReusedCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnReused.Count(), "Test [%d] incorrect number of reused connections to adapter", i) + assert.Equal(t, test.out.expectedConnCreatedCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnCreated.Count(), "Test [%d] incorrect number of new connections to adapter created", i) + assert.Equal(t, test.out.expectedConnWaitTime.Nanoseconds(), m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnWaitTime.Sum(), "Test [%d] incorrect wait time in connection to adapter", i) + } +} + func TestNewMetricsWithDisabledConfig(t *testing.T) { registry := metrics.NewRegistry() m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true}) diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index 514fbac1015..8133bc739a0 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -301,6 +301,8 @@ type MetricsEngine interface { RecordLegacyImps(labels Labels, numImps int) // RecordImps for the legacy engine RecordRequestTime(labels Labels, length time.Duration) // ignores adapter. only statusOk and statusErr fom status RecordAdapterRequest(labels AdapterLabels) + RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) + RecordDNSTime(dnsLookupTime time.Duration) RecordAdapterPanic(labels AdapterLabels) // This records whether or not a bid of a particular type uses `adm` or `nurl`. // Since the legacy endpoints don't have a bid type, it can only count bids from OpenRTB and AMP. diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go index 6c263f0af4d..42a2d1b4c8f 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -52,6 +52,16 @@ func (me *MetricsEngineMock) RecordAdapterRequest(labels AdapterLabels) { me.Called(labels) } +// RecordAdapterConnections mock +func (me *MetricsEngineMock) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { + me.Called(bidderName, connWasReused, connWaitTime) +} + +// RecordDNSTime mock +func (me *MetricsEngineMock) RecordDNSTime(dnsLookupTime time.Duration) { + me.Called(dnsLookupTime) +} + // RecordAdapterBidReceived mock func (me *MetricsEngineMock) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { me.Called(labels, bidType, hasAdm) diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go index ef1d300c4df..4f62a18aae9 100644 --- a/pbsmetrics/prometheus/preload.go +++ b/pbsmetrics/prometheus/preload.go @@ -85,6 +85,20 @@ func preloadLabelValues(m *Metrics) { hasBidsLabel: boolValues, }) + if !m.metricsDisabled.AdapterConnectionMetrics { + preloadLabelValuesForCounter(m.adapterCreatedConnections, map[string][]string{ + adapterLabel: adapterValues, + }) + + preloadLabelValuesForCounter(m.adapterReusedConnections, map[string][]string{ + adapterLabel: adapterValues, + }) + + preloadLabelValuesForHistogram(m.adapterConnectionWaitTime, map[string][]string{ + adapterLabel: adapterValues, + }) + } + preloadLabelValuesForHistogram(m.adapterRequestsTimer, map[string][]string{ adapterLabel: adapterValues, }) diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index d94c4d78f62..b42399b2a62 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -29,23 +29,29 @@ type Metrics struct { storedImpressionsCacheResult *prometheus.CounterVec storedRequestCacheResult *prometheus.CounterVec timeoutNotifications *prometheus.CounterVec + dnsLookupTimer prometheus.Histogram privacyCCPA *prometheus.CounterVec privacyCOPPA *prometheus.CounterVec privacyLMT *prometheus.CounterVec privacyTCF *prometheus.CounterVec // Adapter Metrics - adapterBids *prometheus.CounterVec - adapterCookieSync *prometheus.CounterVec - adapterErrors *prometheus.CounterVec - adapterPanics *prometheus.CounterVec - adapterPrices *prometheus.HistogramVec - adapterRequests *prometheus.CounterVec - adapterRequestsTimer *prometheus.HistogramVec - adapterUserSync *prometheus.CounterVec + adapterBids *prometheus.CounterVec + adapterCookieSync *prometheus.CounterVec + adapterErrors *prometheus.CounterVec + adapterPanics *prometheus.CounterVec + adapterPrices *prometheus.HistogramVec + adapterRequests *prometheus.CounterVec + adapterRequestsTimer *prometheus.HistogramVec + adapterUserSync *prometheus.CounterVec + adapterReusedConnections *prometheus.CounterVec + adapterCreatedConnections *prometheus.CounterVec + adapterConnectionWaitTime *prometheus.HistogramVec // Account Metrics accountRequests *prometheus.CounterVec + + metricsDisabled config.DisabledMetrics } const ( @@ -97,14 +103,15 @@ const ( ) // 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} +func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMetrics) *Metrics { + standardTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 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() + metrics.metricsDisabled = disabledMetrics metrics.connectionsClosed = newCounterWithoutLabels(cfg, metrics.Registry, "connections_closed", @@ -132,7 +139,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "impressions_requests_legacy", "Count of requested impressions to Prebid Server using the legacy endpoint.") - metrics.prebidCacheWriteTimer = newHistogram(cfg, metrics.Registry, + metrics.prebidCacheWriteTimer = newHistogramVec(cfg, metrics.Registry, "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}, @@ -143,11 +150,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of total requests to Prebid Server labeled by type and status.", []string{requestTypeLabel, requestStatusLabel}) - metrics.requestsTimer = newHistogram(cfg, metrics.Registry, + metrics.requestsTimer = newHistogramVec(cfg, metrics.Registry, "request_time_seconds", "Seconds to resolve successful Prebid Server requests labeled by type.", []string{requestTypeLabel}, - requestTimeBuckets) + standardTimeBuckets) metrics.requestsWithoutCookie = newCounter(cfg, metrics.Registry, "requests_without_cookie", @@ -169,6 +176,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of timeout notifications triggered, and if they were successfully sent.", []string{successLabel}) + metrics.dnsLookupTimer = newHistogram(cfg, metrics.Registry, + "dns_lookup_time", + "Seconds to resolve DNS", + standardTimeBuckets) + metrics.privacyCCPA = newCounter(cfg, metrics.Registry, "privacy_ccpa", "Count of total requests to Prebid Server where CCPA was provided by source and opt-out .", @@ -209,7 +221,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of panics labeled by adapter.", []string{adapterLabel}) - metrics.adapterPrices = newHistogram(cfg, metrics.Registry, + metrics.adapterPrices = newHistogramVec(cfg, metrics.Registry, "adapter_prices", "Monetary value of the bids labeled by adapter.", []string{adapterLabel}, @@ -220,11 +232,29 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of requests labeled by adapter, if has a cookie, and if it resulted in bids.", []string{adapterLabel, cookieLabel, hasBidsLabel}) - metrics.adapterRequestsTimer = newHistogram(cfg, metrics.Registry, + if !metrics.metricsDisabled.AdapterConnectionMetrics { + metrics.adapterCreatedConnections = newCounter(cfg, metrics.Registry, + "adapter_connection_created", + "Count that keeps track of new connections when contacting adapter bidder endpoints.", + []string{adapterLabel}) + + metrics.adapterReusedConnections = newCounter(cfg, metrics.Registry, + "adapter_connection_reused", + "Count that keeps track of reused connections when contacting adapter bidder endpoints.", + []string{adapterLabel}) + + metrics.adapterConnectionWaitTime = newHistogramVec(cfg, metrics.Registry, + "adapter_connection_wait", + "Seconds from when the connection was requested until it is either created or reused", + []string{adapterLabel}, + standardTimeBuckets) + } + + metrics.adapterRequestsTimer = newHistogramVec(cfg, metrics.Registry, "adapter_request_time_seconds", "Seconds to resolve each successful request labeled by adapter.", []string{adapterLabel}, - requestTimeBuckets) + standardTimeBuckets) metrics.adapterUserSync = newCounter(cfg, metrics.Registry, "adapter_user_sync", @@ -236,7 +266,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of total requests to Prebid Server labeled by account.", []string{accountLabel}) - metrics.requestsQueueTimer = newHistogram(cfg, metrics.Registry, + metrics.requestsQueueTimer = newHistogramVec(cfg, metrics.Registry, "request_queue_time", "Seconds request was waiting in queue", []string{requestTypeLabel, requestStatusLabel}, @@ -271,7 +301,7 @@ func newCounterWithoutLabels(cfg config.PrometheusMetrics, registry *prometheus. return counter } -func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string, buckets []float64) *prometheus.HistogramVec { +func newHistogramVec(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string, buckets []float64) *prometheus.HistogramVec { opts := prometheus.HistogramOpts{ Namespace: cfg.Namespace, Subsystem: cfg.Subsystem, @@ -284,6 +314,19 @@ func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, n return histogram } +func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, buckets []float64) prometheus.Histogram { + opts := prometheus.HistogramOpts{ + Namespace: cfg.Namespace, + Subsystem: cfg.Subsystem, + Name: name, + Help: help, + Buckets: buckets, + } + histogram := prometheus.NewHistogram(opts) + registry.MustRegister(histogram) + return histogram +} + func (m *Metrics) RecordConnectionAccept(success bool) { if success { m.connectionsOpened.Inc() @@ -359,6 +402,32 @@ func (m *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) { } } +// Keeps track of created and reused connections to adapter bidders and the time from the +// connection request, to the connection creation, or reuse from the pool across all engines +func (m *Metrics) RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { + if m.metricsDisabled.AdapterConnectionMetrics { + return + } + + if connWasReused { + m.adapterReusedConnections.With(prometheus.Labels{ + adapterLabel: string(adapterName), + }).Inc() + } else { + m.adapterCreatedConnections.With(prometheus.Labels{ + adapterLabel: string(adapterName), + }).Inc() + } + + m.adapterConnectionWaitTime.With(prometheus.Labels{ + adapterLabel: string(adapterName), + }).Observe(connWaitTime.Seconds()) +} + +func (m *Metrics) RecordDNSTime(dnsLookupTime time.Duration) { + m.dnsLookupTimer.Observe(dnsLookupTime.Seconds()) +} + func (m *Metrics) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) { m.adapterPanics.With(prometheus.Labels{ adapterLabel: string(labels.Adapter), diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index b722ab28b5c..b6153b16278 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -1,6 +1,7 @@ package prometheusmetrics import ( + "fmt" "testing" "time" @@ -17,7 +18,7 @@ func createMetricsForTesting() *Metrics { Port: 8080, Namespace: "prebid", Subsystem: "server", - }) + }, config.DisabledMetrics{}) } func TestMetricCountGatekeeping(t *testing.T) { @@ -61,7 +62,7 @@ func TestMetricCountGatekeeping(t *testing.T) { // Verify Per-Adapter Cardinality // - This assertion provides a warning for newly added adapter metrics. Threre are 40+ adapters which makes the // cost of new per-adapter metrics rather expensive. Thought should be given when adding new per-adapter metrics. - assert.True(t, perAdapterCardinalityCount <= 22, "Per-Adapter Cardinality") + assert.True(t, perAdapterCardinalityCount <= 27, "Per-Adapter Cardinality count equals %d \n", perAdapterCardinalityCount) } func TestConnectionMetrics(t *testing.T) { @@ -944,6 +945,175 @@ func TestTimeoutNotifications(t *testing.T) { } +func TestRecordDNSTime(t *testing.T) { + type testIn struct { + dnsLookupDuration time.Duration + } + type testOut struct { + expDuration float64 + expCount uint64 + } + testCases := []struct { + description string + in testIn + out testOut + }{ + { + description: "Five second DNS lookup time", + in: testIn{ + dnsLookupDuration: time.Second * 5, + }, + out: testOut{ + expDuration: 5, + expCount: 1, + }, + }, + { + description: "Zero DNS lookup time", + in: testIn{}, + out: testOut{ + expDuration: 0, + expCount: 1, + }, + }, + } + for i, test := range testCases { + pm := createMetricsForTesting() + pm.RecordDNSTime(test.in.dnsLookupDuration) + + m := dto.Metric{} + pm.dnsLookupTimer.Write(&m) + histogram := *m.GetHistogram() + + assert.Equal(t, test.out.expCount, histogram.GetSampleCount(), "[%d] Incorrect number of histogram entries. Desc: %s\n", i, test.description) + assert.Equal(t, test.out.expDuration, histogram.GetSampleSum(), "[%d] Incorrect number of histogram cumulative values. Desc: %s\n", i, test.description) + } +} + +func TestRecordAdapterConnections(t *testing.T) { + + type testIn struct { + adapterName openrtb_ext.BidderName + connWasReused bool + connWait time.Duration + } + + type testOut struct { + expectedConnReusedCount int64 + expectedConnCreatedCount int64 + expectedConnWaitCount uint64 + expectedConnWaitTime float64 + } + + testCases := []struct { + description string + in testIn + out testOut + }{ + { + description: "[1] Successful, new connection created, was idle, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 5, + }, + out: testOut{ + expectedConnReusedCount: 0, + expectedConnCreatedCount: 1, + expectedConnWaitCount: 1, + expectedConnWaitTime: 5, + }, + }, + { + description: "[2] Successful, new connection created, not idle, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 4, + }, + out: testOut{ + expectedConnReusedCount: 0, + expectedConnCreatedCount: 1, + expectedConnWaitCount: 1, + expectedConnWaitTime: 4, + }, + }, + { + description: "[3] Successful, was reused, was idle, no connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnCreatedCount: 0, + expectedConnWaitCount: 1, + expectedConnWaitTime: 0, + }, + }, + { + description: "[4] Successful, was reused, not idle, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + connWait: time.Second * 5, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnCreatedCount: 0, + expectedConnWaitCount: 1, + expectedConnWaitTime: 5, + }, + }, + } + + for i, test := range testCases { + m := createMetricsForTesting() + assertDesciptions := []string{ + fmt.Sprintf("[%d] Metric: adapterReusedConnections; Desc: %s", i+1, test.description), + fmt.Sprintf("[%d] Metric: adapterCreatedConnections; Desc: %s", i+1, test.description), + fmt.Sprintf("[%d] Metric: adapterWaitConnectionCount; Desc: %s", i+1, test.description), + fmt.Sprintf("[%d] Metric: adapterWaitConnectionTime; Desc: %s", i+1, test.description), + } + + m.RecordAdapterConnections(test.in.adapterName, test.in.connWasReused, test.in.connWait) + + // Assert number of reused connections + assertCounterVecValue(t, + assertDesciptions[0], + "adapter_connection_reused", + m.adapterReusedConnections, + float64(test.out.expectedConnReusedCount), + prometheus.Labels{adapterLabel: string(test.in.adapterName)}) + + // Assert number of new created connections + assertCounterVecValue(t, + assertDesciptions[1], + "adapter_connection_created", + m.adapterCreatedConnections, + float64(test.out.expectedConnCreatedCount), + prometheus.Labels{adapterLabel: string(test.in.adapterName)}) + + // Assert connection wait time + histogram := getHistogramFromHistogramVec(m.adapterConnectionWaitTime, adapterLabel, string(test.in.adapterName)) + assert.Equal(t, test.out.expectedConnWaitCount, histogram.GetSampleCount(), assertDesciptions[2]) + assert.Equal(t, test.out.expectedConnWaitTime, histogram.GetSampleSum(), assertDesciptions[3]) + } +} + +func TestDisableAdapterConnections(t *testing.T) { + prometheusMetrics := NewMetrics(config.PrometheusMetrics{ + Port: 8080, + Namespace: "prebid", + Subsystem: "server", + }, config.DisabledMetrics{AdapterConnectionMetrics: true}) + + // Assert counter vector was not initialized + assert.Nil(t, prometheusMetrics.adapterReusedConnections, "Counter Vector adapterReusedConnections should be nil") + assert.Nil(t, prometheusMetrics.adapterCreatedConnections, "Counter Vector adapterCreatedConnections should be nil") + assert.Nil(t, prometheusMetrics.adapterConnectionWaitTime, "Counter Vector adapterConnectionWaitTime should be nil") +} + func TestRecordRequestPrivacy(t *testing.T) { m := createMetricsForTesting() From f1582a494e407635544be416632e26c76d2b1881 Mon Sep 17 00:00:00 2001 From: PubMatic-OpenWrap Date: Mon, 27 Jul 2020 18:53:23 +0530 Subject: [PATCH 150/381] Pubmatic: Support for video duration and primary category (#1384) * Adding suport for video duration and primary category in pubmatic adapter * Adding code review changes for PR-1384 * Adding changes for syntaxNode suggestion Co-authored-by: Isha Bharti --- adapters/pubmatic/pubmatic.go | 67 ++++++++++++------- adapters/pubmatic/pubmatic_test.go | 33 ++++----- .../pubmatictest/exemplary/video.json | 21 ++++-- 3 files changed, 78 insertions(+), 43 deletions(-) diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go index aae115386d0..795655048b4 100644 --- a/adapters/pubmatic/pubmatic.go +++ b/adapters/pubmatic/pubmatic.go @@ -21,7 +21,6 @@ import ( ) const MAX_IMPRESSIONS_PUBMATIC = 30 -const bidTypeExtKey = "BidType" type PubmaticAdapter struct { http *adapters.HTTPAdapter @@ -48,6 +47,15 @@ type pubmaticParams struct { Keywords map[string]string `json:"keywords,omitempty"` } +type pubmaticBidExtVideo struct { + Duration *int `json:"duration,omitempty"` +} + +type pubmaticBidExt struct { + BidType *int `json:"BidType,omitempty"` + VideoCreativeInfo *pubmaticBidExtVideo `json:"video,omitempty"` +} + const ( INVALID_PARAMS = "Invalid BidParam" MISSING_PUBID = "Missing PubID" @@ -289,7 +297,11 @@ func (a *PubmaticAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder DealId: bid.DealID, } - mediaType := getBidType(bid.Ext) + var bidExt pubmaticBidExt + mediaType := openrtb_ext.BidTypeBanner + if err := json.Unmarshal(bid.Ext, &bidExt); err == nil { + mediaType = getBidType(&bidExt) + } pbid.CreativeMediaType = string(mediaType) bids = append(bids, &pbid) @@ -549,9 +561,24 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb.BidRequest, external for _, sb := range bidResp.SeatBid { for i := 0; i < len(sb.Bid); i++ { bid := sb.Bid[i] + impVideo := &openrtb_ext.ExtBidPrebidVideo{} + + if len(bid.Cat) > 1 { + bid.Cat = bid.Cat[0:1] + } + + var bidExt *pubmaticBidExt + bidType := openrtb_ext.BidTypeBanner + if err := json.Unmarshal(bid.Ext, &bidExt); err == nil && bidExt != nil { + if bidExt.VideoCreativeInfo != nil && bidExt.VideoCreativeInfo.Duration != nil { + impVideo.Duration = *bidExt.VideoCreativeInfo.Duration + } + bidType = getBidType(bidExt) + } bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ - Bid: &bid, - BidType: getBidType(bid.Ext), + Bid: &bid, + BidType: bidType, + BidVideo: impVideo, }) } @@ -560,28 +587,20 @@ func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb.BidRequest, external } // getBidType returns the bid type specified in the response bid.ext -func getBidType(bidExt json.RawMessage) openrtb_ext.BidType { +func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType { // setting "banner" as the default bid type bidType := openrtb_ext.BidTypeBanner - if bidExt != nil { - bidExtMap := make(map[string]interface{}) - extbyte, err := json.Marshal(bidExt) - if err == nil { - err = json.Unmarshal(extbyte, &bidExtMap) - if err == nil && bidExtMap[bidTypeExtKey] != nil { - bidTypeVal := int(bidExtMap[bidTypeExtKey].(float64)) - switch bidTypeVal { - case 0: - bidType = openrtb_ext.BidTypeBanner - case 1: - bidType = openrtb_ext.BidTypeVideo - case 2: - bidType = openrtb_ext.BidTypeNative - default: - // default value is banner - bidType = openrtb_ext.BidTypeBanner - } - } + if bidExt != nil && bidExt.BidType != nil { + switch *bidExt.BidType { + case 0: + bidType = openrtb_ext.BidTypeBanner + case 1: + bidType = openrtb_ext.BidTypeVideo + case 2: + bidType = openrtb_ext.BidTypeNative + default: + // default value is banner + bidType = openrtb_ext.BidTypeBanner } } return bidType diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go index be086f5adf1..5dd236d9742 100644 --- a/adapters/pubmatic/pubmatic_test.go +++ b/adapters/pubmatic/pubmatic_test.go @@ -674,18 +674,18 @@ func TestPubmaticSampleRequest(t *testing.T) { } func TestGetBidTypeVideo(t *testing.T) { - extJSON := `{"BidType":1}` - extrm := json.RawMessage(extJSON) - actualBidTypeValue := getBidType(extrm) + pubmaticExt := new(pubmaticBidExt) + pubmaticExt.BidType = new(int) + *pubmaticExt.BidType = 1 + actualBidTypeValue := getBidType(pubmaticExt) if actualBidTypeValue != openrtb_ext.BidTypeVideo { t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeVideo, actualBidTypeValue) } } func TestGetBidTypeForMissingBidTypeExt(t *testing.T) { - extJSON := `{}` - extrm := json.RawMessage(extJSON) - actualBidTypeValue := getBidType(extrm) + pubmaticExt := pubmaticBidExt{} + actualBidTypeValue := getBidType(&pubmaticExt) // banner is the default bid type when no bidType key is present in the bid.ext if actualBidTypeValue != "banner" { t.Errorf("Expected Bid Type value was: banner, actual value is: %v", actualBidTypeValue) @@ -693,27 +693,30 @@ func TestGetBidTypeForMissingBidTypeExt(t *testing.T) { } func TestGetBidTypeBanner(t *testing.T) { - extJSON := `{"BidType":0}` - extrm := json.RawMessage(extJSON) - actualBidTypeValue := getBidType(extrm) + pubmaticExt := new(pubmaticBidExt) + pubmaticExt.BidType = new(int) + *pubmaticExt.BidType = 0 + actualBidTypeValue := getBidType(pubmaticExt) if actualBidTypeValue != openrtb_ext.BidTypeBanner { t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue) } } func TestGetBidTypeNative(t *testing.T) { - extJSON := `{"BidType":2}` - extrm := json.RawMessage(extJSON) - actualBidTypeValue := getBidType(extrm) + pubmaticExt := new(pubmaticBidExt) + pubmaticExt.BidType = new(int) + *pubmaticExt.BidType = 2 + actualBidTypeValue := getBidType(pubmaticExt) if actualBidTypeValue != openrtb_ext.BidTypeNative { t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeNative, actualBidTypeValue) } } func TestGetBidTypeForUnsupportedCode(t *testing.T) { - extJSON := `{"BidType":99}` - extrm := json.RawMessage(extJSON) - actualBidTypeValue := getBidType(extrm) + pubmaticExt := new(pubmaticBidExt) + pubmaticExt.BidType = new(int) + *pubmaticExt.BidType = 99 + actualBidTypeValue := getBidType(pubmaticExt) if actualBidTypeValue != openrtb_ext.BidTypeBanner { t.Errorf("Expected Bid Type value was: %v, actual value is: %v", openrtb_ext.BidTypeBanner, actualBidTypeValue) } diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json index 25ed366ee05..4c874535a35 100644 --- a/adapters/pubmatic/pubmatictest/exemplary/video.json +++ b/adapters/pubmatic/pubmatictest/exemplary/video.json @@ -112,10 +112,14 @@ "h": 250, "w": 300, "dealid":"test deal", + "cat" : ["IAB-1", "IAB-2"], "ext": { "dspid": 6, "deal_channel": 1, - "BidType": 1 + "BidType": 1, + "video" : { + "duration" : 5 + } } }] } @@ -139,6 +143,9 @@ "adid": "29681110", "adm": "some-test-ad", "adomain": ["pubmatic.com"], + "cat": [ + "IAB-1" + ], "crid": "29681110", "w": 300, "h": 250, @@ -146,12 +153,18 @@ "ext": { "dspid": 6, "deal_channel": 1, - "BidType": 1 + "BidType": 1, + "video" : { + "duration" : 5 + } } }, - "type": "video" + "type": "video", + "video" :{ + "duration" : 5 + } } ] } ] - } \ No newline at end of file + } From a857e6868e0de3445dc0c65cf6522d290ce1df23 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 29 Jul 2020 12:03:31 -0400 Subject: [PATCH 151/381] Add IPv6 Non-Public Network (#1417) --- config/config.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/config/config.go b/config/config.go index a82dbb5edf7..8545523d238 100755 --- a/config/config.go +++ b/config/config.go @@ -896,13 +896,14 @@ func SetupViper(v *viper.Viper, filename string) { /* Loopback: 127.0.0.0/8 /* /* IPv6 - /* Loopback: ::1/128 - /* Unique Local: fc00::/7 - /* Link Local: fe80::/10 - /* Multicast: ff00::/8 + /* Loopback: ::1/128 + /* Documentation: 2001:db8::/32 + /* Unique Local: fc00::/7 + /* Link Local: fe80::/10 + /* Multicast: ff00::/8 */ v.SetDefault("request_validation.ipv4_private_networks", []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"}) - v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8"}) + v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8", "2001:db8::/32"}) // Set environment variable support: v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) From 3fa47ab2218a1fdcb6d9a6fc5edd3fff5ad10318 Mon Sep 17 00:00:00 2001 From: susyt Date: Wed, 29 Jul 2020 14:18:29 -0700 Subject: [PATCH 152/381] GumGum: adds support for video (#1408) --- adapters/gumgum/gumgum.go | 57 ++++++++-- .../gumgum/gumgumtest/exemplary/video.json | 106 ++++++++++++++++++ .../gumgum/gumgumtest/params/race/video.json | 3 + .../supplemental/missing-video-params.json | 36 ++++++ static/bidder-info/gumgum.yaml | 1 + 5 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 adapters/gumgum/gumgumtest/exemplary/video.json create mode 100644 adapters/gumgum/gumgumtest/params/race/video.json create mode 100644 adapters/gumgum/gumgumtest/supplemental/missing-video-params.json diff --git a/adapters/gumgum/gumgum.go b/adapters/gumgum/gumgum.go index 84a008d1891..490979091a4 100644 --- a/adapters/gumgum/gumgum.go +++ b/adapters/gumgum/gumgum.go @@ -8,12 +8,16 @@ import ( "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" "net/http" + "strconv" + "strings" ) +// GumGumAdapter implements Bidder interface. type GumGumAdapter struct { URI string } +// MakeRequests makes the HTTP requests which should be made to fetch bids. func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var validImps []openrtb.Imp var trackingId string @@ -26,15 +30,21 @@ func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt zone, err := preprocess(&imp) if err != nil { errs = append(errs, err) - } else { - if request.Imp[i].Banner != nil { - bannerCopy := *request.Imp[i].Banner - if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 { - format := bannerCopy.Format[0] - bannerCopy.W = &(format.W) - bannerCopy.H = &(format.H) - } - request.Imp[i].Banner = &bannerCopy + } else if request.Imp[i].Banner != nil { + bannerCopy := *request.Imp[i].Banner + if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 { + format := bannerCopy.Format[0] + bannerCopy.W = &(format.W) + bannerCopy.H = &(format.H) + } + request.Imp[i].Banner = &bannerCopy + validImps = append(validImps, request.Imp[i]) + trackingId = zone + } else if request.Imp[i].Video != nil { + err := validateVideoParams(request.Imp[i].Video) + if err != nil { + errs = append(errs, err) + } else { validImps = append(validImps, request.Imp[i]) trackingId = zone } @@ -70,6 +80,7 @@ func (g *GumGumAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt }}, errs } +// MakeBids unpacks the server's response into Bids. func (g *GumGumAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { if response.StatusCode == http.StatusNoContent { return nil, nil @@ -98,12 +109,19 @@ func (g *GumGumAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRe for _, sb := range bidResp.SeatBid { for i := range sb.Bid { + mediaType := getMediaTypeForImpID(sb.Bid[i].ImpID, internalRequest.Imp) + if mediaType == openrtb_ext.BidTypeVideo { + price := strconv.FormatFloat(sb.Bid[i].Price, 'f', -1, 64) + sb.Bid[i].AdM = strings.Replace(sb.Bid[i].AdM, "${AUCTION_PRICE}", price, -1) + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &sb.Bid[i], - BidType: openrtb_ext.BidTypeBanner, + BidType: mediaType, }) } } + return bidResponse, errs } @@ -128,6 +146,25 @@ func preprocess(imp *openrtb.Imp) (string, error) { return zone, nil } +func getMediaTypeForImpID(impID string, imps []openrtb.Imp) openrtb_ext.BidType { + for _, imp := range imps { + if imp.ID == impID && imp.Banner != nil { + return openrtb_ext.BidTypeBanner + } + } + return openrtb_ext.BidTypeVideo +} + +func validateVideoParams(video *openrtb.Video) (err error) { + if video.W == 0 || video.H == 0 || video.MinDuration == 0 || video.MaxDuration == 0 || video.Placement == 0 || video.Linearity == 0 { + return &errortypes.BadInput{ + Message: "Invalid or missing video field(s)", + } + } + return nil +} + +// NewGumGumBidder configures bidder endpoint. func NewGumGumBidder(endpoint string) *GumGumAdapter { return &GumGumAdapter{ URI: endpoint, diff --git a/adapters/gumgum/gumgumtest/exemplary/video.json b/adapters/gumgum/gumgumtest/exemplary/video.json new file mode 100644 index 00000000000..ea76c733f34 --- /dev/null +++ b/adapters/gumgum/gumgumtest/exemplary/video.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2 + ], + "w": 640, + "h": 480, + "startdelay": 1, + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "zone": "ggumtest" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://g2.gumgum.com/providers/prbds2s/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2 + ], + "w": 640, + "h": 480, + "startdelay": 1, + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "zone": "ggumtest" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/gumgum/gumgumtest/params/race/video.json b/adapters/gumgum/gumgumtest/params/race/video.json new file mode 100644 index 00000000000..3ed284384d3 --- /dev/null +++ b/adapters/gumgum/gumgumtest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "zone": "dc9d6be1" +} \ No newline at end of file diff --git a/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json new file mode 100644 index 00000000000..b2475cd7bb4 --- /dev/null +++ b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json @@ -0,0 +1,36 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 1, + 2 + ], + "w": 640, + "h": 480, + "startdelay": 1, + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "zone": "ggumtest" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Invalid or missing video field(s)", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/static/bidder-info/gumgum.yaml b/static/bidder-info/gumgum.yaml index 0feca7cdf73..6ba563b4c1c 100644 --- a/static/bidder-info/gumgum.yaml +++ b/static/bidder-info/gumgum.yaml @@ -4,3 +4,4 @@ capabilities: site: mediaTypes: - banner + - video \ No newline at end of file From 551faadb0502cef9d7dda5b90655f348b3a7bc19 Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Thu, 30 Jul 2020 09:23:27 -0700 Subject: [PATCH 153/381] OpenX adapter: pass optional platform (PBID-598) (#1421) --- adapters/openx/openx.go | 4 +++- .../openxtest/exemplary/optional-params.json | 4 +++- docs/bidders/openx.md | 7 ++++-- openrtb_ext/imp_openx.go | 1 + static/bidder-params/openx.json | 22 +++++++++++++++++-- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go index ca88b18bdb8..c2a42adf295 100644 --- a/adapters/openx/openx.go +++ b/adapters/openx/openx.go @@ -22,7 +22,8 @@ type openxImpExt struct { } type openxReqExt struct { - DelDomain string `json:"delDomain"` + DelDomain string `json:"delDomain,omitempty"` + Platform string `json:"platform,omitempty"` BidderConfig string `json:"bc"` } @@ -125,6 +126,7 @@ func preprocess(imp *openrtb.Imp, reqExt *openxReqExt) error { } reqExt.DelDomain = openxExt.DelDomain + reqExt.Platform = openxExt.Platform imp.TagID = openxExt.Unit imp.BidFloor = openxExt.CustomFloor diff --git a/adapters/openx/openxtest/exemplary/optional-params.json b/adapters/openx/openxtest/exemplary/optional-params.json index 225559875a8..e8fcf394b8b 100644 --- a/adapters/openx/openxtest/exemplary/optional-params.json +++ b/adapters/openx/openxtest/exemplary/optional-params.json @@ -11,6 +11,7 @@ "bidder": { "unit": "539439964", "delDomain": "se-demo-d.openx.net", + "platform": "PLATFORM", "customFloor": 0.1, "customParams": {"foo": "bar"} } @@ -40,7 +41,8 @@ ], "ext": { "bc": "hb_pbs_1.0.0", - "delDomain": "se-demo-d.openx.net" + "delDomain": "se-demo-d.openx.net", + "platform": "PLATFORM" } } }, diff --git a/docs/bidders/openx.md b/docs/bidders/openx.md index c366db3ab61..54a0a5b1e72 100644 --- a/docs/bidders/openx.md +++ b/docs/bidders/openx.md @@ -5,10 +5,13 @@ 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" | +| delDomain | string | required\* | The delivery domain for the customer | "sademo-d.openx.net" | +| platform | uuid | required\* | The platform id for the customer | "a3aece0c-9e80-4316-8deb-faf804779bd1" | | 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"]} | +\* At least one of `delDomain` or `platform` parameters is required. + If you have any questions regarding setting up, please reach out to your account manager or @@ -59,4 +62,4 @@ If you have any questions regarding setting up, please reach out to your account }, } } -``` \ No newline at end of file +``` diff --git a/openrtb_ext/imp_openx.go b/openrtb_ext/imp_openx.go index e63595b0912..2625cb3802d 100644 --- a/openrtb_ext/imp_openx.go +++ b/openrtb_ext/imp_openx.go @@ -3,6 +3,7 @@ package openrtb_ext // ExtImpOpenx defines the contract for bidrequest.imp[i].ext.openx type ExtImpOpenx struct { Unit string `json:"unit"` + Platform string `json:"platform"` DelDomain string `json:"delDomain"` CustomFloor float64 `json:"customFloor"` CustomParams map[string]interface{} `json:"customParams"` diff --git a/static/bidder-params/openx.json b/static/bidder-params/openx.json index 93a672ed629..6dbd10178e4 100644 --- a/static/bidder-params/openx.json +++ b/static/bidder-params/openx.json @@ -16,6 +16,11 @@ "pattern": "\\.[a-zA-Z]{2,3}$", "format": "hostname" }, + "platform": { + "type": "string", + "description": "The platform id for the customer.", + "format": "uuid" + }, "customFloor": { "type": "number", "description": "The minimum CPM price in USD.", @@ -26,6 +31,19 @@ "description": "User-defined targeting key-value pairs." } }, - - "required": ["unit", "delDomain"] + "required": [ + "unit" + ], + "anyOf": [ + { + "required": [ + "delDomain" + ] + }, + { + "required": [ + "platform" + ] + } + ] } From 126455ccfeb3965b5c4a8b49749dbbce144e5317 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Thu, 30 Jul 2020 12:41:26 -0400 Subject: [PATCH 154/381] Adds keyvalue hb_format support (#1414) --- docs/endpoints/openrtb2/auction.md | 3 + exchange/exchange.go | 1 + exchange/targeting.go | 4 + exchange/targeting_test.go | 209 +++++++++++++++++++++++++++++ openrtb_ext/bid.go | 3 + openrtb_ext/request.go | 1 + 6 files changed, 221 insertions(+) diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md index d09216188b8..b532923e793 100644 --- a/docs/endpoints/openrtb2/auction.md +++ b/docs/endpoints/openrtb2/auction.md @@ -173,6 +173,7 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta }, "includewinners": false, // Optional param defaulting to true "includebidderkeys": false // Optional param defaulting to true + "includeformat": false // Optional param defaulting to false } } } @@ -184,6 +185,8 @@ For backwards compatibility the following strings will also be allowed as price One of "includewinners" or "includebidderkeys" must be true (both default to true if unset). If both were false, then no targeting keys would be set, which is better configured by omitting targeting altogether. +The parameter "includeformat" indicates the type of the bid (banner, video, etc) for multiformat requests. It will add the key `hb_format` and/or `hb_format_{bidderName}` as per "includewinners" and "includebidderkeys" above. + 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. ``` diff --git a/exchange/exchange.go b/exchange/exchange.go index 3f0258dd3c1..5001e495440 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -135,6 +135,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys, includeCacheBids: shouldCacheBids, includeCacheVast: shouldCacheVAST, + includeFormat: requestExt.Prebid.Targeting.IncludeFormat, } targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() } diff --git a/exchange/targeting.go b/exchange/targeting.go index 994ad9e7c81..dca57653b44 100644 --- a/exchange/targeting.go +++ b/exchange/targeting.go @@ -22,6 +22,7 @@ type targetData struct { includeBidderKeys bool includeCacheBids bool includeCacheVast bool + includeFormat bool // cacheHost and cachePath exist to supply cache host and path as targeting parameters cacheHost string cachePath string @@ -53,6 +54,9 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi if vastID, ok := auc.vastCacheIds[topBidPerBidder.bid]; ok { targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner) } + if targData.includeFormat { + targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBidPerBidder.bidType), bidderName, isOverallWinner) + } if targData.cacheHost != "" { targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, bidderName, isOverallWinner) diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index 16955e97c5b..11b60db01f3 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -247,3 +247,212 @@ func (f *mockFetcher) GetId(bidder openrtb_ext.BidderName) (string, bool) { func mockServer(w http.ResponseWriter, req *http.Request) { w.Write([]byte("{}")) } + +type TargetingTestData struct { + Description string + TargetData targetData + Auction auction + IsApp bool + CategoryMapping map[string]string + ExpectedBidTargetsByBidder map[string]map[openrtb_ext.BidderName]map[string]string +} + +var bid123 *openrtb.Bid = &openrtb.Bid{ + Price: 1.23, +} + +var bid111 *openrtb.Bid = &openrtb.Bid{ + Price: 1.11, + DealID: "mydeal", +} +var bid084 *openrtb.Bid = &openrtb.Bid{ + Price: 0.84, +} + +var TargetingTests []TargetingTestData = []TargetingTestData{ + { + Description: "Targeting winners only (most basic targeting example)", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeWinners: true, + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid084, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder": "appnexus", + "hb_pb": "1.20", + }, + openrtb_ext.BidderRubicon: {}, + }, + }, + }, + { + Description: "Targeting on bidders only", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeBidderKeys: true, + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid084, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder_appnexus": "appnexus", + "hb_pb_appnexus": "1.20", + }, + openrtb_ext.BidderRubicon: { + "hb_bidder_rubicon": "rubicon", + "hb_pb_rubicon": "0.80", + }, + }, + }, + }, + { + Description: "Full basic targeting with hd_format", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeWinners: true, + includeBidderKeys: true, + includeFormat: true, + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid084, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_pb": "1.20", + "hb_pb_appnexus": "1.20", + "hb_format": "banner", + "hb_format_appnexus": "banner", + }, + openrtb_ext.BidderRubicon: { + "hb_bidder_rubicon": "rubicon", + "hb_pb_rubicon": "0.80", + "hb_format_rubicon": "banner", + }, + }, + }, + }, + { + Description: "Cache and deal targeting test", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeBidderKeys: true, + cacheHost: "cache.prebid.com", + cachePath: "cache", + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid111, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + cacheIds: map[*openrtb.Bid]string{ + bid123: "55555", + bid111: "cacheme", + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder_appnexus": "appnexus", + "hb_pb_appnexus": "1.20", + "hb_cache_id_appnexus": "55555", + "hb_cache_host_appnex": "cache.prebid.com", + "hb_cache_path_appnex": "cache", + }, + openrtb_ext.BidderRubicon: { + "hb_bidder_rubicon": "rubicon", + "hb_pb_rubicon": "1.10", + "hb_cache_id_rubicon": "cacheme", + "hb_deal_rubicon": "mydeal", + "hb_cache_host_rubico": "cache.prebid.com", + "hb_cache_path_rubico": "cache", + }, + }, + }, + }, +} + +func TestSetTargeting(t *testing.T) { + for _, test := range TargetingTests { + auc := &test.Auction + // Set rounded prices from the auction data + auc.setRoundedPrices(test.TargetData.priceGranularity) + winningBids := make(map[string]*pbsOrtbBid) + // Set winning bids from the auction data + for imp, bidsByBidder := range auc.winningBidsByBidder { + for _, bid := range bidsByBidder { + if winningBid, ok := winningBids[imp]; ok { + if winningBid.bid.Price < bid.bid.Price { + winningBids[imp] = bid + } + } else { + winningBids[imp] = bid + } + } + } + auc.winningBids = winningBids + targData := test.TargetData + targData.setTargeting(auc, test.IsApp, test.CategoryMapping) + for imp, targetsByBidder := range test.ExpectedBidTargetsByBidder { + for bidder, expected := range targetsByBidder { + assert.Equal(t, + expected, + auc.winningBidsByBidder[imp][bidder].bidTargets, + "Test: %s\nTargeting failed for bidder %s on imp %s.", + test.Description, + string(bidder), + imp) + } + } + } + +} diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index 3b297c7ab5d..09ee375be82 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -99,6 +99,9 @@ const ( HbSizeConstantKey TargetingKey = "hb_size" HbDealIDConstantKey TargetingKey = "hb_deal" + // HbFormatKey is the format of the bid. For example, "video", "banner" + HbFormatKey TargetingKey = "hb_format" + // HbCacheKey and HbVastCacheKey store UUIDs which can be used to fetch things from prebid cache. // Callers should *never* assume that either of these exist, since the call to the cache may always fail. // diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 86388f60cf4..acfd4a1e71f 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -85,6 +85,7 @@ type ExtRequestTargeting struct { IncludeWinners bool `json:"includewinners"` IncludeBidderKeys bool `json:"includebidderkeys"` IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"` + IncludeFormat bool `json:"includeformat"` DurationRangeSec []int `json:"durationrangesec"` } From deb19c39420a9315a8838d52819cffd7479a48d6 Mon Sep 17 00:00:00 2001 From: gpolaert Date: Mon, 3 Aug 2020 19:38:59 +0200 Subject: [PATCH 155/381] feat: Add new logger module - Pubstack Analytics Module (#1331) * Pubstack Analytics V1 (#11) * V1 Pubstack (#7) * feat: Add Pubstack Logger (#6) * first version of pubstack analytics * bypass viperconfig * commit #1 * gofmt * update configuration and make the tests pass * add readme on how to configure the adapter and update the network calls * update logging and fix intake url definition * feat: Pubstack Analytics Connector * fixing go mod * fix: bad behaviour on appending path to auction url * add buffering * support bootstyrap like configuration * implement route for all the objects * supports termination signal handling for goroutines * move readme to the correct location * wording * enable configuration reload + add tests * fix logs messages * fix tests * fix log line * conclude merge * merge * update go mod Co-authored-by: Amaury Ravanel * fix duplicated channel keys Co-authored-by: Amaury Ravanel * first pass - PR reviews * rename channel* -> eventChannel * dead code * Review (#10) * use json.Decoder * update documentation * use nil instead []byte("") * clean code * do not use http.DefaultClient * fix race condition (need validation) * separate the sender and buffer logics * refactor the default configuration * remove error counter * Review GP + AR * updating default config * add more logs * remove alias fields in json * fix json serializer * close event channels Co-authored-by: Amaury Ravanel * fix race condition * first pass (pr reviews) * refactor: store enabled modules into a dedicated struct * stop goroutine * test: improve coverage * PR Review * Revert "refactor: store enabled modules into a dedicated struct" This reverts commit f57d9d61680c74244effc39a5d96d6cbb2f19f7d. # Conflicts: # analytics/config/config_test.go Co-authored-by: Amaury Ravanel --- analytics/clients/http.go | 12 + analytics/config/config.go | 17 ++ analytics/config/config_test.go | 41 +++ analytics/pubstack/README.md | 28 ++ analytics/pubstack/config.go | 51 ++++ analytics/pubstack/config_test.go | 102 +++++++ .../pubstack/eventchannel/eventchannel.go | 137 +++++++++ .../eventchannel/eventchannel_test.go | 136 +++++++++ analytics/pubstack/eventchannel/sender.go | 45 +++ .../pubstack/eventchannel/sender_test.go | 40 +++ analytics/pubstack/helpers/json.go | 88 ++++++ analytics/pubstack/helpers/json_test.go | 61 ++++ .../pubstack/mocks/mock_openrtb_request.json | 64 ++++ .../pubstack/mocks/mock_openrtb_response.json | 91 ++++++ analytics/pubstack/pubstack_module.go | 273 ++++++++++++++++++ analytics/pubstack/pubstack_module_test.go | 186 ++++++++++++ config/config.go | 24 +- go.mod | 1 + go.sum | 2 + 19 files changed, 1398 insertions(+), 1 deletion(-) create mode 100644 analytics/clients/http.go create mode 100644 analytics/pubstack/README.md create mode 100644 analytics/pubstack/config.go create mode 100644 analytics/pubstack/config_test.go create mode 100644 analytics/pubstack/eventchannel/eventchannel.go create mode 100644 analytics/pubstack/eventchannel/eventchannel_test.go create mode 100644 analytics/pubstack/eventchannel/sender.go create mode 100644 analytics/pubstack/eventchannel/sender_test.go create mode 100644 analytics/pubstack/helpers/json.go create mode 100644 analytics/pubstack/helpers/json_test.go create mode 100644 analytics/pubstack/mocks/mock_openrtb_request.json create mode 100644 analytics/pubstack/mocks/mock_openrtb_response.json create mode 100644 analytics/pubstack/pubstack_module.go create mode 100644 analytics/pubstack/pubstack_module_test.go diff --git a/analytics/clients/http.go b/analytics/clients/http.go new file mode 100644 index 00000000000..bc7f863ebdd --- /dev/null +++ b/analytics/clients/http.go @@ -0,0 +1,12 @@ +package clients + +import ( + "net/http" +) + +var defaultHttpInstance = http.DefaultClient + +func GetDefaultHttpInstance() *http.Client { + // TODO 2020-06-22 @see https://github.com/prebid/prebid-server/pull/1331#discussion_r436110097 + return defaultHttpInstance +} diff --git a/analytics/config/config.go b/analytics/config/config.go index 7be7c8ecca3..7f7ded0ffc4 100644 --- a/analytics/config/config.go +++ b/analytics/config/config.go @@ -3,7 +3,9 @@ package config import ( "github.com/golang/glog" "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/analytics/clients" "github.com/prebid/prebid-server/analytics/filesystem" + "github.com/prebid/prebid-server/analytics/pubstack" "github.com/prebid/prebid-server/config" ) @@ -17,6 +19,21 @@ func NewPBSAnalytics(analytics *config.Analytics) analytics.PBSAnalyticsModule { glog.Fatalf("Could not initialize FileLogger for file %v :%v", analytics.File.Filename, err) } } + if analytics.Pubstack.Enabled { + pubstackModule, err := pubstack.NewPubstackModule( + clients.GetDefaultHttpInstance(), + analytics.Pubstack.ScopeId, + analytics.Pubstack.IntakeUrl, + analytics.Pubstack.ConfRefresh, + analytics.Pubstack.Buffers.EventCount, + analytics.Pubstack.Buffers.BufferSize, + analytics.Pubstack.Buffers.Timeout) + if err == nil { + modules = append(modules, pubstackModule) + } else { + glog.Errorf("Could not initialize PubstackModule: %v", err) + } + } return modules } diff --git a/analytics/config/config_test.go b/analytics/config/config_test.go index 7d97fb5f1be..583d475e786 100644 --- a/analytics/config/config_test.go +++ b/analytics/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "github.com/stretchr/testify/assert" "net/http" "os" "testing" @@ -73,6 +74,13 @@ func initAnalytics(count *int) analytics.PBSAnalyticsModule { } func TestNewPBSAnalytics(t *testing.T) { + pbsAnalytics := NewPBSAnalytics(&config.Analytics{}) + instance := pbsAnalytics.(enabledAnalytics) + + assert.Equal(t, len(instance), 0) +} + +func TestNewPBSAnalytics_FileLogger(t *testing.T) { if _, err := os.Stat(TEST_DIR); os.IsNotExist(err) { if err = os.MkdirAll(TEST_DIR, 0755); err != nil { t.Fatalf("Could not create test directory for FileLogger") @@ -88,4 +96,37 @@ func TestNewPBSAnalytics(t *testing.T) { default: t.Fatalf("Failed to initialize analytics module") } + + pbsAnalytics := NewPBSAnalytics(&config.Analytics{File: config.FileLogs{Filename: TEST_DIR + "/test"}}) + instance := pbsAnalytics.(enabledAnalytics) + + assert.Equal(t, len(instance), 1) +} + +func TestNewPBSAnalytics_Pubstack(t *testing.T) { + + pbsAnalyticsWithoutError := NewPBSAnalytics(&config.Analytics{ + Pubstack: config.Pubstack{ + Enabled: true, + ScopeId: "scopeId", + IntakeUrl: "https://pubstack.io/intake", + Buffers: config.PubstackBuffer{ + BufferSize: "100KB", + EventCount: 0, + Timeout: "30s", + }, + ConfRefresh: "2h", + }, + }) + instanceWithoutError := pbsAnalyticsWithoutError.(enabledAnalytics) + + assert.Equal(t, len(instanceWithoutError), 1) + + pbsAnalyticsWithError := NewPBSAnalytics(&config.Analytics{ + Pubstack: config.Pubstack{ + Enabled: true, + }, + }) + instanceWithError := pbsAnalyticsWithError.(enabledAnalytics) + assert.Equal(t, len(instanceWithError), 0) } diff --git a/analytics/pubstack/README.md b/analytics/pubstack/README.md new file mode 100644 index 00000000000..51c5fdb6bb3 --- /dev/null +++ b/analytics/pubstack/README.md @@ -0,0 +1,28 @@ +# Pubstack Analytics + +In order to use the pubstack analytics module, it needs to be configured by the host. + +You can configure the server using the following environment variables: + +```bash +export PBS_ANALYTICS_PUBSTACK_ENABLED="true" +export PBS_ANALYTICS_PUBSTACK_ENDPOINT="https://openrtb.preview.pubstack.io/v1/openrtb2" +export PBS_ANALYTICS_PUBSTACK_SCOPEID= # should be an UUIDv4 +``` + +Or using the pbs configuration file and by appending the following block: + +```yaml +analytics: + pubstack: + # Required properties + enabled: true + endpoint: "https://openrtb.preview.pubstack.io/v1/openrtb2" + scopeid: "" # The scopeId provided by the Pubstack Support Team + # Optional properties (advanced configuration) + configuration_refresh_delay: "2h" # Dynamic configuration delay + buffers: # Flush events to Pubstack when (first condition reached) + size: "2MB" # greater than 2MB + count : 100 # greater than 100 events + timeout: "15m" # greater than 15 minutes +``` \ No newline at end of file diff --git a/analytics/pubstack/config.go b/analytics/pubstack/config.go new file mode 100644 index 00000000000..472acf68ead --- /dev/null +++ b/analytics/pubstack/config.go @@ -0,0 +1,51 @@ +package pubstack + +import ( + "encoding/json" + "github.com/docker/go-units" + "net/http" + "net/url" + "time" +) + +func fetchConfig(client *http.Client, endpoint *url.URL) (*Configuration, error) { + + res, err := client.Get(endpoint.String()) + if err != nil { + return nil, err + } + + defer res.Body.Close() + c := Configuration{} + err = json.NewDecoder(res.Body).Decode(&c) + if err != nil { + return nil, err + } + return &c, nil +} + +func newBufferConfig(count int, size, duration string) (*bufferConfig, error) { + pDuration, err := time.ParseDuration(duration) + if err != nil { + return nil, err + } + pSize, err := units.FromHumanSize(size) + if err != nil { + return nil, err + } + return &bufferConfig{ + pDuration, + int64(count), + pSize, + }, nil +} + +func (a *Configuration) isSameAs(b *Configuration) bool { + sameEndpoint := a.Endpoint == b.Endpoint + sameScopeID := a.ScopeID == b.ScopeID + sameFeature := len(a.Features) == len(b.Features) + for key := range a.Features { + sameFeature = sameFeature && a.Features[key] == b.Features[key] + } + return sameFeature && sameEndpoint && sameScopeID +} diff --git a/analytics/pubstack/config_test.go b/analytics/pubstack/config_test.go new file mode 100644 index 00000000000..bb6fd0bddbb --- /dev/null +++ b/analytics/pubstack/config_test.go @@ -0,0 +1,102 @@ +package pubstack + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestFetchConfig(t *testing.T) { + configResponse := `{ + "scopeId": "scopeId", + "endpoint": "https://pubstack.io", + "features": { + "auction": true, + "cookiesync": true, + "amp": true, + "setuid": false, + "video": false + } + }` + + server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + res.Write([]byte(configResponse)) + res.WriteHeader(200) + })) + + defer server.Close() + + endpoint, _ := url.Parse(server.URL) + cfg, _ := fetchConfig(server.Client(), endpoint) + + assert.Equal(t, cfg.ScopeID, "scopeId") + assert.Equal(t, cfg.Endpoint, "https://pubstack.io") + assert.Equal(t, cfg.Features[auction], true) + assert.Equal(t, cfg.Features[cookieSync], true) + assert.Equal(t, cfg.Features[amp], true) + assert.Equal(t, cfg.Features[setUID], false) + assert.Equal(t, cfg.Features[video], false) +} + +func TestFetchConfig_Error(t *testing.T) { + configResponse := `{ + "error": "scopeId", + }` + + server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + res.Write([]byte(configResponse)) + res.WriteHeader(200) + })) + + defer server.Close() + + endpoint, _ := url.Parse(server.URL) + cfg, err := fetchConfig(server.Client(), endpoint) + + assert.Nil(t, cfg) + assert.NotNil(t, err) +} + +func TestIsSameAs(t *testing.T) { + copyConfig := func(conf Configuration) *Configuration { + newConfig := conf + newConfig.Features = make(map[string]bool) + for k := range conf.Features { + newConfig.Features[k] = conf.Features[k] + } + return &newConfig + } + + a := &Configuration{ + ScopeID: "scopeId", + Endpoint: "endpoint", + Features: map[string]bool{ + "auction": true, + "cookiesync": true, + "amp": true, + "setuid": false, + "video": false, + }, + } + + assert.True(t, a.isSameAs(copyConfig(*a))) + + b := copyConfig(*a) + b.ScopeID = "anotherId" + assert.False(t, a.isSameAs(b)) + + b = copyConfig(*a) + b.Endpoint = "anotherEndpoint" + assert.False(t, a.isSameAs(b)) + + b = copyConfig(*a) + b.Features["auction"] = true + assert.True(t, a.isSameAs(b)) + b.Features["auction"] = false + assert.False(t, a.isSameAs(b)) + +} diff --git a/analytics/pubstack/eventchannel/eventchannel.go b/analytics/pubstack/eventchannel/eventchannel.go new file mode 100644 index 00000000000..b8dc4dd8e28 --- /dev/null +++ b/analytics/pubstack/eventchannel/eventchannel.go @@ -0,0 +1,137 @@ +package eventchannel + +import ( + "bytes" + "compress/gzip" + "sync" + "time" + + "github.com/golang/glog" +) + +type Metrics struct { + bufferSize int64 + eventCount int64 +} +type Limit struct { + maxByteSize int64 + maxEventCount int64 + maxTime time.Duration +} +type EventChannel struct { + gz *gzip.Writer + buff *bytes.Buffer + + ch chan []byte + endCh chan int + metrics Metrics + muxGzBuffer sync.RWMutex + send Sender + limit Limit +} + +func NewEventChannel(sender Sender, maxByteSize, maxEventCount int64, maxTime time.Duration) *EventChannel { + b := &bytes.Buffer{} + gzw := gzip.NewWriter(b) + + c := EventChannel{ + gz: gzw, + buff: b, + ch: make(chan []byte), + endCh: make(chan int), + metrics: Metrics{}, + send: sender, + limit: Limit{maxByteSize, maxEventCount, maxTime}, + } + go c.start() + return &c +} + +func (c *EventChannel) Push(event []byte) { + c.ch <- event +} + +func (c *EventChannel) Close() { + c.endCh <- 1 +} + +func (c *EventChannel) buffer(event []byte) { + c.muxGzBuffer.Lock() + defer c.muxGzBuffer.Unlock() + + _, err := c.gz.Write(event) + if err != nil { + glog.Warning("[pubstack] fail to compress, skip the event") + return + } + + c.metrics.eventCount++ + c.metrics.bufferSize += int64(len(event)) +} + +func (c *EventChannel) isBufferFull() bool { + c.muxGzBuffer.RLock() + defer c.muxGzBuffer.RUnlock() + return c.metrics.eventCount >= c.limit.maxEventCount || c.metrics.bufferSize >= c.limit.maxByteSize +} + +func (c *EventChannel) reset() { + // reset buffer + c.gz.Reset(c.buff) + c.buff.Reset() + + // reset metrics + c.metrics.eventCount = 0 + c.metrics.bufferSize = 0 +} + +func (c *EventChannel) flush() { + c.muxGzBuffer.Lock() + defer c.muxGzBuffer.Unlock() + + if c.metrics.eventCount == 0 || c.metrics.bufferSize == 0 { + return + } + + // finish writing gzip header + err := c.gz.Flush() + if err != nil { + glog.Warning("[pubstack] fail to flush gzipped buffer") + return + } + + // copy the current buffer to send the payload in a new thread + payload := make([]byte, c.buff.Len()) + _, err = c.buff.Read(payload) + if err != nil { + glog.Warning("[pubstack] fail to copy the buffer") + return + } + + // reset buffers and writers + c.reset() + + // send events (async) + go c.send(payload) +} + +func (c *EventChannel) start() { + ticker := time.NewTicker(c.limit.maxTime) + + for { + select { + case <-c.endCh: + c.flush() + return + // event is received + case event := <-c.ch: + c.buffer(event) + if c.isBufferFull() { + c.flush() + } + // time between 2 flushes has passed + case <-ticker.C: + c.flush() + } + } +} diff --git a/analytics/pubstack/eventchannel/eventchannel_test.go b/analytics/pubstack/eventchannel/eventchannel_test.go new file mode 100644 index 00000000000..9fdcfe976a6 --- /dev/null +++ b/analytics/pubstack/eventchannel/eventchannel_test.go @@ -0,0 +1,136 @@ +package eventchannel + +import ( + "bytes" + "compress/gzip" + "github.com/stretchr/testify/assert" + "io/ioutil" + "sync" + "testing" + "time" +) + +var maxByteSize = int64(15) +var maxEventCount = int64(3) +var maxTime = 2 * time.Hour + +func readGz(encoded bytes.Buffer) string { + gr, _ := gzip.NewReader(bytes.NewBuffer(encoded.Bytes())) + defer gr.Close() + + decoded, _ := ioutil.ReadAll(gr) + return string(decoded) +} + +func newSender(data *[]byte) Sender { + mux := &sync.Mutex{} + return func(payload []byte) error { + mux.Lock() + defer mux.Unlock() + event := bytes.Buffer{} + event.Write(payload) + *data = append(*data, readGz(event)...) + return nil + } +} + +func TestEventChannel_isBufferFull(t *testing.T) { + + send := func(_ []byte) error { return nil } + + eventChannel := NewEventChannel(send, maxByteSize, maxEventCount, maxTime) + defer eventChannel.Close() + + eventChannel.buffer([]byte("one")) + eventChannel.buffer([]byte("two")) + + assert.Equal(t, eventChannel.isBufferFull(), false) + + eventChannel.buffer([]byte("three")) + + assert.Equal(t, eventChannel.isBufferFull(), true) + + eventChannel.reset() + + assert.Equal(t, eventChannel.isBufferFull(), false) + + eventChannel.buffer([]byte("big-event-abcdefghijklmnopqrstuvwxyz")) + + assert.Equal(t, eventChannel.isBufferFull(), true) + +} + +func TestEventChannel_reset(t *testing.T) { + send := func(_ []byte) error { return nil } + + eventChannel := NewEventChannel(send, maxByteSize, maxEventCount, maxTime) + defer eventChannel.Close() + + assert.Equal(t, eventChannel.metrics.eventCount, int64(0)) + assert.Equal(t, eventChannel.metrics.bufferSize, int64(0)) + + eventChannel.buffer([]byte("one")) + eventChannel.buffer([]byte("two")) + + assert.NotEqual(t, eventChannel.metrics.eventCount, int64(0)) + assert.NotEqual(t, eventChannel.metrics.bufferSize, int64(0)) + + eventChannel.reset() + + assert.Equal(t, eventChannel.buff.Len(), 0) + assert.Equal(t, eventChannel.metrics.eventCount, int64(0)) + assert.Equal(t, eventChannel.metrics.bufferSize, int64(0)) +} + +func TestEventChannel_flush(t *testing.T) { + data := make([]byte, 0) + send := newSender(&data) + + eventChannel := NewEventChannel(send, maxByteSize, maxEventCount, maxTime) + defer eventChannel.Close() + + eventChannel.buffer([]byte("one")) + eventChannel.buffer([]byte("two")) + eventChannel.buffer([]byte("three")) + eventChannel.flush() + time.Sleep(10 * time.Millisecond) + + assert.Equal(t, string(data), "onetwothree") +} + +func TestEventChannel_close(t *testing.T) { + data := make([]byte, 0) + send := newSender(&data) + + eventChannel := NewEventChannel(send, 15000, 15000, 2*time.Hour) + + eventChannel.buffer([]byte("one")) + eventChannel.buffer([]byte("two")) + eventChannel.buffer([]byte("three")) + eventChannel.Close() + + time.Sleep(10 * time.Millisecond) + + assert.Equal(t, string(data), "onetwothree") +} + +func TestEventChannel_Push(t *testing.T) { + data := make([]byte, 0) + send := newSender(&data) + + eventChannel := NewEventChannel(send, 15000, 5, 5*time.Millisecond) + defer eventChannel.Close() + + eventChannel.Push([]byte("one")) + eventChannel.Push([]byte("two")) + eventChannel.Push([]byte("three")) + eventChannel.Push([]byte("four")) + eventChannel.Push([]byte("five")) + eventChannel.Push([]byte("six")) + eventChannel.Push([]byte("seven")) + + time.Sleep(10 * time.Millisecond) + + assert.Equal(t, string(data), "onetwothreefourfivesixseven") + +} diff --git a/analytics/pubstack/eventchannel/sender.go b/analytics/pubstack/eventchannel/sender.go new file mode 100644 index 00000000000..951de4d414e --- /dev/null +++ b/analytics/pubstack/eventchannel/sender.go @@ -0,0 +1,45 @@ +package eventchannel + +import ( + "bytes" + "fmt" + "github.com/golang/glog" + "net/http" + "net/url" + "path" +) + +type Sender = func(payload []byte) error + +func NewHttpSender(client *http.Client, endpoint string) Sender { + return func(payload []byte) error { + req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload)) + if err != nil { + glog.Error(err) + return err + } + + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Encoding", "gzip") + + resp, err := client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + glog.Errorf("[pubstack] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) + return fmt.Errorf("wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) + } + return nil + } +} + +func BuildEndpointSender(client *http.Client, baseUrl string, module string) Sender { + endpoint, err := url.Parse(baseUrl) + if err != nil { + glog.Error(err) + } + endpoint.Path = path.Join(endpoint.Path, "intake", module) + return NewHttpSender(client, endpoint.String()) +} diff --git a/analytics/pubstack/eventchannel/sender_test.go b/analytics/pubstack/eventchannel/sender_test.go new file mode 100644 index 00000000000..1185435e4ab --- /dev/null +++ b/analytics/pubstack/eventchannel/sender_test.go @@ -0,0 +1,40 @@ +package eventchannel + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildEndpointSender(t *testing.T) { + requestBody := make([]byte, 10) + server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + requestBody, _ = ioutil.ReadAll(req.Body) + res.WriteHeader(200) + })) + + defer server.Close() + + sender := BuildEndpointSender(server.Client(), server.URL, "module") + err := sender([]byte("message")) + + assert.Equal(t, requestBody, []byte("message")) + assert.Nil(t, err) +} + +func TestBuildEndpointSender_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(400) + })) + + defer server.Close() + + sender := BuildEndpointSender(server.Client(), server.URL, "module") + err := sender([]byte("message")) + + assert.NotNil(t, err) +} diff --git a/analytics/pubstack/helpers/json.go b/analytics/pubstack/helpers/json.go new file mode 100644 index 00000000000..f02f1120626 --- /dev/null +++ b/analytics/pubstack/helpers/json.go @@ -0,0 +1,88 @@ +package helpers + +import ( + "encoding/json" + "fmt" + + "github.com/prebid/prebid-server/analytics" +) + +func JsonifyAuctionObject(ao *analytics.AuctionObject, scope string) ([]byte, error) { + b, err := json.Marshal(&struct { + Scope string `json:"scope"` + *analytics.AuctionObject + }{ + Scope: scope, + AuctionObject: ao, + }) + + if err == nil { + b = append(b, byte('\n')) + return b, nil + } + return nil, fmt.Errorf("auction object badly formed %v", err) +} + +func JsonifyVideoObject(vo *analytics.VideoObject, scope string) ([]byte, error) { + b, err := json.Marshal(&struct { + Scope string `json:"scope"` + *analytics.VideoObject + }{ + Scope: scope, + VideoObject: vo, + }) + + if err == nil { + b = append(b, byte('\n')) + return b, nil + } + return nil, fmt.Errorf("video object badly formed %v", err) +} + +func JsonifyCookieSync(cso *analytics.CookieSyncObject, scope string) ([]byte, error) { + b, err := json.Marshal(&struct { + Scope string `json:"scope"` + *analytics.CookieSyncObject + }{ + Scope: scope, + CookieSyncObject: cso, + }) + + if err == nil { + b = append(b, byte('\n')) + return b, nil + } + return nil, fmt.Errorf("cookie sync object badly formed %v", err) +} + +func JsonifySetUIDObject(so *analytics.SetUIDObject, scope string) ([]byte, error) { + b, err := json.Marshal(&struct { + Scope string `json:"scope"` + *analytics.SetUIDObject + }{ + Scope: scope, + SetUIDObject: so, + }) + + if err == nil { + b = append(b, byte('\n')) + return b, nil + } + return nil, fmt.Errorf("set UID object badly formed %v", err) +} + +func JsonifyAmpObject(ao *analytics.AmpObject, scope string) ([]byte, error) { + b, err := json.Marshal(&struct { + Scope string `json:"scope"` + *analytics.AmpObject + }{ + Scope: scope, + AmpObject: ao, + }) + + if err == nil { + b = append(b, byte('\n')) + return b, nil + } + return nil, fmt.Errorf("amp object badly formed %v", err) +} diff --git a/analytics/pubstack/helpers/json_test.go b/analytics/pubstack/helpers/json_test.go new file mode 100644 index 00000000000..4e36e8db2be --- /dev/null +++ b/analytics/pubstack/helpers/json_test.go @@ -0,0 +1,61 @@ +package helpers + +import ( + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/analytics" + "github.com/prebid/prebid-server/usersync" + "net/http" + "testing" +) + +func TestJsonifyAuctionObject(t *testing.T) { + ao := &analytics.AuctionObject{ + Status: http.StatusOK, + } + if _, err := JsonifyAuctionObject(ao, "scopeId"); err != nil { + t.Fail() + } + +} + +func TestJsonifyVideoObject(t *testing.T) { + vo := &analytics.VideoObject{ + Status: http.StatusOK, + } + if _, err := JsonifyVideoObject(vo, "scopeId"); err != nil { + t.Fail() + } +} + +func TestJsonifyCookieSync(t *testing.T) { + cso := &analytics.CookieSyncObject{ + Status: http.StatusOK, + BidderStatus: []*usersync.CookieSyncBidders{}, + } + if _, err := JsonifyCookieSync(cso, "scopeId"); err != nil { + t.Fail() + } +} + +func TestJsonifySetUIDObject(t *testing.T) { + so := &analytics.SetUIDObject{ + Status: http.StatusOK, + Bidder: "any-bidder", + UID: "uid string", + } + if _, err := JsonifySetUIDObject(so, "scopeId"); err != nil { + t.Fail() + } +} + +func TestJsonifyAmpObject(t *testing.T) { + ao := &analytics.AmpObject{ + Status: http.StatusOK, + Errors: make([]error, 0), + AuctionResponse: &openrtb.BidResponse{}, + AmpTargetingValues: map[string]string{}, + } + if _, err := JsonifyAmpObject(ao, "scopeId"); err != nil { + t.Fail() + } +} diff --git a/analytics/pubstack/mocks/mock_openrtb_request.json b/analytics/pubstack/mocks/mock_openrtb_request.json new file mode 100644 index 00000000000..03b9665b247 --- /dev/null +++ b/analytics/pubstack/mocks/mock_openrtb_request.json @@ -0,0 +1,64 @@ +{ + "id": "19c2eeb8-824c-4604-af41-a59b2b7bb895", + "site": { + "page": "https%3A%2F%2Fdebug.mediasquare.fr%2Fdebug%2Fprebid%2Fmsq_desktop.html%3Fpbjs_debug%3Dtrue" + }, + "user": { + "ext": {} + }, + "regs": { + "ext": {} + }, + "test": 1, + "imp": [ + { + "id": "0341252e-b3b0-4dff-a0ef-1ced63369bd5", + "ext": { + "appnexus": { + "placementId": 5724999 + } + }, + "secure": 1, + "banner": { + "format": [ + { + "w": 970, + "h": 250 + } + ] + } + }, + { + "id": "3ac0ffa3-01de-44d2-9baf-1fee79026624", + "ext": { + "msqClassic": { + "placementId": 10471298 + } + }, + "secure": 1, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "bidadjustmentfactors": { + "msqClassic": 0.8, + "msqBrand": 0.8, + "msqMax": 0.8, + "msqMaxView": 0.8 + } + } + } +} \ No newline at end of file diff --git a/analytics/pubstack/mocks/mock_openrtb_response.json b/analytics/pubstack/mocks/mock_openrtb_response.json new file mode 100644 index 00000000000..6f4d1965b8c --- /dev/null +++ b/analytics/pubstack/mocks/mock_openrtb_response.json @@ -0,0 +1,91 @@ +{ + "id": "19c2eeb8-824c-4604-af41-a59b2b7bb895", + "seatbid": [{ + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "0341252e-b3b0-4dff-a0ef-1ced63369bd5", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 970, + "w": 250, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000 + } + } + }, + { + "id": "7706636740145184842", + "impid": "0341252e-b3b0-4dff-a0ef-1ced63369bd5", + "price": 0.0, + "adid": "29681114", + "adm": "some-test-ad2", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681114", + "cid": "959", + "crid": "29681114", + "h": 970, + "w": 250, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000 + } + } + },{ + "id": "7706636740145184842", + "impid": "3ac0ffa3-01de-44d2-9baf-1fee79026624", + "price": 0.5234, + "adid": "29681113", + "adm": "some-test-ad2", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681113", + "cid": "959", + "crid": "29681113", + "h": 970, + "w": 250, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000 + } + } + }] + }, { + "seat": "improvedigital", + "bid": [{ + "id": "randomid", + "impid": "0341252e-b3b0-4dff-a0ef-1ced63369bd5", + "price": 0.510000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" +} diff --git a/analytics/pubstack/pubstack_module.go b/analytics/pubstack/pubstack_module.go new file mode 100644 index 00000000000..9f1a81c7232 --- /dev/null +++ b/analytics/pubstack/pubstack_module.go @@ -0,0 +1,273 @@ +package pubstack + +import ( + "fmt" + "github.com/prebid/prebid-server/analytics/pubstack/eventchannel" + "net/http" + "net/url" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/golang/glog" + "github.com/prebid/prebid-server/analytics/pubstack/helpers" + + "github.com/prebid/prebid-server/analytics" +) + +type Configuration struct { + ScopeID string `json:"scopeId"` + Endpoint string `json:"endpoint"` + Features map[string]bool `json:"features"` +} + +// routes for events +const ( + auction = "auction" + cookieSync = "cookiesync" + amp = "amp" + setUID = "setuid" + video = "video" +) + +type bufferConfig struct { + timeout time.Duration + count int64 + size int64 +} + +type PubstackModule struct { + eventChannels map[string]*eventchannel.EventChannel + httpClient *http.Client + configCh chan *Configuration + sigTermCh chan os.Signal + scope string + cfg *Configuration + buffsCfg *bufferConfig + muxConfig sync.RWMutex +} + +func NewPubstackModule(client *http.Client, scope, endpoint, configRefreshDelay string, maxEventCount int, maxByteSize, maxTime string) (analytics.PBSAnalyticsModule, error) { + glog.Infof("[pubstack] Initializing module scope=%s endpoint=%s\n", scope, endpoint) + + // parse args + + refreshDelay, err := time.ParseDuration(configRefreshDelay) + if err != nil { + return nil, fmt.Errorf("fail to parse the module args, arg=analytics.pubstack.configuration_refresh_delay, :%v", err) + } + + bufferCfg, err := newBufferConfig(maxEventCount, maxByteSize, maxTime) + if err != nil { + return nil, fmt.Errorf("fail to parse the module args, arg=analytics.pubstack.buffers, :%v", err) + } + + defaultFeatures := map[string]bool{ + auction: false, + video: false, + amp: false, + cookieSync: false, + setUID: false, + } + + defaultConfig := &Configuration{ + ScopeID: scope, + Endpoint: endpoint, + Features: defaultFeatures, + } + + pb := PubstackModule{ + scope: scope, + httpClient: client, + cfg: defaultConfig, + buffsCfg: bufferCfg, + sigTermCh: make(chan os.Signal), + configCh: make(chan *Configuration), + eventChannels: make(map[string]*eventchannel.EventChannel), + muxConfig: sync.RWMutex{}, + } + signal.Notify(pb.sigTermCh, os.Interrupt, syscall.SIGTERM) + + configUrl, err := url.Parse(pb.cfg.Endpoint + "/bootstrap?scopeId=" + pb.cfg.ScopeID) + if err != nil { + glog.Error(err) + return nil, err + } + go pb.start(configUrl, refreshDelay) + go func() { + err = pb.reloadConfig(configUrl) + if err != nil { + glog.Errorf("[pubstack] Fail to fetch remote configuration: %v", err) + } + }() + + glog.Info("[pubstack] Pubstack analytics configured and ready") + return &pb, nil +} + +func (p *PubstackModule) LogAuctionObject(ao *analytics.AuctionObject) { + p.muxConfig.RLock() + defer p.muxConfig.RUnlock() + + if !p.isFeatureEnable(auction) { + return + } + + // serialize event + payload, err := helpers.JsonifyAuctionObject(ao, p.scope) + if err != nil { + glog.Warning("[pubstack] Cannot serialize auction") + return + } + + p.eventChannels[auction].Push(payload) +} + +func (p *PubstackModule) LogVideoObject(vo *analytics.VideoObject) { + p.muxConfig.RLock() + defer p.muxConfig.RUnlock() + + if !p.isFeatureEnable(video) { + return + } + + // serialize event + payload, err := helpers.JsonifyVideoObject(vo, p.scope) + if err != nil { + glog.Warning("[pubstack] Cannot serialize video") + return + } + + p.eventChannels[video].Push(payload) +} + +func (p *PubstackModule) LogSetUIDObject(so *analytics.SetUIDObject) { + p.muxConfig.RLock() + defer p.muxConfig.RUnlock() + + if !p.isFeatureEnable(setUID) { + return + } + + // serialize event + payload, err := helpers.JsonifySetUIDObject(so, p.scope) + if err != nil { + glog.Warning("[pubstack] Cannot serialize video") + return + } + + p.eventChannels[setUID].Push(payload) +} + +func (p *PubstackModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) { + p.muxConfig.RLock() + defer p.muxConfig.RUnlock() + + if !p.isFeatureEnable(cookieSync) { + return + } + + // serialize event + payload, err := helpers.JsonifyCookieSync(cso, p.scope) + if err != nil { + glog.Warning("[pubstack] Cannot serialize video") + return + } + + p.eventChannels[cookieSync].Push(payload) + +} + +func (p *PubstackModule) LogAmpObject(ao *analytics.AmpObject) { + p.muxConfig.RLock() + defer p.muxConfig.RUnlock() + + if !p.isFeatureEnable(amp) { + return + } + + // serialize event + payload, err := helpers.JsonifyAmpObject(ao, p.scope) + if err != nil { + glog.Warning("[pubstack] Cannot serialize video") + return + } + + p.eventChannels[amp].Push(payload) + +} + +func (p *PubstackModule) reloadConfig(configUrl *url.URL) error { + config, err := fetchConfig(p.httpClient, configUrl) + if err != nil { + return err + } + p.configCh <- config + return nil +} + +func (p *PubstackModule) start(configUrl *url.URL, refreshDelay time.Duration) { + + tick := time.NewTicker(refreshDelay) + + for { + select { + case <-p.sigTermCh: + p.closeAllEventChannels() + return + case config := <-p.configCh: + p.updateConfig(config) + glog.Infof("[pubstack] Updating config: %v", p.cfg) + case <-tick.C: + go func() { + err := p.reloadConfig(configUrl) + if err != nil { + glog.Errorf("[pubstack] Fail to fetch remote configuration: %v", err) + } + }() + } + } + +} + +func (p *PubstackModule) updateConfig(config *Configuration) { + p.muxConfig.Lock() + defer p.muxConfig.Unlock() + + if p.cfg.isSameAs(config) { + return + } + + p.cfg = config + p.closeAllEventChannels() + + if p.isFeatureEnable(amp) { + p.eventChannels[amp] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, amp), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout) + } + if p.isFeatureEnable(auction) { + p.eventChannels[auction] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, auction), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout) + } + if p.isFeatureEnable(cookieSync) { + p.eventChannels[cookieSync] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, cookieSync), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout) + } + if p.isFeatureEnable(video) { + p.eventChannels[video] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, video), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout) + } + if p.isFeatureEnable(setUID) { + p.eventChannels[setUID] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, setUID), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout) + } +} + +func (p *PubstackModule) closeAllEventChannels() { + for key, ch := range p.eventChannels { + ch.Close() + delete(p.eventChannels, key) + } +} + +func (p *PubstackModule) isFeatureEnable(feature string) bool { + val, ok := p.cfg.Features[feature] + return ok && val +} diff --git a/analytics/pubstack/pubstack_module_test.go b/analytics/pubstack/pubstack_module_test.go new file mode 100644 index 00000000000..8d4dfdd689f --- /dev/null +++ b/analytics/pubstack/pubstack_module_test.go @@ -0,0 +1,186 @@ +package pubstack + +import ( + "encoding/json" + "github.com/prebid/prebid-server/analytics/pubstack/eventchannel" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/analytics" + "github.com/stretchr/testify/assert" +) + +func loadJsonFromFile() (*analytics.AuctionObject, error) { + req, err := os.Open("mocks/mock_openrtb_request.json") + if err != nil { + return nil, err + } + defer req.Close() + + reqCtn := openrtb.BidRequest{} + reqPayload, err := ioutil.ReadAll(req) + if err != nil { + return nil, err + } + + err = json.Unmarshal(reqPayload, &reqCtn) + if err != nil { + return nil, err + } + + res, err := os.Open("mocks/mock_openrtb_response.json") + if err != nil { + return nil, err + } + defer res.Close() + + resCtn := openrtb.BidResponse{} + resPayload, err := ioutil.ReadAll(res) + if err != nil { + return nil, err + } + + err = json.Unmarshal(resPayload, &resCtn) + if err != nil { + return nil, err + } + + return &analytics.AuctionObject{ + Request: &reqCtn, + Response: &resCtn, + }, nil +} + +func TestPubstackModule(t *testing.T) { + + remoteConfig := &Configuration{} + server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + data, _ := json.Marshal(remoteConfig) + res.Write(data) + })) + client := server.Client() + + defer server.Close() + + // Loading Issues + _, err := NewPubstackModule(client, "scope", server.URL, "1z", 100, "90MB", "15m") + assert.NotNil(t, err) // should raise an error since we can't parse args // configRefreshDelay + + _, err = NewPubstackModule(client, "scope", server.URL, "1h", 100, "90z", "15m") + assert.NotNil(t, err) // should raise an error since we can't parse args // maxByte + + _, err = NewPubstackModule(client, "scope", server.URL, "1h", 100, "90MB", "15z") + assert.NotNil(t, err) // should raise an error since we can't parse args // maxTime + + // Loading OK + module, err := NewPubstackModule(client, "scope", server.URL, "10ms", 100, "90MB", "15m") + assert.Nil(t, err) + + // Default Configuration + pubstack, ok := module.(*PubstackModule) + assert.Equal(t, ok, true) //PBSAnalyticsModule is also a PubstackModule + assert.Equal(t, len(pubstack.cfg.Features), 5) + assert.Equal(t, pubstack.cfg.Features[auction], false) + assert.Equal(t, pubstack.cfg.Features[video], false) + assert.Equal(t, pubstack.cfg.Features[amp], false) + assert.Equal(t, pubstack.cfg.Features[setUID], false) + assert.Equal(t, pubstack.cfg.Features[cookieSync], false) + + assert.Equal(t, len(pubstack.eventChannels), 0) + + // Process Auction Event + counter := 0 + send := func(_ []byte) error { + counter++ + return nil + } + mockedEvent, err := loadJsonFromFile() + if err != nil { + t.Fail() + } + + pubstack.eventChannels[auction] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[video] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[amp] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[setUID] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[cookieSync] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + + pubstack.LogAuctionObject(mockedEvent) + pubstack.LogAmpObject(&analytics.AmpObject{ + Status: http.StatusOK, + }) + pubstack.LogCookieSyncObject(&analytics.CookieSyncObject{ + Status: http.StatusOK, + }) + pubstack.LogVideoObject(&analytics.VideoObject{ + Status: http.StatusOK, + }) + pubstack.LogSetUIDObject(&analytics.SetUIDObject{ + Status: http.StatusOK, + }) + + pubstack.closeAllEventChannels() + time.Sleep(10 * time.Millisecond) // process channel + assert.Equal(t, counter, 0) + + // Hot-Reload config + newFeatures := make(map[string]bool) + newFeatures[auction] = true + newFeatures[video] = true + newFeatures[amp] = true + newFeatures[cookieSync] = true + newFeatures[setUID] = true + + remoteConfig = &Configuration{ + ScopeID: "new-scope", + Endpoint: "new-endpoint", + Features: newFeatures, + } + + endpoint, _ := url.Parse(server.URL) + pubstack.reloadConfig(endpoint) + + time.Sleep(2 * time.Millisecond) // process channel + assert.Equal(t, len(pubstack.cfg.Features), 5) + assert.Equal(t, pubstack.cfg.Features[auction], true) + assert.Equal(t, pubstack.cfg.Features[video], true) + assert.Equal(t, pubstack.cfg.Features[amp], true) + assert.Equal(t, pubstack.cfg.Features[setUID], true) + assert.Equal(t, pubstack.cfg.Features[cookieSync], true) + assert.Equal(t, pubstack.cfg.ScopeID, "new-scope") + assert.Equal(t, pubstack.cfg.Endpoint, "new-endpoint") + assert.Equal(t, len(pubstack.eventChannels), 5) + + counter = 0 + pubstack.eventChannels[auction] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[video] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[amp] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[setUID] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + pubstack.eventChannels[cookieSync] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second) + + pubstack.LogAuctionObject(mockedEvent) + pubstack.LogAmpObject(&analytics.AmpObject{ + Status: http.StatusOK, + }) + pubstack.LogCookieSyncObject(&analytics.CookieSyncObject{ + Status: http.StatusOK, + }) + pubstack.LogVideoObject(&analytics.VideoObject{ + Status: http.StatusOK, + }) + pubstack.LogSetUIDObject(&analytics.SetUIDObject{ + Status: http.StatusOK, + }) + pubstack.closeAllEventChannels() + time.Sleep(10 * time.Millisecond) + + assert.Equal(t, counter, 5) + +} diff --git a/config/config.go b/config/config.go index 8545523d238..67689d1ab1a 100755 --- a/config/config.go +++ b/config/config.go @@ -209,7 +209,8 @@ type LMT struct { } type Analytics struct { - File FileLogs `mapstructure:"file"` + File FileLogs `mapstructure:"file"` + Pubstack Pubstack `mapstructure:"pubstack"` } type CurrencyConverter struct { @@ -230,6 +231,20 @@ type FileLogs struct { Filename string `mapstructure:"filename"` } +type Pubstack struct { + Enabled bool `mapstructure:"enabled"` + ScopeId string `mapstructure:"scopeid"` + IntakeUrl string `mapstructure:"endpoint"` + Buffers PubstackBuffer `mapstructure:"buffers"` + ConfRefresh string `mapstructure:"configuration_refresh_delay"` +} + +type PubstackBuffer struct { + BufferSize string `mapstructure:"size"` + EventCount int `mapstructure:"count"` + Timeout string `mapstructure:"timeout"` +} + type HostCookie struct { Domain string `mapstructure:"domain"` Family string `mapstructure:"family"` @@ -855,6 +870,13 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("max_request_size", 1024*256) v.SetDefault("analytics.file.filename", "") + v.SetDefault("analytics.pubstack.endpoint", "https://s2s.pbstck.com/v1") + v.SetDefault("analytics.pubstack.scopeid", "change-me") + v.SetDefault("analytics.pubstack.enabled", false) + v.SetDefault("analytics.pubstack.configuration_refresh_delay", "2h") + v.SetDefault("analytics.pubstack.buffers.size", "2MB") + v.SetDefault("analytics.pubstack.buffers.count", 100) + v.SetDefault("analytics.pubstack.buffers.timeout", "900s") v.SetDefault("amp_timeout_adjustment_ms", 0) v.SetDefault("gdpr.host_vendor_id", 0) v.SetDefault("gdpr.usersync_if_ambiguous", false) diff --git a/go.mod b/go.mod index 00cadd31ce1..a5b5a161cf4 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/cespare/xxhash v1.0.0 // indirect github.com/chasex/glog v0.0.0-20160217080310-c62392af379c github.com/coocood/freecache v1.0.1 + github.com/docker/go-units v0.4.0 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd github.com/gofrs/uuid v3.2.0+incompatible diff --git a/go.sum b/go.sum index 5eaf37cad9f..1ddab71332a 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/coocood/freecache v1.0.1/go.mod h1:ePwxCDzOYvARfHdr1pByNct1at3CoKnsip github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd h1:biTJQdqouE5by89AAffXG8++TY+9Fsdrg5rinbt3tHk= From 8740179c573f966147a91ce5f32b060bcc3e8243 Mon Sep 17 00:00:00 2001 From: Vikram Date: Thu, 6 Aug 2020 17:12:08 +0200 Subject: [PATCH 156/381] New bid adapter for Smaato (#1413) Co-authored-by: vikram Co-authored-by: Stephan --- adapters/smaato/image.go | 53 ++++ adapters/smaato/image_test.go | 44 +++ adapters/smaato/params_test.go | 65 +++++ adapters/smaato/richmedia.go | 52 ++++ adapters/smaato/richmedia_test.go | 39 +++ adapters/smaato/smaato.go | 276 ++++++++++++++++++ adapters/smaato/smaato_test.go | 11 + .../exemplary/simple-banner-richMedia.json | 194 ++++++++++++ .../smaatotest/exemplary/simple-banner.json | 190 ++++++++++++ adapters/smaato/smaatotest/params/banner.json | 4 + .../supplemental/bad-adm-response.json | 166 +++++++++++ .../smaatotest/supplemental/bad-ext-req.json | 54 ++++ .../bad-imp-banner-format-req.json | 61 ++++ .../supplemental/bad-user-ext-data-req.json | 67 +++++ .../supplemental/bad-user-ext-req.json | 57 ++++ .../supplemental/no-consent-info.json | 137 +++++++++ .../smaatotest/supplemental/no-imp-req.json | 17 ++ config/config.go | 1 + docs/bidders/smaato.md | 42 +++ exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_smaato.go | 9 + static/bidder-info/smaato.yaml | 9 + static/bidder-params/smaato.json | 17 ++ usersync/usersyncers/syncer_test.go | 1 + 25 files changed, 1570 insertions(+) create mode 100644 adapters/smaato/image.go create mode 100644 adapters/smaato/image_test.go create mode 100644 adapters/smaato/params_test.go create mode 100644 adapters/smaato/richmedia.go create mode 100644 adapters/smaato/richmedia_test.go create mode 100644 adapters/smaato/smaato.go create mode 100644 adapters/smaato/smaato_test.go create mode 100644 adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json create mode 100644 adapters/smaato/smaatotest/exemplary/simple-banner.json create mode 100644 adapters/smaato/smaatotest/params/banner.json create mode 100644 adapters/smaato/smaatotest/supplemental/bad-adm-response.json create mode 100644 adapters/smaato/smaatotest/supplemental/bad-ext-req.json create mode 100644 adapters/smaato/smaatotest/supplemental/bad-imp-banner-format-req.json create mode 100644 adapters/smaato/smaatotest/supplemental/bad-user-ext-data-req.json create mode 100644 adapters/smaato/smaatotest/supplemental/bad-user-ext-req.json create mode 100644 adapters/smaato/smaatotest/supplemental/no-consent-info.json create mode 100644 adapters/smaato/smaatotest/supplemental/no-imp-req.json create mode 100644 docs/bidders/smaato.md create mode 100644 openrtb_ext/imp_smaato.go create mode 100644 static/bidder-info/smaato.yaml create mode 100644 static/bidder-params/smaato.json diff --git a/adapters/smaato/image.go b/adapters/smaato/image.go new file mode 100644 index 00000000000..582206ccb0c --- /dev/null +++ b/adapters/smaato/image.go @@ -0,0 +1,53 @@ +package smaato + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +type imageAd struct { + Image image `json:"image"` +} +type image struct { + Img img `json:"img"` + Impressiontrackers []string `json:"impressiontrackers"` + Clicktrackers []string `json:"clicktrackers"` +} +type img struct { + URL string `json:"url"` + W int `json:"w"` + H int `json:"h"` + Ctaurl string `json:"ctaurl"` +} + +func extractAdmImage(adapterResponseAdm string) (string, error) { + var imgMarkup string + var err error + + var imageAd imageAd + err = json.Unmarshal([]byte(adapterResponseAdm), &imageAd) + var image = imageAd.Image + + if err == nil { + var clickEvent strings.Builder + var impressionTracker strings.Builder + + for _, clicktracker := range image.Clicktrackers { + clickEvent.WriteString("fetch(decodeURIComponent('" + url.QueryEscape(clicktracker) + "'.replace(/\\+/g, ' ')), " + + "{cache: 'no-cache'});") + } + + for _, impression := range image.Impressiontrackers { + + impressionTracker.WriteString(fmt.Sprintf(``, impression)) + } + + imgMarkup = fmt.Sprintf(`
%s
`, + &clickEvent, url.QueryEscape(image.Img.Ctaurl), image. + Img.URL, image.Img.W, image.Img. + H, &impressionTracker) + } + return imgMarkup, err +} diff --git a/adapters/smaato/image_test.go b/adapters/smaato/image_test.go new file mode 100644 index 00000000000..5f39c857201 --- /dev/null +++ b/adapters/smaato/image_test.go @@ -0,0 +1,44 @@ +package smaato + +import ( + "testing" +) + +func TestRenderAdMarkup(t *testing.T) { + type args struct { + adType adMarkupType + adapterResponseAdm string + } + expectedResult := `
` + + `` + + `` + + `
` + + tests := []struct { + testName string + args args + result string + }{ + {"imageTest", args{"Img", + "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\"," + + "\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"}," + + "\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"]," + + "\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}"}, + expectedResult, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + got, err := renderAdMarkup(tt.args.adType, tt.args.adapterResponseAdm) + if err != nil { + t.Errorf("error rendering ad markup: %v", err) + } + if got != tt.result { + t.Errorf("renderAdMarkup() got = %v, result %v", got, tt.result) + } + }) + } +} diff --git a/adapters/smaato/params_test.go b/adapters/smaato/params_test.go new file mode 100644 index 00000000000..6c71cbe75c6 --- /dev/null +++ b/adapters/smaato/params_test.go @@ -0,0 +1,65 @@ +package smaato + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// This file intends to test static/bidder-params/smaato.json + +// These also validate the format of the external API: request.imp[i].bidRequestExt.smaato + +// TestValidParams makes sure that the Smaato schema accepts all imp.bidRequestExt fields which Smaato supports. +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.BidderSmaato, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected smaato params: %s \n Error: %s", validParam, err) + } + } +} + +// TestInvalidParams makes sure that the Smaato schema rejects all the imp.bidRequestExt fields which are not 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.BidderSmaato, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"publisherId":"test-id-1234-smaato","adspaceId": "1123581321"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"publisherId":"test-id-1234-smaato"}`, + `{"adspaceId": "1123581321"}`, + `{"publisherId":false}`, + `{"adspaceId":false}`, + `{"publisherId":0,"adspaceId": 1123581321}`, + `{"publisherId":false,"adspaceId": true}`, + `{"instl": 0}`, + `{"secure": 0}`, + `{"adspaceId": "1123581321","instl": 0,"secure": 0}`, + `{"instl": 0,"secure": 0}`, + `{"publisherId":"test-id-1234-smaato","instl": 0,"secure": 0}`, +} diff --git a/adapters/smaato/richmedia.go b/adapters/smaato/richmedia.go new file mode 100644 index 00000000000..1c94a3555c1 --- /dev/null +++ b/adapters/smaato/richmedia.go @@ -0,0 +1,52 @@ +package smaato + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +type richMediaAd struct { + RichMedia richmedia `json:"richmedia"` +} +type mediadata struct { + Content string `json:"content"` + W int `json:"w"` + H int `json:"h"` +} + +type richmedia struct { + MediaData mediadata `json:"mediadata"` + Impressiontrackers []string `json:"impressiontrackers"` + Clicktrackers []string `json:"clicktrackers"` +} + +func extractAdmRichMedia(adapterResponseAdm string) (string, error) { + var richMediaMarkup string + var err error + + var richMediaAd richMediaAd + err = json.Unmarshal([]byte(adapterResponseAdm), &richMediaAd) + var richMedia = richMediaAd.RichMedia + + if err == nil { + var clickEvent strings.Builder + var impressionTracker strings.Builder + + for _, clicktracker := range richMedia.Clicktrackers { + clickEvent.WriteString("fetch(decodeURIComponent('" + url.QueryEscape(clicktracker) + "'), " + + "{cache: 'no-cache'});") + } + for _, impression := range richMedia.Impressiontrackers { + + impressionTracker.WriteString(fmt.Sprintf(``, impression)) + } + + richMediaMarkup = fmt.Sprintf(`
%s%s
`, + &clickEvent, + richMedia.MediaData.Content, + &impressionTracker) + } + return richMediaMarkup, err +} diff --git a/adapters/smaato/richmedia_test.go b/adapters/smaato/richmedia_test.go new file mode 100644 index 00000000000..20fa1ba353c --- /dev/null +++ b/adapters/smaato/richmedia_test.go @@ -0,0 +1,39 @@ +package smaato + +import ( + "testing" +) + +func TestExtractAdmRichMedia(t *testing.T) { + type args struct { + adType adMarkupType + adapterResponseAdm string + } + expectedResult := `
hello
` + + `
` + tests := []struct { + testName string + args args + result string + }{ + {"richmediaTest", args{"Richmedia", "{\"richmedia\":{\"mediadata\":{\"content\":\"
hello
\"," + + "" + "\"w\":350," + + "\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"]," + + "\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}"}, + expectedResult, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + got, err := renderAdMarkup(tt.args.adType, tt.args.adapterResponseAdm) + if err != nil { + t.Errorf("error rendering ad markup: %v", err) + } + if got != tt.result { + t.Errorf("renderAdMarkup() got = %v, result %v", got, tt.result) + } + }) + } +} diff --git a/adapters/smaato/smaato.go b/adapters/smaato/smaato.go new file mode 100644 index 00000000000..06678d77a61 --- /dev/null +++ b/adapters/smaato/smaato.go @@ -0,0 +1,276 @@ +package smaato + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/buger/jsonparser" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +const clientVersion = "prebid_server_0.1" + +type adMarkupType string + +const ( + smtAdTypeImg adMarkupType = "Img" + smtAdTypeRichmedia adMarkupType = "Richmedia" +) + +// SmaatoAdapter describes a Smaato prebid server adapter. +type SmaatoAdapter struct { + URI string +} + +//userExt defines User.Ext object for Smaato +type userExt struct { + Data userExtData `json:"data"` +} + +type userExtData struct { + Keywords string `json:"keywords"` + Gender string `json:"gender"` + Yob int64 `json:"yob"` +} + +//userExt defines Site.Ext object for Smaato +type siteExt struct { + Data siteExtData `json:"data"` +} + +type siteExtData struct { + Keywords string `json:"keywords"` +} + +// NewSmaatoBidder creates a Smaato bid adapter. +func NewSmaatoBidder(uri string) *SmaatoAdapter { + return &SmaatoAdapter{ + URI: uri, + } +} + +// MakeRequests makes the HTTP requests which should be made to fetch bids. +func (a *SmaatoAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + errs := make([]error, 0, len(request.Imp)) + if len(request.Imp) == 0 { + errs = append(errs, &errortypes.BadInput{Message: "no impressions in bid request"}) + return nil, errs + } + + // Use bidRequestExt of first imp to retrieve params which are valid for all imps, e.g. publisherId + publisherId, err := jsonparser.GetString(request.Imp[0].Ext, "bidder", "publisherId") + if err != nil { + errs = append(errs, err) + return nil, errs + } + + for i := 0; i < len(request.Imp); i++ { + err := parseImpressionObject(&request.Imp[i]) + // If the parsing is failed, remove imp and add the error. + if err != nil { + errs = append(errs, err) + request.Imp = append(request.Imp[:i], request.Imp[i+1:]...) + i-- + } + } + if request.Site != nil { + siteCopy := *request.Site + siteCopy.Publisher = &openrtb.Publisher{ID: publisherId} + + if request.Site.Ext != nil { + var siteExt siteExt + err := json.Unmarshal([]byte(request.Site.Ext), &siteExt) + if err != nil { + errs = append(errs, err) + return nil, errs + } + siteCopy.Keywords = siteExt.Data.Keywords + siteCopy.Ext = nil + } + request.Site = &siteCopy + } + + if request.User != nil && request.User.Ext != nil { + var userExt userExt + var userExtRaw map[string]json.RawMessage + + rawExtErr := json.Unmarshal(request.User.Ext, &userExtRaw) + if rawExtErr != nil { + errs = append(errs, rawExtErr) + return nil, errs + } + + userExtErr := json.Unmarshal([]byte(request.User.Ext), &userExt) + if userExtErr != nil { + errs = append(errs, userExtErr) + return nil, errs + } + + userCopy := *request.User + extractUserExtAttributes(userExt, &userCopy) + delete(userExtRaw, "data") + userCopy.Ext, err = json.Marshal(userExtRaw) + if err != nil { + errs = append(errs, err) + return nil, errs + } + request.User = &userCopy + } + + // Setting ext client info + type bidRequestExt struct { + Client string `json:"client"` + } + request.Ext, err = json.Marshal(bidRequestExt{Client: clientVersion}) + if err != nil { + errs = append(errs, err) + return nil, errs + } + reqJSON, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + uri := a.URI + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: uri, + Body: reqJSON, + Headers: headers, + }}, errs +} + +// MakeBids unpacks the server's response into Bids. +func (a *SmaatoAdapter) 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{fmt.Errorf("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(5) + + for _, sb := range bidResp.SeatBid { + for i := 0; i < len(sb.Bid); i++ { + bid := sb.Bid[i] + + var markupError error + bid.AdM, markupError = renderAdMarkup(getAdMarkupType(response, bid.AdM), bid.AdM) + if markupError != nil { + fmt.Println(markupError) + continue // no bid when broken ad markup + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: openrtb_ext.BidTypeBanner, + }) + } + } + return bidResponse, nil +} + +func renderAdMarkup(adMarkupType adMarkupType, adMarkup string) (string, error) { + var markupError error + var adm string + switch adMarkupType { + case smtAdTypeImg: + adm, markupError = extractAdmImage(adMarkup) + case smtAdTypeRichmedia: + adm, markupError = extractAdmRichMedia(adMarkup) + default: + return "", fmt.Errorf("Unknown markup type %s", adMarkupType) + } + return adm, markupError +} + +func getAdMarkupType(response *adapters.ResponseData, adMarkup string) adMarkupType { + if admType := adMarkupType(response.Headers.Get("X-SMT-ADTYPE")); admType != "" { + return admType + } + if strings.HasPrefix(adMarkup, `{"image":`) { + return smtAdTypeImg + } + if strings.HasPrefix(adMarkup, `{"richmedia":`) { + return smtAdTypeRichmedia + } + return "" +} + +func assignBannerSize(banner *openrtb.Banner) (*openrtb.Banner, error) { + if banner.W != nil && banner.H != nil { + return banner, nil + } + if len(banner.Format) == 0 { + return banner, fmt.Errorf("No sizes provided for Banner %v", banner.Format) + } + bannerCopy := *banner + bannerCopy.W = new(uint64) + *bannerCopy.W = banner.Format[0].W + bannerCopy.H = new(uint64) + *bannerCopy.H = banner.Format[0].H + + return &bannerCopy, nil +} + +// parseImpressionObject parse the imp to get it ready to send to smaato +func parseImpressionObject(imp *openrtb.Imp) error { + adSpaceID, err := jsonparser.GetString(imp.Ext, "bidder", "adspaceId") + if err != nil { + return err + } + + // SMAATO supports banner impressions. + if imp.Banner != nil { + bannerCopy, err := assignBannerSize(imp.Banner) + if err != nil { + return err + } + imp.Banner = bannerCopy + imp.TagID = adSpaceID + imp.Ext = nil + return nil + } + return fmt.Errorf("invalid MediaType. SMAATO only supports Banner. Ignoring ImpID=%s", imp.ID) +} + +func extractUserExtAttributes(userExt userExt, userCopy *openrtb.User) { + gender := userExt.Data.Gender + if gender != "" { + userCopy.Gender = gender + } + + yob := userExt.Data.Yob + if yob != 0 { + userCopy.Yob = yob + } + + keywords := userExt.Data.Keywords + if keywords != "" { + userCopy.Keywords = keywords + } +} diff --git a/adapters/smaato/smaato_test.go b/adapters/smaato/smaato_test.go new file mode 100644 index 00000000000..cf76d58de2c --- /dev/null +++ b/adapters/smaato/smaato_test.go @@ -0,0 +1,11 @@ +package smaato + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "smaatotest", NewSmaatoBidder("https://prebid/bidder")) +} diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json new file mode 100644 index 00000000000..7b662e8813a --- /dev/null +++ b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json @@ -0,0 +1,194 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "ext": { + "data": { + "keywords": "power tools", + "search": "drill", + "content": { + "userrating": 4 + } + } + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "ext": { + "consent": "gdprConsentString", + "data": { + "keywords": "a,b", + "gender": "M", + "yob": 1984, + "geo": { + "country": "ca" + } + } + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "banner": { + "h": 50, + "w": 320, + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + } + } + ], + "user": { + "gender": "M", + "keywords": "a,b", + "yob": 1984, + "ext": { + "consent": "gdprConsentString" + } + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.1" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "{\"richmedia\":{\"mediadata\":{\"content\":\"
hello
\", \"w\":350,\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50 + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "
hello
\"\"\"\"
", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner.json b/adapters/smaato/smaatotest/exemplary/simple-banner.json new file mode 100644 index 00000000000..a50fd9289e3 --- /dev/null +++ b/adapters/smaato/smaatotest/exemplary/simple-banner.json @@ -0,0 +1,190 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "ext": { + "data": { + "keywords": "power tools", + "search": "drill", + "content": { + "userrating": 4 + } + } + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "ext": { + "consent": "gdprConsentString", + "data": { + "keywords": "a,b", + "gender": "M", + "yob": 1984, + "geo": { + "country": "ca" + } + } + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "banner": { + "h": 50, + "w": 320, + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + } + } + ], + "user": { + "ext": { + "consent": "gdprConsentString" + }, + "gender": "M", + "keywords": "a,b", + "yob": 1984 + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.1" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50 + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "
\"\"\"\"
", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/params/banner.json b/adapters/smaato/smaatotest/params/banner.json new file mode 100644 index 00000000000..a84c44d4d8e --- /dev/null +++ b/adapters/smaato/smaatotest/params/banner.json @@ -0,0 +1,4 @@ +{ + "publisherId": "1100042525", + "adspaceId": "130563103" +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/bad-adm-response.json b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json new file mode 100644 index 00000000000..6d4990e9ea4 --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json @@ -0,0 +1,166 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "ext": { + "data": { + "keywords": "power tools", + "search": "drill", + "content": { + "userrating": 4 + } + } + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "ext": { + "consent": "gdprConsentString", + "data": { + "keywords": "a,b", + "gender": "M", + "yob": 1984, + "geo": { + "country": "ca" + } + } + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "banner": { + "h": 50, + "w": 320, + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + } + } + ], + "user": { + "ext": { + "consent": "gdprConsentString" + }, + "gender": "M", + "keywords": "a,b", + "yob": 1984 + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + }, + "site": { + "publisher": { + "id": "1100042525" + }, + "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder", + "keywords": "power tools" + }, + "ext": { + "client": "prebid_server_0.1" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "{\"badmedia\":{\"mediadata\":{\"content\":\"
hello
\", \"w\":350,\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50 + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/bad-ext-req.json b/adapters/smaato/smaatotest/supplemental/bad-ext-req.json new file mode 100644 index 00000000000..0c970fc5bad --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/bad-ext-req.json @@ -0,0 +1,54 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "page": "prebid.org", + "publisher": { + "id": "1" + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "ext": { + "consent": "gdprConsentString" + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/bad-imp-banner-format-req.json b/adapters/smaato/smaatotest/supplemental/bad-imp-banner-format-req.json new file mode 100644 index 00000000000..b9560f0f9ca --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/bad-imp-banner-format-req.json @@ -0,0 +1,61 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + } + } + } + ], + "site": { + "page": "prebid.org", + "publisher": { + "id": "1" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [], + "site": { + "page": "prebid.org", + "publisher": { + "id": "1100042525" + } + }, + "ext": { + "client": "prebid_server_0.1" + } + } + } + } + ], + "expectedMakeRequestsErrors": [ + { + "value": "No sizes provided for Banner []", + "comparison": "literal" + } + ], + "expectedMakeBidsErrors": [ + { + "value": "unexpected status code: 0. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/bad-user-ext-data-req.json b/adapters/smaato/smaatotest/supplemental/bad-user-ext-data-req.json new file mode 100644 index 00000000000..9e65fce1c3e --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/bad-user-ext-data-req.json @@ -0,0 +1,67 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "page": "prebid.org", + "publisher": { + "id": "1" + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "gender": "M", + "ext": { + "data": { + "keywords":"a,b", + "gender": "M", + "yob": "", + "geo": { + "country": "ca" + } + }, + "consent":"yes" + } + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal string into Go struct field userExtData.data.yob of type int64", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/bad-user-ext-req.json b/adapters/smaato/smaatotest/supplemental/bad-user-ext-req.json new file mode 100644 index 00000000000..7f05b2dff14 --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/bad-user-ext-req.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "page": "prebid.org", + "publisher": { + "id": "1" + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "user": { + "gender": "M", + "ext": 99 + }, + "regs": { + "coppa": 1, + "ext": { + "gdpr": 1, + "us_privacy": "uspConsentString" + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal number into Go value of type map[string]json.RawMessage", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/no-consent-info.json b/adapters/smaato/smaatotest/supplemental/no-consent-info.json new file mode 100644 index 00000000000..9e0ccfdcdde --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/no-consent-info.json @@ -0,0 +1,137 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "page": "prebid.org", + "publisher": { + "id": "1" + } + }, + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "banner": { + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "publisherId": "1100042525", + "adspaceId": "130563103" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "uri": "https://prebid/bidder", + "body": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "imp": [ + { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "tagid": "130563103", + "banner": { + "h": 50, + "w": 320, + "format": [ + { + "w": 320, + "h": 50 + }, + { + "w": 320, + "h": 250 + } + ] + } + } + ], + "site": { + "page": "prebid.org", + "publisher": { + "id": "1100042525" + } + }, + "ext": { + "client": "prebid_server_0.1" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "5ebea288-f13a-4754-be6d-4ade66c68877", + "seatbid": [ + { + "seat": "CM6523", + "bid": [ + { + "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50 + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "
\"\"\"\"
", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png", + "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D", + "price": 0.01, + "w": 350, + "h": 50 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/no-imp-req.json b/adapters/smaato/smaatotest/supplemental/no-imp-req.json new file mode 100644 index 00000000000..bfaf51e6ea8 --- /dev/null +++ b/adapters/smaato/smaatotest/supplemental/no-imp-req.json @@ -0,0 +1,17 @@ +{ + "mockBidRequest": { + "id": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "site": { + "page": "prebid.org", + "publisher": { + "id": "1" + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "no impressions in bid request", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index 67689d1ab1a..fb0607646ee 100755 --- a/config/config.go +++ b/config/config.go @@ -845,6 +845,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.rubicon.disabled", true) 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.smaato.endpoint", "https://prebid.ad.smaato.net/oapi/prebid") v.SetDefault("adapters.smartadserver.endpoint", "https://ssb.smartadserver.com") 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") diff --git a/docs/bidders/smaato.md b/docs/bidders/smaato.md new file mode 100644 index 00000000000..881f8f2ab54 --- /dev/null +++ b/docs/bidders/smaato.md @@ -0,0 +1,42 @@ + +# Smaato Bidder + +``` +Module Name: Smaato Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@smaato.com +``` + +### Description + +Please contact Smaato Support or prebid@smaato.com to get set up with a publisherId and adspaceId. + +### Test Parameters: + +Following example includes sample `imp` object with publisherId and adSlot which can be used to test Smaato Adapter + +``` +"imp":[ + { + "id":“1C86242D-9535-47D6-9576-7B1FE87F282C, + "banner":{ + "format":[ + { + "w":300, + "h":50 + }, + { + "w":300, + "h":250 + } + ] + }, + "ext":{ + "smaato":{ + "publisherId":"100042525", + "adspaceId":"130563103" + } + } + } + ] +``` diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 2ecddb83cfc..207a7a9b9e9 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -64,6 +64,7 @@ import ( "github.com/prebid/prebid-server/adapters/rtbhouse" "github.com/prebid/prebid-server/adapters/rubicon" "github.com/prebid/prebid-server/adapters/sharethrough" + "github.com/prebid/prebid-server/adapters/smaato" "github.com/prebid/prebid-server/adapters/smartadserver" "github.com/prebid/prebid-server/adapters/smartrtb" "github.com/prebid/prebid-server/adapters/somoaudience" @@ -154,6 +155,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.BidderSmaato: smaato.NewSmaatoBidder(cfg.Adapters[string(openrtb_ext.BidderSmaato)].Endpoint), openrtb_ext.BidderSmartadserver: smartadserver.NewSmartadserverBidder(cfg.Adapters[string(openrtb_ext.BidderSmartadserver)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 62fb9750616..ee0f40903e0 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -80,6 +80,7 @@ const ( BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSharethrough BidderName = "sharethrough" + BidderSmaato BidderName = "smaato" BidderSmartadserver BidderName = "smartadserver" BidderSmartRTB BidderName = "smartrtb" BidderSomoaudience BidderName = "somoaudience" @@ -162,6 +163,7 @@ var BidderMap = map[string]BidderName{ "rtbhouse": BidderRTBHouse, "rubicon": BidderRubicon, "sharethrough": BidderSharethrough, + "smaato": BidderSmaato, "smartadserver": BidderSmartadserver, "smartrtb": BidderSmartRTB, "somoaudience": BidderSomoaudience, diff --git a/openrtb_ext/imp_smaato.go b/openrtb_ext/imp_smaato.go new file mode 100644 index 00000000000..10de97fb017 --- /dev/null +++ b/openrtb_ext/imp_smaato.go @@ -0,0 +1,9 @@ +package openrtb_ext + +// ExtImpSmaato defines the contract for bidrequest.imp[i].ext.smaato +// PublisherId and AdSpaceId are mandatory parameters, others are optional parameters +// AdSpaceId is identifier for specific ad placement or ad tag +type ExtImpSmaato struct { + PublisherID string `json:"publisherId"` + AdSpaceID string `json:"adspaceId"` +} diff --git a/static/bidder-info/smaato.yaml b/static/bidder-info/smaato.yaml new file mode 100644 index 00000000000..662603febdb --- /dev/null +++ b/static/bidder-info/smaato.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "prebid@smaato.com" +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner \ No newline at end of file diff --git a/static/bidder-params/smaato.json b/static/bidder-params/smaato.json new file mode 100644 index 00000000000..aa91c4bacc5 --- /dev/null +++ b/static/bidder-params/smaato.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smaato Adapter Params", + "description": "A schema which validates params accepted by the Smaato adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "A unique identifier for this impression within the context of the bid request" + }, + "adspaceId": { + "type": "string", + "description": "Identifier for specific ad placement is SOMA `adspaceId`" + } + }, + "required": ["publisherId","adspaceId"] +} \ No newline at end of file diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 32ab2e730eb..22b215c3132 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -93,6 +93,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderMobileFuse: true, openrtb_ext.BidderOrbidder: true, openrtb_ext.BidderPubnative: true, + openrtb_ext.BidderSmaato: true, openrtb_ext.BidderTappx: true, openrtb_ext.BidderYeahmobi: true, } From 7615d472149c9dd7ffd06360e54792cd54a0d51f Mon Sep 17 00:00:00 2001 From: Adprime <64427228+Adprime@users.noreply.github.com> Date: Thu, 6 Aug 2020 19:43:30 +0300 Subject: [PATCH 157/381] New Adprime adapter (#1418) Co-authored-by: Aiholkin --- adapters/adprime/adprime.go | 142 ++++++++++++++++++ adapters/adprime/adprime_test.go | 12 ++ .../adprimetest/exemplary/simple-banner.json | 134 +++++++++++++++++ .../adprimetest/exemplary/simple-video.json | 119 +++++++++++++++ .../exemplary/simple-web-banner.json | 133 ++++++++++++++++ .../adprime/adprimetest/params/banner.json | 3 + .../adprimetest/params/race/banner.json | 3 + .../adprimetest/params/race/video.json | 3 + .../adprime/adprimetest/params/video.json | 3 + .../adprimetest/supplemental/bad-imp-ext.json | 42 ++++++ .../supplemental/bad_response.json | 85 +++++++++++ .../supplemental/no-imp-ext-1.json | 39 +++++ .../supplemental/no-imp-ext-2.json | 39 +++++ .../adprimetest/supplemental/status-204.json | 79 ++++++++++ .../adprimetest/supplemental/status-404.json | 85 +++++++++++ adapters/adprime/params_test.go | 46 ++++++ config/config.go | 1 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adprime.go | 6 + static/bidder-info/adprime.yaml | 11 ++ static/bidder-params/adprime.json | 14 ++ usersync/usersyncers/syncer_test.go | 1 + 23 files changed, 1004 insertions(+) create mode 100644 adapters/adprime/adprime.go create mode 100644 adapters/adprime/adprime_test.go create mode 100644 adapters/adprime/adprimetest/exemplary/simple-banner.json create mode 100644 adapters/adprime/adprimetest/exemplary/simple-video.json create mode 100644 adapters/adprime/adprimetest/exemplary/simple-web-banner.json create mode 100644 adapters/adprime/adprimetest/params/banner.json create mode 100644 adapters/adprime/adprimetest/params/race/banner.json create mode 100644 adapters/adprime/adprimetest/params/race/video.json create mode 100644 adapters/adprime/adprimetest/params/video.json create mode 100644 adapters/adprime/adprimetest/supplemental/bad-imp-ext.json create mode 100644 adapters/adprime/adprimetest/supplemental/bad_response.json create mode 100644 adapters/adprime/adprimetest/supplemental/no-imp-ext-1.json create mode 100644 adapters/adprime/adprimetest/supplemental/no-imp-ext-2.json create mode 100644 adapters/adprime/adprimetest/supplemental/status-204.json create mode 100644 adapters/adprime/adprimetest/supplemental/status-404.json create mode 100644 adapters/adprime/params_test.go create mode 100644 openrtb_ext/imp_adprime.go create mode 100644 static/bidder-info/adprime.yaml create mode 100644 static/bidder-params/adprime.json diff --git a/adapters/adprime/adprime.go b/adapters/adprime/adprime.go new file mode 100644 index 00000000000..007d3c86570 --- /dev/null +++ b/adapters/adprime/adprime.go @@ -0,0 +1,142 @@ +package adprime + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/buger/jsonparser" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +// AdprimeAdapter struct +type AdprimeAdapter struct { + URI string +} + +// NewAdprimeBidder Initializes the Bidder +func NewAdprimeBidder(endpoint string) *AdprimeAdapter { + return &AdprimeAdapter{ + URI: endpoint, + } +} + +type adprimeParams struct { + TagID string `json:"TagID"` +} + +// MakeRequests create bid request for adprime demand +func (a *AdprimeAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var err error + var tagID string + + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb.Imp{imp} + + tagID, err = jsonparser.GetString(reqCopy.Imp[0].Ext, "bidder", "TagID") + if err != nil { + errs = append(errs, err) + continue + } + + reqCopy.Imp[0].TagID = tagID + + adapterReq, errors := a.makeRequest(&reqCopy) + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + errs = append(errs, errors...) + } + return adapterRequests, errs +} + +func (a *AdprimeAdapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, []error) { + + var errs []error + + reqJSON, err := json.Marshal(request) + + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.URI, + Body: reqJSON, + Headers: headers, + }, errs +} + +// MakeBids makes the bids +func (a *AdprimeAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode == http.StatusNotFound { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Page not found: %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 { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, errs +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) (openrtb_ext.BidType, error) { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impID { + if imp.Banner == nil && imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } + return mediaType, nil + } + } + + // This shouldnt happen. Lets handle it just incase by returning an error. + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression \"%s\" ", impID), + } +} diff --git a/adapters/adprime/adprime_test.go b/adapters/adprime/adprime_test.go new file mode 100644 index 00000000000..2d3ee9b2b3f --- /dev/null +++ b/adapters/adprime/adprime_test.go @@ -0,0 +1,12 @@ +package adprime + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adprimeAdapter := NewAdprimeBidder("http://delta.adprime.com/?c=o&m=ortb") + adapterstest.RunJSONBidderTest(t, "adprimetest", adprimeAdapter) +} diff --git a/adapters/adprime/adprimetest/exemplary/simple-banner.json b/adapters/adprime/adprimetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..076175c6274 --- /dev/null +++ b/adapters/adprime/adprimetest/exemplary/simple-banner.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } +}, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://delta.adprime.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adprime" + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/adprime/adprimetest/exemplary/simple-video.json b/adapters/adprime/adprimetest/exemplary/simple-video.json new file mode 100644 index 00000000000..3e61c4dddd1 --- /dev/null +++ b/adapters/adprime/adprimetest/exemplary/simple-video.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "TagID": "288" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://delta.adprime.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "tagid": "288", + "ext": { + "bidder": { + "TagID": "288" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "adprime" + } + ], + "cur": "USD" + } + } + } + ], + + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adprime/adprimetest/exemplary/simple-web-banner.json b/adapters/adprime/adprimetest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..a955854fb31 --- /dev/null +++ b/adapters/adprime/adprimetest/exemplary/simple-web-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://delta.adprime.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adprime" + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/adprime/adprimetest/params/banner.json b/adapters/adprime/adprimetest/params/banner.json new file mode 100644 index 00000000000..e3f4cb7605a --- /dev/null +++ b/adapters/adprime/adprimetest/params/banner.json @@ -0,0 +1,3 @@ +{ + "TagID": "1" +} \ No newline at end of file diff --git a/adapters/adprime/adprimetest/params/race/banner.json b/adapters/adprime/adprimetest/params/race/banner.json new file mode 100644 index 00000000000..e3f4cb7605a --- /dev/null +++ b/adapters/adprime/adprimetest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "TagID": "1" +} \ No newline at end of file diff --git a/adapters/adprime/adprimetest/params/race/video.json b/adapters/adprime/adprimetest/params/race/video.json new file mode 100644 index 00000000000..c8d14757903 --- /dev/null +++ b/adapters/adprime/adprimetest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "TagID": "288" +} \ No newline at end of file diff --git a/adapters/adprime/adprimetest/params/video.json b/adapters/adprime/adprimetest/params/video.json new file mode 100644 index 00000000000..c8d14757903 --- /dev/null +++ b/adapters/adprime/adprimetest/params/video.json @@ -0,0 +1,3 @@ +{ + "TagID": "288" +} \ No newline at end of file diff --git a/adapters/adprime/adprimetest/supplemental/bad-imp-ext.json b/adapters/adprime/adprimetest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..a95c56e8426 --- /dev/null +++ b/adapters/adprime/adprimetest/supplemental/bad-imp-ext.json @@ -0,0 +1,42 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "adprime": { + "TagID": "1" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } +}, +"expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } +] +} diff --git a/adapters/adprime/adprimetest/supplemental/bad_response.json b/adapters/adprime/adprimetest/supplemental/bad_response.json new file mode 100644 index 00000000000..329e9c7269f --- /dev/null +++ b/adapters/adprime/adprimetest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://delta.adprime.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/adprime/adprimetest/supplemental/no-imp-ext-1.json b/adapters/adprime/adprimetest/supplemental/no-imp-ext-1.json new file mode 100644 index 00000000000..1e38dbe4541 --- /dev/null +++ b/adapters/adprime/adprimetest/supplemental/no-imp-ext-1.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": "" + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/adprime/adprimetest/supplemental/no-imp-ext-2.json b/adapters/adprime/adprimetest/supplemental/no-imp-ext-2.json new file mode 100644 index 00000000000..f9759fae8ff --- /dev/null +++ b/adapters/adprime/adprimetest/supplemental/no-imp-ext-2.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": {} + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/adprime/adprimetest/supplemental/status-204.json b/adapters/adprime/adprimetest/supplemental/status-204.json new file mode 100644 index 00000000000..44ee59d4d28 --- /dev/null +++ b/adapters/adprime/adprimetest/supplemental/status-204.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://delta.adprime.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }] +} diff --git a/adapters/adprime/adprimetest/supplemental/status-404.json b/adapters/adprime/adprimetest/supplemental/status-404.json new file mode 100644 index 00000000000..c2b303f0cb4 --- /dev/null +++ b/adapters/adprime/adprimetest/supplemental/status-404.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://delta.adprime.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Page not found: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/adprime/params_test.go b/adapters/adprime/params_test.go new file mode 100644 index 00000000000..05adad5c4ff --- /dev/null +++ b/adapters/adprime/params_test.go @@ -0,0 +1,46 @@ +package adprime + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// TestValidParams makes sure that the adprime 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.BidderAdprime, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected adprime params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the adprime 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.BidderAdprime, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"TagID": "1"}`, +} + +var invalidParams = []string{ + `{"id": "123"}`, + `{"tagid": "123"}`, + `{"TagID": 16}`, +} diff --git a/config/config.go b/config/config.go index fb0607646ee..9663b021b5b 100755 --- a/config/config.go +++ b/config/config.go @@ -797,6 +797,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.adprime.endpoint", "http://delta.adprime.com/?c=o&m=ortb") 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}}") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 207a7a9b9e9..d056de664b7 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -19,6 +19,7 @@ import ( "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adoppler" "github.com/prebid/prebid-server/adapters/adpone" + "github.com/prebid/prebid-server/adapters/adprime" "github.com/prebid/prebid-server/adapters/adtarget" "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" @@ -106,6 +107,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderAdprime: adprime.NewAdprimeBidder(cfg.Adapters[string(openrtb_ext.BidderAdprime)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index ee0f40903e0..761f53d441e 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -34,6 +34,7 @@ const ( BidderAdman BidderName = "adman" BidderAdmixer BidderName = "admixer" BidderAdOcean BidderName = "adocean" + BidderAdprime BidderName = "adprime" BidderAdtarget BidderName = "adtarget" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" @@ -116,6 +117,7 @@ var BidderMap = map[string]BidderName{ "adman": BidderAdman, "admixer": BidderAdmixer, "adocean": BidderAdOcean, + "adprime": BidderAdprime, "adpone": BidderAdpone, "adtarget": BidderAdtarget, "adtelligent": BidderAdtelligent, diff --git a/openrtb_ext/imp_adprime.go b/openrtb_ext/imp_adprime.go new file mode 100644 index 00000000000..a089b818b56 --- /dev/null +++ b/openrtb_ext/imp_adprime.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpAdprime defines adprime specifiec param +type ExtImpAdprime struct { + TagID string `json:"TagID"` +} diff --git a/static/bidder-info/adprime.yaml b/static/bidder-info/adprime.yaml new file mode 100644 index 00000000000..9759ed63be7 --- /dev/null +++ b/static/bidder-info/adprime.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "rafal@adprime.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-params/adprime.json b/static/bidder-params/adprime.json new file mode 100644 index 00000000000..d527056597d --- /dev/null +++ b/static/bidder-params/adprime.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adprime Adapter Params", + "description": "A schema which validates params accepted by the Adprime adapter", + + "type": "object", + "properties": { + "TagID": { + "type": "string", + "description": "An ID which identifies the adprime ad tag" + } + }, + "required" : [ "TagID" ] + } \ No newline at end of file diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 22b215c3132..9197ed9507d 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -96,6 +96,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderSmaato: true, openrtb_ext.BidderTappx: true, openrtb_ext.BidderYeahmobi: true, + openrtb_ext.BidderAdprime: true, } for bidder, config := range cfg.Adapters { From a7aaa97af15618f1b4cb7de3cb38866213c41028 Mon Sep 17 00:00:00 2001 From: guscarreon Date: Thu, 6 Aug 2020 14:21:01 -0400 Subject: [PATCH 158/381] Separate "debug" behavior from "billable" behavior (#1387) --- exchange/bidder.go | 2 +- exchange/bidder_test.go | 48 ----- exchange/exchange.go | 99 ++++----- exchange/exchange_test.go | 190 ++++++++++++++++-- exchange/utils.go | 98 ++++++--- exchange/utils_test.go | 412 ++++++++++++++++++++++++++++++++------ openrtb_ext/request.go | 1 + 7 files changed, 646 insertions(+), 204 deletions(-) diff --git a/exchange/bidder.go b/exchange/bidder.go index 7c39b72b348..decad8ccf2f 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -150,7 +150,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.Bi for i := 0; i < len(reqData); i++ { httpInfo := <-responseChannel // If this is a test bid, capture debugging info from the requests. - if request.Test == 1 { + if debugInfo := ctx.Value(DebugContextKey); debugInfo != nil && debugInfo.(bool) { seatBid.httpCalls = append(seatBid.httpCalls, makeExt(httpInfo)) } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 1a27b72aa12..7ae96c09b93 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -938,54 +938,6 @@ func TestSuccessfulResponseLogging(t *testing.T) { } } -// TestServerCallDebugging makes sure that we log the server calls made by the Bidder on test bids. -func TestServerCallDebugging(t *testing.T) { - respBody := "{\"bid\":false}" - respStatus := 200 - server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) - defer server.Close() - - reqBody := "{\"key\":\"val\"}" - reqUrl := server.URL - bidderImpl := &goodSingleBidder{ - httpRequest: &adapters.RequestData{ - Method: "POST", - Uri: reqUrl, - Body: []byte(reqBody), - Headers: http.Header{}, - }, - } - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) - currencyConverter := currencies.NewRateConverterDefault() - - bids, _ := bidder.requestBid( - context.Background(), - &openrtb.BidRequest{ - Test: 1, - }, - "test", - 1.0, - currencyConverter.Rates(), - &adapters.ExtraRequestInfo{}, - ) - - if len(bids.httpCalls) != 1 { - t.Errorf("We should log the server call if this is a test bid. Got %d", len(bids.httpCalls)) - } - if bids.httpCalls[0].Uri != reqUrl { - t.Errorf("Wrong httpcalls URI. Expected %s, got %s", reqUrl, bids.httpCalls[0].Uri) - } - if bids.httpCalls[0].RequestBody != reqBody { - t.Errorf("Wrong httpcalls RequestBody. Expected %s, got %s", reqBody, bids.httpCalls[0].RequestBody) - } - if bids.httpCalls[0].ResponseBody != respBody { - t.Errorf("Wrong httpcalls ResponseBody. Expected %s, got %s", respBody, bids.httpCalls[0].ResponseBody) - } - if bids.httpCalls[0].Status != respStatus { - t.Errorf("Wrong httpcalls Status. Expected %d, got %d", respStatus, bids.httpCalls[0].Status) - } -} - func TestMobileNativeTypes(t *testing.T) { respBody := "{\"bid\":false}" respStatus := 200 diff --git a/exchange/exchange.go b/exchange/exchange.go index 5001e495440..ad591f57794 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -27,6 +27,10 @@ import ( "github.com/prebid/prebid-server/prebid_cache_client" ) +type ContextKey string + +const DebugContextKey = ContextKey("debugInfo") + // Exchange runs Auctions. Implementations must be threadsafe, and will be shared across many goroutines. type Exchange interface { // HoldAuction executes an OpenRTB v2.5 Auction. @@ -86,12 +90,25 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con } func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) { - // Snapshot of resolved bid request for debug if test request - resolvedRequest, err := buildResolvedRequest(bidRequest) + + requestExt, err := extractBidRequestExt(bidRequest) if err != nil { - glog.Errorf("Error marshalling bid request for debug: %v", err) + return nil, err + } + + shouldCacheBids, shouldCacheVAST := getExtCacheInfo(requestExt) + targData := getExtTargetData(requestExt, shouldCacheBids, shouldCacheVAST) + if targData != nil { + targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() + } + + debugInfo := getDebugInfo(bidRequest, requestExt) + if debugInfo { + ctx = e.makeDebugContext(ctx, debugInfo) } + bidAdjustmentFactors := getExtBidAdjustmentFactors(requestExt) + for _, impInRequest := range bidRequest.Imp { var impLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{ BannerImps: impInRequest.Banner != nil, @@ -104,46 +121,16 @@ 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, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) + cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) e.me.RecordRequestPrivacy(privacyLabels) // List of bidders we have requests for. liveAdapters := listBiddersWithRequests(cleanRequests) - // Process the request to check for targeting parameters. - var targData *targetData - shouldCacheBids := false - shouldCacheVAST := false - var bidAdjustmentFactors map[string]float64 - var requestExt openrtb_ext.ExtRequest - if len(bidRequest.Ext) > 0 { - err := json.Unmarshal(bidRequest.Ext, &requestExt) - if err != nil { - return nil, fmt.Errorf("Error decoding Request.ext : %s", err.Error()) - } - bidAdjustmentFactors = requestExt.Prebid.BidAdjustmentFactors - if requestExt.Prebid.Cache != nil { - shouldCacheBids = requestExt.Prebid.Cache.Bids != nil - shouldCacheVAST = requestExt.Prebid.Cache.VastXML != nil - } - - if requestExt.Prebid.Targeting != nil { - targData = &targetData{ - priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, - includeWinners: requestExt.Prebid.Targeting.IncludeWinners, - includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys, - includeCacheBids: shouldCacheBids, - includeCacheVast: shouldCacheVAST, - includeFormat: requestExt.Prebid.Targeting.IncludeFormat, - } - targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() - } - } - // If we need to cache bids, then it will take some time to call prebid cache. // We should reduce the amount of time the bidders have, to compensate. - auctionCtx, cancel := e.makeAuctionContext(ctx, shouldCacheBids) //Why no context for `shouldCacheVast`? + auctionCtx, cancel := e.makeAuctionContext(ctx, shouldCacheBids) defer cancel() // Get currency rates conversions for the auction @@ -180,7 +167,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque } if debugLog != nil && debugLog.Enabled { - bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, errs) + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errs) if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil { debugLog.Data.Response = string(bidRespExtBytes) } else { @@ -205,7 +192,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque } // Build the response - return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, bidResponseExt, errs) + return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, adapterExtra, auc, bidResponseExt, errs) } type DealTierInfo struct { @@ -284,6 +271,11 @@ func updateHbPbCatDur(bid *pbsOrtbBid, dealTierInfo *DealTierInfo, bidCategory m } } +func (e *exchange) makeDebugContext(ctx context.Context, debugInfo bool) (debugCtx context.Context) { + debugCtx = context.WithValue(ctx, DebugContextKey, debugInfo) + return +} + func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auctionCtx context.Context, cancel context.CancelFunc) { auctionCtx = ctx cancel = func() {} @@ -445,7 +437,7 @@ func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError { } // 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, bidResponseExt *openrtb_ext.ExtBidResponse, 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, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, errList []error) (*openrtb.BidResponse, error) { bidResponse := new(openrtb.BidResponse) bidResponse.ID = bidRequest.ID @@ -469,7 +461,12 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ bidResponse.SeatBid = seatBids if bidResponseExt == nil { - bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, errList) + contextDebugValue := ctx.Value(DebugContextKey) + var debugInfo bool + if contextDebugValue != nil { + debugInfo = contextDebugValue.(bool) + } + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errList) } buffer := &bytes.Buffer{} enc := json.NewEncoder(buffer) @@ -480,7 +477,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, []string, 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 { @@ -491,6 +488,8 @@ func applyCategoryMapping(ctx context.Context, requestExt openrtb_ext.ExtRequest dedupe := make(map[string]bidDedupe) + // applyCategoryMapping doesn't get called unless + // requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil brandCatExt := requestExt.Prebid.Targeting.IncludeBrandCategory //If ext.prebid.targeting.includebrandcategory is present in ext then competitive exclusion feature is on. @@ -656,24 +655,22 @@ func getPrimaryAdServer(adServerId int) (string, error) { } // Extract all the data from the SeatBids and build the ExtBidResponse -func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, req *openrtb.BidRequest, resolvedRequest json.RawMessage, errList []error) *openrtb_ext.ExtBidResponse { +func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, req *openrtb.BidRequest, debugInfo bool, errList []error) *openrtb_ext.ExtBidResponse { bidResponseExt := &openrtb_ext.ExtBidResponse{ Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError, len(adapterBids)), ResponseTimeMillis: make(map[openrtb_ext.BidderName]int, len(adapterBids)), RequestTimeoutMillis: req.TMax, } - if req.Test == 1 { + if debugInfo { bidResponseExt.Debug = &openrtb_ext.ExtResponseDebug{ - HttpCalls: make(map[openrtb_ext.BidderName][]*openrtb_ext.ExtHttpCall), - } - if err := json.Unmarshal(resolvedRequest, &bidResponseExt.Debug.ResolvedRequest); err != nil { - glog.Errorf("Error unmarshalling bid request snapshot: %v", err) + HttpCalls: make(map[openrtb_ext.BidderName][]*openrtb_ext.ExtHttpCall), + ResolvedRequest: req, } } for bidderName, responseExtra := range adapterExtra { - if req.Test == 1 { + if debugInfo { bidResponseExt.Debug.HttpCalls[bidderName] = responseExtra.HttpCalls } // Only make an entry for bidder errors if the bidder reported any. @@ -774,14 +771,6 @@ func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auc *auction) (openrtb_ext.E return cacheInfo, found } -// Returns a snapshot of resolved bid request for debug if test field is set in the incomming request -func buildResolvedRequest(bidRequest *openrtb.BidRequest) (json.RawMessage, error) { - if bidRequest.Test == 1 { - return json.Marshal(bidRequest) - } - return nil, nil -} - func listBiddersWithRequests(cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest) []openrtb_ext.BidderName { liveAdapters := make([]openrtb_ext.BidderName, len(cleanRequests)) i := 0 diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 96f740de23a..7da7b62e70b 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -22,6 +22,7 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" metricsConf "github.com/prebid/prebid-server/pbsmetrics/config" + metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config" pbc "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/file_fetcher" @@ -112,9 +113,6 @@ func TestCharacterEscape(t *testing.T) { Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`), } - //resolvedRequest json.RawMessage - resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`) - //adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, 1) adapterExtra["appnexus"] = &seatResponseExtra{ @@ -126,7 +124,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, nil, errList) + bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, errList) /* 5) Assert we have no errors and one '&' character as we are supposed to */ if err != nil { @@ -140,6 +138,137 @@ func TestCharacterEscape(t *testing.T) { } } +// TestDebugBehaviour asserts the HttpCalls object is included inside the json "debug" field of the bidResponse extension when the +// openrtb.BidRequest "Test" value is set to 1 or the openrtb.BidRequest.Ext.Debug boolean field is set to true +func TestDebugBehaviour(t *testing.T) { + + // Define test cases + type inTest struct { + test int8 + debug bool + } + type outTest struct { + debugInfoIncluded bool + } + type aTest struct { + desc string + in inTest + out outTest + } + testCases := []aTest{ + { + desc: "test flag equals zero, ext debug flag false, no debug info expected", + in: inTest{test: 0, debug: false}, + out: outTest{debugInfoIncluded: false}, + }, + { + desc: "test flag equals zero, ext debug flag true, debug info expected", + in: inTest{test: 0, debug: true}, + out: outTest{debugInfoIncluded: true}, + }, + { + desc: "test flag equals 1, ext debug flag false, debug info expected", + in: inTest{test: 1, debug: false}, + out: outTest{debugInfoIncluded: true}, + }, + { + desc: "test flag equals 1, ext debug flag true, debug info expected", + in: inTest{test: 1, debug: true}, + out: outTest{debugInfoIncluded: true}, + }, + { + desc: "test flag not equal to 0 nor 1, ext debug flag false, no debug info expected", + in: inTest{test: 2, debug: false}, + out: outTest{debugInfoIncluded: false}, + }, + { + desc: "test flag not equal to 0 nor 1, ext debug flag true, debug info expected", + in: inTest{test: -1, debug: true}, + out: outTest{debugInfoIncluded: true}, + }, + } + + // Set up test + noBidServer := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(204) + } + server := httptest.NewServer(http.HandlerFunc(noBidServer)) + defer server.Close() + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + bidRequest := &openrtb.BidRequest{ + ID: "some-request-id", + Imp: []openrtb.Imp{{ + ID: "some-impression-id", + Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + }}, + Site: &openrtb.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + Device: &openrtb.Device{UA: "curl/7.54.0", IP: "::1"}, + AT: 1, + TMax: 500, + } + + bidderImpl := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{}, + } + + e := new(exchange) + e.adapterMap = map[openrtb_ext.BidderName]adaptedBidder{ + openrtb_ext.BidderAppnexus: adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus), + } + e.cache = &wellBehavedCache{} + e.me = &metricsConf.DummyMetricsEngine{} + e.gDPR = gdpr.AlwaysAllow{} + e.currencyConverter = currencies.NewRateConverterDefault() + + // Run tests + for _, test := range testCases { + bidRequest.Test = test.in.test + + if test.in.debug { + bidRequest.Ext = json.RawMessage(`{"prebid":{"debug":true}}`) + } else { + bidRequest.Ext = nil + } + + // Run test + outBidResponse, err := e.HoldAuction(context.Background(), bidRequest, &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) + + // Assert no HoldAuction error + assert.NoErrorf(t, err, "%s. ex.HoldAuction returned an error: %v \n", test.desc, err) + assert.NotNilf(t, outBidResponse.Ext, "%s. outBidResponse.Ext should not be nil \n", test.desc) + + actualExt := &openrtb_ext.ExtBidResponse{} + err = json.Unmarshal(outBidResponse.Ext, actualExt) + assert.NoErrorf(t, err, "%s. \"ext\" JSON field could not be unmarshaled. err: \"%v\" \n outBidResponse.Ext: \"%s\" \n", test.desc, err, outBidResponse.Ext) + + if test.out.debugInfoIncluded { + assert.NotNilf(t, actualExt, "%s. ext.debug field is expected to be included in this outBidResponse.Ext and not be nil. outBidResponse.Ext.Debug = %v \n", test.desc, actualExt.Debug) + + // Assert "Debug fields + assert.Greater(t, len(actualExt.Debug.HttpCalls), 0, "%s. ext.debug.httpcalls array should not be empty\n", test.desc) + assert.Equal(t, server.URL, actualExt.Debug.HttpCalls["appnexus"][0].Uri, "%s. ext.debug.httpcalls array should not be empty\n", test.desc) + assert.NotNilf(t, actualExt.Debug.ResolvedRequest, "%s. ext.debug.resolvedrequest field is expected to be included in this outBidResponse.Ext and not be nil. outBidResponse.Ext.Debug = %v \n", test.desc, actualExt.Debug) + + // If not nil, assert bid extension + if test.in.debug { + diffJson(t, test.desc, bidRequest.Ext, actualExt.Debug.ResolvedRequest.Ext) + } + } + } +} + func TestGetBidCacheInfo(t *testing.T) { testUUID := "CACHE_UUID_1234" testExternalCacheHost := "https://www.externalprebidcache.net" @@ -230,9 +359,6 @@ func TestGetBidCacheInfo(t *testing.T) { }, } - //resolvedRequest json.RawMessage - resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`) - //adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ bidderName: { @@ -278,7 +404,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, nil, errList) + bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, errList) /* 5) Assert we have no errors and the bid response we expected*/ assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error") @@ -342,8 +468,6 @@ func TestBidResponseCurrency(t *testing.T) { Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`), } - resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`) - adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ "appnexus": {ResponseTimeMillis: 5}, } @@ -449,7 +573,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, nil, errList) + actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, nil, 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)) } @@ -702,6 +826,38 @@ func TestTimeoutComputation(t *testing.T) { } } +func TestSetDebugContextKey(t *testing.T) { + // Test cases + testCases := []struct { + desc string + inDebugInfo bool + expectedDebugInfo bool + }{ + { + desc: "debugInfo flag on, we expect to find DebugContextKey key in context", + inDebugInfo: true, + expectedDebugInfo: true, + }, + { + desc: "debugInfo flag off, we don't expect to find DebugContextKey key in context", + inDebugInfo: false, + expectedDebugInfo: false, + }, + } + + // Setup test + ex := exchange{} + + // Run tests + for _, test := range testCases { + auctionCtx := ex.makeDebugContext(context.Background(), test.inDebugInfo) + + debugInfo := auctionCtx.Value(DebugContextKey) + assert.NotNil(t, debugInfo, "%s. Flag set, `debugInfo` shouldn't be nil") + assert.Equal(t, test.expectedDebugInfo, debugInfo.(bool), "Desc: %s. Incorrect value mapped to DebugContextKey(`debugInfo`) in the context\n", test.desc) + } +} + // TestExchangeJSON executes tests for all the *.json files in exchangetest. func TestExchangeJSON(t *testing.T) { if specFiles, err := ioutil.ReadDir("./exchangetest"); err == nil { @@ -974,7 +1130,7 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, 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") @@ -1029,7 +1185,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, 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") @@ -1081,7 +1237,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, 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") @@ -1163,7 +1319,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, 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") @@ -1229,7 +1385,7 @@ func TestCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, 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") @@ -1340,7 +1496,7 @@ func TestBidRejectionErrors(t *testing.T) { adapterBids[bidderName] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, test.reqExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &test.reqExt, adapterBids, categoriesFetcher, targData) if len(test.expectedCatDur) > 0 { // Bid deduplication case diff --git a/exchange/utils.go b/exchange/utils.go index bc1b555e507..97fae7b78ca 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -22,12 +22,8 @@ import ( func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) - if len(req.Prebid.SChains) == 0 { - return bidderToSChains, nil - } - - for _, schainWrapper := range req.Prebid.SChains { - if schainWrapper != nil && len(schainWrapper.Bidders) > 0 { + if req != nil { + for _, schainWrapper := range req.Prebid.SChains { for _, bidder := range schainWrapper.Bidders { if _, present := bidderToSChains[bidder]; present { return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+ @@ -49,6 +45,7 @@ func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext // 3. BidRequest.User.BuyerUID will be set to that Bidder's ID. func cleanOpenRTBRequests(ctx context.Context, orig *openrtb.BidRequest, + requestExt *openrtb_ext.ExtRequest, usersyncs IdFetcher, blables map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels, @@ -66,7 +63,7 @@ func cleanOpenRTBRequests(ctx context.Context, return } - requestsByBidder, errs = splitBidRequest(orig, impsByBidder, aliases, usersyncs, blables, labels) + requestsByBidder, errs = splitBidRequest(orig, requestExt, impsByBidder, aliases, usersyncs, blables, labels) gdpr := extractGDPR(orig, usersyncIfAmbiguous) consent := extractConsent(orig) @@ -125,6 +122,7 @@ func cleanOpenRTBRequests(ctx context.Context, } func splitBidRequest(req *openrtb.BidRequest, + requestExt *openrtb_ext.ExtRequest, impsByBidder map[string][]openrtb.Imp, aliases map[string]string, usersyncs IdFetcher, @@ -137,20 +135,16 @@ func splitBidRequest(req *openrtb.BidRequest, return nil, []error{err} } - var requestExt openrtb_ext.ExtRequest var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain - if len(req.Ext) > 0 { - err := json.Unmarshal(req.Ext, &requestExt) - if err != nil { - return nil, []error{err} - } - sChainsByBidder, err = BidderToPrebidSChains(&requestExt) - if err != nil { - return nil, []error{err} - } - } else { - sChainsByBidder = make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) + sChainsByBidder, err = BidderToPrebidSChains(requestExt) + if err != nil { + return nil, []error{err} + } + + reqExt, err := getExtJson(req, requestExt) + if err != nil { + return nil, []error{err} } for bidder, imps := range impsByBidder { @@ -174,23 +168,21 @@ func splitBidRequest(req *openrtb.BidRequest, reqCopy.Imp = imps prepareSource(&reqCopy, bidder, sChainsByBidder) - prepareExt(&reqCopy, &requestExt) + reqCopy.Ext = reqExt requestsByBidder[openrtb_ext.BidderName(bidder)] = &reqCopy } return requestsByBidder, nil } -func prepareExt(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) { - if len(req.Ext) == 0 { - return +func getExtJson(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) (json.RawMessage, error) { + if len(req.Ext) == 0 || unpackedExt == nil { + return json.RawMessage(``), nil } + extCopy := *unpackedExt extCopy.Prebid.SChains = nil - reqExt, err := json.Marshal(extCopy) - if err == nil { - req.Ext = reqExt - } + return json.Marshal(extCopy) } func prepareSource(req *openrtb.BidRequest, bidder string, sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) { @@ -434,3 +426,55 @@ func randomizeList(list []openrtb_ext.BidderName) { list[i], list[j] = list[j], list[i] } } + +func extractBidRequestExt(bidRequest *openrtb.BidRequest) (*openrtb_ext.ExtRequest, error) { + requestExt := &openrtb_ext.ExtRequest{} + + if bidRequest == nil { + return requestExt, fmt.Errorf("Error bidRequest should not be nil") + } + + if len(bidRequest.Ext) > 0 { + err := json.Unmarshal(bidRequest.Ext, &requestExt) + if err != nil { + return requestExt, fmt.Errorf("Error decoding Request.ext : %s", err.Error()) + } + } + return requestExt, nil +} + +func getExtCacheInfo(requestExt *openrtb_ext.ExtRequest) (shouldCacheBids bool, shouldCacheVAST bool) { + if requestExt != nil && requestExt.Prebid.Cache != nil { + shouldCacheBids = requestExt.Prebid.Cache.Bids != nil + shouldCacheVAST = requestExt.Prebid.Cache.VastXML != nil + } + return +} + +func getExtTargetData(requestExt *openrtb_ext.ExtRequest, shouldCacheBids bool, shouldCacheVAST bool) *targetData { + var targData *targetData + + if requestExt != nil && requestExt.Prebid.Targeting != nil { + targData = &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: requestExt.Prebid.Targeting.IncludeWinners, + includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys, + includeCacheBids: shouldCacheBids, + includeCacheVast: shouldCacheVAST, + includeFormat: requestExt.Prebid.Targeting.IncludeFormat, + } + } + return targData +} + +func getDebugInfo(bidRequest *openrtb.BidRequest, requestExt *openrtb_ext.ExtRequest) bool { + return (bidRequest != nil && bidRequest.Test == 1) || (requestExt != nil && requestExt.Prebid.Debug) +} + +func getExtBidAdjustmentFactors(requestExt *openrtb_ext.ExtRequest) map[string]float64 { + var bidAdjustmentFactors map[string]float64 + if requestExt != nil { + bidAdjustmentFactors = requestExt.Prebid.BidAdjustmentFactors + } + return bidAdjustmentFactors +} diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 608e6a17a10..3b919d3da56 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -3,6 +3,7 @@ package exchange import ( "context" "encoding/json" + "fmt" "testing" "github.com/mxmCherry/openrtb" @@ -79,7 +80,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { } for _, test := range testCases { - reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -141,7 +142,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { }, } - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) result := results["appnexus"] assert.Nil(t, errs) @@ -185,7 +186,7 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { req := newBidRequest(t) req.Regs = &openrtb.Regs{COPPA: test.coppa} - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}) result := results["appnexus"] assert.Nil(t, errs) @@ -202,77 +203,92 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { func TestCleanOpenRTBRequestsSChain(t *testing.T) { testCases := []struct { - description string - inSourceExt json.RawMessage - inExt json.RawMessage - outSourceExt json.RawMessage - outExt json.RawMessage - hasError bool + description string + inExt json.RawMessage + inSourceExt json.RawMessage + outSourceExt json.RawMessage + outRequestExt json.RawMessage + hasError bool }{ { - description: "Empty root ext and source ext", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(``), - outSourceExt: json.RawMessage(``), - outExt: json.RawMessage(``), - hasError: false, + description: "Empty root ext and source ext, nil unmarshaled ext", + inExt: nil, + inSourceExt: json.RawMessage(``), + outSourceExt: json.RawMessage(``), + outRequestExt: json.RawMessage(``), + hasError: false, }, { - description: "No schains in root ext and empty source ext", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"schains":[]}}`), - outSourceExt: json.RawMessage(``), - outExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, + description: "Empty root ext, source ext, and unmarshaled ext", + inExt: json.RawMessage(``), + inSourceExt: json.RawMessage(``), + outSourceExt: json.RawMessage(``), + outRequestExt: json.RawMessage(``), + hasError: false, }, { - description: "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains", - inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), - outExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, + description: "No schains in root ext and empty source ext. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[]}}`), + outSourceExt: json.RawMessage(``), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, }, { - description: "Use schain for bidder in ext.prebid.schains", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), - outExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, + description: "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, }, { - description: "Use wildcard schain in ext.prebid.schains", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), - outExt: json.RawMessage(`{"prebid":{}}`), - hasError: false, + description: "Use schain for bidder in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, }, { - description: "Use schain for bidder in ext.prebid.schains instead of wildcard", - inSourceExt: json.RawMessage(``), - inExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`), - outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), - outExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`), - hasError: false, + description: "Use wildcard schain in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, }, { - description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains", - inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), - inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), - outSourceExt: nil, - outExt: nil, - hasError: true, + description: "Use schain for bidder in ext.prebid.schains instead of wildcard. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`), + hasError: false, + }, + { + description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: nil, + outRequestExt: nil, + hasError: true, }, } for _, test := range testCases { req := newBidRequest(t) req.Source.Ext = test.inSourceExt - req.Ext = test.inExt - results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{}) + var extRequest *openrtb_ext.ExtRequest + if test.inExt != nil { + req.Ext = test.inExt + unmarshaledExt, err := extractBidRequestExt(req) + assert.NoErrorf(t, err, test.description+":Error unmarshaling inExt") + extRequest = unmarshaledExt + } + + results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, extRequest, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{}) result := results["appnexus"] if test.hasError == true { @@ -281,11 +297,295 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { } else { assert.Nil(t, errs) assert.Equal(t, test.outSourceExt, result.Source.Ext, test.description+":Source.Ext") - assert.Equal(t, test.outExt, result.Ext, test.description+":Ext") + assert.Equal(t, test.outRequestExt, result.Ext, test.description+":Ext") } } } +func TestExtractBidRequesteExt(t *testing.T) { + testCases := []struct { + desc string + inBidRequest *openrtb.BidRequest + outRequestExt *openrtb_ext.ExtRequest + outError error + }{ + { + desc: "Valid", + inBidRequest: &openrtb.BidRequest{Ext: json.RawMessage(`{"prebid":{"debug":true}}`)}, + outRequestExt: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: true}}, + outError: nil, + }, + { + desc: "bidRequest nil, we expect an error", + inBidRequest: nil, + outRequestExt: &openrtb_ext.ExtRequest{}, + outError: fmt.Errorf("Error bidRequest should not be nil"), + }, + { + desc: "Non-nil bidRequest with empty Ext, we expect a blank requestExt", + inBidRequest: &openrtb.BidRequest{}, + outRequestExt: &openrtb_ext.ExtRequest{}, + outError: nil, + }, + { + desc: "Non-nil bidRequest with non-empty, invalid Ext, we expect unmarshaling error", + inBidRequest: &openrtb.BidRequest{Ext: json.RawMessage(`invalid`)}, + outRequestExt: &openrtb_ext.ExtRequest{}, + outError: fmt.Errorf("Error decoding Request.ext : invalid character 'i' looking for beginning of value"), + }, + } + for _, test := range testCases { + actualRequestExt, actualErr := extractBidRequestExt(test.inBidRequest) + + assert.Equal(t, test.outRequestExt, actualRequestExt, "%s. Unexpected RequestExt value. \n", test.desc) + assert.Equal(t, test.outError, actualErr, "%s. Unexpected error value. \n", test.desc) + } +} + +func TestGetExtCacheInfo(t *testing.T) { + testCases := []struct { + desc string + inRequestExt *openrtb_ext.ExtRequest + outCacheBids bool + outCacheVAST bool + }{ + { + desc: "Nil inRequestExt, both cache flags false", + inRequestExt: nil, + outCacheBids: false, + outCacheVAST: false, + }, + { + desc: "Non-nil inRequestExt, nil Cache info, both cache flags false", + inRequestExt: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Cache: nil}}, + outCacheBids: false, + outCacheVAST: false, + }, + { + desc: "Non-nil inRequestExt, non-nil Cache info, both ExtRequestPrebidCacheBids and ExtRequestPrebidCacheVAST nil", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: nil, + VastXML: nil, + }, + }, + }, + outCacheBids: false, + outCacheVAST: false, + }, + { + desc: "Non-nil inRequestExt, non-nil Cache info, both ExtRequestPrebidCacheBids nil, shouldCacheVast true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: nil, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{}, + }, + }, + }, + outCacheBids: false, + outCacheVAST: true, + }, + { + desc: "Non-nil inRequestExt, non-nil Cache info, both ExtRequestPrebidCacheVAST nil, shouldCacheBids true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, + VastXML: nil, + }, + }, + }, + outCacheBids: true, + outCacheVAST: false, + }, + { + desc: "Non-nil inRequestExt, non-nil Cache info values, both shouldCacheBids and shouldCacheVAST return true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{}, + }, + }, + }, + outCacheBids: true, + outCacheVAST: true, + }, + } + for _, test := range testCases { + shouldCacheBids, shouldCacheVAST := getExtCacheInfo(test.inRequestExt) + + assert.Equal(t, test.outCacheBids, shouldCacheBids, "%s. Unexpected shouldCacheBids value. \n", test.desc) + assert.Equal(t, test.outCacheVAST, shouldCacheVAST, "%s. Unexpected shouldCacheVAST value. \n", test.desc) + } +} + +func TestGetExtTargetData(t *testing.T) { + type inTest struct { + requestExt *openrtb_ext.ExtRequest + shouldCacheBids bool + shouldCacheVAST bool + } + type outTest struct { + targetData *targetData + nilTargetData bool + } + testCases := []struct { + desc string + in inTest + out outTest + }{ + { + "nil requestExt, nil outTargetData", + inTest{ + requestExt: nil, + shouldCacheBids: true, + shouldCacheVAST: true, + }, + outTest{targetData: nil, nilTargetData: true}, + }, + { + "Valid requestExt, nil Targeting field, nil outTargetData", + inTest{ + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Targeting: nil, + }, + }, + shouldCacheBids: true, + shouldCacheVAST: true, + }, + outTest{targetData: nil, nilTargetData: true}, + }, + { + "Valid targeting data in requestExt, valid outTargetData", + inTest{ + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Targeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: openrtb_ext.PriceGranularity{ + Precision: 2, + Ranges: []openrtb_ext.GranularityRange{{Min: 0.00, Max: 5.00, Increment: 1.00}}, + }, + IncludeWinners: true, + IncludeBidderKeys: true, + }, + }, + }, + shouldCacheBids: true, + shouldCacheVAST: true, + }, + outTest{ + targetData: &targetData{ + priceGranularity: openrtb_ext.PriceGranularity{ + Precision: 2, + Ranges: []openrtb_ext.GranularityRange{{Min: 0.00, Max: 5.00, Increment: 1.00}}, + }, + includeWinners: true, + includeBidderKeys: true, + includeCacheBids: true, + includeCacheVast: true, + }, + nilTargetData: false, + }, + }, + } + for _, test := range testCases { + actualTargetData := getExtTargetData(test.in.requestExt, test.in.shouldCacheBids, test.in.shouldCacheVAST) + + if test.out.nilTargetData { + assert.Nil(t, actualTargetData, "%s. Targeting data should be nil. \n", test.desc) + } else { + assert.NotNil(t, actualTargetData, "%s. Targeting data should NOT be nil. \n", test.desc) + assert.Equal(t, *test.out.targetData, *actualTargetData, "%s. Unexpected targeting data value. \n", test.desc) + } + } +} + +func TestGetDebugInfo(t *testing.T) { + type inTest struct { + bidRequest *openrtb.BidRequest + requestExt *openrtb_ext.ExtRequest + } + testCases := []struct { + desc string + in inTest + out bool + }{ + { + desc: "Nil bid request, nil requestExt", + in: inTest{nil, nil}, + out: false, + }, + { + desc: "bid request test == 0, nil requestExt", + in: inTest{&openrtb.BidRequest{Test: 0}, nil}, + out: false, + }, + { + desc: "Nil bid request, requestExt debug flag false", + in: inTest{nil, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: false}}}, + out: false, + }, + { + desc: "bid request test == 0, requestExt debug flag false", + in: inTest{&openrtb.BidRequest{Test: 0}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: false}}}, + out: false, + }, + { + desc: "bid request test == 1, requestExt debug flag false", + in: inTest{&openrtb.BidRequest{Test: 1}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: false}}}, + out: true, + }, + { + desc: "bid request test == 0, requestExt debug flag true", + in: inTest{&openrtb.BidRequest{Test: 0}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: true}}}, + out: true, + }, + { + desc: "bid request test == 1, requestExt debug flag true", + in: inTest{&openrtb.BidRequest{Test: 1}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: true}}}, + out: true, + }, + } + for _, test := range testCases { + actualDebugInfo := getDebugInfo(test.in.bidRequest, test.in.requestExt) + + assert.Equal(t, test.out, actualDebugInfo, "%s. Unexpected debug value. \n", test.desc) + } +} + +func TestGetExtBidAdjustmentFactors(t *testing.T) { + testCases := []struct { + desc string + inRequestExt *openrtb_ext.ExtRequest + outBidAdjustmentFactors map[string]float64 + }{ + { + desc: "Nil request ext", + inRequestExt: nil, + outBidAdjustmentFactors: nil, + }, + { + desc: "Non-nil request ext, nil BidAdjustmentFactors field", + inRequestExt: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{BidAdjustmentFactors: nil}}, + outBidAdjustmentFactors: nil, + }, + { + desc: "Non-nil request ext, valid BidAdjustmentFactors field", + inRequestExt: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{BidAdjustmentFactors: map[string]float64{"bid-factor": 1.0}}}, + outBidAdjustmentFactors: map[string]float64{"bid-factor": 1.0}, + }, + } + for _, test := range testCases { + actualBidAdjustmentFactors := getExtBidAdjustmentFactors(test.inRequestExt) + + assert.Equal(t, test.outBidAdjustmentFactors, actualBidAdjustmentFactors, "%s. Unexpected BidAdjustmentFactors value. \n", test.desc) + } +} + func TestCleanOpenRTBRequestsLMT(t *testing.T) { var ( enabled int8 = 1 @@ -346,7 +646,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { }, } - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) result := results["appnexus"] assert.Nil(t, errs) @@ -427,7 +727,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { }, } - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: !test.gdprScrub}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: !test.gdprScrub}, true, privacyConfig) result := results["appnexus"] assert.Nil(t, errs) diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index acfd4a1e71f..23daaf0f76e 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -19,6 +19,7 @@ type ExtRequestPrebid struct { StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` Targeting *ExtRequestTargeting `json:"targeting,omitempty"` SupportDeals bool `json:"supportdeals,omitempty"` + Debug bool `json:"debug,omitempty"` } // ExtRequestPrebid defines the contract for bidrequest.ext.prebid.schains From cc4350270973749b5cbcc4f5bd191f4daeb13dbe Mon Sep 17 00:00:00 2001 From: Adprime <64427228+Adprime@users.noreply.github.com> Date: Fri, 7 Aug 2020 18:06:46 +0300 Subject: [PATCH 159/381] Remove redundad struct (#1432) --- adapters/adprime/adprime.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/adapters/adprime/adprime.go b/adapters/adprime/adprime.go index 007d3c86570..8594cb5d2e4 100644 --- a/adapters/adprime/adprime.go +++ b/adapters/adprime/adprime.go @@ -24,10 +24,6 @@ func NewAdprimeBidder(endpoint string) *AdprimeAdapter { } } -type adprimeParams struct { - TagID string `json:"TagID"` -} - // MakeRequests create bid request for adprime demand func (a *AdprimeAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var errs []error From e67dfa4b8b58f995bda215299e1a4435a3d0e59b Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Wed, 12 Aug 2020 10:14:36 -0400 Subject: [PATCH 160/381] Tcf2 id support (#1420) --- endpoints/auction_test.go | 5 +++-- endpoints/cookie_sync_test.go | 4 ++-- endpoints/setuid_test.go | 4 ++-- exchange/utils.go | 4 +++- exchange/utils_test.go | 4 ++-- gdpr/gdpr.go | 2 +- gdpr/impl.go | 35 ++++++++++++++++++------------ gdpr/impl_test.go | 35 +++++++++++++++++++++++------- privacy/enforcement.go | 5 +++-- privacy/enforcement_test.go | 40 +++++++++++++++++++++++++++++++---- 10 files changed, 100 insertions(+), 38 deletions(-) diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index 5e9e9639a9c..028f119640a 100644 --- a/endpoints/auction_test.go +++ b/endpoints/auction_test.go @@ -408,6 +408,7 @@ type auctionMockPermissions struct { allowHostCookies bool allowPI bool allowGeo bool + allowID bool } func (m *auctionMockPermissions) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { @@ -418,8 +419,8 @@ 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, bool, error) { - return m.allowPI, m.allowGeo, nil +func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return m.allowPI, m.allowGeo, m.allowID, nil } func (m *auctionMockPermissions) AMPException() bool { diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 824e32f1957..f7974d2bc77 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -254,8 +254,8 @@ 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, bool, error) { - return true, true, nil +func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return true, true, true, nil } func (g *gdprPerms) AMPException() bool { diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 3f47b257d2e..e63944e2aec 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -437,8 +437,8 @@ 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, bool, error) { - return g.allowPI, g.allowPI, nil +func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return g.allowPI, g.allowPI, g.allowPI, nil } func (g *mockPermsSetUID) AMPException() bool { diff --git a/exchange/utils.go b/exchange/utils.go index 97fae7b78ca..2131aac5f41 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -107,12 +107,14 @@ func cleanOpenRTBRequests(ctx context.Context, coreBidder := resolveBidder(bidder.String(), aliases) var publisherID = labels.PubID - ok, geo, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) + ok, geo, id, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) privacyEnforcement.GDPR = !ok && err == nil privacyEnforcement.GDPRGeo = !geo && err == nil + privacyEnforcement.GDPRID = !id && err == nil } else { privacyEnforcement.GDPR = false privacyEnforcement.GDPRGeo = false + privacyEnforcement.GDPRID = false } privacyEnforcement.Apply(bidReq, ampGDPRException) diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 3b919d3da56..528e875ab16 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -28,8 +28,8 @@ 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, bool, error) { - return p.personalInfoAllowed, p.personalInfoAllowed, nil +func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil } func (p *permissionsMock) AMPException() bool { diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 0dfa12f5ebd..04db8cb92ed 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -23,7 +23,7 @@ type Permissions interface { // Determines whether or not to send PI information to a bidder, or mask it out. // // 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) + PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) // Exposes the AMP execption flag AMPException() bool diff --git a/gdpr/impl.go b/gdpr/impl.go index 60db804aec6..2deddc7b2ba 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -42,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, bool, error) { +func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { _, ok := p.cfg.NonStandardPublisherMap[PublisherID] if ok { - return true, true, nil + return true, true, true, nil } id, ok := p.vendorIDs[bidder] @@ -54,10 +54,10 @@ func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrt } if consent == "" { - return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } - return false, false, nil + return false, false, false, nil } func (p *permissionsImpl) AMPException() bool { @@ -98,19 +98,19 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen return false, nil } -func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, error) { +func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, bool, error) { // If we're not given a consent string, respect the preferences in the app config. if consent == "" { - return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent) if err != nil { - return false, false, err + return false, false, false, err } if vendor == nil { - return false, false, nil + return false, false, false, nil } if parsedConsent.Version() == 2 { @@ -118,21 +118,22 @@ func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent 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 + return true, 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 true, true, true, nil } } - return false, false, nil + return false, false, false, nil } -func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16) (allowPI bool, allowGeo bool, err error) { +func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16) (allowPI bool, allowGeo bool, allowID bool, err error) { consent, ok := parsedConsent.(tcf2.ConsentMetadata) err = nil allowPI = false allowGeo = false + allowID = false if !ok { err = fmt.Errorf("Unable to access TCF2 parsed consent") return @@ -142,6 +143,12 @@ func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor a } else { allowGeo = true } + for i := 2; i <= 10; i++ { + if p.checkPurpose(consent, vendor, vendorID, tcf1constants.Purpose(i)) { + allowID = true + break + } + } // Set to true so any purpose check can flip it to false allowPI = true if p.cfg.TCF2.Purpose1.Enabled { @@ -214,8 +221,8 @@ 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, bool, error) { - return true, true, nil +func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return true, true, true, nil } func (a AlwaysAllow) AMPException() bool { diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 05b2fb6d98e..053e87536ab 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -207,17 +207,17 @@ func TestAllowPersonalInfo(t *testing.T) { } // 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) } @@ -257,6 +257,7 @@ type tcf2TestDef struct { consent string allowPI bool allowGeo bool + allowID bool } func TestAllowPersonalInfoTCF2(t *testing.T) { @@ -285,6 +286,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -292,6 +294,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", allowPI: true, allowGeo: true, + allowID: true, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -299,14 +302,16 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", allowPI: true, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowGeo failure on %s", td.description) } } @@ -328,10 +333,11 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { } // 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") + allowPI, allowGeo, allowID, 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") + assert.EqualValuesf(t, true, allowID, "AllowID failure") } @@ -361,6 +367,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -368,6 +375,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -375,14 +383,16 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", allowPI: false, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowPI failure on %s", td.description) } } @@ -413,6 +423,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -420,6 +431,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: true, allowGeo: true, + allowID: true, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -427,14 +439,16 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: true, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowID failure on %s", td.description) } } @@ -458,6 +472,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { perms.cfg.TCF2.PurposeOneTreatment.AccessAllowed = false // COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA Purpose one flag set + // Purpose one treatment will fail PI, but allow passing the IDs. testDefs := []tcf2TestDef{ { description: "Appnexus vendor test, insufficient purposes claimed", @@ -465,6 +480,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -472,6 +488,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: true, + allowID: true, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -479,14 +496,16 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowID failure on %s", td.description) } } diff --git a/privacy/enforcement.go b/privacy/enforcement.go index 8a5d201fc95..9c23c320680 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -10,12 +10,13 @@ type Enforcement struct { COPPA bool GDPR bool GDPRGeo bool + GDPRID 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 || e.GDPRGeo || e.LMT + return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo || e.GDPRID || e.LMT } // Apply cleans personally identifiable information from an OpenRTB bid request. @@ -64,7 +65,7 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs } // If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above) - if e.CCPA || e.GDPR || e.LMT { + if e.CCPA || e.GDPRID || e.LMT { return ScrubStrategyUserID } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 968c6354710..ef02e28147a 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -21,6 +21,7 @@ func TestAny(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: false, }, expected: false, @@ -32,6 +33,7 @@ func TestAny(t *testing.T) { COPPA: true, GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: true, }, expected: true, @@ -43,6 +45,7 @@ func TestAny(t *testing.T) { COPPA: true, GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: true, }, expected: true, @@ -72,6 +75,7 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: true, }, ampGDPRException: false, @@ -87,6 +91,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: false, }, ampGDPRException: false, @@ -102,6 +107,7 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: false, }, ampGDPRException: false, @@ -117,6 +123,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: false, }, ampGDPRException: false, @@ -132,6 +139,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: false, }, ampGDPRException: true, @@ -147,6 +155,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: false, }, ampGDPRException: true, @@ -162,6 +171,7 @@ func TestApply(t *testing.T) { COPPA: true, GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: false, }, ampGDPRException: true, @@ -177,6 +187,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: true, GDPRGeo: false, + GDPRID: true, LMT: false, }, ampGDPRException: false, @@ -192,6 +203,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: true, + GDPRID: false, LMT: false, }, ampGDPRException: false, @@ -200,6 +212,22 @@ func TestApply(t *testing.T) { expectedUser: ScrubStrategyUserNone, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, + { + description: "GDPR Only, ID exception", + enforcement: Enforcement{ + CCPA: false, + COPPA: false, + GDPR: true, + GDPRGeo: true, + GDPRID: false, + LMT: false, + }, + ampGDPRException: false, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedUser: ScrubStrategyUserNone, + expectedUserGeo: ScrubStrategyGeoReducedPrecision, + }, { description: "LMT Only", enforcement: Enforcement{ @@ -207,6 +235,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: true, }, ampGDPRException: false, @@ -222,6 +251,7 @@ func TestApply(t *testing.T) { COPPA: false, GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: true, }, ampGDPRException: true, @@ -258,10 +288,12 @@ func TestApplyNoneApplicable(t *testing.T) { m := &mockScrubber{} enforcement := Enforcement{ - CCPA: false, - COPPA: false, - GDPR: false, - LMT: false, + CCPA: false, + COPPA: false, + GDPR: false, + GDPRGeo: false, + GDPRID: false, + LMT: false, } enforcement.apply(req, false, m) From 549cc791f7d06d1a3dfaa1be061b1753c4a8146a Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Wed, 12 Aug 2020 12:27:57 -0400 Subject: [PATCH 161/381] Default TCF1 GVL in anticipation of IAB no longer hosting the v1 GVL (#1433) --- config/config.go | 9 +++ gdpr/vendorlist-fetching.go | 33 ++++++++++- gdpr/vendorlist-fetching_test.go | 97 ++++++++++++++++++++++++++++++++ static/tcf1/fallback_gvl.json | 1 + 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 static/tcf1/fallback_gvl.json diff --git a/config/config.go b/config/config.go index 9663b021b5b..7fc77855810 100755 --- a/config/config.go +++ b/config/config.go @@ -156,6 +156,7 @@ type GDPR struct { Timeouts GDPRTimeouts `mapstructure:"timeouts_ms"` NonStandardPublishers []string `mapstructure:"non_standard_publishers,flow"` NonStandardPublisherMap map[string]int + TCF1 TCF1 `mapstructure:"tcf1"` TCF2 TCF2 `mapstructure:"tcf2"` AMPException bool `mapstructure:"amp_exception"` } @@ -180,6 +181,12 @@ func (t *GDPRTimeouts) ActiveTimeout() time.Duration { return time.Duration(t.ActiveVendorlistFetch) * time.Millisecond } +// TCF1 defines the TCF1 specific configurations for GDPR +type TCF1 struct { + FetchGVL bool `mapstructure:"fetch_gvl"` + FallbackGVLPath string `mapstructure:"fallback_gvl_path"` +} + // TCF2 defines the TCF2 specific configurations for GDPR type TCF2 struct { Enabled bool `mapstructure:"enabled"` @@ -885,6 +892,8 @@ 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.tcf1.fetch_gvl", true) + v.SetDefault("gdpr.tcf1.fallback_gvl_path", "./static/tcf1/fallback_gvl.json") v.SetDefault("gdpr.tcf2.enabled", true) v.SetDefault("gdpr.tcf2.purpose1.enabled", true) v.SetDefault("gdpr.tcf2.purpose2.enabled", true) diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index 987622a6a8a..a0a73c93008 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -27,8 +27,20 @@ type saveVendors func(uint16, api.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, uint8) string, TCFVer uint8) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { + var fallbackVL api.VendorList = nil + + if TCFVer == tCF1 && len(cfg.TCF1.FallbackGVLPath) > 0 { + fallbackVL = loadFallbackGVL(cfg.TCF1.FallbackGVLPath) + } + + // If we are not going to try fetching the GVL dynamically, we have a simple fetcher + if !cfg.TCF1.FetchGVL && TCFVer == tCF1 && fallbackVL != nil { + return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { + return fallbackVL, nil + } + } // These save and load functions can be used to store & retrieve lists from our cache. - save, load := newVendorListCache() + save, load := newVendorListCache(fallbackVL) withTimeout, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) defer cancel() @@ -46,6 +58,9 @@ func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http if list != nil { return list, nil } + if fallbackVL != nil { + return fallbackVL, nil + } return nil, fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", id) } } @@ -132,7 +147,7 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen return newList.Version() } -func newVendorListCache() (save func(id uint16, list api.VendorList), load func(id uint16) api.VendorList) { +func newVendorListCache(fallbackVL api.VendorList) (save func(id uint16, list api.VendorList), load func(id uint16) api.VendorList) { cache := &sync.Map{} save = func(id uint16, list api.VendorList) { @@ -143,7 +158,19 @@ func newVendorListCache() (save func(id uint16, list api.VendorList), load func( if ok { return list.(vendorlist.VendorList) } - return nil + return fallbackVL } return } + +func loadFallbackGVL(fallbackGVLPath string) vendorlist.VendorList { + fallbackVLbody, err := ioutil.ReadFile(fallbackGVLPath) + if err != nil { + glog.Fatalf("Error reading from file %s: %v", fallbackGVLPath, err) + } + fallbackVL, err := vendorlist.ParseEagerly(fallbackVLbody) + if err != nil { + glog.Fatalf("Error processing default GVL from %s: %v", fallbackGVLPath, err) + } + return fallbackVL +} diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index 824f9178faa..c989ef4cef8 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/prebid/prebid-server/config" ) @@ -139,6 +141,101 @@ func TestVendorListMaker(t *testing.T) { assertStringsEqual(t, "https://vendorlist.consensu.org/v2/archives/vendor-list-v7.json", vendorListURLMaker(7, 2)) } +func TestDefaultVendorList(t *testing.T) { + firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ + 32: { + purposes: []int{1, 2}, + }, + }) + secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ + 12: { + purposes: []int{2}, + }, + }) + server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ + 1: firstVendorList, + 2: secondVendorList, + }))) + defer server.Close() + + testcfg := testConfig() + testcfg.TCF1.FetchGVL = true + testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" + fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1) + + list, err := fetcher(context.Background(), 12) + assert.NoError(t, err, "Error with fetching default vendorlist: %v", err) + assert.Equal(t, uint16(214), list.Version(), "Expected to fetch default version 214, got %d", list.Version()) + + // Testing that we got the default vendorlist data, and not the version off the server. + vendor := list.Vendor(12) + assert.Equal(t, true, vendor.Purpose(1)) + assert.Equal(t, false, vendor.Purpose(2)) +} + +func TestDefaultVendorListPassthrough(t *testing.T) { + firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ + 32: { + purposes: []int{1, 2}, + }, + }) + secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ + 12: { + purposes: []int{2}, + }, + }) + server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ + 1: firstVendorList, + 2: secondVendorList, + }))) + defer server.Close() + + testcfg := testConfig() + testcfg.TCF1.FetchGVL = true + testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" + fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1) + list, err := fetcher(context.Background(), 2) + assert.NoError(t, err, "Error with fetching existing vendorlist: %v", err) + assert.Equal(t, uint16(2), list.Version(), "Expected to fetch mock list version 2, got version %d", list.Version()) + + // Testing that we got the testing vendorlist data, and not the default. + vendor := list.Vendor(12) + assert.Equal(t, false, vendor.Purpose(1)) + assert.Equal(t, true, vendor.Purpose(2)) +} + +func TestDefaultVendorListNoFetch(t *testing.T) { + firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ + 32: { + purposes: []int{1, 2}, + }, + }) + secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ + 12: { + purposes: []int{2}, + }, + }) + server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ + 1: firstVendorList, + 2: secondVendorList, + }))) + defer server.Close() + + testcfg := testConfig() + testcfg.TCF1.FetchGVL = false + testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" + fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1) + list, err := fetcher(context.Background(), 2) + assert.NoError(t, err, "Error with fetching default vendorlist: %v", err) + assert.Equal(t, uint16(214), list.Version(), "Expected to fetch default version 214, got %d", list.Version()) + + // Testing that we got the default vendorlist data, and not the version off the server. + vendor := list.Vendor(12) + assert.Equal(t, true, vendor.Purpose(1)) + assert.Equal(t, false, vendor.Purpose(2)) + +} + // mockServer returns a handler which returns the given response for each global vendor list version. // The latestVersion param can be used to mock "updates" which occur after PBS has been turned on. // For example, if latestVersion is 3, but the responses map has data at "4", the server will return diff --git a/static/tcf1/fallback_gvl.json b/static/tcf1/fallback_gvl.json new file mode 100644 index 00000000000..86895a52362 --- /dev/null +++ b/static/tcf1/fallback_gvl.json @@ -0,0 +1 @@ +{"vendorListVersion":214,"lastUpdated":"2020-08-06T16:00:35Z","purposes":[{"id":1,"name":"Information storage and access","description":"The storage of information, or access to information that is already stored, on your device such as advertising identifiers, device identifiers, cookies, and similar technologies."},{"id":2,"name":"Personalisation","description":"The collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as on other websites or apps, over time. Typically, the content of the site or app is used to make inferences about your interests, which inform future selection of advertising and/or content."},{"id":3,"name":"Ad selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver advertisements for you, and to measure the delivery and effectiveness of such advertisements. This includes using previously collected information about your interests to select ads, processing data about what advertisements were shown, how often they were shown, when and where they were shown, and whether you took any action related to the advertisement, including for example clicking an ad or making a purchase. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as websites or apps, over time."},{"id":4,"name":"Content selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver content for you, and to measure the delivery and effectiveness of such content. This includes using previously collected information about your interests to select content, processing data about what content was shown, how often or how long it was shown, when and where it was shown, and whether the you took any action related to the content, including for example clicking on content. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, such as websites or apps, over time."},{"id":5,"name":"Measurement","description":"The collection of information about your use of the content, and combination with previously collected information, used to measure, understand, and report on your usage of the service. This does not include personalisation, the collection of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, i.e. on other service, such as websites or apps, over time."}],"features":[{"id":1,"name":"Matching Data to Offline Sources","description":"Combining data from offline sources that were initially collected in other contexts."},{"id":2,"name":"Linking Devices","description":"Allow processing of a user's data to connect such user across multiple devices."},{"id":3,"name":"Precise Geographic Location Data","description":"Allow processing of a user's precise geographic location data in support of a purpose for which that certain third party has consent."}],"vendors":[{"id":8,"name":"Emerse Sverige AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.emerse.com/privacy-policy/"},{"id":9,"name":"AdMaxim Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.admaxim.com/admaxim-privacy-policy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":12,"name":"BeeswaxIO Corporation","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beeswax.com/privacy/"},{"id":28,"name":"TripleLift, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://triplelift.com/privacy/"},{"id":27,"name":"ADventori SAS","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adventori.com/with-us/legal-notice/"},{"id":25,"name":"Verizon Media EMEA Limited","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.verizonmedia.com/policies/ie/en/verizonmedia/privacy/index.html"},{"id":26,"name":"Venatus Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.venatusmedia.com/privacy/"},{"id":1,"name":"Exponential Interactive, Inc d/b/a VDX.tv","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://vdx.tv/privacy/"},{"id":6,"name":"AdSpirit GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adspirit.de/privacy"},{"id":30,"name":"BidTheatre AB","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.bidtheatre.com/privacy-policy"},{"id":24,"name":"Epsilon","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.conversantmedia.eu/legal/privacy-policy"},{"id":29,"name":"Etarget SE","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.etarget.sk/privacy.php","deletedDate":"2020-06-01T00:00:00Z"},{"id":39,"name":"ADITION technologies AG","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.adition.com/datenschutz"},{"id":11,"name":"Quantcast International Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.quantcast.com/privacy/"},{"id":15,"name":"Adikteev","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adikteev.com/privacy-policy-eng/"},{"id":4,"name":"Roq.ad Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.roq.ad/privacy-policy"},{"id":7,"name":"Vibrant Media Limited","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vibrantmedia.com/en/privacy-policy/"},{"id":2,"name":"Captify Technologies Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.captify.co.uk/privacy-policy/"},{"id":37,"name":"NEURAL.ONE","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://web.neural.one/privacy-policy/"},{"id":13,"name":"Sovrn Holdings Inc","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sovrn.com/sovrn-privacy/"},{"id":34,"name":"NEORY GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.neory.com/privacy.html"},{"id":32,"name":"Xandr, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.xandr.com/privacy/platform-privacy-policy/"},{"id":10,"name":"Index Exchange, Inc. ","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.indexexchange.com/privacy"},{"id":57,"name":"ADARA MEDIA UNLIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://adara.com/privacy-promise/"},{"id":63,"name":"Avocet Systems Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://avocet.io/privacy-portal"},{"id":51,"name":"xAd, Inc. dba GroundTruth","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.groundtruth.com/privacy-policy/"},{"id":49,"name":"TRADELAB","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://tradelab.com/en/privacy/"},{"id":45,"name":"Smart Adserver","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://smartadserver.com/end-user-privacy-policy/"},{"id":52,"name":"The Rubicon Project, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[3],"policyUrl":"http://www.rubiconproject.com/rubicon-project-yield-optimization-privacy-policy/"},{"id":71,"name":"Roku Advertising Services","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://docs.roku.com/published/userprivacypolicy/en/us"},{"id":79,"name":"MediaMath, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.mediamath.com/privacy-policy/"},{"id":91,"name":"Criteo SA","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.criteo.com/privacy/"},{"id":85,"name":"Crimtan Holdings Limited","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[1,3],"policyUrl":"https://crimtan.com/privacy/"},{"id":16,"name":"RTB House S.A.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.rtbhouse.com/privacy-center/services-privacy-policy/"},{"id":86,"name":"Scene Stealer Limited","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"http://scenestealer.tv/privacy-policy/"},{"id":94,"name":"Blis Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.blis.com/privacy/"},{"id":73,"name":"Simplifi Holdings Inc.","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2,3],"policyUrl":"https://simpli.fi/site-privacy-policy/"},{"id":33,"name":"ShareThis, Inc","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://sharethis.com/privacy/"},{"id":20,"name":"N Technologies Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://n.rich/privacy-notice"},{"id":55,"name":"Madison Logic, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.madisonlogic.com/privacy/"},{"id":53,"name":"Sirdata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.sirdata.com/privacy/"},{"id":69,"name":"OpenX","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.openx.com/legal/privacy-policy/"},{"id":98,"name":"GroupM UK Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.groupm.com/privacy-notice"},{"id":62,"name":"Justpremium BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://justpremium.com/privacy-policy/"},{"id":19,"name":"Intent Media, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://intentmedia.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":43,"name":"Vdopia DBA Chocolate Platform","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://chocolateplatform.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":36,"name":"RhythmOne DBA Unruly Group Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.rhythmone.com/privacy-policy"},{"id":80,"name":"Sharethrough, Inc","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://platform-cdn.sharethrough.com/privacy-policy"},{"id":81,"name":"PulsePoint, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pulsepoint.com/privacy-policy/website","deletedDate":"2020-07-06T00:00:00Z"},{"id":23,"name":"Amobee, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.amobee.com/trust/privacy-guidelines"},{"id":35,"name":"Purch Group, Inc.","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://www.purch.com/privacy-policy/","deletedDate":"2019-05-30T00:00:00Z"},{"id":3,"name":"affilinet","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.affili.net/de/footeritem/datenschutz","deletedDate":"2019-06-21T00:00:00Z"},{"id":74,"name":"Admotion SRL","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.admotion.com/policy/","deletedDate":"2019-07-24T00:00:00Z"},{"id":191,"name":"realzeit GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://realzeitmedia.com/privacy.html","deletedDate":"2019-04-29T00:00:00Z"},{"id":197,"name":"Switch Concepts Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.switchconcepts.com/privacy-policy","deletedDate":"2019-07-26T00:00:00Z"},{"id":390,"name":"Parsec Media Inc.","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,3],"policyUrl":"www.parsec.media/privacy-policy","deletedDate":"2019-06-27T00:00:00Z"},{"id":459,"name":"uppr GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://netzwerk.uppr.de/privacy-policy.do","deletedDate":"2019-06-17T00:00:00Z"},{"id":221,"name":"LEMO MEDIA GROUP LIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.lemomedia.com/terms.pdf","deletedDate":"2019-06-28T00:00:00Z"},{"id":478,"name":"RevLifter Ltd","purposeIds":[1],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.revlifter.com/privacy-policy","deletedDate":"2019-07-15T00:00:00Z"},{"id":500,"name":"Turbo","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.turboadv.com/white-rabbit-privacy-policy/","deletedDate":"2019-07-12T00:00:00Z"},{"id":68,"name":"Sizmek by Amazon","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.sizmek.com/privacy-policy/"},{"id":75,"name":"M32 Connect Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://m32.media/privacy-cookie-policy/"},{"id":17,"name":"Greenhouse Group BV (with its trademark LemonPI)","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.lemonpi.io/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":61,"name":"GumGum, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://gumgum.com/privacy-policy"},{"id":40,"name":"Active Agent (ADITION technologies AG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.active-agent.com/de/unternehmen/datenschutzerklaerung/"},{"id":76,"name":"PubMatic, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://pubmatic.com/privacy-policy/"},{"id":89,"name":"Tapad, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.tapad.com/eu-privacy-policy"},{"id":46,"name":"Skimbit Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://skimlinks.com/pages/privacy-policy"},{"id":66,"name":"adsquare GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adsquare.com/privacy"},{"id":105,"name":"Impression Desk Technologies Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://impressiondesk.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":41,"name":"Adverline","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.adverline.com/privacy/"},{"id":82,"name":"Smaato, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.smaato.com/privacy/"},{"id":60,"name":"Rakuten Marketing LLC","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://rakutenadvertising.com/legal-notices/services-privacy-policy/"},{"id":70,"name":"Yieldlab AG","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[3],"policyUrl":"http://www.yieldlab.de/meta-navigation/datenschutz/"},{"id":50,"name":"Adform","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://site.adform.com/privacy-center/platform-privacy/product-and-services-privacy-policy/"},{"id":48,"name":"NetSuccess, s.r.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inres.sk/pp/"},{"id":100,"name":"Fifty Technology Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://fifty.io/privacy-policy.php"},{"id":21,"name":"The Trade Desk","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.thetradedesk.com/general/privacy-policy"},{"id":110,"name":"Dynata LLC","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.opinionoutpost.co.uk/en-gb/policies/privacy"},{"id":42,"name":"Taboola Europe Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.taboola.com/privacy-policy"},{"id":112,"name":"Maytrics GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://maytrics.com/privacy.php","deletedDate":"2019-09-17T00:00:00Z"},{"id":77,"name":"comScore, Inc.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.scorecardresearch.com/privacy.aspx?newlanguage=1"},{"id":109,"name":"LoopMe Limited","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://loopme.com/privacy-policy/"},{"id":120,"name":"Eyeota Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.eyeota.com/privacy-center"},{"id":93,"name":"Adloox SA","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://adloox.com/disclaimer"},{"id":132,"name":"Teads ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.teads.com/privacy-policy/"},{"id":22,"name":"admetrics GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://admetrics.io/en/privacy_policy/"},{"id":102,"name":"Telaria SAS","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":108,"name":"Rich Audience Technologies SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://richaudience.com/privacy/"},{"id":18,"name":"Widespace AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.widespace.com/legal/privacy-policy-notice/"},{"id":122,"name":"Avid Media Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.avidglobalmedia.eu/privacy-policy.html"},{"id":97,"name":"LiveRamp, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.liveramp.com/service-privacy-policy/"},{"id":138,"name":"ConnectAd Realtime GmbH","purposeIds":[1,2],"legIntPurposeIds":[3,4],"featureIds":[],"policyUrl":"http://connectadrealtime.com/privacy/"},{"id":72,"name":"Nano Interactive GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.nanointeractive.com/privacy"},{"id":127,"name":"PIXIMEDIA SAS","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://piximedia.com/privacy/"},{"id":136,"name":"Str\u00f6er SSP GmbH (SSP)","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[2,3],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":111,"name":"Showheroes SE","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://showheroes.com/privacy/"},{"id":56,"name":"Confiant Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.confiant.com/privacy","deletedDate":"2020-05-18T00:00:00Z"},{"id":124,"name":"Teemo SA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://teemo.co/fr/confidentialite/"},{"id":154,"name":"YOC AG","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://yoc.com/privacy/"},{"id":38,"name":"Beemray Oy","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beemray.com/privacy-policy/","deletedDate":"2020-06-19T00:00:00Z"},{"id":101,"name":"MiQ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://wearemiq.com/privacy-policy/"},{"id":149,"name":"ADman Interactive SLU","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://admanmedia.com/politica.html?setLng=es"},{"id":151,"name":"Admedo Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[3],"policyUrl":"https://www.admedo.com/privacy-policy","deletedDate":"2020-07-17T00:00:00Z"},{"id":153,"name":"MADVERTISE MEDIA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://madvertise.com/en/gdpr/"},{"id":159,"name":"Underdog Media LLC ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://underdogmedia.com/privacy-policy/"},{"id":157,"name":"Seedtag Advertising S.L","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.seedtag.com/en/privacy-policy/"},{"id":145,"name":"Snapsort Inc., operating as Sortable","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://help.sortable.com/help/privacy-policy"},{"id":131,"name":"ID5 Technology SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.id5.io/privacy"},{"id":158,"name":"Reveal Mobile, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://revealmobile.com/privacy"},{"id":147,"name":"Adacado Technologies Inc. (DBA Adacado)","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adacado.com/privacy-policy-april-25-2018/"},{"id":130,"name":"NextRoll, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.nextroll.com/privacy"},{"id":129,"name":"IPONWEB GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.iponweb.com/privacy-policy/"},{"id":128,"name":"BIDSWITCH GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bidswitch.com/privacy-policy/"},{"id":168,"name":"EASYmedia GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://login.rtbmarket.com/gdpr"},{"id":164,"name":"Outbrain UK Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.outbrain.com/legal/privacy#privacy-policy"},{"id":144,"name":"district m inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://districtm.net/en/page/platforms-data-and-privacy-policy/"},{"id":163,"name":"Bombora Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://bombora.com/privacy"},{"id":173,"name":"Yieldmo, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.yieldmo.com/privacy/"},{"id":88,"name":"TreSensa, Inc.","purposeIds":[1,3],"legIntPurposeIds":[2,5],"featureIds":[1],"policyUrl":"https://www.tresensa.com/eu-privacy"},{"id":78,"name":"Flashtalking, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.flashtalking.com/privacypolicy/"},{"id":59,"name":"Sift Media, Inc","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.sift.co/privacy"},{"id":114,"name":"Sublime","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://ayads.co/privacy.php"},{"id":175,"name":"FORTVISION","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://fortvision.com/POC/index.html","deletedDate":"2019-08-09T00:00:00Z"},{"id":133,"name":"digitalAudience","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://digitalaudience.io/legal/privacy-cookies/"},{"id":14,"name":"Adkernel LLC","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://adkernel.com/privacy-policy/"},{"id":180,"name":"Thirdpresence Oy","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"http://www.thirdpresence.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":183,"name":"EMX Digital LLC","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://emxdigital.com/privacy/"},{"id":58,"name":"33Across","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.33across.com/privacy-policy"},{"id":140,"name":"Platform161","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://platform161.com/cookie-and-privacy-policy/"},{"id":90,"name":"Teroa S.A.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.e-planning.net/en/privacy.html"},{"id":141,"name":"1020, Inc. dba Placecast and Ericsson Emodo","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.emodoinc.com/privacy-policy/"},{"id":142,"name":"Media.net Advertising FZ-LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.media.net/en/privacy-policy"},{"id":209,"name":"Delta Projects AB","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[3],"policyUrl":"https://deltaprojects.com/data-collection-policy"},{"id":195,"name":"advanced store GmbH","purposeIds":[2,3],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.advanced-store.com/de/datenschutz/"},{"id":190,"name":"video intelligence AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.vi.ai/privacy-policy/"},{"id":84,"name":"Semasio GmbH","purposeIds":[],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"http://www.semasio.com/privacy-policy/"},{"id":65,"name":"Location Sciences AI Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.locationsciences.ai/privacy-policy/"},{"id":210,"name":"Zemanta, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1],"policyUrl":"http://www.zemanta.com/legal/privacy"},{"id":200,"name":"Tapjoy, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.tapjoy.com/legal/#privacy-policy"},{"id":188,"name":"Sellpoints Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://retargeter.com/service-privacy-policy/","deletedDate":"2019-09-17T00:00:00Z"},{"id":217,"name":"2KDirect, Inc. (dba iPromote)","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.ipromote.com/privacy-policy/"},{"id":156,"name":"Centro, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.centro.net/privacy-policy/"},{"id":194,"name":"Rezonence Limited","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://rezonence.com/privacy-policy/"},{"id":226,"name":"Publicis Media GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.publicismedia.de/datenschutz/"},{"id":198,"name":"SYNC","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://redirect.sync.tv/privacy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":227,"name":"ORTEC B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.ortecadscience.com/privacy-policy/"},{"id":225,"name":"Ligatus GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.ligatus.com/en/privacy-policy","deletedDate":"2020-06-19T00:00:00Z"},{"id":205,"name":"Adssets AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://adssets.com/policy/"},{"id":179,"name":"Collective Europe Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.collectiveuk.com/privacy.html"},{"id":31,"name":"Ogury Ltd.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://www.ogury.com/privacy-policy/"},{"id":92,"name":"1plusX AG","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.1plusx.com/privacy-policy/"},{"id":155,"name":"AntVoice","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.antvoice.com/en/privacypolicy/"},{"id":115,"name":"smartclip Europe GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://privacy-portal.smartclip.net/"},{"id":126,"name":"DoubleVerify Inc.\u200b","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.doubleverify.com/privacy/"},{"id":193,"name":"Mediasmart Mobile S.L.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://mediasmart.io/privacy/"},{"id":245,"name":"IgnitionOne","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.ignitionone.com/privacy-policy/","deletedDate":"2020-06-30T00:00:00Z"},{"id":213,"name":"emetriq GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.emetriq.com/datenschutz/"},{"id":244,"name":"Temelio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://temelio.com/vie-privee"},{"id":224,"name":"adrule mobile GmbH","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.adrule.net/de/datenschutz/"},{"id":174,"name":"A Million Ads Ltd","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.amillionads.com/privacy-policy"},{"id":192,"name":"remerge GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://remerge.io/privacy-policy.html"},{"id":232,"name":"Rockerbox, Inc","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"http://rockerbox.com/privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":256,"name":"Bounce Exchange, Inc","purposeIds":[1],"legIntPurposeIds":[2,4,5],"featureIds":[1,2],"policyUrl":"https://www.bouncex.com/privacy/"},{"id":234,"name":"ZBO Media","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zbo.media/mentions-legales/politique-de-confidentialite-service-publicitaire/"},{"id":246,"name":"Smartology Limited","purposeIds":[3],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://www.smartology.net/privacy-policy/"},{"id":241,"name":"OneTag Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.onetag.com/privacy/"},{"id":254,"name":"LiquidM Technology GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liquidm.com/privacy-policy/"},{"id":215,"name":"ARMIS SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://armis.tech/en/armis-personal-data-privacy-policy/"},{"id":167,"name":"Audiens S.r.l.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.audiens.com/privacy"},{"id":240,"name":"7Hops.com Inc. (ZergNet)","purposeIds":[],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://zergnet.com/privacy"},{"id":235,"name":"Bucksense Inc","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.bucksense.com/platform-privacy-policy/"},{"id":185,"name":"Bidtellect, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.bidtellect.com/privacy-policy/"},{"id":258,"name":"Adello Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.adello.com/privacy-policy/"},{"id":169,"name":"RTK.IO, Inc","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://www.rtk.io/privacy.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":208,"name":"Spotad","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.spotad.co/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":211,"name":"AdTheorent, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://adtheorent.com/privacy-policy"},{"id":229,"name":"Digitize New Media Ltd","purposeIds":[2,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitize.ie/online-privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":273,"name":"Bannerflow AB","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.bannerflow.com/privacy "},{"id":104,"name":"Sonobi, Inc","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"http://sonobi.com/privacy-policy/"},{"id":162,"name":"Unruly Group Ltd","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://unruly.co/privacy/"},{"id":249,"name":"Spolecznosci Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.spolecznosci.pl/polityka-prywatnosci"},{"id":125,"name":"Research Now Group, Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.valuedopinions.co.uk/privacy","deletedDate":"2019-09-17T00:00:00Z"},{"id":170,"name":"Goodway Group, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://goodwaygroup.com/privacy-policy/"},{"id":160,"name":"Netsprint SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://netsprint.eu/privacy.html"},{"id":189,"name":"Intowow Innovation Ltd.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.intowow.com/privacy/","deletedDate":"2019-08-12T00:00:00Z"},{"id":279,"name":"Mirando GmbH & Co KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://wwwmirando.de/datenschutz/"},{"id":269,"name":"Sanoma Media Finland","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://sanoma.fi/tietoa-meista/tietosuoja/","deletedDate":"2019-08-07T00:00:00Z"},{"id":276,"name":"Viralize SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://viralize.com/privacy-policy"},{"id":87,"name":"Genius Sports Media Limited","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[2,3],"policyUrl":"https://www.geniussports.com/privacy-policy"},{"id":182,"name":"Collective, Inc. dba Visto","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vistohub.com/privacy-policy/","deletedDate":"2019-07-26T00:00:00Z"},{"id":255,"name":"Onnetwork Sp. z o.o.","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.onnetwork.tv/pp_services.php"},{"id":203,"name":"Revcontent, LLC","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://intercom.help/revcontent2/en/articles/2290675-revcontent-s-privacy-policy"},{"id":260,"name":"RockYou, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,5],"featureIds":[3],"policyUrl":"https://rockyou.com/privacy-policy/","deletedDate":"2019-08-09T00:00:00Z"},{"id":237,"name":"LKQD, a division of Nexstar Digital, LLC.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.lkqd.com/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":274,"name":"Golden Bees","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.goldenbees.fr/en/privacy-charter/"},{"id":280,"name":"Spot.IM LTD","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.spot.im/privacy/"},{"id":239,"name":"Triton Digital Canada Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.tritondigital.com/privacy-policies"},{"id":177,"name":"plista GmbH","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.plista.com/about/privacy/"},{"id":201,"name":"TimeOne","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://privacy.timeonegroup.com/en/","deletedDate":"2020-05-15T00:00:00Z"},{"id":150,"name":"Inskin Media LTD","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.inskinmedia.com/privacy-policy.html"},{"id":252,"name":"Jaduda GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.jadudamobile.com/datenschutzerklaerung/"},{"id":248,"name":"Converge-Digital","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://converge-digital.com/privacy-policy/"},{"id":161,"name":"Smadex SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://smadex.com/end-user-privacy-policy/"},{"id":285,"name":"Comcast International France SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.freewheel.com/privacy-policy"},{"id":228,"name":"McCann Discipline LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.primis.tech/privacy-policy/"},{"id":299,"name":"AdClear GmbH","purposeIds":[1,5],"legIntPurposeIds":[2,3,4],"featureIds":[1,2],"policyUrl":"https://www.adclear.de/datenschutzerklaerung/"},{"id":277,"name":"Codewise VL Sp. z o.o. Sp. k","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://voluumdsp.com/end-user-privacy-policy/"},{"id":259,"name":"ADYOULIKE SA","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.adyoulike.com/privacy_policy.php"},{"id":272,"name":"A.Mob","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.we-are-adot.com/privacy-policy/"},{"id":230,"name":"Steel House, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://steelhouse.com/privacy-policy/"},{"id":253,"name":"Improve Digital BV","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.improvedigital.com/platform-privacy-policy"},{"id":304,"name":"On Device Research Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://s.on-device.com/privacyPolicy"},{"id":314,"name":"Keymantics","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.keymantics.com/assets/privacy-policy.pdf"},{"id":257,"name":"R-TARGET","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"http://www.r-target.com/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":317,"name":"mainADV Srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.mainad.com/privacy-policy/"},{"id":278,"name":"Integral Ad Science, Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://integralads.com/privacy-policy/"},{"id":291,"name":"Qwertize","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.qwertize.com/en/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":295,"name":"Sojern, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.sojern.com/privacy/product-privacy-policy/"},{"id":315,"name":"Celtra, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.celtra.com/privacy-policy/"},{"id":165,"name":"SpotX, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.spotx.tv/privacy-policy/"},{"id":47,"name":"ADMAN - Phaistos Networks, S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adman.gr/privacy"},{"id":134,"name":"SMARTSTREAM.TV GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://www.smartstream.tv/en/productprivacy"},{"id":325,"name":"Knorex","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.knorex.com/privacy"},{"id":316,"name":"Gamned","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.gamned.com/privacy-policy/"},{"id":318,"name":"Accorp Sp. z o.o.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"http://www.instytut-pollster.pl/privacy-policy/"},{"id":199,"name":"ADUX","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adux.com/donnees-personelles/"},{"id":236,"name":"PowerLinks Media Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[3],"policyUrl":"https://www.powerlinks.com/privacy-policy/"},{"id":294,"name":"Jivox Corporation","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.jivox.com/privacy"},{"id":143,"name":"Connatix Native Exchange Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://connatix.com/privacy-policy/"},{"id":297,"name":"Polar Mobile Group Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://privacy.polar.me"},{"id":319,"name":"Clipcentric, Inc.","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://clipcentric.com/privacy.bhtml"},{"id":290,"name":"Readpeak Oy","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://readpeak.com/privacy-policy/"},{"id":323,"name":"DAZN Media Services Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.goal.com/en-gb/legal/privacy-policy"},{"id":119,"name":"Fusio by S4M","purposeIds":[1,2,5],"legIntPurposeIds":[3],"featureIds":[1,3],"policyUrl":"http://www.s4m.io/privacy-policy/"},{"id":302,"name":"Mobile Professionals BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mobpro.com/privacy.html"},{"id":212,"name":"usemax advertisement (Emego GmbH)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.usemax.de/?l=privacy"},{"id":264,"name":"Adobe Advertising Cloud","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.adobe.com/privacy/experience-cloud.html"},{"id":44,"name":"The ADEX GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://theadex.com/privacy-opt-out/"},{"id":282,"name":"Welect GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.welect.de/datenschutz"},{"id":238,"name":"StackAdapt","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.stackadapt.com/privacy"},{"id":284,"name":"WEBORAMA","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://weborama.com/privacy_en/"},{"id":148,"name":"Liveintent Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://liveintent.com/services-privacy-policy/"},{"id":64,"name":"DigiTrust / IAB Tech Lab","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitru.st/privacy-policy/"},{"id":301,"name":"zeotap GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://zeotap.com/privacy_policy"},{"id":275,"name":"TabMo SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://static.tabmo.io.s3.amazonaws.com/privacy-policy/index.html"},{"id":310,"name":"Adevinta Spain S.L.U.","purposeIds":[],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"https://www.adevinta.com/about/privacy/"},{"id":139,"name":"Permodo GmbH","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://permodo.com/de/privacy.html"},{"id":326,"name":"AdTiming Technology Company Limited","purposeIds":[3,5],"legIntPurposeIds":[1,2,4],"featureIds":[],"policyUrl":"http://www.adtiming.com/en/privacypolicy.html"},{"id":262,"name":"Fyber ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.fyber.com/legal/privacy-policy/"},{"id":331,"name":"ad6media","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.ad6media.fr/privacy"},{"id":345,"name":"The Kantar Group Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.kantar.com/cookies-policies"},{"id":308,"name":"Rockabox Media Ltd","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[],"policyUrl":"http://scoota.com/privacy-policy"},{"id":270,"name":"Marfeel Solutions, SL","purposeIds":[],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.marfeel.com/privacy-policy/"},{"id":333,"name":"InMobi Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":202,"name":"Telaria, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":328,"name":"Gemius SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.gemius.com/cookie-policy.html"},{"id":281,"name":"Wizaly","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.wizaly.com/terms-of-use#privacy-policy"},{"id":354,"name":"Apester Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://apester.com/privacy-policy/"},{"id":320,"name":"Adelphic LLC","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://adelphic.com/platform/privacy/"},{"id":359,"name":"AerServ LLC","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":265,"name":"Instinctive, Inc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://instinctive.io/privacy"},{"id":349,"name":"Optomaton UG","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://optomaton.com/privacy.html"},{"id":288,"name":"Video Media Groep B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://www.videomediagroup.com/wp-content/uploads/2016/01/Privacy-policy-VMG.pdf","deletedDate":"2019-09-17T00:00:00Z"},{"id":266,"name":"Digilant Spain, SLU","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.digilant.com/es/politica-privacidad/"},{"id":339,"name":"Vuble","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vuble.tv/us/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":303,"name":"Orion Semantics","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://static.orion-semantics.com/privacy.html"},{"id":261,"name":"Signal Digital Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.signal.co/privacy-policy/"},{"id":83,"name":"Visarity Technologies GmbH","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://primo.design/docs/PrivacyPolicyPrimo.html"},{"id":343,"name":"DIGITEKA Technologies","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.ultimedia.com/POLICY.html"},{"id":330,"name":"Linicom","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.linicom.com/privacy/","deletedDate":"2020-06-08T00:00:00Z"},{"id":231,"name":"AcuityAds Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.acuityads.com/corporate-privacy-policy.html"},{"id":216,"name":"Mindlytix SAS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://mindlytix.com/privacy/"},{"id":360,"name":"Permutive Technologies, Inc.","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[1,2],"policyUrl":"https://permutive.com/privacy","deletedDate":"2020-03-31T00:00:00Z"},{"id":361,"name":"Permutive","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://permutive.com/privacy"},{"id":311,"name":"Mobfox US LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobfox.com/privacy-policy/"},{"id":358,"name":"MGID Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mgid.com/privacy-policy"},{"id":152,"name":"Meetrics GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.meetrics.com/en/data-privacy/"},{"id":251,"name":"Yieldlove GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"http://www.yieldlove.com/cookie-policy"},{"id":344,"name":"My6sense Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[2,4],"featureIds":[],"policyUrl":"https://my6sense.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":347,"name":"Ezoic Inc.","purposeIds":[2,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.ezoic.com/terms/"},{"id":218,"name":"Bigabid Media ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.bigabid.com/privacy-policy"},{"id":350,"name":"Free Stream Media Corp. dba Samba TV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":351,"name":"Samba TV UK Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":341,"name":"Somo Audience Corp","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"https://somoaudience.com/legal/","deletedDate":"2020-07-06T00:00:00Z"},{"id":380,"name":"Vidoomy Media SL","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"http://vidoomy.com/privacy-policy.html"},{"id":378,"name":"communicationAds GmbH & Co. KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.communicationads.net/aboutus/privacy/"},{"id":369,"name":"Getintent USA, inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://getintent.com/privacy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":184,"name":"mediarithmics SAS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mediarithmics.com/en-us/content/privacy-policy"},{"id":368,"name":"VECTAURY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vectaury.io/en/personal-data"},{"id":373,"name":"Nielsen Marketing Cloud","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"http://www.nielsen.com/us/en/privacy-statement/exelate-privacy-policy.html"},{"id":214,"name":"Digital Control GmbH & Co. KG","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://advolution.de/privacy.php","deletedDate":"2020-05-06T00:00:00Z"},{"id":388,"name":"numberly","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://numberly.com/en/privacy/"},{"id":250,"name":"Qriously Ltd","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.brandwatch.com/legal/qriously-privacy-notice/"},{"id":223,"name":"Audience Trading Platform Ltd.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://atp.io/privacy-policy"},{"id":384,"name":"Pixalate, Inc.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"http://pixalate.com/privacypolicy/","deletedDate":"2019-11-08T00:00:00Z"},{"id":387,"name":"Triapodi Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appreciate.mobi/page.html#/end-user-privacy-policy"},{"id":312,"name":"Exactag GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.exactag.com/en/data-privacy/"},{"id":178,"name":"Hybrid Theory","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://hybridtheory.com/privacy-policy/"},{"id":377,"name":"AddApptr GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.addapptr.com/data-privacy"},{"id":382,"name":"The Reach Group GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://trg.de/en/privacy-statement/"},{"id":206,"name":"Hybrid Adtech GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://hybrid.ai/data_protection_policy"},{"id":403,"name":"Mobusi Mobile Advertising S.L.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobusi.com/privacy.en.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":385,"name":"Oracle Data Cloud","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://www.oracle.com/legal/privacy/marketing-cloud-data-cloud-privacy-policy.html"},{"id":404,"name":"Duplo Media AS","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.easy-ads.com/privacypolicy.htm"},{"id":242,"name":"twiago GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.twiago.com/datenschutz/"},{"id":376,"name":"Pocketmath Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pocketmath.com/privacy-policy"},{"id":402,"name":"Effiliation","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://inter.effiliation.com/politique-confidentialite.html"},{"id":413,"name":"Eulerian Technologies","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.eulerian.com/en/privacy/"},{"id":400,"name":"Whenever Media Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.whenevermedia.com/privacy-policy","deletedDate":"2019-07-29T00:00:00Z"},{"id":171,"name":"Webedia","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webedia-group.com/site/privacy-policy","deletedDate":"2020-07-01T00:00:00Z"},{"id":398,"name":"Yormedia Solutions Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.yormedia.com/privacy-and-cookies-notice/","deletedDate":"2019-08-06T00:00:00Z"},{"id":415,"name":"Seenthis AB","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://seenthis.co/privacy-notice-2018-04-18.pdf"},{"id":263,"name":"Nativo, Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.nativo.com/interest-based-ads"},{"id":329,"name":"Browsi Mobile Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://gobrowsi.com/browsi-privacy-policy/"},{"id":389,"name":"Bidmanagement GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adspert.net/en/privacy/","deletedDate":"2020-07-01T00:00:00Z"},{"id":337,"name":"SheMedia, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shemedia.com/ad-services-privacy-policy"},{"id":422,"name":"Brand Metrics Sweden AB","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://collector.brandmetrics.com/brandmetrics_privacypolicy.pdf"},{"id":421,"name":"LeftsnRight, Inc. dba LIQWID","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liqwid.solutions/privacy-policy","deletedDate":"2020-06-30T00:00:00Z"},{"id":426,"name":"TradeTracker","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[2],"policyUrl":"https://tradetracker.com/privacy-policy/","deletedDate":"2019-08-21T00:00:00Z"},{"id":394,"name":"AudienceProject Aps","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://privacy.audienceproject.com"},{"id":287,"name":"Avazu Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4],"featureIds":[3],"policyUrl":"http://avazuinc.com/opt-out/","deletedDate":"2020-08-03T00:00:00Z"},{"id":243,"name":"Cloud Technologies S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cloudtechnologies.pl/en/internet-advertising-privacy-policy"},{"id":113,"name":"iotec global Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.iotecglobal.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":338,"name":"dunnhumby Germany GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.sociomantic.com/privacy/en/","deletedDate":"2020-07-17T00:00:00Z"},{"id":405,"name":"IgnitionAi Ltd","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[2],"policyUrl":"https://www.isitelab.io/default.aspx","deletedDate":"2020-07-03T00:00:00Z"},{"id":416,"name":"Commanders Act","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.commandersact.com/en/privacy/"},{"id":434,"name":"DynAdmic","purposeIds":[1,3],"legIntPurposeIds":[2,4],"featureIds":[1,3],"policyUrl":"http://eu.dynadmic.com/privacy-policy/"},{"id":435,"name":"SINGLESPOT SAS ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.singlespot.com/privacy_policy?locale=fr"},{"id":409,"name":"Arrivalist Co.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[1,2],"policyUrl":"https://www.arrivalist.com/privacy"},{"id":321,"name":"Ziff Davis LLC","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.ziffdavis.com/privacy-policy"},{"id":436,"name":"INVIBES GROUP","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[1,2,3],"policyUrl":"http://www.invibes.com/terms"},{"id":442,"name":"R-Advertising","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-20T00:00:00Z"},{"id":362,"name":"Myntelligence S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://myntelligence.com/privacy-page/"},{"id":418,"name":"PROXISTORE","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://www.proxistore.com/common/en/cgv"},{"id":449,"name":"Mobile Journey B.V.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://mobilejourney.com/Privacy-Policy","deletedDate":"2019-09-05T00:00:00Z"},{"id":443,"name":"Tradedoubler AB","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-13T00:00:00Z"},{"id":429,"name":"Signals","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://signalsdata.com/platform-cookie-policy/"},{"id":335,"name":"Beachfront Media LLC","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://beachfront.com/privacy-policy/"},{"id":407,"name":"Publishers Internationale Pty Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pi-rate.com.au/privacy.html","deletedDate":"2019-11-08T00:00:00Z"},{"id":427,"name":"Proxi.cloud Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://proxi.cloud/info/privacy-policy/"},{"id":374,"name":"Bmind a Sales Maker Company, S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bmind.es/legal-notice/"},{"id":438,"name":"INVIDI technologies AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.invidi.com/wp-content/uploads/2020/02/ad-tech-services-privacy-policy.pdf"},{"id":450,"name":"Neodata Group srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.neodatagroup.com/en/security-policy"},{"id":452,"name":"Innovid Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.innovid.com/privacy-policy"},{"id":444,"name":"Playbuzz Ltd (aka EX.CO)","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://ex.co/privacy-policy/"},{"id":412,"name":"Cxense ASA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.cxense.com/about-us/privacy-policy"},{"id":454,"name":"Adimo","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://adimo.co/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":455,"name":"GDMServices, Inc. d/b/a FiksuDSP","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://fiksu.com/privacy-policy/"},{"id":298,"name":"Cuebiq Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.cuebiq.com/privacypolicy/","deletedDate":"2019-08-30T00:00:00Z"},{"id":423,"name":"travel audience GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://travelaudience.com/product-privacy-policy/"},{"id":397,"name":"Demandbase, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.demandbase.com/privacy-policy/"},{"id":381,"name":"Solocal","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://frontend.adhslx.com/privacy.html?"},{"id":425,"name":"ADRINO Sp. z o.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.adrino.pl/ciasteczkowa-polityka/","deletedDate":"2019-09-05T00:00:00Z"},{"id":365,"name":"Forensiq LLC","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1,3],"policyUrl":"https://impact.com/privacy-policy/"},{"id":447,"name":"Adludio Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adludio.com/privacy-policy/"},{"id":410,"name":"Adtelligent Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtelligent.com/privacy-policy/"},{"id":137,"name":"Str\u00f6er SSP GmbH (DSP)","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":395,"name":"PREX Programmatic Exchange GmbH&Co KG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[],"policyUrl":"http://www.programmatic-exchange.com/privacy","deletedDate":"2020-07-03T00:00:00Z"},{"id":462,"name":"Bidstack Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[2],"policyUrl":"https://www.bidstack.com/privacy-policy/"},{"id":466,"name":"TACTIC\u2122 Real-Time Marketing AS","purposeIds":[],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://tacticrealtime.com/privacy/"},{"id":340,"name":"Yieldr UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.yieldr.com/privacy"},{"id":336,"name":"Telecoming S.A.","purposeIds":[3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.telecoming.com/privacy-policy/"},{"id":430,"name":"Ad Unity Ltd","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"http://www.adunity.com/privacy-policy.html","deletedDate":"2019-08-13T00:00:00Z"},{"id":346,"name":"Cybba, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://cybba.com/about/legal/data-processing-agreement/","deletedDate":"2020-08-03T00:00:00Z"},{"id":469,"name":"Zeta Global","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://zetaglobal.com/privacy-policy/"},{"id":440,"name":"DEFINE MEDIA GMBH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.definemedia.de/datenschutz-conative/"},{"id":375,"name":"Affle International","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://affle.com/privacy-policy "},{"id":196,"name":"AdElement Media Solutions Pvt Ltd","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"http://adelement.com/privacy-policy.html"},{"id":268,"name":"Social Tokens Ltd. ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://woobi.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":475,"name":"TAPTAP Digital SL","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1,2,3],"policyUrl":"http://www.taptapnetworks.com/privacy_policy/"},{"id":474,"name":"hbfsTech","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.hbfstech.com/fr/privacy.html"},{"id":448,"name":"Targetspot Belgium SPRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://marketing.targetspot.com/Targetspot/Legal/TargetSpot%20Privacy%20Policy%20-%20June%202018.pdf"},{"id":428,"name":"Internet BillBoard a.s.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.ibillboard.com/en/privacy-information/"},{"id":461,"name":"B2B Media Group EMEA GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selfcampaign.com/static/privacy","deletedDate":"2019-08-14T00:00:00Z"},{"id":476,"name":"HIRO Media Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"http://hiro-media.com/privacy.php"},{"id":480,"name":"pilotx.tv","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[1,2,3],"policyUrl":"https://pilotx.tv/privacy/"},{"id":366,"name":"CerebroAd.com s.r.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.cerebroad.com/privacy-policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":392,"name":"Str\u00f6er Mobile Performance GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[3],"policyUrl":"https://stroeermobileperformance.com/?dl=privacy"},{"id":357,"name":"Totaljobs Group Ltd ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.totaljobs.com/privacy-policy"},{"id":486,"name":"Madington","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://delivered-by-madington.com/dat-privacy-policy/"},{"id":468,"name":"NeuStar, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://www.home.neustar/privacy"},{"id":458,"name":"AdColony, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"adcolony.com/privacy-policy/"},{"id":489,"name":"YellowHammer Media Group","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.yhmg.com/privacy-policy/","deletedDate":"2019-11-27T00:00:00Z"},{"id":293,"name":"SpringServe, LLC","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://springserve.com/privacy-policy/"},{"id":484,"name":"STRIATUM SAS","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://adledge.com/data-privacy/"},{"id":493,"name":"Carbon (AI) Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://carbonrmp.com/privacy.html"},{"id":495,"name":"Arcspire Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://public.arcspire.io/privacy.pdf"},{"id":496,"name":"Automattic Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://en.blog.wordpress.com/2017/12/04/updated-privacy-policy/"},{"id":424,"name":"KUPONA GmbH","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.kupona.de/dsgvo/"},{"id":408,"name":"Fidelity Media","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://fidelity-media.com/privacy-policy/"},{"id":473,"name":"Sub2 Technologies Ltd","purposeIds":[3,4,5],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.sub2tech.com/privacy-policy/"},{"id":467,"name":"Haensel AMS GmbH","purposeIds":[3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://haensel-ams.com/data-privacy/"},{"id":490,"name":"PLAYGROUND XYZ EMEA LTD","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://playground.xyz/privacy"},{"id":464,"name":"Oracle AddThis","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.addthis.com/privacy/privacy-policy/","deletedDate":"2020-02-12T00:00:00Z"},{"id":491,"name":"Triboo Data Analytics","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shinystat.com/it/informativa_privacy_generale.html"},{"id":499,"name":"PurposeLab, LLC","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://purposelab.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":502,"name":"NEXD","purposeIds":[5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://nexd.com/privacy-policy"},{"id":465,"name":"Schibsted Product and Tech UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.schibsted.com/","deletedDate":"2019-07-26T00:00:00Z"},{"id":497,"name":"Little Big Data sp.z.o.o.","purposeIds":[1,2,4],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://dtxngr.com/legal/"},{"id":492,"name":"LotaData, Inc.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1],"policyUrl":"https://lotadata.com/privacy_policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":512,"name":"PubNative GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://pubnative.net/privacy-notice/"},{"id":471,"name":"FlexOffers.com, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.flexoffers.com/privacy-policy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":494,"name":"Cablato Limited","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://cablato.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":516,"name":"Pexi B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://pexi.nl/privacy-policy/"},{"id":507,"name":"AdsWizz Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://www.adswizz.com/our-privacy-policy/"},{"id":482,"name":"UberMedia, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ubermedia.com/summary-of-privacy-policy/"},{"id":505,"name":"Shopalyst Inc","purposeIds":[1,2],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shortlyst.com/eu/privacy_terms.html"},{"id":517,"name":"SunMedia ","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2],"policyUrl":"https://www.sunmedia.tv/en/cookies"},{"id":518,"name":"Accelerize Inc.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://getcake.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":511,"name":"Admixer EU GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://admixer.com/privacy/"},{"id":479,"name":"INFINIA MOBILE S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.infiniamobile.com/privacy_policy"},{"id":513,"name":"Shopstyle","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shopstyle.co.uk/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":509,"name":"ATG Ad Tech Group GmbH","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ad-tech-group.com/privacy-policy/"},{"id":521,"name":"netzeffekt GmbH","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.netzeffekt.de/en/imprint"},{"id":487,"name":"nugg.ad GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1],"policyUrl":"https://www.nugg.ad/en/privacy/general-information.html","deletedDate":"2019-10-03T00:00:00Z"},{"id":515,"name":"ZighZag","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zighzag.com/privacy"},{"id":520,"name":"ChannelSight ","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.channelsight.com/privacypolicy/"},{"id":524,"name":"The Ozone Project Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://ozoneproject.com/privacy-policy"},{"id":529,"name":"Fidzup","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.fidzup.com/en/privacy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":528,"name":"Kayzen","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://kayzen.io/data-privacy-policy"},{"id":527,"name":"Jampp LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://jampp.com/privacy.html"},{"id":506,"name":"salesforce.com, inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.salesforce.com/company/privacy/"},{"id":534,"name":"SmartyAds Inc.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://smartyads.com/privacy-policy"},{"id":535,"name":"INNITY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.innity.com/privacy-policy.php"},{"id":514,"name":"Uprival LLC","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://uprival.com/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":522,"name":"Tealium Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://tealium.com/privacy-policy/"},{"id":530,"name":"Near Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://near.co/privacy"},{"id":539,"name":"AdDefend GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.addefend.com/en/privacy-policy/"},{"id":501,"name":"Alliance Gravity Data Media","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.alliancegravity.com/politiquedeprotectiondesdonneespersonnelles"},{"id":519,"name":"Chargeads","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.chargeplatform.com/privacy"},{"id":523,"name":"X-Mode Social, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://xmode.io/privacy-policy.html"},{"id":537,"name":"RUN, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.runads.com/privacy-policy"},{"id":531,"name":"Smartclip Hispania SL","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://rgpd-smartclip.com/"},{"id":536,"name":"GlobalWebIndex","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"http://legal.trendstream.net/non-panellist_privacy_policy"},{"id":542,"name":"Densou Trading Desk ApS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://densou.dk/Policy.html","deletedDate":"2020-01-21T00:00:00Z"},{"id":525,"name":"PUB OCEAN LIMITED","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://rta.pubocean.com/privacy-policy/","deletedDate":"2019-10-03T00:00:00Z"},{"id":544,"name":"Kochava Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://www.kochava.com/support-privacy/"},{"id":543,"name":"PaperG, Inc. dba Thunder Industries","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.makethunder.com/privacy"},{"id":334,"name":"Cydersoft","purposeIds":[],"legIntPurposeIds":[1,2,3,4],"featureIds":[2,3],"policyUrl":"http://www.videmob.com/privacy.html"},{"id":551,"name":"Illuma Technology Limited","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.weareilluma.com/endddd","deletedDate":"2019-11-14T00:00:00Z"},{"id":540,"name":"Tunnl BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://tunnl.com/privacy.html","deletedDate":"2019-12-20T00:00:00Z"},{"id":547,"name":"Video Reach","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.videoreach.de/about/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":546,"name":"Smart Traffik","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://okube-attribution.com/politique-de-confidentialite/"},{"id":541,"name":"DeepIntent, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.deepintent.com/privacypolicy"},{"id":545,"name":"Reignn Platform Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://reignn.com/user-privacy-policy"},{"id":439,"name":"Bit Q Holdings Limited","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.rippll.com/privacy"},{"id":553,"name":"Adhese","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://adhese.com/privacy-and-cookie-policy"},{"id":556,"name":"adhood.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://v3.adhood.com/en/site/politikavekurallar/gizlilik.php?lang=en"},{"id":550,"name":"Happydemics","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.iubenda.com/privacy-policy/69056167/full-legal"},{"id":560,"name":"Leiki Ltd.","purposeIds":[1,2,3],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"http://www.leiki.com/privacy","deletedDate":"2020-01-07T00:00:00Z"},{"id":554,"name":"RMSi Radio Marketing Service interactive GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.rms.de/datenschutz/"},{"id":498,"name":"Dr. Banner","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://drbanner.com/privacypolicy_en/"},{"id":565,"name":"Adobe Audience Manager","purposeIds":[1,2,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adobe.com/privacy/policy.html"},{"id":118,"name":"Drawbridge, Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.drawbridge.com/privacy/","deletedDate":"2020-03-06T00:00:00Z"},{"id":572,"name":"CHEQ AI TECHNOLOGIES LTD.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.cheq.ai/privacy"},{"id":571,"name":"ViewPay","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://viewpay.tv/mentions-legales/"},{"id":568,"name":"Jointag S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.jointag.com/privacy/kariboo/publisher/third/"},{"id":570,"name":"Czech Publisher Exchange z.s.p.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cpex.cz/pro-uzivatele/ochrana-soukromi/"},{"id":559,"name":"Otto (GmbH & Co KG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2],"policyUrl":"https://www.otto.de/shoppages/service/datenschutz"},{"id":548,"name":"LBC France","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.leboncoin.fr/dc/cookies","deletedDate":"2020-04-23T00:00:00Z"},{"id":569,"name":"Kairos Fire","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.kairosfire.com/privacy"},{"id":577,"name":"Neustar on behalf of The Procter & Gamble Company","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pg.com/privacy/english/privacy_statement.shtml"},{"id":590,"name":"Sourcepoint Technologies, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.sourcepoint.com/privacy-policy"},{"id":587,"name":"Localsensor B.V.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.localsensor.com/privacy.html"},{"id":578,"name":"MAIRDUMONT NETLETIX GmbH&Co. KG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mairdumont-netletix.com/datenschutz"},{"id":580,"name":"Goldbach Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://goldbach.com/ch/de/datenschutz"},{"id":593,"name":"Programatica de publicidad S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://datmean.com/politica-privacidad/"},{"id":574,"name":"Realeyes OU","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://realview.realeyesit.com/privacy"},{"id":581,"name":"Mobilewalla, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.mobilewalla.com/business-services-privacy-policy"},{"id":598,"name":"audio content & control GmbH","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://www.audio-cc.com/audiocc_privacy_policy.pdf"},{"id":596,"name":"InsurAds Technologies SA.","purposeIds":[3],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.insurads.com/privacy.html"},{"id":576,"name":"StartApp Inc.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://www.startapp.com/policy/privacy-policy/","deletedDate":"2020-04-23T00:00:00Z"},{"id":592,"name":"Colpirio.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy-policy.colpirio.com/en/","deletedDate":"2020-03-18T00:00:00Z"},{"id":549,"name":"Bandsintown Amplified LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://corp.bandsintown.com/privacy"},{"id":597,"name":"Better Banners A/S","purposeIds":[],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://betterbanners.com/en/privacy"},{"id":601,"name":"WebAds B.V","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.webads.eu/"},{"id":599,"name":"Maximus Live LLC","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://maximusx.com/privacy-policy/"},{"id":604,"name":"Join","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.teamjoin.fr/privacy.html","deletedDate":"2020-04-23T00:00:00Z"},{"id":606,"name":"Impactify ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://impactify.io/privacy-policy/"},{"id":608,"name":"News and Media Holding, a.s.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.newsandmedia.sk/gdpr/"},{"id":602,"name":"Online Solution Int Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://adsafety.net/privacy.html"},{"id":612,"name":"Adnami Aps","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adnami.io/privacy","deletedDate":"2020-03-17T00:00:00Z"},{"id":591,"name":"Consumable, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://consumable.com/privacy-policy.html"},{"id":614,"name":"Market Resource Partners LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.mrpfd.com/privacy-policy/"},{"id":615,"name":"Adsolutions BV","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adsolutions.com/privacy-policy/"},{"id":607,"name":"ucfunnel Co., Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.ucfunnel.com/privacy-policy"},{"id":609,"name":"Predicio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.predic.io/privacy"},{"id":617,"name":"Onfocus (Adagio)","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adagio.io/privacy"},{"id":620,"name":"Blue","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.getblue.io/privacy/"},{"id":610,"name":"Azerion Holding B.V.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://azerion.com/business/privacy.html"},{"id":621,"name":"Seznam.cz, a.s.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://www.seznam.cz/ochranaudaju"},{"id":624,"name":"Norstat AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.norstatpanel.com/en/data-protection"},{"id":623,"name":"Adprime Media Inc. ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adprimehealth.com/privacy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":95,"name":"Lotame Solutions, inc","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[2],"policyUrl":"https://www.lotame.com/about-lotame/privacy/lotame-corporate-websites-privacy-policy/"},{"id":618,"name":"BEINTOO SPA","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.beintoo.com/privacy-cookie-policy/"},{"id":619,"name":"Capitaldata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.capitaldata.fr/privacy"},{"id":625,"name":"BILENDI SA","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.maximiles.com/privacy-policy"},{"id":628,"name":": Tappx","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.tappx.com/en/privacy-policy/"},{"id":626,"name":"Hivestack Inc.","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://hivestack.com/privacy-policy"},{"id":631,"name":"Relay42 Netherlands B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://relay42.com/privacy"},{"id":627,"name":"D-Edge","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.d-edge.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":644,"name":"Gamoshi LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.gamoshi.com/privacy-policy"},{"id":639,"name":"Smile Wanted Group","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.smilewanted.com/privacy.php"},{"id":635,"name":"WebMediaRM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webmediarm.com/vie_privee_et_opposition_en.php"},{"id":579,"name":"Ve Global","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.ve.com/privacy-policy"},{"id":645,"name":"Noster Finance S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.finect.com/terminos-legales/politica-de-cookies"},{"id":653,"name":"Smartme Analytics","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"http://smartmeapp.com/info/smartme/aviso_legal.php","deletedDate":"2020-07-03T00:00:00Z"},{"id":613,"name":"Adserve.zone / Artworx AS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adserve.zone/adserveprivacypolicy.html"},{"id":573,"name":"Dailymotion SA","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2],"policyUrl":"https://www.dailymotion.com/legal/privacy"},{"id":652,"name":"Skaze","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.skaze.fr/rgpd/"},{"id":646,"name":"Notify","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"https://notify-group.com/en/mentions-legales/"},{"id":648,"name":"TrueData Solutions, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.truedata.co/privacy-policy/"},{"id":647,"name":"Axel Springer Teaser Ad GmbH","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://www.adup-tech.com/privacy"},{"id":654,"name":"GRAPHINIUM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.graphinium.com/privacy/"},{"id":659,"name":"Research and Analysis of Media in Sweden AB","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www2.rampanel.com/privacy-policy/"},{"id":656,"name":"Think Clever Media","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.contentignite.com/privacy-policy/"},{"id":504,"name":"Alive & Kicking Global Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mcsaatchiplc.com/legal/privacy-cookies","deletedDate":"2020-07-27T00:00:00Z"},{"id":657,"name":"GP One GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.gsi-one.org/de/privacy-policy.html"},{"id":655,"name":"Sportradar AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sportradar.com/about-us/privacy/"},{"id":662,"name":"SoundCast","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://soundcast.fm/en/data-privacy"},{"id":665,"name":"Digital East GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.digitaleast.mobi/en/legal/privacy-policy/"},{"id":650,"name":"Telefonica Investigaci\u00f3n y Desarrollo S.A.U","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.cognitivemarketing.tid.es/"},{"id":666,"name":"BeOp","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://beop.io/privacy"},{"id":663,"name":"Mobsuccess","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.mobsuccess.com/en/privacy"},{"id":658,"name":"BLIINK SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://bliink.io/privacy-policy"},{"id":667,"name":"Liftoff Mobile, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://liftoff.io/privacy-policy/"},{"id":668,"name":"WhatRocks Inc. ","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.whatrocks.co/en/privacy-policy "},{"id":670,"name":"Timehop, Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.timehop.com/privacy"},{"id":674,"name":"Duration Media, LLC.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.durationmedia.net/privacy-policy"},{"id":675,"name":"Instreamatic inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://instreamatic.com/privacy-policy/"},{"id":676,"name":"BusinessClick","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.businessclick.com/documents/RegulaminProgramuBusinessClick-2019.pdf"},{"id":677,"name":"Intercept Interactive Inc. dba Undertone","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.undertone.com/privacy/"},{"id":660,"name":"Schibsted Norge AS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://static.vg.no/privacy/","deletedDate":"2019-09-16T00:00:00Z"},{"id":673,"name":"TTNET AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.programattik.com/en/privacy-policy.aspx"},{"id":664,"name":"adMarketplace, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.admarketplace.com/privacy-policy/"},{"id":671,"name":"Mediaforce LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://casino.mindthebet.co.uk/themes/mindthebetv2-casino/privacy.php"},{"id":561,"name":"AuDigent","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://audigent.com/platform-privacy-policy"},{"id":682,"name":"Radio Net Media Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.adtonos.com/service-privacy-policy/"},{"id":684,"name":"Blue Billywig BV","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.bluebillywig.com/privacy-statement/"},{"id":686,"name":"The MediaGrid Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.themediagrid.com/privacy-policy/"},{"id":685,"name":"Arkeero","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://arkeero.com/privacy-2/"},{"id":687,"name":"MISSENA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://missena.com/confidentialite/"},{"id":690,"name":"Go.pl sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://go.pl/polityka-prywatnosci/"},{"id":691,"name":"Lifesight Pte. Ltd.","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.lifesight.io/privacy-policy/"},{"id":697,"name":"ADWAYS SAS","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.adways.com/confidentialite/?lang=en"},{"id":681,"name":"MyTraffic","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mytraffic.io/en/privacy"},{"id":649,"name":"adality GmbH","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[1],"policyUrl":"https://adality.de/en/privacy/"},{"id":712,"name":"Inspired Mobile Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://byinspired.com/privacypolicy.pdf"},{"id":688,"name":"Effinity","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.effiliation.com/politique-de-confidentialite/"},{"id":702,"name":"Kwanko","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.kwanko.com/fr/rgpd/"},{"id":715,"name":"BidBerry SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.bidberrymedia.com/privacy-policy/"},{"id":713,"name":"Dataseat Ltd","purposeIds":[2,5],"legIntPurposeIds":[1,3,4],"featureIds":[],"policyUrl":"https://dataseat.com/privacy-policy"},{"id":716,"name":"OnAudience Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.onaudience.com/internet-advertising-privacy-policy"},{"id":708,"name":"Dugout Limited ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://dugout.com/privacy-policy"},{"id":717,"name":"Audience Network","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.en.audiencenetwork.pl/internet-advertising-privacy-policy"},{"id":718,"name":"AppConsent Xchange","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://appconsent.io/en/privacy-policy"},{"id":720,"name":"AAX LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://aax.media/privacy/"},{"id":678,"name":"Axonix LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://axonix.com/privacy-cookie-policy/"},{"id":719,"name":"Online Advertising Network Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.oan.pl/en/privacy-policy"},{"id":707,"name":"Dentsu Aegis Network Italia SpA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.dentsuaegisnetwork.com/it/it/policies/info-cookie"},{"id":721,"name":"Beaconspark Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1],"policyUrl":"https://www.engageya.com/privacy"},{"id":724,"name":"Between Exchange","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"https://en.betweenx.com/pdata.pdf"},{"id":728,"name":"Appier PTE Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.appier.com/privacy-policy/"},{"id":729,"name":"Cavai AS & UK ","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://cav.ai/privacy-policy/"},{"id":723,"name":"Adzymic Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.adzymic.co/privacy"},{"id":737,"name":"Monet Engine Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appmonet.com/privacy-policy/"},{"id":740,"name":"6Sense Insights, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://6sense.com/privacy-policy/"},{"id":744,"name":"Vidazoo Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[2],"policyUrl":"https://vidazoo.gitbook.io/vidazoo-legal/privacy-policy"},{"id":731,"name":"GeistM Technologies LTD","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.geistm.com/privacy"},{"id":741,"name":"Brand Advance Limited","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.wearebrandadvance.com/website-privacy-policy"},{"id":734,"name":"Cint AB","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.cint.com/participant-privacy-notice"},{"id":709,"name":"NC Audience Exchange, LLC (NewsIQ)","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.ncaudienceexchange.com/privacy/"},{"id":739,"name":"Blingby LLC","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://blingby.com/privacy"},{"id":732,"name":"Performax.cz, s.r.o.","purposeIds":[2,4,5],"legIntPurposeIds":[1,3],"featureIds":[2,3],"policyUrl":"https://reg.tiscali.cz/privacy-policy"},{"id":736,"name":"BidMachine Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://explorestack.com/privacy-policy/"},{"id":738,"name":"adbility media GmbH","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adbility-media.com/datenschutzerklaerung/"},{"id":742,"name":"Audiencerate LTD","purposeIds":[],"legIntPurposeIds":[1,2,5],"featureIds":[],"policyUrl":"https://www.audiencerate.com/privacy/"},{"id":743,"name":"MOVIads Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://moviads.pl/polityka-prywatnosci/"},{"id":746,"name":"Adxperience SAS","purposeIds":[2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://adxperience.com/privacy-policy/"},{"id":747,"name":"Kairion GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://kairion.de/datenschutzbestimmungen/"},{"id":748,"name":"AUDIOMOB LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.audiomob.io/privacy"},{"id":749,"name":"Good-Loop Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://doc.good-loop.com/policy/privacy-policy.html"},{"id":754,"name":"DistroScale, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.distroscale.com/privacy-policy/"},{"id":756,"name":"Fandom, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"https://www.fandom.com/privacy-policy"},{"id":758,"name":"GfK Netherlands B.V.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://gfkpanel.nl/privacy"},{"id":759,"name":"RevJet","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.revjet.com/privacy"},{"id":760,"name":"VEXPRO TECHNOLOGIES LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://onedash.com/privacy-policy.html"},{"id":761,"name":"Digiseg ApS","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://digiseg.io/privacy-center/"},{"id":763,"name":"Delidatax SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.delidatax.net/privacy.htm"},{"id":764,"name":"Lucidity","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://golucidity.com/privacy-policy/"},{"id":765,"name":"Grabit Interactive Media Inc dba KERV Interctive","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://kervit.com/privacy-policy/"},{"id":766,"name":"ADCELL | Firstlead GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.adcell.de/agb#sector_6"},{"id":768,"name":"Global Media & Entertainment Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://global.com/privacy-policy/"},{"id":770,"name":"MARKETPERF CORP","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.marketperf.com/assets/images/app/marketperf/pdf/privacy-policy.pdf"},{"id":773,"name":"360e-com Sp. z o.o.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.clickonometrics.com/optout/"},{"id":775,"name":"SelectMedia International LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selectmedia.asia/terms-and-privacy/"},{"id":778,"name":"Discover-Tech ltd","purposeIds":[2,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://discover-tech.io/dsp-privacy-policy/"},{"id":779,"name":"Adtarget Medya A.S.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtarget.com.tr/adtarget-privacy-policy-2020.pdf"},{"id":780,"name":"Aniview LTD","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.aniview.com/privacy-policy/"},{"id":781,"name":"FeedAd GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://feedad.com/privacy/"},{"id":784,"name":"Nubo LTD","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.recod3.com/privacypolicy.php"},{"id":786,"name":"TargetVideo GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.target-video.com/datenschutz/"},{"id":798,"name":"Adverticum cPlc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://adverticum.net/english/privacy-and-data-processing-information/"},{"id":803,"name":"Click Tech Limited","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[1],"policyUrl":"https://en.yeahmobi.com/html/privacypolicy/"}]} \ No newline at end of file From b5f89336da09398de6d723c026825ad4bcf1afba Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Wed, 12 Aug 2020 12:37:43 -0400 Subject: [PATCH 162/381] update to the latest go-gdpr release (#1436) --- go.mod | 2 +- go.sum | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a5b5a161cf4..108e383e743 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,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.8.2 + github.com/prebid/go-gdpr v0.8.3 github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 diff --git a/go.sum b/go.sum index 1ddab71332a..6da3f8898ba 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLM github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/buger/jsonparser v0.0.0-20180318095312-2cac668e8456/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 h1:y853v6rXx+zefEcjET3JuKAqvhj+FKflQijjeaSv2iA= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cespare/xxhash v1.0.0 h1:naDmySfoNg0nKS62/ujM6e71ZgM2AoVdaqGwMG0w18A= @@ -77,8 +78,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.8.2 h1:mN2jKYZZpJkCYFQB/nDTJoPpuGYblOYP2UUzOzRggII= -github.com/prebid/go-gdpr v0.8.2/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= +github.com/prebid/go-gdpr v0.8.3 h1:rjCZNV0AdKygiGHpVhNB42usjEpTN3qidXUPB1yarb0= +github.com/prebid/go-gdpr v0.8.3/go.mod h1:TGzgqQDGKOVUkbqmY25K4uvcwMywSddXEaY4zUFiVBQ= github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtWKsUFH5IzdQxHIgP609VSIVe5Hywg2phs= github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98= github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed h1:0dloFFFNNDG7c+8qtkYw2FdADrWy9s5cI8wHp6tK3Mg= From 48c865cb0bc7596b6fdb001f4b992b519fa45575 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Wed, 12 Aug 2020 10:07:58 -0700 Subject: [PATCH 163/381] Video endpoint bid selection enhancements (#1419) Co-authored-by: Veronika Solovei --- endpoints/openrtb2/video_auction.go | 18 +++- endpoints/openrtb2/video_auction_test.go | 29 +++-- exchange/auction.go | 2 +- .../impcustomcachekeytest/multiImpVideo.json | 101 ++++++++++++++++++ .../multiImpVideoNoIncludeBidderKeys.json | 86 +++++++++++++++ exchange/targeting.go | 4 +- exchange/targeting_test.go | 6 +- 7 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 exchange/impcustomcachekeytest/multiImpVideo.json create mode 100644 exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 18678be541c..49ba287610b 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -470,7 +470,7 @@ func buildVideoResponse(bidresponse *openrtb.BidResponse, podErrors []PodError) if err := json.Unmarshal(bid.Ext, &tempRespBidExt); err != nil { return nil, err } - if tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbVastCacheKey)] == "" { + if tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)] == "" { continue } @@ -479,9 +479,9 @@ func buildVideoResponse(bidresponse *openrtb.BidResponse, podErrors []PodError) podId, _ := strconv.ParseInt(podNum, 0, 64) videoTargeting := openrtb_ext.VideoTargeting{ - HbPb: tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbpbConstantKey)], - HbPbCatDur: tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbCategoryDurationKey)], - HbCacheID: tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbVastCacheKey)], + HbPb: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbpbConstantKey, seatBid.Seat)], + HbPbCatDur: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbCategoryDurationKey, seatBid.Seat)], + HbCacheID: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)], } adPod := findAdPod(podId, adPods) @@ -519,6 +519,14 @@ func buildVideoResponse(bidresponse *openrtb.BidResponse, podErrors []PodError) return &openrtb_ext.BidResponseVideo{AdPods: adPods}, nil } +func formatTargetingKey(key openrtb_ext.TargetingKey, bidderName string) string { + fullKey := fmt.Sprintf("%s_%s", string(key), bidderName) + if len(fullKey) > exchange.MaxKeyLength { + return string(fullKey[0:exchange.MaxKeyLength]) + } + return fullKey +} + func findAdPod(podInd int64, pods []*openrtb_ext.AdPod) *openrtb_ext.AdPod { for _, pod := range pods { if pod.PodId == podInd { @@ -623,9 +631,9 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro targeting := openrtb_ext.ExtRequestTargeting{ PriceGranularity: priceGranularity, - IncludeWinners: true, IncludeBrandCategory: inclBrandCat, DurationRangeSec: durationRangeSec, + IncludeBidderKeys: true, } vastXml := openrtb_ext.ExtRequestPrebidCacheVAST{} diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index f29ac3bfed9..b15c6a7b47a 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -81,6 +81,10 @@ func TestVideoEndpointImpressionsDuration(t *testing.T) { t.Fatalf("The request never made it into the Exchange.") } + var extData openrtb_ext.ExtRequest + json.Unmarshal(ex.lastRequest.Ext, &extData) + assert.True(t, extData.Prebid.Targeting.IncludeBidderKeys, "Request ext incorrect: IncludeBidderKeys should be true ") + assert.Len(t, ex.lastRequest.Imp, 22, "Incorrect number of impressions in request") assert.Equal(t, ex.lastRequest.Imp[0].ID, "1_0", "Incorrect impression id in request") assert.Equal(t, ex.lastRequest.Imp[0].Video.MaxDuration, int64(15), "Incorrect impression max duration in request") @@ -643,9 +647,9 @@ func TestVideoBuildVideoResponseMissedCacheForOneBid(t *testing.T) { bid2 := openrtb.Bid{} bid3 := openrtb.Bid{} - extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_123_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) - extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_456_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) - extBid3 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_406_30s","hb_size":"1x1"}}}`) + extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_123_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_456_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid3 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_406_30s","hb_size":"1x1"}}}`) bid1.Ext = extBid1 bids = append(bids, bid1) @@ -657,6 +661,7 @@ func TestVideoBuildVideoResponseMissedCacheForOneBid(t *testing.T) { bids = append(bids, bid3) seatBid.Bid = bids + seatBid.Seat = "appnexus" seatBids = append(seatBids, seatBid) openRtbBidResp.SeatBid = seatBids @@ -713,8 +718,8 @@ func TestVideoBuildVideoResponsePodErrors(t *testing.T) { bid1 := openrtb.Bid{} bid2 := openrtb.Bid{} - extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_123_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) - extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_456_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_123_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_456_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) bid1.Ext = extBid1 bids = append(bids, bid1) @@ -723,6 +728,7 @@ func TestVideoBuildVideoResponsePodErrors(t *testing.T) { bids = append(bids, bid2) seatBid.Bid = bids + seatBid.Seat = "appnexus" seatBids = append(seatBids, seatBid) openRtbBidResp.SeatBid = seatBids @@ -1107,6 +1113,16 @@ func TestCCPA(t *testing.T) { } } +func TestFormatTargetingKey(t *testing.T) { + res := formatTargetingKey(openrtb_ext.HbCategoryDurationKey, "appnexus") + assert.Equal(t, "hb_pb_cat_dur_appnex", res, "Tergeting key constructed incorrectly") +} + +func TestFormatTargetingKeyLongKey(t *testing.T) { + res := formatTargetingKey(openrtb_ext.HbpbConstantKey, "20.00") + assert.Equal(t, "hb_pb_20.00", res, "Tergeting key constructed incorrectly") +} + func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) mockModule := &mockAnalyticsModule{} @@ -1205,9 +1221,10 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb 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"]}}}}}`) + ext := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"20.00","hb_pb_cat_dur_appnex":"20.00_395_30s","hb_size":"1x1", "hb_uuid_appnexus":"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{{ + Seat: "appnexus", Bid: []openrtb.Bid{ {ID: "01", ImpID: "1_0", Ext: ext}, {ID: "02", ImpID: "1_1", Ext: ext}, diff --git a/exchange/auction.go b/exchange/auction.go index dcb73708df7..45e1422540e 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -130,7 +130,7 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, var customCacheKey string var catDur string useCustomCacheKey := false - if competitiveExclusion && isOverallWinner { + if competitiveExclusion && isOverallWinner || includeBidderKeys { // set custom cache key for winning bid when competitive exclusion applies catDur = bidCategory[topBidPerBidder.bid.ID] if len(catDur) > 0 { diff --git a/exchange/impcustomcachekeytest/multiImpVideo.json b/exchange/impcustomcachekeytest/multiImpVideo.json new file mode 100644 index 00000000000..7c1d7af02ea --- /dev/null +++ b/exchange/impcustomcachekeytest/multiImpVideo.json @@ -0,0 +1,101 @@ +{ + "bidRequest": { + "imp": [{ + "id": "1_0" + }, + { + "id": "1_1" + } + ] + }, + "pbsBids": [{ + "bid": { + "id": "apn_1_0", + "impid": "1_0", + "price": 12.00, + "nurl": "http://apn_1_0.com", + "cat": ["12.00_sports_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_0", + "impid": "1_0", + "price": 20.00, + "nurl": "http://spotx_1_0.com", + "cat": ["20_news_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "apn_1_1", + "impid": "1_1", + "price": 18.00, + "nurl": "http://apn_1_1.com", + "cat": ["18_furniture_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_1", + "impid": "1_1", + "price": 17.00, + "nurl": "http://spotx_1_1.com", + "cat": ["17_auto_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "rubicon_1_1", + "impid": "1_1", + "price": 17.50, + "nurl": "http://rubicon_1_1.com", + "cat": ["17_music_30s"] + }, + "bidType": "video", + "bidder": "rubicon" + }], + "expectedCacheables": [{ + "Type": "xml", + "TTLSeconds": 3660, + "Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "Key": "12.00_sports_30s" + }, { + "Type": "xml", + "TTLSeconds": 3660, + "Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "Key": "20_news_30s" + }, { + "Type": "xml", + "TTLSeconds": 3660, + "Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "Key": "18_furniture_30s" + }, + { + "Type": "xml", + "TTLSeconds": 3660, + "Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "Key": "17_auto_30s" + }, + { + "Type": "xml", + "TTLSeconds": 3660, + "Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://rubicon_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "Key": "17_music_30s" + } + ], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners": false, + "targetDataIncludeBidderKeys": true, + "targetDataIncludeCacheBids": false, + "targetDataIncludeCacheVast": true +} diff --git a/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json b/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json new file mode 100644 index 00000000000..0b8ee1415e3 --- /dev/null +++ b/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json @@ -0,0 +1,86 @@ +{ + "bidRequest": { + "imp": [{ + "id": "1_0" + }, + { + "id": "1_1" + } + ] + }, + "pbsBids": [{ + "bid": { + "id": "apn_1_0", + "impid": "1_0", + "price": 12.00, + "nurl": "http://apn_1_0.com", + "cat": ["12.00_sports_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_0", + "impid": "1_0", + "price": 20.00, + "nurl": "http://spotx_1_0.com", + "cat": ["20_news_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "apn_1_1", + "impid": "1_1", + "price": 18.00, + "nurl": "http://apn_1_1.com", + "cat": ["18_furniture_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_1", + "impid": "1_1", + "price": 17.00, + "nurl": "http://spotx_1_1.com", + "cat": ["17_auto_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "rubicon_1_1", + "impid": "1_1", + "price": 17.50, + "nurl": "http://rubicon_1_1.com", + "cat": ["17_music_30s"] + }, + "bidType": "video", + "bidder": "rubicon" + }], + "expectedCacheables": [ + { + "Type": "xml", + "TTLSeconds": 3660, + "Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "Key": "20_news_30s" + }, + { + "Type": "xml", + "TTLSeconds": 3660, + "Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "Key": "18_furniture_30s" + } + ], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners": true, + "targetDataIncludeBidderKeys": false, + "targetDataIncludeCacheBids": false, + "targetDataIncludeCacheVast": true +} diff --git a/exchange/targeting.go b/exchange/targeting.go index dca57653b44..47bfeb655fe 100644 --- a/exchange/targeting.go +++ b/exchange/targeting.go @@ -7,7 +7,7 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -const maxKeyLength = 20 +const MaxKeyLength = 20 // targetData tracks information about the winning Bid in each Imp. // @@ -83,7 +83,7 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool) { if targData.includeBidderKeys { - keys[key.BidderKey(bidderName, maxKeyLength)] = value + keys[key.BidderKey(bidderName, MaxKeyLength)] = value } if targData.includeWinners && overallWinner { keys[string(key)] = value diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index 11b60db01f3..284d56be42e 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -50,13 +50,13 @@ func TestTargetingCache(t *testing.T) { // Make sure that the cache keys exist on the bids where they're expected to assertKeyExists(t, bids["winning-bid"], string(openrtb_ext.HbCacheKey), true) - assertKeyExists(t, bids["winning-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, maxKeyLength), true) + assertKeyExists(t, bids["winning-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, MaxKeyLength), true) assertKeyExists(t, bids["contending-bid"], string(openrtb_ext.HbCacheKey), false) - assertKeyExists(t, bids["contending-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderRubicon, maxKeyLength), true) + assertKeyExists(t, bids["contending-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderRubicon, MaxKeyLength), true) assertKeyExists(t, bids["losing-bid"], string(openrtb_ext.HbCacheKey), false) - assertKeyExists(t, bids["losing-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, maxKeyLength), false) + assertKeyExists(t, bids["losing-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, MaxKeyLength), false) //assert hb_cache_host was included assert.Contains(t, string(bids["winning-bid"].Ext), string(openrtb_ext.HbConstantCacheHostKey)) From cce496720f13d30d45154ca7d80e81884fb9a1ac Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Wed, 12 Aug 2020 10:19:18 -0700 Subject: [PATCH 164/381] [WIP] Bid deduplication enhancement (#1430) Co-authored-by: Veronika Solovei --- exchange/exchange.go | 29 ++++++++++-- exchange/exchange_test.go | 96 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/exchange/exchange.go b/exchange/exchange.go index ad591f57794..57e13644163 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -10,6 +10,7 @@ import ( "net/http" "runtime/debug" "sort" + "strconv" "strings" "time" @@ -484,6 +485,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques bidderName openrtb_ext.BidderName bidIndex int bidID string + bidPrice string } dedupe := make(map[string]bidDedupe) @@ -580,15 +582,34 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques } var categoryDuration string + var dupeKey string if brandCatExt.WithCategory { categoryDuration = fmt.Sprintf("%s_%s_%ds", pb, category, newDur) + dupeKey = category } else { categoryDuration = fmt.Sprintf("%s_%ds", pb, newDur) + dupeKey = categoryDuration } - if dupe, ok := dedupe[categoryDuration]; ok { - // 50% chance for either bid with duplicate categoryDuration values to be kept - if rand.Intn(100) < 50 { + if dupe, ok := dedupe[dupeKey]; ok { + + dupeBidPrice, err := strconv.ParseFloat(dupe.bidPrice, 64) + if err != nil { + dupeBidPrice = 0 + } + currBidPrice, err := strconv.ParseFloat(pb, 64) + if err != nil { + currBidPrice = 0 + } + if dupeBidPrice == currBidPrice { + if rand.Intn(100) < 50 { + dupeBidPrice = -1 + } else { + currBidPrice = -1 + } + } + + if dupeBidPrice < currBidPrice { if dupe.bidderName == bidderName { // An older bid from the current bidder bidsToRemove = append(bidsToRemove, dupe.bidIndex) @@ -612,7 +633,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques } } res[bidID] = categoryDuration - dedupe[categoryDuration] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID} + dedupe[dupeKey] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID, bidPrice: pb} } if len(bidsToRemove) > 0 { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 7da7b62e70b..5fbdb1c57a9 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -1352,19 +1352,22 @@ func TestCategoryDedupe(t *testing.T) { cats4 := []string{"IAB1-2000"} bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 15.0000, Cat: cats2, W: 1, H: 1} - bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.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} + bid5 := openrtb.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: cats1, W: 1, H: 1} 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} + bid1_5 := pbsOrtbBid{&bid5, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} selectedBids := make(map[string]int) expectedCategories := map[string]string{ "bid_id1": "10.00_Electronics_30s", "bid_id2": "14.00_Sports_50s", - "bid_id3": "10.00_Electronics_30s", + "bid_id3": "20.00_Electronics_30s", + "bid_id5": "20.00_Electronics_30s", } numIterations := 10 @@ -1378,6 +1381,7 @@ func TestCategoryDedupe(t *testing.T) { &bid1_2, &bid1_3, &bid1_4, + &bid1_5, } seatBid := pbsOrtbSeatBid{innerBids, "USD", nil, nil} @@ -1388,7 +1392,7 @@ func TestCategoryDedupe(t *testing.T) { 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.Equal(t, 3, 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") @@ -1401,8 +1405,90 @@ func TestCategoryDedupe(t *testing.T) { } assert.Equal(t, numIterations, selectedBids["bid_id2"], "Bid 2 did not make it through every time") - assert.NotEqual(t, numIterations, selectedBids["bid_id1"], "Bid 1 made it through every time") - assert.NotEqual(t, numIterations, selectedBids["bid_id3"], "Bid 3 made it through every time") + assert.Equal(t, 0, selectedBids["bid_id1"], "Bid 1 should be rejected on every iteration due to lower price") + assert.NotEqual(t, 0, selectedBids["bid_id3"], "Bid 3 should be accepted at least once") + assert.NotEqual(t, 0, selectedBids["bid_id5"], "Bid 5 should be accepted at least once") +} + +func TestNoCategoryDedupe(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + requestExt := newExtRequestNoBrandCat() + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-3"} + cats2 := []string{"IAB1-4"} + cats4 := []string{"IAB1-2000"} + bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 14.0000, Cat: cats1, W: 1, H: 1} + bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 14.0000, Cat: cats2, W: 1, H: 1} + bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.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} + bid5 := openrtb.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 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} + bid1_5 := pbsOrtbBid{&bid5, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + + selectedBids := make(map[string]int) + expectedCategories := map[string]string{ + "bid_id1": "14.00_30s", + "bid_id2": "14.00_30s", + "bid_id3": "20.00_30s", + "bid_id4": "20.00_30s", + "bid_id5": "10.00_30s", + } + + numIterations := 10 + + // Run the function many times, this should be enough for the 50% chance of which bid to remove to remove bid1 sometimes + // and bid3 others. It's conceivably possible (but highly unlikely) that the same bid get chosen every single time, but + // if you notice false fails from this test increase numIterations to make it even less likely to happen. + for i := 0; i < numIterations; i++ { + innerBids := []*pbsOrtbBid{ + &bid1_1, + &bid1_2, + &bid1_3, + &bid1_4, + &bid1_5, + } + + seatBid := pbsOrtbSeatBid{innerBids, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("appnexus") + + adapterBids[bidderName1] = &seatBid + + 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|2)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected") + assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_id(3|4)\] reason: Bid was deduplicated`), rejections[1], "Rejection message did not match expected") + assert.Equal(t, 3, len(adapterBids[bidderName1].bids), "Bidders number doesn't match") + assert.Equal(t, 3, len(bidCategory), "Bidders category mapping doesn't match") + + for bidId, bidCat := range bidCategory { + assert.Equal(t, expectedCategories[bidId], bidCat, "Category mapping doesn't match") + selectedBids[bidId]++ + } + } + assert.Equal(t, numIterations, selectedBids["bid_id5"], "Bid 5 did not make it through every time") + assert.NotEqual(t, 0, selectedBids["bid_id1"], "Bid 1 should be selected at least once") + assert.NotEqual(t, 0, selectedBids["bid_id2"], "Bid 2 should be selected at least once") + assert.NotEqual(t, 0, selectedBids["bid_id1"], "Bid 3 should be selected at least once") + assert.NotEqual(t, 0, selectedBids["bid_id4"], "Bid 4 should be selected at least once") + } func TestBidRejectionErrors(t *testing.T) { From 346617b0d08623e7976f18a63db28d2f8bd8bd1c Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 12 Aug 2020 13:58:40 -0400 Subject: [PATCH 165/381] Refactor rate converter separating scheduler from converter logic to improve testability (#1394) --- currencies/converter_info.go | 15 +- currencies/rate_converter.go | 120 +--- currencies/rate_converter_test.go | 676 ++++++------------- currencies/rates_test.go | 1 + endpoints/currency_rates.go | 7 +- endpoints/currency_rates_test.go | 51 +- endpoints/openrtb2/auction_benchmark_test.go | 3 +- exchange/bidder_test.go | 18 +- exchange/exchange_test.go | 25 +- exchange/legacy_test.go | 12 +- exchange/targeting_test.go | 2 +- main.go | 9 +- router/admin.go | 5 +- util/task/ticker_task.go | 53 ++ util/task/ticker_task_test.go | 63 ++ util/timeutil/time.go | 16 + 16 files changed, 431 insertions(+), 645 deletions(-) create mode 100644 util/task/ticker_task.go create mode 100644 util/task/ticker_task_test.go create mode 100644 util/timeutil/time.go diff --git a/currencies/converter_info.go b/currencies/converter_info.go index 6f4fda64c09..abbcde29fbc 100644 --- a/currencies/converter_info.go +++ b/currencies/converter_info.go @@ -5,18 +5,16 @@ import "time" // ConverterInfo holds information about converter setup type ConverterInfo interface { Source() string - FetchingInterval() time.Duration LastUpdated() time.Time Rates() *map[string]map[string]float64 AdditionalInfo() interface{} } type converterInfo struct { - source string - fetchingInterval time.Duration - lastUpdated time.Time - rates *map[string]map[string]float64 - additionalInfo interface{} + source string + lastUpdated time.Time + rates *map[string]map[string]float64 + additionalInfo interface{} } // Source returns converter's URL source @@ -24,11 +22,6 @@ func (ci converterInfo) Source() string { return ci.source } -// FetchingInterval returns converter's fetching interval in nanoseconds -func (ci converterInfo) FetchingInterval() time.Duration { - return ci.fetchingInterval -} - // LastUpdated returns converter's last updated time func (ci converterInfo) LastUpdated() time.Time { return ci.lastUpdated diff --git a/currencies/rate_converter.go b/currencies/rate_converter.go index d22f347b17c..f9ae67436f9 100644 --- a/currencies/rate_converter.go +++ b/currencies/rate_converter.go @@ -2,83 +2,43 @@ package currencies import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "sync/atomic" "time" "github.com/golang/glog" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/util/timeutil" ) // RateConverter holds the currencies conversion rates dictionary type RateConverter struct { httpClient httpClient - done chan bool - updateNotifier chan<- int - fetchingInterval time.Duration staleRatesThreshold time.Duration syncSourceURL string rates atomic.Value // Should only hold Rates struct lastUpdated atomic.Value // Should only hold time.Time constantRates Conversions + time timeutil.Time } // NewRateConverter returns a new RateConverter func NewRateConverter( httpClient httpClient, syncSourceURL string, - fetchingInterval time.Duration, staleRatesThreshold time.Duration, ) *RateConverter { - return NewRateConverterWithNotifier( - httpClient, - syncSourceURL, - fetchingInterval, - staleRatesThreshold, - nil, // no notifier channel specified, won't send any notifications - ) -} - -// NewRateConverterDefault returns a RateConverter with default values. -// By default there will be no currencies conversions done. -// `currencies.ConstantRate` will be used. -func NewRateConverterDefault() *RateConverter { - return NewRateConverter(&http.Client{}, "", time.Duration(0), time.Duration(0)) -} - -// NewRateConverterWithNotifier returns a new RateConverter -// it allow to pass an update chan in which the number of ticks will be passed after each tick -// allowing clients to listen on updates -// Do not use this method -func NewRateConverterWithNotifier( - httpClient httpClient, - syncSourceURL string, - fetchingInterval time.Duration, - staleRatesThreshold time.Duration, - updateNotifier chan<- int, -) *RateConverter { - rc := &RateConverter{ + return &RateConverter{ httpClient: httpClient, - done: make(chan bool), - updateNotifier: updateNotifier, - fetchingInterval: fetchingInterval, staleRatesThreshold: staleRatesThreshold, syncSourceURL: syncSourceURL, rates: atomic.Value{}, lastUpdated: atomic.Value{}, constantRates: NewConstantRates(), + time: &timeutil.RealTime{}, } - - // In case host do not want to support currency lookup - // we just stop here and do nothing - if rc.fetchingInterval == time.Duration(0) { - return rc - } - - rc.Update() // Make sure to populate data before to return the converter - go rc.startPeriodicFetching() // Start periodic ticking - - return rc } // fetch allows to retrieve the currencies rates from the syncSourceURL provided @@ -93,6 +53,11 @@ func (rc *RateConverter) fetch() (*Rates, error) { return nil, err } + if response.StatusCode >= 400 { + message := fmt.Sprintf("The currency rates request failed with status code %d", response.StatusCode) + return nil, &errortypes.BadServerResponse{Message: message} + } + defer response.Body.Close() bytesJSON, err := ioutil.ReadAll(response.Body) @@ -110,14 +75,14 @@ func (rc *RateConverter) fetch() (*Rates, error) { } // Update updates the internal currencies rates from remote sources -func (rc *RateConverter) Update() error { +func (rc *RateConverter) update() error { rates, err := rc.fetch() if err == nil { rc.rates.Store(rates) - rc.lastUpdated.Store(time.Now()) + rc.lastUpdated.Store(rc.time.Now()) } else { - if rc.CheckStaleRates() { - rc.ClearRates() + if rc.checkStaleRates() { + rc.clearRates() glog.Errorf("Error updating conversion rates, falling back to constant rates: %v", err) } else { glog.Errorf("Error updating conversion rates: %v", err) @@ -127,37 +92,8 @@ func (rc *RateConverter) Update() error { return err } -// startPeriodicFetching starts the periodic fetching at the given interval -// triggers a first fetch when called before the first tick happen in order to initialize currencies rates map -// returns a chan in which the number of data updates everytime a new update was done -func (rc *RateConverter) startPeriodicFetching() { - - ticker := time.NewTicker(rc.fetchingInterval) - updatesTicksCount := 0 - - for { - select { - case <-ticker.C: - // Retries are handled by clients directly. - rc.Update() - updatesTicksCount++ - if rc.updateNotifier != nil { - rc.updateNotifier <- updatesTicksCount - } - case <-rc.done: - if ticker != nil { - ticker.Stop() - ticker = nil - } - return - } - } -} - -// StopPeriodicFetching stops the periodic fetching while keeping the latest currencies rates map -func (rc *RateConverter) StopPeriodicFetching() { - rc.done <- true - close(rc.done) +func (rc *RateConverter) Run() error { + return rc.update() } // LastUpdated returns time when currencies rates were updated @@ -178,18 +114,19 @@ func (rc *RateConverter) Rates() Conversions { return rc.constantRates } -// ClearRates sets the rates to nil -func (rc *RateConverter) ClearRates() { +// clearRates sets the rates to nil +func (rc *RateConverter) clearRates() { // atomic.Value field rates must be of type *Rates so we cast nil to that type rc.rates.Store((*Rates)(nil)) } -// CheckStaleRates checks if loaded third party conversion rates are stale -func (rc *RateConverter) CheckStaleRates() bool { +// checkStaleRates checks if loaded third party conversion rates are stale +func (rc *RateConverter) checkStaleRates() bool { if rc.staleRatesThreshold <= 0 { return false } - currentTime := time.Now().UTC() + + currentTime := rc.time.Now().UTC() if lastUpdated := rc.lastUpdated.Load(); lastUpdated != nil { delta := currentTime.Sub(lastUpdated.(time.Time).UTC()) if delta.Seconds() > rc.staleRatesThreshold.Seconds() { @@ -202,14 +139,11 @@ func (rc *RateConverter) CheckStaleRates() bool { // 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() - } + rates = rc.Rates().GetRates() return converterInfo{ - source: rc.syncSourceURL, - fetchingInterval: rc.fetchingInterval, - lastUpdated: rc.LastUpdated(), - rates: rates, + source: rc.syncSourceURL, + lastUpdated: rc.LastUpdated(), + rates: rates, } } diff --git a/currencies/rate_converter_test.go b/currencies/rate_converter_test.go index d717d1a3f9c..7beb9523d28 100644 --- a/currencies/rate_converter_test.go +++ b/currencies/rate_converter_test.go @@ -1,4 +1,4 @@ -package currencies_test +package currencies import ( "io/ioutil" @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/prebid/prebid-server/currencies" + "github.com/prebid/prebid-server/util/task" "github.com/stretchr/testify/assert" ) @@ -27,423 +27,148 @@ func getMockRates() []byte { }`) } -func TestFetch_Success(t *testing.T) { - - // Setup: - calledURLs := []string{} - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - rw.WriteHeader(http.StatusOK) - rw.Write([]byte(getMockRates())) - }), - ) - - defer mockedHttpServer.Close() - - expectedRates := ¤cies.Rates{ - DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), - Conversions: map[string]map[string]float64{ - "USD": { - "GBP": 0.77208, - }, - "GBP": { - "USD": 1.2952, - }, - }, - } - - // Execute: - beforeExecution := time.Now() - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(24)*time.Hour, - time.Duration(24)*time.Hour, - ) - - // Verify: - assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs)) - assert.NotEqual(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() should return a time set") - assert.True(t, currencyConverter.LastUpdated().After(beforeExecution), "LastUpdated() should be after last update") - 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) { - - // Setup: - calledURLs := []string{} - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - rw.WriteHeader(http.StatusNotFound) - }), - ) - - defer mockedHttpServer.Close() - - // Execute: - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(24)*time.Hour, - time.Duration(24)*time.Hour, - ) - - // Verify: - 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") - assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") +// FakeTime implements the Time interface +type FakeTime struct { + time time.Time } -func TestFetch_FailErrorHttpClient(t *testing.T) { - - // Setup: - calledURLs := []string{} - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - rw.WriteHeader(http.StatusBadRequest) - }), - ) - - defer mockedHttpServer.Close() - - // Execute: - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(24)*time.Hour, - time.Duration(24)*time.Hour, - ) - - // Verify: - 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") - assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") +func (mc *FakeTime) Now() time.Time { + return mc.time } -func TestFetch_FailBadSyncURL(t *testing.T) { - - // Setup: - - // Execute: - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - "justaweirdurl", - time.Duration(24)*time.Hour, - time.Duration(24)*time.Hour, - ) - - // Verify: - assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set") - assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") - assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") -} - -func TestFetch_FailBadJSON(t *testing.T) { - - // Setup: - calledURLs := []string{} - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - rw.WriteHeader(http.StatusOK) - rw.Write([]byte( - `{ - "dataAsOf":"2018-09-12", - "conversions":{ - "USD":{ - "GBP":0.77208 - }, - "GBP":{ - "USD":1.2952 - }, - "badJsonHere" - } - }`, - )) - }), - ) - - defer mockedHttpServer.Close() - - // Execute: - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(24)*time.Hour, - time.Duration(24)*time.Hour, - ) - - // Verify: - 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") - assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") -} - -func TestFetch_InvalidRemoteResponseContent(t *testing.T) { - - // Setup: - calledURLs := []string{} - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - rw.WriteHeader(http.StatusOK) - rw.Write(nil) - }), - ) - - defer mockedHttpServer.Close() - - // Execute: - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(24)*time.Hour, - time.Duration(24)*time.Hour, - ) - - // Verify: - 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.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") - assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") -} - -func TestInit(t *testing.T) { - - // Setup: - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write([]byte(getMockRates())) - }), - ) - - // Execute: - expectedTicks := 5 - ticksTimes := []time.Time{} - ticks := make(chan int) - currencyConverter := currencies.NewRateConverterWithNotifier( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(100)*time.Millisecond, - time.Duration(24)*time.Hour, - ticks, - ) +func TestReadWriteRates(t *testing.T) { + // Setup + mockServerHandler := func(mockResponse []byte, mockStatus int) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(mockStatus) + rw.Write([]byte(mockResponse)) + }) + } - // Verify: - expectedIntervalDuration := time.Duration(100) * time.Millisecond - errorMargin := 0.1 // 10% error margin - expectedRates := ¤cies.Rates{ - DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), - Conversions: map[string]map[string]float64{ - "USD": { - "GBP": 0.77208, - }, - "GBP": { - "USD": 1.2952, - }, + tests := []struct { + description string + giveFakeTime time.Time + giveMockUrl string + giveMockResponse []byte + giveMockStatus int + wantUpdateErr bool + wantConstantRates bool + wantLastUpdated time.Time + wantDataAsOf time.Time + wantConversions map[string]map[string]float64 + }{ + { + description: "Fetching currency rates successfully", + giveFakeTime: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + giveMockResponse: getMockRates(), + giveMockStatus: 200, + wantLastUpdated: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + wantDataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), + wantConversions: map[string]map[string]float64{"USD": {"GBP": 0.77208}, "GBP": {"USD": 1.2952}}, + }, + { + description: "Currency rates endpoint returns empty response", + giveFakeTime: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + giveMockResponse: []byte("{}"), + giveMockStatus: 200, + wantLastUpdated: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + wantDataAsOf: time.Time{}, + wantConversions: nil, + }, + { + description: "Currency rates endpoint returns nil response", + giveFakeTime: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + giveMockResponse: nil, + giveMockStatus: 200, + wantUpdateErr: true, + wantConstantRates: true, + wantLastUpdated: time.Time{}, + }, + { + description: "Currency rates endpoint returns non-2xx status code", + giveFakeTime: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + giveMockResponse: []byte(`{"message": "Not Found"}`), + giveMockStatus: 404, + wantUpdateErr: true, + wantConstantRates: true, + wantLastUpdated: time.Time{}, + }, + { + description: "Currency rates endpoint returns invalid json response", + giveFakeTime: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + giveMockResponse: []byte(`{"message": Invalid-JSON-No-Surrounding-Quotes}`), + giveMockStatus: 200, + wantUpdateErr: true, + wantConstantRates: true, + wantLastUpdated: time.Time{}, + }, + { + description: "Currency rates endpoint url is invalid", + giveFakeTime: time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC), + giveMockUrl: "invalidurl", + giveMockResponse: getMockRates(), + giveMockStatus: 200, + wantUpdateErr: true, + wantConstantRates: true, + wantLastUpdated: time.Time{}, }, } - // At each ticks, do couple checks - for ticksCount := range ticks { - ticksTimes = append(ticksTimes, time.Now()) - if len(ticksTimes) > 1 { - intervalDuration := ticksTimes[len(ticksTimes)-1].Truncate(time.Millisecond).Sub(ticksTimes[len(ticksTimes)-2].Truncate(time.Millisecond)) - intervalDiff := float64(float64(intervalDuration.Nanoseconds()) / float64(expectedIntervalDuration.Nanoseconds())) - assert.False(t, intervalDiff > float64(errorMargin*100), "Interval between ticks should be: %d but was: %d", expectedIntervalDuration, intervalDuration) - } - - assert.NotEqual(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated should be set") - assert.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones") - assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") + for _, tt := range tests { + mockedHttpServer := httptest.NewServer(mockServerHandler(tt.giveMockResponse, tt.giveMockStatus)) + defer mockedHttpServer.Close() - if ticksCount == expectedTicks { - currencyConverter.StopPeriodicFetching() - return + var url string + if len(tt.giveMockUrl) > 0 { + url = tt.giveMockUrl + } else { + url = mockedHttpServer.URL } - } -} - -func TestStop(t *testing.T) { - - // Setup: - calledURLs := []string{} - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - rw.WriteHeader(http.StatusOK) - rw.Write([]byte(getMockRates())) - }), - ) - - // Execute: - expectedTicks := 2 - ticks := make(chan int) - currencyConverter := currencies.NewRateConverterWithNotifier( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(100)*time.Millisecond, - time.Duration(24)*time.Hour, - ticks, - ) - - // Let the currency converter fetch 5 times before stopping it - for ticksCount := range ticks { - if ticksCount == expectedTicks { - currencyConverter.StopPeriodicFetching() - break + currencyConverter := NewRateConverter( + &http.Client{}, + url, + 24*time.Hour, + ) + currencyConverter.time = &FakeTime{time: tt.giveFakeTime} + err := currencyConverter.Run() + + if tt.wantUpdateErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) } - } - lastFetched := time.Now() - - // Verify: - // Check for the next 1 second that no fetch was triggered - time.Sleep(1 * time.Second) - - assert.False(t, currencyConverter.LastUpdated().After(lastFetched), "LastUpdated() shouldn't be after `lastFetched` since the periodic fetching is stopped") -} - -func TestInitWithZeroDuration(t *testing.T) { - - // Setup: - calledURLs := []string{} - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - rw.WriteHeader(http.StatusOK) - rw.Write([]byte(getMockRates())) - }), - ) - - // Execute: - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(0), - time.Duration(24)*time.Hour, - ) - - // Verify: - // Check for the next 1 second that no fetch was triggered - time.Sleep(1 * time.Second) - - assert.Equal(t, 0, len(calledURLs), "sync URL shouldn't have been called but was called %d times", 0, len(calledURLs)) - assert.Equal(t, (time.Time{}), currencyConverter.LastUpdated(), "LastUpdated() shouldn't be set") - assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") - assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil") -} - -func TestRates(t *testing.T) { - - // Setup: - testCases := []struct { - from string - to string - expectedRate float64 - hasError bool - }{ - {from: "USD", to: "GBP", expectedRate: 0.77208, hasError: false}, - {from: "GBP", to: "USD", expectedRate: 1.2952, hasError: false}, - {from: "GBP", to: "EUR", expectedRate: 0, hasError: true}, - {from: "CNY", to: "EUR", expectedRate: 0, hasError: true}, - {from: "", to: "EUR", expectedRate: 0, hasError: true}, - {from: "CNY", to: "", expectedRate: 0, hasError: true}, - {from: "", to: "", expectedRate: 0, hasError: true}, - {from: "USD", to: "USD", expectedRate: 1, hasError: false}, - } - - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write([]byte(getMockRates())) - }), - ) - - // Execute: - ticks := make(chan int) - currencyConverter := currencies.NewRateConverterWithNotifier( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(100)*time.Millisecond, - time.Duration(24)*time.Hour, - ticks, - ) - rates := currencyConverter.Rates() - // Let the currency converter ticks 1 time before to stop it - select { - case <-ticks: - currencyConverter.StopPeriodicFetching() - } - - // Verify: - assert.NotNil(t, rates, "rates shouldn't be nil") - for _, tc := range testCases { - rate, err := rates.GetRate(tc.from, tc.to) - - if tc.hasError { - assert.NotNil(t, err, "err shouldn't be nil") - assert.Equal(t, float64(0), rate, "rate should be 0") + if tt.wantConstantRates { + assert.Equal(t, currencyConverter.Rates(), &ConstantRates{}, tt.description) } else { - assert.Nil(t, err, "err should be nil") - assert.Equal(t, tc.expectedRate, rate, "rate doesn't match the expected one") + rates := currencyConverter.Rates().(*Rates) + assert.Equal(t, tt.wantConversions, (*rates).Conversions, tt.description) + assert.Equal(t, tt.wantDataAsOf, (*rates).DataAsOf, tt.description) } - } -} - -func TestRates_EmptyRates(t *testing.T) { - // Setup: - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write([]byte("")) - }), - ) - - // Execute: - // Will try to fetch directly on method call but will fail - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(100)*time.Millisecond, - time.Duration(24)*time.Hour, - ) - defer currencyConverter.StopPeriodicFetching() - - // Verify: - assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") + lastUpdated := currencyConverter.LastUpdated() + assert.Equal(t, tt.wantLastUpdated, lastUpdated, tt.description) + } } -func TestSelectRatesBasedOnStaleness(t *testing.T) { - calledURLs := []string{} - callCnt := 0 +func TestRateStaleness(t *testing.T) { + callCount := 0 mockedHttpServer := httptest.NewServer(http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { - calledURLs = append(calledURLs, req.RequestURI) - if callCnt == 0 || callCnt >= 5 { + callCount++ + if callCount == 2 || callCount >= 5 { rw.WriteHeader(http.StatusOK) rw.Write([]byte(getMockRates())) } else { rw.WriteHeader(http.StatusNotFound) + rw.Write([]byte(`{"message": "Not Found"}`)) } - callCnt++ }), ) defer mockedHttpServer.Close() - expectedRates := ¤cies.Rates{ + expectedRates := &Rates{ DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), Conversions: map[string]map[string]float64{ "USD": { @@ -455,97 +180,83 @@ func TestSelectRatesBasedOnStaleness(t *testing.T) { }, } + initialFakeTime := time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC) + fakeTime := &FakeTime{time: initialFakeTime} + // Execute: - currencyConverter := currencies.NewRateConverter( + currencyConverter := NewRateConverter( &http.Client{}, mockedHttpServer.URL, - time.Duration(100)*time.Millisecond, - time.Duration(200)*time.Millisecond, + 30*time.Second, // stale rates threshold ) + currencyConverter.time = fakeTime - // Verify: - // Rates are valid at t=0, then invalid for 500ms before being valid again - assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + // First Update call results in error + err1 := currencyConverter.Run() + assert.NotNil(t, err1) - time.Sleep(100 * time.Millisecond) - // Rates have been invalid for ~100ms, rates not stale yet - assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + // Verify constant rates are used and last update ts is not set + assert.Equal(t, &ConstantRates{}, currencyConverter.Rates(), "Rates should return constant rates") + assert.Equal(t, time.Time{}, currencyConverter.LastUpdated(), "LastUpdated return is incorrect") - time.Sleep(200 * time.Millisecond) - // Rates have been invalid for ~300ms, rates are stale - assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") + // Second Update call is successful and yields valid rates + err2 := currencyConverter.Run() + assert.Nil(t, err2) - time.Sleep(300 * time.Millisecond) - // Rates have been valid again for ~100ms - assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") -} + // Verify rates are valid and last update timestamp is set + assert.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones") + assert.Equal(t, initialFakeTime, currencyConverter.LastUpdated(), "LastUpdated should be set") -func TestUseConstantRatesUntilFetchIsSuccessful(t *testing.T) { - callCnt := 0 - mockedHttpServer := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - if callCnt >= 5 { - rw.WriteHeader(http.StatusOK) - rw.Write([]byte(getMockRates())) - } else { - rw.WriteHeader(http.StatusNotFound) - } - callCnt++ - }), - ) + // Advance time so the rates fall just short of being considered stale + fakeTime.time = fakeTime.time.Add(29 * time.Second) - defer mockedHttpServer.Close() + // Third Update call results in error but stale rate threshold has not been exceeded + err3 := currencyConverter.Run() + assert.NotNil(t, err3) - expectedRates := ¤cies.Rates{ - DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), - Conversions: map[string]map[string]float64{ - "USD": { - "GBP": 0.77208, - }, - "GBP": { - "USD": 1.2952, - }, - }, - } + // Verify rates are valid and last update ts has not changed + assert.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones") + assert.Equal(t, initialFakeTime, currencyConverter.LastUpdated(), "LastUpdated should be set") - // Execute: - currencyConverter := currencies.NewRateConverter( - &http.Client{}, - mockedHttpServer.URL, - time.Duration(100)*time.Millisecond, - time.Duration(1)*time.Second, - ) + // Advance time just past the threshold so the rates are considered stale + fakeTime.time = fakeTime.time.Add(2 * time.Second) + + // Fourth Update call results in error and stale rate threshold has been exceeded + err4 := currencyConverter.Run() + assert.NotNil(t, err4) - // Verify: - // Rates are invalid at t=0 and remain invalid until 500ms have elapsed - assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") + // Verify constant rates are used and last update ts has not changed + assert.Equal(t, &ConstantRates{}, currencyConverter.Rates(), "Rates should return constant rates") + assert.Equal(t, initialFakeTime, currencyConverter.LastUpdated(), "LastUpdated return is incorrect") - time.Sleep(400 * time.Millisecond) - // Rates have been invalid for ~400ms - assert.Equal(t, currencyConverter.Rates(), ¤cies.ConstantRates{}, "Rates() should return constant rates") + // Fifth Update call is successful and yields valid rates + err5 := currencyConverter.Run() + assert.Nil(t, err5) - time.Sleep(200 * time.Millisecond) - // Rates have been valid for ~100ms - assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + // Verify rates are valid and last update ts has changed + thirtyOneSec := 31 * time.Second + assert.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones") + assert.Equal(t, (initialFakeTime.Add(thirtyOneSec)), currencyConverter.LastUpdated(), "LastUpdated should be set") } -func TestRatesAreNeverStale(t *testing.T) { - callCnt := 0 +func TestRatesAreNeverConsideredStale(t *testing.T) { + callCount := 0 mockedHttpServer := httptest.NewServer(http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { - if callCnt == 0 { + callCount++ + if callCount == 1 { rw.WriteHeader(http.StatusOK) rw.Write([]byte(getMockRates())) } else { rw.WriteHeader(http.StatusNotFound) + rw.Write([]byte(`{"message": "Not Found"}`)) } - callCnt++ }), ) defer mockedHttpServer.Close() - expectedRates := ¤cies.Rates{ + expectedRates := &Rates{ DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC), Conversions: map[string]map[string]float64{ "USD": { @@ -557,25 +268,38 @@ func TestRatesAreNeverStale(t *testing.T) { }, } + initialFakeTime := time.Date(2018, time.September, 12, 30, 0, 0, 0, time.UTC) + fakeTime := &FakeTime{time: initialFakeTime} + // Execute: - currencyConverter := currencies.NewRateConverter( + currencyConverter := NewRateConverter( &http.Client{}, mockedHttpServer.URL, - time.Duration(100)*time.Millisecond, - time.Duration(0)*time.Millisecond, + 0*time.Millisecond, // stale rates threshold ) + currencyConverter.time = fakeTime + + // First Update call is successful and yields valid rates + err1 := currencyConverter.Run() + assert.Nil(t, err1) + + // Verify rates are valid and last update timestamp is correct + assert.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones") + assert.Equal(t, fakeTime.time, currencyConverter.LastUpdated(), "LastUpdated should be set") + + // Advance time so the current time is well past the the time the rates were last updated + fakeTime.time = initialFakeTime.Add(24 * time.Hour) - // Verify: - // Rates are valid at t=0 and are then invalid at 100ms - assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + // Second Update call results in error but rates from a day ago are still valid + err2 := currencyConverter.Run() + assert.NotNil(t, err2) - time.Sleep(500 * time.Millisecond) - // Rates have been invalid for ~400ms - assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates") + // Verify rates are valid and last update ts is correct + assert.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones") + assert.Equal(t, initialFakeTime, currencyConverter.LastUpdated(), "LastUpdated should be set") } func TestRace(t *testing.T) { - // This test is checking that no race conditions appear in rate converter. // It simulate multiple clients (in different goroutines) asking for updates // and rates while the rate converter is also updating periodically. @@ -599,20 +323,19 @@ func TestRace(t *testing.T) { } // Execute: - - // Create a rate converter which will be fetching new values every 10 ms - currencyConverter := currencies.NewRateConverter( + // Create a rate converter which will be fetching new values every 1 ms + interval := 1 * time.Millisecond + currencyConverter := NewRateConverter( mockedHttpClient, "currency.fake.com", - time.Duration(10)*time.Millisecond, - time.Duration(24)*time.Hour, + 24*time.Hour, ) - defer currencyConverter.StopPeriodicFetching() + ticker := task.NewTickerTask(interval, currencyConverter) + ticker.Start() + defer ticker.Stop() - // Create 50 clients asking for updates and rates conversion at random intervals - // from 1ms to 50ms for 10 seconds var wg sync.WaitGroup - clientsCount := 50 + clientsCount := 10 wg.Add(clientsCount) dones := make([]chan bool, clientsCount) @@ -623,12 +346,9 @@ func TestRace(t *testing.T) { clientTicker := time.NewTicker(randomTickInterval) for { select { - case tickTime := <-clientTicker.C: - // Either ask for an Update() or for GetRate() - // based on the tick ms - tickMs := tickTime.UnixNano() / int64(time.Millisecond) - if tickMs%2 == 0 { - err := currencyConverter.Update() + case <-clientTicker.C: + if clientNum < 5 { + err := currencyConverter.Run() assert.Nil(t, err) } else { rate, err := currencyConverter.Rates().GetRate("USD", "GBP") @@ -643,7 +363,7 @@ func TestRace(t *testing.T) { }(dones[c], c) } - time.Sleep(10 * time.Second) + time.Sleep(100 * time.Millisecond) // Sending stop signals to all clients for i := range dones { dones[i] <- true diff --git a/currencies/rates_test.go b/currencies/rates_test.go index 915b817d7a5..5b1c4497b63 100644 --- a/currencies/rates_test.go +++ b/currencies/rates_test.go @@ -146,6 +146,7 @@ func TestGetRate(t *testing.T) { {from: "", to: "EUR", expectedRate: 0, hasError: true}, {from: "CNY", to: "", expectedRate: 0, hasError: true}, {from: "", to: "", expectedRate: 0, hasError: true}, + {from: "USD", to: "USD", expectedRate: 1, hasError: false}, } for _, tc := range testCases { diff --git a/endpoints/currency_rates.go b/endpoints/currency_rates.go index 745dbe3e7d4..90650cc2886 100644 --- a/endpoints/currency_rates.go +++ b/endpoints/currency_rates.go @@ -24,7 +24,7 @@ type rateConverter interface { } // newCurrencyRatesInfo creates a new CurrencyRatesInfo instance. -func newCurrencyRatesInfo(rateConverter rateConverter) currencyRatesInfo { +func newCurrencyRatesInfo(rateConverter rateConverter, fetchingInterval time.Duration) currencyRatesInfo { currencyRatesInfo := currencyRatesInfo{ Active: false, @@ -44,7 +44,6 @@ func newCurrencyRatesInfo(rateConverter rateConverter) currencyRatesInfo { source := infos.Source() currencyRatesInfo.Source = &source - fetchingInterval := infos.FetchingInterval() currencyRatesInfo.FetchingInterval = &fetchingInterval lastUpdated := infos.LastUpdated() @@ -57,8 +56,8 @@ func newCurrencyRatesInfo(rateConverter rateConverter) currencyRatesInfo { } // NewCurrencyRatesEndpoint returns current currency rates applied by the PBS server. -func NewCurrencyRatesEndpoint(rateConverter rateConverter) http.HandlerFunc { - currencyRateInfo := newCurrencyRatesInfo(rateConverter) +func NewCurrencyRatesEndpoint(rateConverter rateConverter, fetchingInterval time.Duration) http.HandlerFunc { + currencyRateInfo := newCurrencyRatesInfo(rateConverter, fetchingInterval) return func(w http.ResponseWriter, _ *http.Request) { jsonOutput, err := json.Marshal(currencyRateInfo) diff --git a/endpoints/currency_rates_test.go b/endpoints/currency_rates_test.go index e0b127fcd95..86c4e50fb3e 100644 --- a/endpoints/currency_rates_test.go +++ b/endpoints/currency_rates_test.go @@ -14,20 +14,21 @@ import ( func TestCurrencyRatesEndpoint(t *testing.T) { // Setup: var testCases = []struct { - input rateConverter - expectedBody string - expectedCode int - description string + inputConverter rateConverter + inputFetchingInterval time.Duration + expectedBody string + expectedCode int + description string }{ { nil, + time.Duration(0), `{"active": false}`, http.StatusOK, "case 1 - rate converter is nil", }, { newRateConverterMock( - 5*time.Minute, "https://sync.test.com", time.Date(2019, 3, 2, 12, 54, 56, 651387237, time.UTC), newConversionMock(&map[string]map[string]float64{ @@ -36,6 +37,7 @@ func TestCurrencyRatesEndpoint(t *testing.T) { }, }), ), + 5 * time.Minute, `{ "active": true, "source": "https://sync.test.com", @@ -52,11 +54,11 @@ func TestCurrencyRatesEndpoint(t *testing.T) { }, { newRateConverterMock( - time.Duration(0), "", time.Time{}, nil, ), + time.Duration(0), `{ "active": true, "source": "", @@ -70,12 +72,14 @@ func TestCurrencyRatesEndpoint(t *testing.T) { newRateConverterMockWithInfo( newUnmarshableConverterInfoMock(), ), + time.Duration(0), "", http.StatusInternalServerError, "case 4 - invalid rates input for marshaling", }, { newRateConverterMockWithNilInfo(), + time.Duration(0), `{ "active": true }`, @@ -86,7 +90,7 @@ func TestCurrencyRatesEndpoint(t *testing.T) { for _, tc := range testCases { - handler := NewCurrencyRatesEndpoint(tc.input) + handler := NewCurrencyRatesEndpoint(tc.inputConverter, tc.inputFetchingInterval) w := httptest.NewRecorder() // Execute: @@ -117,21 +121,16 @@ func newConversionMock(rates *map[string]map[string]float64) *conversionMock { } type converterInfoMock struct { - source string - fetchingInterval time.Duration - lastUpdated time.Time - rates *map[string]map[string]float64 - additionalInfo interface{} + source string + lastUpdated time.Time + rates *map[string]map[string]float64 + additionalInfo interface{} } func (m converterInfoMock) Source() string { return m.source } -func (m converterInfoMock) FetchingInterval() time.Duration { - return m.fetchingInterval -} - func (m converterInfoMock) LastUpdated() time.Time { return m.lastUpdated } @@ -150,10 +149,6 @@ func (m unmarshableConverterInfoMock) Source() string { return "" } -func (m unmarshableConverterInfoMock) FetchingInterval() time.Duration { - return time.Duration(0) -} - func (m unmarshableConverterInfoMock) LastUpdated() time.Time { return time.Time{} } @@ -172,7 +167,6 @@ func newUnmarshableConverterInfoMock() unmarshableConverterInfoMock { } type rateConverterMock struct { - fetchingInterval time.Duration syncSourceURL string rates *conversionMock lastUpdated time.Time @@ -197,23 +191,20 @@ func (m rateConverterMock) GetInfo() currencies.ConverterInfo { rates = m.rates.GetRates() } return converterInfoMock{ - source: m.syncSourceURL, - fetchingInterval: m.fetchingInterval, - lastUpdated: m.lastUpdated, - rates: rates, + source: m.syncSourceURL, + lastUpdated: m.lastUpdated, + rates: rates, } } func newRateConverterMock( - fetchingInterval time.Duration, syncSourceURL string, lastUpdated time.Time, rates *conversionMock) rateConverterMock { return rateConverterMock{ - fetchingInterval: fetchingInterval, - syncSourceURL: syncSourceURL, - rates: rates, - lastUpdated: lastUpdated, + syncSourceURL: syncSourceURL, + rates: rates, + lastUpdated: lastUpdated, } } diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go index 93d7575e865..fba0daecea8 100644 --- a/endpoints/openrtb2/auction_benchmark_test.go +++ b/endpoints/openrtb2/auction_benchmark_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/currencies" @@ -77,7 +78,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) { theMetrics, infos, gdpr.AlwaysAllow{}, - currencies.NewRateConverterDefault(), + currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), ), paramValidator, empty_fetcher.EmptyFetcher{}, diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 7ae96c09b93..4f207cf5a65 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -75,7 +75,7 @@ func TestSingleBidder(t *testing.T) { bidResponse: mockBidderResponse, } bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) // Make sure the goodSingleBidder was called with the expected arguments. @@ -163,7 +163,7 @@ func TestMultiBidder(t *testing.T) { bidResponse: mockBidderResponse, } bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if seatBid == nil { @@ -528,9 +528,11 @@ func TestMultiCurrencies(t *testing.T) { currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, - time.Duration(10)*time.Second, time.Duration(24)*time.Hour, ) + time.Sleep(time.Duration(500) * time.Millisecond) + currencyConverter.Run() + seatBid, errs := bidder.requestBid( context.Background(), &openrtb.BidRequest{}, @@ -674,7 +676,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { // Execute: bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) seatBid, errs := bidder.requestBid( context.Background(), &openrtb.BidRequest{}, @@ -843,7 +845,6 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, - time.Duration(10)*time.Second, time.Duration(24)*time.Hour, ) seatBid, errs := bidder.requestBid( @@ -1020,7 +1021,7 @@ func TestMobileNativeTypes(t *testing.T) { bidResponse: tc.mockBidderResponse, } bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) seatBids, _ := bidder.requestBid( context.Background(), @@ -1041,7 +1042,7 @@ func TestMobileNativeTypes(t *testing.T) { func TestErrorReporting(t *testing.T) { bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) bids, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if bids != nil { t.Errorf("There should be no seatbid if no http requests are returned.") @@ -1224,7 +1225,8 @@ func TestCallRecordAdapterConnections(t *testing.T) { // Run requestBid using an http.Client with a mock handler bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, metrics, openrtb_ext.BidderAppnexus) - _, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencies.NewRateConverterDefault().Rates(), &adapters.ExtraRequestInfo{}) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + _, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) // Assert no errors assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 5fbdb1c57a9..545f04fd0ef 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -50,7 +50,8 @@ func TestNewExchange(t *testing.T) { Adapters: blankAdapterConfig(openrtb_ext.BidderList()), } - e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), knownAdapters, config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), knownAdapters, config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter).(*exchange) for _, bidderName := range knownAdapters { if _, ok := e.adapterMap[bidderName]; !ok { t.Errorf("NewExchange produced an Exchange without bidder %s", bidderName) @@ -87,7 +88,8 @@ func TestCharacterEscape(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() - e := NewExchange(server.Client(), nil, 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) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter).(*exchange) /* 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */ //liveAdapters []openrtb_ext.BidderName, @@ -230,7 +232,7 @@ func TestDebugBehaviour(t *testing.T) { e.cache = &wellBehavedCache{} e.me = &metricsConf.DummyMetricsEngine{} e.gDPR = gdpr.AlwaysAllow{} - e.currencyConverter = currencies.NewRateConverterDefault() + e.currencyConverter = currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) // Run tests for _, test := range testCases { @@ -299,7 +301,8 @@ func TestGetBidCacheInfo(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() - 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) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + 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{}, currencyConverter).(*exchange) /* 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */ liveAdapters := []openrtb_ext.BidderName{bidderName} @@ -449,7 +452,8 @@ func TestBidResponseCurrency(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() - e := NewExchange(server.Client(), nil, 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) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter).(*exchange) liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" @@ -616,7 +620,8 @@ func TestRaceIntegration(t *testing.T) { 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()) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + ex := NewExchange(server.Client(), &wellBehavedCache{}, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter) _, err := ex.HoldAuction(context.Background(), newRaceCheckingRequest(t), &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) @@ -700,7 +705,8 @@ func TestPanicRecovery(t *testing.T) { } theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - e := NewExchange(&http.Client{}, nil, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(&http.Client{}, nil, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter).(*exchange) chBids := make(chan *bidResponseWrapper, 1) panicker := func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) { panic("panic!") @@ -765,7 +771,8 @@ func TestPanicRecoveryHighLevel(t *testing.T) { Endpoint: server.URL, } } - e := NewExchange(server.Client(), &mockCache{}, 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) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), &mockCache{}, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter).(*exchange) e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{} e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{} @@ -1025,7 +1032,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] cache: &wellBehavedCache{}, cacheTime: 0, gDPR: gdpr.AlwaysAllow{}, - currencyConverter: currencies.NewRateConverterDefault(), + currencyConverter: currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), UsersyncIfAmbiguous: false, privacyConfig: privacyConfig, } diff --git a/exchange/legacy_test.go b/exchange/legacy_test.go index 3ca804a115c..61414c0ed73 100644 --- a/exchange/legacy_test.go +++ b/exchange/legacy_test.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "errors" + "net/http" "reflect" "testing" + "time" "github.com/buger/jsonparser" "github.com/evanphx/json-patch" @@ -58,7 +60,7 @@ func TestSiteVideo(t *testing.T) { mockAdapter := mockLegacyAdapter{} exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) > 0 { t.Errorf("Unexpected error requesting bids: %v", errs) @@ -92,7 +94,7 @@ func TestAppBanner(t *testing.T) { mockAdapter := mockLegacyAdapter{} exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) > 0 { t.Errorf("Unexpected error requesting bids: %v", errs) @@ -138,7 +140,7 @@ func TestBidTransforms(t *testing.T) { } exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) seatBid, errs := exchangeBidder.requestBid(context.Background(), newAppOrtbRequest(), openrtb_ext.BidderRubicon, bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) != 1 { t.Fatalf("Bad error count. Expected 1, got %d", len(errs)) @@ -287,7 +289,7 @@ func TestErrorResponse(t *testing.T) { } exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) != 1 { t.Fatalf("Bad error count. Expected 1, got %d", len(errs)) @@ -326,7 +328,7 @@ func TestWithTargeting(t *testing.T) { }}, } exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) bid, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderFacebook, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) != 0 { t.Fatalf("This should not produce errors. Got %v", errs) diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index 284d56be42e..e596e5aa215 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -88,7 +88,7 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op cache: &wellBehavedCache{}, cacheTime: time.Duration(0), gDPR: gdpr.AlwaysAllow{}, - currencyConverter: currencies.NewRateConverterDefault(), + currencyConverter: currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), UsersyncIfAmbiguous: false, } diff --git a/main.go b/main.go index 9a835f42a4c..035d386e3b0 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( pbc "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/router" "github.com/prebid/prebid-server/server" + "github.com/prebid/prebid-server/util/task" "github.com/golang/glog" "github.com/spf13/viper" @@ -53,8 +54,10 @@ func loadConfig() (*config.Configuration, error) { func serve(revision string, cfg *config.Configuration) error { fetchingInterval := time.Duration(cfg.CurrencyConverter.FetchIntervalSeconds) * time.Second staleRatesThreshold := time.Duration(cfg.CurrencyConverter.StaleRatesSeconds) * time.Second - currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL, - fetchingInterval, staleRatesThreshold) + currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL, staleRatesThreshold) + + currencyConverterTickerTask := task.NewTickerTask(fetchingInterval, currencyConverter) + currencyConverterTickerTask.Start() r, err := router.New(cfg, currencyConverter) if err != nil { @@ -64,7 +67,7 @@ func serve(revision string, cfg *config.Configuration) error { pbc.InitPrebidCache(cfg.CacheURL.GetBaseURL()) corsRouter := router.SupportCORS(r) - server.Listen(cfg, router.NoCache{Handler: corsRouter}, router.Admin(revision, currencyConverter), r.MetricsEngine) + server.Listen(cfg, router.NoCache{Handler: corsRouter}, router.Admin(revision, currencyConverter, fetchingInterval), r.MetricsEngine) r.Shutdown() return nil diff --git a/router/admin.go b/router/admin.go index 83c4701bb19..fe268c48b2c 100644 --- a/router/admin.go +++ b/router/admin.go @@ -3,12 +3,13 @@ package router import ( "net/http" "net/http/pprof" + "time" "github.com/prebid/prebid-server/currencies" "github.com/prebid/prebid-server/endpoints" ) -func Admin(revision string, rateConverter *currencies.RateConverter) *http.ServeMux { +func Admin(revision string, rateConverter *currencies.RateConverter, rateConverterFetchingInterval time.Duration) *http.ServeMux { // Add endpoints to the admin server // Making sure to add pprof routes mux := http.NewServeMux() @@ -19,7 +20,7 @@ func Admin(revision string, rateConverter *currencies.RateConverter) *http.Serve mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) mux.HandleFunc("/debug/pprof/trace", pprof.Trace) // Register prebid-server defined admin handlers - mux.HandleFunc("/currency/rates", endpoints.NewCurrencyRatesEndpoint(rateConverter)) + mux.HandleFunc("/currency/rates", endpoints.NewCurrencyRatesEndpoint(rateConverter, rateConverterFetchingInterval)) mux.HandleFunc("/version", endpoints.NewVersionEndpoint(revision)) return mux } diff --git a/util/task/ticker_task.go b/util/task/ticker_task.go new file mode 100644 index 00000000000..a8d523b75d5 --- /dev/null +++ b/util/task/ticker_task.go @@ -0,0 +1,53 @@ +package task + +import ( + "time" +) + +type Runner interface { + Run() error +} + +type TickerTask struct { + interval time.Duration + runner Runner + done chan struct{} +} + +func NewTickerTask(interval time.Duration, runner Runner) *TickerTask { + return &TickerTask{ + interval: interval, + runner: runner, + done: make(chan struct{}), + } +} + +// Start runs the task immediately and then schedules the task to run periodically +// if a positive fetching interval has been specified. +func (t *TickerTask) Start() { + t.runner.Run() + + if t.interval > 0 { + go t.runRecurring() + } +} + +// Stop stops the periodic task but the task runner maintains state +func (t *TickerTask) Stop() { + close(t.done) +} + +// run creates a ticker that ticks at the specified interval. On each tick, +// the task is executed +func (t *TickerTask) runRecurring() { + ticker := time.NewTicker(t.interval) + + for { + select { + case <-ticker.C: + t.runner.Run() + case <-t.done: + return + } + } +} diff --git a/util/task/ticker_task_test.go b/util/task/ticker_task_test.go new file mode 100644 index 00000000000..27551c9a2c2 --- /dev/null +++ b/util/task/ticker_task_test.go @@ -0,0 +1,63 @@ +package task_test + +import ( + "testing" + "time" + + "github.com/prebid/prebid-server/util/task" + "github.com/stretchr/testify/assert" +) + +type MockRunner struct { + RunCount int +} + +func (mcc *MockRunner) Run() error { + mcc.RunCount++ + return nil +} + +func TestStartWithSingleRun(t *testing.T) { + // Setup: + runner := &MockRunner{RunCount: 0} + interval := 0 * time.Millisecond + ticker := task.NewTickerTask(interval, runner) + + // Execute: + ticker.Start() + time.Sleep(10 * time.Millisecond) + + // Verify: + assert.Equal(t, runner.RunCount, 1, "runner should have run one time") +} + +func TestStartWithPeriodicRun(t *testing.T) { + // Setup: + runner := &MockRunner{RunCount: 0} + interval := 10 * time.Millisecond + ticker := task.NewTickerTask(interval, runner) + + // Execute: + ticker.Start() + time.Sleep(25 * time.Millisecond) + ticker.Stop() + + // Verify: + assert.Equal(t, runner.RunCount, 3, "runner should have run three times") +} + +func TestStop(t *testing.T) { + // Setup: + runner := &MockRunner{RunCount: 0} + interval := 10 * time.Millisecond + ticker := task.NewTickerTask(interval, runner) + + // Execute: + ticker.Start() + time.Sleep(25 * time.Millisecond) + ticker.Stop() + time.Sleep(25 * time.Millisecond) // wait in case stop failed so additional runs can happen + + // Verify: + assert.Equal(t, runner.RunCount, 3, "runner should have run three times") +} diff --git a/util/timeutil/time.go b/util/timeutil/time.go new file mode 100644 index 00000000000..e8eaae7d61f --- /dev/null +++ b/util/timeutil/time.go @@ -0,0 +1,16 @@ +package timeutil + +import ( + "time" +) + +type Time interface { + Now() time.Time +} + +// RealTime wraps the time package for testability +type RealTime struct{} + +func (c *RealTime) Now() time.Time { + return time.Now() +} From a4ac6b63f312ac94d4844b7129fcf8f3dd044204 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Wed, 12 Aug 2020 18:57:54 -0400 Subject: [PATCH 166/381] Fix TCF1 Fetcher Fallback (#1438) --- gdpr/vendorlist-fetching.go | 2 +- gdpr/vendorlist-fetching_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index a0a73c93008..1442f81c3ba 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -158,7 +158,7 @@ func newVendorListCache(fallbackVL api.VendorList) (save func(id uint16, list ap if ok { return list.(vendorlist.VendorList) } - return fallbackVL + return nil } return } diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index c989ef4cef8..031e564094c 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -173,7 +173,7 @@ func TestDefaultVendorList(t *testing.T) { assert.Equal(t, false, vendor.Purpose(2)) } -func TestDefaultVendorListPassthrough(t *testing.T) { +func TestFallbackVendorListPassthrough(t *testing.T) { firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { purposes: []int{1, 2}, @@ -184,7 +184,7 @@ func TestDefaultVendorListPassthrough(t *testing.T) { purposes: []int{2}, }, }) - server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ + server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ 1: firstVendorList, 2: secondVendorList, }))) @@ -204,7 +204,7 @@ func TestDefaultVendorListPassthrough(t *testing.T) { assert.Equal(t, true, vendor.Purpose(2)) } -func TestDefaultVendorListNoFetch(t *testing.T) { +func TestFallbackVendorListNoFetch(t *testing.T) { firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ 32: { purposes: []int{1, 2}, From dee2ca502fbd8573a46380df461159368d4dba84 Mon Sep 17 00:00:00 2001 From: ShriprasadM Date: Fri, 14 Aug 2020 17:15:19 +0530 Subject: [PATCH 167/381] UOE-5440: Changes for capturing Pod algorithm execution time using pbmetrics (#65) * Added function getPrometheusRegistry() * Exported function GetPrometheusRegistry * UOE-5440: Capturing execution time in nanoseconds for algorithms * UOE-5440: Changes for prometheus algorithem metrics for pod using pbsmetrics * UOE-5440: Test cases for prometheus * UOE-5440: Added test cases * UOE-5440: Changing buckets * UOE-5440: changes in pbsmetrics for newly added metrics Co-authored-by: Sachin Survase Co-authored-by: PubMatic-OpenWrap Co-authored-by: Shriprasad --- endpoints/openrtb2/ctv/adpod_generator.go | 57 ++++++++++--- endpoints/openrtb2/ctv/constant.go | 10 +++ .../openrtb2/ctv/impressions/impressions.go | 6 ++ endpoints/openrtb2/ctv_auction.go | 16 +++- pbsmetrics/config/metrics.go | 33 +++++++ pbsmetrics/go_metrics.go | 12 +++ pbsmetrics/metrics.go | 31 +++++++ pbsmetrics/prometheus/prometheus.go | 85 +++++++++++++++++++ pbsmetrics/prometheus/prometheus_test.go | 41 +++++++++ router/router.go | 10 +++ 10 files changed, 285 insertions(+), 16 deletions(-) diff --git a/endpoints/openrtb2/ctv/adpod_generator.go b/endpoints/openrtb2/ctv/adpod_generator.go index 10f4cc4427d..bec01ee069a 100644 --- a/endpoints/openrtb2/ctv/adpod_generator.go +++ b/endpoints/openrtb2/ctv/adpod_generator.go @@ -7,6 +7,7 @@ import ( "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" ) /********************* AdPodGenerator Functions *********************/ @@ -20,14 +21,15 @@ type filteredBid struct { reasonCode FilterReasonCode } type highestCombination struct { - bids []*Bid - bidIDs []string - durations []int - price float64 - categoryScore map[string]int - domainScore map[string]int - filteredBids map[string]*filteredBid - timeTaken time.Duration + bids []*Bid + bidIDs []string + durations []int + price float64 + categoryScore map[string]int + domainScore map[string]int + filteredBids map[string]*filteredBid + timeTakenCompExcl time.Duration // time taken by comp excl + timeTakenCombGen time.Duration // time taken by combination generator } //AdPodGenerator AdPodGenerator @@ -38,16 +40,18 @@ type AdPodGenerator struct { buckets BidsBuckets comb ICombination adpod *openrtb_ext.VideoAdPod + met pbsmetrics.MetricsEngine } //NewAdPodGenerator will generate adpod based on configuration -func NewAdPodGenerator(request *openrtb.BidRequest, impIndex int, buckets BidsBuckets, comb ICombination, adpod *openrtb_ext.VideoAdPod) *AdPodGenerator { +func NewAdPodGenerator(request *openrtb.BidRequest, impIndex int, buckets BidsBuckets, comb ICombination, adpod *openrtb_ext.VideoAdPod, met pbsmetrics.MetricsEngine) *AdPodGenerator { return &AdPodGenerator{ request: request, impIndex: impIndex, buckets: buckets, comb: comb, adpod: adpod, + met: met, } } @@ -74,7 +78,8 @@ func (o *AdPodGenerator) cleanup(wg *sync.WaitGroup, responseCh chan *highestCom } func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombination { - defer TimeTrack(time.Now(), fmt.Sprintf("Tid:%v ImpId:%v getAdPodBids", o.request.ID, o.request.Imp[o.impIndex].ID)) + start := time.Now() + defer TimeTrack(start, fmt.Sprintf("Tid:%v ImpId:%v getAdPodBids", o.request.ID, o.request.Imp[o.impIndex].ID)) maxRoutines := 3 isTimedOutORReceivedAllResponses := false @@ -84,20 +89,24 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati lock := sync.Mutex{} ticker := time.NewTicker(timeout) + combinationCount := 0 for i := 0; i < maxRoutines; i++ { wg.Add(1) go func() { for !isTimedOutORReceivedAllResponses { + combGenStartTime := time.Now() lock.Lock() durations := o.comb.Get() lock.Unlock() + combGenElapsedTime := time.Since(combGenStartTime) if len(durations) == 0 { break } hbc := o.getUniqueBids(durations) + hbc.timeTakenCombGen = combGenElapsedTime responseCh <- hbc - Logf("Tid:%v GetUniqueBids Durations:%v Price:%v Time:%v Bids:%v", o.request.ID, hbc.durations[:], hbc.price, hbc.timeTaken, hbc.bidIDs[:]) + Logf("Tid:%v GetUniqueBids Durations:%v Price:%v Time:%v Bids:%v", o.request.ID, hbc.durations[:], hbc.price, hbc.timeTakenCompExcl, hbc.bidIDs[:]) } wg.Done() }() @@ -107,14 +116,20 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati // when all go routines are executed go o.cleanup(wg, responseCh) + totalTimeByCombGen := int64(0) + totalTimeByCompExcl := int64(0) for !isTimedOutORReceivedAllResponses { select { case hbc, ok := <-responseCh: + if false == ok { isTimedOutORReceivedAllResponses = true break } if nil != hbc { + combinationCount++ + totalTimeByCombGen += int64(hbc.timeTakenCombGen) + totalTimeByCompExcl += int64(hbc.timeTakenCompExcl) results = append(results, hbc) } case <-ticker.C: @@ -124,6 +139,24 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati } defer ticker.Stop() + + labels := pbsmetrics.PodLabels{ + AlgorithmName: string(CombinationGeneratorV1), + NoOfCombinations: new(int), + } + *labels.NoOfCombinations = combinationCount + o.met.RecordPodCombGenTime(labels, time.Duration(totalTimeByCombGen)) + + compExclLabels := pbsmetrics.PodLabels{ + AlgorithmName: string(CompetitiveExclusionV1), + NoOfResponseBids: new(int), + } + *compExclLabels.NoOfResponseBids = 0 + for _, ads := range o.buckets { + *compExclLabels.NoOfResponseBids += len(ads) + } + o.met.RecordPodCompititveExclusionTime(compExclLabels, time.Duration(totalTimeByCompExcl)) + return results[:] } @@ -193,7 +226,7 @@ func (o *AdPodGenerator) getUniqueBids(durationSequence []int) *highestCombinati } hbc := findUniqueCombinations(data[:], combinations[:], *o.adpod.IABCategoryExclusionPercent, *o.adpod.AdvertiserExclusionPercent) hbc.durations = durationSequence[:] - hbc.timeTaken = time.Since(startTime) + hbc.timeTakenCompExcl = time.Since(startTime) return hbc } diff --git a/endpoints/openrtb2/ctv/constant.go b/endpoints/openrtb2/ctv/constant.go index fd7beebc6fc..e3b7af8ad3e 100644 --- a/endpoints/openrtb2/ctv/constant.go +++ b/endpoints/openrtb2/ctv/constant.go @@ -39,3 +39,13 @@ const ( CTVRCCategoryExclusion FilterReasonCode = 2 CTVRCDomainExclusion FilterReasonCode = 3 ) + +// MonitorKey provides the unique key for moniroting the algorithms +type MonitorKey string + +const ( + // CombinationGeneratorV1 ... + CombinationGeneratorV1 MonitorKey = "comp_exclusion_v1" + // CompetitiveExclusionV1 ... + CompetitiveExclusionV1 MonitorKey = "comp_exclusion_v1" +) diff --git a/endpoints/openrtb2/ctv/impressions/impressions.go b/endpoints/openrtb2/ctv/impressions/impressions.go index 1131e05f927..9338e6ece94 100644 --- a/endpoints/openrtb2/ctv/impressions/impressions.go +++ b/endpoints/openrtb2/ctv/impressions/impressions.go @@ -29,6 +29,12 @@ const ( MinMaxAlgorithm ) +// MonitorKey provides the unique key for moniroting the impressions algorithm +var MonitorKey = map[Algorithm]string{ + MaximizeForDuration: `a1_max`, + MinMaxAlgorithm: `a2_min_max`, +} + // Value use to compute Ad Slot Durations and Pod Durations for internal computation // Right now this value is set to 5, based on passed data observations // Observed that typically video impression contains contains minimum and maximum duration in multiples of 5 diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index df8835af10d..4754ad06c40 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -407,7 +407,7 @@ func (deps *ctvEndpointDeps) getAllAdPodImpsConfigs() { if nil == imp.Video || nil == deps.impData[index].VideoExt || nil == deps.impData[index].VideoExt.AdPod { continue } - deps.impData[index].Config = getAdPodImpsConfigs(&imp, deps.impData[index].VideoExt.AdPod) + deps.impData[index].Config = deps.getAdPodImpsConfigs(&imp, deps.impData[index].VideoExt.AdPod) if 0 == len(deps.impData[index].Config) { errorCode := new(int) *errorCode = 101 @@ -417,9 +417,17 @@ func (deps *ctvEndpointDeps) getAllAdPodImpsConfigs() { } //getAdPodImpsConfigs will return number of impressions configurations within adpod -func getAdPodImpsConfigs(imp *openrtb.Imp, adpod *openrtb_ext.VideoAdPod) []*ctv.ImpAdPodConfig { - impGen := impressions.NewImpressions(imp.Video.MinDuration, imp.Video.MaxDuration, adpod, impressions.MinMaxAlgorithm) +func (deps *ctvEndpointDeps) getAdPodImpsConfigs(imp *openrtb.Imp, adpod *openrtb_ext.VideoAdPod) []*ctv.ImpAdPodConfig { + selectedAlgorithm := impressions.MinMaxAlgorithm + labels := pbsmetrics.PodLabels{AlgorithmName: impressions.MonitorKey[selectedAlgorithm], NoOfImpressions: new(int)} + + // monitor + start := time.Now() + impGen := impressions.NewImpressions(imp.Video.MinDuration, imp.Video.MaxDuration, adpod, selectedAlgorithm) impRanges := impGen.Get() + *labels.NoOfImpressions = len(impRanges) + deps.metricsEngine.RecordPodImpGenTime(labels, start) + config := make([]*ctv.ImpAdPodConfig, len(impRanges)) for i, value := range impRanges { config[i] = &ctv.ImpAdPodConfig{ @@ -610,7 +618,7 @@ func (deps *ctvEndpointDeps) doAdPodExclusions() ctv.AdPodBids { deps.impData[index].VideoExt.AdPod) //adpod generator - adpodGenerator := ctv.NewAdPodGenerator(deps.request, index, buckets, comb, deps.impData[index].VideoExt.AdPod) + adpodGenerator := ctv.NewAdPodGenerator(deps.request, index, buckets, comb, deps.impData[index].VideoExt.AdPod, deps.metricsEngine) adpodBids := adpodGenerator.GetAdPodBids() if adpodBids != nil { diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index ce6c0f5a707..edc0d1c1192 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -195,6 +195,27 @@ func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) { } } +// RecordPodImpGenTime across all engines +func (me *MultiMetricsEngine) RecordPodImpGenTime(labels pbsmetrics.PodLabels, startTime time.Time) { + for _, thisME := range *me { + thisME.RecordPodImpGenTime(labels, startTime) + } +} + +// RecordPodCombGenTime as a noop +func (me *MultiMetricsEngine) RecordPodCombGenTime(labels pbsmetrics.PodLabels, elapsedTime time.Duration) { + for _, thisME := range *me { + thisME.RecordPodCombGenTime(labels, elapsedTime) + } +} + +// RecordPodCompititveExclusionTime as a noop +func (me *MultiMetricsEngine) RecordPodCompititveExclusionTime(labels pbsmetrics.PodLabels, elapsedTime time.Duration) { + for _, thisME := range *me { + thisME.RecordPodCompititveExclusionTime(labels, elapsedTime) + } +} + // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests) type DummyMetricsEngine struct{} @@ -273,3 +294,15 @@ func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType p // RecordTimeoutNotice as a noop func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) { } + +// RecordPodImpGenTime as a noop +func (me *DummyMetricsEngine) RecordPodImpGenTime(labels pbsmetrics.PodLabels, start time.Time) { +} + +// RecordPodCombGenTime as a noop +func (me *DummyMetricsEngine) RecordPodCombGenTime(labels pbsmetrics.PodLabels, elapsedTime time.Duration) { +} + +// RecordPodCompititveExclusionTime as a noop +func (me *DummyMetricsEngine) RecordPodCompititveExclusionTime(labels pbsmetrics.PodLabels, elapsedTime time.Duration) { +} diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go index cf634cc5ae1..01305fb46f3 100644 --- a/pbsmetrics/go_metrics.go +++ b/pbsmetrics/go_metrics.go @@ -562,6 +562,18 @@ func (me *Metrics) RecordTimeoutNotice(success bool) { return } +// RecordPodImpGenTime as a noop +func (me *Metrics) RecordPodImpGenTime(labels PodLabels, startTime time.Time) { +} + +// RecordPodCombGenTime as a noop +func (me *Metrics) RecordPodCombGenTime(labels PodLabels, elapsedTime time.Duration) { +} + +// RecordPodCompititveExclusionTime as a noop +func (me *Metrics) RecordPodCompititveExclusionTime(labels PodLabels, elapsedTime time.Duration) { +} + func doMark(bidder openrtb_ext.BidderName, meters map[openrtb_ext.BidderName]metrics.Meter) { met, ok := meters[bidder] if ok { diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index 770f5750335..430849deca0 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -36,6 +36,15 @@ type ImpLabels struct { NativeImps bool } +// PodLabels defines metric labels describing algorithm type +// and other labels as per scenario +type PodLabels struct { + AlgorithmName string // AlgorithmName which is used for generating impressions + NoOfImpressions *int // NoOfImpressions represents number of impressions generated + NoOfCombinations *int // NoOfCombinations represents number of combinations generated + NoOfResponseBids *int // NoOfResponseBids represents number of bids responded (including bids with similar duration) +} + // RequestLabels defines metric labels describing the result of a network request. type RequestLabels struct { RequestStatus RequestStatus @@ -276,4 +285,26 @@ type MetricsEngine interface { RecordPrebidCacheRequestTime(success bool, length time.Duration) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) RecordTimeoutNotice(sucess bool) + // ad pod specific metrics + + // RecordPodImpGenTime records number of impressions generated and time taken + // by underneath algorithm to generate them + // labels accept name of the algorithm and no of impressions generated + // startTime indicates the time at which algorithm started + // This function will take care of computing the elpased time + RecordPodImpGenTime(labels PodLabels, startTime time.Time) + + // RecordPodCombGenTime records number of combinations generated and time taken + // by underneath algorithm to generate them + // labels accept name of the algorithm and no of combinations generated + // elapsedTime indicates the time taken by combination generator to compute all requested combinations + // This function will take care of computing the elpased time + RecordPodCombGenTime(labels PodLabels, elapsedTime time.Duration) + + // RecordPodCompititveExclusionTime records time take by competitive exclusion + // to compute the final Ad pod Response. + // labels accept name of the algorithm and no of combinations evaluated, total bids + // elapsedTime indicates the time taken by competitive exclusion to form final ad pod response using combinations and exclusion algorithm + // This function will take care of computing the elpased time + RecordPodCompititveExclusionTime(labels PodLabels, elapsedTime time.Duration) } diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index 9c06d6032f4..cde0cab0283 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -42,6 +42,20 @@ type Metrics struct { // Account Metrics accountRequests *prometheus.CounterVec + + // Ad Pod Metrics + + // podImpGenTimer indicates time taken by impression generator + // algorithm to generate impressions for given ad pod request + podImpGenTimer *prometheus.HistogramVec + + // podImpGenTimer indicates time taken by combination generator + // algorithm to generate combination based on bid response and ad pod request + podCombGenTimer *prometheus.HistogramVec + + // podCompExclTimer indicates time taken by compititve exclusion + // algorithm to generate final pod response based on bid response and ad pod request + podCompExclTimer *prometheus.HistogramVec } const ( @@ -85,6 +99,14 @@ const ( requestFailed = "failed" ) +// pod specific constants +const ( + podAlgorithm = "algorithm" + podNoOfImpressions = "no_of_impressions" + podTotalCombinations = "total_combinations" + podNoOfResponseBids = "no_of_response_bids" +) + // 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} @@ -211,6 +233,28 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { []string{requestTypeLabel, requestStatusLabel}, queuedRequestTimeBuckets) + // adpod specific metrics + metrics.podImpGenTimer = newHistogram(cfg, metrics.Registry, + "impr_gen", + "Time taken by Ad Pod Impression Generator in seconds", []string{podAlgorithm, podNoOfImpressions}, + // 200 µS, 250 µS, 275 µS, 300 µS + //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) + // 100 µS, 200 µS, 300 µS, 400 µS, 500 µS, 600 µS, + []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) + + metrics.podCombGenTimer = newHistogram(cfg, metrics.Registry, + "comb_gen", + "Time taken by Ad Pod Combination Generator in seconds", []string{podAlgorithm, podTotalCombinations}, + // 200 µS, 250 µS, 275 µS, 300 µS + //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) + []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) + + metrics.podCompExclTimer = newHistogram(cfg, metrics.Registry, + "comp_excl", + "Time taken by Ad Pod Compititve Exclusion in seconds", []string{podAlgorithm, podNoOfResponseBids}, + // 200 µS, 250 µS, 275 µS, 300 µS + //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) + []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) preloadLabelValues(&metrics) return &metrics @@ -421,3 +465,44 @@ func (m *Metrics) RecordTimeoutNotice(success bool) { }).Inc() } } + +// pod specific metrics + +// recordAlgoTime is common method which handles algorithm time performance +func recordAlgoTime(timer *prometheus.HistogramVec, labels pbsmetrics.PodLabels, elapsedTime time.Duration) { + + pmLabels := prometheus.Labels{ + podAlgorithm: labels.AlgorithmName, + } + + if labels.NoOfImpressions != nil { + pmLabels[podNoOfImpressions] = strconv.Itoa(*labels.NoOfImpressions) + } + if labels.NoOfCombinations != nil { + pmLabels[podTotalCombinations] = strconv.Itoa(*labels.NoOfCombinations) + } + if labels.NoOfResponseBids != nil { + pmLabels[podNoOfResponseBids] = strconv.Itoa(*labels.NoOfResponseBids) + } + + timer.With(pmLabels).Observe(elapsedTime.Seconds()) +} + +// RecordPodImpGenTime records number of impressions generated and time taken +// by underneath algorithm to generate them +func (m *Metrics) RecordPodImpGenTime(labels pbsmetrics.PodLabels, start time.Time) { + elapsedTime := time.Since(start) + recordAlgoTime(m.podImpGenTimer, labels, elapsedTime) +} + +// RecordPodCombGenTime records number of combinations generated and time taken +// by underneath algorithm to generate them +func (m *Metrics) RecordPodCombGenTime(labels pbsmetrics.PodLabels, elapsedTime time.Duration) { + recordAlgoTime(m.podCombGenTimer, labels, elapsedTime) +} + +// RecordPodCompititveExclusionTime records number of combinations comsumed for forming +// final ad pod response and time taken by underneath algorithm to generate them +func (m *Metrics) RecordPodCompititveExclusionTime(labels pbsmetrics.PodLabels, elapsedTime time.Duration) { + recordAlgoTime(m.podCompExclTimer, labels, elapsedTime) +} diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index 21f182e2094..ba187603b60 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -1,6 +1,7 @@ package prometheusmetrics import ( + "strconv" "testing" "time" @@ -944,6 +945,46 @@ func TestTimeoutNotifications(t *testing.T) { } +func TestRecordPodImpGenTime(t *testing.T) { + impressions := 4 + testAlgorithmMetrics(t, impressions, func(m *Metrics) dto.Histogram { + m.RecordPodImpGenTime(pbsmetrics.PodLabels{AlgorithmName: "sample_imp_algo", NoOfImpressions: &impressions}, time.Now()) + return getHistogramFromHistogramVec(m.podImpGenTimer, podNoOfImpressions, strconv.Itoa(impressions)) + }) +} + +func TestRecordPodCombGenTime(t *testing.T) { + combinations := 5 + testAlgorithmMetrics(t, combinations, func(m *Metrics) dto.Histogram { + m.RecordPodCombGenTime(pbsmetrics.PodLabels{AlgorithmName: "sample_comb_algo", NoOfCombinations: &combinations}, time.Now()) + return getHistogramFromHistogramVec(m.podCombGenTimer, podTotalCombinations, strconv.Itoa(combinations)) + }) +} + +func TestRecordPodCompetitiveExclusionTime(t *testing.T) { + totalBids := 8 + testAlgorithmMetrics(t, totalBids, func(m *Metrics) dto.Histogram { + m.RecordPodCompititveExclusionTime(pbsmetrics.PodLabels{AlgorithmName: "sample_comt_excl_algo", NoOfResponseBids: &totalBids}, time.Now()) + return getHistogramFromHistogramVec(m.podCompExclTimer, podNoOfResponseBids, strconv.Itoa(totalBids)) + }) +} + +func testAlgorithmMetrics(t *testing.T, input int, f func(m *Metrics) dto.Histogram) { + // test input + adRequests := 2 + m := createMetricsForTesting() + var result dto.Histogram + for req := 1; req <= adRequests; req++ { + result = f(m) + } + + // assert observations + assert.Equal(t, uint64(adRequests), result.GetSampleCount(), "ad requests : count") + for _, bucket := range result.Bucket { + assert.Equal(t, uint64(adRequests), bucket.GetCumulativeCount(), "total observations") + } +} + func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) { m := dto.Metric{} counter.Write(&m) diff --git a/router/router.go b/router/router.go index 6da9800ba43..843fda9ab25 100644 --- a/router/router.go +++ b/router/router.go @@ -6,6 +6,7 @@ import ( "database/sql" "encoding/json" "fmt" + "github.com/prometheus/client_golang/prometheus" "io/ioutil" "net/http" "path/filepath" @@ -422,3 +423,12 @@ func readDefaultRequest(defReqConfig config.DefReqConfig) (map[string]string, [] } return aliases, []byte{} } + +func GetPrometheusRegistry() *prometheus.Registry { + mEngine, ok := g_metrics.(*metricsConf.DetailedMetricsEngine) + if !ok || mEngine == nil || mEngine.PrometheusMetrics == nil { + return nil + } + + return mEngine.PrometheusMetrics.Registry +} From db9a64382c715fe11005af49f5aada690c904e4b Mon Sep 17 00:00:00 2001 From: ShriprasadM Date: Fri, 14 Aug 2020 18:28:00 +0530 Subject: [PATCH 168/381] UOE-5440: Fixed the Unit test issues (#72) Fixed unit test issues Co-authored-by: Sachin Survase Co-authored-by: PubMatic-OpenWrap Co-authored-by: Shriprasad --- pbsmetrics/metrics_mock.go | 15 +++++++++++++++ pbsmetrics/prometheus/prometheus_test.go | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go index d5661f4bfe4..946900ee202 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -106,3 +106,18 @@ func (me *MetricsEngineMock) RecordRequestQueueTime(success bool, requestType Re func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) { me.Called(success) } + +// RecordPodImpGenTime mock +func (me *MetricsEngineMock) RecordPodImpGenTime(labels PodLabels, startTime time.Time) { + me.Called(labels, startTime) +} + +// RecordPodCombGenTime mock +func (me *MetricsEngineMock) RecordPodCombGenTime(labels PodLabels, elapsedTime time.Duration) { + me.Called(labels, elapsedTime) +} + +// RecordPodCompititveExclusionTime mock +func (me *MetricsEngineMock) RecordPodCompititveExclusionTime(labels PodLabels, elapsedTime time.Duration) { + me.Called(labels, elapsedTime) +} diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index ba187603b60..ed9a81caef6 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -956,7 +956,7 @@ func TestRecordPodImpGenTime(t *testing.T) { func TestRecordPodCombGenTime(t *testing.T) { combinations := 5 testAlgorithmMetrics(t, combinations, func(m *Metrics) dto.Histogram { - m.RecordPodCombGenTime(pbsmetrics.PodLabels{AlgorithmName: "sample_comb_algo", NoOfCombinations: &combinations}, time.Now()) + m.RecordPodCombGenTime(pbsmetrics.PodLabels{AlgorithmName: "sample_comb_algo", NoOfCombinations: &combinations}, time.Since(time.Now())) return getHistogramFromHistogramVec(m.podCombGenTimer, podTotalCombinations, strconv.Itoa(combinations)) }) } @@ -964,7 +964,7 @@ func TestRecordPodCombGenTime(t *testing.T) { func TestRecordPodCompetitiveExclusionTime(t *testing.T) { totalBids := 8 testAlgorithmMetrics(t, totalBids, func(m *Metrics) dto.Histogram { - m.RecordPodCompititveExclusionTime(pbsmetrics.PodLabels{AlgorithmName: "sample_comt_excl_algo", NoOfResponseBids: &totalBids}, time.Now()) + m.RecordPodCompititveExclusionTime(pbsmetrics.PodLabels{AlgorithmName: "sample_comt_excl_algo", NoOfResponseBids: &totalBids}, time.Since(time.Now())) return getHistogramFromHistogramVec(m.podCompExclTimer, podNoOfResponseBids, strconv.Itoa(totalBids)) }) } From 5a7d3652d448e179be7add376047b562c115d0ef Mon Sep 17 00:00:00 2001 From: chino117 Date: Mon, 17 Aug 2020 11:09:22 -0300 Subject: [PATCH 169/381] Eplanning adapter: Get domain from page (#1434) --- adapters/eplanning/eplanning.go | 25 ++++--- .../supplemental/bad-page-site.json | 31 ++++++++ .../site-page-and-url-correctly-parsed.json | 75 +++++++++++++++++++ 3 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 adapters/eplanning/eplanningtest/supplemental/bad-page-site.json create mode 100644 adapters/eplanning/eplanningtest/supplemental/site-page-and-url-correctly-parsed.json diff --git a/adapters/eplanning/eplanning.go b/adapters/eplanning/eplanning.go index 2a46b5469e0..032edfd1b06 100644 --- a/adapters/eplanning/eplanning.go +++ b/adapters/eplanning/eplanning.go @@ -104,25 +104,28 @@ func (adapter *EPlanningAdapter) MakeRequests(request *openrtb.BidRequest, reqIn } } - var pageURL string + pageURL := defaultPageURL if request.Site != nil && request.Site.Page != "" { pageURL = request.Site.Page - } else { - pageURL = defaultPageURL } - var pageDomain string - if request.Site != nil && request.Site.Domain != "" { - pageDomain = request.Site.Domain - } else { - pageDomain = defaultPageURL + pageDomain := defaultPageURL + if request.Site != nil { + if request.Site.Domain != "" { + pageDomain = request.Site.Domain + } else if request.Site.Page != "" { + u, err := url.Parse(request.Site.Page) + if err != nil { + errors = append(errors, err) + return nil, errors + } + pageDomain = u.Hostname() + } } - var requestTarget string + requestTarget := pageDomain if request.App != nil && request.App.Bundle != "" { requestTarget = request.App.Bundle - } else { - requestTarget = pageDomain } uriObj, err := url.Parse(adapter.URI) diff --git a/adapters/eplanning/eplanningtest/supplemental/bad-page-site.json b/adapters/eplanning/eplanningtest/supplemental/bad-page-site.json new file mode 100644 index 00000000000..5efe604f7e6 --- /dev/null +++ b/adapters/eplanning/eplanningtest/supplemental/bad-page-site.json @@ -0,0 +1,31 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 600, + "h": 300 + }, + "ext": { + "bidder": { + "ci": "12345", + "adunit_code": "test_adunitcode" + } + } + } + ], + "site": { + "page": "http://www.page%test.com" + } + }, + + "expectedMakeRequestsErrors": [ + { + "value": "parse (\\\")?http://www.page%test.com(\\\")?: invalid URL escape \\\"%te\\\"", + "comparison": "regex" + } + ] +} + diff --git a/adapters/eplanning/eplanningtest/supplemental/site-page-and-url-correctly-parsed.json b/adapters/eplanning/eplanningtest/supplemental/site-page-and-url-correctly-parsed.json new file mode 100644 index 00000000000..20a419cdbfd --- /dev/null +++ b/adapters/eplanning/eplanningtest/supplemental/site-page-and-url-correctly-parsed.json @@ -0,0 +1,75 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 600, + "h": 300 + }, + "ext": { + "bidder": { + "ci": "12345", + "adunit_code": "test_adunitcode" + } + } + } + ], + "site": { + "page": "http://www.publisher.com/awesome/site?with=some¶meters=here" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.e-planning.net/pbs/1/12345/1/www.publisher.com/ROS?e=testadunitcode%3A600x300&ncb=1&ur=http%3A%2F%2Fwww.publisher.com%2Fawesome%2Fsite%3Fwith%3Dsome%26parameters%3Dhere", + "body": {} + }, + "mockResponse": { + "status": 200, + "body": { + "sI": { "k": "12345" }, + "sec": "ROS", + "sp": [ + { + "k": "testadunitcode", + "a": [{ + "i": "123456789abcdef", + "pr": "0.5", + "adm": "
test
", + "crid": "abcdef123456789", + "id": "adid12345", + "w": 600, + "h": 300 + }] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "123456789abcdef", + "impid": "test-imp-id", + "price": 0.5, + "adm": "
test
", + "adid": "adid12345", + "crid": "abcdef123456789", + "w": 600, + "h": 300 + }, + "type": "banner" + } + ] + } + ] + } + From e065488276139a56adf9500908bb93fc70c1ac91 Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Mon, 17 Aug 2020 08:17:15 -0700 Subject: [PATCH 170/381] Fix no bid debug log (#1375) --- endpoints/openrtb2/video_auction.go | 45 +++++------- endpoints/openrtb2/video_auction_test.go | 71 ++++++++++++++++++ exchange/auction.go | 51 ++++++++++++- .../debuglog_enabled_no_winners_nor_bids.json | 54 ++++++++++++++ exchange/exchange.go | 18 +++++ .../debuglog_enabled_no_bids.json | 72 +++++++++++++++++++ openrtb_ext/bid_response_video.go | 6 +- 7 files changed, 283 insertions(+), 34 deletions(-) create mode 100644 exchange/cachetest/debuglog_enabled_no_winners_nor_bids.json create mode 100644 exchange/exchangetest/debuglog_enabled_no_bids.json diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 49ba287610b..a6ca527874a 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -122,7 +122,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re defer func() { if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil { - err := putDebugLogError(deps.cache, &debugLog, start) + err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors) if err != nil { vo.Errors = append(vo.Errors, err) } @@ -279,6 +279,21 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re bidResp.Ext = response.Ext } + if len(bidResp.AdPods) == 0 && debugLog.Enabled { + err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors) + if err != nil { + vo.Errors = append(vo.Errors, err) + } else { + bidResp.AdPods = append(bidResp.AdPods, &openrtb_ext.AdPod{ + Targeting: []openrtb_ext.VideoTargeting{ + { + HbCacheID: debugLog.CacheKey, + }, + }, + }) + } + } + vo.VideoResponse = bidResp resp, err := json.Marshal(bidResp) @@ -294,34 +309,6 @@ 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:]...) diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index b15c6a7b47a..534db3c79e2 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -284,6 +284,42 @@ func TestVideoEndpointDebugError(t *testing.T) { assert.Equal(t, recorder.Code, 500, "Should catch error in request") } +func TestVideoEndpointDebugNoAdPods(t *testing.T) { + ex := &mockExchangeVideoNoBids{ + 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 := mockDepsNoBids(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, resp.AdPods, 1, "Debug AdPod should be added to response") + assert.Empty(t, resp.AdPods[0].Errors, "AdPod Errors should be empty") + assert.Empty(t, resp.AdPods[0].Targeting[0].HbPb, "Hb_pb should be empty") + assert.Empty(t, resp.AdPods[0].Targeting[0].HbPbCatDur, "Hb_pb_cat_dur should be empty") + assert.NotEmpty(t, resp.AdPods[0].Targeting[0].HbCacheID, "Hb_cache_id should not be empty") + assert.Equal(t, int64(0), resp.AdPods[0].PodId, "Pod ID should be 0") +} + func TestVideoEndpointNoPods(t *testing.T) { ex := &mockExchangeVideo{} reqData, err := ioutil.ReadFile("sample-requests/video/video_invalid_sample.json") @@ -1189,6 +1225,29 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { return deps } +func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps { + theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + edep := &endpointDeps{ + ex, + newParamsValidator(t), + &mockVideoStoredReqFetcher{}, + &mockVideoStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + theMetrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + ex.cache, + regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, + } + + return edep +} + type mockCacheClient struct { called bool } @@ -1247,6 +1306,18 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb }, nil } +type mockExchangeVideoNoBids struct { + lastRequest *openrtb.BidRequest + cache *mockCacheClient +} + +func (m *mockExchangeVideoNoBids) 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{{}}, + }, nil +} + var testVideoStoredImpData = map[string]json.RawMessage{ "fba10607-0c12-43d1-ad07-b8a513bc75d6": json.RawMessage(`{"ext": {"appnexus": {"placementId": 14997137}}}`), "8b452b41-2681-4a20-9086-6f16ffad7773": json.RawMessage(`{"ext": {"appnexus": {"placementId": 15016213}}}`), diff --git a/exchange/auction.go b/exchange/auction.go index 45e1422540e..aa446ddba13 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -8,6 +8,7 @@ import ( "fmt" "regexp" "strings" + "time" uuid "github.com/gofrs/uuid" "github.com/golang/glog" @@ -47,6 +48,52 @@ func (d *DebugLog) BuildCacheString() { d.CacheString = fmt.Sprintf("%s%s%s%s", xml.Header, d.Data.Request, d.Data.Headers, d.Data.Response) } +func (d *DebugLog) PutDebugLogError(cache prebid_cache_client.Client, timeout int, errors []error) error { + if len(d.Data.Response) == 0 && len(errors) == 0 { + d.Data.Response = "No response or errors created" + } + + if len(errors) > 0 { + errStrings := []string{} + for _, err := range errors { + errStrings = append(errStrings, err.Error()) + } + d.Data.Response = fmt.Sprintf("%s\nErrors:\n%s", d.Data.Response, strings.Join(errStrings, "\n")) + } + + d.BuildCacheString() + + if len(d.CacheKey) == 0 { + rawUUID, err := uuid.NewV4() + if err != nil { + return err + } + d.CacheKey = rawUUID.String() + } + + data, err := json.Marshal(d.CacheString) + if err != nil { + return err + } + + toCache := []prebid_cache_client.Cacheable{ + { + Type: d.CacheType, + Data: data, + TTLSeconds: d.TTL, + Key: "log_" + d.CacheKey, + }, + } + + if cache != nil { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(timeout)*time.Millisecond)) + defer cancel() + cache.PutJson(ctx, toCache) + } + + return nil +} + 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) @@ -179,9 +226,9 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, } } - if debugLog != nil && debugLog.Enabled { - debugLog.BuildCacheString() + if len(toCache) > 0 && debugLog != nil && debugLog.Enabled { debugLog.CacheKey = hbCacheID + debugLog.BuildCacheString() if jsonBytes, err := json.Marshal(debugLog.CacheString); err == nil { toCache = append(toCache, prebid_cache_client.Cacheable{ Type: debugLog.CacheType, diff --git a/exchange/cachetest/debuglog_enabled_no_winners_nor_bids.json b/exchange/cachetest/debuglog_enabled_no_winners_nor_bids.json new file mode 100644 index 00000000000..637b33e171b --- /dev/null +++ b/exchange/cachetest/debuglog_enabled_no_winners_nor_bids.json @@ -0,0 +1,54 @@ +{ + "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": [], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners": false, + "targetDataIncludeBidderKeys": false, + "targetDataIncludeCacheBids": true, + "targetDataIncludeCacheVast": false +} \ No newline at end of file diff --git a/exchange/exchange.go b/exchange/exchange.go index 57e13644163..cf5ec9cc000 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -14,6 +14,7 @@ import ( "strings" "time" + uuid "github.com/gofrs/uuid" "github.com/prebid/prebid-server/stored_requests" "github.com/golang/glog" @@ -192,6 +193,23 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque } + if !anyBidsReturned { + if debugLog != nil && debugLog.Enabled { + if rawUUID, err := uuid.NewV4(); err == nil { + debugLog.CacheKey = rawUUID.String() + + bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, 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" + } + } else { + errs = append(errs, err) + } + } + } + // Build the response return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, adapterExtra, auc, bidResponseExt, errs) } diff --git a/exchange/exchangetest/debuglog_enabled_no_bids.json b/exchange/exchangetest/debuglog_enabled_no_bids.json new file mode 100644 index 00000000000..4823acf8f16 --- /dev/null +++ b/exchange/exchangetest/debuglog_enabled_no_bids.json @@ -0,0 +1,72 @@ +{ + "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 + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + } + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": {} + } + } + }, + "response": { + "bids": {} + } +} \ No newline at end of file diff --git a/openrtb_ext/bid_response_video.go b/openrtb_ext/bid_response_video.go index 4c123498ec8..22661547ca7 100644 --- a/openrtb_ext/bid_response_video.go +++ b/openrtb_ext/bid_response_video.go @@ -14,7 +14,7 @@ type AdPod struct { } type VideoTargeting struct { - HbPb string `json:"hb_pb"` - HbPbCatDur string `json:"hb_pb_cat_dur"` - HbCacheID string `json:"hb_cache_id"` + HbPb string `json:"hb_pb,omitempty"` + HbPbCatDur string `json:"hb_pb_cat_dur,omitempty"` + HbCacheID string `json:"hb_cache_id,omitempty"` } From 2e9d8337d32971d4c8e03b3a68dd69d782610cb6 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Mon, 17 Aug 2020 12:09:51 -0400 Subject: [PATCH 171/381] Update the fallback GVL to last version (#1440) --- gdpr/vendorlist-fetching_test.go | 4 ++-- static/tcf1/fallback_gvl.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index 031e564094c..484a0a54b41 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -165,7 +165,7 @@ func TestDefaultVendorList(t *testing.T) { list, err := fetcher(context.Background(), 12) assert.NoError(t, err, "Error with fetching default vendorlist: %v", err) - assert.Equal(t, uint16(214), list.Version(), "Expected to fetch default version 214, got %d", list.Version()) + assert.Equal(t, uint16(215), list.Version(), "Expected to fetch default version 215, got %d", list.Version()) // Testing that we got the default vendorlist data, and not the version off the server. vendor := list.Vendor(12) @@ -227,7 +227,7 @@ func TestFallbackVendorListNoFetch(t *testing.T) { fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1) list, err := fetcher(context.Background(), 2) assert.NoError(t, err, "Error with fetching default vendorlist: %v", err) - assert.Equal(t, uint16(214), list.Version(), "Expected to fetch default version 214, got %d", list.Version()) + assert.Equal(t, uint16(215), list.Version(), "Expected to fetch default version 215, got %d", list.Version()) // Testing that we got the default vendorlist data, and not the version off the server. vendor := list.Vendor(12) diff --git a/static/tcf1/fallback_gvl.json b/static/tcf1/fallback_gvl.json index 86895a52362..9f1c8506b32 100644 --- a/static/tcf1/fallback_gvl.json +++ b/static/tcf1/fallback_gvl.json @@ -1 +1 @@ -{"vendorListVersion":214,"lastUpdated":"2020-08-06T16:00:35Z","purposes":[{"id":1,"name":"Information storage and access","description":"The storage of information, or access to information that is already stored, on your device such as advertising identifiers, device identifiers, cookies, and similar technologies."},{"id":2,"name":"Personalisation","description":"The collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as on other websites or apps, over time. Typically, the content of the site or app is used to make inferences about your interests, which inform future selection of advertising and/or content."},{"id":3,"name":"Ad selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver advertisements for you, and to measure the delivery and effectiveness of such advertisements. This includes using previously collected information about your interests to select ads, processing data about what advertisements were shown, how often they were shown, when and where they were shown, and whether you took any action related to the advertisement, including for example clicking an ad or making a purchase. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as websites or apps, over time."},{"id":4,"name":"Content selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver content for you, and to measure the delivery and effectiveness of such content. This includes using previously collected information about your interests to select content, processing data about what content was shown, how often or how long it was shown, when and where it was shown, and whether the you took any action related to the content, including for example clicking on content. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, such as websites or apps, over time."},{"id":5,"name":"Measurement","description":"The collection of information about your use of the content, and combination with previously collected information, used to measure, understand, and report on your usage of the service. This does not include personalisation, the collection of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, i.e. on other service, such as websites or apps, over time."}],"features":[{"id":1,"name":"Matching Data to Offline Sources","description":"Combining data from offline sources that were initially collected in other contexts."},{"id":2,"name":"Linking Devices","description":"Allow processing of a user's data to connect such user across multiple devices."},{"id":3,"name":"Precise Geographic Location Data","description":"Allow processing of a user's precise geographic location data in support of a purpose for which that certain third party has consent."}],"vendors":[{"id":8,"name":"Emerse Sverige AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.emerse.com/privacy-policy/"},{"id":9,"name":"AdMaxim Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.admaxim.com/admaxim-privacy-policy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":12,"name":"BeeswaxIO Corporation","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beeswax.com/privacy/"},{"id":28,"name":"TripleLift, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://triplelift.com/privacy/"},{"id":27,"name":"ADventori SAS","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adventori.com/with-us/legal-notice/"},{"id":25,"name":"Verizon Media EMEA Limited","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.verizonmedia.com/policies/ie/en/verizonmedia/privacy/index.html"},{"id":26,"name":"Venatus Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.venatusmedia.com/privacy/"},{"id":1,"name":"Exponential Interactive, Inc d/b/a VDX.tv","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://vdx.tv/privacy/"},{"id":6,"name":"AdSpirit GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adspirit.de/privacy"},{"id":30,"name":"BidTheatre AB","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.bidtheatre.com/privacy-policy"},{"id":24,"name":"Epsilon","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.conversantmedia.eu/legal/privacy-policy"},{"id":29,"name":"Etarget SE","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.etarget.sk/privacy.php","deletedDate":"2020-06-01T00:00:00Z"},{"id":39,"name":"ADITION technologies AG","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.adition.com/datenschutz"},{"id":11,"name":"Quantcast International Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.quantcast.com/privacy/"},{"id":15,"name":"Adikteev","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adikteev.com/privacy-policy-eng/"},{"id":4,"name":"Roq.ad Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.roq.ad/privacy-policy"},{"id":7,"name":"Vibrant Media Limited","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vibrantmedia.com/en/privacy-policy/"},{"id":2,"name":"Captify Technologies Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.captify.co.uk/privacy-policy/"},{"id":37,"name":"NEURAL.ONE","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://web.neural.one/privacy-policy/"},{"id":13,"name":"Sovrn Holdings Inc","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sovrn.com/sovrn-privacy/"},{"id":34,"name":"NEORY GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.neory.com/privacy.html"},{"id":32,"name":"Xandr, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.xandr.com/privacy/platform-privacy-policy/"},{"id":10,"name":"Index Exchange, Inc. ","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.indexexchange.com/privacy"},{"id":57,"name":"ADARA MEDIA UNLIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://adara.com/privacy-promise/"},{"id":63,"name":"Avocet Systems Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://avocet.io/privacy-portal"},{"id":51,"name":"xAd, Inc. dba GroundTruth","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.groundtruth.com/privacy-policy/"},{"id":49,"name":"TRADELAB","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://tradelab.com/en/privacy/"},{"id":45,"name":"Smart Adserver","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://smartadserver.com/end-user-privacy-policy/"},{"id":52,"name":"The Rubicon Project, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[3],"policyUrl":"http://www.rubiconproject.com/rubicon-project-yield-optimization-privacy-policy/"},{"id":71,"name":"Roku Advertising Services","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://docs.roku.com/published/userprivacypolicy/en/us"},{"id":79,"name":"MediaMath, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.mediamath.com/privacy-policy/"},{"id":91,"name":"Criteo SA","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.criteo.com/privacy/"},{"id":85,"name":"Crimtan Holdings Limited","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[1,3],"policyUrl":"https://crimtan.com/privacy/"},{"id":16,"name":"RTB House S.A.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.rtbhouse.com/privacy-center/services-privacy-policy/"},{"id":86,"name":"Scene Stealer Limited","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"http://scenestealer.tv/privacy-policy/"},{"id":94,"name":"Blis Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.blis.com/privacy/"},{"id":73,"name":"Simplifi Holdings Inc.","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2,3],"policyUrl":"https://simpli.fi/site-privacy-policy/"},{"id":33,"name":"ShareThis, Inc","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://sharethis.com/privacy/"},{"id":20,"name":"N Technologies Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://n.rich/privacy-notice"},{"id":55,"name":"Madison Logic, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.madisonlogic.com/privacy/"},{"id":53,"name":"Sirdata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.sirdata.com/privacy/"},{"id":69,"name":"OpenX","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.openx.com/legal/privacy-policy/"},{"id":98,"name":"GroupM UK Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.groupm.com/privacy-notice"},{"id":62,"name":"Justpremium BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://justpremium.com/privacy-policy/"},{"id":19,"name":"Intent Media, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://intentmedia.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":43,"name":"Vdopia DBA Chocolate Platform","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://chocolateplatform.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":36,"name":"RhythmOne DBA Unruly Group Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.rhythmone.com/privacy-policy"},{"id":80,"name":"Sharethrough, Inc","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://platform-cdn.sharethrough.com/privacy-policy"},{"id":81,"name":"PulsePoint, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pulsepoint.com/privacy-policy/website","deletedDate":"2020-07-06T00:00:00Z"},{"id":23,"name":"Amobee, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.amobee.com/trust/privacy-guidelines"},{"id":35,"name":"Purch Group, Inc.","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://www.purch.com/privacy-policy/","deletedDate":"2019-05-30T00:00:00Z"},{"id":3,"name":"affilinet","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.affili.net/de/footeritem/datenschutz","deletedDate":"2019-06-21T00:00:00Z"},{"id":74,"name":"Admotion SRL","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.admotion.com/policy/","deletedDate":"2019-07-24T00:00:00Z"},{"id":191,"name":"realzeit GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://realzeitmedia.com/privacy.html","deletedDate":"2019-04-29T00:00:00Z"},{"id":197,"name":"Switch Concepts Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.switchconcepts.com/privacy-policy","deletedDate":"2019-07-26T00:00:00Z"},{"id":390,"name":"Parsec Media Inc.","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,3],"policyUrl":"www.parsec.media/privacy-policy","deletedDate":"2019-06-27T00:00:00Z"},{"id":459,"name":"uppr GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://netzwerk.uppr.de/privacy-policy.do","deletedDate":"2019-06-17T00:00:00Z"},{"id":221,"name":"LEMO MEDIA GROUP LIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.lemomedia.com/terms.pdf","deletedDate":"2019-06-28T00:00:00Z"},{"id":478,"name":"RevLifter Ltd","purposeIds":[1],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.revlifter.com/privacy-policy","deletedDate":"2019-07-15T00:00:00Z"},{"id":500,"name":"Turbo","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.turboadv.com/white-rabbit-privacy-policy/","deletedDate":"2019-07-12T00:00:00Z"},{"id":68,"name":"Sizmek by Amazon","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.sizmek.com/privacy-policy/"},{"id":75,"name":"M32 Connect Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://m32.media/privacy-cookie-policy/"},{"id":17,"name":"Greenhouse Group BV (with its trademark LemonPI)","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.lemonpi.io/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":61,"name":"GumGum, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://gumgum.com/privacy-policy"},{"id":40,"name":"Active Agent (ADITION technologies AG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.active-agent.com/de/unternehmen/datenschutzerklaerung/"},{"id":76,"name":"PubMatic, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://pubmatic.com/privacy-policy/"},{"id":89,"name":"Tapad, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.tapad.com/eu-privacy-policy"},{"id":46,"name":"Skimbit Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://skimlinks.com/pages/privacy-policy"},{"id":66,"name":"adsquare GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adsquare.com/privacy"},{"id":105,"name":"Impression Desk Technologies Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://impressiondesk.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":41,"name":"Adverline","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.adverline.com/privacy/"},{"id":82,"name":"Smaato, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.smaato.com/privacy/"},{"id":60,"name":"Rakuten Marketing LLC","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://rakutenadvertising.com/legal-notices/services-privacy-policy/"},{"id":70,"name":"Yieldlab AG","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[3],"policyUrl":"http://www.yieldlab.de/meta-navigation/datenschutz/"},{"id":50,"name":"Adform","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://site.adform.com/privacy-center/platform-privacy/product-and-services-privacy-policy/"},{"id":48,"name":"NetSuccess, s.r.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inres.sk/pp/"},{"id":100,"name":"Fifty Technology Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://fifty.io/privacy-policy.php"},{"id":21,"name":"The Trade Desk","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.thetradedesk.com/general/privacy-policy"},{"id":110,"name":"Dynata LLC","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.opinionoutpost.co.uk/en-gb/policies/privacy"},{"id":42,"name":"Taboola Europe Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.taboola.com/privacy-policy"},{"id":112,"name":"Maytrics GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://maytrics.com/privacy.php","deletedDate":"2019-09-17T00:00:00Z"},{"id":77,"name":"comScore, Inc.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.scorecardresearch.com/privacy.aspx?newlanguage=1"},{"id":109,"name":"LoopMe Limited","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://loopme.com/privacy-policy/"},{"id":120,"name":"Eyeota Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.eyeota.com/privacy-center"},{"id":93,"name":"Adloox SA","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://adloox.com/disclaimer"},{"id":132,"name":"Teads ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.teads.com/privacy-policy/"},{"id":22,"name":"admetrics GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://admetrics.io/en/privacy_policy/"},{"id":102,"name":"Telaria SAS","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":108,"name":"Rich Audience Technologies SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://richaudience.com/privacy/"},{"id":18,"name":"Widespace AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.widespace.com/legal/privacy-policy-notice/"},{"id":122,"name":"Avid Media Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.avidglobalmedia.eu/privacy-policy.html"},{"id":97,"name":"LiveRamp, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.liveramp.com/service-privacy-policy/"},{"id":138,"name":"ConnectAd Realtime GmbH","purposeIds":[1,2],"legIntPurposeIds":[3,4],"featureIds":[],"policyUrl":"http://connectadrealtime.com/privacy/"},{"id":72,"name":"Nano Interactive GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.nanointeractive.com/privacy"},{"id":127,"name":"PIXIMEDIA SAS","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://piximedia.com/privacy/"},{"id":136,"name":"Str\u00f6er SSP GmbH (SSP)","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[2,3],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":111,"name":"Showheroes SE","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://showheroes.com/privacy/"},{"id":56,"name":"Confiant Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.confiant.com/privacy","deletedDate":"2020-05-18T00:00:00Z"},{"id":124,"name":"Teemo SA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://teemo.co/fr/confidentialite/"},{"id":154,"name":"YOC AG","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://yoc.com/privacy/"},{"id":38,"name":"Beemray Oy","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beemray.com/privacy-policy/","deletedDate":"2020-06-19T00:00:00Z"},{"id":101,"name":"MiQ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://wearemiq.com/privacy-policy/"},{"id":149,"name":"ADman Interactive SLU","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://admanmedia.com/politica.html?setLng=es"},{"id":151,"name":"Admedo Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[3],"policyUrl":"https://www.admedo.com/privacy-policy","deletedDate":"2020-07-17T00:00:00Z"},{"id":153,"name":"MADVERTISE MEDIA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://madvertise.com/en/gdpr/"},{"id":159,"name":"Underdog Media LLC ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://underdogmedia.com/privacy-policy/"},{"id":157,"name":"Seedtag Advertising S.L","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.seedtag.com/en/privacy-policy/"},{"id":145,"name":"Snapsort Inc., operating as Sortable","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://help.sortable.com/help/privacy-policy"},{"id":131,"name":"ID5 Technology SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.id5.io/privacy"},{"id":158,"name":"Reveal Mobile, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://revealmobile.com/privacy"},{"id":147,"name":"Adacado Technologies Inc. (DBA Adacado)","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adacado.com/privacy-policy-april-25-2018/"},{"id":130,"name":"NextRoll, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.nextroll.com/privacy"},{"id":129,"name":"IPONWEB GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.iponweb.com/privacy-policy/"},{"id":128,"name":"BIDSWITCH GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bidswitch.com/privacy-policy/"},{"id":168,"name":"EASYmedia GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://login.rtbmarket.com/gdpr"},{"id":164,"name":"Outbrain UK Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.outbrain.com/legal/privacy#privacy-policy"},{"id":144,"name":"district m inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://districtm.net/en/page/platforms-data-and-privacy-policy/"},{"id":163,"name":"Bombora Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://bombora.com/privacy"},{"id":173,"name":"Yieldmo, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.yieldmo.com/privacy/"},{"id":88,"name":"TreSensa, Inc.","purposeIds":[1,3],"legIntPurposeIds":[2,5],"featureIds":[1],"policyUrl":"https://www.tresensa.com/eu-privacy"},{"id":78,"name":"Flashtalking, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.flashtalking.com/privacypolicy/"},{"id":59,"name":"Sift Media, Inc","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.sift.co/privacy"},{"id":114,"name":"Sublime","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://ayads.co/privacy.php"},{"id":175,"name":"FORTVISION","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://fortvision.com/POC/index.html","deletedDate":"2019-08-09T00:00:00Z"},{"id":133,"name":"digitalAudience","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://digitalaudience.io/legal/privacy-cookies/"},{"id":14,"name":"Adkernel LLC","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://adkernel.com/privacy-policy/"},{"id":180,"name":"Thirdpresence Oy","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"http://www.thirdpresence.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":183,"name":"EMX Digital LLC","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://emxdigital.com/privacy/"},{"id":58,"name":"33Across","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.33across.com/privacy-policy"},{"id":140,"name":"Platform161","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://platform161.com/cookie-and-privacy-policy/"},{"id":90,"name":"Teroa S.A.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.e-planning.net/en/privacy.html"},{"id":141,"name":"1020, Inc. dba Placecast and Ericsson Emodo","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.emodoinc.com/privacy-policy/"},{"id":142,"name":"Media.net Advertising FZ-LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.media.net/en/privacy-policy"},{"id":209,"name":"Delta Projects AB","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[3],"policyUrl":"https://deltaprojects.com/data-collection-policy"},{"id":195,"name":"advanced store GmbH","purposeIds":[2,3],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.advanced-store.com/de/datenschutz/"},{"id":190,"name":"video intelligence AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.vi.ai/privacy-policy/"},{"id":84,"name":"Semasio GmbH","purposeIds":[],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"http://www.semasio.com/privacy-policy/"},{"id":65,"name":"Location Sciences AI Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.locationsciences.ai/privacy-policy/"},{"id":210,"name":"Zemanta, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1],"policyUrl":"http://www.zemanta.com/legal/privacy"},{"id":200,"name":"Tapjoy, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.tapjoy.com/legal/#privacy-policy"},{"id":188,"name":"Sellpoints Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://retargeter.com/service-privacy-policy/","deletedDate":"2019-09-17T00:00:00Z"},{"id":217,"name":"2KDirect, Inc. (dba iPromote)","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.ipromote.com/privacy-policy/"},{"id":156,"name":"Centro, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.centro.net/privacy-policy/"},{"id":194,"name":"Rezonence Limited","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://rezonence.com/privacy-policy/"},{"id":226,"name":"Publicis Media GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.publicismedia.de/datenschutz/"},{"id":198,"name":"SYNC","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://redirect.sync.tv/privacy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":227,"name":"ORTEC B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.ortecadscience.com/privacy-policy/"},{"id":225,"name":"Ligatus GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.ligatus.com/en/privacy-policy","deletedDate":"2020-06-19T00:00:00Z"},{"id":205,"name":"Adssets AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://adssets.com/policy/"},{"id":179,"name":"Collective Europe Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.collectiveuk.com/privacy.html"},{"id":31,"name":"Ogury Ltd.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://www.ogury.com/privacy-policy/"},{"id":92,"name":"1plusX AG","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.1plusx.com/privacy-policy/"},{"id":155,"name":"AntVoice","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.antvoice.com/en/privacypolicy/"},{"id":115,"name":"smartclip Europe GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://privacy-portal.smartclip.net/"},{"id":126,"name":"DoubleVerify Inc.\u200b","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.doubleverify.com/privacy/"},{"id":193,"name":"Mediasmart Mobile S.L.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://mediasmart.io/privacy/"},{"id":245,"name":"IgnitionOne","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.ignitionone.com/privacy-policy/","deletedDate":"2020-06-30T00:00:00Z"},{"id":213,"name":"emetriq GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.emetriq.com/datenschutz/"},{"id":244,"name":"Temelio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://temelio.com/vie-privee"},{"id":224,"name":"adrule mobile GmbH","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.adrule.net/de/datenschutz/"},{"id":174,"name":"A Million Ads Ltd","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.amillionads.com/privacy-policy"},{"id":192,"name":"remerge GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://remerge.io/privacy-policy.html"},{"id":232,"name":"Rockerbox, Inc","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"http://rockerbox.com/privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":256,"name":"Bounce Exchange, Inc","purposeIds":[1],"legIntPurposeIds":[2,4,5],"featureIds":[1,2],"policyUrl":"https://www.bouncex.com/privacy/"},{"id":234,"name":"ZBO Media","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zbo.media/mentions-legales/politique-de-confidentialite-service-publicitaire/"},{"id":246,"name":"Smartology Limited","purposeIds":[3],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://www.smartology.net/privacy-policy/"},{"id":241,"name":"OneTag Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.onetag.com/privacy/"},{"id":254,"name":"LiquidM Technology GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liquidm.com/privacy-policy/"},{"id":215,"name":"ARMIS SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://armis.tech/en/armis-personal-data-privacy-policy/"},{"id":167,"name":"Audiens S.r.l.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.audiens.com/privacy"},{"id":240,"name":"7Hops.com Inc. (ZergNet)","purposeIds":[],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://zergnet.com/privacy"},{"id":235,"name":"Bucksense Inc","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.bucksense.com/platform-privacy-policy/"},{"id":185,"name":"Bidtellect, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.bidtellect.com/privacy-policy/"},{"id":258,"name":"Adello Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.adello.com/privacy-policy/"},{"id":169,"name":"RTK.IO, Inc","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://www.rtk.io/privacy.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":208,"name":"Spotad","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.spotad.co/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":211,"name":"AdTheorent, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://adtheorent.com/privacy-policy"},{"id":229,"name":"Digitize New Media Ltd","purposeIds":[2,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitize.ie/online-privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":273,"name":"Bannerflow AB","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.bannerflow.com/privacy "},{"id":104,"name":"Sonobi, Inc","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"http://sonobi.com/privacy-policy/"},{"id":162,"name":"Unruly Group Ltd","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://unruly.co/privacy/"},{"id":249,"name":"Spolecznosci Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.spolecznosci.pl/polityka-prywatnosci"},{"id":125,"name":"Research Now Group, Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.valuedopinions.co.uk/privacy","deletedDate":"2019-09-17T00:00:00Z"},{"id":170,"name":"Goodway Group, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://goodwaygroup.com/privacy-policy/"},{"id":160,"name":"Netsprint SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://netsprint.eu/privacy.html"},{"id":189,"name":"Intowow Innovation Ltd.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.intowow.com/privacy/","deletedDate":"2019-08-12T00:00:00Z"},{"id":279,"name":"Mirando GmbH & Co KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://wwwmirando.de/datenschutz/"},{"id":269,"name":"Sanoma Media Finland","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://sanoma.fi/tietoa-meista/tietosuoja/","deletedDate":"2019-08-07T00:00:00Z"},{"id":276,"name":"Viralize SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://viralize.com/privacy-policy"},{"id":87,"name":"Genius Sports Media Limited","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[2,3],"policyUrl":"https://www.geniussports.com/privacy-policy"},{"id":182,"name":"Collective, Inc. dba Visto","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vistohub.com/privacy-policy/","deletedDate":"2019-07-26T00:00:00Z"},{"id":255,"name":"Onnetwork Sp. z o.o.","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.onnetwork.tv/pp_services.php"},{"id":203,"name":"Revcontent, LLC","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://intercom.help/revcontent2/en/articles/2290675-revcontent-s-privacy-policy"},{"id":260,"name":"RockYou, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,5],"featureIds":[3],"policyUrl":"https://rockyou.com/privacy-policy/","deletedDate":"2019-08-09T00:00:00Z"},{"id":237,"name":"LKQD, a division of Nexstar Digital, LLC.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.lkqd.com/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":274,"name":"Golden Bees","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.goldenbees.fr/en/privacy-charter/"},{"id":280,"name":"Spot.IM LTD","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.spot.im/privacy/"},{"id":239,"name":"Triton Digital Canada Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.tritondigital.com/privacy-policies"},{"id":177,"name":"plista GmbH","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.plista.com/about/privacy/"},{"id":201,"name":"TimeOne","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://privacy.timeonegroup.com/en/","deletedDate":"2020-05-15T00:00:00Z"},{"id":150,"name":"Inskin Media LTD","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.inskinmedia.com/privacy-policy.html"},{"id":252,"name":"Jaduda GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.jadudamobile.com/datenschutzerklaerung/"},{"id":248,"name":"Converge-Digital","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://converge-digital.com/privacy-policy/"},{"id":161,"name":"Smadex SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://smadex.com/end-user-privacy-policy/"},{"id":285,"name":"Comcast International France SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.freewheel.com/privacy-policy"},{"id":228,"name":"McCann Discipline LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.primis.tech/privacy-policy/"},{"id":299,"name":"AdClear GmbH","purposeIds":[1,5],"legIntPurposeIds":[2,3,4],"featureIds":[1,2],"policyUrl":"https://www.adclear.de/datenschutzerklaerung/"},{"id":277,"name":"Codewise VL Sp. z o.o. Sp. k","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://voluumdsp.com/end-user-privacy-policy/"},{"id":259,"name":"ADYOULIKE SA","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.adyoulike.com/privacy_policy.php"},{"id":272,"name":"A.Mob","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.we-are-adot.com/privacy-policy/"},{"id":230,"name":"Steel House, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://steelhouse.com/privacy-policy/"},{"id":253,"name":"Improve Digital BV","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.improvedigital.com/platform-privacy-policy"},{"id":304,"name":"On Device Research Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://s.on-device.com/privacyPolicy"},{"id":314,"name":"Keymantics","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.keymantics.com/assets/privacy-policy.pdf"},{"id":257,"name":"R-TARGET","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"http://www.r-target.com/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":317,"name":"mainADV Srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.mainad.com/privacy-policy/"},{"id":278,"name":"Integral Ad Science, Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://integralads.com/privacy-policy/"},{"id":291,"name":"Qwertize","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.qwertize.com/en/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":295,"name":"Sojern, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.sojern.com/privacy/product-privacy-policy/"},{"id":315,"name":"Celtra, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.celtra.com/privacy-policy/"},{"id":165,"name":"SpotX, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.spotx.tv/privacy-policy/"},{"id":47,"name":"ADMAN - Phaistos Networks, S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adman.gr/privacy"},{"id":134,"name":"SMARTSTREAM.TV GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://www.smartstream.tv/en/productprivacy"},{"id":325,"name":"Knorex","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.knorex.com/privacy"},{"id":316,"name":"Gamned","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.gamned.com/privacy-policy/"},{"id":318,"name":"Accorp Sp. z o.o.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"http://www.instytut-pollster.pl/privacy-policy/"},{"id":199,"name":"ADUX","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adux.com/donnees-personelles/"},{"id":236,"name":"PowerLinks Media Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[3],"policyUrl":"https://www.powerlinks.com/privacy-policy/"},{"id":294,"name":"Jivox Corporation","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.jivox.com/privacy"},{"id":143,"name":"Connatix Native Exchange Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://connatix.com/privacy-policy/"},{"id":297,"name":"Polar Mobile Group Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://privacy.polar.me"},{"id":319,"name":"Clipcentric, Inc.","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://clipcentric.com/privacy.bhtml"},{"id":290,"name":"Readpeak Oy","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://readpeak.com/privacy-policy/"},{"id":323,"name":"DAZN Media Services Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.goal.com/en-gb/legal/privacy-policy"},{"id":119,"name":"Fusio by S4M","purposeIds":[1,2,5],"legIntPurposeIds":[3],"featureIds":[1,3],"policyUrl":"http://www.s4m.io/privacy-policy/"},{"id":302,"name":"Mobile Professionals BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mobpro.com/privacy.html"},{"id":212,"name":"usemax advertisement (Emego GmbH)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.usemax.de/?l=privacy"},{"id":264,"name":"Adobe Advertising Cloud","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.adobe.com/privacy/experience-cloud.html"},{"id":44,"name":"The ADEX GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://theadex.com/privacy-opt-out/"},{"id":282,"name":"Welect GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.welect.de/datenschutz"},{"id":238,"name":"StackAdapt","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.stackadapt.com/privacy"},{"id":284,"name":"WEBORAMA","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://weborama.com/privacy_en/"},{"id":148,"name":"Liveintent Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://liveintent.com/services-privacy-policy/"},{"id":64,"name":"DigiTrust / IAB Tech Lab","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitru.st/privacy-policy/"},{"id":301,"name":"zeotap GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://zeotap.com/privacy_policy"},{"id":275,"name":"TabMo SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://static.tabmo.io.s3.amazonaws.com/privacy-policy/index.html"},{"id":310,"name":"Adevinta Spain S.L.U.","purposeIds":[],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"https://www.adevinta.com/about/privacy/"},{"id":139,"name":"Permodo GmbH","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://permodo.com/de/privacy.html"},{"id":326,"name":"AdTiming Technology Company Limited","purposeIds":[3,5],"legIntPurposeIds":[1,2,4],"featureIds":[],"policyUrl":"http://www.adtiming.com/en/privacypolicy.html"},{"id":262,"name":"Fyber ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.fyber.com/legal/privacy-policy/"},{"id":331,"name":"ad6media","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.ad6media.fr/privacy"},{"id":345,"name":"The Kantar Group Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.kantar.com/cookies-policies"},{"id":308,"name":"Rockabox Media Ltd","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[],"policyUrl":"http://scoota.com/privacy-policy"},{"id":270,"name":"Marfeel Solutions, SL","purposeIds":[],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.marfeel.com/privacy-policy/"},{"id":333,"name":"InMobi Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":202,"name":"Telaria, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":328,"name":"Gemius SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.gemius.com/cookie-policy.html"},{"id":281,"name":"Wizaly","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.wizaly.com/terms-of-use#privacy-policy"},{"id":354,"name":"Apester Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://apester.com/privacy-policy/"},{"id":320,"name":"Adelphic LLC","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://adelphic.com/platform/privacy/"},{"id":359,"name":"AerServ LLC","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":265,"name":"Instinctive, Inc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://instinctive.io/privacy"},{"id":349,"name":"Optomaton UG","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://optomaton.com/privacy.html"},{"id":288,"name":"Video Media Groep B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://www.videomediagroup.com/wp-content/uploads/2016/01/Privacy-policy-VMG.pdf","deletedDate":"2019-09-17T00:00:00Z"},{"id":266,"name":"Digilant Spain, SLU","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.digilant.com/es/politica-privacidad/"},{"id":339,"name":"Vuble","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vuble.tv/us/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":303,"name":"Orion Semantics","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://static.orion-semantics.com/privacy.html"},{"id":261,"name":"Signal Digital Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.signal.co/privacy-policy/"},{"id":83,"name":"Visarity Technologies GmbH","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://primo.design/docs/PrivacyPolicyPrimo.html"},{"id":343,"name":"DIGITEKA Technologies","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.ultimedia.com/POLICY.html"},{"id":330,"name":"Linicom","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.linicom.com/privacy/","deletedDate":"2020-06-08T00:00:00Z"},{"id":231,"name":"AcuityAds Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.acuityads.com/corporate-privacy-policy.html"},{"id":216,"name":"Mindlytix SAS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://mindlytix.com/privacy/"},{"id":360,"name":"Permutive Technologies, Inc.","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[1,2],"policyUrl":"https://permutive.com/privacy","deletedDate":"2020-03-31T00:00:00Z"},{"id":361,"name":"Permutive","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://permutive.com/privacy"},{"id":311,"name":"Mobfox US LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobfox.com/privacy-policy/"},{"id":358,"name":"MGID Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mgid.com/privacy-policy"},{"id":152,"name":"Meetrics GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.meetrics.com/en/data-privacy/"},{"id":251,"name":"Yieldlove GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"http://www.yieldlove.com/cookie-policy"},{"id":344,"name":"My6sense Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[2,4],"featureIds":[],"policyUrl":"https://my6sense.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":347,"name":"Ezoic Inc.","purposeIds":[2,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.ezoic.com/terms/"},{"id":218,"name":"Bigabid Media ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.bigabid.com/privacy-policy"},{"id":350,"name":"Free Stream Media Corp. dba Samba TV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":351,"name":"Samba TV UK Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":341,"name":"Somo Audience Corp","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"https://somoaudience.com/legal/","deletedDate":"2020-07-06T00:00:00Z"},{"id":380,"name":"Vidoomy Media SL","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"http://vidoomy.com/privacy-policy.html"},{"id":378,"name":"communicationAds GmbH & Co. KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.communicationads.net/aboutus/privacy/"},{"id":369,"name":"Getintent USA, inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://getintent.com/privacy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":184,"name":"mediarithmics SAS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mediarithmics.com/en-us/content/privacy-policy"},{"id":368,"name":"VECTAURY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vectaury.io/en/personal-data"},{"id":373,"name":"Nielsen Marketing Cloud","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"http://www.nielsen.com/us/en/privacy-statement/exelate-privacy-policy.html"},{"id":214,"name":"Digital Control GmbH & Co. KG","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://advolution.de/privacy.php","deletedDate":"2020-05-06T00:00:00Z"},{"id":388,"name":"numberly","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://numberly.com/en/privacy/"},{"id":250,"name":"Qriously Ltd","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.brandwatch.com/legal/qriously-privacy-notice/"},{"id":223,"name":"Audience Trading Platform Ltd.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://atp.io/privacy-policy"},{"id":384,"name":"Pixalate, Inc.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"http://pixalate.com/privacypolicy/","deletedDate":"2019-11-08T00:00:00Z"},{"id":387,"name":"Triapodi Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appreciate.mobi/page.html#/end-user-privacy-policy"},{"id":312,"name":"Exactag GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.exactag.com/en/data-privacy/"},{"id":178,"name":"Hybrid Theory","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://hybridtheory.com/privacy-policy/"},{"id":377,"name":"AddApptr GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.addapptr.com/data-privacy"},{"id":382,"name":"The Reach Group GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://trg.de/en/privacy-statement/"},{"id":206,"name":"Hybrid Adtech GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://hybrid.ai/data_protection_policy"},{"id":403,"name":"Mobusi Mobile Advertising S.L.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobusi.com/privacy.en.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":385,"name":"Oracle Data Cloud","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://www.oracle.com/legal/privacy/marketing-cloud-data-cloud-privacy-policy.html"},{"id":404,"name":"Duplo Media AS","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.easy-ads.com/privacypolicy.htm"},{"id":242,"name":"twiago GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.twiago.com/datenschutz/"},{"id":376,"name":"Pocketmath Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pocketmath.com/privacy-policy"},{"id":402,"name":"Effiliation","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://inter.effiliation.com/politique-confidentialite.html"},{"id":413,"name":"Eulerian Technologies","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.eulerian.com/en/privacy/"},{"id":400,"name":"Whenever Media Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.whenevermedia.com/privacy-policy","deletedDate":"2019-07-29T00:00:00Z"},{"id":171,"name":"Webedia","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webedia-group.com/site/privacy-policy","deletedDate":"2020-07-01T00:00:00Z"},{"id":398,"name":"Yormedia Solutions Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.yormedia.com/privacy-and-cookies-notice/","deletedDate":"2019-08-06T00:00:00Z"},{"id":415,"name":"Seenthis AB","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://seenthis.co/privacy-notice-2018-04-18.pdf"},{"id":263,"name":"Nativo, Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.nativo.com/interest-based-ads"},{"id":329,"name":"Browsi Mobile Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://gobrowsi.com/browsi-privacy-policy/"},{"id":389,"name":"Bidmanagement GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adspert.net/en/privacy/","deletedDate":"2020-07-01T00:00:00Z"},{"id":337,"name":"SheMedia, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shemedia.com/ad-services-privacy-policy"},{"id":422,"name":"Brand Metrics Sweden AB","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://collector.brandmetrics.com/brandmetrics_privacypolicy.pdf"},{"id":421,"name":"LeftsnRight, Inc. dba LIQWID","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liqwid.solutions/privacy-policy","deletedDate":"2020-06-30T00:00:00Z"},{"id":426,"name":"TradeTracker","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[2],"policyUrl":"https://tradetracker.com/privacy-policy/","deletedDate":"2019-08-21T00:00:00Z"},{"id":394,"name":"AudienceProject Aps","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://privacy.audienceproject.com"},{"id":287,"name":"Avazu Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4],"featureIds":[3],"policyUrl":"http://avazuinc.com/opt-out/","deletedDate":"2020-08-03T00:00:00Z"},{"id":243,"name":"Cloud Technologies S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cloudtechnologies.pl/en/internet-advertising-privacy-policy"},{"id":113,"name":"iotec global Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.iotecglobal.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":338,"name":"dunnhumby Germany GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.sociomantic.com/privacy/en/","deletedDate":"2020-07-17T00:00:00Z"},{"id":405,"name":"IgnitionAi Ltd","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[2],"policyUrl":"https://www.isitelab.io/default.aspx","deletedDate":"2020-07-03T00:00:00Z"},{"id":416,"name":"Commanders Act","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.commandersact.com/en/privacy/"},{"id":434,"name":"DynAdmic","purposeIds":[1,3],"legIntPurposeIds":[2,4],"featureIds":[1,3],"policyUrl":"http://eu.dynadmic.com/privacy-policy/"},{"id":435,"name":"SINGLESPOT SAS ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.singlespot.com/privacy_policy?locale=fr"},{"id":409,"name":"Arrivalist Co.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[1,2],"policyUrl":"https://www.arrivalist.com/privacy"},{"id":321,"name":"Ziff Davis LLC","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.ziffdavis.com/privacy-policy"},{"id":436,"name":"INVIBES GROUP","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[1,2,3],"policyUrl":"http://www.invibes.com/terms"},{"id":442,"name":"R-Advertising","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-20T00:00:00Z"},{"id":362,"name":"Myntelligence S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://myntelligence.com/privacy-page/"},{"id":418,"name":"PROXISTORE","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://www.proxistore.com/common/en/cgv"},{"id":449,"name":"Mobile Journey B.V.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://mobilejourney.com/Privacy-Policy","deletedDate":"2019-09-05T00:00:00Z"},{"id":443,"name":"Tradedoubler AB","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-13T00:00:00Z"},{"id":429,"name":"Signals","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://signalsdata.com/platform-cookie-policy/"},{"id":335,"name":"Beachfront Media LLC","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://beachfront.com/privacy-policy/"},{"id":407,"name":"Publishers Internationale Pty Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pi-rate.com.au/privacy.html","deletedDate":"2019-11-08T00:00:00Z"},{"id":427,"name":"Proxi.cloud Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://proxi.cloud/info/privacy-policy/"},{"id":374,"name":"Bmind a Sales Maker Company, S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bmind.es/legal-notice/"},{"id":438,"name":"INVIDI technologies AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.invidi.com/wp-content/uploads/2020/02/ad-tech-services-privacy-policy.pdf"},{"id":450,"name":"Neodata Group srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.neodatagroup.com/en/security-policy"},{"id":452,"name":"Innovid Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.innovid.com/privacy-policy"},{"id":444,"name":"Playbuzz Ltd (aka EX.CO)","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://ex.co/privacy-policy/"},{"id":412,"name":"Cxense ASA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.cxense.com/about-us/privacy-policy"},{"id":454,"name":"Adimo","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://adimo.co/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":455,"name":"GDMServices, Inc. d/b/a FiksuDSP","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://fiksu.com/privacy-policy/"},{"id":298,"name":"Cuebiq Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.cuebiq.com/privacypolicy/","deletedDate":"2019-08-30T00:00:00Z"},{"id":423,"name":"travel audience GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://travelaudience.com/product-privacy-policy/"},{"id":397,"name":"Demandbase, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.demandbase.com/privacy-policy/"},{"id":381,"name":"Solocal","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://frontend.adhslx.com/privacy.html?"},{"id":425,"name":"ADRINO Sp. z o.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.adrino.pl/ciasteczkowa-polityka/","deletedDate":"2019-09-05T00:00:00Z"},{"id":365,"name":"Forensiq LLC","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1,3],"policyUrl":"https://impact.com/privacy-policy/"},{"id":447,"name":"Adludio Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adludio.com/privacy-policy/"},{"id":410,"name":"Adtelligent Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtelligent.com/privacy-policy/"},{"id":137,"name":"Str\u00f6er SSP GmbH (DSP)","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":395,"name":"PREX Programmatic Exchange GmbH&Co KG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[],"policyUrl":"http://www.programmatic-exchange.com/privacy","deletedDate":"2020-07-03T00:00:00Z"},{"id":462,"name":"Bidstack Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[2],"policyUrl":"https://www.bidstack.com/privacy-policy/"},{"id":466,"name":"TACTIC\u2122 Real-Time Marketing AS","purposeIds":[],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://tacticrealtime.com/privacy/"},{"id":340,"name":"Yieldr UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.yieldr.com/privacy"},{"id":336,"name":"Telecoming S.A.","purposeIds":[3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.telecoming.com/privacy-policy/"},{"id":430,"name":"Ad Unity Ltd","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"http://www.adunity.com/privacy-policy.html","deletedDate":"2019-08-13T00:00:00Z"},{"id":346,"name":"Cybba, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://cybba.com/about/legal/data-processing-agreement/","deletedDate":"2020-08-03T00:00:00Z"},{"id":469,"name":"Zeta Global","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://zetaglobal.com/privacy-policy/"},{"id":440,"name":"DEFINE MEDIA GMBH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.definemedia.de/datenschutz-conative/"},{"id":375,"name":"Affle International","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://affle.com/privacy-policy "},{"id":196,"name":"AdElement Media Solutions Pvt Ltd","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"http://adelement.com/privacy-policy.html"},{"id":268,"name":"Social Tokens Ltd. ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://woobi.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":475,"name":"TAPTAP Digital SL","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1,2,3],"policyUrl":"http://www.taptapnetworks.com/privacy_policy/"},{"id":474,"name":"hbfsTech","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.hbfstech.com/fr/privacy.html"},{"id":448,"name":"Targetspot Belgium SPRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://marketing.targetspot.com/Targetspot/Legal/TargetSpot%20Privacy%20Policy%20-%20June%202018.pdf"},{"id":428,"name":"Internet BillBoard a.s.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.ibillboard.com/en/privacy-information/"},{"id":461,"name":"B2B Media Group EMEA GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selfcampaign.com/static/privacy","deletedDate":"2019-08-14T00:00:00Z"},{"id":476,"name":"HIRO Media Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"http://hiro-media.com/privacy.php"},{"id":480,"name":"pilotx.tv","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[1,2,3],"policyUrl":"https://pilotx.tv/privacy/"},{"id":366,"name":"CerebroAd.com s.r.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.cerebroad.com/privacy-policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":392,"name":"Str\u00f6er Mobile Performance GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[3],"policyUrl":"https://stroeermobileperformance.com/?dl=privacy"},{"id":357,"name":"Totaljobs Group Ltd ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.totaljobs.com/privacy-policy"},{"id":486,"name":"Madington","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://delivered-by-madington.com/dat-privacy-policy/"},{"id":468,"name":"NeuStar, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://www.home.neustar/privacy"},{"id":458,"name":"AdColony, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"adcolony.com/privacy-policy/"},{"id":489,"name":"YellowHammer Media Group","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.yhmg.com/privacy-policy/","deletedDate":"2019-11-27T00:00:00Z"},{"id":293,"name":"SpringServe, LLC","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://springserve.com/privacy-policy/"},{"id":484,"name":"STRIATUM SAS","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://adledge.com/data-privacy/"},{"id":493,"name":"Carbon (AI) Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://carbonrmp.com/privacy.html"},{"id":495,"name":"Arcspire Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://public.arcspire.io/privacy.pdf"},{"id":496,"name":"Automattic Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://en.blog.wordpress.com/2017/12/04/updated-privacy-policy/"},{"id":424,"name":"KUPONA GmbH","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.kupona.de/dsgvo/"},{"id":408,"name":"Fidelity Media","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://fidelity-media.com/privacy-policy/"},{"id":473,"name":"Sub2 Technologies Ltd","purposeIds":[3,4,5],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.sub2tech.com/privacy-policy/"},{"id":467,"name":"Haensel AMS GmbH","purposeIds":[3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://haensel-ams.com/data-privacy/"},{"id":490,"name":"PLAYGROUND XYZ EMEA LTD","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://playground.xyz/privacy"},{"id":464,"name":"Oracle AddThis","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.addthis.com/privacy/privacy-policy/","deletedDate":"2020-02-12T00:00:00Z"},{"id":491,"name":"Triboo Data Analytics","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shinystat.com/it/informativa_privacy_generale.html"},{"id":499,"name":"PurposeLab, LLC","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://purposelab.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":502,"name":"NEXD","purposeIds":[5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://nexd.com/privacy-policy"},{"id":465,"name":"Schibsted Product and Tech UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.schibsted.com/","deletedDate":"2019-07-26T00:00:00Z"},{"id":497,"name":"Little Big Data sp.z.o.o.","purposeIds":[1,2,4],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://dtxngr.com/legal/"},{"id":492,"name":"LotaData, Inc.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1],"policyUrl":"https://lotadata.com/privacy_policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":512,"name":"PubNative GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://pubnative.net/privacy-notice/"},{"id":471,"name":"FlexOffers.com, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.flexoffers.com/privacy-policy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":494,"name":"Cablato Limited","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://cablato.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":516,"name":"Pexi B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://pexi.nl/privacy-policy/"},{"id":507,"name":"AdsWizz Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://www.adswizz.com/our-privacy-policy/"},{"id":482,"name":"UberMedia, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ubermedia.com/summary-of-privacy-policy/"},{"id":505,"name":"Shopalyst Inc","purposeIds":[1,2],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shortlyst.com/eu/privacy_terms.html"},{"id":517,"name":"SunMedia ","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2],"policyUrl":"https://www.sunmedia.tv/en/cookies"},{"id":518,"name":"Accelerize Inc.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://getcake.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":511,"name":"Admixer EU GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://admixer.com/privacy/"},{"id":479,"name":"INFINIA MOBILE S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.infiniamobile.com/privacy_policy"},{"id":513,"name":"Shopstyle","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shopstyle.co.uk/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":509,"name":"ATG Ad Tech Group GmbH","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ad-tech-group.com/privacy-policy/"},{"id":521,"name":"netzeffekt GmbH","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.netzeffekt.de/en/imprint"},{"id":487,"name":"nugg.ad GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1],"policyUrl":"https://www.nugg.ad/en/privacy/general-information.html","deletedDate":"2019-10-03T00:00:00Z"},{"id":515,"name":"ZighZag","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zighzag.com/privacy"},{"id":520,"name":"ChannelSight ","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.channelsight.com/privacypolicy/"},{"id":524,"name":"The Ozone Project Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://ozoneproject.com/privacy-policy"},{"id":529,"name":"Fidzup","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.fidzup.com/en/privacy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":528,"name":"Kayzen","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://kayzen.io/data-privacy-policy"},{"id":527,"name":"Jampp LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://jampp.com/privacy.html"},{"id":506,"name":"salesforce.com, inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.salesforce.com/company/privacy/"},{"id":534,"name":"SmartyAds Inc.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://smartyads.com/privacy-policy"},{"id":535,"name":"INNITY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.innity.com/privacy-policy.php"},{"id":514,"name":"Uprival LLC","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://uprival.com/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":522,"name":"Tealium Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://tealium.com/privacy-policy/"},{"id":530,"name":"Near Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://near.co/privacy"},{"id":539,"name":"AdDefend GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.addefend.com/en/privacy-policy/"},{"id":501,"name":"Alliance Gravity Data Media","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.alliancegravity.com/politiquedeprotectiondesdonneespersonnelles"},{"id":519,"name":"Chargeads","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.chargeplatform.com/privacy"},{"id":523,"name":"X-Mode Social, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://xmode.io/privacy-policy.html"},{"id":537,"name":"RUN, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.runads.com/privacy-policy"},{"id":531,"name":"Smartclip Hispania SL","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://rgpd-smartclip.com/"},{"id":536,"name":"GlobalWebIndex","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"http://legal.trendstream.net/non-panellist_privacy_policy"},{"id":542,"name":"Densou Trading Desk ApS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://densou.dk/Policy.html","deletedDate":"2020-01-21T00:00:00Z"},{"id":525,"name":"PUB OCEAN LIMITED","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://rta.pubocean.com/privacy-policy/","deletedDate":"2019-10-03T00:00:00Z"},{"id":544,"name":"Kochava Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://www.kochava.com/support-privacy/"},{"id":543,"name":"PaperG, Inc. dba Thunder Industries","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.makethunder.com/privacy"},{"id":334,"name":"Cydersoft","purposeIds":[],"legIntPurposeIds":[1,2,3,4],"featureIds":[2,3],"policyUrl":"http://www.videmob.com/privacy.html"},{"id":551,"name":"Illuma Technology Limited","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.weareilluma.com/endddd","deletedDate":"2019-11-14T00:00:00Z"},{"id":540,"name":"Tunnl BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://tunnl.com/privacy.html","deletedDate":"2019-12-20T00:00:00Z"},{"id":547,"name":"Video Reach","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.videoreach.de/about/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":546,"name":"Smart Traffik","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://okube-attribution.com/politique-de-confidentialite/"},{"id":541,"name":"DeepIntent, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.deepintent.com/privacypolicy"},{"id":545,"name":"Reignn Platform Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://reignn.com/user-privacy-policy"},{"id":439,"name":"Bit Q Holdings Limited","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.rippll.com/privacy"},{"id":553,"name":"Adhese","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://adhese.com/privacy-and-cookie-policy"},{"id":556,"name":"adhood.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://v3.adhood.com/en/site/politikavekurallar/gizlilik.php?lang=en"},{"id":550,"name":"Happydemics","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.iubenda.com/privacy-policy/69056167/full-legal"},{"id":560,"name":"Leiki Ltd.","purposeIds":[1,2,3],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"http://www.leiki.com/privacy","deletedDate":"2020-01-07T00:00:00Z"},{"id":554,"name":"RMSi Radio Marketing Service interactive GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.rms.de/datenschutz/"},{"id":498,"name":"Dr. Banner","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://drbanner.com/privacypolicy_en/"},{"id":565,"name":"Adobe Audience Manager","purposeIds":[1,2,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adobe.com/privacy/policy.html"},{"id":118,"name":"Drawbridge, Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.drawbridge.com/privacy/","deletedDate":"2020-03-06T00:00:00Z"},{"id":572,"name":"CHEQ AI TECHNOLOGIES LTD.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.cheq.ai/privacy"},{"id":571,"name":"ViewPay","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://viewpay.tv/mentions-legales/"},{"id":568,"name":"Jointag S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.jointag.com/privacy/kariboo/publisher/third/"},{"id":570,"name":"Czech Publisher Exchange z.s.p.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cpex.cz/pro-uzivatele/ochrana-soukromi/"},{"id":559,"name":"Otto (GmbH & Co KG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2],"policyUrl":"https://www.otto.de/shoppages/service/datenschutz"},{"id":548,"name":"LBC France","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.leboncoin.fr/dc/cookies","deletedDate":"2020-04-23T00:00:00Z"},{"id":569,"name":"Kairos Fire","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.kairosfire.com/privacy"},{"id":577,"name":"Neustar on behalf of The Procter & Gamble Company","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pg.com/privacy/english/privacy_statement.shtml"},{"id":590,"name":"Sourcepoint Technologies, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.sourcepoint.com/privacy-policy"},{"id":587,"name":"Localsensor B.V.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.localsensor.com/privacy.html"},{"id":578,"name":"MAIRDUMONT NETLETIX GmbH&Co. KG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mairdumont-netletix.com/datenschutz"},{"id":580,"name":"Goldbach Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://goldbach.com/ch/de/datenschutz"},{"id":593,"name":"Programatica de publicidad S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://datmean.com/politica-privacidad/"},{"id":574,"name":"Realeyes OU","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://realview.realeyesit.com/privacy"},{"id":581,"name":"Mobilewalla, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.mobilewalla.com/business-services-privacy-policy"},{"id":598,"name":"audio content & control GmbH","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://www.audio-cc.com/audiocc_privacy_policy.pdf"},{"id":596,"name":"InsurAds Technologies SA.","purposeIds":[3],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.insurads.com/privacy.html"},{"id":576,"name":"StartApp Inc.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://www.startapp.com/policy/privacy-policy/","deletedDate":"2020-04-23T00:00:00Z"},{"id":592,"name":"Colpirio.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy-policy.colpirio.com/en/","deletedDate":"2020-03-18T00:00:00Z"},{"id":549,"name":"Bandsintown Amplified LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://corp.bandsintown.com/privacy"},{"id":597,"name":"Better Banners A/S","purposeIds":[],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://betterbanners.com/en/privacy"},{"id":601,"name":"WebAds B.V","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.webads.eu/"},{"id":599,"name":"Maximus Live LLC","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://maximusx.com/privacy-policy/"},{"id":604,"name":"Join","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.teamjoin.fr/privacy.html","deletedDate":"2020-04-23T00:00:00Z"},{"id":606,"name":"Impactify ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://impactify.io/privacy-policy/"},{"id":608,"name":"News and Media Holding, a.s.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.newsandmedia.sk/gdpr/"},{"id":602,"name":"Online Solution Int Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://adsafety.net/privacy.html"},{"id":612,"name":"Adnami Aps","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adnami.io/privacy","deletedDate":"2020-03-17T00:00:00Z"},{"id":591,"name":"Consumable, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://consumable.com/privacy-policy.html"},{"id":614,"name":"Market Resource Partners LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.mrpfd.com/privacy-policy/"},{"id":615,"name":"Adsolutions BV","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adsolutions.com/privacy-policy/"},{"id":607,"name":"ucfunnel Co., Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.ucfunnel.com/privacy-policy"},{"id":609,"name":"Predicio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.predic.io/privacy"},{"id":617,"name":"Onfocus (Adagio)","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adagio.io/privacy"},{"id":620,"name":"Blue","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.getblue.io/privacy/"},{"id":610,"name":"Azerion Holding B.V.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://azerion.com/business/privacy.html"},{"id":621,"name":"Seznam.cz, a.s.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://www.seznam.cz/ochranaudaju"},{"id":624,"name":"Norstat AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.norstatpanel.com/en/data-protection"},{"id":623,"name":"Adprime Media Inc. ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adprimehealth.com/privacy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":95,"name":"Lotame Solutions, inc","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[2],"policyUrl":"https://www.lotame.com/about-lotame/privacy/lotame-corporate-websites-privacy-policy/"},{"id":618,"name":"BEINTOO SPA","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.beintoo.com/privacy-cookie-policy/"},{"id":619,"name":"Capitaldata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.capitaldata.fr/privacy"},{"id":625,"name":"BILENDI SA","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.maximiles.com/privacy-policy"},{"id":628,"name":": Tappx","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.tappx.com/en/privacy-policy/"},{"id":626,"name":"Hivestack Inc.","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://hivestack.com/privacy-policy"},{"id":631,"name":"Relay42 Netherlands B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://relay42.com/privacy"},{"id":627,"name":"D-Edge","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.d-edge.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":644,"name":"Gamoshi LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.gamoshi.com/privacy-policy"},{"id":639,"name":"Smile Wanted Group","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.smilewanted.com/privacy.php"},{"id":635,"name":"WebMediaRM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webmediarm.com/vie_privee_et_opposition_en.php"},{"id":579,"name":"Ve Global","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.ve.com/privacy-policy"},{"id":645,"name":"Noster Finance S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.finect.com/terminos-legales/politica-de-cookies"},{"id":653,"name":"Smartme Analytics","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"http://smartmeapp.com/info/smartme/aviso_legal.php","deletedDate":"2020-07-03T00:00:00Z"},{"id":613,"name":"Adserve.zone / Artworx AS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adserve.zone/adserveprivacypolicy.html"},{"id":573,"name":"Dailymotion SA","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2],"policyUrl":"https://www.dailymotion.com/legal/privacy"},{"id":652,"name":"Skaze","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.skaze.fr/rgpd/"},{"id":646,"name":"Notify","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"https://notify-group.com/en/mentions-legales/"},{"id":648,"name":"TrueData Solutions, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.truedata.co/privacy-policy/"},{"id":647,"name":"Axel Springer Teaser Ad GmbH","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://www.adup-tech.com/privacy"},{"id":654,"name":"GRAPHINIUM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.graphinium.com/privacy/"},{"id":659,"name":"Research and Analysis of Media in Sweden AB","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www2.rampanel.com/privacy-policy/"},{"id":656,"name":"Think Clever Media","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.contentignite.com/privacy-policy/"},{"id":504,"name":"Alive & Kicking Global Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mcsaatchiplc.com/legal/privacy-cookies","deletedDate":"2020-07-27T00:00:00Z"},{"id":657,"name":"GP One GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.gsi-one.org/de/privacy-policy.html"},{"id":655,"name":"Sportradar AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sportradar.com/about-us/privacy/"},{"id":662,"name":"SoundCast","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://soundcast.fm/en/data-privacy"},{"id":665,"name":"Digital East GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.digitaleast.mobi/en/legal/privacy-policy/"},{"id":650,"name":"Telefonica Investigaci\u00f3n y Desarrollo S.A.U","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.cognitivemarketing.tid.es/"},{"id":666,"name":"BeOp","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://beop.io/privacy"},{"id":663,"name":"Mobsuccess","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.mobsuccess.com/en/privacy"},{"id":658,"name":"BLIINK SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://bliink.io/privacy-policy"},{"id":667,"name":"Liftoff Mobile, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://liftoff.io/privacy-policy/"},{"id":668,"name":"WhatRocks Inc. ","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.whatrocks.co/en/privacy-policy "},{"id":670,"name":"Timehop, Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.timehop.com/privacy"},{"id":674,"name":"Duration Media, LLC.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.durationmedia.net/privacy-policy"},{"id":675,"name":"Instreamatic inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://instreamatic.com/privacy-policy/"},{"id":676,"name":"BusinessClick","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.businessclick.com/documents/RegulaminProgramuBusinessClick-2019.pdf"},{"id":677,"name":"Intercept Interactive Inc. dba Undertone","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.undertone.com/privacy/"},{"id":660,"name":"Schibsted Norge AS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://static.vg.no/privacy/","deletedDate":"2019-09-16T00:00:00Z"},{"id":673,"name":"TTNET AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.programattik.com/en/privacy-policy.aspx"},{"id":664,"name":"adMarketplace, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.admarketplace.com/privacy-policy/"},{"id":671,"name":"Mediaforce LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://casino.mindthebet.co.uk/themes/mindthebetv2-casino/privacy.php"},{"id":561,"name":"AuDigent","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://audigent.com/platform-privacy-policy"},{"id":682,"name":"Radio Net Media Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.adtonos.com/service-privacy-policy/"},{"id":684,"name":"Blue Billywig BV","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.bluebillywig.com/privacy-statement/"},{"id":686,"name":"The MediaGrid Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.themediagrid.com/privacy-policy/"},{"id":685,"name":"Arkeero","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://arkeero.com/privacy-2/"},{"id":687,"name":"MISSENA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://missena.com/confidentialite/"},{"id":690,"name":"Go.pl sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://go.pl/polityka-prywatnosci/"},{"id":691,"name":"Lifesight Pte. Ltd.","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.lifesight.io/privacy-policy/"},{"id":697,"name":"ADWAYS SAS","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.adways.com/confidentialite/?lang=en"},{"id":681,"name":"MyTraffic","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mytraffic.io/en/privacy"},{"id":649,"name":"adality GmbH","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[1],"policyUrl":"https://adality.de/en/privacy/"},{"id":712,"name":"Inspired Mobile Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://byinspired.com/privacypolicy.pdf"},{"id":688,"name":"Effinity","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.effiliation.com/politique-de-confidentialite/"},{"id":702,"name":"Kwanko","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.kwanko.com/fr/rgpd/"},{"id":715,"name":"BidBerry SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.bidberrymedia.com/privacy-policy/"},{"id":713,"name":"Dataseat Ltd","purposeIds":[2,5],"legIntPurposeIds":[1,3,4],"featureIds":[],"policyUrl":"https://dataseat.com/privacy-policy"},{"id":716,"name":"OnAudience Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.onaudience.com/internet-advertising-privacy-policy"},{"id":708,"name":"Dugout Limited ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://dugout.com/privacy-policy"},{"id":717,"name":"Audience Network","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.en.audiencenetwork.pl/internet-advertising-privacy-policy"},{"id":718,"name":"AppConsent Xchange","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://appconsent.io/en/privacy-policy"},{"id":720,"name":"AAX LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://aax.media/privacy/"},{"id":678,"name":"Axonix LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://axonix.com/privacy-cookie-policy/"},{"id":719,"name":"Online Advertising Network Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.oan.pl/en/privacy-policy"},{"id":707,"name":"Dentsu Aegis Network Italia SpA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.dentsuaegisnetwork.com/it/it/policies/info-cookie"},{"id":721,"name":"Beaconspark Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1],"policyUrl":"https://www.engageya.com/privacy"},{"id":724,"name":"Between Exchange","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"https://en.betweenx.com/pdata.pdf"},{"id":728,"name":"Appier PTE Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.appier.com/privacy-policy/"},{"id":729,"name":"Cavai AS & UK ","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://cav.ai/privacy-policy/"},{"id":723,"name":"Adzymic Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.adzymic.co/privacy"},{"id":737,"name":"Monet Engine Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appmonet.com/privacy-policy/"},{"id":740,"name":"6Sense Insights, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://6sense.com/privacy-policy/"},{"id":744,"name":"Vidazoo Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[2],"policyUrl":"https://vidazoo.gitbook.io/vidazoo-legal/privacy-policy"},{"id":731,"name":"GeistM Technologies LTD","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.geistm.com/privacy"},{"id":741,"name":"Brand Advance Limited","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.wearebrandadvance.com/website-privacy-policy"},{"id":734,"name":"Cint AB","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.cint.com/participant-privacy-notice"},{"id":709,"name":"NC Audience Exchange, LLC (NewsIQ)","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.ncaudienceexchange.com/privacy/"},{"id":739,"name":"Blingby LLC","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://blingby.com/privacy"},{"id":732,"name":"Performax.cz, s.r.o.","purposeIds":[2,4,5],"legIntPurposeIds":[1,3],"featureIds":[2,3],"policyUrl":"https://reg.tiscali.cz/privacy-policy"},{"id":736,"name":"BidMachine Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://explorestack.com/privacy-policy/"},{"id":738,"name":"adbility media GmbH","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adbility-media.com/datenschutzerklaerung/"},{"id":742,"name":"Audiencerate LTD","purposeIds":[],"legIntPurposeIds":[1,2,5],"featureIds":[],"policyUrl":"https://www.audiencerate.com/privacy/"},{"id":743,"name":"MOVIads Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://moviads.pl/polityka-prywatnosci/"},{"id":746,"name":"Adxperience SAS","purposeIds":[2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://adxperience.com/privacy-policy/"},{"id":747,"name":"Kairion GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://kairion.de/datenschutzbestimmungen/"},{"id":748,"name":"AUDIOMOB LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.audiomob.io/privacy"},{"id":749,"name":"Good-Loop Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://doc.good-loop.com/policy/privacy-policy.html"},{"id":754,"name":"DistroScale, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.distroscale.com/privacy-policy/"},{"id":756,"name":"Fandom, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"https://www.fandom.com/privacy-policy"},{"id":758,"name":"GfK Netherlands B.V.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://gfkpanel.nl/privacy"},{"id":759,"name":"RevJet","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.revjet.com/privacy"},{"id":760,"name":"VEXPRO TECHNOLOGIES LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://onedash.com/privacy-policy.html"},{"id":761,"name":"Digiseg ApS","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://digiseg.io/privacy-center/"},{"id":763,"name":"Delidatax SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.delidatax.net/privacy.htm"},{"id":764,"name":"Lucidity","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://golucidity.com/privacy-policy/"},{"id":765,"name":"Grabit Interactive Media Inc dba KERV Interctive","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://kervit.com/privacy-policy/"},{"id":766,"name":"ADCELL | Firstlead GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.adcell.de/agb#sector_6"},{"id":768,"name":"Global Media & Entertainment Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://global.com/privacy-policy/"},{"id":770,"name":"MARKETPERF CORP","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.marketperf.com/assets/images/app/marketperf/pdf/privacy-policy.pdf"},{"id":773,"name":"360e-com Sp. z o.o.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.clickonometrics.com/optout/"},{"id":775,"name":"SelectMedia International LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selectmedia.asia/terms-and-privacy/"},{"id":778,"name":"Discover-Tech ltd","purposeIds":[2,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://discover-tech.io/dsp-privacy-policy/"},{"id":779,"name":"Adtarget Medya A.S.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtarget.com.tr/adtarget-privacy-policy-2020.pdf"},{"id":780,"name":"Aniview LTD","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.aniview.com/privacy-policy/"},{"id":781,"name":"FeedAd GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://feedad.com/privacy/"},{"id":784,"name":"Nubo LTD","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.recod3.com/privacypolicy.php"},{"id":786,"name":"TargetVideo GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.target-video.com/datenschutz/"},{"id":798,"name":"Adverticum cPlc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://adverticum.net/english/privacy-and-data-processing-information/"},{"id":803,"name":"Click Tech Limited","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[1],"policyUrl":"https://en.yeahmobi.com/html/privacypolicy/"}]} \ No newline at end of file +{"vendorListVersion":215,"lastUpdated":"2020-08-13T16:00:19Z","purposes":[{"id":1,"name":"Information storage and access","description":"The storage of information, or access to information that is already stored, on your device such as advertising identifiers, device identifiers, cookies, and similar technologies."},{"id":2,"name":"Personalisation","description":"The collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as on other websites or apps, over time. Typically, the content of the site or app is used to make inferences about your interests, which inform future selection of advertising and/or content."},{"id":3,"name":"Ad selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver advertisements for you, and to measure the delivery and effectiveness of such advertisements. This includes using previously collected information about your interests to select ads, processing data about what advertisements were shown, how often they were shown, when and where they were shown, and whether you took any action related to the advertisement, including for example clicking an ad or making a purchase. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as websites or apps, over time."},{"id":4,"name":"Content selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver content for you, and to measure the delivery and effectiveness of such content. This includes using previously collected information about your interests to select content, processing data about what content was shown, how often or how long it was shown, when and where it was shown, and whether the you took any action related to the content, including for example clicking on content. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, such as websites or apps, over time."},{"id":5,"name":"Measurement","description":"The collection of information about your use of the content, and combination with previously collected information, used to measure, understand, and report on your usage of the service. This does not include personalisation, the collection of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, i.e. on other service, such as websites or apps, over time."}],"features":[{"id":1,"name":"Matching Data to Offline Sources","description":"Combining data from offline sources that were initially collected in other contexts."},{"id":2,"name":"Linking Devices","description":"Allow processing of a user's data to connect such user across multiple devices."},{"id":3,"name":"Precise Geographic Location Data","description":"Allow processing of a user's precise geographic location data in support of a purpose for which that certain third party has consent."}],"vendors":[{"id":8,"name":"Emerse Sverige AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.emerse.com/privacy-policy/"},{"id":9,"name":"AdMaxim Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.admaxim.com/admaxim-privacy-policy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":12,"name":"BeeswaxIO Corporation","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beeswax.com/privacy/"},{"id":28,"name":"TripleLift, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://triplelift.com/privacy/"},{"id":27,"name":"ADventori SAS","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adventori.com/with-us/legal-notice/"},{"id":25,"name":"Verizon Media EMEA Limited","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.verizonmedia.com/policies/ie/en/verizonmedia/privacy/index.html"},{"id":26,"name":"Venatus Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.venatusmedia.com/privacy/"},{"id":1,"name":"Exponential Interactive, Inc d/b/a VDX.tv","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://vdx.tv/privacy/"},{"id":6,"name":"AdSpirit GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adspirit.de/privacy"},{"id":30,"name":"BidTheatre AB","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.bidtheatre.com/privacy-policy"},{"id":24,"name":"Epsilon","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.conversantmedia.eu/legal/privacy-policy"},{"id":29,"name":"Etarget SE","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.etarget.sk/privacy.php","deletedDate":"2020-06-01T00:00:00Z"},{"id":39,"name":"ADITION technologies AG","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.adition.com/datenschutz"},{"id":11,"name":"Quantcast International Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.quantcast.com/privacy/"},{"id":15,"name":"Adikteev","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adikteev.com/privacy-policy-eng/"},{"id":4,"name":"Roq.ad Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.roq.ad/privacy-policy"},{"id":7,"name":"Vibrant Media Limited","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vibrantmedia.com/en/privacy-policy/"},{"id":2,"name":"Captify Technologies Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.captify.co.uk/privacy-policy/"},{"id":37,"name":"NEURAL.ONE","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://web.neural.one/privacy-policy/"},{"id":13,"name":"Sovrn Holdings Inc","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sovrn.com/sovrn-privacy/"},{"id":34,"name":"NEORY GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.neory.com/privacy.html"},{"id":32,"name":"Xandr, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.xandr.com/privacy/platform-privacy-policy/"},{"id":10,"name":"Index Exchange, Inc. ","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.indexexchange.com/privacy"},{"id":57,"name":"ADARA MEDIA UNLIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://adara.com/privacy-promise/"},{"id":63,"name":"Avocet Systems Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://avocet.io/privacy-portal"},{"id":51,"name":"xAd, Inc. dba GroundTruth","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.groundtruth.com/privacy-policy/"},{"id":49,"name":"TRADELAB","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://tradelab.com/en/privacy/"},{"id":45,"name":"Smart Adserver","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://smartadserver.com/end-user-privacy-policy/"},{"id":52,"name":"The Rubicon Project, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[3],"policyUrl":"http://www.rubiconproject.com/rubicon-project-yield-optimization-privacy-policy/"},{"id":71,"name":"Roku Advertising Services","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://docs.roku.com/published/userprivacypolicy/en/us"},{"id":79,"name":"MediaMath, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.mediamath.com/privacy-policy/"},{"id":91,"name":"Criteo SA","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.criteo.com/privacy/"},{"id":85,"name":"Crimtan Holdings Limited","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[1,3],"policyUrl":"https://crimtan.com/privacy/"},{"id":16,"name":"RTB House S.A.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.rtbhouse.com/privacy-center/services-privacy-policy/"},{"id":86,"name":"Scene Stealer Limited","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"http://scenestealer.tv/privacy-policy/"},{"id":94,"name":"Blis Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.blis.com/privacy/"},{"id":73,"name":"Simplifi Holdings Inc.","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2,3],"policyUrl":"https://simpli.fi/site-privacy-policy/"},{"id":33,"name":"ShareThis, Inc","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://sharethis.com/privacy/"},{"id":20,"name":"N Technologies Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://n.rich/privacy-notice"},{"id":55,"name":"Madison Logic, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.madisonlogic.com/privacy/"},{"id":53,"name":"Sirdata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.sirdata.com/privacy/"},{"id":69,"name":"OpenX","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.openx.com/legal/privacy-policy/"},{"id":98,"name":"GroupM UK Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.groupm.com/privacy-notice"},{"id":62,"name":"Justpremium BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://justpremium.com/privacy-policy/"},{"id":19,"name":"Intent Media, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://intentmedia.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":43,"name":"Vdopia DBA Chocolate Platform","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://chocolateplatform.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":36,"name":"RhythmOne DBA Unruly Group Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.rhythmone.com/privacy-policy"},{"id":80,"name":"Sharethrough, Inc","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://platform-cdn.sharethrough.com/privacy-policy"},{"id":81,"name":"PulsePoint, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pulsepoint.com/privacy-policy/website","deletedDate":"2020-07-06T00:00:00Z"},{"id":23,"name":"Amobee, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.amobee.com/trust/privacy-guidelines"},{"id":35,"name":"Purch Group, Inc.","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://www.purch.com/privacy-policy/","deletedDate":"2019-05-30T00:00:00Z"},{"id":3,"name":"affilinet","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.affili.net/de/footeritem/datenschutz","deletedDate":"2019-06-21T00:00:00Z"},{"id":74,"name":"Admotion SRL","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.admotion.com/policy/","deletedDate":"2019-07-24T00:00:00Z"},{"id":191,"name":"realzeit GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://realzeitmedia.com/privacy.html","deletedDate":"2019-04-29T00:00:00Z"},{"id":197,"name":"Switch Concepts Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.switchconcepts.com/privacy-policy","deletedDate":"2019-07-26T00:00:00Z"},{"id":390,"name":"Parsec Media Inc.","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,3],"policyUrl":"www.parsec.media/privacy-policy","deletedDate":"2019-06-27T00:00:00Z"},{"id":459,"name":"uppr GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://netzwerk.uppr.de/privacy-policy.do","deletedDate":"2019-06-17T00:00:00Z"},{"id":221,"name":"LEMO MEDIA GROUP LIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.lemomedia.com/terms.pdf","deletedDate":"2019-06-28T00:00:00Z"},{"id":478,"name":"RevLifter Ltd","purposeIds":[1],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.revlifter.com/privacy-policy","deletedDate":"2019-07-15T00:00:00Z"},{"id":500,"name":"Turbo","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.turboadv.com/white-rabbit-privacy-policy/","deletedDate":"2019-07-12T00:00:00Z"},{"id":68,"name":"Sizmek by Amazon","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.sizmek.com/privacy-policy/"},{"id":75,"name":"M32 Connect Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://m32.media/privacy-cookie-policy/"},{"id":17,"name":"Greenhouse Group BV (with its trademark LemonPI)","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.lemonpi.io/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":61,"name":"GumGum, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://gumgum.com/privacy-policy"},{"id":40,"name":"Active Agent (ADITION technologies AG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.active-agent.com/de/unternehmen/datenschutzerklaerung/"},{"id":76,"name":"PubMatic, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://pubmatic.com/privacy-policy/"},{"id":89,"name":"Tapad, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.tapad.com/eu-privacy-policy"},{"id":46,"name":"Skimbit Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://skimlinks.com/pages/privacy-policy"},{"id":66,"name":"adsquare GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adsquare.com/privacy"},{"id":105,"name":"Impression Desk Technologies Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://impressiondesk.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":41,"name":"Adverline","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.adverline.com/privacy/"},{"id":82,"name":"Smaato, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.smaato.com/privacy/"},{"id":60,"name":"Rakuten Marketing LLC","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://rakutenadvertising.com/legal-notices/services-privacy-policy/"},{"id":70,"name":"Yieldlab AG","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[3],"policyUrl":"http://www.yieldlab.de/meta-navigation/datenschutz/"},{"id":50,"name":"Adform","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://site.adform.com/privacy-center/platform-privacy/product-and-services-privacy-policy/"},{"id":48,"name":"NetSuccess, s.r.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inres.sk/pp/"},{"id":100,"name":"Fifty Technology Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://fifty.io/privacy-policy.php"},{"id":21,"name":"The Trade Desk","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.thetradedesk.com/general/privacy-policy"},{"id":110,"name":"Dynata LLC","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.opinionoutpost.co.uk/en-gb/policies/privacy"},{"id":42,"name":"Taboola Europe Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.taboola.com/privacy-policy"},{"id":112,"name":"Maytrics GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://maytrics.com/privacy.php","deletedDate":"2019-09-17T00:00:00Z"},{"id":77,"name":"comScore, Inc.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.scorecardresearch.com/privacy.aspx?newlanguage=1"},{"id":109,"name":"LoopMe Limited","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://loopme.com/privacy-policy/"},{"id":120,"name":"Eyeota Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.eyeota.com/privacy-center"},{"id":93,"name":"Adloox SA","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://adloox.com/disclaimer"},{"id":132,"name":"Teads ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.teads.com/privacy-policy/"},{"id":22,"name":"admetrics GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://admetrics.io/en/privacy_policy/"},{"id":102,"name":"Telaria SAS","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":108,"name":"Rich Audience Technologies SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://richaudience.com/privacy/"},{"id":18,"name":"Widespace AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.widespace.com/legal/privacy-policy-notice/"},{"id":122,"name":"Avid Media Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.avidglobalmedia.eu/privacy-policy.html"},{"id":97,"name":"LiveRamp, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.liveramp.com/service-privacy-policy/"},{"id":138,"name":"ConnectAd Realtime GmbH","purposeIds":[1,2],"legIntPurposeIds":[3,4],"featureIds":[],"policyUrl":"http://connectadrealtime.com/privacy/"},{"id":72,"name":"Nano Interactive GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.nanointeractive.com/privacy"},{"id":127,"name":"PIXIMEDIA SAS","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://piximedia.com/privacy/"},{"id":136,"name":"Str\u00f6er SSP GmbH (SSP)","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[2,3],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":111,"name":"Showheroes SE","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://showheroes.com/privacy/"},{"id":56,"name":"Confiant Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.confiant.com/privacy","deletedDate":"2020-05-18T00:00:00Z"},{"id":124,"name":"Teemo SA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://teemo.co/fr/confidentialite/"},{"id":154,"name":"YOC AG","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://yoc.com/privacy/"},{"id":38,"name":"Beemray Oy","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beemray.com/privacy-policy/","deletedDate":"2020-06-19T00:00:00Z"},{"id":101,"name":"MiQ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://wearemiq.com/privacy-policy/"},{"id":149,"name":"ADman Interactive SLU","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://admanmedia.com/politica.html?setLng=es"},{"id":151,"name":"Admedo Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[3],"policyUrl":"https://www.admedo.com/privacy-policy","deletedDate":"2020-07-17T00:00:00Z"},{"id":153,"name":"MADVERTISE MEDIA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://madvertise.com/en/gdpr/"},{"id":159,"name":"Underdog Media LLC ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://underdogmedia.com/privacy-policy/"},{"id":157,"name":"Seedtag Advertising S.L","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.seedtag.com/en/privacy-policy/"},{"id":145,"name":"Snapsort Inc., operating as Sortable","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://help.sortable.com/help/privacy-policy"},{"id":131,"name":"ID5 Technology SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.id5.io/privacy"},{"id":158,"name":"Reveal Mobile, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://revealmobile.com/privacy"},{"id":147,"name":"Adacado Technologies Inc. (DBA Adacado)","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adacado.com/privacy-policy-april-25-2018/"},{"id":130,"name":"NextRoll, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.nextroll.com/privacy"},{"id":129,"name":"IPONWEB GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.iponweb.com/privacy-policy/"},{"id":128,"name":"BIDSWITCH GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bidswitch.com/privacy-policy/"},{"id":168,"name":"EASYmedia GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://login.rtbmarket.com/gdpr"},{"id":164,"name":"Outbrain UK Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.outbrain.com/legal/privacy#privacy-policy"},{"id":144,"name":"district m inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://districtm.net/en/page/platforms-data-and-privacy-policy/"},{"id":163,"name":"Bombora Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://bombora.com/privacy"},{"id":173,"name":"Yieldmo, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.yieldmo.com/privacy/"},{"id":88,"name":"TreSensa, Inc.","purposeIds":[1,3],"legIntPurposeIds":[2,5],"featureIds":[1],"policyUrl":"https://www.tresensa.com/eu-privacy"},{"id":78,"name":"Flashtalking, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.flashtalking.com/privacypolicy/"},{"id":59,"name":"Sift Media, Inc","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.sift.co/privacy"},{"id":114,"name":"Sublime","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://ayads.co/privacy.php"},{"id":175,"name":"FORTVISION","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://fortvision.com/POC/index.html","deletedDate":"2019-08-09T00:00:00Z"},{"id":133,"name":"digitalAudience","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://digitalaudience.io/legal/privacy-cookies/"},{"id":14,"name":"Adkernel LLC","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://adkernel.com/privacy-policy/"},{"id":180,"name":"Thirdpresence Oy","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"http://www.thirdpresence.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":183,"name":"EMX Digital LLC","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://emxdigital.com/privacy/"},{"id":58,"name":"33Across","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.33across.com/privacy-policy"},{"id":140,"name":"Platform161","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://platform161.com/cookie-and-privacy-policy/"},{"id":90,"name":"Teroa S.A.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.e-planning.net/en/privacy.html"},{"id":141,"name":"1020, Inc. dba Placecast and Ericsson Emodo","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.emodoinc.com/privacy-policy/"},{"id":142,"name":"Media.net Advertising FZ-LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.media.net/en/privacy-policy"},{"id":209,"name":"Delta Projects AB","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[3],"policyUrl":"https://deltaprojects.com/data-collection-policy"},{"id":195,"name":"advanced store GmbH","purposeIds":[2,3],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.advanced-store.com/de/datenschutz/"},{"id":190,"name":"video intelligence AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.vi.ai/privacy-policy/"},{"id":84,"name":"Semasio GmbH","purposeIds":[],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"http://www.semasio.com/privacy-policy/"},{"id":65,"name":"Location Sciences AI Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.locationsciences.ai/privacy-policy/"},{"id":210,"name":"Zemanta, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1],"policyUrl":"http://www.zemanta.com/legal/privacy"},{"id":200,"name":"Tapjoy, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.tapjoy.com/legal/#privacy-policy"},{"id":188,"name":"Sellpoints Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://retargeter.com/service-privacy-policy/","deletedDate":"2019-09-17T00:00:00Z"},{"id":217,"name":"2KDirect, Inc. (dba iPromote)","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.ipromote.com/privacy-policy/"},{"id":156,"name":"Centro, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.centro.net/privacy-policy/"},{"id":194,"name":"Rezonence Limited","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://rezonence.com/privacy-policy/"},{"id":226,"name":"Publicis Media GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.publicismedia.de/datenschutz/"},{"id":198,"name":"SYNC","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://redirect.sync.tv/privacy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":227,"name":"ORTEC B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.ortecadscience.com/privacy-policy/"},{"id":225,"name":"Ligatus GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.ligatus.com/en/privacy-policy","deletedDate":"2020-06-19T00:00:00Z"},{"id":205,"name":"Adssets AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://adssets.com/policy/"},{"id":179,"name":"Collective Europe Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.collectiveuk.com/privacy.html"},{"id":31,"name":"Ogury Ltd.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://www.ogury.com/privacy-policy/"},{"id":92,"name":"1plusX AG","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.1plusx.com/privacy-policy/"},{"id":155,"name":"AntVoice","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.antvoice.com/en/privacypolicy/"},{"id":115,"name":"smartclip Europe GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://privacy-portal.smartclip.net/"},{"id":126,"name":"DoubleVerify Inc.\u200b","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.doubleverify.com/privacy/"},{"id":193,"name":"Mediasmart Mobile S.L.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://mediasmart.io/privacy/"},{"id":245,"name":"IgnitionOne","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.ignitionone.com/privacy-policy/","deletedDate":"2020-06-30T00:00:00Z"},{"id":213,"name":"emetriq GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.emetriq.com/datenschutz/"},{"id":244,"name":"Temelio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://temelio.com/vie-privee"},{"id":224,"name":"adrule mobile GmbH","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.adrule.net/de/datenschutz/"},{"id":174,"name":"A Million Ads Ltd","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.amillionads.com/privacy-policy"},{"id":192,"name":"remerge GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://remerge.io/privacy-policy.html"},{"id":232,"name":"Rockerbox, Inc","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"http://rockerbox.com/privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":256,"name":"Bounce Exchange, Inc","purposeIds":[1],"legIntPurposeIds":[2,4,5],"featureIds":[1,2],"policyUrl":"https://www.bouncex.com/privacy/"},{"id":234,"name":"ZBO Media","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zbo.media/mentions-legales/politique-de-confidentialite-service-publicitaire/"},{"id":246,"name":"Smartology Limited","purposeIds":[3],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://www.smartology.net/privacy-policy/"},{"id":241,"name":"OneTag Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.onetag.com/privacy/"},{"id":254,"name":"LiquidM Technology GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liquidm.com/privacy-policy/"},{"id":215,"name":"ARMIS SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://armis.tech/en/armis-personal-data-privacy-policy/"},{"id":167,"name":"Audiens S.r.l.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.audiens.com/privacy"},{"id":240,"name":"7Hops.com Inc. (ZergNet)","purposeIds":[],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://zergnet.com/privacy"},{"id":235,"name":"Bucksense Inc","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.bucksense.com/platform-privacy-policy/"},{"id":185,"name":"Bidtellect, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.bidtellect.com/privacy-policy/"},{"id":258,"name":"Adello Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.adello.com/privacy-policy/"},{"id":169,"name":"RTK.IO, Inc","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://www.rtk.io/privacy.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":208,"name":"Spotad","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.spotad.co/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":211,"name":"AdTheorent, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://adtheorent.com/privacy-policy"},{"id":229,"name":"Digitize New Media Ltd","purposeIds":[2,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitize.ie/online-privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":273,"name":"Bannerflow AB","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.bannerflow.com/privacy "},{"id":104,"name":"Sonobi, Inc","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"http://sonobi.com/privacy-policy/"},{"id":162,"name":"Unruly Group Ltd","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://unruly.co/privacy/"},{"id":249,"name":"Spolecznosci Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.spolecznosci.pl/polityka-prywatnosci"},{"id":125,"name":"Research Now Group, Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.valuedopinions.co.uk/privacy","deletedDate":"2019-09-17T00:00:00Z"},{"id":170,"name":"Goodway Group, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://goodwaygroup.com/privacy-policy/"},{"id":160,"name":"Netsprint SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://netsprint.eu/privacy.html"},{"id":189,"name":"Intowow Innovation Ltd.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.intowow.com/privacy/","deletedDate":"2019-08-12T00:00:00Z"},{"id":279,"name":"Mirando GmbH & Co KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://wwwmirando.de/datenschutz/"},{"id":269,"name":"Sanoma Media Finland","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://sanoma.fi/tietoa-meista/tietosuoja/","deletedDate":"2019-08-07T00:00:00Z"},{"id":276,"name":"Viralize SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://viralize.com/privacy-policy"},{"id":87,"name":"Genius Sports Media Limited","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[2,3],"policyUrl":"https://www.geniussports.com/privacy-policy"},{"id":182,"name":"Collective, Inc. dba Visto","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vistohub.com/privacy-policy/","deletedDate":"2019-07-26T00:00:00Z"},{"id":255,"name":"Onnetwork Sp. z o.o.","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.onnetwork.tv/pp_services.php"},{"id":203,"name":"Revcontent, LLC","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://intercom.help/revcontent2/en/articles/2290675-revcontent-s-privacy-policy"},{"id":260,"name":"RockYou, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,5],"featureIds":[3],"policyUrl":"https://rockyou.com/privacy-policy/","deletedDate":"2019-08-09T00:00:00Z"},{"id":237,"name":"LKQD, a division of Nexstar Digital, LLC.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.lkqd.com/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":274,"name":"Golden Bees","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.goldenbees.fr/en/privacy-charter/"},{"id":280,"name":"Spot.IM LTD","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.spot.im/privacy/"},{"id":239,"name":"Triton Digital Canada Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.tritondigital.com/privacy-policies"},{"id":177,"name":"plista GmbH","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.plista.com/about/privacy/"},{"id":201,"name":"TimeOne","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://privacy.timeonegroup.com/en/","deletedDate":"2020-05-15T00:00:00Z"},{"id":150,"name":"Inskin Media LTD","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.inskinmedia.com/privacy-policy.html"},{"id":252,"name":"Jaduda GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.jadudamobile.com/datenschutzerklaerung/"},{"id":248,"name":"Converge-Digital","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://converge-digital.com/privacy-policy/"},{"id":161,"name":"Smadex SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://smadex.com/end-user-privacy-policy/"},{"id":285,"name":"Comcast International France SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.freewheel.com/privacy-policy"},{"id":228,"name":"McCann Discipline LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.primis.tech/privacy-policy/"},{"id":299,"name":"AdClear GmbH","purposeIds":[1,5],"legIntPurposeIds":[2,3,4],"featureIds":[1,2],"policyUrl":"https://www.adclear.de/datenschutzerklaerung/"},{"id":277,"name":"Codewise VL Sp. z o.o. Sp. k","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://voluumdsp.com/end-user-privacy-policy/"},{"id":259,"name":"ADYOULIKE SA","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.adyoulike.com/privacy_policy.php"},{"id":272,"name":"A.Mob","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.we-are-adot.com/privacy-policy/"},{"id":230,"name":"Steel House, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://steelhouse.com/privacy-policy/"},{"id":253,"name":"Improve Digital BV","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.improvedigital.com/platform-privacy-policy"},{"id":304,"name":"On Device Research Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://s.on-device.com/privacyPolicy"},{"id":314,"name":"Keymantics","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.keymantics.com/assets/privacy-policy.pdf"},{"id":257,"name":"R-TARGET","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"http://www.r-target.com/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":317,"name":"mainADV Srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.mainad.com/privacy-policy/"},{"id":278,"name":"Integral Ad Science, Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://integralads.com/privacy-policy/"},{"id":291,"name":"Qwertize","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.qwertize.com/en/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":295,"name":"Sojern, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.sojern.com/privacy/product-privacy-policy/"},{"id":315,"name":"Celtra, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.celtra.com/privacy-policy/"},{"id":165,"name":"SpotX, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.spotx.tv/privacy-policy/"},{"id":47,"name":"ADMAN - Phaistos Networks, S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adman.gr/privacy"},{"id":134,"name":"SMARTSTREAM.TV GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://www.smartstream.tv/en/productprivacy"},{"id":325,"name":"Knorex","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.knorex.com/privacy"},{"id":316,"name":"Gamned","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.gamned.com/privacy-policy/"},{"id":318,"name":"Accorp Sp. z o.o.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"http://www.instytut-pollster.pl/privacy-policy/"},{"id":199,"name":"ADUX","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adux.com/donnees-personelles/"},{"id":236,"name":"PowerLinks Media Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[3],"policyUrl":"https://www.powerlinks.com/privacy-policy/"},{"id":294,"name":"Jivox Corporation","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.jivox.com/privacy"},{"id":143,"name":"Connatix Native Exchange Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://connatix.com/privacy-policy/"},{"id":297,"name":"Polar Mobile Group Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://privacy.polar.me"},{"id":319,"name":"Clipcentric, Inc.","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://clipcentric.com/privacy.bhtml"},{"id":290,"name":"Readpeak Oy","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://readpeak.com/privacy-policy/"},{"id":323,"name":"DAZN Media Services Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.goal.com/en-gb/legal/privacy-policy"},{"id":119,"name":"Fusio by S4M","purposeIds":[1,2,5],"legIntPurposeIds":[3],"featureIds":[1,3],"policyUrl":"http://www.s4m.io/privacy-policy/"},{"id":302,"name":"Mobile Professionals BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mobpro.com/privacy.html"},{"id":212,"name":"usemax advertisement (Emego GmbH)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.usemax.de/?l=privacy"},{"id":264,"name":"Adobe Advertising Cloud","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.adobe.com/privacy/experience-cloud.html"},{"id":44,"name":"The ADEX GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://theadex.com/privacy-opt-out/"},{"id":282,"name":"Welect GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.welect.de/datenschutz"},{"id":238,"name":"StackAdapt","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.stackadapt.com/privacy"},{"id":284,"name":"WEBORAMA","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://weborama.com/privacy_en/"},{"id":148,"name":"Liveintent Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://liveintent.com/services-privacy-policy/"},{"id":64,"name":"DigiTrust / IAB Tech Lab","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitru.st/privacy-policy/"},{"id":301,"name":"zeotap GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://zeotap.com/privacy_policy"},{"id":275,"name":"TabMo SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://static.tabmo.io.s3.amazonaws.com/privacy-policy/index.html"},{"id":310,"name":"Adevinta Spain S.L.U.","purposeIds":[],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"https://www.adevinta.com/about/privacy/"},{"id":139,"name":"Permodo GmbH","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://permodo.com/de/privacy.html"},{"id":326,"name":"AdTiming Technology Company Limited","purposeIds":[3,5],"legIntPurposeIds":[1,2,4],"featureIds":[],"policyUrl":"http://www.adtiming.com/en/privacypolicy.html"},{"id":262,"name":"Fyber ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.fyber.com/legal/privacy-policy/"},{"id":331,"name":"ad6media","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.ad6media.fr/privacy"},{"id":345,"name":"The Kantar Group Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.kantar.com/cookies-policies"},{"id":308,"name":"Rockabox Media Ltd","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[],"policyUrl":"http://scoota.com/privacy-policy"},{"id":270,"name":"Marfeel Solutions, SL","purposeIds":[],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.marfeel.com/privacy-policy/"},{"id":333,"name":"InMobi Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":202,"name":"Telaria, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":328,"name":"Gemius SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.gemius.com/cookie-policy.html"},{"id":281,"name":"Wizaly","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.wizaly.com/terms-of-use#privacy-policy"},{"id":354,"name":"Apester Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://apester.com/privacy-policy/"},{"id":320,"name":"Adelphic LLC","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://adelphic.com/platform/privacy/"},{"id":359,"name":"AerServ LLC","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":265,"name":"Instinctive, Inc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://instinctive.io/privacy"},{"id":349,"name":"Optomaton UG","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://optomaton.com/privacy.html"},{"id":288,"name":"Video Media Groep B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://www.videomediagroup.com/wp-content/uploads/2016/01/Privacy-policy-VMG.pdf","deletedDate":"2019-09-17T00:00:00Z"},{"id":266,"name":"Digilant Spain, SLU","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.digilant.com/es/politica-privacidad/"},{"id":339,"name":"Vuble","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vuble.tv/us/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":303,"name":"Orion Semantics","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://static.orion-semantics.com/privacy.html"},{"id":261,"name":"Signal Digital Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.signal.co/privacy-policy/"},{"id":83,"name":"Visarity Technologies GmbH","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://primo.design/docs/PrivacyPolicyPrimo.html"},{"id":343,"name":"DIGITEKA Technologies","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.ultimedia.com/POLICY.html"},{"id":330,"name":"Linicom","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.linicom.com/privacy/","deletedDate":"2020-06-08T00:00:00Z"},{"id":231,"name":"AcuityAds Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.acuityads.com/corporate-privacy-policy.html"},{"id":216,"name":"Mindlytix SAS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://mindlytix.com/privacy/"},{"id":311,"name":"Mobfox US LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobfox.com/privacy-policy/"},{"id":358,"name":"MGID Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mgid.com/privacy-policy"},{"id":152,"name":"Meetrics GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.meetrics.com/en/data-privacy/"},{"id":251,"name":"Yieldlove GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"http://www.yieldlove.com/cookie-policy"},{"id":344,"name":"My6sense Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[2,4],"featureIds":[],"policyUrl":"https://my6sense.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":347,"name":"Ezoic Inc.","purposeIds":[2,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.ezoic.com/terms/"},{"id":218,"name":"Bigabid Media ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.bigabid.com/privacy-policy"},{"id":350,"name":"Free Stream Media Corp. dba Samba TV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":351,"name":"Samba TV UK Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":341,"name":"Somo Audience Corp","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"https://somoaudience.com/legal/","deletedDate":"2020-07-06T00:00:00Z"},{"id":380,"name":"Vidoomy Media SL","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"http://vidoomy.com/privacy-policy.html"},{"id":378,"name":"communicationAds GmbH & Co. KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.communicationads.net/aboutus/privacy/"},{"id":369,"name":"Getintent USA, inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://getintent.com/privacy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":184,"name":"mediarithmics SAS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mediarithmics.com/en-us/content/privacy-policy"},{"id":368,"name":"VECTAURY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vectaury.io/en/personal-data"},{"id":373,"name":"Nielsen Marketing Cloud","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"http://www.nielsen.com/us/en/privacy-statement/exelate-privacy-policy.html"},{"id":214,"name":"Digital Control GmbH & Co. KG","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://advolution.de/privacy.php","deletedDate":"2020-05-06T00:00:00Z"},{"id":388,"name":"numberly","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://numberly.com/en/privacy/"},{"id":250,"name":"Qriously Ltd","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.brandwatch.com/legal/qriously-privacy-notice/"},{"id":223,"name":"Audience Trading Platform Ltd.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://atp.io/privacy-policy"},{"id":387,"name":"Triapodi Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appreciate.mobi/page.html#/end-user-privacy-policy"},{"id":312,"name":"Exactag GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.exactag.com/en/data-privacy/"},{"id":178,"name":"Hybrid Theory","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://hybridtheory.com/privacy-policy/"},{"id":377,"name":"AddApptr GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.addapptr.com/data-privacy"},{"id":382,"name":"The Reach Group GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://trg.de/en/privacy-statement/"},{"id":206,"name":"Hybrid Adtech GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://hybrid.ai/data_protection_policy"},{"id":403,"name":"Mobusi Mobile Advertising S.L.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobusi.com/privacy.en.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":385,"name":"Oracle Data Cloud","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://www.oracle.com/legal/privacy/marketing-cloud-data-cloud-privacy-policy.html"},{"id":404,"name":"Duplo Media AS","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.easy-ads.com/privacypolicy.htm"},{"id":242,"name":"twiago GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.twiago.com/datenschutz/"},{"id":376,"name":"Pocketmath Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pocketmath.com/privacy-policy"},{"id":402,"name":"Effiliation","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://inter.effiliation.com/politique-confidentialite.html"},{"id":413,"name":"Eulerian Technologies","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.eulerian.com/en/privacy/"},{"id":400,"name":"Whenever Media Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.whenevermedia.com/privacy-policy","deletedDate":"2019-07-29T00:00:00Z"},{"id":171,"name":"Webedia","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webedia-group.com/site/privacy-policy","deletedDate":"2020-07-01T00:00:00Z"},{"id":398,"name":"Yormedia Solutions Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.yormedia.com/privacy-and-cookies-notice/","deletedDate":"2019-08-06T00:00:00Z"},{"id":415,"name":"Seenthis AB","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://seenthis.co/privacy-notice-2018-04-18.pdf"},{"id":263,"name":"Nativo, Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.nativo.com/interest-based-ads"},{"id":329,"name":"Browsi Mobile Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://gobrowsi.com/browsi-privacy-policy/"},{"id":389,"name":"Bidmanagement GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adspert.net/en/privacy/","deletedDate":"2020-07-01T00:00:00Z"},{"id":337,"name":"SheMedia, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shemedia.com/ad-services-privacy-policy"},{"id":422,"name":"Brand Metrics Sweden AB","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://collector.brandmetrics.com/brandmetrics_privacypolicy.pdf"},{"id":421,"name":"LeftsnRight, Inc. dba LIQWID","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liqwid.solutions/privacy-policy","deletedDate":"2020-06-30T00:00:00Z"},{"id":426,"name":"TradeTracker","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[2],"policyUrl":"https://tradetracker.com/privacy-policy/","deletedDate":"2019-08-21T00:00:00Z"},{"id":394,"name":"AudienceProject Aps","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://privacy.audienceproject.com"},{"id":287,"name":"Avazu Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4],"featureIds":[3],"policyUrl":"http://avazuinc.com/opt-out/","deletedDate":"2020-08-03T00:00:00Z"},{"id":243,"name":"Cloud Technologies S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cloudtechnologies.pl/en/internet-advertising-privacy-policy"},{"id":113,"name":"iotec global Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.iotecglobal.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":338,"name":"dunnhumby Germany GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.sociomantic.com/privacy/en/","deletedDate":"2020-07-17T00:00:00Z"},{"id":405,"name":"IgnitionAi Ltd","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[2],"policyUrl":"https://www.isitelab.io/default.aspx","deletedDate":"2020-07-03T00:00:00Z"},{"id":416,"name":"Commanders Act","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.commandersact.com/en/privacy/"},{"id":434,"name":"DynAdmic","purposeIds":[1,3],"legIntPurposeIds":[2,4],"featureIds":[1,3],"policyUrl":"http://eu.dynadmic.com/privacy-policy/"},{"id":435,"name":"SINGLESPOT SAS ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.singlespot.com/privacy_policy?locale=fr"},{"id":409,"name":"Arrivalist Co.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[1,2],"policyUrl":"https://www.arrivalist.com/privacy"},{"id":321,"name":"Ziff Davis LLC","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.ziffdavis.com/privacy-policy"},{"id":436,"name":"INVIBES GROUP","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[1,2,3],"policyUrl":"http://www.invibes.com/terms"},{"id":442,"name":"R-Advertising","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-20T00:00:00Z"},{"id":362,"name":"Myntelligence S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://myntelligence.com/privacy-page/"},{"id":418,"name":"PROXISTORE","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://www.proxistore.com/common/en/cgv"},{"id":449,"name":"Mobile Journey B.V.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://mobilejourney.com/Privacy-Policy","deletedDate":"2019-09-05T00:00:00Z"},{"id":443,"name":"Tradedoubler AB","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-13T00:00:00Z"},{"id":429,"name":"Signals","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://signalsdata.com/platform-cookie-policy/"},{"id":335,"name":"Beachfront Media LLC","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://beachfront.com/privacy-policy/"},{"id":407,"name":"Publishers Internationale Pty Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pi-rate.com.au/privacy.html","deletedDate":"2019-11-08T00:00:00Z"},{"id":427,"name":"Proxi.cloud Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://proxi.cloud/info/privacy-policy/"},{"id":374,"name":"Bmind a Sales Maker Company, S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bmind.es/legal-notice/"},{"id":438,"name":"INVIDI technologies AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.invidi.com/wp-content/uploads/2020/02/ad-tech-services-privacy-policy.pdf"},{"id":450,"name":"Neodata Group srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.neodatagroup.com/en/security-policy"},{"id":452,"name":"Innovid Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.innovid.com/privacy-policy"},{"id":444,"name":"Playbuzz Ltd (aka EX.CO)","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://ex.co/privacy-policy/"},{"id":412,"name":"Cxense ASA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.cxense.com/about-us/privacy-policy"},{"id":454,"name":"Adimo","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://adimo.co/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":455,"name":"GDMServices, Inc. d/b/a FiksuDSP","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://fiksu.com/privacy-policy/"},{"id":298,"name":"Cuebiq Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.cuebiq.com/privacypolicy/","deletedDate":"2019-08-30T00:00:00Z"},{"id":423,"name":"travel audience GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://travelaudience.com/product-privacy-policy/"},{"id":397,"name":"Demandbase, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.demandbase.com/privacy-policy/"},{"id":381,"name":"Solocal","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://frontend.adhslx.com/privacy.html?"},{"id":425,"name":"ADRINO Sp. z o.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.adrino.pl/ciasteczkowa-polityka/","deletedDate":"2019-09-05T00:00:00Z"},{"id":365,"name":"Forensiq LLC","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1,3],"policyUrl":"https://impact.com/privacy-policy/"},{"id":447,"name":"Adludio Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adludio.com/privacy-policy/"},{"id":410,"name":"Adtelligent Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtelligent.com/privacy-policy/"},{"id":137,"name":"Str\u00f6er SSP GmbH (DSP)","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":395,"name":"PREX Programmatic Exchange GmbH&Co KG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[],"policyUrl":"http://www.programmatic-exchange.com/privacy","deletedDate":"2020-07-03T00:00:00Z"},{"id":462,"name":"Bidstack Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[2],"policyUrl":"https://www.bidstack.com/privacy-policy/"},{"id":466,"name":"TACTIC\u2122 Real-Time Marketing AS","purposeIds":[],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://tacticrealtime.com/privacy/"},{"id":340,"name":"Yieldr UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.yieldr.com/privacy"},{"id":336,"name":"Telecoming S.A.","purposeIds":[3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.telecoming.com/privacy-policy/"},{"id":430,"name":"Ad Unity Ltd","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"http://www.adunity.com/privacy-policy.html","deletedDate":"2019-08-13T00:00:00Z"},{"id":346,"name":"Cybba, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://cybba.com/about/legal/data-processing-agreement/","deletedDate":"2020-08-03T00:00:00Z"},{"id":469,"name":"Zeta Global","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://zetaglobal.com/privacy-policy/"},{"id":440,"name":"DEFINE MEDIA GMBH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.definemedia.de/datenschutz-conative/"},{"id":375,"name":"Affle International","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://affle.com/privacy-policy "},{"id":196,"name":"AdElement Media Solutions Pvt Ltd","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"http://adelement.com/privacy-policy.html"},{"id":268,"name":"Social Tokens Ltd. ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://woobi.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":475,"name":"TAPTAP Digital SL","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1,2,3],"policyUrl":"http://www.taptapnetworks.com/privacy_policy/"},{"id":474,"name":"hbfsTech","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.hbfstech.com/fr/privacy.html"},{"id":448,"name":"Targetspot Belgium SPRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://marketing.targetspot.com/Targetspot/Legal/TargetSpot%20Privacy%20Policy%20-%20June%202018.pdf"},{"id":428,"name":"Internet BillBoard a.s.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.ibillboard.com/en/privacy-information/"},{"id":461,"name":"B2B Media Group EMEA GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selfcampaign.com/static/privacy","deletedDate":"2019-08-14T00:00:00Z"},{"id":476,"name":"HIRO Media Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"http://hiro-media.com/privacy.php"},{"id":480,"name":"pilotx.tv","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[1,2,3],"policyUrl":"https://pilotx.tv/privacy/"},{"id":366,"name":"CerebroAd.com s.r.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.cerebroad.com/privacy-policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":392,"name":"Str\u00f6er Mobile Performance GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[3],"policyUrl":"https://stroeermobileperformance.com/?dl=privacy"},{"id":357,"name":"Totaljobs Group Ltd ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.totaljobs.com/privacy-policy"},{"id":486,"name":"Madington","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://delivered-by-madington.com/dat-privacy-policy/"},{"id":468,"name":"NeuStar, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://www.home.neustar/privacy"},{"id":458,"name":"AdColony, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"adcolony.com/privacy-policy/"},{"id":489,"name":"YellowHammer Media Group","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.yhmg.com/privacy-policy/","deletedDate":"2019-11-27T00:00:00Z"},{"id":293,"name":"SpringServe, LLC","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://springserve.com/privacy-policy/"},{"id":484,"name":"STRIATUM SAS","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://adledge.com/data-privacy/"},{"id":493,"name":"Carbon (AI) Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://carbonrmp.com/privacy.html"},{"id":495,"name":"Arcspire Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://public.arcspire.io/privacy.pdf"},{"id":496,"name":"Automattic Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://en.blog.wordpress.com/2017/12/04/updated-privacy-policy/"},{"id":424,"name":"KUPONA GmbH","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.kupona.de/dsgvo/"},{"id":408,"name":"Fidelity Media","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://fidelity-media.com/privacy-policy/"},{"id":473,"name":"Sub2 Technologies Ltd","purposeIds":[3,4,5],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.sub2tech.com/privacy-policy/"},{"id":467,"name":"Haensel AMS GmbH","purposeIds":[3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://haensel-ams.com/data-privacy/"},{"id":490,"name":"PLAYGROUND XYZ EMEA LTD","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://playground.xyz/privacy"},{"id":464,"name":"Oracle AddThis","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.addthis.com/privacy/privacy-policy/","deletedDate":"2020-02-12T00:00:00Z"},{"id":491,"name":"Triboo Data Analytics","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shinystat.com/it/informativa_privacy_generale.html"},{"id":499,"name":"PurposeLab, LLC","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://purposelab.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":502,"name":"NEXD","purposeIds":[5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://nexd.com/privacy-policy"},{"id":465,"name":"Schibsted Product and Tech UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.schibsted.com/","deletedDate":"2019-07-26T00:00:00Z"},{"id":497,"name":"Little Big Data sp.z.o.o.","purposeIds":[1,2,4],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://dtxngr.com/legal/"},{"id":492,"name":"LotaData, Inc.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1],"policyUrl":"https://lotadata.com/privacy_policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":512,"name":"PubNative GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://pubnative.net/privacy-notice/"},{"id":471,"name":"FlexOffers.com, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.flexoffers.com/privacy-policy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":494,"name":"Cablato Limited","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://cablato.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":516,"name":"Pexi B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://pexi.nl/privacy-policy/"},{"id":507,"name":"AdsWizz Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://www.adswizz.com/our-privacy-policy/"},{"id":482,"name":"UberMedia, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ubermedia.com/summary-of-privacy-policy/"},{"id":505,"name":"Shopalyst Inc","purposeIds":[1,2],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shortlyst.com/eu/privacy_terms.html"},{"id":517,"name":"SunMedia ","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2],"policyUrl":"https://www.sunmedia.tv/en/cookies"},{"id":518,"name":"Accelerize Inc.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://getcake.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":511,"name":"Admixer EU GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://admixer.com/privacy/"},{"id":479,"name":"INFINIA MOBILE S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.infiniamobile.com/privacy_policy"},{"id":513,"name":"Shopstyle","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shopstyle.co.uk/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":509,"name":"ATG Ad Tech Group GmbH","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ad-tech-group.com/privacy-policy/"},{"id":521,"name":"netzeffekt GmbH","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.netzeffekt.de/en/imprint"},{"id":487,"name":"nugg.ad GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1],"policyUrl":"https://www.nugg.ad/en/privacy/general-information.html","deletedDate":"2019-10-03T00:00:00Z"},{"id":515,"name":"ZighZag","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zighzag.com/privacy"},{"id":520,"name":"ChannelSight ","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.channelsight.com/privacypolicy/"},{"id":524,"name":"The Ozone Project Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://ozoneproject.com/privacy-policy"},{"id":529,"name":"Fidzup","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.fidzup.com/en/privacy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":528,"name":"Kayzen","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://kayzen.io/data-privacy-policy"},{"id":527,"name":"Jampp LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://jampp.com/privacy.html"},{"id":506,"name":"salesforce.com, inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.salesforce.com/company/privacy/"},{"id":534,"name":"SmartyAds Inc.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://smartyads.com/privacy-policy"},{"id":535,"name":"INNITY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.innity.com/privacy-policy.php"},{"id":514,"name":"Uprival LLC","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://uprival.com/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":522,"name":"Tealium Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://tealium.com/privacy-policy/"},{"id":530,"name":"Near Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://near.co/privacy"},{"id":539,"name":"AdDefend GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.addefend.com/en/privacy-policy/"},{"id":501,"name":"Alliance Gravity Data Media","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.alliancegravity.com/politiquedeprotectiondesdonneespersonnelles"},{"id":519,"name":"Chargeads","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.chargeplatform.com/privacy"},{"id":523,"name":"X-Mode Social, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://xmode.io/privacy-policy.html"},{"id":537,"name":"RUN, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.runads.com/privacy-policy"},{"id":531,"name":"Smartclip Hispania SL","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://rgpd-smartclip.com/"},{"id":536,"name":"GlobalWebIndex","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"http://legal.trendstream.net/non-panellist_privacy_policy"},{"id":542,"name":"Densou Trading Desk ApS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://densou.dk/Policy.html","deletedDate":"2020-01-21T00:00:00Z"},{"id":525,"name":"PUB OCEAN LIMITED","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://rta.pubocean.com/privacy-policy/","deletedDate":"2019-10-03T00:00:00Z"},{"id":544,"name":"Kochava Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://www.kochava.com/support-privacy/"},{"id":543,"name":"PaperG, Inc. dba Thunder Industries","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.makethunder.com/privacy"},{"id":334,"name":"Cydersoft","purposeIds":[],"legIntPurposeIds":[1,2,3,4],"featureIds":[2,3],"policyUrl":"http://www.videmob.com/privacy.html"},{"id":551,"name":"Illuma Technology Limited","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.weareilluma.com/endddd","deletedDate":"2019-11-14T00:00:00Z"},{"id":540,"name":"Tunnl BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://tunnl.com/privacy.html","deletedDate":"2019-12-20T00:00:00Z"},{"id":547,"name":"Video Reach","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.videoreach.de/about/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":546,"name":"Smart Traffik","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://okube-attribution.com/politique-de-confidentialite/"},{"id":541,"name":"DeepIntent, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.deepintent.com/privacypolicy"},{"id":545,"name":"Reignn Platform Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://reignn.com/user-privacy-policy"},{"id":439,"name":"Bit Q Holdings Limited","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.rippll.com/privacy"},{"id":553,"name":"Adhese","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://adhese.com/privacy-and-cookie-policy"},{"id":556,"name":"adhood.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://v3.adhood.com/en/site/politikavekurallar/gizlilik.php?lang=en"},{"id":550,"name":"Happydemics","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.iubenda.com/privacy-policy/69056167/full-legal"},{"id":560,"name":"Leiki Ltd.","purposeIds":[1,2,3],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"http://www.leiki.com/privacy","deletedDate":"2020-01-07T00:00:00Z"},{"id":554,"name":"RMSi Radio Marketing Service interactive GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.rms.de/datenschutz/"},{"id":498,"name":"Mediakeys Platform","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://drbanner.com/privacypolicy_en/"},{"id":565,"name":"Adobe Audience Manager","purposeIds":[1,2,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adobe.com/privacy/policy.html"},{"id":118,"name":"Drawbridge, Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.drawbridge.com/privacy/","deletedDate":"2020-03-06T00:00:00Z"},{"id":572,"name":"CHEQ AI TECHNOLOGIES LTD.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.cheq.ai/privacy"},{"id":571,"name":"ViewPay","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://viewpay.tv/mentions-legales/"},{"id":568,"name":"Jointag S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.jointag.com/privacy/kariboo/publisher/third/"},{"id":570,"name":"Czech Publisher Exchange z.s.p.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cpex.cz/pro-uzivatele/ochrana-soukromi/"},{"id":559,"name":"Otto (GmbH & Co KG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2],"policyUrl":"https://www.otto.de/shoppages/service/datenschutz"},{"id":548,"name":"LBC France","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.leboncoin.fr/dc/cookies","deletedDate":"2020-04-23T00:00:00Z"},{"id":569,"name":"Kairos Fire","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.kairosfire.com/privacy"},{"id":577,"name":"Neustar on behalf of The Procter & Gamble Company","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pg.com/privacy/english/privacy_statement.shtml"},{"id":590,"name":"Sourcepoint Technologies, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.sourcepoint.com/privacy-policy"},{"id":587,"name":"Localsensor B.V.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.localsensor.com/privacy.html"},{"id":578,"name":"MAIRDUMONT NETLETIX GmbH&Co. KG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mairdumont-netletix.com/datenschutz"},{"id":580,"name":"Goldbach Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://goldbach.com/ch/de/datenschutz"},{"id":593,"name":"Programatica de publicidad S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://datmean.com/politica-privacidad/"},{"id":574,"name":"Realeyes OU","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://realview.realeyesit.com/privacy"},{"id":581,"name":"Mobilewalla, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.mobilewalla.com/business-services-privacy-policy"},{"id":598,"name":"audio content & control GmbH","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://www.audio-cc.com/audiocc_privacy_policy.pdf"},{"id":596,"name":"InsurAds Technologies SA.","purposeIds":[3],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.insurads.com/privacy.html"},{"id":576,"name":"StartApp Inc.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://www.startapp.com/policy/privacy-policy/","deletedDate":"2020-04-23T00:00:00Z"},{"id":592,"name":"Colpirio.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy-policy.colpirio.com/en/","deletedDate":"2020-03-18T00:00:00Z"},{"id":549,"name":"Bandsintown Amplified LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://corp.bandsintown.com/privacy"},{"id":597,"name":"Better Banners A/S","purposeIds":[],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://betterbanners.com/en/privacy"},{"id":601,"name":"WebAds B.V","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.webads.eu/"},{"id":599,"name":"Maximus Live LLC","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://maximusx.com/privacy-policy/"},{"id":604,"name":"Join","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.teamjoin.fr/privacy.html","deletedDate":"2020-04-23T00:00:00Z"},{"id":606,"name":"Impactify ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://impactify.io/privacy-policy/"},{"id":608,"name":"News and Media Holding, a.s.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.newsandmedia.sk/gdpr/"},{"id":602,"name":"Online Solution Int Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://adsafety.net/privacy.html"},{"id":591,"name":"Consumable, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://consumable.com/privacy-policy.html"},{"id":614,"name":"Market Resource Partners LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.mrpfd.com/privacy-policy/"},{"id":615,"name":"Adsolutions BV","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adsolutions.com/privacy-policy/"},{"id":607,"name":"ucfunnel Co., Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.ucfunnel.com/privacy-policy"},{"id":609,"name":"Predicio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.predic.io/privacy"},{"id":617,"name":"Onfocus (Adagio)","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adagio.io/privacy"},{"id":620,"name":"Blue","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.getblue.io/privacy/"},{"id":610,"name":"Azerion Holding B.V.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://azerion.com/business/privacy.html"},{"id":621,"name":"Seznam.cz, a.s.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://www.seznam.cz/ochranaudaju"},{"id":624,"name":"Norstat AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.norstatpanel.com/en/data-protection"},{"id":623,"name":"Adprime Media Inc. ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adprimehealth.com/privacy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":95,"name":"Lotame Solutions, inc","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[2],"policyUrl":"https://www.lotame.com/about-lotame/privacy/lotame-corporate-websites-privacy-policy/"},{"id":618,"name":"BEINTOO SPA","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.beintoo.com/privacy-cookie-policy/"},{"id":619,"name":"Capitaldata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.capitaldata.fr/privacy"},{"id":625,"name":"BILENDI SA","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.maximiles.com/privacy-policy"},{"id":628,"name":": Tappx","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.tappx.com/en/privacy-policy/"},{"id":626,"name":"Hivestack Inc.","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://hivestack.com/privacy-policy"},{"id":631,"name":"Relay42 Netherlands B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://relay42.com/privacy"},{"id":627,"name":"D-Edge","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.d-edge.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":644,"name":"Gamoshi LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.gamoshi.com/privacy-policy"},{"id":639,"name":"Smile Wanted Group","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.smilewanted.com/privacy.php"},{"id":635,"name":"WebMediaRM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webmediarm.com/vie_privee_et_opposition_en.php"},{"id":579,"name":"Ve Global","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.ve.com/privacy-policy"},{"id":645,"name":"Noster Finance S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.finect.com/terminos-legales/politica-de-cookies"},{"id":653,"name":"Smartme Analytics","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"http://smartmeapp.com/info/smartme/aviso_legal.php","deletedDate":"2020-07-03T00:00:00Z"},{"id":613,"name":"Adserve.zone / Artworx AS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adserve.zone/adserveprivacypolicy.html"},{"id":573,"name":"Dailymotion SA","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2],"policyUrl":"https://www.dailymotion.com/legal/privacy"},{"id":652,"name":"Skaze","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.skaze.fr/rgpd/"},{"id":646,"name":"Notify","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"https://notify-group.com/en/mentions-legales/"},{"id":648,"name":"TrueData Solutions, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.truedata.co/privacy-policy/"},{"id":647,"name":"Axel Springer Teaser Ad GmbH","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://www.adup-tech.com/privacy"},{"id":654,"name":"GRAPHINIUM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.graphinium.com/privacy/"},{"id":659,"name":"Research and Analysis of Media in Sweden AB","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www2.rampanel.com/privacy-policy/"},{"id":656,"name":"Think Clever Media","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.contentignite.com/privacy-policy/"},{"id":504,"name":"Alive & Kicking Global Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mcsaatchiplc.com/legal/privacy-cookies","deletedDate":"2020-07-27T00:00:00Z"},{"id":657,"name":"GP One GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.gsi-one.org/de/privacy-policy.html"},{"id":655,"name":"Sportradar AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sportradar.com/about-us/privacy/"},{"id":662,"name":"SoundCast","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://soundcast.fm/en/data-privacy"},{"id":665,"name":"Digital East GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.digitaleast.mobi/en/legal/privacy-policy/"},{"id":650,"name":"Telefonica Investigaci\u00f3n y Desarrollo S.A.U","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.cognitivemarketing.tid.es/"},{"id":666,"name":"BeOp","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://beop.io/privacy"},{"id":663,"name":"Mobsuccess","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.mobsuccess.com/en/privacy"},{"id":658,"name":"BLIINK SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://bliink.io/privacy-policy"},{"id":667,"name":"Liftoff Mobile, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://liftoff.io/privacy-policy/"},{"id":668,"name":"WhatRocks Inc. ","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.whatrocks.co/en/privacy-policy "},{"id":670,"name":"Timehop, Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.timehop.com/privacy"},{"id":674,"name":"Duration Media, LLC.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.durationmedia.net/privacy-policy"},{"id":675,"name":"Instreamatic inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://instreamatic.com/privacy-policy/"},{"id":676,"name":"BusinessClick","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.businessclick.com/documents/RegulaminProgramuBusinessClick-2019.pdf"},{"id":677,"name":"Intercept Interactive Inc. dba Undertone","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.undertone.com/privacy/"},{"id":660,"name":"Schibsted Norge AS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://static.vg.no/privacy/","deletedDate":"2019-09-16T00:00:00Z"},{"id":673,"name":"TTNET AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.programattik.com/en/privacy-policy.aspx"},{"id":664,"name":"adMarketplace, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.admarketplace.com/privacy-policy/"},{"id":671,"name":"Mediaforce LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://casino.mindthebet.co.uk/themes/mindthebetv2-casino/privacy.php"},{"id":561,"name":"AuDigent","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://audigent.com/platform-privacy-policy"},{"id":682,"name":"Radio Net Media Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.adtonos.com/service-privacy-policy/"},{"id":684,"name":"Blue Billywig BV","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.bluebillywig.com/privacy-statement/"},{"id":686,"name":"The MediaGrid Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.themediagrid.com/privacy-policy/"},{"id":685,"name":"Arkeero","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://arkeero.com/privacy-2/"},{"id":687,"name":"MISSENA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://missena.com/confidentialite/"},{"id":690,"name":"Go.pl sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://go.pl/polityka-prywatnosci/"},{"id":691,"name":"Lifesight Pte. Ltd.","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.lifesight.io/privacy-policy/"},{"id":697,"name":"ADWAYS SAS","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.adways.com/confidentialite/?lang=en"},{"id":681,"name":"MyTraffic","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mytraffic.io/en/privacy"},{"id":649,"name":"adality GmbH","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[1],"policyUrl":"https://adality.de/en/privacy/"},{"id":712,"name":"Inspired Mobile Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://byinspired.com/privacypolicy.pdf"},{"id":688,"name":"Effinity","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.effiliation.com/politique-de-confidentialite/"},{"id":702,"name":"Kwanko","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.kwanko.com/fr/rgpd/"},{"id":715,"name":"BidBerry SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.bidberrymedia.com/privacy-policy/"},{"id":713,"name":"Dataseat Ltd","purposeIds":[2,5],"legIntPurposeIds":[1,3,4],"featureIds":[],"policyUrl":"https://dataseat.com/privacy-policy"},{"id":716,"name":"OnAudience Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.onaudience.com/internet-advertising-privacy-policy"},{"id":708,"name":"Dugout Limited ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://dugout.com/privacy-policy"},{"id":717,"name":"Audience Network","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.en.audiencenetwork.pl/internet-advertising-privacy-policy"},{"id":718,"name":"AppConsent Xchange","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://appconsent.io/en/privacy-policy"},{"id":720,"name":"AAX LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://aax.media/privacy/"},{"id":678,"name":"Axonix LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://axonix.com/privacy-cookie-policy/"},{"id":719,"name":"Online Advertising Network Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.oan.pl/en/privacy-policy"},{"id":707,"name":"Dentsu Aegis Network Italia SpA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.dentsuaegisnetwork.com/it/it/policies/info-cookie"},{"id":721,"name":"Beaconspark Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1],"policyUrl":"https://www.engageya.com/privacy"},{"id":724,"name":"Between Exchange","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"https://en.betweenx.com/pdata.pdf"},{"id":728,"name":"Appier PTE Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.appier.com/privacy-policy/"},{"id":729,"name":"Cavai AS & UK ","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://cav.ai/privacy-policy/"},{"id":723,"name":"Adzymic Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.adzymic.co/privacy"},{"id":737,"name":"Monet Engine Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appmonet.com/privacy-policy/"},{"id":740,"name":"6Sense Insights, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://6sense.com/privacy-policy/"},{"id":744,"name":"Vidazoo Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[2],"policyUrl":"https://vidazoo.gitbook.io/vidazoo-legal/privacy-policy"},{"id":731,"name":"GeistM Technologies LTD","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.geistm.com/privacy"},{"id":741,"name":"Brand Advance Limited","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.wearebrandadvance.com/website-privacy-policy"},{"id":734,"name":"Cint AB","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.cint.com/participant-privacy-notice"},{"id":709,"name":"NC Audience Exchange, LLC (NewsIQ)","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.ncaudienceexchange.com/privacy/"},{"id":739,"name":"Blingby LLC","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://blingby.com/privacy"},{"id":732,"name":"Performax.cz, s.r.o.","purposeIds":[2,4,5],"legIntPurposeIds":[1,3],"featureIds":[2,3],"policyUrl":"https://reg.tiscali.cz/privacy-policy"},{"id":736,"name":"BidMachine Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://explorestack.com/privacy-policy/"},{"id":738,"name":"adbility media GmbH","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adbility-media.com/datenschutzerklaerung/"},{"id":742,"name":"Audiencerate LTD","purposeIds":[],"legIntPurposeIds":[1,2,5],"featureIds":[],"policyUrl":"https://www.audiencerate.com/privacy/"},{"id":743,"name":"MOVIads Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://moviads.pl/polityka-prywatnosci/"},{"id":746,"name":"Adxperience SAS","purposeIds":[2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://adxperience.com/privacy-policy/"},{"id":747,"name":"Kairion GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://kairion.de/datenschutzbestimmungen/"},{"id":748,"name":"AUDIOMOB LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.audiomob.io/privacy"},{"id":749,"name":"Good-Loop Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://doc.good-loop.com/policy/privacy-policy.html"},{"id":754,"name":"DistroScale, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.distroscale.com/privacy-policy/"},{"id":756,"name":"Fandom, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"https://www.fandom.com/privacy-policy"},{"id":758,"name":"GfK Netherlands B.V.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://gfkpanel.nl/privacy"},{"id":759,"name":"RevJet","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.revjet.com/privacy"},{"id":760,"name":"VEXPRO TECHNOLOGIES LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://onedash.com/privacy-policy.html"},{"id":761,"name":"Digiseg ApS","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://digiseg.io/privacy-center/"},{"id":763,"name":"Delidatax SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.delidatax.net/privacy.htm"},{"id":764,"name":"Lucidity","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://golucidity.com/privacy-policy/"},{"id":765,"name":"Grabit Interactive Media Inc dba KERV Interctive","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://kervit.com/privacy-policy/"},{"id":766,"name":"ADCELL | Firstlead GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.adcell.de/agb#sector_6"},{"id":768,"name":"Global Media & Entertainment Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://global.com/privacy-policy/"},{"id":770,"name":"MARKETPERF CORP","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.marketperf.com/assets/images/app/marketperf/pdf/privacy-policy.pdf"},{"id":773,"name":"360e-com Sp. z o.o.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.clickonometrics.com/optout/"},{"id":775,"name":"SelectMedia International LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selectmedia.asia/terms-and-privacy/"},{"id":778,"name":"Discover-Tech ltd","purposeIds":[2,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://discover-tech.io/dsp-privacy-policy/"},{"id":779,"name":"Adtarget Medya A.S.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtarget.com.tr/adtarget-privacy-policy-2020.pdf"},{"id":780,"name":"Aniview LTD","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.aniview.com/privacy-policy/"},{"id":781,"name":"FeedAd GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://feedad.com/privacy/"},{"id":784,"name":"Nubo LTD","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.recod3.com/privacypolicy.php"},{"id":786,"name":"TargetVideo GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.target-video.com/datenschutz/"},{"id":798,"name":"Adverticum cPlc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://adverticum.net/english/privacy-and-data-processing-information/"},{"id":803,"name":"Click Tech Limited","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[1],"policyUrl":"https://en.yeahmobi.com/html/privacypolicy/"},{"id":808,"name":"Pure Local Media GmbH","purposeIds":[],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://purelocalmedia.de/?page_id=593"}]} \ No newline at end of file From 501927a632f2ed3e8bdc9e9db16b0541e3113e89 Mon Sep 17 00:00:00 2001 From: PubMatic-OpenWrap Date: Thu, 20 Aug 2020 15:28:32 +0530 Subject: [PATCH 172/381] UOE-5511 Support for skadnetwork in pubmatic (#73) Co-authored-by: Isha Bharti --- adapters/pubmatic/pubmatic.go | 30 ++++++++---- .../pubmatictest/supplemental/app.json | 46 +++++++++++++++++-- openrtb_ext/imp.go | 2 + 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go index 394e4189dfe..7c7063e07b1 100644 --- a/adapters/pubmatic/pubmatic.go +++ b/adapters/pubmatic/pubmatic.go @@ -20,10 +20,13 @@ import ( "golang.org/x/net/context/ctxhttp" ) -const MAX_IMPRESSIONS_PUBMATIC = 30 -const PUBMATIC = "[PUBMATIC]" -const buyId = "buyid" -const buyIdTargetingKey = "hb_buyid_pubmatic" +const ( + MAX_IMPRESSIONS_PUBMATIC = 30 + PUBMATIC = "[PUBMATIC]" + buyId = "buyid" + buyIdTargetingKey = "hb_buyid_pubmatic" + skAdnetworkKey = "skadn" +) type PubmaticAdapter struct { http *adapters.HTTPAdapter @@ -613,13 +616,22 @@ func parseImpressionObject(imp *openrtb.Imp, wrapExt *string, pubID *string) err } } + imp.Ext = nil + impExt := "" if pubmaticExt.Keywords != nil && len(pubmaticExt.Keywords) != 0 { - kvstr := makeKeywordStr(pubmaticExt.Keywords) - imp.Ext = json.RawMessage([]byte(kvstr)) - } else { - imp.Ext = nil + impExt = makeKeywordStr(pubmaticExt.Keywords) } + if bidderExt.Prebid != nil && bidderExt.Prebid.SKAdnetwork != nil { + if impExt == "" { + impExt = fmt.Sprintf(`"%s":%s`, skAdnetworkKey, string(bidderExt.Prebid.SKAdnetwork)) + } else { + impExt = fmt.Sprintf(`%s,"%s":%s`, impExt, skAdnetworkKey, string(bidderExt.Prebid.SKAdnetwork)) + } + } + if len(impExt) != 0 { + imp.Ext = json.RawMessage([]byte(fmt.Sprintf(`{%s}`, impExt))) + } return nil } @@ -635,7 +647,7 @@ func makeKeywordStr(keywords []*openrtb_ext.ExtImpPubmaticKeyVal) string { } } - kvStr := "{" + strings.Join(eachKv, ",") + "}" + kvStr := strings.Join(eachKv, ",") return kvStr } diff --git a/adapters/pubmatic/pubmatictest/supplemental/app.json b/adapters/pubmatic/pubmatictest/supplemental/app.json index 636433ca1f5..3aabe54a7dd 100644 --- a/adapters/pubmatic/pubmatictest/supplemental/app.json +++ b/adapters/pubmatic/pubmatictest/supplemental/app.json @@ -26,6 +26,12 @@ "version": 1, "profile": 5123 } + }, + "prebid": { + "skadn": { + "skadnetids": ["k674qkevps.skadnetwork"], + "version": "2.0" + } } } }], @@ -62,7 +68,11 @@ }, "ext": { "pmZoneID": "Zone1,Zone2", - "preference": "sports,movies" + "preference": "sports,movies", + "skadn": { + "skadnetids": ["k674qkevps.skadnetwork"], + "version": "2.0" + } } } ], @@ -100,7 +110,21 @@ "crid": "29681110", "h": 250, "w": 300, - "dealid":"test deal" + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1, + "skadn": { + "signature": "MDUCGQDreBN5/xBN547tJeUdqcMSBtBA+Lk06b8CGFkjR1V56rh/H9osF8iripkuZApeDsZ+lQ==", + "campaign": "4", + "network": "k674qkevps.skadnetwork", + "nonce": "D0EC0F04-A4BF-445B-ADF1-E010430C29FD", + "timestamp": "1596695461984", + "sourceapp": "525463029", + "itunesitem": "1499436635", + "version": "2.0" + } + } }] } ], @@ -126,11 +150,25 @@ "crid": "29681110", "w": 300, "h": 250, - "dealid":"test deal" + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1, + "skadn": { + "signature": "MDUCGQDreBN5/xBN547tJeUdqcMSBtBA+Lk06b8CGFkjR1V56rh/H9osF8iripkuZApeDsZ+lQ==", + "campaign": "4", + "network": "k674qkevps.skadnetwork", + "nonce": "D0EC0F04-A4BF-445B-ADF1-E010430C29FD", + "timestamp": "1596695461984", + "sourceapp": "525463029", + "itunesitem": "1499436635", + "version": "2.0" + } + } }, "type": "banner" } ] } ] - } \ No newline at end of file + } diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go index ed3a88d62eb..d8ebecb3dc6 100644 --- a/openrtb_ext/imp.go +++ b/openrtb_ext/imp.go @@ -28,6 +28,8 @@ type ExtImpPrebid struct { // at this time // https://github.com/PubMatic-OpenWrap/prebid-server/pull/846#issuecomment-476352224 Bidder map[string]json.RawMessage `json:"bidder"` + + SKAdnetwork json.RawMessage `json:"skadn"` } // ExtStoredRequest defines the contract for bidrequest.imp[i].ext.prebid.storedrequest From 21b41ff22c9fce7823f061077dd00bc679ddd7b9 Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Thu, 20 Aug 2020 12:59:27 -0400 Subject: [PATCH 173/381] Enable geo activation of GDPR flag (#1427) --- config/config.go | 9 +++ exchange/exchange.go | 25 +++++++- exchange/exchange_test.go | 27 +++++--- .../exchangetest/gdpr-geo-eu-off-device.json | 64 +++++++++++++++++++ exchange/exchangetest/gdpr-geo-eu-off.json | 60 +++++++++++++++++ exchange/exchangetest/gdpr-geo-eu-on.json | 60 +++++++++++++++++ exchange/exchangetest/gdpr-geo-usa-off.json | 61 ++++++++++++++++++ exchange/exchangetest/gdpr-geo-usa-on.json | 61 ++++++++++++++++++ gdpr/impl.go | 19 ++++++ 9 files changed, 377 insertions(+), 9 deletions(-) create mode 100644 exchange/exchangetest/gdpr-geo-eu-off-device.json create mode 100644 exchange/exchangetest/gdpr-geo-eu-off.json create mode 100644 exchange/exchangetest/gdpr-geo-eu-on.json create mode 100644 exchange/exchangetest/gdpr-geo-usa-off.json create mode 100644 exchange/exchangetest/gdpr-geo-usa-on.json diff --git a/config/config.go b/config/config.go index 7fc77855810..9e6b1370128 100755 --- a/config/config.go +++ b/config/config.go @@ -159,6 +159,11 @@ type GDPR struct { TCF1 TCF1 `mapstructure:"tcf1"` TCF2 TCF2 `mapstructure:"tcf2"` AMPException bool `mapstructure:"amp_exception"` + // EEACountries (EEA = European Economic Area) are a list of countries where we should assume GDPR applies. + // If the gdpr flag is unset in a request, but geo.country is set, we will assume GDPR applies if and only + // if the country matches one on this list. If both the GDPR flag and country are not set, we default + // to UsersyncIfAmbiguous + EEACountries []string `mapstructure:"eea_countries"` } func (cfg *GDPR) validate(errs configErrors) configErrors { @@ -903,6 +908,10 @@ func SetupViper(v *viper.Viper, filename string) { 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("gdpr.eea_countries", []string{"ALA", "AUT", "BEL", "BGR", "HRV", "CYP", "CZE", "DNK", "EST", + "FIN", "FRA", "GUF", "DEU", "GIB", "GRC", "GLP", "GGY", "HUN", "ISL", "IRL", "IMN", "ITA", "JEY", "LVA", + "LIE", "LTU", "LUX", "MLT", "MTQ", "MYT", "NLD", "NOR", "POL", "PRT", "REU", "ROU", "BLM", "MAF", "SPM", + "SVK", "SVN", "ESP", "SWE", "GBR"}) 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") diff --git a/exchange/exchange.go b/exchange/exchange.go index cf5ec9cc000..53f4a7a3e1f 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -55,6 +55,7 @@ type exchange struct { UsersyncIfAmbiguous bool defaultTTLs config.DefaultTTLs privacyConfig config.Privacy + eeaCountries map[string]struct{} } // Container to pass out response ext data from the GetAllBids goroutines back into the main thread @@ -75,6 +76,10 @@ 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) + var s struct{} + for _, c := range cfg.GDPR.EEACountries { + e.eeaCountries[c] = s + } e.adapterMap = newAdapterMap(client, cfg, infos, metricsEngine) e.cache = cache e.cacheTime = time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond @@ -121,9 +126,27 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque e.me.RecordImps(impLabels) } + // Make our best guess if GDPR applies + usersyncIfAmbiguous := e.UsersyncIfAmbiguous + var geo *openrtb.Geo = nil + if bidRequest.User != nil && bidRequest.User.Geo != nil { + geo = bidRequest.User.Geo + } else if bidRequest.Device != nil && bidRequest.Device.Geo != nil { + geo = bidRequest.Device.Geo + } + if geo != nil { + // If we have a country set, and it is on the list, we assume GDPR applies if not set on the request. + // Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long). + if _, found := e.eeaCountries[strings.ToUpper(geo.Country)]; found { + usersyncIfAmbiguous = false + } else if len(geo.Country) == 3 { + // The country field is formatted properly as a three character country code + usersyncIfAmbiguous = true + } + } // 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, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig) + cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig) e.me.RecordRequestPrivacy(privacyLabels) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 545f04fd0ef..aad448f397f 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -909,6 +909,9 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { LMT: config.LMT{ Enforce: spec.EnforceLMT, }, + GDPR: config.GDPR{ + UsersyncIfAmbiguous: !spec.AssumeGDPRApplies, + }, } ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig) @@ -1026,15 +1029,22 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] } } + var s struct{} + eeac := make(map[string]struct{}) + for _, c := range []string{"FIN", "FRA", "GUF"} { + eeac[c] = s + } + return &exchange{ adapterMap: adapters, me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.BidderList()), cache: &wellBehavedCache{}, cacheTime: 0, - gDPR: gdpr.AlwaysAllow{}, + gDPR: gdpr.AlwaysFail{}, currencyConverter: currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), - UsersyncIfAmbiguous: false, + UsersyncIfAmbiguous: privacyConfig.GDPR.UsersyncIfAmbiguous, privacyConfig: privacyConfig, + eeaCountries: eeac, } } @@ -1882,12 +1892,13 @@ func TestUpdateHbPbCatDur(t *testing.T) { } 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"` + IncomingRequest exchangeRequest `json:"incomingRequest"` + OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` + Response exchangeResponse `json:"response,omitempty"` + EnforceCCPA bool `json:"enforceCcpa"` + EnforceLMT bool `json:"enforceLmt"` + AssumeGDPRApplies bool `json:"assume_gdpr_applies"` + DebugLog *DebugLog `json:"debuglog,omitempty"` } type exchangeRequest struct { diff --git a/exchange/exchangetest/gdpr-geo-eu-off-device.json b/exchange/exchangetest/gdpr-geo-eu-off-device.json new file mode 100644 index 00000000000..fc655de8162 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-off-device.json @@ -0,0 +1,64 @@ +{ + "assume_gdpr_applies": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id" + }, + "device": { + "geo": { + "country": "FRA" + } + } +} + }, + "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 + } + } + }], + "user": { + }, + "device": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-off.json b/exchange/exchangetest/gdpr-geo-eu-off.json new file mode 100644 index 00000000000..27a030f11fc --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-off.json @@ -0,0 +1,60 @@ +{ + "assume_gdpr_applies": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "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 + } + } + }], + "user": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-on.json b/exchange/exchangetest/gdpr-geo-eu-on.json new file mode 100644 index 00000000000..4ec42fc6c70 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-on.json @@ -0,0 +1,60 @@ +{ + "assume_gdpr_applies": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "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 + } + } + }], + "user": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-usa-off.json b/exchange/exchangetest/gdpr-geo-usa-off.json new file mode 100644 index 00000000000..d56c9318a56 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-usa-off.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + } + }, + "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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-usa-on.json b/exchange/exchangetest/gdpr-geo-usa-on.json new file mode 100644 index 00000000000..f922be9ea4e --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-usa-on.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + } + }, + "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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/gdpr/impl.go b/gdpr/impl.go index 2deddc7b2ba..2fbd9c5a07c 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -228,3 +228,22 @@ func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext func (a AlwaysAllow) AMPException() bool { return false } + +// Exporting to allow for easy test setups +type AlwaysFail struct{} + +func (a AlwaysFail) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { + return false, nil +} + +func (a AlwaysFail) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, consent string) (bool, error) { + return false, nil +} + +func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return false, false, false, nil +} + +func (a AlwaysFail) AMPException() bool { + return false +} From f4b0a7cfc95aba6a27e9aaf06a57716fc2057c76 Mon Sep 17 00:00:00 2001 From: guscarreon Date: Thu, 20 Aug 2020 14:19:37 -0400 Subject: [PATCH 174/381] Validate External Cache Host (#1422) * first draft * Little tweaks * Scott's review part 1 * Scott's review corrections part 2 * Scotts refactor * correction in config_test.go * Correction and refactor * Multiple return statements * Test case refactor Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon --- config/config.go | 36 +++++++++++++++++ config/config_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 9e6b1370128..e3b7d8ebda0 100755 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "bytes" + "errors" "fmt" "net/url" "reflect" @@ -111,6 +112,7 @@ func (cfg *Configuration) validate() configErrors { errs = cfg.CurrencyConverter.validate(errs) errs = validateAdapters(cfg.Adapters, errs) errs = cfg.Debug.validate(errs) + errs = cfg.ExtCacheURL.validate(errs) return errs } @@ -128,6 +130,40 @@ func (cfg *AuctionTimeouts) validate(errs configErrors) configErrors { return errs } +func (data *ExternalCache) validate(errs configErrors) configErrors { + if data.Host == "" && data.Path == "" { + // Both host and path can be blank. No further validation needed + return errs + } + + // Either host or path or both not empty, validate. + if data.Host == "" && data.Path != "" || data.Host != "" && data.Path == "" { + return append(errs, errors.New("External cache Host and Path must both be specified")) + } + if strings.HasSuffix(data.Host, "/") { + return append(errs, errors.New(fmt.Sprintf("External cache Host '%s' must not end with a path separator", data.Host))) + } + if strings.ContainsAny(data.Host, "://") { + return append(errs, errors.New(fmt.Sprintf("External cache Host must not specify a protocol. '%s'", data.Host))) + } + if !strings.HasPrefix(data.Path, "/") { + return append(errs, errors.New(fmt.Sprintf("External cache Path '%s' must begin with a path separator", data.Path))) + } + + urlObj, err := url.Parse("https://" + data.Host + data.Path) + if err != nil { + return append(errs, errors.New(fmt.Sprintf("External cache Path validation error: %s ", err.Error()))) + } + if urlObj.Host != data.Host { + return append(errs, errors.New(fmt.Sprintf("External cache Host '%s' is invalid", data.Host))) + } + if urlObj.Path != data.Path { + return append(errs, errors.New("External cache Path is invalid")) + } + + return errs +} + // LimitAuctionTimeout returns the min of requested or cfg.MaxAuctionTimeout. // Both values treat "0" as "infinite". func (cfg *AuctionTimeouts) LimitAuctionTimeout(requested time.Duration) time.Duration { diff --git a/config/config_test.go b/config/config_test.go index 4774d9d6e46..3da3f72137b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -14,6 +14,91 @@ import ( "github.com/stretchr/testify/assert" ) +func TestExternalCacheURLValidate(t *testing.T) { + testCases := []struct { + desc string + data ExternalCache + expErrors int + }{ + { + desc: "With http://", + data: ExternalCache{Host: "http://www.google.com", Path: "/path/v1"}, + expErrors: 1, + }, + { + desc: "Without http://", + data: ExternalCache{Host: "www.google.com", Path: "/path/v1"}, + expErrors: 0, + }, + { + desc: "No scheme but '//' prefix", + data: ExternalCache{Host: "//www.google.com", Path: "/path/v1"}, + expErrors: 1, + }, + { + desc: "// appears twice", + data: ExternalCache{Host: "//www.google.com//", Path: "path/v1"}, + expErrors: 1, + }, + { + desc: "Host has an only // value", + data: ExternalCache{Host: "//", Path: "path/v1"}, + expErrors: 1, + }, + { + desc: "only scheme host, valid path", + data: ExternalCache{Host: "http://", Path: "/path/v1"}, + expErrors: 1, + }, + { + desc: "No host, path only", + data: ExternalCache{Host: "", Path: "path/v1"}, + expErrors: 1, + }, + { + desc: "No host, nor path", + data: ExternalCache{Host: "", Path: ""}, + expErrors: 0, + }, + { + desc: "Invalid http at the end", + data: ExternalCache{Host: "www.google.com", Path: "http://"}, + expErrors: 1, + }, + { + desc: "Host has an unknown scheme", + data: ExternalCache{Host: "unknownscheme://host", Path: "/path/v1"}, + expErrors: 1, + }, + { + desc: "Wrong colon side in scheme", + data: ExternalCache{Host: "http//:www.appnexus.com", Path: "/path/v1"}, + expErrors: 1, + }, + { + desc: "Missing '/' in scheme", + data: ExternalCache{Host: "http:/www.appnexus.com", Path: "/path/v1"}, + expErrors: 1, + }, + { + desc: "host with scheme, no path", + data: ExternalCache{Host: "http://www.appnexus.com", Path: ""}, + expErrors: 1, + }, + { + desc: "scheme, no host nor path", + data: ExternalCache{Host: "http://", Path: ""}, + expErrors: 1, + }, + } + for _, test := range testCases { + var errs configErrors + errs = test.data.validate(errs) + + assert.Equal(t, test.expErrors, len(errs), "Test case threw unexpected number of errors. Desc: %s errMsg = %v \n", test.desc, errs) + } +} + func TestDefaults(t *testing.T) { v := viper.New() SetupViper(v, "") @@ -66,7 +151,7 @@ cache: query: uuid=%PBS_CACHE_UUID% external_cache: host: www.externalprebidcache.net - path: endpoints/cache + path: /endpoints/cache http_client: max_connections_per_host: 10 max_idle_connections: 500 @@ -223,7 +308,7 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "cache.host", cfg.CacheURL.Host, "prebidcache.net") cmpStrings(t, "cache.query", cfg.CacheURL.Query, "uuid=%PBS_CACHE_UUID%") cmpStrings(t, "external_cache.host", cfg.ExtCacheURL.Host, "www.externalprebidcache.net") - cmpStrings(t, "external_cache.path", cfg.ExtCacheURL.Path, "endpoints/cache") + cmpStrings(t, "external_cache.path", cfg.ExtCacheURL.Path, "/endpoints/cache") cmpInts(t, "http_client.max_connections_per_host", cfg.Client.MaxConnsPerHost, 10) cmpInts(t, "http_client.max_idle_connections", cfg.Client.MaxIdleConns, 500) cmpInts(t, "http_client.max_idle_connections_per_host", cfg.Client.MaxIdleConnsPerHost, 20) From 80d557ece7ae79413755db8021e9fd0559b1954c Mon Sep 17 00:00:00 2001 From: hhhjort <31041505+hhhjort@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:36:33 -0400 Subject: [PATCH 175/381] Fixes bug (#1448) * Fixes bug * shortens list --- exchange/exchange.go | 1 + exchange/exchange_test.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/exchange/exchange.go b/exchange/exchange.go index 53f4a7a3e1f..e465a78389b 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -76,6 +76,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.eeaCountries = make(map[string]struct{}, len(cfg.GDPR.EEACountries)) var s struct{} for _, c := range cfg.GDPR.EEACountries { e.eeaCountries[c] = s diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index aad448f397f..a6f69f70c59 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -48,6 +48,9 @@ func TestNewExchange(t *testing.T) { ExpectedTimeMillis: 20, }, Adapters: blankAdapterConfig(openrtb_ext.BidderList()), + GDPR: config.GDPR{ + EEACountries: []string{"FIN", "FRA", "GUF"}, + }, } currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) From d66338035f232aef73099ae1c257628142c62e2c Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Mon, 24 Aug 2020 13:43:02 -0700 Subject: [PATCH 176/381] Added adpod_id to request extension (#1444) * Added adpod_id to request -> ext -> appnexus and modified requests splitting based on pod * Unit test fix * Unit test fix * Minor unit test fixes * Code refactoring * Minor code and unit tests refactoring * Unit tests refactoring Co-authored-by: Veronika Solovei --- adapters/appnexus/appnexus.go | 62 ++++- adapters/appnexus/appnexus_test.go | 229 ++++++++++++++++++ .../video/simple-video.json | 132 ---------- 3 files changed, 283 insertions(+), 140 deletions(-) delete mode 100644 adapters/appnexus/appnexusplatformtest/video/simple-video.json diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index 9bec9bf1e3b..334817ebca7 100644 --- a/adapters/appnexus/appnexus.go +++ b/adapters/appnexus/appnexus.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math/rand" "net/http" "strconv" "strings" @@ -95,10 +96,11 @@ type appnexusBidExt struct { } type appnexusReqExtAppnexus struct { - IncludeBrandCategory *bool `json:"include_brand_category,omitempty"` - BrandCategoryUniqueness *bool `json:"brand_category_uniqueness,omitempty"` - IsAMP int `json:"is_amp,omitempty"` - HeaderBiddingSource int `json:"hb_source,omitempty"` + IncludeBrandCategory *bool `json:"include_brand_category,omitempty"` + BrandCategoryUniqueness *bool `json:"brand_category_uniqueness,omitempty"` + IsAMP int `json:"is_amp,omitempty"` + HeaderBiddingSource int `json:"hb_source,omitempty"` + AdPodId string `json:"adpod_id,omitempty"` } // Full request extension including appnexus extension object @@ -354,14 +356,56 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada } reqExt.Appnexus.IsAMP = isAMP reqExt.Appnexus.HeaderBiddingSource = a.hbSource + isVIDEO + + imps := request.Imp + + // For long form requests adpod_id must be sent downstream. + // Adpod id is a unique identifier for pod + // All impressions in the same pod must have the same pod id in request extension + // For this all impressions in request should belong to the same pod + // If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same + if isVIDEO == 1 { + podImps := groupByPods(imps) + + requests := make([]*adapters.RequestData, 0, len(podImps)) + for _, podImps := range podImps { + reqExt.Appnexus.AdPodId = generatePodId() + + reqs, errors := splitRequests(podImps, request, reqExt, thisURI, errs) + requests = append(requests, reqs...) + errs = append(errs, errors...) + } + return requests, errs + } + + return splitRequests(imps, request, reqExt, thisURI, errs) +} + +func generatePodId() string { + val := rand.Int63() + return fmt.Sprint(val) +} + +func groupByPods(imps []openrtb.Imp) map[string]([]openrtb.Imp) { + // find number of pods in response + podImps := make(map[string][]openrtb.Imp) + for _, imp := range imps { + pod := strings.Split(imp.ID, "_")[0] + podImps[pod] = append(podImps[pod], imp) + } + return podImps +} + +func marshalAndSetRequestExt(request *openrtb.BidRequest, requestExtension appnexusReqExt, errs []error) { var err error - request.Ext, err = json.Marshal(reqExt) + request.Ext, err = json.Marshal(requestExtension) if err != nil { errs = append(errs, err) - return nil, errs } +} + +func splitRequests(imps []openrtb.Imp, request *openrtb.BidRequest, requestExtension appnexusReqExt, uri string, errs []error) ([]*adapters.RequestData, []error) { - imps := request.Imp // Initial capacity for future array of requests, memory optimization. // Let's say there are 35 impressions and limit impressions per request equals to 10. // In this case we need to create 4 requests with 10, 10, 10 and 5 impressions. @@ -375,6 +419,8 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada headers.Add("Content-Type", "application/json;charset=utf-8") headers.Add("Accept", "application/json") + marshalAndSetRequestExt(request, requestExtension, errs) + for impsLeft { endInd := startInd + maxImpsPerReq @@ -393,7 +439,7 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada resArr = append(resArr, &adapters.RequestData{ Method: "POST", - Uri: thisURI, + Uri: uri, Body: reqJSON, Headers: headers, }) diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go index bf49374940a..26380406624 100644 --- a/adapters/appnexus/appnexus_test.go +++ b/adapters/appnexus/appnexus_test.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "encoding/json" + "github.com/stretchr/testify/assert" "io/ioutil" "net/http" "net/http/httptest" + "regexp" "testing" "time" @@ -38,6 +40,233 @@ func TestMemberQueryParam(t *testing.T) { } } +func TestVideoSinglePod(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + result, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, result, 1, "Only one request should be returned") + + var error error + var reqData *openrtb.BidRequest + error = json.Unmarshal(result[0].Body, &reqData) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt *appnexusReqExt + error = json.Unmarshal(reqData.Ext, &reqDataExt) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + regMatch, matchErr := regexp.Match(`[0-9]19`, []byte(reqDataExt.Appnexus.AdPodId)) + assert.NoError(t, matchErr, "Regex match error should be nil") + assert.True(t, regMatch, "AdPod id doesn't present in Appnexus extension or has incorrect format") +} + +func TestVideoSinglePodManyImps(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_3", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_4", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_5", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_6", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_7", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_8", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_9", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_10", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_11", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_12", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_13", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_14", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 2, "Two requests should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId2 := reqDataExt2.Appnexus.AdPodId + + assert.Equal(t, adPodId1, adPodId2, "AdPod id is not the same for the same pod") +} + +func TestVideoTwoPods(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_2", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 2, "Two request should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId2 := reqDataExt2.Appnexus.AdPodId + + assert.NotEqual(t, adPodId1, adPodId2, "AdPod id should be different for different pods") +} + +func TestVideoTwoPodsManyImps(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_2", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_3", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_4", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_5", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_6", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_7", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_8", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_9", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_10", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_11", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_12", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_13", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_14", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 3, "Three requests should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + var reqData3 *openrtb.BidRequest + error = json.Unmarshal(res[2].Body, &reqData3) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt3 *appnexusReqExt + error = json.Unmarshal(reqData3.Ext, &reqDataExt3) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + adPodId2 := reqDataExt2.Appnexus.AdPodId + adPodId3 := reqDataExt3.Appnexus.AdPodId + + podIds := make(map[string]int) + podIds[adPodId1] = podIds[adPodId1] + 1 + podIds[adPodId2] = podIds[adPodId2] + 1 + podIds[adPodId3] = podIds[adPodId3] + 1 + + assert.Len(t, podIds, 2, "Incorrect number of unique pod ids") +} + // ---------------------------------------------------------------------------- // Code below this line tests the legacy, non-openrtb code flow. It can be deleted after we // clean up the existing code and make everything openrtb. diff --git a/adapters/appnexus/appnexusplatformtest/video/simple-video.json b/adapters/appnexus/appnexusplatformtest/video/simple-video.json deleted file mode 100644 index 7ee192be2c1..00000000000 --- a/adapters/appnexus/appnexusplatformtest/video/simple-video.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "minduration": 15, - "maxduration": 30, - "protocols": [2, 3, 5, 6, 7, 8], - "w": 940, - "h": 560 - }, - "ext": { - "bidder": { - "placement_id": 1 - } - } - } - ] - }, - - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://ib.adnxs.com/openrtb2", - "body": { - "id": "test-request-id", - "ext": { - "appnexus": { - "hb_source": 9 - }, - "prebid": {} - }, - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "minduration": 15, - "maxduration": 30, - "protocols": [2, 3, 5, 6, 7, 8], - "w": 940, - "h": 560 - }, - "ext": { - "appnexus": { - "placement_id": 1 - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [{ - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["appnexus.com"], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "h": 250, - "w": 300, - "cat": ["IAB9-1"], - "ext": { - "appnexus": { - "brand_id": 9, - "brand_category_id": 9, - "auction_id": 8189378542222915032, - "bid_ad_type": 1, - "bidder_id": 2, - "ranking_price": 0.000000, - "deal_priority": 5 - } - } - }] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - } - ], - - "expectedBidResponses": [ - { - "currency": "USD", - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["appnexus.com"], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250, - "cat": ["IAB5-3"], - "ext": { - "appnexus": { - "brand_id": 9, - "brand_category_id": 9, - "auction_id": 8189378542222915032, - "bid_ad_type": 1, - "bidder_id": 2, - "ranking_price": 0.000000, - "deal_priority": 5 - } - } - }, - "type": "video" - } - ] - } - ] - } \ No newline at end of file From 30ef8581806f5957c4417f05cd305e709d53a92e Mon Sep 17 00:00:00 2001 From: Jurij Sinickij Date: Mon, 24 Aug 2020 23:49:06 +0300 Subject: [PATCH 177/381] Adform adapter: additional targeting params added (#1424) --- adapters/adform/adform.go | 21 +++++++++++++++++++++ adapters/adform/adform_test.go | 30 ++++++++++++++++++++++-------- adapters/adform/params_test.go | 8 ++++++++ openrtb_ext/imp_adform.go | 11 +++++++---- static/bidder-params/adform.json | 14 ++++++++++++++ 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/adapters/adform/adform.go b/adapters/adform/adform.go index 69f1c12f073..5881f4ab86e 100644 --- a/adapters/adform/adform.go +++ b/adapters/adform/adform.go @@ -43,6 +43,7 @@ type adformRequest struct { digitrust *adformDigitrust currency string eids string + url string } type adformDigitrust struct { @@ -61,6 +62,9 @@ type adformAdUnit struct { PriceType string `json:"priceType,omitempty"` KeyValues string `json:"mkv,omitempty"` KeyWords string `json:"mkw,omitempty"` + CDims string `json:"cdims,omitempty"` + MinPrice float64 `json:"minp,omitempty"` + Url string `json:"url,omitempty"` bidId string adUnitCode string @@ -284,6 +288,10 @@ func (r *adformRequest) buildAdformUrl(a *AdformAdapter) string { parameters.Add("eids", r.eids) } + if r.url != "" { + parameters.Add("url", r.url) + } + URL := *a.URL URL.RawQuery = parameters.Encode() @@ -302,6 +310,12 @@ func (r *adformRequest) buildAdformUrl(a *AdformAdapter) string { if adUnit.KeyWords != "" { buffer.WriteString(fmt.Sprintf("&mkw=%s", adUnit.KeyWords)) } + if adUnit.CDims != "" { + buffer.WriteString(fmt.Sprintf("&cdims=%s", adUnit.CDims)) + } + if adUnit.MinPrice > 0 { + buffer.WriteString(fmt.Sprintf("&minp=%.2f", adUnit.MinPrice)) + } adUnitsParams = append(adUnitsParams, base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(buffer.Bytes())) } @@ -407,6 +421,8 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro adUnits := make([]*adformAdUnit, 0, len(request.Imp)) errors := make([]error, 0, len(request.Imp)) secure := false + url := "" + for _, imp := range request.Imp { params, _, _, err := jsonparser.Get(imp.Ext, "bidder") if err != nil { @@ -441,6 +457,10 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro secure = true } + if url == "" { + url = adformAdUnit.Url + } + adformAdUnit.bidId = imp.ID adformAdUnit.adUnitCode = imp.ID adUnits = append(adUnits, &adformAdUnit) @@ -520,6 +540,7 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro digitrust: digitrust, currency: requestCurrency, eids: eids, + url: url, }, errors } diff --git a/adapters/adform/adform_test.go b/adapters/adform/adform_test.go index 2fca7d1722d..f227776207d 100644 --- a/adapters/adform/adform_test.go +++ b/adapters/adform/adform_test.go @@ -35,6 +35,9 @@ type aTagInfo struct { keyValues string keyWords string code string + cdims string + url string + minp float64 price float64 content string @@ -320,9 +323,9 @@ func createTestData(secure bool) aBidInfo { tid: "transaction-id", buyerUID: "user-id", tags: []aTagInfo{ - {mid: 32344, keyValues: "color:red,age:30-40", keyWords: "red,blue", priceType: "gross", code: "code1", price: 1.23, content: "banner-content1", dealId: "dealId1", creativeId: "creativeId1"}, - {mid: 32345, priceType: "net", code: "code2"}, // no bid for ad unit - {mid: 32346, code: "code3", price: 1.24, content: "banner-content2", dealId: "dealId2"}, + {mid: 32344, keyValues: "color:red,age:30-40", keyWords: "red,blue", cdims: "300x300,400x200", priceType: "gross", code: "code1", price: 1.23, content: "banner-content1", dealId: "dealId1", creativeId: "creativeId1"}, + {mid: 32345, priceType: "net", code: "code2", minp: 23.1, cdims: "300x200"}, // no bid for ad unit + {mid: 32346, code: "code3", price: 1.24, content: "banner-content2", dealId: "dealId2", url: "https://adform.com?a=b"}, }, secure: secure, currency: "EUR", @@ -519,11 +522,22 @@ func getUserExt() []byte { } func formatAdUnitJson(tag aTagInfo) string { - return fmt.Sprintf("{ \"mid\": %d%s%s%s}", + return fmt.Sprintf("{ \"mid\": %d%s%s%s%s%s%s}", tag.mid, formatAdUnitParam("priceType", tag.priceType), formatAdUnitParam("mkv", tag.keyValues), - formatAdUnitParam("mkw", tag.keyWords)) + formatAdUnitParam("mkw", tag.keyWords), + formatAdUnitParam("cdims", tag.cdims), + formatAdUnitParam("url", tag.url), + formatDemicalAdUnitParam("minp", tag.minp)) +} + +func formatDemicalAdUnitParam(fieldName string, fieldValue float64) string { + if fieldValue > 0 { + return fmt.Sprintf(", \"%s\": %.2f", fieldName, fieldValue) + } + + return "" } func formatAdUnitParam(fieldName string, fieldValue string) string { @@ -547,10 +561,10 @@ func assertAdformServerRequest(testData aBidInfo, r *http.Request, isOpenRtb boo var midsWithCurrency = "" var queryString = "" if isOpenRtb { - midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9RVVSJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZQ&bWlkPTMyMzQ1JnJjdXI9RVVS&bWlkPTMyMzQ2JnJjdXI9RVVS" - queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&eids=eyJ0ZXN0LmNvbSI6eyJvdGhlcl91c2VyX2lkIjpbMF0sInNvbWVfdXNlcl9pZCI6WzFdfSwidGVzdDIub3JnIjp7Im90aGVyX3VzZXJfaWQiOlsyXX19&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&" + midsWithCurrency + midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9RVVSJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZSZjZGltcz0zMDB4MzAwLDQwMHgyMDA&bWlkPTMyMzQ1JnJjdXI9RVVSJmNkaW1zPTMwMHgyMDAmbWlucD0yMy4xMA&bWlkPTMyMzQ2JnJjdXI9RVVS" + queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&eids=eyJ0ZXN0LmNvbSI6eyJvdGhlcl91c2VyX2lkIjpbMF0sInNvbWVfdXNlcl9pZCI6WzFdfSwidGVzdDIub3JnIjp7Im90aGVyX3VzZXJfaWQiOlsyXX19&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&url=https%3A%2F%2Fadform.com%3Fa%3Db&" + midsWithCurrency } else { - midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZQ&bWlkPTMyMzQ1JnJjdXI9VVNE&bWlkPTMyMzQ2JnJjdXI9VVNE" // no way to pass currency in legacy adapter + midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZSZjZGltcz0zMDB4MzAwLDQwMHgyMDA&bWlkPTMyMzQ1JnJjdXI9VVNEJmNkaW1zPTMwMHgyMDAmbWlucD0yMy4xMA&bWlkPTMyMzQ2JnJjdXI9VVNE" // no way to pass currency in legacy adapter queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&" + midsWithCurrency } diff --git a/adapters/adform/params_test.go b/adapters/adform/params_test.go index ae0a02b6a97..b392463f426 100644 --- a/adapters/adform/params_test.go +++ b/adapters/adform/params_test.go @@ -48,6 +48,10 @@ var validParams = []string{ `{"mid":"123","mkv":"color:"}`, `{"mid":"123","mkw":"green,male"}`, `{"mid":"123","mkv":" ","mkw":" "}`, + `{"mid":"123","cdims":"500x300,400x200","mkw":" "}`, + `{"mid":"123","cdims":"500x300","mkv":" ","mkw":" "}`, + `{"mid":"123","minp":2.1}`, + `{"mid":"123","url":"https://adform.com/page"}`, } var invalidParams = []string{ @@ -66,4 +70,8 @@ var invalidParams = []string{ `{"mid":"123","mkv":"color:blue,l&ngth:350"}`, `{"mid":"123","mkv":"color::blue"}`, `{"mid":"123","mkw":"fem&le"}`, + `{"mid":"123","minp":"2.1"}`, + `{"mid":"123","cdims":"500x300:400:200","mkw":" "}`, + `{"mid":"123","cdims":"500x300,400:200","mkv":" ","mkw":" "}`, + `{"mid":"123","url":10}`, } diff --git a/openrtb_ext/imp_adform.go b/openrtb_ext/imp_adform.go index 3e7c1a7261e..3206ece7c9b 100644 --- a/openrtb_ext/imp_adform.go +++ b/openrtb_ext/imp_adform.go @@ -1,8 +1,11 @@ package openrtb_ext type ExtImpAdform struct { - MasterTagId string `json:"mid"` - PriceType string `json:"priceType,omitempty"` - KeyValues string `json:"mkv,omitempty"` - KeyWords string `json:"mkw,omitempty"` + MasterTagId string `json:"mid"` + PriceType string `json:"priceType,omitempty"` + KeyValues string `json:"mkv,omitempty"` + KeyWords string `json:"mkw,omitempty"` + CDims string `json:"cdims,omitempty"` + MinPrice float64 `json:"minp,omitempty"` + Url string `json:"url,omitempty"` } diff --git a/static/bidder-params/adform.json b/static/bidder-params/adform.json index 67f09623ee4..f0b8c7a6be0 100644 --- a/static/bidder-params/adform.json +++ b/static/bidder-params/adform.json @@ -22,6 +22,20 @@ "type": "string", "description": "Comma-separated keywords. Forbidden symbols: &.", "pattern": "^[^&]*$" + }, + "cdims": { + "type": "string", + "description": "Comma-separated creative dimentions.", + "pattern": "(^\\d+x\\d+)(,\\d+x\\d+)*$" + }, + "minp": { + "type": "number", + "description": "The minimum CPM price.", + "minimum": 0 + }, + "url": { + "type": "string", + "description": "Custom URL for targeting." } }, "required": ["mid"] From 9dbd0083704315ac80721da30823753ee146bf19 Mon Sep 17 00:00:00 2001 From: Rob Hazan Date: Mon, 24 Aug 2020 17:28:42 -0400 Subject: [PATCH 178/381] Fix minor error message spelling mistake "vastml" -> "vastxml" (#1455) --- .../openrtb2/sample-requests/invalid-whole/cache-nothing.json | 2 +- openrtb_ext/request.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json index d4b875498ae..f256e4eb34c 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json @@ -1,5 +1,5 @@ { - "message": "Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the \"bids\" or \"vastml\" properties\n", + "message": "Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the \"bids\" or \"vastxml\" properties\n", "requestPayload": { "id": "some-request-id", "site": { diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 23daaf0f76e..d6edf47f939 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -67,7 +67,7 @@ func (ert *ExtRequestPrebidCache) UnmarshalJSON(b []byte) error { } if proxy.Bids == nil && proxy.VastXML == nil { - return errors.New(`request.ext.prebid.cache requires one of the "bids" or "vastml" properties`) + return errors.New(`request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`) } *ert = ExtRequestPrebidCache(proxy) From 055ab8062c29038a3ac451e119f07d002ff57498 Mon Sep 17 00:00:00 2001 From: Cameron Rice <37162584+camrice@users.noreply.github.com> Date: Tue, 25 Aug 2020 08:37:04 -0700 Subject: [PATCH 179/381] Fixing comment for usage of deal priority field (#1451) --- adapters/bidder.go | 2 +- exchange/bidder.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/bidder.go b/adapters/bidder.go index 627caf67344..41218aa6a2f 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -108,7 +108,7 @@ func NewBidderResponse() *BidderResponse { // TypedBid.Bid.Ext will become "response.seatbid[i].bid.ext.bidder" in the final OpenRTB response. // TypedBid.BidType will become "response.seatbid[i].bid.ext.prebid.type" in the final OpenRTB response. // TypedBid.BidVideo will become "response.seatbid[i].bid.ext.prebid.video" in the final OpenRTB response. -// TypedBid.DealPriority will become "response.seatbid[i].bid.dealPriority" in the final OpenRTB response. +// TypedBid.DealPriority is optionally provided by adapters and used internally by the exchange to support deal targeted campaigns. type TypedBid struct { Bid *openrtb.Bid BidType openrtb_ext.BidType diff --git a/exchange/bidder.go b/exchange/bidder.go index decad8ccf2f..5924e39b031 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -57,7 +57,7 @@ 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. +// pbsOrtbBid.dealPriority is optionally provided by adapters and used internally by the exchange to support deal targeted campaigns. type pbsOrtbBid struct { bid *openrtb.Bid bidType openrtb_ext.BidType From e96b980d391ae89ba1a9631f5921a7db6aa24ba8 Mon Sep 17 00:00:00 2001 From: bretg Date: Tue, 25 Aug 2020 12:43:06 -0400 Subject: [PATCH 180/381] moving docs to website repo (#1443) --- README.md | 14 +- docs/bidders/adtarget.md | 5 - docs/bidders/appnexus.md | 45 - docs/bidders/audienceNetwork.md | 8 - docs/bidders/avocet.md | 5 - docs/bidders/beachfront.md | 13 - docs/bidders/emx_digital.md | 10 - docs/bidders/kidoz.md | 9 - docs/bidders/openx.md | 65 -- docs/bidders/pubmatic.md | 33 - docs/bidders/pubnative.md | 62 -- docs/bidders/rubicon.md | 7 - docs/bidders/smaato.md | 42 - docs/bidders/smartAdserver.md | 59 -- docs/bidders/smartrtb.md | 39 - docs/bidders/sovrn.md | 3 - docs/bidders/tappx.md | 13 - ...Server Event Notifications - Tech Spec.pdf | Bin 89983 -> 0 bytes docs/developers/add-new-analytics-module.md | 33 - docs/developers/add-new-bidder.md | 117 --- docs/developers/cookie-syncs.md | 30 - docs/developers/currency-converter.md | 56 -- docs/developers/default-request.md | 44 - docs/developers/features.md | 12 + docs/developers/gdpr.md | 31 - docs/developers/stored-requests.md | 4 +- docs/endpoints.md | 1 + docs/endpoints/bidders/params.md | 24 - docs/endpoints/cookieSync.md | 55 -- docs/endpoints/currency_rates.md | 111 --- docs/endpoints/info/bidders.md | 23 - docs/endpoints/info/bidders/bidderName.md | 43 - docs/endpoints/openrtb2/amp.md | 127 --- docs/endpoints/openrtb2/auction.md | 789 ------------------ docs/endpoints/setuid.md | 26 - docs/endpoints/status.md | 9 - 36 files changed, 22 insertions(+), 1945 deletions(-) delete mode 100644 docs/bidders/adtarget.md delete mode 100644 docs/bidders/appnexus.md delete mode 100644 docs/bidders/audienceNetwork.md delete mode 100644 docs/bidders/avocet.md delete mode 100644 docs/bidders/beachfront.md delete mode 100644 docs/bidders/emx_digital.md delete mode 100644 docs/bidders/kidoz.md delete mode 100644 docs/bidders/openx.md delete mode 100644 docs/bidders/pubmatic.md delete mode 100644 docs/bidders/pubnative.md delete mode 100644 docs/bidders/rubicon.md delete mode 100644 docs/bidders/smaato.md delete mode 100644 docs/bidders/smartAdserver.md delete mode 100644 docs/bidders/smartrtb.md delete mode 100644 docs/bidders/sovrn.md delete mode 100644 docs/bidders/tappx.md delete mode 100644 docs/developers/Prebid Server Event Notifications - Tech Spec.pdf delete mode 100644 docs/developers/add-new-analytics-module.md delete mode 100644 docs/developers/add-new-bidder.md delete mode 100644 docs/developers/cookie-syncs.md delete mode 100644 docs/developers/currency-converter.md delete mode 100644 docs/developers/default-request.md create mode 100644 docs/developers/features.md delete mode 100644 docs/developers/gdpr.md create mode 100644 docs/endpoints.md delete mode 100644 docs/endpoints/bidders/params.md delete mode 100644 docs/endpoints/cookieSync.md delete mode 100644 docs/endpoints/currency_rates.md delete mode 100644 docs/endpoints/info/bidders.md delete mode 100644 docs/endpoints/info/bidders/bidderName.md delete mode 100644 docs/endpoints/openrtb2/amp.md delete mode 100644 docs/endpoints/openrtb2/auction.md delete mode 100644 docs/endpoints/setuid.md delete mode 100644 docs/endpoints/status.md diff --git a/README.md b/README.md index b69e7e76db4..673c2b1bdeb 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ It is managed by [Prebid.org](http://prebid.org/overview/what-is-prebid-org.html and upholds the principles from the [Prebid Code of Conduct](http://prebid.org/wrapper_code_of_conduct.html). This project does not support the same set of Bidders as Prebid.js, although there is overlap. -The current set can be found in the [adapters](./adapters) package. If you don't see the one you want, feel free to [contribute it](docs/developers/add-new-bidder.md). +The current set can be found in the [adapters](./adapters) package. If you don't see the one you want, feel free to [contribute it](https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html). For more information, see: -- [What is Prebid?](http://prebid.org/overview/intro.html) -- [Getting started with Prebid Server](http://prebid.org/dev-docs/get-started-with-prebid-server.html) -- [Current Bidders](http://prebid.org/dev-docs/prebid-server-bidders.html) +- [What is Prebid?](https://prebid.org/overview/intro.html) +- [Prebid Server Overview](https://docs.prebid.org/prebid-server/overview/prebid-server-overview.html) +- [Current Bidders](http://prebid.org/dev-docs/pbs-bidders.html) ## Installation @@ -45,14 +45,12 @@ go build . ``` Load the landing page in your browser at `http://localhost:8000/`. -For the full API reference, see [docs/endpoints](docs/endpoints) +For the full API reference, see [the endpoint documentation](https://docs.prebid.org/prebid-server/endpoints/pbs-endpoint-overview.html) ## Contributing -Want to [add an adapter](docs/developers/add-new-bidder.md)? Found a bug? Great! -This project is in its infancy, and many things can be improved. - +Want to [add an adapter](https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html)? Found a bug? Great! Report bugs, request features, and suggest improvements [on Github](https://github.com/prebid/prebid-server/issues). diff --git a/docs/bidders/adtarget.md b/docs/bidders/adtarget.md deleted file mode 100644 index b658a728a2b..00000000000 --- a/docs/bidders/adtarget.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index e4032313f25..00000000000 --- a/docs/bidders/appnexus.md +++ /dev/null @@ -1,45 +0,0 @@ -# Appnexus Bidder - -## Using Keywords - -The `keywords` [bidder param](../../static/bidder-params/appnexus.json) will only work if -it's enabled for your Account with Appnexus. - -**This permission is _distinct_ from the keywords feature used by Prebid.js.** - -If you want to enable Appnexus keywords, contact your account manager. - -## Display Manager Version - -The AppNexus endpoint expects `imp.displaymanagerver` to be populated for mobile app sources -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 concatenate the two `app` fields as `-` fo fill in -the empty `displaymanagerver` before sending the request to AppNexus. - -## Test Request - -The following test parameters can be used to verify that Prebid Server is working properly with the -Appnexus adapter. This example includes an `imp` object with an Appnexus test placement ID and sizes -that would match with the test creative. - -``` - "imp": [{ - "id": "some-impression-id", - "banner": { - "format": [{ - "w": 600, - "h": 500 - }, { - "w": 300, - "h": 600 - }] - }, - "ext": { - "appnexus": { - "placementId": 13144370 - } - } - }] -``` \ No newline at end of file diff --git a/docs/bidders/audienceNetwork.md b/docs/bidders/audienceNetwork.md deleted file mode 100644 index d55e8218a81..00000000000 --- a/docs/bidders/audienceNetwork.md +++ /dev/null @@ -1,8 +0,0 @@ -# Audience Network Bidder - -## Mobile Bids - -Audience Network will not bid on requests made from device simulators. -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 deleted file mode 100644 index 6aa67391af4..00000000000 --- a/docs/bidders/avocet.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index ecd7a8f95d1..00000000000 --- a/docs/bidders/beachfront.md +++ /dev/null @@ -1,13 +0,0 @@ -# Beachfront bidder - -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/mxmCherry/openrtb/blob/master/openrtb2/bid.go). - diff --git a/docs/bidders/emx_digital.md b/docs/bidders/emx_digital.md deleted file mode 100644 index 0ba81d59fea..00000000000 --- a/docs/bidders/emx_digital.md +++ /dev/null @@ -1,10 +0,0 @@ -# EMX Digital Bidder - -[EMX Digital](https://emxdigital.com/) supports the following parameters to be present in the `ext` object of impression requests: - -- "tagid" type string - Required. Unique inventory ID. -- "bidfloor" type string - Optional. The minimum acceptable bid for the unit, in CPM and USD. - -To use this bidder you will need an account and a valid tagid from our exchange. - -For further information, please contact your Account Manager or adops@emxdigital.com. diff --git a/docs/bidders/kidoz.md b/docs/bidders/kidoz.md deleted file mode 100644 index 433dd71c2ca..00000000000 --- a/docs/bidders/kidoz.md +++ /dev/null @@ -1,9 +0,0 @@ -# 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 deleted file mode 100644 index 54a0a5b1e72..00000000000 --- a/docs/bidders/openx.md +++ /dev/null @@ -1,65 +0,0 @@ -# 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" | -| platform | uuid | required\* | The platform id for the customer | "a3aece0c-9e80-4316-8deb-faf804779bd1" | -| 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"]} | - -\* At least one of `delDomain` or `platform` parameters is required. - -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" - }, - } -} -``` diff --git a/docs/bidders/pubmatic.md b/docs/bidders/pubmatic.md deleted file mode 100644 index 610108b2e07..00000000000 --- a/docs/bidders/pubmatic.md +++ /dev/null @@ -1,33 +0,0 @@ -# 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 deleted file mode 100644 index a25cafe0cd5..00000000000 --- a/docs/bidders/pubnative.md +++ /dev/null @@ -1,62 +0,0 @@ -# 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 deleted file mode 100644 index ea376da427d..00000000000 --- a/docs/bidders/rubicon.md +++ /dev/null @@ -1,7 +0,0 @@ -# Rubicon Bidder - -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/prebid/Prebid.js/blob/master/modules/rubiconBidAdapter.md) will work for server as well. diff --git a/docs/bidders/smaato.md b/docs/bidders/smaato.md deleted file mode 100644 index 881f8f2ab54..00000000000 --- a/docs/bidders/smaato.md +++ /dev/null @@ -1,42 +0,0 @@ - -# Smaato Bidder - -``` -Module Name: Smaato Bidder Adapter -Module Type: Bidder Adapter -Maintainer: prebid@smaato.com -``` - -### Description - -Please contact Smaato Support or prebid@smaato.com to get set up with a publisherId and adspaceId. - -### Test Parameters: - -Following example includes sample `imp` object with publisherId and adSlot which can be used to test Smaato Adapter - -``` -"imp":[ - { - "id":“1C86242D-9535-47D6-9576-7B1FE87F282C, - "banner":{ - "format":[ - { - "w":300, - "h":50 - }, - { - "w":300, - "h":250 - } - ] - }, - "ext":{ - "smaato":{ - "publisherId":"100042525", - "adspaceId":"130563103" - } - } - } - ] -``` diff --git a/docs/bidders/smartAdserver.md b/docs/bidders/smartAdserver.md deleted file mode 100644 index 4d2663f8a3b..00000000000 --- a/docs/bidders/smartAdserver.md +++ /dev/null @@ -1,59 +0,0 @@ -# Smart Adserver Bidder - -## Parameters -The `ext.smartadserver` object of impression bid requests supports the following parameters : -- "networkId" - Required. The network identifier you have been provided with. -- "siteId" - Optional. The site identifier from your campaign configuration. -- "pageId" - Optional. The page identifier from your campaign configuration. -- "formatId" - Optional. The format identifier from your campaign configuration. - -The network identifier is provided by your Account Manager. -**Note:** The site, page and format identifiers have to all be provided or all empty. - -## Examples - -Without site/page/format : -``` - "imp": [{ - "id": "some-impression-id", - "banner": { - "format": [{ - "w": 600, - "h": 500 - }, { - "w": 300, - "h": 600 - }] - }, - "ext": { - "smartadserver": { - "networkId": 73 - } - } - }] -``` - -With site/page/format : - -``` - "imp": [{ - "id": "some-impression-id", - "banner": { - "format": [{ - "w": 600, - "h": 500 - }, { - "w": 300, - "h": 600 - }] - }, - "ext": { - "smartadserver": { - "networkId": 73 - "siteId": 1, - "pageId": 2, - "formatId": 3 - } - } - }] -``` \ No newline at end of file diff --git a/docs/bidders/smartrtb.md b/docs/bidders/smartrtb.md deleted file mode 100644 index ffa88f663e8..00000000000 --- a/docs/bidders/smartrtb.md +++ /dev/null @@ -1,39 +0,0 @@ -# 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 deleted file mode 100644 index bc6d42333e8..00000000000 --- a/docs/bidders/sovrn.md +++ /dev/null @@ -1,3 +0,0 @@ -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 minimum acceptable bid, in CPM, using US Dollars. This is an optional field. \ No newline at end of file diff --git a/docs/bidders/tappx.md b/docs/bidders/tappx.md deleted file mode 100644 index f92e1cd4fe8..00000000000 --- a/docs/bidders/tappx.md +++ /dev/null @@ -1,13 +0,0 @@ -# Tappx Bidder - -## Parameters -Please contact [Tappx](../../static/bidder-info/tappx.yaml) to get set up. Our operations team will provide the 3 required parameters: -- Tappx Key -- Tappx Publisher Endpoint -- Tappx Host URL - -**Note:** The Tappx prebid bidder only supports in app traffic at the moment - -As for test parameters, use the official test parameter specified in the oRTB standard (https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/OpenRTB%20v3.0%20FINAL.md#object_request) - -For any additional information contact tappx@tappx.com diff --git a/docs/developers/Prebid Server Event Notifications - Tech Spec.pdf b/docs/developers/Prebid Server Event Notifications - Tech Spec.pdf deleted file mode 100644 index c0bcca753fdf8810619f620f2e24dff43fe346e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89983 zcmd41WpHIXkS1s-Gc()QY?qmt*=1&CyUfhY%*@QpP-SLjW@cvg`1R|V>5ZP){kOX} z6w;LZI~ofc8`>Cs zOa0r*>Dya50RC};kd392m9+za1wbcfZ){{{=xAdPU|{?<4M3;*?Foi&dl~-mgM_t_ zvFrB$D}b5p-@b_rfRW*Q8wvnA1sfYj0QK8X6es zZU5FY(9>gwB1+Tjv4lVzX+@P<^b^k}05D!)h8m@V5`BR7)Gq)O>^W1b;7PEE5qwwh zV5K6z5G;J94fSxtNq`^-VG{p|>wl{K|0u%$&1e7r8-V^N;{TIP!p07c4ghB6e?cIp z|J(Sx$MkRSUw{hfJL+57{QfVj;rK7}|Bq-@1h9Ss=|3^e&i;Q5ww~TUkt?jH^Swx%%AQ~JIATdm}x`Zj>) zwlsj^2_8K7K{S#k9S#;2h~*TC>%Ry7oBsdXN&#zYn{P8U0gV3;PyxXBFX$Qnu|xsD z^pEIw^Diry82|14>jb8MJ;TKE@8Ca)=v(q%ElmGvVft4K^S@e{|JB0$ua<8H3fWjY z8e4y_{+);ZnM^1B%~yRvn}1mO&lJ{g+Olyl{zJ}het%2;FaIkTJJ>kc8-7RRAIgik zI*KVee&>Y$=!r1_82?@3AIgh- z9CjZVc?~g?aPmz^vO{v?^)vZX$0Rgnic#U0^3R3pc;TUwB{l({R={7M_qPK+y1D`A z6}I=Av9YbM`45?@@wCsi6Ps5h$D5@s@5^AH_nWmb1ip&Pmaj)9SADLYs3^tv*IW^( zvykMj*&5V8db7A<-CFc+wIAm_x4+2=O5EREUv{)zB$+J!#^~zy*W6I%gI-0dgFI-0%wfH* zXq$E!{3S6o{-rZN9vZVxYJ|#mpWuX&(K;My_xt!slSHxb;<=%C>k4UsB5uAU-e!-D z@N2Pk=Qr_h`>jsyKG2b>2VU)cB#k3}u!}mV`y4u8@{3Ptf=1Vd#&Dn7>XpZpr`wVHGlj(f>$f-AKb8K0Cf1RM z&=^A6v(L^Q*RpNOJ=yeG*M+wSw;z) zfDGa!NjJaFLXBI3#+&E8o`HMghFiP4X&2L#*m--y{e#E5_~)(I$u#QsSXUx$9 z1$dz_N|?6}T}|P1koi(c_;!jrZ`I|B84W_n*J=YpfZ&wJIiAVhE6uW0k+y6 z=R~T2M26=@F3pM5yDEH5JJKJI?i)QMnL^Pwj-vU?dUt;;-Yl5{^_$`cbfW}R2Zr|G z+m$I-h|j!v^=;)u2-$L2kdfJxY>Tx)GL!g|B(6Ak^KkGysv|;h-Q_5HtdeUxyG6@eS@=;W>?LrWfP^oSLLW27SvR*G5MP}Q2`YC6cbQEP}!vE>_@P3h-vMEMW9~x zdt$6ckSeo@@D}}TT$RdN?^>@B$b%HFGG>8ZOkjf-K~k2Y4po!!N8kxcstvvB|%3^l78=Yp=4s)R(s3=vv9tv*BL2gzlb#GvDEy&KzE zGM$I>-fFHyP&@~xhd)B%(|Jg+DCuI5^E>I{G-&b?_Bi-*?n}k!N3ErQp!%ELM*02W zGjaG|;83Ucx_MAQ2+n!0$0;Re4N7?evWk~rHJmNTYEJ=JK_eWdQIC)ZkTnv6B!~bP z{y+IF+bHz&!1_m)4G%S};$=%C9k=1mQ0LCkw7W4T!Tz4HRdi8Xc}hy8NAmVWpd0Do zf$EC6FsoR#KSMH&V_<=_i#Y3}P2$AnO{J&}B0qB)R=M1^#pj+koB^6kkF6x(@d><}1T?HJ-Z4>z@nr6*-pXTLTGSG3WR$FvY0lYIk74vUx zxKqOIS&%W!$NGG|_Fy?FPE!^K5gtzS*sx}Jw<^jV+*Z<}q;3^@a?`!T9m^hCa(dM2 zByQZH#UK@R6QrjZR^Z$Sg_kI}$*v}4*8GEt)m3`wb|J<(?w-=RQAJlQObwTh%>$2q z(Eg@!GtKozg=M%MX{J02RVH_-`;xaZiPyoRB&C{21TWk=Jh%eC>w#S7^E{~k`uTeBjK|UqVC_}= zQUJ}sDQL4m_;-M*!>yJ|V-swxj&NBM5eV9nKUe+vu|m_a**z960JymWP2D05=EH(J zRR?7Vo}Ig={x6|>%})hf{;gWE4^i{iuKv2nYxJPe2Hk75shT2(t)tjKt}BnCtRm z6;M);K~mDDv0OJEF7A(Wm2o7u6^^{UHH2?;@TaY}rZ29s=>T&rrM=**mvd4hW-f_( zO@em=ed%j}+Gr}C5AzNx=R(PR%m%+T`JglnDmK8eij04Yt36ECL2ho4wKGeGmDd?u zn+fFInAbuQ^@P@()?&bAeE#FmfCAeJ@X&&zZGiO5HnZ9$v3*$I?tzjy54em=Pj>zn zm|y)6Bav;?Ts_bhReu`{)z}*Z&O32$WF*Hf{cW-R`FO+gaS=7sUu`$~$#c$6M;{SoScQ)XSrY#4Ze2?sB?uf0(<{9fN_)~= zQtlYA^s63K<;*2lgXxjLrDEVYBdpT$b0Oy!vcIEIv&_mIrGFSJvbuL<&@8R=K1QZ7 z=zkbdiH+h(RF&ByDx!hl|Ef~vuJG@iDu>%~@RdtqpXhOOj#7kEoG4YPX|u0;Ne3;4 zxLunG!ZW>$`v7*fBpJ+7&^NujQ4SDcy{rU0MdtuL#A6Qtik~n`(t!pV69m@EIXg}W zW9%nOnFV|CXP^(JBWi!6D7t1V+{rvbyH_&>)uc4>I!u46V0d1@c0>UElD_WPc)b0P z^{HtsDkmU>A!rP7p)5b{=>zd-9ADy7K-vn_!h9@lF&4+j9Y4kP-;UegXPN(f>iWNV?`Hrov$B7m4FBgLk&%Vt``+fiUJ9IdxOgflKhd>6 z^1ex3rNv8;DKe%(QN~I_3o|DAGM~f({H8M?2~kB70Sj_gkf92~MUth#zW&1n{zR;D zz`$szFueuC1te*1OT0`1+h$!JTOMC+1SYpU7E29J>K;vAlM*05fac|(KnfA?*j#&u zwY5`#)H;DeHMku|NuKJVa-@Jwk>CKo2pIJDTg$?5d?3WJ!2l{Tp4&S}djwtY0RF2S zU`~=4Y+l#pnfJ3_H<52h90j%A%#*E9b4!R&1%uM=$H#xdfq+Ip>1*8d-luzv^u|xNZ5^UDvN$hKE6Xl~ZFDWK!8Zdu27px`4&|+RK5T*H4D{ z_!ysh!v$Xbx6taKw{x7f$jBW2yk+BEZ7eU+&rS=@fHg>3$`*pVCbmFtLRi7s8fwyy zO%o}2-x0I!kQ$(HqxQ-n`uPLT=E4EZZMTBRsTaabbcLC}a=;p)=?OR{4)rM2(V-@| zP3KtqpWq7O{#Zs&4Ygv^?_`la1 z`F=uUL}I)JW81bH@BXC+xgjcEsISlR!#Qt|YKBTSeEC^P5N=FI zL^JrVA%)QGeG=EG&vOuGl(@~oMR>T2xe?ba<~YbI zFTMsijC0ZFbz^IUKa1<7#`VFc6Kjl^_PF78sO?02Bh~XpFTP+$z*6Ap4KNjK;pb)P z^DZ=V{rKozigLvey|L(YlV<-K&iusM;c)ImV?V@!q?Fm;~jogJ+BmWeHvqY;zsusXs zlun@OJHrfqpXApHVSZ&|PI}P;A64bz{*vmCaGm@W&kY-}$?nZmsD7SLBIJMsV^>KP z<+el+CWfvxIrR1v3M@RD^s3l!FkRGw3$ClywhxXt)NmYyt$4yhgI!sL?M8NuD(Cc%i)8H`c%0DCsRI^6cmj=zGAiQ^ZNlW+!;qMkYS zIOyPzwaYjsPWpmBc98v^rAwYtmVxRN^YH`k8J7f4c*VR>>&kXgiy(Q1m!9B6GI38` z%jD%9oAL-Ig(Zx<6a`7ccC4xR7pK1x(6y+MmkP1f{(v+m^x_`Jn32$QaiyQf3Q4P~ zD{^pc#k7pLzli~7^3H?an)xfry%KI9wbqz*s;(V`_9P*nz+foE4JR8hpKLeG@SMG~ z|6l`(Fe+P!H=-Xd65HHR;XU6BmjHb58T&zN)1^<-yAQ;DVJnu^;o)@5ZkQZo%@Jia z%59D~XYYo1ohL*-a$Kc2;TW2sL{vKhrqS^l;+{H?Mkw&GF@&8Lwds*OMs0h{GYc*v zdJoFE@bNDNsHFQJ<4Kx&^xDC!a6|dz`sfgRe6vt3!!uTlE+y0~v!ZSt3HBKTag-Pe zSf^VdNLr!WoVzZG1u5dLky_R{q+h~X_BcaUBrOi>lg!Os37fNB;nsp2htAKK_c6y#ob&>Hl# z`m?K2&L?WFBlpGLB<~bQn|omE;Fs{Ma+$YSknn`PciV^R8TVf#)xJSPJTBzbZ{H6z ziDE_rl;5Vi%`5MU{xa@0LzGIIIq2&5GyzhuPY0bxF!>Rx6wI5Xs6+M`;8dRjNO%y? z^gs;0Y1&=tIqF-|VZdsHhFJ|miS1!Ez=0{P-M|im0CrW5huL(Y9uygdd6A1P7~c%B zdVX1y^a9Zfzc%dXhbiM5B+}|+r@PK%gt<@L4a?DMT84PdgRe?b?bT2xnCF18>~?66 z{CK@?)8qTQUy~E`Cf{{fV3i)j3PJL6MNp2yy7#~x=!{3GL=?j+)TkE5jZ3BN!G+#j zch~v}nbAFEEc{5ls*f+r4WmpA7H(vykCaSl*(JTA#9A9^qMYWFb2Vf>Wi9=@``GIpk!m*(}&^( z9jU`n@4KOcNe7y@e`;fD-rnv_XQj-^(s$v$Hh`S-5#SpIkCxelech_^U*(sw=G zUYlQbwQMFE?4rL6yl0(m?+o}hgLx2Ie?w*oJohp$2hjRYgtFYDHlqFM?K1+!PF0@H z))KA^*P~jenz;zYKi|E^y2Jd-5;7Oc;d27w z&puF(fi#A6#kRNs&AZt3vkT3DdOdp!E(v#EqttJ2@^SbG7nd$%Bj8;)mzwq^`-}%5*s(-0&4goxSKDXL&>oK785S zCwqXqOw$wCPTj)|dM&hEdVS8}m&EP5gWG_ep_q54D)jA{;*Z@vtA`U1E3I}(*ilEr z=vfzXJ0IRq#bmj?hda{DzA(zC%Mx@>9o>P96p7o4Lj#Xyr}ib5W2Q2OYy}*&2oNIR z6Eh7X-=eDjDj&f@&L_Rsb`SI>MHE0x^jCG2Dn&+zwg^H<3wAODT?RZddqj71>lkpY z@II2h39M6f(tJ|rB$yf1F{V@oRhC#*T4q*iJ*HY#TeewtSQc1joP(}Ft>jE{O!95G zw+vi34*mVOG`VziN&Xx3w|irBW0Gf+r>|r4que7x>|WTdGTSv~Yi@jQjcNL>^0yFo zRLa2Fp7c$!t4BK(FC{PO=jLfPQ}B;6W(LT71!n4>{C`(H^FtwP0CBitJw394l_Iu6 z;O#GAfUWJVS?#Usu91nQhTLoYEGabyRqCD2SycKu^-}uH?Q)*uTv>WKVyFRBZz-97 zPb(d(?JU;ituxl5GJlCq8vnT?&cq4G^nl`MIbofAxLv;d-}6(| zKJDI)Ug+H|ML=7bP+JRpd=NM1OHVmDUY}DnY?(RXZA(a|s#B_2KKExMM}wIj&z=`8 zWk(n}Yqr)aE5^T+w{I;Jo%97wq za3uSbI>)-hvVTpP7eux`SkZpdUtz~e1U$ke?fXtdC_<=zt`m()(C6mLJPLvAmwP` zE6LpoveW!5*RvXj>D%V>41AlOeLqVd2*qF^`V;eDWGo)Ct}zBqBpP^g#aqb*afUUr z@sx9DCnn2hVb7FxO`uu+X(gSqRu9ttqd`BK9x=Jcg&qUF55yIiwbvaMsXVr-VcV<~ z+9yQh8h1QI$QHc1JE9iKJ3w~_yE1_08owP>Qy&sGsE8gmy4ThfBf2}}n%5I(Xxjr5 zn4r(@8B!-8?iz+0DyN71nsW3M;TnY-*ier-GRTgi1ECii6XAd!m`o5%Dp-PIPH_jB zJ8<@co*N`4KeG$oLLZzPJc%B~J1|F|PF*ycI1w@s3F$O9o0^vtwIbl$ zOJM7-6bEL6u_FB#yCeZOgN(QG%RDR_Gc+!Bo7+=5KQ!%}_$@3*ZA9=E+${kY05PJu zk60IHx(vQR61+UJwoe~M3Xd$7xo@+N_l#oQ&2~*=hs1Uh>v~J;7K2MDHuAO)+L%Uw zOeX>0-6bCy5!*Msg?dT!3`P(3foEXT(06Wgj`v9G5rd~Mw(@ok>Ky4c9NiGJJamroNck3-rANMUbX?SFRI+U5 z9PBkPS?_Sp{g&?4*J+HgEb@r=maT2TS4*%Wd;Z%w?W4E4w&MKiT=fy>@1QsNRy@o( z=NrUZz{iMnE$1>kmnqsv@J21f=HSMR%raQWj-4yaVIIvX**)qX7@k00LEXF@IqG|4 z_r9xr5BFFe@Hc%hEzt>2tZbs7+Q3FK#o>fwx@FvQ5i7{|!?KdRFK*4yhUpj#_3D!< zwPGvg&B0Y7vLEr&TEj@*D9`xv;M32!sdC)YT;uR6&{BybXp%`91q&x|qKb(t22CPE z%Z7M%GAU~)SJKxnC>bQAC55b3$;sJqX-OO0+hZs*qb&>J1s?huPcWAm7tgYMzu^5uCN3>1zi7W3X; zH;NVWy)H+Nro~L9^ZdSSf00$9QEN55i7%4neYj_uqV=$T2~V1;ao-tOuvo12F#8y* zY5$|YS3J?1TvcOLvDs;ORK4M$Q?sJ^*X9ey2A}mti#i_8B010mYW^_}?n zIYE|>%kK7$u9Ms8bzJrL)J7EE)nK%X;n3f@y^ZA?>vLB&=XM;f%_zLD-*tw$PG|i3 z6&}zU&BIe;R@qreX>r+$m1gFW_6iBYU35J-S9X6h?V{B@q#7s88N|{e!8>Fsx}G*YGH%;l=+R+2i*hShqeL!jY!okO6VlskY_PhMi7!pD zAX{M9EhS(tK`2bqPib%eRKzsh{jm`L!XHT0nV;0$t?r~Q`YU--q@WRD76ZZ68j&y7 zxNkv3jT2(sW3=K^q zQ@(m>rK*ItXEpR&j^;TW9HHWJv!Jz9yK+czmbodt5$_aF0dA9Z;T5$Esxc|JEV-#F z`Bz`7*j-chBM)74R!YVe6#Cj zRa2dyvi!X%+mwp(goA3$iM*#pRS3Z^$!px@l-Jc1<))!L3?dY+C;L59M;Qq_mb3t& zKQBqw70|j{wl+v~lsJ3UD(-8pV#-mJeMNPu%0g2iJY46G-J8>HwHR#`?ie0C3qf_8 z@jVx~YZGz{cRN`Oz%Jt#EYgZCIFb5;)=MYEP*IPI{nr^z3X=#$~>^DMvhJOR_aN4V01?x6(R(Kp~_taR&;kd zpD&N`J#olsQ`cPABq<3Q`N0oSC#Q*tr~5{v#xc9Ln{Yw~F8lzRqQdm#oAiJvmSo=c;`+KnlP5+x z$y-{prQc;L&o-aOaEqn+@-*dP4}3}p2um)P2N&xFK6QUf%c*XTu~ zt7uOV{yarbGjWPYT)U&O8QWszkxueOG4Wn)bNMR&xnSi)VZH;*B?>9YeNX$?x~3vT zFfT>$IUfjrXqVWp0ugI$Qj#TT5x z8BLIEW5e(mY!)cRVP*Q}x5g^=<+CREmR1APAH+X`pj=7r>5rj+F)ER$<)U)^sTGhB zrMOVu(<%@kVd-%ZA%lymrv?1Zr?HtdD@;RmfKghH>F(`YgLO8`m`tjzH^RI@*AO8P zK|a8{bmwaH!Fgh^bB7EG4On)8cG-8K1x2U!o^ZXVW*e4IslMYT4Pi5*Vb!?4AO@?7=b4C@~(MpfbgKRGEu8ytbD#k0N174U(N(5#;7&@m`~e z%DkNOXQmNlp{|V+8M`X8EE&E4^B)5}ynP8i;MT$}!dM0%g2818shm(rp+%uAq2__G zZ<+nP{ScuEvC)u#Kg6i_1$gXIa6FxTTZ0R$l6-|%{%~pTk)7l-2*+H=UOAuA z`C_r0ari}7=eB=xo3PJ--!NmbV~h1W#J5({^z4H)a87`YVqjv-LIs7{`T_mOfsy#R zH(bGOQm*k{wo1Uu_7LxY8CAV35D-HxU?0sy1~$HD&Gi6E?n>vH$2ni073lHX0G}Jh z)Ln?L^Pqp~#ij<;;zv;xP5?{P-Wv8_5$0!U>}Vh4xfsm4GB~g%jc=sKQxkdr+47h62N6;c$}r29_nN z!l%a3`8`~oVR?k`CWwVkAp96&K?PdhsP&tjK?WI_Jr^>J;JXP8qi7p1Ixifq&Zx0# z)=VLKt=ty*$j!lo_9*h{y*fk&NIOJ3FeJg@S|@H9&oIaNJq9@WP|UzozgGWPiMcla zTzX!{0jt^mS)zo?L5F**_rbj-As#>WfWBCRR#mlKfIIogAuNX(t=bFimXQ*sT}$&3 z!7ATp9+z4FbKFX~m9G=KjO^{#**EkS(o;A>M+p|mQ5ZoN=R(V+S_mG2Qoe)Ns+(*Fb}{k$ zN|=Ejh#!1UDr0iI+$iRx zD{_e9o^Rk}D{^RRfBP6%gHqS>FIsT)h7-6_6%uSWm7zvZegYvJ|9d9kK|6SX>#MVEf1+=9X%esYeeKO>u zP8)=LS*Gkq&irJFlH>*E&PB5-32M_?b}XQ&t}Rc{Tv6X)YxS@cVeQ#d0mp(AH)$2O zN#&Mxp{kG!9)i`5vyiMG9=&yK6VRPF*K_)9s47Fdw8f}Yn@iNG|dbdn= z;PZ;LNk$rUZmG`b2($%}=oS%7JX($+g@vZA&C!3c$=gU$FL0d#hs7B_*V9+=tG#bc z8R4PX!N!JTTUQBTdx56oqPfpEe{*0B;zw}H+6?1#{V-Rn_Q)jsMuUS5&RJN7p|Q&s z2Km{fZ1Tj=Y3v(axr3V%1g{D$0aZ%O0^zg(HVg^Yl}@`hu$3hJdE&85xk_Gp@_@VK zsu~=MhD7h$EXs$PCCwh)V;KYV(sr~=#I~}uCY#kng%Q83g0JFHPyhkW@oenAWl{Cr&BHMP`gX# zHr!uh&!oyNrqqfR5%lY@7_5E0=`DbR&D4?vtC>h-4bL1 zB4KhD=j$2yl*f2s=x3uD6v9mMB?ZLyypSPf?P#eepONFZTf25JAq zUa`9i@S0W%YeEdDji?cXy%S>QKlbP+l?f;iN^#{Wxfiq4LILguUzQZ@Q(*@R8CKqQ zd3@}p-A0n-QUjaY!AcF$spL!M2Ije;GI(JkA!~%0)uVZ>YqEu1dkh%LMXw~`yayfk z<>)P0wtzP!#McmV!9<70^tSQ#T4kG_9lljO{kl_zaHGxv(zs$E6z5XLN;@AFQQLGVcQS; z4j(Wxe0AXZ*g(PJQ5uYop<|Gto<&XT%3IincGGuv4bm>s=vrj0Ab?L!KGM=igD0F* zvIRL+8+DHIqSt=vM-PwQWAY5a5XU_mur4bacVdL^7GDV%8W!6}wvzUktGf@Um!!bh z)U&!?i%_mr-_^47J6EV>cV&OJyC%pRpVm6|yQN3@{YBj#s@jYZ-Iwyr1F?k|R~iQ2 z^P9epT8jcvGVq4I!mU@~{Sv*?#MwDBD0V7sm(i}NRbZVOe|KtbKc?WMEt zA{dh_GcBttMIT*Y4Kzmnj(H6KI{3Bkv7bXZ`mC+A3|F)DS{jgvVFE4EYpo{Lpay%X z#w+ZG|D!Lk8kIE38gO?~V1yx!ND$vtNHgMPi`McuHSKb2?w-|*`X_#}5F>J@t8A2y zk!i2%MZ0@5c+8SdXD!6_FvJM^7PEtBt{a}{W0UzMBX&|D@M$N}HhOqPgMC7&UzB!E zHRzH8%%)bwfWb9T`8r)r20v({5t3iIL@m_blVBy#Igf3G#0Qcv()~(^aulsINS!{} z8#qNi=BUrX2onb?f-piVBLiBtb#H z=p6b3nXJ#Q*uS9{J@dovHGMis8_gqLz7#m7SBnB+A~0mY;1LQ73giTa*BQTR&5f)U7S6@UIxqY<^ARj>^H0fD8+SxzvN0cbuRfx*D&@$GjhruwSgh>@~%+sdE* z#OqYJftA}{jvl2Q^uRSwxc879*!Do0 ziar91D0R3uSibFeK%*2Cbrt1`n5QA1aJ+9Y>CXD54!zSfpwl%V^z$H?OFPNrXm#L# zQ%+wuLSND=^l^w<{jK9q{r+p+rasi7#K>zUvE+>w+oE zd{A+7(AY@B;&CwXft!r{Bgt#m0#ih_&-Cj|dwt-$eBm6&dSbZP{kL+{1uIzyK}+OQp$l*2 zEN?GBR+C2sA`hsC0V&--2l2xUl4|D<`d|i4Gy*dNpP>vs1A_y5!m37dw4(48l)^c^ ziH*BtaF68WM%0tLLWf_3c>eWh?Hn63%`l%RGNK{GXvB~ILcN`FNkYxTLQY=QQ4&+) zp7mOa`yz!4moKE1NE@FLT~MaY%Zr5tKnl>6XSxeUaRbIMUyFR_Q`G(PMe)a+JSIcE%8?SU3;RNW< zh5-$;hBOiJYQi?I(4Q#3Tu8kn8>2PiL!?u#Om%i@pzvTzrCv@xeYw@)$r-Aup^p}yPV`!GsYYr5m#F_2kqT-6vsKHRGt!!uNnPMxomN z-k+MDa&2V>BXjJO-rZ?+2KjeXtypM|{p@a!Ick(D6sx&(>0cQZUZfXPa?}wXBa%7d z8A(FFIZQl`X{jh1;2z~FNcHeQd&!O{sj6(1`e#NS!|EIvxebbxo7L+a3l3PQvJNWk z=YB3wLwId{$Pn?E@Sy&(xVTC^InLCe!n4B376~bd>r@A|H+`%8q+MNDB}*<|yVIU0 z$k;BPZY*msV@iTHs9I!?STlF}YoRPX@l#pRC*IG9oI?}xbh*uL)6ZBQC4W&E%1qvm z0^5nqT7o&OI`0ECI#p#d?2;WhB_)N~HY(Y4$q3eQEX-5N`o+GwsVJo;$)57B?S(xh zvXriGFH8yyox{uwk2yQ_659BjodHEGGS*4qeGVUD9&g$>PEP1pX2KW-7dh3OIYR^q z`4U;e9UA%Ut@~b46Pd8aUFA6Lj@k(MTxT|q^O}Xw8m^E2ir==Z@Fuf~uKUzZ5<(0N zCpiY(>9k$#(yu?@Puwg9H+wwI)_9zbFZXD7vQO!)trO=h7AD^BQnns9g=7#VQyo4M z9{MsfrM;5@;LQfT)d>n|JJTMbrETQEXJV1|o+?VAT zda-oLUcODm{lb5T(VPSIwqU0|Z-^3QIpPOh^Z2y3W53c6i}OWyo7UZGP*W*q#1Yup4+ z@*;EC{Q2g@7EHxJA9rR4P$xh0K>`h=tQ}4*i&|H36&#EsUCmOpS(2?k?vbm9L~%1OGSMe4 z)|$qGud&BbGKvy;u@{39QDH`ijP_>r>>{7<;F!L$j(GE~(&V{A&u}6Bx{RzdfQv~V zpTeK2bYXEKs02dgiEXh2jvUUsoW8sgbIMbyip}Mh(k9C=SQ&bV!PUZso?vmQ@T%}I zp8}69r`9iroG7iJJ}`f9uh_pZ_Qf45-WQ%1zBX=JyD-k2&zoDJjrtB{WL1;oGE65+OVE1|*jEa%IQj+8&ja+Xqo}Sb@R-(uB^f8AP7ICx%X39c=uO0kSCng~EC%S8%>Ynf5Y_$#J&X(aOWsW~+|g=Bt#CS7HuB{iI*)l^ zk7d;8_K>1Xz0q3rOxA<%NqUv!QM}@$dWjPkum4EZ_l#!fg=6Q9U*=a`k!6l6f3xgo z^=_!pro3YQNfk~7R&j<&I{HJPS=UMahrzk>-$UQm2MTNpTg+WpnSk?&Y30~G5USXp zLaLz7C8N2qBhj4aCLw**w#)K|sLmm|PT2IgD=QP?kt`(cEsJ4!Aq~NoFvm~}r9Ulm zc0Hfn!35)bo?j0j8RqCHqdC2g5wxJ&taf>SZo{Iod_JAP8qicXqF002OAVyU zrzfRHr4wVRhPeKHxN;+Z)3^^o{0M8#qf+&tI%=k&%O?0Pwa z*{P!8(!@lI;o>XUcuT#0$BypQskX!%z8ZCCAu@?NB+3YI)o>m@@XJyqtt-O@m}?iywtfmE$*pt12VPALPTf)HG{c;F*tF2yI14v=jBe0UVvSk`79M@7F&`L zGc-iC2M-o5Kx&jQ+3m-4)9gMAdqQnhs`h_&b@3b83-p_WPzJR!M1}?3D5W)Q_w?p= z7ibCf*X_nr4A@VtE;!D(oukFHdWn+*Gekj#=hmFjB+axdDJrm^q`w8T(_lh=JgCai zsAF`u8Ry&l%H;o2AK-A-aEOeQp^rLQ%{Lw$6Viy(>2M5sSS};brPO3Q>69@>_>r@R z3(vA$UG~FR8=i9)7FL?65J&W;eC6!Vl>7mT)Y&`Y6e4Kv>$A9sdYiKl;O}NQr%JGfxgJ7 z=>#C}7L8}`m|QIZ*Z%^{&m|60J0PMHN3Vdw9-1?YHd$^0Is zI#Bv-{ME-U*>HE_c zaulW?PJN|Sb-B>Cx>DeCLtBi2daKdskN6spk%7!u*FGO{IZNdlND;Fwn>YfN{=h)< zY3|T>XVFFCqdUX98ArOfX)5~&u{QH*w6a+ifu6fgD-`w${9+*-lMc%Sx`AbJb#aEb zQzbX4GAg=89)XgGcB;Nh-9$Z8j+&2>khREO#5lSyB_ksVB9@8Lwr|fFl^gmv=Kl7M z?O~odN8^?>o0tqrq95;EyYyGbnmG?Lk)-o%RF>&*4-6%NW>>sE68}D0%lBDctfM~x zi#6Aqu3W@KZJxd(@|CS!m%(5^ll0@~mIkJ0`Z^O+jpET`Ocy4%^dQ`Q%f(jyQv*zZ z=D=!vM;}CId{S$d{r+P;Y+lI>XTHVIjf9iMetLV|g$HkDkJnu2^wTctdw4#!+Fx(gkHoErPnwzXzr&dZ2cSmk$p(!=))oZT zme!|c*-R4ISuc_1K;|U%gRF}TLDgm1FlCSPyJ4LJR`h;z^RLd3Bnn2?=aG`2JOBny6SrhMjFRQQ zOhuLhdm%$aLvHaU!`H3V4!MNt^t!RXzB_7(YYv%=oll0Shv(_ISp(kd%y^q+OEl3c z^N8Sn9ub-S{r2*dQSD}~eLb4+=Fg{eAy#ombgJi*h{SXI+qwJ7Ij%YUx)%pdhKIs7 zUoyY(?=Ebdlgnq*<$B_})_tVoI>ixWoqdoL;m=h65rh1T8Kq(sGbN2AK!e)EB1Hut z0?iWNV0MR4W9J&NV)WP#wG%>Sa7u~@3hL5# zt(eEnQpT2R#OL-PGQWC?$98LR8^WN9w0bOd`=+l3JOBscxAnvb#p_Pe)o0xKzCta$ zlV6v1W$+s)dfKrP#lAfvW|v&jY#De^W=&e3-e~d@I%k`*HI4F0lN45)x%AjuAIv}> zu~}KQws@Q3HiI{-J9ReXVqM@XMo{7_O0Q*|vqi1#FS)--Q$M13G6I<71&{danG;A` zn=CV7WsH(=40R+gPBRKBG~ine9oKC{!D%}j_ThR}A27_Egj{&dBdXwtuVMG(ARhDK zpSL6Uy__(f0|prD|(8CFUWY#oUjIGp^4C>4=+W$#J1o#Bt-)q-!j`$vW2 zF^^m-LVphO-G7RfU6@md$RCF*@=MDm7^N@?joNRBU2j8)6yV(ra-K(Hh5+WC;dmpq z4pg60x6UFK2i>~MV{Y{f9TGL8+gxXk2;o^qLsvo}N@n-`egGrP{yqL5jJ;E=Fg(;? zeQevdZQHhO+qP}nHlJhLwr!vDzu%usGLyNPG`(rBnw_R;yZ2fY)Qi5!qwwii%Soqv zN48o)+-GJeD`v8YAp`XmK^+zM)a8PBa%vu)+RH|7syoIuNgbGzn4P;{8Zp)NW}URFBRMmNiwmA zittrFDWutua-~+Wm&B;5kttQE6)<+4I%-;#q*_&Q^c3i<&~2ifPS%myTe{k1xdo=Q z(nZQrQ;h3LDb>$jfhcx`At&i&l2~F&3=B+O2zoqq{37-^T+0N=*4TQ5KlUx<+}K@* z6Y%@XW`{K(9%k(63oLiFeXk0U<$p1GRCMjQD$|gys-t#F8?)X$?0<06;$ZAX3EWQm!}pe zR#eoFO&neJO-&1Oc#1Bot#g&B3n;@(pG>3R`zjr?BClLJ-4Zj!5~!H=B~ef^)U3SE zJd%LGkf1+Px>mL7B}{?B64Np%WY(2M*aA{aZjtt)FcT0JtkOx``Dq1YSK6Cs#CR-g z3)nDxDPF*Kzo5YV(XzWMyGEHB6wt;nyLHQBB*YXM%?u7l@lX`$7*!hr4hiq3Q;)Y};pM_4XU{~3OxabG`EmtZ%tjH7o zt)q#xQm+yZKY6=8I%ajLTf3;z#0=h25MmSx`qUWq#!V-uNN~~=k4Q&ONrJa>ZDR%x z^20uZJ=+1J#?sWqAd4D$!P;STd5GEbGihoI^yJM3yQat=Uwj zhg;||!y3&D!4Q9d)2pi1FGXkqB ztEyo?%+>;R7KB)zpjq(jajZ+TVY8=Suy5I!T1O%SAlfeL&KciO-CJ_^V|cZ(7IM?I zhFYGKU{>tB;_D?MhfEi7OfMcW8GRSFmW#@|_#Rcq>HCTMK9A$S=+fOL({Ox*tCuoE zZ!ufwHWuqd38Bnj(@p!M$Wg-4Bl^R0?_ayaC>~Y`{;m0=B4=6^n*%!A@7cO$^)#9` zR?-jcD9|>v-@3zeZE?R!jq&a4{CYkj8Dw-QXi;;6Qii-XXmbA`uS7d6x-o3sBPbJS z+E6qr-&LNO{vi3EG?RZeR(<@(&g0^U)DzpA`|FF=*T)w37WnJxtMj|{zbza?Ol$rS z|BS|r{w~g$e=(q?ZsOK`_P-Xrjh-qEY>CFR--~NIEQkMX(kbNuG7elUL#rh2r%>Pg zR>&>i%GJq~P2llEWpj~TtVY0YV8X4JM9?1I+I$#HGvP4eK#DssBvOlgh~W1pXH#^K z2tGi7OFPGGpbT*`U=dI=ujP;X<+C&XjiBp&Zgmg*Y*k4vB)&<0&A_wyv>R@phWmRw zPVKXkyiQjDzQOw!vY6`q62IY-0L%GGhOP9W`g|mR3vr!WDUOy?Y{HgY(PS zU zjC|6&jeZQhtnVTe_V6hLLgv>c7bkb8dj%Nh7#BSf?hJi$R(*V)wP3%neoA-Uy@2J4 z9kE@`h!yWvtWn4|E*rO3nUEC=RwEXtN>$I`9kp!yL#CH>acR-3SLXQXQf*Rl9}EeZ z1R*5k<09JFO~d|$igKVdq7!+tNYVZm5%2`g|6#d!lXJg=^Kg1IHcsxtE>sF}{`3-t zCZ-Hsmt3O5n+oVn7S7-4~p{<=D|D|+EF<}+HPaTU=#ZYHA8V3r@lyhv72B{ zTg&x5&{CZ5X1iD}eZE=92mfk)dGXDv$!)&jJJ*XMgKpj3`*vTI?ARCbMFu`4IeH!H zY2`U&LP-5~09Rbn?GG%p*PPGbB^_>Hu%ci{QH?;xcq(l}cg(#Bg?plV7^QQAv`PMo zBy|jY_{r)?B()}TC0_t3R75YHe`9+_Ep>Hwdv$ZKj zmu0{8b}#M_Vg3DzU5B|x7v3JPpcN;Dl6qC5nVGmF5cF&!({*JQdmTYzmYup$va%m_ zihtkbHt!uDx3^sV6_7Rn?iGXgv0gvd^L(b$sMhal{IdDY^JwaL($eWl--G>a3;MBr zzhC5MOSOp_`@9KOe%#06^Q}E3=FgzqZtyVu)op4TYrE+5mNGF$P4*M8mJp}bsn+GZ zKwE%XiM#>4h_9lwIv8j>=N)N5Bz~*=Q}$Ou525NPhkW#Oix5(ir9$q*7MXkj)#@5lu7 z;P@VIx`DiMqBeZ~i&IyYpWB=L;IE)N@=4p%eMFt}!Lz&{NOlOmq)MwxbIC`tOOxEalp!q&t%?uKJJn~i(kQ^o z->%)VL=z6|VvK6MT5P+l=`zZWiEOJZEY@_@iE5R(CajUGGWT`Fk+W;)k@T+Qvbzn^ z5z-y^GL_$Sx73h7PZmL229XO_Em}s57BOX&6s-DKtCb)!sZuf~;8>-qqN42r5lVF^ zm1L(sKt6@8Vd!g~zYUJVmh+fT$0R zA01ra?i}xW`v(6`#^Z(FCu?Ihqqr2USpR%~r{v}ssv)$doJn|c0eR6SN$==9!gti{h zwIuKxhGML?z0k5hF)xC$OwqM5a(q6Fee8V76-rMJXbB>ZlR5^DNYyfNidoCcEs~KR z=`AKh9WtMXmeJFpKM=`Q%i)Rqh5^kkkFp9%VVdaY? z<5+eH?I7(~=}M5-t=cZ>E z4C-6oe}ew%5Pq8hvmrr+-QYIc`iDn(XUFrhCYhklYHb zS}d~O>^6OM?=YjPJJ8h;?Abq_&5#3YM^r43N6d^6Dbl#8c2`WH-i(=OAmqsbsHuf!Z&GU&p1h}RA3SI@j2G`NEmfMsY^+kSeygE;}JD%whs>f){ zER|<2jR^S)To>KZ#UaR#{LI>6uz*F!O;8MTDHy4uh0EdQ-X~^ZECX(js#RrrA=yC? zO5ZDbB^ao*hN&c1hjdC1UWkDp9bG42yRn@hfB3dr^XO5<+Xz9GV&5X2HDXQj>OHSw z5FG5z0_?tv03H?BOphZ=FFvp6BDceDsNq5UevbY&vUJZSE4TIVcut}!8U zR?2<@#U{S{akFxtpX~R3c$K-%mXY~$Zm-4fHDkmi?&jr)B(99ga1f6RwL7Szg2! z)tT|K=p*W;JR>>@L{qm8g&Vzl){QRs(TqiKYkD(?ZOyGrbo2EzF3zv2Z!>W-y6LW# zeVjEnPy-})vp{@8X(~3Lh_s@(8R9+0D}UfVF%csM&^WVkaXcnj1`_;u?Bwj2kn^bL ziVXdjarXKaap2MmQ&h@Jy>vea1=Q)AR7`;kI-|q~dZR>(Z{JybM0B?`5_U9*%g9tC)>Tv5989 zxjE2~&OyThzfgrn^;HobeKhJQb!Y_&7$YK(EUOR2KBIo?1hWi{5i@SJtnPI+4zCQ5 zkry{u_UJko7k&b-NvC|R7}kK)t9E8}XL3ky;BuaxwT4k4IBsVq~81aPFJW_v?T5p?A)Hl2<+g}fxEULMh? z9Bev?AT_Od1w91MMI6Vr6J+er*t@p@i!|>Nhb)ipBwZA+)w18FxA1*bBR=P#bBQ0) zl{ijebhex#UKUT^{EWWD=hohq=#F_b3yI|Yjs1MEx3*2idI}f$)DG>I zf!Tni8j{b9mW<~Nf$5NpGuG-B>{h7mw#YWFQv;aj1NF7x+sQa(3((lzLL#&*qd23W z|Gq?LhH;{x7_#F71@eg&eOq%eJvSDASW|~mdr`)`D7nn|D?#CP$f^D@oN(FImZDRA zDBn$$`Y0XZqj8~wZC$1wfW2)fnv`alG=HcDIA;)&*W7wZ(_ zJ+`tqxC6S@=;fN^vzAH+Z1l|J-NNOktI^TmW9#&+=48*7npU zb(7S|>LOFkJDQNBlCiC~yQ7r4yt&9_(c)WXW~roUWqMILB%Pz*rH&NKRyC_E+;V4i z6qz7}Hgh;COW3NH;()*hOs^9Mt`!XzuBVD}w@^`&PNSDTiH`dn7lUaFsGU}`G;Wjh zIt=zu9|Zhy{NePKnA7~a9b>XDt0xIdby~E_v^9vYf%r&*LymR zr`Gdy`3$RF!rN4^xjUG`S>Y(4n2;C$9vzuao-(X$puJ{l>AGVry%J zWy9bkK?5|3FwQm*HVy)A;%Ni*!jNop6hd0(kyvCYy|@9|Df4uETDOISf0AiFXp?Nq z9)sN=nIZdGOrUR(-CUfy{g1RMOuP4≧}THO>bd-n0iX-;CFoZX&% zBR48Qs9iX4&Jt;sk6@v*%tEVB4k~p{LRr&oskkJ#28zym=r!m%&w68|+p<(npDJ(y z?IE58l*1dXv7EWqwpnbiPBiCgO=s2JlxE>fFxwc>rO8>nV168Q$HnVb;Opcvb!kr%XnC)Xf3x-1TN(SD+H=&P?zaIt#wBbgh}jJ)>t}IZSnp*{qANU~8pW2scMQEI|&?f+HeUDjZhzW!cn7Mlbv++c5ci!>+LMzj|9 z7_g#-A7BM(Yjji>sh$=}+jBx~K-vh9R3jEt0vTwxYCS`o6}fhJ4kf0b^i94jTpB%< zg+9whr^_7mYlQpm`ykGDRW-f|n~wKEY7F4j?w|kZ-ytje<%~dIqV`xVI!5-1KX%`7 zM(cEk#8l290p(FBu@k8rA-&9?CR+kflCmDeHZ&>D6qZJvrUme=!_%<#u%nsEcf>~w zGvAP+sZQv^*jML>P<83l;?%2CJBD{C_Xgw;-vR4w{(8RSSqFX0U-%brK6!LeprJUc_)+!^uMU+yCy=aav^NAfaWy zuhGwDmnZrq`8`_>m+|~XaAjj&mBc_!x&O# zAGmB*$*iqds!3+q1t|tlwwWvIt%J;usSL=dgzIxf2A}!C)3RvxG3Of3LgaV)RbcM0 z`&2C0>r)`}fJ6;4nhu5N>gPeoBpgjY^dxzXitp=l=J^^#tL@o0uGig{kG6CLPytZx ziMkr6#d8Hw;?ggWLRv!wcb)bit+o;|g+p-+FL6L9&gfrd6UD(2(DpeoaiA1QySd$C zcdgvGsB+A_0ZFwVnQoJ807tMN$BWh2H%=sc$TD-Bd(V=g^EPT`v2O+C)sq^ zANR=YF*_{Hp|u}!XJdcbbG#1LRY-p{OpK@8U*&HK;qABDKD+m_J{9uC{Xn;!3fY0- zfQLBG&q|ON&8O>e(KG0AaiVSA_!u07ie9D6dCYm2@YD`+)yk#AAVUvz&=!nXwWnlF z)EE`-_m(dRgNB=<&Vs`69(WEo&pB@ld0}=k&KAkhJXePgXJx3+(=9UTWT=lXu92<} zo1UVgo1uy5u|&g?sjhQc1DYb6W*K^L0b|0?U3Q6hM`jGy>&nIWcb|`~By(zZo0vXm zv>VStQHgP(Z*!y;-^Plh@qL}4?xr;19vn#Thl6^~{XsrWiqBcw9jQNDb?EtrLp}Q> ztrE3$pn0iF+&Xt!2b%{P1T~0XCVcBy^C}3bU?dbdFR|`pls$pW8@4B(F}#MgXAs3$ zD~yI|x}Y_SaaCqD<=px>CVgdL#h@@ri-8JnzU3Q=1Uu2e`vJ746alIXM2i=`eQ@5b zz6#q{pM5vh*qceQ%)!^cL+ymy=k3qYH5welmtk@QHl00Fu%l`A{`RZ+%XQqpU$62@ z=JI)Zu6~EaZ?{}C+!RvdV@mmRy{_8*MNyU0*!Mi1a@F@daJzO>;hLQT=$S-3?ZJP$ zHM7WdsavIOs+|-HrBgajn~Fd^p`+^Pu@v2?eWJTd_r=~~L3_VzPhoFiL9;(-Pa9BN z?I}5Gd#=9MpB#U?N6vE2qUNZUqN-zR6RjT6nrIhcuVWXzF4D{P&`L>@cc3zXH3JT3~e?fpEw?v^g3_4_MLFOi!6 zYSni={$kho0sLxZ!ZVxy(LZ5)qwkg((WWsxvr#cI9h1S?v6Ax;Z$NsmKw#c^Nu&t#x>XTg2Q1#KytORdINSi-` z+mzgRCG|_h+tN#OT35VR`%-@OAKdS}T(6Xso&6eUKc=CLi{;CQSo~$lRmCy$CH9`C ziS=oX{oYr#`9H$n{9fTXvO3@gOVkwU9>ct+Bx;3fsrH~&mhsM>pzVwL73ebJFU*3if%^EY^6OZEjd{AuqPgX%l(Dz64>+jre|Ue@%Tvzs$&J)Ev4)6A*2wEm;$?G7E;=lU*NoOy4<-n8;NV?&*{ve42Nh|Gcc z1vdz88*Ccv8f;A4qp?M2%Vy7H(}&uJ`BFKWB=v=~s>$%esnwa4=HQJhXXr_h9$Z zd+fbBvDrtm-~J3wrFryUl=jjuS`!|LbNDCS%V9-w0V^VrDzfYfc@AG#5ouFK*S?ZP zRc$GtP!>f)J;m4<=2nvAPtw1E;pCCsL4K|djXqjcc=pWumt|GW>IZ163b6HC4qSk` zTm|7mpMxTaDxOGCwx{|f!PcFcBfZz+zVoJvkUM^Ig!gL{^V4lYS^B_wJb^H74|iUV zvbD3Jf>52IrVAEn8m5~}5Hjjiic$&)r8WjIO$Q4%BGa8|OhGAFW{ynHg)($hDIs)X zwE?6`ho&Vj2Qam7jKkSdYX!w;>PMDWw)MQtm;Dy^A*1|+zfEH z*=wJFy;^^DrP0;>s=ejX*S6!ckPmIKck8@>zV-E(>%O;g@BJopwK+%&>H67bjo#?h!E{Es09XJ zECrSVT;B_Y8Mj5C1nVRz7$Xm;GLdDgL`hLbUo$H&iTQ;#7$rLncMNrGjT2=U=P}^1 zEHP<4yPz{Tcka@MXh)*sc=pw@0BIz?cNp%wNou)t)ggfO>#EUb{t}B1TA^G$*YX zHSe#;_oSm4!K~^8(Ddj^=T%j^TSc>d3U^FGB?)v!W+j+JS?>J)+RD32mPsb_x;@Rr z>o9-xPNXi>g-U)dR1)uH?72C=vEvu~B+w|m`y!t@?}tk4xcLy4oR_-wN7QZ@y+I@B zu|B*DwVp@GW3sB@Og;ebWsON)&M6@MB!#TVt?pvtpA}j41Fwcpq3Wc7F zLyx3Z$m)pK7JGkahX`6Fz&#lnVq=w$UO@U~Gtk`@xh=dg!(-v^<0Ih-dxzRb@(ajE z(ia|$3Lo+psgK0Ze;wY9iUni)fk*O3VW0Er?d(c}|w zfeMPiz4zHGYGhX5fUCl^gl0aP3~0c{>E04PWZyh3To z{ueU(+NaOG%D#$z&-qs;|MyUW?W5_-{H85<*vwjgHcJ(DvaXejV2U##?)!&RKeGJP zoqUP0&FFHs&|?nc2Vi!W@|zeT?1%s{lMf}qMy@I=mwJ(X)4rA8$oyJ^KQxI`R>?3i zZ3oOxLuq%|$!uT%6+&<0A5j`JBpEfm^!NZrY^YCQB}5Q=eq&aBBU@qUDB3U9l~Y+P zGzRy(+Y*y@_K(!cdx8aH~e znP*Oa{@&CzBfm7SIKR2qs1FM6k({PKl;%VD;Mw(AoBkAfXMKoJCL#2@STI3U&x8#c zX*ce5^-l!g#AM`!YM|hPnW~==`(xAs-4rVraml1uwFTQ3javDL(a8;)cI-HJA*bHT zT4{vEBt>)2j9r_SNcT8X(}>MzcW0x9DfTWrIahxQ4a%-k_t2n*oWdNKNU3zm;o%Mu zZC=3|SO^_AYWtq;m^R5Om`qaOrpTip7|7qX;-&Ohda#Msy_?SU)l<8`|0aBz%N~=Z;xyDx@kMh)Q2c)s3lgga%E!oy=HpuCM=FW0 zfigRZiH{5B@EBMW%8aEQzlOvb=ldmdcudq}xUuD$FY0pTo*C?TyhhdA`2;O=J2Ja(y4aTkwKsf9Kg>F5K+RuTkyi{nGgJ8}Yw) zw%=H&d+|S@O?t(nFX)Q&*g#{wKd&wQ=KpwX&3p*n=d`Ae6 zG#`sXRsTV1y@rR^ru2=yV|&ZAYyPV}I@DWo{0#XhFI%g%m%?YfgTB-7qx(@xF^w~B zG4+!pfc|024q5xKWs6SHJx&*y726WIV13RKknX!?`m;uxJe<6vs& z?^J04nh1NhlrR!>ehR`+L`;GdAB6^_R*DBGMLZ6${TCT6Y zSVD#9=ap7_v2D{eA^bzVfgpb@^E{=TD_K!KCXc=?BkJ#5zA@hqu7{s{kUoC!&C93C zJwgWx&9t@Ah(d7O2WRpy-HGK@W%k1xQc+^N+s(vNMo$}xbJzu8H_L3;yY1n< z9ycL`Hp?JKI_*kE8*uThmIZq%^QPqXw0 zdHHF~R)tbaf$%`PQI-J&II?YU%fwtgvt%Ma_9A|1D?qcurCDGDewKKbT6S-3NTAvy zyG5+ab2dW1kvI1yaRwQ}!_hGjkum3M7j9Z}w`##}0UG@rAler(y_8S9^n9ig$IDgj z?^v@zpT_o8%K(W#^R)Gn8`=BrtH&UjP+qL0jb=gi~##&0snlD>MEIozYR&Ol@&r7eGif+@0+Ni4hCFX92U)-+qc>%rl=4Q?c z4a}>j1?jo~{Jbu_Vm6dFcunfi_{Kl=*J>nfxu+|g?>*A~qiBreZT_wDAY3h3U$+wc z5;^2E_kHJm4fVX375BwXTqk7wdPE-?R52t&@+;LhFrTQcwXHV(8 z+NEurudb=|5@4jw0es%d3cQ5xV6*=N%(K&B0tS6{<}iqND3pg<_DG$nlL;ciu_$0q zdepi(HGyECNCM6zJz7p@07P)pMf#B~m@ysl`QOs>CHPV}dYx*0s!V+A-qT9cyKmoV zG5&Xn(Z=8y$=dzfk~h8%EDlEup2<{3eFEsZAf5(+#LNR3yqENKZ==iS`h$ztHScNl zus37?#iz&PPROh7E;~c+@tgfaf6K3SM@Bi3YcAZHbkR`5}^Cd>Y*u6LI zX+kRXsv)sgX!VLAo+?KBRYL$?)AgER;t%5m<`CJkeeIGVGgiy(sv$J{gzmf{^`^lO z(--oM`_}3?Lr6-S|AHYF=1D2nbb*&=Al!dhkTByk^^1mZm*B4J5uYc9xTlFjgcl6~ zY8{iV8YRJP?k?*gr`>~GHeoHt-CQq5bL0)v>S_0|6qS4kM zFL|vW1_y~iv<}`;4B^%ibRTjFFQ|)-AcdmV7^IktAVUn_6a$!{T$D;)na@ET`3qhW z$WX#XHqqNL2sEEjD6O!hZeQ(7edoXdJ&yAvZwu&cy5f`n67rvopz{NKN%9YLdq1*;KVI6Xy zrZpE5xG};G_A+PfAqL|qK_Y5dZj`~H1Udf<30k4c7zFO67Dh2GtCV9acaCaJsO`Nn zX)-CXU{~d2ZKd{O2{#-?C)VQLO6qUfN2pIBFZ>^nA&P_Nf2LiPMYRff2iFuIq@^&S z!6NObMZjAS_Q@i5N+MXonw3Y5)~|84m2B3slDbo7--&MQQY1(s?a8m36^y$Go!rHi zT6e%9+Qh?BWKAk2pQATp6F&q8B0_h-TM45hD35{Nt z-GoJd8eOy&xx4zKYexE=7;#WlKr$L&kVf!XxHB^YD}Y|S&aMIHoa5@+QzjohT2tuzjTBsx_HvMYK-B zUNGf0hHm~Sp7C_t7Ywo(W2nq6BtQCK{#=&7l@s_`u*om1fnV`pm)e|t=TmKmyDl{Q zZapO*?6Q};>@$uqP;Bl)e`-^|gY`~i+%bPMH|MRLTh2d_5o;f9)>Bxa)t=du3wp0i zxn&1)?d`|vXIhQpEIT#Gd_Zgd!k&MDzH;J!__tZ{Kxgb+_V$m#;5MvSIfHtRXXk_oN zc^Xw0-H3yqb%Cz^2XNrRVcsTnETYr8u^EZen;VJv%Ts=TUIgMVR)3g&fKC3mCsdMo zCClXJQD%m7Y2{~@;(&96Kf72QORe5r+;z74Uaz?lQHRfr_v?iF>5MXSK1L<GdDVxOgO$kZ5Hcl}SQp;0|*-qmSd^8@Nj!mzZ&#V>0&ex9OSroh4i4MdRoZ zlQY=94TsfkMr4#Q$hi&IoCK{WXf61!aADTL(+xg-3^hGoVx{;5WjX;9#v@G=s`LpV z4;#IhY#HXM5NPbffffESmh7E{Aji2*^u}uj?6Epb_P%^zrUH?#V}+;%!3p_$U_D$? zh6(6rWoVD%qngjVv-DDO^w1{p(3uEv?C8hw)nW3`tSZwc$Lu$8k#IllSO3nczV zIa?C>SS~}N06UKP32c=rNArN=fS>KwONk%KcV&oClm{eDl{eFy4T*N)FtW>L=5G^k zR1&6OMfFeGYht^)UkJas!<$TOdB8wIHBa3uOtq94FS@)yLK96sW*B)ug#%AnjHr;y z)RO`A8#?upk*Sm$4|9Z6(o_{@bLQ;}`*griFt>>XNpUqKJWN^AG&V~G+4%BblLt+y z7-Q0KND_@2L~(6 zj4DMe{QfFN97rL&1%AkY$)wVe6%DFdp&LZN!mvoLkOmUIGO{26eX2rmCB^n2tWdFl z5t4ga4E!L&1Ok{iF(SZ(M&XwQ4j1GYm{>BUkQEavqM|crU}*AiZ$+d9hnN+~p*Dam z!YDpgXq=IW3Gl(b0?r;jJ{)l9kd+At2adRZLkb;4F4k+Tsre z;=&n!lLr5}mSO}{s8Hiz4HuEA#(7g~MvSC@rXuhLOj$TkdU$bwm#wH&su^Lk;6sv$ z6Zb)Gf!gCLk0uiLAz)yuVE!Q}ACQQ}RL7GXE^rK}r7x_Y#kR0s&RdXVSZIRZG_2B9 zX!i(IUP6Q;Inoq&mZ*utbN+{gArW|8Fff8Hd!Mu9n`Z)%stD>s%nhot?oM49NKq<$ zcA?M>4t6y$up+69gBNb?nyt%IWv|g{YcW?sV;yTRBLoWvZ4U( zVua|)$3#c-_K_e}jnFdL3gi$x`Vv zLnnu@j}-4PQb=@QiGd7~ljwc$Fp^eMwkaAaVc>(~V&24uBoBAw50Ex?M@hjRg)hR& zRA32R<8<*955cbnGmjX;v%jjyBH0P3hpCLrkP{TbXRpk`iHjEl6Hp%;yzDqVG)s)X z3M5gz#{y=yW0WR_iRL&`uo4^1X!wtHI#69qDm=?J?;9Ei@d;U`YlHl4q>3^H~)YF6sGAoM5HFNz~qDyDULH8wVORfIVZez>5M zt&n;WxzBM5nn3^q#ZVjh798ppG#3>dTt6a~1ra_dSjsJ7Lk!8hdI;(dxVfI@D`04HEXG9QYP(Dcuw53{bH zpitn{j~x=tQ6eNb4e+#kn(&nL)bu^aM2?Yvd;pasQ1J9qIY_&=hW>@g1_WPwde(ev zgY8v|i-So9w-gT#i~u3%j>Q4QJBDKA2Fr6px0!*KSt5`H=I;4oTQrUd~Q2 z5q`_3g&-pIoJA8O`RlC!v`^p zXDTVcYzD2$Aaz1;dIH>J3<9_SgccAh!$=8CKr45dYYC?}$=Ml;8!G+#JY?+U^viLW zD=G-+hV8-2Gzl^QBz}lJm8U@zF(vu0hge6j-A8RXgdY+7rk8NQy~;G4Y0>E&*!4Yl zGTq*&s9Wojd-`;QIzM3vzd>Gg`>D;$;rHq_BLCm9`5&j}|3hs4Gwd+1a{TA#{9mF? z1|~+f|07ucW7tVqd1T-_<2mzeGx28AUUXxhu)`uGgbWcN!G=H-Ay7ePLExL`>RopWTq9P`_*Mxx35X3lH%$lgo5I zozBc;HkSqf0#H-{M>>wl8s%jX|RgaOQ?z#zz$0zmQOs0UerWLOxA zxDF?~xTP><76@7>lM0afc*PLp1I})+WfG#N1T@@CZiw-kAAF)2aaR~xsCLbdbATAa z6Y>ke7jlJI7yS<*1nJll*^oPqF)5z-evLu$g-$2b8F0`mL=oa`a>x+3ZKoV%&_XZX zc_+}Agt!;+3SXsdNLRxC->cNFy1^_zPTjyOoWve6Jbb81wP2`gfD85Nmcws%=X8WG z#~%$FwF}Y(3{E$F8@(_O%)@*{51gR?jOVb{2bQ`6)=M+Q)twMVOas(`*E0OsoFE^1 znmRFVsFNka6#@(l0pu6TLN!4E2lUw;swR7c>k-%HTF4`G<}Xf%oHQL+N@V>N$Y3`F zIDOKMlZG$kbdVCI{58IQGhQLLFS=KeUXYLOBYWWC0cM|0<_)=QGx(4MfntjkVaTB~1_TAO6La6h4j}<~+6ZJl= za@>zM5SdWM1IaEz0Q=?i6LZQo^;2qVW2e1cx7E;aF|mC+0q>K$T}C(fHaWu(F-34H zx@9%Aedno|K)85WD{W549`$hZ~2|?FyQHx8&w+Q%w99mmU+jhXF z+j8`_?}y{u;e@_<5zi3d3k6)_ZVf#;*LG~2^PW?0^>@yQIIE-r84xdWPc-!A@`#&`#KzG z6=sA6?7M=TeidH14$aY>Htvt>15xeBk5+X=pveWun0Qu{fAk43Fh0KHPt?sNaz@uq z+O}gxkA7_1%wt=WhS;rW??!k%{@xO#Pvjly#xd(&p9kB+2ki`a)FNcX&>J?_rhSgx z5c!aM>-vd_d!}QWPc?n(fp;x3~#@RQzz`r-*)*esUTGP$wnS7G2Ssy^5au6N`O&zDb47aYC{!1kTG zBR_YDmHh}yAN?KBP(G)f(Iu{4eg*fsDdg(N7l9-Go1laEQ-j`RfySjpZk^#WD8AB% zXv^o9%0B9WQ|?r7@5B)mLHBe71tqrF~-du zVlGIy!=C?8i%eL(x4c#ps)>Ywtw_`WVga6t?$(G=n&B8D~yJmIX+ zH}D`~h%5v+!tVsv&<)3HbOiQCZve0_o-w+SjX%x-XUN>vCLI{xTK3naN|uspg8@YUED%{)BDG_|9_7(C=UMU#4-q<_2F@YVSVC1)dIqB z|6-3ZHuuOn(u7NNXrDh2K7CE{8~i#e`fdK^)w(2t2mC#G7u%gIsP+5m|5P!@c03td zHUzONoEXss4f2isL#C!Y@Mum79I&qlCZRc7Qn${2s4<{ED$1FL32d&Lnv8-taV%)l z*a6y~DseWagc<9xu-fF|Dz>oTDunj~T#-2nZUcZ{bq5cKdet0y4|8(RH($$VX6g6v z>CnAFzi`KLJSm3t;E4Gy_XPAmsu}5Ln6l_;TiWJ4C+29b{Y4=Rsd?J0B8Fl=9!R=5O%Ay^9H*NCB6{;pa%k7 zStA};xZ>4TgH|!VAr5pyywJeeMIWWe)4anywcABTpTa&k_c*8BX5As5^7X^+0T%tm z1*p0Im=3)W|H6KN$JHY}5Lb7iIPj-VAWSv<@TYhYC>D1R zY5oZE)m)@<4wbqngHtCaRZPR0Bs8mR6!4Vu6x^upD8f_tk?iHahiqNOI*@lH?ee>e zxyN$PZcpi)-W<}q%Dd6K@ty5m`7aWA7x1#lCyt(6o?%|L&o$3B(nOL|NQ{$6Dak0= zCQ?)KQZkpxEmamfOtNgGUOfh+d%5Z-tY5f#9(Gae!R`Cn$GJze&vXxUZ+nNlzY*{H~}UKYV$8SNa(H9{MKsruMk|?%bmCE?x5S-1G08`p$jg zpGeQ7=iqttJ^cglRs$pn`~?w8#~$9wr$(C zH@0otHh!^f+qQG^f8TR=?#{(L{q)q-)YR0}e5Y!<`$M%LN5`Zp>k{ue%D%8+S=!c* z)M#k~X>lcUDld}i2W!Au-(v8MyGTjrJ*%gC%xk}q{-z%Y;Ou+E~g;Q3}5%ahs|LEh`GmDOs_v6F=o@#pA zy}RgeXbG>hjib5^p+~j~TK!b>ft`ZWDX}{MPhTjaGh>1%VRRzdgL>I61J(qL(f<^{ zMEnf~tw$^67iLHEVD?&hXf_T=ZqbZx@|wBByI!rYGZq|Apn>1N=BA0zOm5s*Q<#qj zzi~$GRTb^8Im7S3)!$56#@BSXh@or2%?K$nMO-4R6mP#^Ev`?H?hX?duxuap4Hg$@fj&%SP~8n$WWe&S^b2Pv>~(L)4Q^z} z^DZ|Vgls>^3)u!JJ^=iNVC@X@1yQA!xYTd-FKU%5)~kLtPxRL4%e?L1Y#(8MCQN| z(z`$Ip;kj`4}WJ7Xu=3N?jYPzrh2ar=G|9r6P_VNC1f!(;=w=;M>7<``3g;w1$QJyx2yu52WwV+%q}@=lY zmj#I^Qk{w!mPQ#R;8>KvofXzGMGwt`q>KGfc&>Bt(uFwUw9=*S%%C0>@TH5)nX*h~ zq}ns9W2NiUzNj||H%T{XX%bY#DvFrrxaT;`3m*PtXU0g%ig6Z}6lTrQn#VM%ddPUl zdnkJ-dZ2qhdY{HVggvA^6g@;a32;z+Qh!E#MSKbP7L}aZKEQltWf%F*I6iP$|A1E$ z3;XATcJlEIV;X3zsMd$F_10J6o`^l6WRtR}dtK_lxYAr3xGylDsIwV8S9!Qf;A@Xp z?k_->CUlJG>i&n_^MvJ@)dkoxvaM@b<-E{-a(%+%9N99(uZ3R~y+C+R{roLkCc9E{ z0l_)OW0=!Gw?h9zTnqP}jBc=9X}REh;^qYS=<(L)tcX9!J~46*ehvEQ_tei{u--AV zPfgWsuc|+Ra}H1S)0*W{iSu0eneG^O;xhNyxPjUPp}(H1%<-AmD$&Bm2aER?ofkrx zhccn~5N*$zd~rZq1ty%N#q@6$Vl2fXF{Eq+9S?FF;W5Sa#3dZ$Sb=4X+UPeFN|*;a zbvXqn4JOyJTj6zNO&UwCdNoJ>RLZYNG>2~jYz&_37cXR*!DJ6Q2GTx$tp~DC3Z|}F z>0$po!D)*PA-9D-N@vv0aXv{e9dl58>)3-Jj1IXeLAX|lsg=h6c%q+c&Zc4^V38m}$6)8U$5_FEVA?++VXgP@HHkmby;L@dm%*v%4fbQ?xfr zu9-ipFM0L!UULD0!Eo3<+QNsS=eDKo_C}B77(Wc2)>~xKC8=~SJOGiP?rC;sif>^lX&uJRQ#7r#nA| ze|~6rCMI=7L@{=@dzNtl-=dLrVU0r!pp4bgPS-kx%S3VV<*z|)#QQ<)p zDO_IRl4o$*8(*I(;Mk}pHmf?+*soQ>)k(@LzUyT;}I470CIm z9L#M2nL1FU{Q2YV8H0sQj;}P^!8EmBHgSgVr)!8f29D2R`X9Zblb?=3`oB4 z#bv4e{oYv8_=evRKPq~Q$4f&!(lDRS8BK+bda&ZQpTq_0Kvcu*Iu#Z0flKDxq-vik zs?K)n!B%InM?7b)_*EL0{b8ZSM*2z;5C+%wJOcLlHZ})XUDqa6hzhYM@Sy!@Qh~(2 ztGfQ-&TEuR&$_$0q)Lc3siRt52h0#e-n_0lE*Pry2}WUJ7q_v|0wD|(y6 zLPE^tg(Th~=b$9Rb_3RrGU5?bj0jEz#ZfZf_C^z> zLXq2apWiYLoaU^|R<;Gd)6)DRVQdJ?by`cv4dLalG8gr2kT2u4EFFjh9{JhNT$GGk ztdx3&`&r$T6Yxb03FKHy3AJ4MPg=olOifk5%$RM>Z7jnuhcwUDG}cxt5@zFFBP4aD z_%1EKO_^5nJ!LP@{xpBjaM5mT*@`*z@DovPbg8O{I<4EDkQCW@ERKK3s z=|HzB(NbM`slC~pC!ObAu?KAIs7CA+#Rf?^$;d z!+O7edr6N8x;q2g;jUk(U^Grs#~{fDm@q1#n4h8brjG>{2{DF0^Pd|N>5KE=R061_#MT1-em!b_^2k10og4@?fnL z&z=KO@z1$C5oweZTlWA8(r0!lJSDwB9p{ztD+V4ia{_F?8Y8m;k(`cZp^y**Zs0+k zdArcby`NqO<{dsZU0Vt~s}&Y?iGLUa#tH_4krnPePBAAgtoWeE=d-y}`=#=>Bi6LD zV%)XN|I~dMfpe^_3u-KB6JH9MM)-3}7Y4;>j_~tSR0x}pTFgmoCM7n~lgI}p5Nja%OMwkmU8+x)kR_bPMMh13q#w) ziQ*8nxpkbQ#t7w;e_4%k5xsi&R0vj+2oVv*F)g*8IgxXIy3zSl5Ooc3ROn13XNvp7 zgg|bupY~7Q0SFLr%wLWBM!-GroYEa)9K9XA<4W{$t&d|-432G&=8OFKZ{LvQ_5mBm ziue(a3{Jw-kvvXHmjC&V;Lgi*cV^G))2a+ zuFc3qGy<({VC}FlL z^p5HYkj?yq`WuXs@WzVX4j~AZ63(TS9GgcwP+!rV)iTf<(P}?Bi)jx7fFr=vBM*Wvf_)@@*sG6iIQ>{OrI8}96O@sh0&pD`a z)U*aa5Rdd_RSAf)lx>I~1j?{lU@b--+%B5li`G9o#=dO?wD9y*2uFk?{g+bVXpMva z1Rx)Q(=_s*_!p>5@PPX{DiMh?JN!t~PN_(=#le390325R7v!UH4oChI{{kHLvAF*P zRFvHzkw~;{=Y}W*pZ#NG7ahmv;D5b;f6RX#IvVF-g#SN5#9<$a3;0in*c@Vsz}X!@ zLa^C4Ms`KuT6XM+{@0`7*c|+LO}o_C{y-ekgTIh)$PWMO0s2Vqj6YY1BN%NsVh_U} z4f%f~!7x4Gzx*e1*rqtu9w-~Jhf;&b9p>u&Z~Rk_gYsYg6C!-Ddt>k-z9Vd>wi8qjS`Xk0OzI_@lds+^y|A01?`}#-&#F0%v?77OF-2?Be2t3 zK#v)_)_W{GmAPT5pP1!}$!tq)q%)ik6EOP5CAk=9%?tA8hrj5_NnfJ{9Cs6)H~X!rQV^Bbtc8TN+<6SK<6&|F z#y!0>A5R@ASY_R=r#coprS%4M6-7f^0q)!tG*tsO!@{hMCcj+*l%i`h+R0koStH4~ z-B~vZ?0E6(4SC^3S7a-|b54%u&xCnJ+z)_Z;-G5uu~NtLW`+MA{rJuBX&T@gqSz)pq!n z8k+c)SWlxLJs+6Mj0sFG=?y2wU;YBl4%hY&i+;+EWE{ZG1)Epe5l2Eg<~PbQc#LuT z^83J!@xw`AXe24^uFf8>vgx`I!d0r<%V;D7%9`}gcOmC`mHLY)9e1Xq80A~vz4_Ni zS1Z}X+kDQd?-QfxRHpg|J((jKUk~3GUpSHwMtPy_1e$b1U7vdz842 zk8j#$>Lz>St!x=UG$3-(N zUmI>kfC)(tp38hAq9Ig?84pgnc19x zXo0%+6XltXR^?r;bc5*xG>`+1-C)pqe_imx3;`fdu}fh#;Vsm_kIN6BTb6uiFIJ_Z=3F1Z^Z82P8(X+ok!@i~#E?8W$o5s3#>ULgd z{>|9BH|@WZ1PZUmAr7``0MVdvI$BRQl*je(AYaw8zu7F=_Q%-HL~I+_-i;WIyCRWIc2_A-PUYA+6#x|FbsI zT(x=X_NwAS^~I0Vjm{J=1^xkR{Egs)s6l0 zp2z5pibkc>R~dsF0OpX0XGk|+v4c=XD$}f-PTC{PUHiLWR>R}p6&bfaD@}g?DJkR# ztJ{Hhz>^R1h9qf%dTl@^@#ctt0v}qx=mIG$fn+ZE1B9`V;9I=nKcrq7E9#AxoNYz2 zAUhNp5PSoVC)P+==~_J32k{qwE}-L3oHxjVjKEnl6p=K}nJrszyvOpHX|w`f4tjqn z`K^G>j|RnZ;39C%UnM*6Yy{VW5ocj>4;^~2o}u2jln};wA$chPmWPfoJ7$jwRx^@m z{j?MtKG|gh>QUhu+7QIQXlMot9aJa~)=-;h>G#a zo_E1tvP$i3GiQMA)UgfFcHe0|`&_fdwz zuy@?qq{!>`!-sZ0EO1Kj&)kWhw){EWu+0b3>oA%acSN;X{G0~YUBGYl#m?z1m?Ft6pag0Qi_ zg6@_B1wwZI$*F1(afg^nL7bo8K7$L!{ObN&&f z104-V5c=l$^+z@LnHvQx>6*6}^Vg)tuglZFLRByUS9RY!7)!x`7r1|r$IYEU2vG

2^svq`#;l1 zA57ZAecMXI3R$BQ!#0F$y;Q2=AWxWTbCiC83RHY}V5Ua2ssE!tTDT(86z7(SVN^wZ zJZ9KA7!)q4kU?`9^P`Lzt}Nwn6{i?U@B&<4`TAnzu@9@?t!?EcvZmbGQpy7BNglGk znnvxi_y!v=2px};CXn=G6lBcP!AD@?cL)}lStAZ{e3Si8wrd@Ysu!Z zvG{DJoL3+pn4BOU?VftoZbxxC&AgVBQBc~VrEqVf9x5+I5f)IM%MmoX!3=qBK22C2 z0u7KSzFOB6Gnhnxs0YRW_f{4_7d!g z6JeMUi~Pb6?XC+ed`+NM*BE_i&}aLAVIwA~W4|TlA#KtTj-*VX6eY=9+?>T|F%wgB zWo*4GZ+()<0a4O4S&ZCI!*TSlh*nZoMpV}ywfURTyWhNiQ6Ez=V^x26=%PP3u*mpw zB?~A392x93!b;VT8vCNX9uXPO2M68q7#Kdcx&;emt z-(pEyn4Za?%G-CD<=?&Lf6O}L?L8g1l@43k2dpO=<`Avo&T@WY3L?MptAY)c|%S9KSaaa8DeeXon~ zF1-i7?O;m209bvs)n#ZEt2MB^Sv7pg!2tH-*CEg=}yyz zq&H&wX@FG=JKTf~zQf;gXgRbOx=9`NT^3=!Ucq7oCt)Fj9285d2~={mXM@tx(vnuh zlZB-m8ybokeJAQ>lozn#`MB`tLT?-laPfAXp?l`P485L1f?wFf}p=m?>5P@ ztS)7Sn{D>$xqX@+X{%RfPdumduG@BNdnzprdIomO+s@kB*B2$7TGmqr%uza!OzSlF z<|fRe8&?{IoO4RgSLG%{OT{ltntSOvpD_Cwirm*4acAh?rVwz~#$*;W5Jj?8Dp*Bs8Q1_jM{*Ir*uu*qVfWj&$%9YH{ z5cc{zWWiW>bBhdVe|)JGBUUR|G5$;q&`d&D ztoIE5%Ag`3%j3vn$)n|$`HM-%(*7dZ&*zB}nz|7r_76!vwPAO+cb~65AwT-H%}-tr zmt6oR0Nonjd;6cbZTHK_R9lzL#eAF2mg&7$v!l9V6npU_{zZ|V^YARmA@ZX}W-9}e zvbfNBq#sk{9CQjP_5B9?vnJ~*%W6`F6!hi&hGdf@?6~xJ71iE?g#QpWwyAWjN9NY( z6KHNWY@q_PymNwu&&9{U>s3-Y>Je(4-Ix{@NIfp`wtEYyVGpqw)9=lZG51&{@XeWca>mmn z-Y+NqB!{xlXk}bw!uX0fi&%?ji+Bg`O|d2MGisGRau_VMIk9ty4^MJ<&yHW3Y@%t$ zwB(}--kv6;6))|K?j;OZ)uH>dd*Mv;MTnH%tzA$mS(MMYH2=d4sA-Wrh>YF^m-3@) z>Lqne*I;c**}l10`&lvqT4FioQWE2Yv*!KZp`c?fSMLu~WZ$4gm0;UExBo?7YbGeO zq9<^U3NiRqLQX;0c==p1`PLlU!e!fwCQu0bU2;^kutBu%9{N<@OQX&z^-OjKETTE+*zed$8=%vQTj_ z$$z~c7E_u7Y$2G`p*IK0BO&{YH(atx6B%0iQGDeGM?m~wk8S!vDMRZzKS|JsQwcS^7SBhs(6dXN7cy zhDd_$>CbxbVPfxMC~_QQHKPQ9Y|5-6PhGwh_fEKw~QsCRJ2`euN;|I}c<7nOW zl?4Gr3n0(TBgAQ`&B*WoU#9X$CJKUv8Nv3ZFUsqmaVa>ud*3QH1dg6*HTa zNVtpV*kfpmG!wbUEW@vYK4hhs2uNBI+5&h6b5`kj2XhjG;Y|g;9ukvL(iM_>r&DkO z=)cQBfFX+m`TFo7sR{CakN)Oe3Z?_^=lXJUtQ*ER?F%7d*$!Y0H ziD`y!o@0}8G1ZwK-|Pr|zmG{)@luwBNsD`YyduukGjpJUxGRH$j+f$&RD+#%Vi*rV zX&%5-{!kOYoFj>OPd92>IT)tkD?m|7)LdqnhUe=#cYbe;p-zKkT(y3#y#SZA zS61r3hAw{YbzQyicXfdC)*f&uM6|>$;?PEL>rIIFw#~{5U)_ zraPddnY8wcE)=nCN3UT4?*I-^U#3V*wFbM}gS%V$fpqt`O=JLm*yVxb_DqgL2uzpd zyJJRmPWI=ACcGE1=1yw zIU9xUYE7x`d#o}gS81s-#{v<9o<=y3Fa^^Lxakq6hQRz+v<@sqk%pJ`hT*fZsx=;L6KQj$=DB8{MQv?R%@k5iE6veY}u#cB# zDxVsz`YMqe)lJMrY9+KDS(H>pb|Z5HDGIMi*LFfQNc4mIK3?)x{Oi`aqCdxk#^nV$ z>ZSM3{?2Tu@4t@vR^^QbYI2Y#gi~afdLe(rVOVQz`q)reA84>%Fi8URcEFT;u6)5I zK{!da!R_w-*gjXJQrIc1&qBPPti{g!{^hm~HIKGVSeT^=De0Ws)NxoLRyhMCtG#P^ z4P%RaEEkK-uh|W7rSPn5qG(z&7ck$>u3$A@E^%s!3f*>RrD?>nIq7~|a!NuYyQfe1 z>$Y>o#-NFvbHFiL^_gHXwc19-GBqA6&WA0lzKCT2=@0o%9LzCe<48i~ICk*@6g;Xf zt!&YN5j#Ihk3s034qctu!Vm=$6SXhm&o)Grz=Q}kc>Qnxti4je_wM`PsBCre#^5 zQLj4=&Iu{kCqMa+sD3q~FCl=9BPK-K2`(uVir$@m)D2SH#fox+?YLOITZg;)>;nJ} zxMrc(eVro_RM4SEg*$szA8%05-F*k}S88O&tr9Qma?|On12>&KG-V~9opaE$0=T5d z2;7-u?-FzxQj$L#L5Z+Vf#feZLkCmIZXehxaDTW4KF*W`~{T z7ATRZVR$ffO@n*J?6~d)ET(}R4+!J-3JAhij4JDv+{g-JrYlE>RzQ?pR!wX}QrkRk?DGoA>&%`~_dC7bcq9mF}DH{8@W~q?{3aU&b8;yzvMwCk zF^gFK4xM{j!0O>7@hu4sWD*J(6B=BPSGZf|IKypRvLEC3bwL1E^KQmKOs}=|hG9n) zG{}6~tM#}A!*PcMDPxR_5546|;SA$wir^BUVbF+tF=1L4!U{M_r;BC^N-!F}I~K9V zN=5jX;yx*_H+Scsn9%H5-6(N+O<(FTE2$#lNU2s^dqbwhx_riSY9LDb^A^X9ALLILbXTjk=gqvP82ys}o2@K^>9mOBkcGc|cN9ZgZ6l|i zN{FA%JR51Jtt?P$&2^&~Mi~t@F%fahL%iCp$w4CX46fB1f3-}#{wQe`L?^}})QPw$ zaeO^+NLv~OYE$PS|HknFGN{vvL1lm<(x)VNg=iE9mU*&`7do1Po~F{V{Zk9UxB71@ zQ= z1BI3Gh^Za+puC5P$nuGN=p*uK^xY1x12}OLlRG;Jh5~MSuC}HKlw}*_qqH@fY;eaAYzz*om6sfJLL&S# zwFT3~w?9S3@R28S%|?cn&{wP48v*vyR<*0wa>DBcqNQZz>c~W`n#BsvWw(LiDuEs8 z%3xjj5#fC(5lA_(fgO9W2;i?&u8ShCPGHr#opmjqRneJ!FJ5KKP|}|n`{7h-o-Rji z1JXh9Gw$lfHyw%mLEfgO`M2X0JeBRfxe=s1?A4a9@xi$fuPm2W%=V*`8>--jTh6@I zN-^qh-d{Z13D*lfqThJ^IwjlV3ww!KSQ_S7c-$K*r>!wIaxA6S^UUUOvqX3DlLVWt zcdld49`h=X>kqhx#*da9?$I7W^+GghL8v0DZQ+$t#xzQUD#MFIvupw2wBi+}nY(nc zK?#%1q19#dQNvmdLjVZ)<3dV~ZU1q93@T!>qL)te!7BijChKR!0r>XE>DA6PRhZ{z zqxt!gbVhSF=_Y?-5@6Z)OZDcEB+nY&dR?W*e4V9xOBq0M!J}0Jd4Kl)iQ6Ia99Ep{ zRVJs4k%g~H`MCHTMf=keC&Q?Pxs`33Bmaq5BJv%E8rBqkyzHZ?lsIZu>$yZG;5rqC~T>D>W6kcf(=&z^fjJZO*aPym14xjAr|a$)bblYwr{F`umDlmr;k{ z1}bGt@dCO+M>w&wgzS=pX{El!V3KdcU_EPJbi3VYBJi-a>CiAB2V672)ITQ?){>jx z(t6`oZ+}VDE!vyA+MB6*69)1ox2IP@=>G9_DgVXNtgL&%L%OqlNf>E?{^>)aO~EtE z$IYpzi^0XM5A8Dfr6Ha1ZM%wt5o50vNlghxRlq56%uE5tEl#Yla6f9ylm&axy=O>B zs;>G5$oDA!F{j(=HI0!Fsx;2BalgRwf_dr|vh;S}N}fULv=?D41+ZN(wx{y!Qq^oh06Yzxg4icl=xSe!>Cs zpVA)}-wfVMo-{KUqfjoLY$D0kWT<{2!Kxs0=8q)Nb`kWsS%XbJ+(qVD_b;a;C~mF% za28l5(w`BrD*a4#5i}VzPKCyG#wCZjLF!UFMDex1B+kXS^9Iamw4DD_M=4>bjy!E7x{b6K;e=GyLKfP+gPvlCmI%uWE({P;~f~PDO#^Q)=>0 z!vyr|vhBo4B-S(A84nV3qT=CX;w7fkXoQ&52e(?yv)Rg0gekc4GIvLg7qY$gNMdle z2@ZYb;<)U>#C(zzWQ%UMN8AP0D+vg`G=CZ5vkVfGiiB4`*cxf3uZHa}eQ*}8uC-!q zk82oLKewE`c3MR`Bs&ZHFZvBWF>a&k3b|NNBn`*&m0e;cuf?ZYUiY)pN$eg=OPBN%ezY^~z44Ks&2w6Ods$1+}vBUW8Kyj<_aM(R+w`S`%JgMS?q0VnJ;E zYrP-amDGIiCcT>~wYUUiWxLP)R8A<}b(poGNl}({xrziFKVE#ApLUGl zoZ(c&22lls4px;wiAyyTe)&S?2767zvJ^U#XwguK&OXLxizPaWh`CK$&;Y}B-Oh|A z#<2T*|CM;)9h62)(1)FW_g?m4aYoX|RvBCq z>&4b7a)o2vq+*YNS=jQf19J94j-r&saRFOp#jzu&3gME@CJEH9B$dCq%EcTqg zRpCIWVz5|E#QNimr|Pc+VRm!2jYS8NJmqx?<#9=+rDFGG8!Ngp+Tb zemHLO|IqKsU?qO)$#^BWZE}4YzEXAOBs{LKw9v=5=j*)+!6~xn5(W6x&fxISD5|*D|#DUX7N4i@uUf6-9jN#sWr`~ zQLmI=axa3-`na$?ep@v7bP7`V#&S|9`KV~pX~ehBBKZ~c9xQQLK9Y&@Io4voU8Q8C zoFfNnhW4ax4kNG|NOM)c0Q}BRHAar|j0~pp?VxHARf*x|g1z|JBz! z3HHnLOViVZa3yiFNHHSRzAa(6+YP_|1nX*JTSK`V2pN>8c696m9)?wei&4g;d8lF# zOJywzN6&*LW^_v?23LK!duq3_f6WD&%FYtqJ~8T6`TbcGbXA4rtsMVa>Z3yPZF4Tx z{HyP~1C39Pi#>`R{uD8fqV#Y&@6va-eCIo)HWyRv5T39R2JZD8qUxWZy^tAdXa0eZ z8S{&hGor?Cs>gN0#^f$ylOrdTNR|T;Gu%kl4eY$fF`GlDK$c$EV8_dE??Hq88whDW zmM^qCiocTA5H$SYk|_|Ae3lf^GYlux%R*+Lr?ZF}panGZgpFAF1=c?&?y|;2)Q}X# zgcm*`KQYR{%X5x3t%#ZNFb_P`3uNIFT@U&2r7=VrGpRi%VhT{>L~M+F)bO+jG|_7q zhSUIf4EiYl_&Qgl#_;P1Z@s{wuG(j$Myzw^CwjzmzkZ*rT;{k>*!kW6ukEl=<>c%L zV$zvedq!T(wyq{(1_tSp3Zx2|!#r|Ei%b*LddIhR{>vxO1i{u#HOd1>VKdt48vkg3M@2to9%d`xggcms8Lg~ajduQbIK<>T9f}iF3()WpL=?+TtLO}-7H_mE5!2al zo#2eRL1qgP6aF!%OaT%hu!6P5 z;V<8iecmSe+PVX~uN?dYUq^f@fn%smBqQ{ow|WO@Kf}qcBKjBDRnRwY1I~h!`i3sBnnfOe3Dm4N#)nq~?QQnB$rj!t2_E3$`}wFL&g1s`}@4 z(74fIl|3RCYbdiNDlh6QSS65CHxjhXSLrU}4u_%sZXfU=*^dCEOrSqm(=>M4TCmP zX4}rRo-5mJbk1ha#p;Mu9SAXfRf>brk^Z3mywuG2u90S4dgi;U!%)_XP;_bA-Qmq` z?B86oJQ8LGCPr{qg8DR5Xdl){Em1mDR%Yd)dDivLA1$qGm+{)?6okiWmAH6mWi(@V zvw_A`MxDn5ij5J+MLnL_m7^a_uZ~Egs|X! zLi#I#N~jnPm}wB!L}(w@mjSQOTXuxwFWxHcO`8oNlr2h6!6L02kjOwGehHU^couQQ z&{$Fui-cUGfCVZh%js&-I_|3pHWPPXJWyKqUjwkRecHPJEV~`Kp4L_yixg0+mN^~2 zrd_9S-boonY>}D!O7=s^_}9$rz}g{YVhz3Eb`dvo43l1B?7ac!qH1jeNoPBlGSNgI zS$AhfwLGoUIgmuln|_wF)yrtht5gC$DUY>SId2<`p?=1J#8R6t^G@Gjii%s>|GKSy z{`%op8LBc|*MFDE5iArXb9Y>!ay*#+44ud^C1_}vXLu6Whgv^XD|#wFHGB?!M# z;Zt~a)e|1BJSmM|6iwk?9ygO7Y}PSTvsLIhurkfb?OhiSjY^tsbGSejWE24vO`WBk zetx;hjt&&zUJ}FNQTApS-FTI$*39T^-MG*kuzr|%^CTAMj5%&=XY6rDIrYtK+ORI2 zuG3k_lw0*KHHt{~Dt<0&t%|;>ezN8Kab8Xd%#*C$9h2AM-Z(G}`QDx0W-?W9FCAoF z`&67wLo{XKLk0e%(wP!J%G*CLi}Q8{JFUN%%Q@P``5x8z0|`rIeJgPwvSp5OZcLuH zA7D^DZyinTL_J^iRPTtqD-Zs%nr#sa3R~T;v?89NaQwG836vG@8IT2_l@akfXYsM# zOEUz|sRH6Q)rHLlAEUXjF*GIHxQ6R4^dMb%9#l%B;!g!nPDB$9rPvQ*?vdG zw?ForXXkg7ZR7Zv;`T^UD%e7jLE9?0Yu9LO?Fuorm^z)EgF*7hMQ=gDTNxZ~`Y&C@8-ZqW>je!@b2ULY&xAo? z&y`+0k&U_SRt+g+2yIBBMw&L92`j8*Zrl|iN<6id$y}UDCi7rteQSGrZQyWM45zTH zb>@D6(>^b=o|Te^sZA9VvJQHeIJLIglu$)x`X1I^uSrT6gtDwI4YNW>!GvLXDuO&t zMFbCK>U;z;XGBZSmS954%#5NkT)8llaS1YQSU|uBlSBYqo&&vmBB}JJxv;v{!eFoX zT}c4^g(T8^$*ahr2DOxEBhiH(2?xospfb>Ni}IAw3Qw!Zr7fC|HbQ_P(!DUZ5U!9iqh?Vvv zqL*Gd--)uipiC#p$O=>bV{oF^SS(O+>)cDGS~IBAvxVQ>0RnR$f|rp!rRM}EKPiUg zEZ2{FsaovNUkSmrno@NHxY&<4I828VN$MboIZfZZ0IWjRV!4~q{Nq6MFQO5C^b=nA zw5+ocEXfHp=_+yjJmmH_DJk0ObYT~1&UUc38zVRjD#CCn@abaF3%|#0T54wa{i@YK zaoIu{CD7{0g_ncF|6%MLgENWSE+5;r%}FvbCw4N)#GE7(+qq-g<{jI%ZQHiZ&2DYg z?z>Ob*88cezjRlf?)vLG*ZFzayZ+$j7DPW9+8r3#nOQlA<#Khlb94kaa%gmQd3nc? z;L4TLvQXe$_Sj6hnO^+5NNq_JtJUX+@D7`IB*iMEm<;&=cQ(pr{0@rggF%j8Jl)MU ze6UU@_DmR>tS>{+ZCLonWBZ=pkj4npSCMoh?AB0a-!}?3x2rxJ&E_yn3I~Ug7!7V* zK?Zr|$6PiNFA~3Pm+%#~s+BszW zb2Ft!V86$4^ZM!c)Rm)r3CAid{F4}mlH7Y4^OQ2b1(r^!WPMy+m^Q|^%PsLqwNEoo zjv*Q@?aKt+tR^m@!^#cv2vO8S9ojQ$bc_OQmML+o`dj1Drb8oJR;P#R7>xdeQnjfs zJ*8^e6__E7hw~)Y?A$>D5B-Of0zSUEk#;c+=_1N~EU3KAP4qL3Oqm zfTmDUPJ>1B3kOmP+c*r4Uu22Z^alUWccP_cn9NASj;-+pzlZ<1-EN&@#*4PGc}5G6 z;vp23@lzp4xFbeKc_pMiSp|Ghvx61E?l|0O?9C-ys^ z>jy&6)dxoBIg_-!{HidFBd)LZd3P>|6bI5W-sJCNHBAg~0~nH3{bV5;K=tKHPXRvGBecfI?TGWoxs5|-9wNA3NrZ|d5=(`a&JB5~Cf*%1!J_e#x6h=8q zil9p)db2(kto7SH0{rsHNfe!}P;W3+5 z8Grd#5I=rBlaT!;x8G|#ibBV1@+peTzq(xF*(%Sw0P~UO zEViNVQyr4+BM>jz_&27msza*W;hREvr(oR`b$|Voo5{*E7vi2OqG2v7#>%B?jAWYo z?hgnzZ7st&^G}#!)a)ZJ!dg-?-$_=+{KCC7nR9!GT?gi1jZ_&~=b1l3IBtkzS3{Sq zdO|l;JLqG#?q_Zw6097s>3<>e0KM_1{Cy}qz&1Qd-#zg>(41$)MvszrM0i^^-g2&* zeDF?Yeu(9d}>!Twu%F9d_Sv)dv=SX z*75=?29E&`qjHialzyb;QJB@XS+Vdw_8k}tDVnohC(-)n@HW*&s{-+eYkP#+--(ozW)zgr^}qdaG~5i{?wf%F@P%r~a%{hV=016e zI$E-B#I&b3Qri?18c{EPV%@uwEJ1t35-xoOMdfgc_uUNS?R4t0ij=Ze$k5t{6Q8z!D-pVjgy9GESJ6Sx8q*mW$IlI*&I++h(iNgTn#d9+{D{L+uNwQ2tW=dO`W?p93K*b24TAYGq#@^+Z_@1Mb7 zNSXJ>0}r{tS&C?FYG1i{#l==)3+d>(uVjoulA!*LfXn<&a@EzgLAX%RsmX7nn(|k$ zw@cCI>Ha6B)m9j39lulL-rC3Yo%Q;Y{Bb%Y@1n$w+~F0)=LX1@we9Vb#IuWiOXx{E zbp!O970aV*%axzWu~@gaFt#z(i}dSdC#e$|zP;v&JuGDM@86#efA$4J9Lhj>9u1*t zNj;4>0v}vTT2V~$O-Ra^T@0}(%Ksc`H@pGG{m;!{tLqS)lQ897_Hi$gw(;`ED4O%K zL2u1jWU3EZ7ol7dXX#Bs24BvfQ*LO-K8@NJzx!Ci?&60hhF8>Ie@||#4+$)`@P=`m zu6x2BU3OD%qFcr^cUs@!L*UKDoQ<@r60u4OiW)HurQEO$FR<0ymE? zKGrj3xY(5IK%jLf=eGg8iS`|GF0D<9_}lWKZIyj@pl_*ig;&=SW+k~(*}M*ibh-_d z`$$InjY8I){zpdyMF#747}^rLkNmGl`}m+sLR;qRm_lD>NM4bTRUwD zXJLXl`)We++b?w6--Hk3rx7&habs6<+rMf?V<+w*c9hI{%zK^UU~eoHqYEbpOIN1w zo3Ly4yeLBt`wN<8{t(A%-O1Ar=RaO^eCQH7pv|ZaG#MYpQq)N|kUgpNNi8e!`ZMWU7tnp`_lvIVBJJu}Om0kT-!z+ihy^R|mu z^o`~+L1s5zVn-}K;w}D%^E3o9WwdllVXMM1oW{e?mT9x_*s42Mt(Fzzew5Zu)W4{} ze3N%Sma*Una!qD@rF+2wcT;hXx9#~Z-i-S;LA*@X8p}Bia{09TV!fnX4|j&#dYL^0 z5)GE?1MXu2!VJ<0=_+(9qjCIElWu0;=neTw-V%J`=HH`%8)j8>><&PXIWrjO|XiRsXUgzeobWn`v%ejdylXwL%X-yi%L|s&Z+vG*M>kW~2Of}B220fzD^BO|=KyE$ zH?p+>Ya{S;DrdH~z>U$)e&;&o@9Ze%6{~Y+XXv*0je*YI&e6`nsU!Lq$ai69&i5)4 zo7%o}#z&4fn@9IYB=_VGU&LDSa~k*Pj;W}cJw|oMj zOSc}n-Tdt_3Wr5EWCDR3Lqz|!46xdOp2;8VHrr>#@vSsMR66o%iZKA&+@Ez=2sJ?5 zbI?bUHz@ZI{_)3Jn_Bl}@^hPIf!xzlIXQgJa=j*JIm5nz2d4)1GBp)F#~n!esVdEy zceW$OvT}8G0hh;ehjuoZ@FF={o{DM`ZzWypsI`Gq@dKyLSLeSI)=k=V9f*kV2X$)D zzoy9QwZQUUXZVs#)hrEuny_>64%w=%Wh~&zS4fuUC&G`3WvZ!9Eq))!TdftUZa2*X z)&d@qdY9w1*|Ru(}b7TTT4A7Zs7_v!lz2-K2+xl7h6~3YHK;f^6(1AMoVh!6Pf->15c=G zs@}W1C*Hz~^E`crt{%>k8#T3qr$MUlah`M8|M{{{X<1C1eNQ&R9Od>lR+$am@c%40 zO%P0z+*M=M5C}-Qt~}4KO~Wn~?Ts-BNkd0b*vxtXc+}5O$f$Z6srLtk9}Dxo&}H7r zWgiU1dHcUhE48PMYn)r`Njh#z80`xLJK|7no{~nC78-cJ0AK^E-ofhXQRijeb=Sye zy}Na5ydI!#OOc>9Ru!2|NvdV7SdtBw{hCcSzbT&|oFV>>{Qb2EmI3qtq7R4tlqViv`ir4yVhVwv=5b;NeharZDf7B z17%9&5@<2eaMNfF6B>RtTH`LU;AnC*%_?RyuOlERn7`mUHcekvpmNy32@=g>FcATS zPWA5l4Q7kS8exV9ipk|JSh%jWALpDFs^X@PHWHOoIJ{VOoPnC14 zztTh9wp*k_Q$#Omkg-ggqNY{h88&*HDy3^idz$NKZ_=MauR`B{lzbvp(cB`PYqdF( zC@nE%T8i~P6%lon^p|$EOwgGlo2n>vHO&ZaTUqOb{v7VB=iwM(uekr2OvC7inS9tf zDqV$;@f+}<@~^-+yWH_g-s(zab<-zlFH7gtd4ef(T%P%$yu3cnEY?<|qzJ@Ry^LYH zaJqhWGplx%Ei5@f)q07p$Rg3iJdt1XUk%krP*?dM@*i&pK(Ok-3a|SJ+Eu2%6YD>> zH>_1`HanU1&U-KG|GfzmRy00G)Ur;bwI(e#mN&{T+1lt@)ugFvT2!pJFQ&~qN}UPF zwKY^2U3gi!aDcY;R({|$s8{N|;aV$OH0sQ@>hUTw>FV8j&i<@jY}E6XNndbGI?!8N zH-7*w7&9gfU)HVCU};tB|C05ZC#>$KVyG7E*E_n}Z94ngPJQv6m*>?ASFQ8AQcfcq zLR--BijPIe*YIP12-ZD>ng524-7HvO1%4ZNx@T?6*>jeB-ROes>8mS&fK`BTApW|v zu4YI1Df4+43h|-?1RwgicWMQ8oQG5iKo)IdWcVrD!pZm0a{cd{Yj2R0_b=;qV3&M` zy&1sCH(kO0ccnNJgUIX=VRl%B&-Erk?Yt4Ubixc!8y8#o1~*b@ zmwz|SW`NB+q^x+csk|3AL8tErGF}z?A6a0=rxCRKFpY1WY!j^}eg*hr1B|ifd7h2T z1@~^D`*+2{lfoYgnen0mdG~Uu2qFXW0P6_Bbp56JilP&hLVQ%Y;^QHYIGd2?fXe4G7o@N!1~Rk)aPwA+631XN(+m6eA&u{-dJ$`1q*m$jg%E9D>v9mx zz$w=E8Nf_rAs!|NL@i73nqP&=oA9-*LbkS9Uj8;3sn?Yj*zG~p)aaI5l$Wf1*G*M* z+FL5EJvV}|Vu4#|LJ>;tMVD+w?W}~crd$T1d%U8}%ChPpn8l%FW7Ry)A%r^t2@iqp z5j0@(Bp1#Gd*b!HR13#sFoGhi@H)TyjyYn$W}s?(HvJ)-ha-ep1x5*1^g00ZC_%9Q ze($JZB3+_n&e%S=P8qTE=>!>E1onrtZ$%8`8B35Mq1$p2u;wew{yIpU`eA@RpcyN# zC?0D`Hj@X94v#sgW_L$D+=b#wS)2OFR^s?ki zJxhJln7?cgzSfE9r5@aF_KJq~o40poIdqJcoDBrNqOw=4tcjrzh_ac-EQVCAV6l&Q zgkW6b16G0P3ILaCB{X`%56rkYVAte1o3JNg5OGAPA~pv`MB;Kx$kPsOPy|GF^IU&% zplBT|eyF^y{*>60_u_HLU*m{qn+B;@LRcz;UO{Sl;#9g^PDmcD=e)n%U!UBe&7qw& z_@mWJ;)w!8VrFRKDDVflLy{@oulvFbs1e})7R3hV@!Vw+PAx9OuLWgCY*z@dmPEgS zQH1Iceai~gk8;B-N!1FZv5!h*SWHdfCLu8hqR77EHki=>HIO(Qn10zCkMl?zv1o=egR z=BWI^&W}X@oPjOEE$>?09v>}a*OVKUYrM{Ay_DL=OL)j~g z>2*oDM0G35EEH?3B5sLd#!W;fl6qJP4-7T0mTjVD?{;$zUG%?j3M-LpGmZTCK9}2T0{JBZa z#umumJW41U%-J&@F!;xkcqn&W%Wrf_U3Dm8e!B8Xaw+AM``a_84Dif$=6PEWehO2> z(AhaT&A&LU{vNSv6reoU%~Y~>CO0$1&98e>;Y&07$N$Vm8HvhB@{6(3v(oV*JJ^X& zD1VUgp1>_&lbf6kdvWikuj>e#PokHK7do)6pssP$9f_-oKPKw1xa*S37ZtpDTXK3S zj-Pm$|7pS1)6f7d`H*(9?q{vZe}ZfC&pSOIsZ>fB>=ev*8k*0dn{pLgvB9*b=GdMQ zy|cUhlsu~|+ORd>CT6cN^W3AiGO^eXxTnn|?i9(VFODAh=%4%e#hWw!I&iw8m`UV# zIM_IyxxlHiEemuzL1$0m-pbDc43*U+)w!JB(~`JLtG%r~r-l~OE)kv(EwB~VsZnHP zP7CS523t#D6CdmhanH5=tzzxd;2!)6Lx_&BBV2r)*8~EnZ+P+0PtCcM&4!wPIe7u= zup9ss+pg;+6dzHtb*BP3n}6*YuY$sWxW=dSbHY&^N|)P`=(4OtWpZx6DAATvJn)}$IS>CR49pMr zkR~QI=Tj#IO^lrm++I7#`MnKI6tYyk6>(=W%U=@`wo`$YEuBnPKRid5N5(gQdM;;6A57ujQ%IKSTXBkz zP5dyIzOZH)a+J|_3a@kw8&4O0&7AmiW?U#ZbR_I@`?u8eA|iTHipwH+Rt0*}&UmA7 zI@sn}AuC<*vnk5TtlUedL(1xlI3}yd9?qLNiI$RCOLi`$ymZ{R@3-cImT(8Yt9dR}D^;!pA+x zZfS`sp21(z3VLM4(`vsqAX)%P^vJ$5JBRUS4eZ$E_fkJ^29Y+-P5C`NuaN>NW*jWCHZs(pZ9vBOs$4?ol7i}xLbt{=06V{`JoM5mc zK^TzvaB(2MiUqnt-F|`UC}v<}aDHKs@Yw+%cwv5FG-Q5o9E#KMk2HYTGxm|ox2zR8 z$!oxtCvfZS$diceB}>3$(&O!EoB|XK1g{tx2}<>`6zBH+&_W!H&yC2ZO>yNc?KVIk ze-X$HPcnkf={Y`r`XoRIn!O69f60D1$lW^_Y3Kzy{E8*yywsn%q_E|A`m_Q9rpw&& z`fAoByFNoT1$j&jO>^W_9+R`^FCm+tdg$_e`Xn*A2IIuAs5t=AzUy0Px^ zM-3E-&&MdXFF(HL*U;Nk;3tQs)_c0P?^yR9?N>Gn!`6yJ8=3ukG$vXwUx*516!k8M zY{67h1>|t;9Bf1I2-32U!%> zeVaiz#Y4(MipK+zUJ}JQp8T3Uqc3aX8p;C#Is{Z8z$PS?pvQy&9)0_lK|u5`Z9jrTCF%p?pqj)dQNfe;eB##Dd<(AmcFh^f zvte^-D5fEj#B)AKnhcro8Hv1AKF|Dhw{*Le@%^!5H$fA?6CjkJj zK$Jc+4B#D)?Fy3DzKi%=^uQZ_%pO#)Y=ZRSCem{By9&b7&glY|Jmk%vsP~%SVNwh; zF(&$O?ST1VyJwYzd05vW^xT9+5{{IJO5xS0Z_a{`1x~Pwq>sTlA+lktxy&#Tj<{~Oz!LS&=*Sti(15{M)4M`UyX&ndgY&oB}plMJUMHmRN*`7GSE z&NaRg1jV6PQ0%&e@CM{0yk2smcBqrkp=X0%nH>3MKpfSQe>{BQYBlCY-B9p7t zy$ae^oOd6g0{@|}(=1j=;C8@g`8*U$p%fFAM0$W4ZsYY2OYI{f+IK65gleJ)`8m2m zPoYajw$&tW{NpdD-iqHEwxQWl9+9{Aeb0nA=IYl3AC<#}7{;}H;e82h4`S;KRCh(V zdSH1)yy`(^4IJ;mB=YTO9yHiL5F7RdJj1qlGH2QJkQWPHQZ~4Dh8^kN!f<=St`Vy1 zXY^iYklcdDlwFqxd3T%ayX0d7G;ow1q?bX00#lSUny zgdmz~vK;})AFcoHDI5jW4fSGA4aLN@KOmI+IM@fhg)kFhikQZG#o`wgJc1drgPR=M z=wUoevRAOZx-s_x5sV|AsM;kyBt8J{Hr=^DE4*A$v{Bedb|!1blos45HPr_@Ry&qD zwmK#`M%^Qwbj-5P78Oj%G)ZqIX2oPBevUPCKQRBIPk~I{#sm{iilM(p2@6Ea5l&&| zW8|Z^ypiM+j~>{2fZ)T;BF*}pwUBkxK2QH(-T_s$a(3O?LA#m#WPXKn1ws69;z492 zcdGX0-SgDXBGH> zuVS`ZHq#HLA1pr}v__g*+N2G!ZMfD=;g-H7zuU8s*J0S9qG7+|EW9p|yZ!o|R6@XGx1ei|7@=dauo- z1=Uq&y|t>HMO%tlyigKah=&v}^iM4*f1~LAlzk6`Jt8wn&0rkhiw%p{rpnbSNik%eJ|+a}9=Grl zQiNb2&elBS-V#9ERn<{nI~u3SU56!N$!#bc2m2Fj51jW$XKzRwLLZBl8{nPy@AxP`{Ly|;`-nRdUcvxS+( z*W&5o?~t35iaM<;Y?jVW@V#J^Js{~|xLCxJ1Pjp%FI;bF2&`RuvP^VFuIXH_W`a-R zF{UhOohGNgXKit_|IWOvG#768o#NOUn-vJHy3zX*qbn&_O~oRJ9#kKX-%*)G(HYO*q$<8mXfD3TTaHumTSTO z-lVQ|Ww--IPqZe_Ki#j#KZ=07AJf>#o;Um0G9aC!7R8qJF3r_*BAqIMSzLM=a!`EKEY zTtTlLs>z^*AC3DbZKnR3Ua#?*c~5_WdxAadKg(HQBl#r#B z7^|JY5D!9pSj0jo^0~NBZC^>_SVHaa!Fzi4Rl#q4)sbw0cBk-Z_05&2AA0Y2H}IM` z=y;!{d0*(+8usbpR!zSHF50amE?pCz9lgEVNEF>bCO62Qw1$24^yJt8kQuhCf`hj$ znz60k&|ES0T8dbNH6r$#@eRZ7uHhL(!aa%Gk&3oW>eth!Rlne{r(HJuu%1cUeHM)f zR`YIfBxHq{w^3HF`;O;`d~ForHYJEZa0bnn@_^N7QGMThAkDO^ZRm)`qHd#lfw`H_ zcSQbE++)Ovesu$mS1O0;y%yMy)AZk2);*Ft&Jho~;!`G_&0V(C9>{{ZSDz>V&ji~5 zYBW<{)qBjv+^HI*RTA0!W6Uv9Ki%=X;{m=Xo;%oF66str^Lsy_U--4ceIRGtH~r_B zkWYal$E7B)T}W4SW7TKIjeG^Es}Hs|g@KfJS{VS_5l(eqzBdiMB3CaOOwU0OFT5KXPl19BV1?e%J3jM<^rp5Z zLqWdDt$N_#4%l4%d&_kC_!NTtK_Cblledopa3hXE9TWmMuQg|`GhJeS?_%UGQCtGDjy8|vZGrYc2R-|v=BsAoOOs3NSNM;_Pk-I) zu9@(A;Ju6dDCM;GjG!sJ)j>i=m~tK^#k0ObZuHI1u2}CN?=WE`VMz+J(ku5E=olV|3as^_jdlNX2g zrFYKvrgxY3r+4J{Xb-k8^e;B=tgpbY#4qUAWs?{9cbj*yPvHC2dzU``&)ESAWanO) zYbfqu)O)1+uPeVLeNuq^*7V2w#j#;kpm9Q9S_#MG}3M`Xcdz3#0D9&1u>;w~8W4YW_H`@W>xvFEp!gJa_I!=>Ed>L|VF ztQC=CV8xEj9`PRW5$CnSH1n|;AjnIqE7!Zof3eh*@f#=Z{-iemJi` z!=hIKf4Ys%o6UW37#^168&9RW@5ym)v0g2%S$7$FePp_9cux7bV{bN@-ozD*UFvlj z9DtV*X@5-jSu6_FT7Tq4^LOrqQUd{iJ+>)IuP zs`gf!FN>-!yX^^on-0(8%)Tokx2t28|H%i620kC}DF}R=@0Uz3+dxXyE3kDpxh=5EE`8-uxO#9S7y$%UNngvy*@}@BJ?AM@M3u;Amtu_c85hwGC zOCiH%a2!r&$7boiy#8THMzm0(3{g-S_pqtrBWbNcB@fnu0R zc;%g`YatB*A&}-3=QWXj+Wz6@w$F5Q_~)H)jeby*dfdhyecitn5;1Rx6yyM!;Iza? z_-i1Yx`?aUTo9uJYdV9qMG~{sw))~L_g&^7 z?|RbXmETRSGO5_vi?}`XPIla^?@F&3`Kn+u>(czR#QC$)veY-5hL-_eRq$EbI-ufu zRuIzPxdi9R*_vJ0>dVlr1$QpBtSkn!@vDk!n3qGv!|2rQlnO7JCp9Ao$Jf7R%n}bK zucx<%B1r7rC0!6u5HV}7GL{W=NQcEj?)=xrPqEzgm|tWycaj&ffmIZVIiEHe2utW` zAVn}K?FsFopHz+c0w5Rp39|>GE!BVwEQz5+hx>ceEKCq2f!I@qH%Kl<#Yr=IhIIz0 zc6Rpk?~j&0-`!FyS$awHK3`>}S|oco^8!~j@|f*0U*6KShJnIYQpOD*9*r$*9z?6j z0%kcIqGz6`m!SU;x$hC`zeq_VN)7Rf4n}pAQid5cD9G_rbgF0SUVavb`3GfzXG(as zvbAYQGzvIfbLU~Ix+A5&Lg~|R@aLPp$y?BfNQ?*kL>cow&O*7ka<8bm%HCNJKG7J^FlKp-dalWQs&rM$_h5j8Jq~TUMcRkWtm=GHCX86` zYL}=`tf}4PDyy9DRjx4of_1eErmcT7KHBOW#v$g+9BNansp*f*5_&JWg}E|yVMCCH zZ{AxToMII%GhSlSB29i68MZ!KD{Cz{F1j3Mi{TqQlQJ3IV7on7kKxz)nz zSFo1_VzCxq%h_q8EF!jy|ty%T`#U$El`E3(fau^JtmFJ!cxV>{|p!I;kUmf?c){Es`rn)@( zEgv5dTa`0A6+uI3+eN44PNcP^zi&J>o`fiNbPCdzrRsk@^A^!8dW2Ks%62OK2{Lkd zl92Z*EynGx=*#wkcIUMQ9Kksu<_-*aG-aVfPCRN{I@PmClT1Pmlxno`=<9ygDW75~ zGs?8|tT)8?G5R+P7zo6aK?oj#S5Mx4YTK|}4e3xGF6p-+-gR{KE#FUxCF)rliLF|LMI>1`1)n+xGY(JVBgszG<^ms#=VUSK3px$G!*=&l)t z>LTd-=!Ll?VBCef?MV4#ZWwl?tBa_kE<(*h=&^t`I1-q9nPI4E16&R)aYZ;I$_T8L zILf|b!3+T?y+Q#z5U`W+K}!WYt+UriX4*UyyV^vp7_uqwHS2Q8KJX6MI_2oz@o`*oA)ua?coH>-6tZ2Vx8jjiFD{L=%eWZDWc-S_PoSHNNk5irMSG8SkP|FIfadjG}J9v8z~GKajT>TYetpb+D5C*f=nyWUYm94AxZL35T>5M6sG|>8ralMS zESVU*!ipZxUpzeC;-ByD2QSe_HWORP%xfY;+ZqyQ!qV01)7Lon_7bU4w?EK+?jy5! z{*tvpnTtu$6p4yWt0QN(iJmVfEYaXsQs!5h^Ug0c&!=+8FARn!Se21==%1%Agf&vngTinhfw>-z-IJO{n##O%$fC;n z*?RT(bA+XhUK1a6B20Zx=8|FR;j-RTh9r#=pu1xSg(KbQ69FM&Rx46FQyWv>#vqc! zB$Grr;=$T9PQf|sbj#JES(z|)pv28wWFM2BMrI=XtFg&4t~OIG=9b}Lpf;*D!Ac~r zpiopk2l*#XOK1I6Lx<#6(8pf(m3q$3ZbK(kt|&Uy%>x-?Q0KIZB(s)Cll~T!hLNqv z-)b9wgYA%0tAKSq{{|?49=b2-f*eI}8dg7>jU6^hT%*2*2SZiK-=#k`dn2{~*!5d# z=2gvUW?ZUDpE;#;7!Y-NEPEy~wWWmnL_Di1td|oM=VJQcVgu4TL%0vMFLMLyvuh2o zF9vJVPIX2Bn(Evs4fJR9=1h^`plkHbzQO-?xloq5P?+$KayOQ_1jlmsw0F7GlI_+2 z06O|cH@*u)K$s!Gnb5&=iDfC=urfMwC)bcKb8vXlM}lB4HuGf;XUUso@XKb%vG*`Q zx97_?MrS(MqrSp}=x+SXBM}|t6fbF=Yh+PTP!+}b7xxRzVLkNeU{relEem?uutX;; zCgMRS++QuAoA;$n%24auGhJg(`zYzc+F>BopP!Gg4<+}JCB-XV3n}y1t?Smm-!2*p zqFy-TBmc%1)Vk|D^md(i`8w$hTt0xcEy17bss&b}T~XTe*A@@}7P9GS4%bSgLlLyG zuS!n-%}g+$wzLczwvl^rBJXo6=9rWZ(#ZICx6Y2uj;UiyXV8KaM`8CJe~dYF0n)c1Q6Dd~UtgBA^| zYaKCuZHE@^!=M#Kz-Wtg)_`gF%cqnj(a0`+L?Y|RXeaj-0? z`>%C9A7dQmOXB$vve;m6e13{oe;3S^Fx$;7N+)kEMJ-b;(e5@_^7NxP1urXM6&{?^ z4{Yl`%=tb;+kpA$EJr<~*k_%02un{~qVJ-0oEh%EZ_O81J8Y=fll&0Y0qHxc>P(-L z-9y*3859I6v!sqRPhkBb3O5#yi6M1EDDlDDKV#$hX8i+(JB;8vVvkmlvOSM_B5>L@ z`#+`CgSj?fw01xU{GaS5^-zikYRFH5<6`%nYkk%gh$!$!ux`m-e5H@635GUCi1)o= zyfJ=@6NU19ujrtuVUAACBZeEn6@MtNCr%B}YV5+R)gB{4##117kBRmiU zBztT4XUZx9ynZ04dBU$Fx^I){x@pkrSmK@mdc6UsXMETCK-G@7*>Jt7JxXD(?%B(O z&>LP*!4D~Qp3J*xVU?%D%5sg|$|iuV7jvg%hjM4BmpHI1teEVL>}VQwqL+QK`=NPo z?dE00`fG2PINdfw6B>8URfStQtuEGvE(qu-lN0ddrZ8WqtiXyQeueB8TD8ebsU z(UovfU5sEKV}a+flw~?p*|5cPgu6c8hDerZD@HEgrVv!zgEq(dFJn@U);^3^64w*i zQ!y_hxGxeHR5odA_*tr0d5EmJX*=7DMUsVRRPYHB7ZRX+Fr^Voek?~V@n!*ij*slN zu2n-5rSj~mY@?l8AIEGj@h)=C*@j<=tk7g)UfT!d?LANX*sp6_(ET*w7{$j1OGfi< z3n2w>@}?QHAF1U#o#gmtxClP5>z0~g-!C6bwrdHg*(jD9E!a39p-rN=c9XoZ$%bgT z0f3xPeAe$XsU8aKGl`m$WnG&}X+L9(i=1mmu3Ev4&MsWz*2MfaLLtuae39JeS(a_O zEh1CRr6O__^h3kilnRqF5GAQ2qhp2P_P}`SVDp0Q@&S%*1vQpHHXL?bgYbb><*~cr zU8th%-r)5gO3w4g&xjlSLfYS(OB7dvGlbv&3Ppg z5Z`e^{|nbRB@XXIzwUe6g_4wuB#B<1+=rVBef?aIKsZU z+tG~hfkq|YY{6F=iCkig^ayd0@pYa5bfYO4eu^a~BNofD;0?Ru_qXsCT7~zDNpcD4 z9sdF)=v(#`!x;X^e?YV4{F)MCg*F3#3V9*>u(HFIF4CCZem-SqqC;-fe}ONfLH0R- z6w-m*A%V~jN;Q6hJj#LG^ns#PpG!m8LL2y?oqD2>+@SghfeOu5fzU#O`cZ-IDgWt`u~7VzQDUbQ{W}9A&`s|8|3IL z$W95D>2@@H!3+NQ1txPv@nEnnKB9rPd30Aa>U(bl?s{1bd^ll;zn!dx<wA?>-gdleV~f!q(Wtm=cdg__)JPIkRTe#P_1PHg%2 zr!l!4lv0)&Rv-S9X)()+7d~}jMI%4SHDvw>asSAP{4G@A-Oi)?a!*oxjhb7aO=&`) zW?AlEd+g-UkzK%iHPBB)_~N~J!m`K2Nl(OBkRx^_ehOa;Sa4_4iM!-Taf>OjjXmwd zmQN9BkNFikO2p}jZRh)4hfK@CsJcBmR3E1*QP36w{ia}X*(!1-y|ZRlOL#-MepQk8 zHD6KA{eqqo;m3f3uc2=iKCUb#>%b((P{!npOUDe~563``Z3v6pcN>toMso`%Q1TG% zq}rloird0eP4p{H8(!n(R)LLB{242*JrDCjpGvSJu*ez9zxGI`$~51|u4fvx^H0@^ z@kB`Wmv6KDrRummphm)VFKcHu2;=O4A{gnjA961mdC;b_u{ zry%1o;vf!qAVc!p#=Toij50HNXh59WL7dvKMt)tGDasJJGYy7xLtf~+MQ}-x8kqYq zj3Vh*`xmJgq8j)n^=P;H22Mq9Jog15|q zs$rav=Cr?GS@dQ~W|o#R2@YI6d6{b&y^fofvme67C-jclI6)t#>bDBbRXnB+N+f z)NWa?r?6|$6ZpgVi&KwVnNwMC?#CR>8TtL)z5F?TJEweH9wQpS74R&h9PXF6TIOgk zR;XhO*!{RExk(kUGFS8px3<~UO4UxBUh~egt*Yj-XsG_ZOglWcFCWei6QiGFFBKC5 z;g*Cl^b)scACMoA;6q9KKNx$*C`+PlTd=AUm9}l$wryMIq;1=_ZC2V@Y1_8#tYqhR z`@SCU_3a=1Cw6R&wMWDlF;}d;*PH~dq^1P1@PzbgJ+Qv1-!_;|sC} zR|-q}>rTg$Wl4~w41d@mMXowz zI|rC|nmP>0%v+7itix5-XEqR2#um%>LY#-aQ!YvVgU+&KPHtP(BHWVpOB~j$+H?TT zm9G3bm3Mauf%U$rBJGySzY@J~u7HRhx9tAwr+Dm(AKQ9%cDioHBw)RR{z4?kXHvr7 z$J!d^qZsC1@jHow80>~z3S{DOv!Nl+Ht=k%8#X*JME&{~;9yv+3(`g0Cn#tnmYWu> za^5UFTn)B78?328qQfdZjfHgg^nOfaw~uUOlJW!%l^!#pH#4dC@c06Efy7nvO@h5_KU%yakZeuET-ylP`}Ilux_#i@hPAAOKYr!EF zCf)b95eLdYjo?-BS3yP$gY0rK(U?$9Tm{RpIjdHaTJd4=Pu~Ro)!e<~VzIzcfnq%0 z3MR%t2s3@~TgyPq0ZTi8fQ*omva&H2VzmU|U)|p53E4S9oQ@*Z-8}|ox(SKS;EWuc z%xVTB0qJfC*!~q1dP9mciRW02mZwqeKyhH7xUrC;E{BIy?oyR#cT8ET|Hc2kM( zbcVlqmm*$8JYKp)td|i4r&tF-;#iuqi;51&KvLEz=h{7;x^sXsS2FT>sijUiE^Myl zS^mH!g^WTL$_iv5Fps zVDD2*j0;MuR)HO|5}*cC zZczBo@cFumx-$@wRI2>Oe93`i!?khc9$8wPIQdM`l2)KvRpwWIUvVGtugQhvGw_qv zt+R@ObCssEbEI5F-o#e*cJ1mNZ#8{^>;q^LG!iN3yf>(mEnFn1JLdGd0}Dq9IYBWw zIWsY#Zlx=^xnk__aSP$aG`+Ypxf%PMmpuVNvpfSMzIJmwUmWyp5wcLw#8C<5kIp_; zMq=VkQ3wvHgkM^1+H!|aInV%|k~xtU6<0LTa+M^~q{4}YR2A%b%gv5RHL>lk$go8* zQyQ&V+>?UGv7|g&tb5Wm zXTycbzf{u?2Hf_finf8%RqN~?yYX+oX%afs8lq>8S$xd797zkccKIWE_I z`+0$Mp;IDQ{g!vo;59&A07L05Jy0BN;$RrAR{M6q9MXbarvgH>s%e2ny4)Gn@p* z4pT$zoZ{~aXeWy%wbZ=2j=}bZA6atjBrAnnilmfbry`mDi-{9p9u2E`@^x(>F2a9p z3Rwu2!BL>va1N#T1;m|P3{TCqL+OH)W)Uf9&CpJ3QFeTlqJtpi7XZ(V(ov;}AOw%I z$sz_ER+(f*N)Wk-(I0^{htgWKOJ#WjB!NHQB8g=5yCU4ALv0Sib$7yXARf_qNClDa zZo=NfR~;KTHS5G(axZpc&Mr9nvKlk*Qx9!rHj}(r?D{_P?YQiSK92qH@OeAtbPwA& zt^Gai*QA~kuO`iHp+kA}f3w!`#&6F$E=6d-4b^SLEN;+g&@%lt){u3CE*Fel$jmts ze$+##BTlep!PQB}BuvgnLAEv*3)x87gDxszs_Jl^caM`}4`T6y-~`d=7$8f)G0I(of3Ug1yU%&P+ak+Am^%~ZN+{>9E#&ev%r)2TYFpC;SWYi7dE|3- zu1rXTiO`tUhk}O&hfS0?KV5!mYoQgOV0|-}pQu51D1!3FLvsh5c|~?mI{aajAF+w> z+P+#F4vKUCc-^4miCZ6pOe#PJTEkxe2_OSI(~u=XY8<(7u5Lo4v6343UT*^)`_EH@ zHLQK#o_2y0=fE^HM!U0q1 zr&W%4&UadXaz{VMMn^Iw>&p8^<*WCpuU8mGryS>ejuNoJop+`;hqk7F%oS_06UD!D zZ__Clzf4X6yT>c@_VjhfmsAY9)0j^qvb&;2-#^+{%PuN=5N`}qOBZp`1XO&?%ve1x zh*F=GDWxL+DxKFat;M-PWBZ^Q1(xj}<6yu{tbj9QtMgZ~m^uPSuIRUpQS6*HQMK%} zc02}Jmn$^u1|Dm-Mn`h1Tdme5xG}ml{r2eflTt?H9XI03Po?wZoZK5)4|(P&^88LajPYMAdoWS&*>g&!mA&%l+jjcoOr) zlQR2rN)ZcZCi5QI5IP*As0#ou2J@oFoiQN;Q?M#b zq%os?OWOL@6s?ZH1#>3NM1uVyJ-(;ge0r?s276w&&jp)cegoD1P&NLG+rG>Yq%GR! z{M+IA+qP5x1Mj|#yUAsgJH5H{v#Q)hjmTTc0+$m0cS62SQX2VV`%lrp+H@8xmie|* zi8sS{b_{y*!~tt8;=*L9RP(vhD6-lib3^As-g&DQ?Fqe7=^?~q1cl{_jFHytpRqzW zc@t64#SU_f?o7c{y@jF^%H_n1?4Sz)t0;3+QcWe8CG%ucCi0lhz5oXXqAXL;Q}Q8+ zL6=QU>@|`%&GpqZ_*;AwbR!L1Vlm(d3fQQUDXCtvm+sxm!EqNhvOZadil;6;DQ+B2 z%J=bu(P^pf)N@)dw$bq6<1>#sg#WXQA(!Ep5`G{;BL?FeKmc2Gaigtakc$&^8 z<+k>Wb9rz60$^Tj*Z6oaJ})0ZcJF_)GwmKp4_L>w;D5c}4>2 z>a_uH4>>657TomCeCCfYD)ypx8L&3>eO=qKn=vej$a~P`>`wYa1WRaJo%jdU4BKJXg}{ z9F5Q-jp%(cf#1U(6uj|}p;M@oVR||@S8EX}1N2-9gb`0(my3s1h0|=}$yCP@NxIsr z!SGMCqDQWS0lBC4roqU5`x1gS$w0lbX&4FB@ygl6F{3(4(zys)e7o4aNsZXfsswkF zF1b9jTls}tQ3>)3-!z(>mfY0X+<%zl3#pF$>22MA8$SsP@@3{sFIbFRYu!hx1m(^|?fhr1O0J#YU& zzzg&m{!HIxek*_JS)XeAssmn3sQ~b3ogTw`hIpBLT0OPdV~`EAbbUlRUPttpn60Qp9LpiRt(4(X^UH=Mu{(gC1dJuoOVJ{ zMf~PQP-<~9n)R8SjQTNYq%yzJt+^yR%AzHML72OZXzS1cR6ZDnQW>8v%7=XF7wv`wo+uXY?$ zY!ALGDq|ntmCoIMoT>Tk^P5t-Kc4x1$8H2)2|j5-`bup(y-RSjYzxtg*NvyFacp+i zUAy$2>W+yZ(}Z2|lk`Va!a)KV5)>zdgN;!=`g;>)4(N|SgK3EtNmPeoba%!g8b1~y zj+lt5Hm;&kme2|2$>E94)g1oRkJESH!;C!4K%}#Nq>VhIBlr4L#8Umap3$XgW`uot zuNw(413TcB(x%U6^K6Ry3rEzcY*t6{sd|r(s6sOyaq==>lnn6^xDJzo=M!E`Z{7A) z65M8%DQV;7Hn(j<6e%KFY2Hz>XRqeoMX{hAu`2zO|cQ8RR zLRzEXQfyulo^$7Q^7ZY0Z11(jQDkQ@@p|hVwQjg)tSgxv(#e)jd$X9DUL7UDwsq** zt4uZaYA6a)kRY}7N%x{oG(&P#`ZY#5R*yYFjOG1S12efJq9RpF$Ra=xEMK5xqrgJR zQezZg)cT`puR^HEamUQ_*p1o^+YOH2Gwr(II^?=F{C449ZrsjLSw_nM2k0O!qsx$< zW=hst>+V_qU?QCjk9-#3d3}x<2_8~$ms37?7E8s+s7oQ6HqwmXN@Xq4W}p1&aqk$3C zOpB>kkO&E$TjtUN9Ybcuq|c_-6~rsT9+_|Sw<;Zro&%V0lW?2+G0^sXh$h=+<6E5A7__^ce`XFBRZJF07^ z=V#{T=OXSe7jC6gRu)8^?(pvF{#k zfM-BN=Gm9`FO^M+(=z0fgkX+htSXdA@52}RACa8HRoG;5UrWx-{lDGO; z&2s;BJarsD)z;<8MJnsg4Xun@(TtSJ9oc=CH0QaZ03IhoSrS+HBA^!opm}H{czF_+ zqZH%VwunVYX!t|kt;3RiRdOrzDRfvI;0cKG?#{6=tciXG`wrILP~bHsif0?pM( z!JG0RE_U>*hKltGoNs>br*XB*Qje;gkVC{|jmkYzGJmkV19JF6O=G)cJ;|c1l|?A` zYqOF%O?SXc*an)OMW>5=zeWP zg`27x*$cRMXiYo(`A3~1B5r3KI#p9uQ9;vg=C&?3RQ|9#6E_BFjURECeAthqG$c-) znuUL`V?NT`9~THc+NjWiJcsPyA>bh(mz%&Yg?+d|quCV|lNg;E8+*JoXqz;ry}3RL zH^#ZCynzBCUk+``Xdygl!_2C_I%$@;dHM9Cqy9-f`9oDGyyH?bCQv;Z_eB;3Vl z&Qa_)=?cW2^!&;C3dUs}Uo`a`>ga+t5V z_jbFc#exCGX!|5jGn1_PX(AV@x~;Fwo@W;%@j#!RQ_*hu+H`N`++4yKlBTa5zo#|V zad|Ag)OfUX2WH))GX#dBW<;VErIYuIx{XT(Sa*Y#xY&nF6skggA~||ok4$x{vTDMc zEkJ&gc$k>1%rgIg9EDu%&=5NBfs!RfsO%}=WeKE5z2~S*3uQTa2Y;GQuFr#m)k4_TmFS=k%8_)`zN9(IubU(&VQ=pwSOY<8 zbS)VP#^qAZ8g1vkL&YH0w_a|2sFCKgpk2>2E5KILlxcraLY9888XqWgPOgwi8%a(Z z(I^1muaRJ=6f-4``DLeR$aq+Whm(pWE)9s`Kw#G8sZ=<-k&-3Nw@0qxN1l9BWi;b2 z;N3gNWJUH**5X5aiNO5UBOl7ww^_L%1*v!Tec_qCHpbV^_PzW=FUQ~hIbq(69r_0% zQGq|i?lgbSs<_z`Nyz7wfGKIoVhFh?+^ND0u8s9E1ZEL??qf{eG?$7``jzF)@|3f9 zW2)G`Wh}+PvW+%tVGm_+E+^ffg&Vmur7T$ybL+}TX{zNKsu9wz|g5L3Y4!mXWm^nR?^V^%|*}g4@S~^N7?~@ zPuvNP19@dbT52L)cgze#kRmVl-M?lM{Z#dUh8351&ZCA1Y)4;8Mct?4SDSRt_1_Ks z*0o-354ws4W=?A*o)7)3ma+KyZ#VWf7i);b$2eVv0FFBcjj;jAjU;-bJ(QfzyOTet z)Umjx+&B^)vJ%|*CJB}knU}zqN_n$xy$qz+m@!{Vy-jq-%nTi&&|zu!xk6JEKPs7eaFx3hRk_p3*Cx^` zz{Hi>t4H#vm`?WRc;$)%M6)F^d_7FV2Wsk|AEgRZzjKh(9i+rvDoVGy`nq~IcaCD) zr(35xI9qrty^Sjz8&_*F7pW@LNvJ$mNxFI7mCOF&vzi0?xC(MxOvHSw9x~??5U>+v zd&!Ed#!Qolu?lo*1;*Cl`3XXom5eJ9| zkhaiZJo6HT)VDLL_eeV4ZaY~#wnoSOLgPW2&xgYm`7MFoq@6aVh!UKl6vKv*hKYy7 zdx64HOj!7Nov&s6{GMxnvg0(;fH*&{&!iz;cIzcCHyR^ugP)OeHTMLzVozpo+c$ZH zWdNdtBb#YfA5^fsUL_QKS_kEt)`4q3S!>lDGnsR)mxRQ^8^n87*8iTbd^%^zrKfrx zK&!u8FnUhju6Y5%Hya&mlfxWa`R4mYgfLnd8xJU*MFB8!tm}TW;5fVYe5!M^C-Hr# zm~L}likW83rUI^;ZSd2HUzg`K4eZ=-rQ5!xfN{8b4F~>C9+z$5Od(Sm%xIr(x#+(E z4_MH}G%E;tg07g8xyH+OvC8~MQMH3;H*+Ywmfgyh(T8Gb@RPGH9s_T!wpzNhqS5bs zu1ys%i_s**3sTpC&1uqmp0u|$Q|pDQu*`7uSvnlWK)qdD(MTwCRBIO2jHGAuOQ(6k zJA(-NINUSMtm8WKeG@Zh*OBqjyIV}KH4fHNP&r%rv(@>+=OpS(iBCAWk#D;3gZwyV zg`Hc0cq^dm^p9oWG7S2$gNiBqEoyNZFQFMUE$~r@m03w5qN`qKND@RU)kTTno zc-s%vwDDJP!@vTpsZ+)8JK1msAqN+7*3iEL%DY2`)r=MYJ%F66>$lXQ8YVkUm*M^c9i!mfQk{p z`_Itk&@ZdZ7qL9Mn?BQz$cp-_O)W;V2T$8BR(+c#hIsas>h31yoGk{R-Anab4QKb6 zC|#OgReRrI-AZI4RLAf8$i!@|!b#WlMdjS=9o9ik9eRSV4CW2Z;(MxBfEko~VnpdA z;QjT-tBUA5NE~>mP&n28OHc##t+153YhwNwV63$k-!* zoIPm5At!aTO9Y^l_IQz7l1AdDV=N3_9Ee_YEnZQjMtZZ50cgLj)^yP3v7-Det z6iL_hx31wMD69^>%G(ERo>a4Xv0H7*kYU8IVJsUO5rdf4{ikHtn>I{;2YVx5X2^yp%WFDZlNPmE`0i_TLNov~h>xXk=sG?le3kSO@ z0Y40y>VQJ=1@q8%g-k@o+K!{Cwv(vnk=vro)nh@VuehJS)(>m0yD(yURJYjfUDz;6 z$|%E%{>($5SEvkyd73kAj3V6;U8)HBNaW5!jS|5&sTG&kVb_~?Pu2OBqPd5Yk{_{F zXo#N;7^oZV-JKqE&f!3PHn*`YAY(i9V4d#3X9T9vy0}x_?PD(N@&ooe5Aj!C{ zfIDVB01t!7SsU>!_ViTGfV7J*B1PtCo*zA$LUP`*Mu6`(NgyB@Mi0Zhq#!;|c64?o+(((G3Id)lY{z!g1}T#FYJ|YZ^ecU^3|zQjwta zO#4hXgL|ZD>|x$P-f7aI_FeU(>zdKO&5Bqb07B{c%gkSsCz3xUjp^Z_NWlXNM6Nl> zY$SNn2WDhTCVTG==m87WuKCBd7LbmN$`81?^Tb&1(igHyETqMB41C5sAKjk{6m*#+w}f*B z)B2=Rf~r#TS&}w+W9#jcq6-PL`C{4P0m4|}3IMVrcA0l7<@g|LdDFuND^J5`oAl8m=j*YZ>W5qqfVqe>Ysp|K62NfCV;x z5tyboH8nMueY9OpM;AQkt_iNB)3l7cZ;~ztZ|ua|>mV)HC>c8dS&p!xQ=9CcW!Dy2 zP6tuQqPFWy&dx6PGgIUZI|lWczu3|h0PC7_l0xK4%ImQY@l#4%{vXd+NplWd^o?uP zuGnSy3j8wi`>Q$O>)PTa*k*2KFK1)h1hywz%$>*X%g;7b=kB$smWKv`ta@+`Vfi|? z6wjxfeA(pm?4fRV4qjY=as-@YLJoIhu;76^qs;C+?u}t6=8+1D-+PRrIYjv*@w!RM zNg4;#QkSYKdP(|8>MAHy^z^heG>`uhn3xizEJ@sDNsL1kJ|)Uyyc1F7!$dA`H5M!& zHNplH1n_31%m^$ws2uBjJFeqN0y(DcC6uDs2$`E{2_);(DU#t#D;gzUAHSn*K4IIV zhrE_O2A0j|42F8OUE7-2N>ykdy4=4!p?f{1xiYNV8>-@22ONH{I8D0gq)4Edfr|1k zAh-iMcuN;%P&m6cGp6on*JVd(dRjMkjQ^=y)k59VW9>&u8_@w|)*Gyeb8CLoey45> zwj=13EAN?dq3abo=37?YyXcVWl?)P>>=t4VwZ+#8K?PQ*Tq|KpIo`VD^J&ab(=H+L zW(+0VF{S1w)fIJSD(g#WC~eW|g_6pQ$+}om8itFf6C@p&GmbK@?n^sOBu*LDFdv0z z=xw2J*69L_I_7>0VbQ!qFy7V$z5kou77F#0b)eA|Vi#%`Sp-`__`oU(e1kkCf>XlZ z6u5#(YsAp~8cXw)bW?YW1(x;kci?Cl`)y!uB>@LM9Vcv}$!|#vd61JGtJve;Yu9R6 zTcgjpwg=yuNbpEr5Ws3vsz{WH{-(-E${8sX;5Al>i}XpEbYzg$5f}vi=+r+t^VAA9#Je&Z-u^>9M7zRTexFF$Q_XAmm^m6uW)@L4dJ@{fZu%#Tr z@ZTb8t;WN4zaz85R>%Ih8ZF;wIo<^+8%*UXJEUY^bJTL`PIZUxE|+P|bF-`Gx^cbW zC2`(Q{v0P6epy<~b>HL85?c(O?6SBb&wfek=ykDaA%CHqE(MlZAx^w~U1jK1LvsEZ zR4en%r(dFX5#KIkKM7Lqlgzm3_SE;SqxL!G|4ehR^(&FGDeE=-vo0H*%zg*+ z!!A{IEqHBxx5zKKysnSy=iVd86{sUv6yw5&w#9kGPGyLa52s;;RC`W_@aw;R>NhFv zGez#Q;OeGskz>n4{k(5OSNGu`-oviAYwU~&(Qq&pjoz-@! zb~ns#_9G?+)R~~_5yO3nonCpn`!+;7sK!c$#jeOc zo9e6T+rhqsd%uSFrTUTk#wKper(j~|StWge*lqaZ6Fc|!X?UnhpY#5xcUr1q8k@b9 zF>c>6j1{0X9Qsdx-+s_s8f#r7EQCH&UFJvAO@KeIRpliv4m|8d!(e9#4nOtF zJ~&)m)wUINd$+MPR%;+H9*~dWS!c%{1bPC$)xrSVF%l1CeN|$|8ibpMM-B~v8}`Bo zcaIFDV>QdI$Dfym=yT?;-V)+_vA?$vU?yoF03TF?&GDD4&rEyPN9(=7pId|HodZb3 zagnD@@O~VW?H9O<4$%?TEwv*1aEsCQxR*8@W*izE`hCUw-<$ql zl>xrUE}#0t)F2QvkKen`5Wi>r!`MBpsIyX$I~clU^OFoLZ-N<5PKyhYapU+ z?j)@Etjzz|*ZS#^!$FdVlIM*x12dcaRahh<%p5-18~=+JEb(5&j}2OTYa3%7#JA9D zKp;^hI|L1EB`1h2GA~fBS}vdyiUvzN#Mvxc%~P_H3TGNQbs(m<8T`R?t(K(`Csfa+ zU(0A+Pzuj!c1Hp%g+`(y*Fy_b60AIp?1ufkxJZJ3hCz7N$ z6mVtxYik@yL2;Vrq$CmU1Qqfro?I3ss<W_4UF?E8852G&P&lWcsCHa3ajKdov35Mfg&LN=swL-G z0g^`Ke0&D44RvQv?XId!NlxOy^9_jxSiDNU)@gMsle~7f!SJ}7qfO*f18L^#UoTYb zr5fHWp~T=p<&Jp&a4%-d$0kcEa(r7;7x>sVHN9~gAp@{3g3w#u8FBNc|;s9zE){$GnW?D zrW!`03i<0nhqO?W$o5!npM0PwKrI*#BKf5g;~XAiJ=t|ttwujWHTb{f##YqF4AJ25rSR4)0takG`oERp3vCOC@ zSZgJg(AHWHHe&#-KW(JM-lSBfnZ*`$O0YB<7X0Oryp{3r6FJmUqk$~LX_U6!IBhMh zU|fqoy!-8~#AM9xou6FinokcOd5cE-ImQ;9vFtq=0-xAOit;|?L7anjF&){nic5EB zD0@9%o^B*T66>Ovvigfx5Ca`iVq#{IMLBYekn0R8f3MJSEbj-*F=}xEo6(P8A#c7v zO1&{ph`A+Wu4#AZ-Vq*n`BeGJxHAS%1#|8me-yow#gZ8h7bwJ?3fb5!|C~N*HpBCT z7&m#dDKZzOCnHk}PTqW<70Cd)_qY81d9OKbR7h_L&%vxDdELn9GMB_!arjOXucG|S zGgvKKy(P`GipU*s-UJ>v!FEf7s%1t1@_Qez+U zRrMsNXa%(mAIq45t&HrG-btk|$>st{+C&_G=}|h*4Q2(ho@*=XXM3dtB+ZzNmkBD&w0ru$9yN};!4bYCaBSwN953>7CR8)rkbJX{=+B%g-Se<3Z_z` znzKS&i@scC=K57R+RCC!Awhm0$=OAetwLSVTxBMs==q#lp{2K2WNcx5U0uzkTnz5V|)m{XYmn^>ERZU@WX4Icm zm7SFMSCFK}s@rQ_uC*YjLh5tz@_x~~`Z0gsFdQp63@;Tsam8+w6eRALrA`Xv} ziSAus%a>X!(S9m9Fk`Aooh-NkRWWmd$D^XPkmO(^suVFp+FGEcSR9OMUaT^hayrLE zsZ4A37n|aDl6FNNf+*F&yanP)vZ$OuitDj)o$!&p64GMa)Tu)KvLFs70+gR0ye zp+qvhv!vv)FCuACN?Nu{YDV2$ku(L%j&0raFRzwQenlWqp+QBZ8r-@vP0GI(s|Lsd zUL`Rum0v-FB5)}sx>#MkuT-I-K#j_jQ}rV=O5=nSs6|cy6;XWIsrQEE*YH5(v`<|R0w1glikt}vqRumV4_ zccrJKMlyphq^c)4%*wm4hWO8zZJkAqvPME)eG$qzkq(t`Jt71wBcGj7-J#fm5Tv{< zOWiUF$3G>U}K#wBWMO4?|jt21U{iJ<+zO?;& zSMkC6wyj}8>Ey!n{FQ7{N$XusJ13Dte1zeMV~^wDg|+1Ar=3qd5oIqo0lAIRZ3b{U zWPp9oxUC5bh|O(+4%mEB4UJy4*3??g3fQUh-82Qjc~T`8T_P{C>$>!wa(tQ1MX%5L z-n8OQoN{=-K0wCm+WjQlv1dR>l0)VIsRbPN>tkvSs_&uWZ}BqJa=vB4k%;sKw*v79{xxNrzX}e5%LY zptw4_)@>oSRPbUjKPN?~2mJ(%6=YNY4){_=Epor@hMw-*&S6uouW>c5-rHB)>Lqf3 ze=SLuu+_dQ%ijS1h0{gPFUJ~Y%jeIYAMpWcx^8qd@RJ+X;_acNDTWO1N@qH_CVD(g zPc{~pLqpuxGsLBs(ETIRz0IkZx%VqzK7YGI(pdF`M9a8vekoiWSHHtye+i5$if@cv zJ0X3MQuoXs?chT$BDUOh=nQKM?Vqme;q$++o@J_STi1E}FWiCZ_iw3Ad9A}YTyTCq^Xb_!d~bc7jCYwuSm5I5IOo(Uub)3UjX@S%YeGmYwNpqnD)Ow z!9Ck>+tS``_y6=!h`iI@UOK&AkKw~1`$~Ip>tyxplkKT}N`U^8ln~C%qZSwrWdAnd z>Wui^Eld|56KX?bb9jA)2D-uY?ldn)499zA^b^gdP&|CAx4`-#J1DTKHrmI}F6@#0 zgpKI{rvG;uSNgbS&*WMRDxxq<*VL=*64|vprce9YaC1}BKW!M(-#O&h8$eq}%tar{q33?LxF{pn*;_L{lN_W4z5?}0_@HB1;HScr-dj|y=Yf`nbS#%?HFV1kLfZBJ+ zFvCP&imRn$k>e9`P5SJ@bMxlx#FN_U1+qU);CG*~*?HPu{RXdNMz5-94dJ_Zm}^_M z`H_=nwRnQSeqRrj;W>1?=O=xTv{9zmf4~LS-2gBshN#>-WUPwtU;NtBR?7Bx+;pFq z5^6?JQi-4YMVTn!5SnBgW>G+^B$f_lYKC2aLzyZfUbKg4-XN|8T>#SF@k;Tjvk7lM z(3s=`RXqE1F-hJeNa~n#_9y>&l21Y>!MJ6@^=a7~MI2FeEnJzIl`77uHQd>rH^$E? zTu=NC8=8%p5NutQ!DDpwz(S_~@gBaPE(3wBkp&bF4*|W3yPYWky}XfyvXk}y!x+uT z%E(DTFJ^A#Wa{u;S{XW-ikKSPntYFx`+htB>2h#%B4A?qKk2v!H8dTO#Swg_Yfr@Q z^ETT6Y@&*$`xrWq1HptO_NRiwMD!9`qFRQL`nk4d#k@{GN<`Wi){NWV&OJTU?2Jf2 zCFO(?mPkFybmZLekw>>0$|?gt?F+ zvYKd8^Dt~^K4{R;O5&U!!ek^E@)ClGaAsfxKYhSR%4sQgsJ~PEgF^J6q_`-G2{LvC z1sm_32n^x85KLi+CvUz}s`fH8#kJy6Cf@HJCAxZQBJsi{B;+zmy*M)>8UBA2~Gm za8cBIX?1h8(MGK9EpyNX~VtE6%w=nc)pf{;e-StnqmAgO4(hkH@LU%ave3I&%C6hH|g-$Bp=C@@)Nk$hn<`R*D2;$T$$1H z;m^OI`w>XGCiQi4dd?=#IFV~St`|4v#H-2Asg8RuBs=d4SY3y%h20G?W=!KDEs4>y z)cGu}iE6+qps<@66%!z$YISqjam49ZWWk>u`6sqn($0flb-kA#iR1;i`uvM~Jiu7o4=qP!lR;%FHuQ-b^kBt?bC9#N;%MwYnc- zB|%o}?K;h==5JKMI$WFBD%mCrot>)K$gZawDen=}w5AHR4oHWRYgGG0_NFCold zNe}(IGhaZmEmx;sFxRVHa=k23;O9&g02`83)Ii1NyLOj9jvtOk_ zG}bUdkvok>vfr47dm9g=ZYa-X#eLplSu;pI@PUA7xm;sdZ&LJ*=A5F_4EHRGkigSH zG|gR)K~N%|As_N~Vss&JM<=js*V!Cs8*iab>4(^!cxvIP~c%Sup8=s%+5dYJ@rA1%wrCsIX z>1jQZeb={@v8Ba#<8uNd(YLMZyH#8!1JM?ywjlL9D%Ze>cs|5avUBs{uR(a4+7imHm%-1yP~tIat& z%}w&`0Stt5#SaIvLQoG@={$x)PzN?(X^<+@rj((ZTICmRa#jHXHDF|q$1VD@$d*+O$_bvpylnQTNIX8dMNaX}$9 zvF9E`n123zARjK7SqUTOHe%)OQEnG?^dGXC#=^=ff*~f+-FF6||I6qF(ic(OdUzsH zib|zH!t@~Mc|*Xhh#)`fQSL;Tw&S4>RFE#StZyXR2w3w!ANnbrOQ){9*6?yiY|2S( zZEpO`>mQDYb-C_FGoOT8jg zI=L)Bzq%?w&yCLzDR~_`U&&n^bhLp8+x#t&{xt5k6v4YRc0Le^FXTB!(Q?@B#bDbt z1`>_Oun;O_t8ugQe|2|OL2-TA9>!gRCb%_DXr!T$4j!BU4M78q6Wjw01P>4_XwU#b zgL~ud?iLydA$X8rL5IwLW^Ua(SKjZbI&Y^=)qYsDSAE~w`=>KmFjmDRJm_ixeO&;0NyIw?S+;m5!%5a%^n95FA2TkIoGE1E0^h~FP{l!-k%;y+ymzZDK1F?Q6)ER5 z@+9vB#Xu+AI2Xo3f&Zwp)VnqVj*Vu6LEnelV%HQHe33b@<@IELT)6JMnfg*rQmt3> zvOaDbGIm3_V?nZ#$}9DG6iaOF-A@#mR8pNuINnAc9_IlHvc^gh1K;!jbyb8>3hSzX zWoNoPWheM!>^gTq?BqJ_+S0%h*o*Guk;-^QUQL!D$RS&e8+vb2N3nk&+!Vb(n!cHo zO$f%o2{$hK3EP6c%)OB1|GnQWzu>_(M-xa**%y7l(PIVWcsS$qkW%i$(u$#yZ*}1v zl;4(twK*0q$|*yZ6+KD&j*Up@bDQLwFU+n>38p{KpOiE32kYh`X-wu38#L84>*TIL$bKk6L^Z z%aJx^dlANx&g^Uny@1kMHdDj#h`t@}ZsU)Q`o76$jYh@&)(I9Z1%T(20Qam(Vo9P? zZq}x$#pdx+AxxGC6HSIKOwJGJZR@2QEjXe&r#PHGwZGXpoYjyv7PrdIjd2EvGaUdM zx(>K-&y;N+5BLQxl0Rpezgk$~)OfmEya;t{h6ZDwO!}qZK>IzWlJkWP-3cL@a6vjP zuR;``1c|S_iN-0GfsK#K!sK6x znLiM8g%o_;RNX<(^kBT+ORZTOByk+zdF=Cz7IcEivU@EfuIv0sX=byUhf4pBFM*j! zLE(2r>WPzpYg+AFng+#*xXvJB_wR-qttH4Ie_Kt5@iJ~?d=G0_y-N7ZfYiL&X|tHG z+pN2WVjJ;mh0v>FSm5g6W=YnF5*+$@J_L1^(nWxL8)<1)#=>CQ1N{tyK?dNfZ3&xbzt6 zD4GRVd$Qa&4{1eLqILs&CYLMkz#;@rXJYP*@qH7G`7mPY zgi<=?L|;e$Q_nJ0H9!Xd@Cix^MfZ+nWXrt6`5L1nkesa2T(d)z=?ey%{QFza-(&|2`>bA4s0y>?+9tNE8X>cZMMNf4xe= z%k{OUms!|>H6|KWg!W`0DzHeidJMQiZSA{D2ZW~Pa_?N85}oCyFlw7a`4{n>bwTH* zH?$C75sl~w{Oi%eN6R)*k#m3)BG*-YQ+laY6Z5Lfz4}exH*Qr*E==O~8Vr>|tl^a+ zWnJmEB~HihpxRcg?M(W07wI687^XI6We!JujoI3h zZr~^^wCDwMgN&2&B}&W&_bxhRb5MjF|6BSP^dGt(1bl%HQU_yXn4VY06GxSBdBsoc3jPft1Xd~Z zP^xLM=jlO9yqJU27|#2sxs)6}eCq7kYX`}GNR2RAA%p!9o@T#QrxyM2;7QRGa&$A; z*x{NJ9M2g0?zGi9Dpmb`qr^=aHJ(K-dq$@UzYih9M*x=7+Q3oubiafTFE+TPnzz1& zN&Z0UIJFQmc7%;*B+o9%@k##ai4rg4!ij#GwHSgizKU(H688*ccCH_m+U5)s&9SPA z#G-Kv*QM}mJdptJ7!8@eHtcqqDuy2|DdOWBP3&JDOYz39Au`Cjk<9WO9~8QJ>p^)p z4;4ukQNu-s8C?_f_d?~M;H&?8p|Wu5Di#5&@_Sbux^UgJ_64-KjO-Qvl=gH9hove( zx=X2sP|or7F&DoVTlB9W@PrOdWi+Ac=h@dHKEy4C^4jUki^yr4O|3AC%#nj+N&_M< zK!_k+*IeQdbsT-^4yd(CBO31SCJ28u&$1yGBs|gC_zvzX4Rh+gLM@7KmjU?QH3sD2U(KWa zfG4>=+A=KhilHA}sb$WLGoX|q-R}B9ReL_d_S6DG`hg&wzJ5EflF1Fcp~MQTv;fX$ zZ?z&m8Po!fWN2+(w-z_=c7y{HjMNnTUG1Trxs?9fC7PKx9gz%r$F8Ld#+Bxrx5Pzn zP@y44ot%OD{!MyXA(QwaMw|IWOza>Z=7CMtJE=GZoD+MD#dvtv(2bSM&%uj89zo7~ z`VW@*SPKVx8=e#SGq%CUcaee;-p&#{V3)1POLQN3h-)##M`*y`FZ9O? za-*FKDMo@@_$LnM$G?_qe;7l2kuMDEj(=qq**_vkg}29Mv~A7DnBH{g&&I;6#B)V_ zZQ3Go>hTB|8Cta`i>D2OEDO^F#9r7Q`aa91lN*zGNUJ`tC8qBznpQ2g>chw==3#*N zZQe#gyNTkyqFlMToaqxE>h%Adv9D~^2;m#-Y&znigwa408jCAIt|G<<8;uYL#dK0H+M5o zf{Jn7-E$`S7@TQdvD1$kVqGd#?V$xHi3F$1{MP05Y`l|LEX@1Z>{79*=IR|QlW8-S zD8$>KSq|pxt8jcmqvUOPVN!OeWAI5a=lv5hOf~W|4Y#QUOCnL=4| zwvEK>uOM!!dyl9O^#Ldq$#^i1FjSMnZbZ`O!1xd|Tv65a&edd0PdwlPG;(Gnj40w@h;teqa%L^#Xsr5zv#$6 zSjGP*I`SX~{!$A6g^v7L>Oa^2ztIsO81$dBDi1Q2W0pZAVTZTmO?)Jw)>=}Uj7ZvY zL!Jq2n{*URt?Zp^^nBI|wWTYwn3vqx(w>2|igs3_v_$vdfhO(W7fEApdph0U`XX(Y$?oJu!NJd18BU6?_1>O(~|%j9{e` zEAyr1^vjbLj@F}_k~p!uN$_1LP-C4?h;CU4Y@q)d){@EX%}p_gwD($!Og zOhW?8DQJ>6?}uBl&ZiWcTyg!#Ss`;;62ZfruW@8GxQh>IS}9$%!Dy1YF--WrVg&u{ zV>gCpaxMhNyOquaD)ZVJDEYhD*i`Cd=sZE66o-9WNiA}9KL483lG#4ruPzdb@uy~$ zcSu-`(XxCw@7_%q08+|Te*D$+sZ#*@0_o@Af@KHux+!KHwCp0@rEr)&lww3@xx~JC z2D7}(GuKvBQcHZ&H7tWeASY8w++4Y6NwN|psYb@$MOq&_Ko{>?rOA|67T zv{Tdm@i?g&zby@@F3OdaD-^c&U5;OmRJ#iybubu6c!Ys%Zua9_cF5=2$GtYwTl|s= zIwOocObjfL*Kgd~2b8!g9&$`5ib`mKfhb)z<29p{mNtZDa3_L!_x&=h(}C@bCmgIo+$xu9o-u6 z5k~?H?;y5i!oFlLX0cY_nVoO^UN>Cmf-#!X8+^XCemQJh|3D#$J1=;?#*dg_Qs1k1 z9+53vL9;?1b`}D}p?P#^^S(sIx{iIE@vP({rJr_M8p9J`Vl2kE#qLCAA2XRV1dggT z=@%!dXj+dXZzxPu+6$MxeU0y!m=Jq2ra1k}823t+p|%K`KJYM1Nq@u*!k%l7=Q^BW zpBhSTO{3PK^WOaKZ0gWteihK*TUkZ?y0W-wuc1d&dpu2-wd=g=#&STzzafEpD1tL5I@3B>L8u9FyS>D% zT1%K-u*?)Pi>qsw?NmnX?Q|7By;u6j|#R#;eKEv-Cr$dY+=c->-I-j1D~ z<7ce)wOQfZ^$d*&g@+TQc36l$l0~lzPXX6*36bMe#$+$}C9;270NsVpUh-5eaiAjN z7gVIf9wt)}x1+ie&XCW`023#n>W_+0-|B1e_T&s@W3gAZwm()T_z~&Iku_+-9Y2rK z`&&?5ak50r;qVB$bURmL;h@B7tMhsp>!cp5Ib~3bhBhGOJmB(wgxKMFXVF`Bh_bcrf+wa-+4wR z1kkm@S`1FCc3zs3)xqfoUQ^ZlHX^Br&+S5f&iU^Xlg zFh+0KrD}<3j`|JAmyj)CG;8MHY3Vqh`k*9DZv5J zZsquzhfnl^JY}v7XvuW-MqoVsQB2`|t|2Ty_Gi)huQzHYM~Xr~@kguAErXTf2MYAL zeh+~sYNtH--IzTN&Z;r#^B%oD=NrJnN}Zp&{^;(IycILBaLF1rO4(9R0)lt!LZQ(E z6a<|e{2d|O0LqD(7V)K5Js2r;#QCT^W|~Fq{`tEuq$s;;XFn6c=bw&$FqHCd2jea+ zR2ZF0yy?x`Gk&F5L(Jc)VY(R@Nv%f_mm)$(NnPn{Ze#Wy^UG}QFxQ6^r>V>l7wXs9 zP5Bs0&__I18zc9Wh0b+B;^G?{u%1K5PT$<45>T2L?8(&Yukz1?NPKk9RWqb}?Nkj? zUn@iFd>IIpS#NSFUJ3SPpBNFbeL GET some-bidder-domain.com/usersync-url?redirectUri=www.prebid-domain.com%2Fsetuid%3Fbidder%3Dsomebidder%26uid%3D%24UID - -This example endpoint would URL-decode the `redirectUri` param to get `www.prebid-domain.com/setuid?bidder=somebidder&uid=$UID`. -It would then replace the `$UID` macro with the user's ID from their cookie. Supposing this user's ID was "132", -it would then return a redirect to `www.prebid-domain.com/setuid?bidder=somebidder&uid=132`. - -Prebid Server would then save this ID mapping of `somebidder: 132` under the cookie at `prebid-domain.com`. - -When the client then calls `www.prebid-domain.com/openrtb2/auction`, the ID for `somebidder` will be available in the Cookie. -Prebid Server will then stick this into `request.user.buyeruid` in the OpenRTB request it sends to `somebidder`'s Bidder. diff --git a/docs/developers/currency-converter.md b/docs/developers/currency-converter.md deleted file mode 100644 index 64f770608bd..00000000000 --- a/docs/developers/currency-converter.md +++ /dev/null @@ -1,56 +0,0 @@ -**For the time being, currency conversion is not enabled, feature is still under dev (check #280).** - -# Currency Converter Mechanics - -Prebid server supports currency conversions when receiving bids. - -## Default currency - -The default currency is `USD`. It means that any bids coming without an explicit currency will be interpreted as being `USD`. - -## Setup - -By default, the currency converter uses https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json for currency conversion. This data is updated every 24 hours on prebid.org side. -By default, currency conversions are updated from the endpoint every 30 minutes in prebid server. - -Default configuration: -``` -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) // 30 minutes -``` - -This configuration can be changed: -- currency_converter.fetch_url can be any URL exposing currency using the following JSON schema: - ``` - { - "dataAsOf":"2018-09-12", - "conversions":{ - "USD":{ - "GBP":0.77208 - }, - "GBP":{ - "USD":1.2952 - } - } - } - ``` -- currency_converter.fetch_interval_seconds can be anything from 0 to max int. - **The currency conversion mechanism can be disable by setting it to 0, in this case, there will be no currency conversions at all and all bidders will need to provide bids as `USD`** - - ## Examples - - Here are couple examples showing the logic behind the currency converter: - -| Bidder bid price | Currency | Rate to USD | Rate converter is active | Converted bid price (USD) | Valid bid | -| :--------------- | :------------ |:--------------| :------------------------| :-------------------------|:----------| -| 1 | USD | 1 | YES | 1 | YES | -| 1 | N/A | 1 | YES | 1 | YES | -| 1 | USD | 1 | NO | 1 | YES | -| 1 | EUR | 1.13 | YES | 1.13 | YES | -| 1 | EUR | N/A | YES | N/A | NO | -| 1 | EUR | 1.13 | NO | N/A | NO | - -## Debug - -A dedicated endpoint will allow you to see what's happening within the currency converter. -See [currency rates endpoint](../endpoints/currency_rates.md) for more details. diff --git a/docs/developers/default-request.md b/docs/developers/default-request.md deleted file mode 100644 index f071d91bad6..00000000000 --- a/docs/developers/default-request.md +++ /dev/null @@ -1,44 +0,0 @@ -# Server Based Global Default Request - -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. - -## Config Options - -Three config options are exposed to support this feature. -``` -default_request: - type: "file" - file: - name : /path/to/aliases.json - alias_info : false -``` - -The `filename` option is the path/filename of a JSON file containing the default stored request JSON as documented in the [openrtb2 docs](../endpoints/openrtb2/auction.md) and [stored request docs](stored-request.md) -``` -{ - "tmax": "", - "regs": { - "ext": { - "gdpr": 1 - } - }, - "ext": { - "prebid": { - "aliases": { - "districtm": "appnexus" - } - } - } -} -``` -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 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/developers/features.md b/docs/developers/features.md new file mode 100644 index 00000000000..b9bb9053ed5 --- /dev/null +++ b/docs/developers/features.md @@ -0,0 +1,12 @@ +# Features + +Prebid Server documentation has been moved to the prebid.org website: + +- [Adding a new bidder](https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html) +- [Adding a new analytics module](https://docs.prebid.org/prebid-server/developers/pbs-build-an-analytics-adapter.html) +- [Currency](https://docs.prebid.org/prebid-server/features/pbs-currency.html) +- [Prebid Server and GDPR](https://docs.google.com/document/d/1g0zAYc_EfqyilKD8N2qQ47uz0hdahY-t8vfb-vxZL5w/edit#heading=h.8zebax5ncz0t) +- [Prebid and TCF2](https://docs.google.com/document/d/1fBRaodKifv1pYsWY3ia-9K96VHUjd8kKvxZlOsozm8E/edit#heading=h.hlpacpauqwkx) +- [Prebid Server User ID Sync](https://docs.prebid.org/prebid-server/developers/pbs-cookie-sync.html) +- [Cookie Sync](https://docs.prebid.org/prebid-server/developers/pbs-cookie-sync.html) +- [Default Request](https://docs.prebid.org/prebid-server/features/pbs-default-request.html) diff --git a/docs/developers/gdpr.md b/docs/developers/gdpr.md deleted file mode 100644 index 8da2e917623..00000000000 --- a/docs/developers/gdpr.md +++ /dev/null @@ -1,31 +0,0 @@ -# GDPR Mechanics - -Within the framework of [GDPR](https://www.gdpreu.org/), Prebid Server behaves like a [data processor](https://www.gdpreu.org/the-regulation/key-concepts/data-controllers-and-processors/). -[Cookie syncs](./cookie-syncs.md) save the user ID for each Bidder in the cookie, and each Bidder's ID is sent back to that Bidder during the [auction](../endpoints/openrtb2/auction.md). -Prebid Server does not use this ID for any other reason. - -## IDs during Auction - -The [`/openrtb2/auction`](../endpoints/openrtb2/auction.md#gdpr) endpoint accepts `user.regs.gdpr` and `user.ext.consent` fields, -[as recommended by the IAB](https://iabtechlab.com/wp-content/uploads/2018/02/OpenRTB_Advisory_GDPR_2018-02.pdf). - -## IDs during Cookie Syncs - -The [`POST /cookie_sync`](../endpoints/cookieSync.md) endpoint accepts `gdpr` and `gdpr_consent` properties in the request body. - -If the Prebid Server host company does not have consent to read/write cookies, `/cookie_sync` will return an empty response with no syncs. -Otherwise, it will return a response limited to syncs for Bidders that have consent to read/write cookies. -This limitation is in place for performance reasons; it results in fewer syncs called on the page, and their -sync endpoints will almost certainly read from the cookie anyway. - -The [`/setuid`](../endpoints/setuid.md) endpoint accepts `gdpr` and `gdpr_consent` query params. This endpoint -will no-op if the Prebid Server host company does not have consent to read/write cookies. - -## Handling the params - -For all endpoints, `gdpr` should be `1` if GDPR is in effect, `0` if not, and omitted if the caller isn't sure. -`gdpr_consent` should be an [unpadded base64-URL](https://tools.ietf.org/html/rfc4648#page-7) encoded [Vendor Consent String](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md#vendor-consent-string-format-). - -`gdpr_consent` is required if `gdpr` is `1` and ignored if `gdpr` is `0`. If `gdpr` is omitted, the Prebid Server -host company can decide whether it behaves like a `1` or `0` through the [app configuration](./configuration.md). -Callers are encouraged to send the `gdpr_consent` param if `gdpr` is omitted. diff --git a/docs/developers/stored-requests.md b/docs/developers/stored-requests.md index 8b7177160c3..9adf4ed1309 100644 --- a/docs/developers/stored-requests.md +++ b/docs/developers/stored-requests.md @@ -1,6 +1,8 @@ # Stored Requests -This document gives a technical overview of the Stored Requests feature. +See https://docs.prebid.org/prebid-server/features/pbs-storedreqs.html + +This document gives a technical overview of the Stored Requests feature in PBS-Go. Docs outlining the motivation and uses will be added sometime in the future. diff --git a/docs/endpoints.md b/docs/endpoints.md new file mode 100644 index 00000000000..88116144a41 --- /dev/null +++ b/docs/endpoints.md @@ -0,0 +1 @@ +Endpoint documentation has been moved to prebid.org: [https://docs.prebid.org/prebid-server/endpoints/pbs-endpoint-overview.html](https://docs.prebid.org/prebid-server/endpoints/pbs-endpoint-overview.html) diff --git a/docs/endpoints/bidders/params.md b/docs/endpoints/bidders/params.md deleted file mode 100644 index ebe0401c2a5..00000000000 --- a/docs/endpoints/bidders/params.md +++ /dev/null @@ -1,24 +0,0 @@ -## GET /bidders/params - -This endpoint gets information about all the custom bidders params that Prebid Server supports. - -### Returns - -A JSON object whose keys are bidder codes, and values are Draft 4 JSON schemas which describe that bidders' params. - -For example: - -``` -{ - "appnexus": { /* A json-schema describing AppNexus' bidder params */ }, - "rubicon": { /* A json-schema describing Rubicon's bidder params */ } - ... all other bidders will have similar keys & values here ... -} -``` - -The exact contents of the json-schema values can be found [here](../../../static/bidder-params). - -### See also - -- [JSON schema homepage](http://json-schema.org/specification-links.html#draft-4) -- [Understanding JSON schema](https://spacetelescope.github.io/understanding-json-schema/) diff --git a/docs/endpoints/cookieSync.md b/docs/endpoints/cookieSync.md deleted file mode 100644 index 2378aaa1cdc..00000000000 --- a/docs/endpoints/cookieSync.md +++ /dev/null @@ -1,55 +0,0 @@ -# Starting Cookie Syncs - -This endpoint is used during cookie syncs. For technical details, see the -[Cookie Sync developer docs](../developers/cookie-syncs.md). - -## POST /cookie_sync - -### Sample Request -This returns a set of URLs to enable cookie syncs across bidders. (See Prebid.js documentation?) The request -must supply a JSON object to define the list of bidders that may need to be synced. - -``` -{ - "bidders": ["appnexus", "rubicon"], - "gdpr": 1, - "gdpr_consent": "BONV8oqONXwgmADACHENAO7pqzAAppY", - "limit": 2 -} -``` - -`bidders` is optional. If present, it limits the endpoint to return syncs for bidders defined in the list. - -`gdpr` is optional. It should be 1 if GDPR is in effect, 0 if not, and omitted if the caller is unsure. - -`gdpr_consent` is required if `gdpr` is `1`, and optional otherwise. If present, it should be an [unpadded base64-URL](https://tools.ietf.org/html/rfc4648#page-7) encoded [Vendor Consent String](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md#vendor-consent-string-format-). - -If `gdpr` is omitted, callers are still encouraged to send `gdpr_consent` if they have it. -Depending on how the Prebid Server host company has configured their servers, they may or may not require it for cookie syncs. - -`limit` is optional. If present and greater than zero, it will limit the number of syncs returned to `limit`, dropping some syncs to -get the count down to limit if more would otherwise have been returned. This is to facilitate clients not overloading a user with syncs -the first time they are encountered. - -If the `bidders` field is an empty list, it will not supply any syncs. If the `bidders` field is omitted completely, it will attempt -to sync all bidders. - -### Sample Response - -This will return a JSON object that will allow the client to request cookie syncs with bidders that still need to be synced: - -``` -{ - "status": "ok", - "bidder_status": [ - { - "bidder": "appnexus", - "usersync": { - "url": "someurl.com", - "type": "redirect", - "supportCORS": false - } - } - ] -} -``` diff --git a/docs/endpoints/currency_rates.md b/docs/endpoints/currency_rates.md deleted file mode 100644 index 537713b147e..00000000000 --- a/docs/endpoints/currency_rates.md +++ /dev/null @@ -1,111 +0,0 @@ -## `GET /currency/rates` - -This endpoint exposes active currency rate converter information in the server. -Information are: -- `info.active`: true if currency converter is active -- `info.source`: URL from which rates are fetched -- `info.fetchingIntervalNs`: Fetching interval from source in nanoseconds -- `info.lastUpdated`: Datetime when the rates where updated -- `info.rates`: Internal rates values - -### Sample responses -#### Rate converter active -```json -{ - "active": true, - "info": { - "source": "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json", - "fetchingIntervalNs": 60000000000, - "lastUpdated": "2019-03-02T14:18:41.221063+01:00", - "rates": { - "GBP": { - "AUD": 1.8611576401, - "BGN": 2.2750325703, - "BRL": 5.0061650847, - "CAD": 1.7414619393, - "CHF": 1.3217708915, - "CNY": 8.8791178113, - "CZK": 29.8203982877, - "DKK": 8.6791596873, - "EUR": 1.163223525, - "GBP": 1, - "HKD": 10.3927042621, - "HRK": 8.645077238, - "HUF": 367.6484273218, - "IDR": 18689.5123766983, - "ILS": 4.8077191513, - "INR": 93.8663223525, - "ISK": 158.0820770519, - "JPY": 148.1365159129, - "KRW": 1491.3921459148, - "MXN": 25.5839382096, - "MYR": 5.394332775, - "NOK": 11.3144425833, - "NZD": 1.9374651033, - "PHP": 68.6139028476, - "PLN": 5.0130281035, - "RON": 5.5172855016, - "RUB": 87.2333891681, - "SEK": 12.2141959799, - "SGD": 1.7908989391, - "THB": 42.0074911595, - "TRY": 7.1224176438, - "USD": 1.3240973385, - "ZAR": 18.7774520752 - }, - "USD": { - "AUD": 1.4056048493, - "BGN": 1.7181762277, - "BRL": 3.7808134938, - "CAD": 1.3152068875, - "CHF": 0.9982429939, - "CNY": 6.705789335, - "CZK": 22.5213036985, - "DKK": 6.554774664, - "EUR": 0.8785030308, - "GBP": 0.7552314855, - "HKD": 7.8488974787, - "HRK": 6.5290345252, - "HUF": 277.6596679259, - "IDR": 14114.9081964333, - "ILS": 3.6309408767, - "INR": 70.8908020733, - "ISK": 119.3885618905, - "JPY": 111.8773609769, - "KRW": 1126.3463058948, - "MXN": 19.3217956602, - "MYR": 4.0739699552, - "NOK": 8.5450232803, - "NZD": 1.4632346482, - "PHP": 51.8193797769, - "PLN": 3.7859966617, - "RON": 4.1668277256, - "RUB": 65.8814020908, - "SEK": 9.2245453747, - "SGD": 1.3525432663, - "THB": 31.7253799526, - "TRY": 5.3790740578, - "USD": 1, - "ZAR": 14.1813230256 - } - } - } -} -``` - -#### Rate converter set with constant rates -```json -{ - "active": true, - "source": "", - "fetchingIntervalNs": 0, - "lastUpdated": "0001-01-01T00:00:00Z" -} -``` - -#### Rate converter not set -```json -{ - "active": false -} -``` \ No newline at end of file diff --git a/docs/endpoints/info/bidders.md b/docs/endpoints/info/bidders.md deleted file mode 100644 index 7c6ce960479..00000000000 --- a/docs/endpoints/info/bidders.md +++ /dev/null @@ -1,23 +0,0 @@ -# Prebid Server Bidder List - -## `GET /info/bidders` - -This endpoint returns a list of Bidders supported by Prebid Server. -These are the core values allowed to be used as `request.imp[i].ext.{bidder}` -keys in [Auction](../openrtb2/auction.md) requests. - -For detailed info about a specific Bidder, use [`/info/bidders/{bidderName}`](./bidders/bidderName.md) - -### Sample Response - -This endpoint returns JSON like: - -``` -[ - "appnexus", - "audienceNetwork", - "pubmatic", - "rubicon", - "other-bidders-here" -] -``` diff --git a/docs/endpoints/info/bidders/bidderName.md b/docs/endpoints/info/bidders/bidderName.md deleted file mode 100644 index cd525e640f6..00000000000 --- a/docs/endpoints/info/bidders/bidderName.md +++ /dev/null @@ -1,43 +0,0 @@ -# Prebid Server Bidders - -## `GET /info/bidders/{bidderName}` - -This endpoint returns some metadata about the Bidder whose name is `{bidderName}`. -Legal values for `{bidderName}` can be retrieved from the [/info/bidders](../bidders.md) endpoint. - -### Sample Response - -This endpoint returns JSON like: - -``` -{ - "maintainer": { - "email": "info@prebid.org" - }, - "capabilities": { - "app": { - "mediaTypes": [ - "banner", - "native" - ] - }, - "site": { - "mediaTypes": [ - "banner", - "video", - "native" - ] - } - } -} -``` - -The fields hold the following information: - -- `maintainer.email`: A contact email for the Bidder's maintainer. In general, Bidder bugs should be logged as [issues](https://github.com/prebid/prebid-server/issues)... but this contact email may be useful in case of emergency. -- `capabilities.app.mediaTypes`: A list of media types this Bidder supports from Mobile Apps. -- `capabilities.site.mediaTypes`: A list of media types this Bidder supports from Web pages. - -If `capabilities.app` or `capabilities.site` do not exist, then this Bidder does not support that platform. -OpenRTB Requests which define a `request.app` or `request.site` property will fail if a -`request.imp[i].ext.{bidderName}` exists for a Bidder which doesn't support them. diff --git a/docs/endpoints/openrtb2/amp.md b/docs/endpoints/openrtb2/amp.md deleted file mode 100644 index 16fa451ef36..00000000000 --- a/docs/endpoints/openrtb2/amp.md +++ /dev/null @@ -1,127 +0,0 @@ -# Prebid Server AMP Endpoint - -This document describes the behavior of the Prebid Server AMP endpoint in detail. -For a User's Guide, see the [AMP feature docs](http://prebid.org/dev-docs/show-prebid-ads-on-amp-pages.html). - -## `GET /openrtb2/amp?tag_id={ID}` - -The `tag_id` ID must reference a [Stored BidRequest](../../developers/stored-requests.md#stored-bidrequests). -For a thorough description of BidRequest JSON, see the [/openrtb2/auction](./auction.md) docs. - -To be compatible with AMP, this endpoint behaves slightly different from normal `/openrtb2/auction` requests. - -1. The Stored `request.imp` data must have exactly one element. -2. `request.imp[0].secure` will be always be set to `1`, because AMP requires all content to be `https`. -3. AMP query params will overwrite parts of your Stored Request. For details, see the Query Params section. - -### Request - -Valid Stored Requests for AMP pages must contain an `imp` array with exactly one element. It is not necessary to include a `tmax` field in the Stored Request, as Prebid Server will always use the smaller of the AMP default timeout (1000ms) and the value passed via the `timeoutMillis` field of the `amp-ad.rtc-config`. - -An example Stored Request is given below: - -``` -{ - "id": "some-request-id", - "site": { - "page": "prebid.org" - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { // This is equivalent to the deprecated "pricegranularity": "medium" - "precision": 2, - "ranges": [{ - "max": 20.00, - "increment": 0.10 - }] - } - } - } - }, - "imp": [ - { - "id": "some-impression-id", - "banner": {}, // The sizes are defined is set by your AMP tag query params - "ext": { - "appnexus": { - // Insert parameters here - }, - "rubicon": { - // Insert parameters here - } - } - } - ] -} -``` - -### Response - -A sample response payload looks like this: - -``` -{ - "targeting": { - "hb_bidder": "appnexus", - "hb_bidder_appnexus": "appnexus", - "hb_cache_id": "420d7329-30e8-4c4e-8eaa-fe937172e4e0", - "hb_cache_id_appnexus": "420d7329-30e8-4c4e-8eaa-fe937172e4e0", - "hb_pb": "0.50", - "hb_pb_appnexus": "0.50", - "hb_size": "300x250", - "hb_size_appnexus": "300x250" - } - "errors": { - "openx":[ - { - "code": 1, - "message": "The request exceeded the timeout allocated" - } - ] - } -} -``` - -In [the typical AMP setup](http://prebid.org/dev-docs/show-prebid-ads-on-amp-pages.html), -these targeting params will be sent to DFP. - -Note that "errors" will only appear if there were any errors generated. They are identical to the "errors" field in the response.ext of the OpenRTB endpoint. - -### Query Parameters - -This endpoint supports the following query parameters: - -1. `h` - `amp-ad` `height` -2. `w` - `amp-ad` `width` -3. `oh` - `amp-ad` `data-override-height` -4. `ow` - `amp-ad` `data-override-width` -5. `ms` - `amp-ad` `data-multi-size` -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 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). - -If present, these will override parts of your Stored Request. - -1. `ow`, `oh`, `w`, `h`, and/or `ms` will be used to set `request.imp[0].banner.format` if `request.imp[0].banner` is present. -2. `curl` will be used to set `request.site.page` -3. `timeout` will generally be used to set `request.tmax`. However, the Prebid Server host can [configure](../../developers/configuration.md) their deploy to reduce this timeout for technical reasons. -4. `debug` will be used to set `request.test`, causing the `response.debug` to have extra debugging info in it. - -### Resolving Sizes - -We strive to return ads with sizes which are valid for the `amp-ad` on your page. This logic intends to -track the logic used by `doubleclick` when resolving sizes used to fetch ads from their ad server. - -Specifically: - -1. If `ow` and `oh` exist, `request.imp[0].banner.format` will be a single element with `w: ow` and `h: oh` -2. If `ow` and `h` exist, `request.imp[0].banner.format` will be a single element with `w: ow` and `h: h` -3. If `oh` and `w` exist, `request.imp[0].banner.format` will be a single element with `w: w` and `h: oh` -4. If `ms` exists, `request.imp[0].banner.format` will contain an element for every size it uses. -5. If `w` and `h` exist, `request.imp[0].banner.format` will be a single element with `w: w` and `h: h` -6. If `w` _or_ `h` exist, it will be used to override _one_ of the dimensions inside each element of `request.imp[0].banner.format` -7. If none of these exist then the Stored Request values for `request.imp[0].banner.format` will be used without modification. diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md deleted file mode 100644 index b532923e793..00000000000 --- a/docs/endpoints/openrtb2/auction.md +++ /dev/null @@ -1,789 +0,0 @@ -# Prebid Server Auction Endpoint - -This document describes the behavior of the Prebid Server auction endpoint, including: - -- Request/response formats -- OpenRTB extensions -- Debugging and performance tips -- How user syncing works -- Departures from OpenRTB - -## `POST /openrtb2/auction` - -This endpoint runs an auction with the given OpenRTB 2.5 bid request. - -### Sample request - -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. - -``` -{ - "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 bid response, or -- HTTP 400 if the request is malformed, or -- HTTP 503 if the account or app specified in the request is blacklisted - -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": "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 - } -} -``` - -### OpenRTB Extensions - -#### Conventions - -OpenRTB 2.5 permits exchanges to define their own extensions to any object from the spec. -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`. -The only exception here is the top-level `BidResponse`, because it's bidder-independent. - -`ext.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders. -`ext.prebid` extensions are defined by Prebid Server. - -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. - -#### Bid Adjustments - -Bidders [are encouraged](../../developers/add-new-bidder.md) to make Net bids. However, there's no way for Prebid to enforce this. -If you find that some bidders use Gross bids, publishers can adjust for it with `request.ext.prebid.bidadjustmentfactors`: - -``` -{ - "ext": { - "prebid": { - "bidadjustmentfactors": { - "appnexus": 0.8, - "rubicon": 0.7 - } - } - } -} -``` - -This may also be useful for publishers who want to account for different discrepancies with different bidders. - -#### Targeting - -Targeting refers to strings which are sent to the adserver to -[make header bidding possible](http://prebid.org/overview/intro.html#how-does-prebid-work). - -`request.ext.prebid.targeting` is an optional property which causes Prebid Server -to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.targeting`. - -**Request format** (optional param `request.ext.prebid.targeting`) - -``` -{ - "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 - "includeformat": false // Optional param defaulting to false - } - } - } -} -``` -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. - -For backwards compatibility the following strings will also be allowed as price granularity definitions. There is no guarantee that these will be honored in the future. "One of ['low', 'med', 'high', 'auto', 'dense']" See [price granularity definitions](http://prebid.org/prebid-mobile/adops-price-granularity.html) - -One of "includewinners" or "includebidderkeys" must be true (both default to true if unset). If both were false, then no targeting keys would be set, which is better configured by omitting targeting altogether. - -The parameter "includeformat" indicates the type of the bid (banner, video, etc) for multiformat requests. It will add the key `hb_format` and/or `hb_format_{bidderName}` as per "includewinners" and "includebidderkeys" above. - -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 - } - } -} -``` - -**Response format** (returned in `bid.ext.prebid.targeting`) - -``` -{ - "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." - } - } - } - }] - }] -} -``` - -The winning bid for each `request.imp[i]` will also contain `hb_bidder`, `hb_size`, and `hb_pb` -(with _no_ {bidderName} suffix). To prevent these keys, set `request.ext.prebid.targeting.includeWinners` to false. - -**NOTE**: Targeting keys are limited to 20 characters. If {bidderName} is too long, the returned key -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 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. - -2. The request payload can store a `buyeruid` for each Bidder by defining `request.user.ext.prebid.buyeruids` like so: - -``` -{ - "user": { - "ext": { - "prebid": { - "buyeruids": { - "appnexus": "some-appnexus-id", - "rubicon": "some-rubicon-id" - } - } - } - } -} -``` - -Prebid Server's core logic will preprocess the request so that each Bidder sees their own value in the `request.user.buyeruid` field. - -3. Prebid Server will use its Cookie to map IDs for each Bidder. - -If you're using [Prebid.js](https://github.com/prebid/Prebid.js), this is happening automatically. - -If you're using another client, you can populate the Cookie of the Prebid Server host with User IDs -for each Bidder by using the `/cookie_sync` endpoint, and calling the URLs that it returns in the response. - -#### Native Request - -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 - -Requests can define Bidder aliases if they want to refer to a Bidder by a separate name. -This can be used to request bids from the same Bidder with different params. For example: - -``` -{ - "imp": [{ - "id": "some-impression-id", - "video": { - "mimes": ["video/mp4"] - }, - "ext": { - "appnexus": { - "placementId": 123 - }, - "districtm": { - "placementId": 456 - } - } - }], - "ext": { - "prebid": { - "aliases": { - "districtm": "appnexus" - } - } - } -} -``` - -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 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. -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. - -#### Bidder Response Times - -`response.ext.responsetimemillis.{bidderName}` tells how long each bidder took to respond. -These can help quantify the performance impact of "the slowest bidder." - -#### Bidder Errors - -`response.ext.errors.{bidderName}` contains messages which describe why a request may be "suboptimal". -For example, suppose a `banner` and a `video` impression are offered to a bidder -which only supports `banner`. - -In cases like these, the bidder can ignore the `video` impression and bid on the `banner` one. -However, the publisher can improve performance by only offering impressions which the bidder supports. - -For example, a request may return this in `response.ext` - -``` -{ - "ext": { - "errors": { - "appnexus": [{ - "code": 2, - "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio." - }], - "rubicon": [{ - "code": 1, - "message": "The request exceeded the timeout allocated" - }] - } - } -} -``` - -The codes currently defined are: - -``` -0 NoErrorCode -1 TimeoutCode -2 BadInputCode -3 BadServerResponseCode -999 UnknownErrorCode -``` - -#### Debugging - -`response.ext.debug.httpcalls.{bidder}` will be populated **only if** `request.test` **was set to 1**. - -This contains info about every request and response sent by the bidder to its server. -It is only returned on `test` bids for performance reasons, but may be useful during debugging. - -`response.ext.debug.resolvedrequest` will be populated **only if** `request.test` **was set to 1**. - -This contains the request after the resolution of stored requests and implicit information (e.g. site domain, device user agent). - -#### Stored Requests - -`request.imp[i].ext.prebid.storedrequest` incorporates a [Stored Request](../../developers/stored-requests.md) from the server. - -A typical `storedrequest` value looks like this: - -``` -{ - "imp": [{ - "ext": { - "prebid": { - "storedrequest": { - "id": "some-id" - } - } - } - }] -} -``` - -For more information, see the docs for [Stored Requests](../../developers/stored-requests.md). - -#### Cache bids - -Bids can be temporarily cached on the server by sending the following data as `request.ext.prebid.cache`: - -``` -{ - "ext": { - "prebid": { - "cache": { - "bids": {}, - "vastxml": {} - } - } - } -} -``` - -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 -`bid.ext.prebid.targeting` keys: - -- `hb_cache_id`: On the highest overall Bid in each Imp. -- `hb_cache_id_{bidderName}`: On the highest Bid from {bidderName} in each Imp. - -Clients _should not assume_ that these keys will exist, just because they were requested, though. -If they exist, the value will be a UUID which can be used to fetch Bid JSON from [Prebid Cache](https://github.com/prebid/prebid-cache). -They may not exist if the host company's cache is full, having connection problems, or other issues like that. - -If `vastxml` is present, PBS will try to add analogous keys `hb_uuid` and `hb_uuid_{bidderName}`. -In addition to the caveats above, these will exist _only if the relevant Bids are for Video_. -If they exist, the values can be used to fetch the bid's VAST XML from Prebid Cache directly. - -These options are mainly intended for certain limited Prebid Mobile setups, where bids cannot be cached client-side. - -#### GDPR - -Prebid Server supports the IAB's GDPR recommendations, which can be found [here](https://iabtechlab.com/wp-content/uploads/2018/02/OpenRTB_Advisory_GDPR_2018-02.pdf). - -This adds two optional properties: - -- `request.user.ext.consent`: Is the consent string required by the IAB standards. -- `request.regs.ext.gdpr`: Is 0 if the caller believes that the user is *not* under GDPR, 1 if the user *is* under GDPR, and undefined if we're not certain. - -These fields will be forwarded to each Bidder, so they can decide how to process them. - -#### Interstitial support -Additional support for interstitials is enabled through the addition of two fields to the request: -device.ext.prebid.interstitial.minwidthperc and device.ext.interstial.minheightperc -The values will be numbers that indicate the minimum allowed size for the ad, as a percentage of the base side. For example, a width of 600 and "minwidthperc": 60 would allow ads with widths from 360 to 600 pixels inclusive. - -Example: -``` -{ - "imp": [{ - ... - "banner": { - ... - } - "instl": 1, - ... - }] - "device": { - ... - "h": 640, - "w": 320, - "ext": { - "prebid": { - "interstitial": { - "minwidthperc": 60, - "minheightperc": 60 - } - } - } - } -} -``` - -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-responses at the bidder adapter level - -**Single Stored Auction Response ID** - -When a storedauctionresponse ID is specified: - -- the rest of the ext.prebid block is irrelevant and ignored -- nothing is sent to any bidder adapter for that imp -- the response retrieved from the stored-response-id is assumed to be the entire contents of the seatbid object corresponding to that impression. - -This request: -``` -{ - "test":1, - "tmax":500, - "id": "test-auction-id", - "app": { ... }, - "ext": { - "prebid": { - "targeting": {}, - "cache": { "bids": {} } - } - }, - "imp": [ - { - "id": "a", - "ext": { "prebid": { "storedauctionresponse": { "id": "1111111111" } } } - }, - { - "id": "b", - "ext": { "prebid": { "storedauctionresponse": { "id": "22222222222" } } } - } - ] -} -``` - -Will result in this response, assuming that the ids exist in the appropriate DB table read by Prebid Server: -``` -{ - "id": "test-auction-id", - "seatbid": [ - { - // BidderA bids from storedauctionresponse=1111111111 - // BidderA bids from storedauctionresponse=22222222 - }, - { - // BidderB bids from storedauctionresponse=1111111111 - // BidderB bids from storedauctionresponse=22222222 - } - ] -} -``` - -**Multiple Stored Bid Response IDs** - -In contrast to what's outlined above, this approach lets some real auctions take place while some bidders have test responses that still exercise bidder code. For example, this request: - -``` -{ - "test":1, - "tmax":500, - "id": "test-auction-id", - "app": { ... }, - "ext": { - "prebid": { - "targeting": {}, - "cache": { "bids": {} } - } - }, - "imp": [ - { - "id": "a", - "ext": { - "prebid": { - "storedbidresponse": [ - { "bidder": "BidderA", "id": "333333" }, - { "bidder": "BidderB", "id": "444444" }, - ] - } - } - }, - { - "id": "b", - "ext": { - "prebid": { - "storedbidresponse": [ - { "bidder": "BidderA", "id": "5555555" }, - { "bidder": "BidderB", "id": "6666666" }, - ] - } - } - } - ] -} -``` -Could result in this response: - -``` -{ - "id": "test-auction-id", - "seatbid": [ - { - "bid": [ - // contents of storedbidresponse=3333333 as parsed by bidderA adapter - // contents of storedbidresponse=5555555 as parsed by bidderA adapter - ] - }, - { - // contents of storedbidresponse=4444444 as parsed by bidderB adapter - // contents of storedbidresponse=6666666 as parsed by bidderB adapter - } - ] -} -``` - -Setting up the storedresponse DB entries is the responsibility of each Prebid Server host company. - -See Prebid.org troubleshooting pages for how to utilize this feature within the context of the browser. - - -#### User IDs (PBS-Java only) - -Prebid Server adapters can support the [Prebid.js User ID modules](http://prebid.org/dev-docs/modules/userId.html) by reading the following extensions and passing them through to their server endpoints: - -``` -{ - "user": { - "ext": { - "eids": [{ - "source": "adserver.org", - "uids": [{ - "id": "111111111111", - "ext": { - "rtiPartner": "TDID" - } - }] - }, - { - "source": "pubcommon", - "id":"11111111" - } - ], - "digitrust": { - "id": "11111111111", - "keyv": 4 - } - } - } -} -``` - -#### First Party Data Support (PBS-Java only) - -This is the Prebid Server version of the Prebid.js First Party Data feature. It's a standard way for the page (or app) to supply first party data and control which bidders have access to it. - -It specifies where in the OpenRTB request non-standard attributes should be passed. For example: - -``` -{ - "ext": { - "prebid": { - "data": { "bidders": [ "rubicon", "appnexus" ] } // these are the bidders allowed to see protected data - } - }, - "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": { - 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 - } - } - ] -``` - -Prebid Server enforces the data permissioning - -So before passing the values to the bidder adapters, core will: - -1. check for ext.prebid.data.bidders -1. if it exists, store it locally, but remove it from the OpenRTB before being sent to the adapters -1. As the OpenRTB request is being sent to each adapter: - 1. if ext.prebid.data.bidders exists in the original request, and this bidder is on the list then copy site.ext.data, app.ext.data, and user.ext.data to their bidder request -- otherwise don't copy those blocks - 1. copy other objects as normal - -Each adapter must be coded to read the values from these locations and pass it to their endpoints appropriately. - -### OpenRTB Ambiguities - -This section describes the ways in which Prebid Server **implements** OpenRTB spec ambiguous parts. - -- `request.cur`: If `request.cur` is not specified in the bid request, Prebid Server will consider it as being `USD` whereas OpenRTB spec doesn't mention any default currency for bid request. -```request.cur: ['USD'] // Default value if not set``` - - -### OpenRTB Differences - -This section describes the ways in which Prebid Server **breaks** the OpenRTB spec. - -#### Allowed Bidders - -Prebid Server returns a 400 on requests which define `wseat` or `bseat`. -We may add support for these in the future, if there's compelling need. - -Instead, an impression is only offered to a bidder if `bidrequest.imp[i].ext.{bidderName}` exists. - -This supports publishers who want to sell different impressions to different bidders. - -#### Deprecated Properties - -This endpoint returns a 400 if the request contains deprecated properties (e.g. `imp.wmin`, `imp.hmax`). - -The error message in the response should describe how to "fix" the request to make it legal. -If the message is unclear, please [log an issue](https://github.com/prebid/prebid-server/issues) -or [submit a pull request](https://github.com/prebid/prebid-server/pulls) to improve it. - -#### Determining Bid Security (http/https) - -In the OpenRTB spec, `request.imp[i].secure` says: - -> Flag to indicate if the impression requires secure HTTPS URL creative assets and markup, -> where 0 = non-secure, 1 = secure. If omitted, the secure state is unknown, but non-secure -> HTTP support can be assumed. - -In Prebid Server, an `https` request which does not define `secure` will be forwarded to Bidders with a `1`. -Publishers who run `https` sites and want insecure ads can still set this to `0` explicitly. - -### See also - -- [The OpenRTB 2.5 spec](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf) diff --git a/docs/endpoints/setuid.md b/docs/endpoints/setuid.md deleted file mode 100644 index c1746806371..00000000000 --- a/docs/endpoints/setuid.md +++ /dev/null @@ -1,26 +0,0 @@ -# Saving User Syncs - -This endpoint is used during cookie syncs. For technical details, see the -[Cookie Sync developer docs](../developers/cookie-syncs.md). - -## `GET /setuid` - -This endpoint saves a UserID for a Bidder in the Cookie. Saved IDs will be recognized for 7 days before being considered "stale" and being re-synced. - -### Query Params - -- `bidder`: The FamilyName of the [Usersyncer](../../usersync/usersync.go) which is being synced. -- `uid`: The ID which the Bidder uses to recognize this user. If undefined, the UID for `bidder` will be deleted. -- `gdpr`: This should be `1` if GDPR is in effect, `0` if not, and undefined if the caller isn't sure -- `gdpr_consent`: This is required if `gdpr` is one, and optional (but encouraged) otherwise. If present, it should be an [unpadded base64-URL](https://tools.ietf.org/html/rfc4648#page-7) encoded [Vendor Consent String](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md#vendor-consent-string-format-). - -If the `gdpr` and `gdpr_consent` params are included, this endpoint will _not_ write a cookie unless: - -1. The Vendor ID set by the Prebid Server host company has permission to save cookies for that user. -2. The Prebid Server host company did not configure it to run with GDPR support. - -If in doubt, contact the company hosting Prebid Server and ask if they're GDPR-ready. - -### Sample request - -`GET http://prebid.site.com/setuid?bidder=adnxs&uid=12345&gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw` diff --git a/docs/endpoints/status.md b/docs/endpoints/status.md deleted file mode 100644 index 0c252397423..00000000000 --- a/docs/endpoints/status.md +++ /dev/null @@ -1,9 +0,0 @@ -## `GET /status` - -This endpoint will return a 2xx response whenever Prebid Server is ready to serve requests. -Its exact response can be [configured](../developers/configuration.md) with the `status_response` -config option. For example, in `pbs.yaml`: - -```yaml -status_response: "ok" -``` From ceaf883f3d94332ef8678e8e1694e48dbce56020 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Tue, 25 Aug 2020 15:05:13 -0700 Subject: [PATCH 181/381] Fix bid dedup (#1456) Co-authored-by: Veronika Solovei --- exchange/exchange.go | 2 +- exchange/exchange_test.go | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/exchange/exchange.go b/exchange/exchange.go index e465a78389b..1fbdfe8ea67 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -660,7 +660,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques // 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) + seatBidsToRemove = append(seatBidsToRemove, dupe.bidderName) rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") } else { oldSeatBid.bids = append(oldSeatBid.bids[:dupe.bidIndex], oldSeatBid.bids[dupe.bidIndex+1:]...) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index a6f69f70c59..d1531237688 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -1619,6 +1619,78 @@ func TestBidRejectionErrors(t *testing.T) { } } +func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + requestExt := newExtRequestTranslateCategories(nil) + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{30} + requestExt.Prebid.Targeting.IncludeBrandCategory.WithCategory = false + + cats1 := []string{"IAB1-3"} + cats2 := []string{"IAB1-4"} + + bidApn1 := openrtb.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bidApn2 := openrtb.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1} + + bid1_Apn1 := pbsOrtbBid{&bidApn1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + bid1_Apn2 := pbsOrtbBid{&bidApn2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0} + + innerBidsApn1 := []*pbsOrtbBid{ + &bid1_Apn1, + } + + innerBidsApn2 := []*pbsOrtbBid{ + &bid1_Apn2, + } + + for i := 1; i < 10; i++ { + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + seatBidApn1 := pbsOrtbSeatBid{innerBidsApn1, "USD", nil, nil} + bidderNameApn1 := openrtb_ext.BidderName("appnexus1") + + seatBidApn2 := pbsOrtbSeatBid{innerBidsApn2, "USD", nil, nil} + bidderNameApn2 := openrtb_ext.BidderName("appnexus2") + + adapterBids[bidderNameApn1] = &seatBidApn1 + adapterBids[bidderNameApn2] = &seatBidApn2 + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData) + + assert.NoError(t, err, "Category mapping error should be empty") + assert.Len(t, rejections, 1, "There should be 1 bid rejection message") + assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_idApn(1|2)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected") + assert.Len(t, bidCategory, 1, "Bidders category mapping should have only one element") + + var resultBid string + for bidId := range bidCategory { + resultBid = bidId + } + + if resultBid == "bid_idApn1" { + assert.Nil(t, seatBidApn2.bids, "Appnexus_2 seat bid should not have any bids back") + assert.Len(t, seatBidApn1.bids, 1, "Appnexus_1 seat bid should have only one back") + + } else { + assert.Nil(t, seatBidApn1.bids, "Appnexus_1 seat bid should not have any bids back") + assert.Len(t, seatBidApn2.bids, 1, "Appnexus_2 seat bid should have only one back") + + } + + } + +} + func TestUpdateRejections(t *testing.T) { rejections := []string{} From 1c9b521f0f98086b6f27c00762353d78977bc54f Mon Sep 17 00:00:00 2001 From: Daniel Cassidy Date: Thu, 27 Aug 2020 15:36:30 +0100 Subject: [PATCH 182/381] consumable: Correct width and height reported in response. (#1459) Prebid Server now responds with the width and height specified in the Bid Response from Consumable. Previously it would reuse the width and height specified in the Bid Request. That older behaviour was ported from an older version of the prebid.js adapter but is no longer valid. --- adapters/consumable/consumable.go | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/adapters/consumable/consumable.go b/adapters/consumable/consumable.go index 243f1b8000b..ff7451f15f7 100644 --- a/adapters/consumable/consumable.go +++ b/adapters/consumable/consumable.go @@ -69,6 +69,8 @@ type decision struct { CreativeID string `json:"creativeId,omitempty"` Contents []contents `json:"contents"` ImpressionUrl *string `json:"impressionUrl,omitempty"` + Width uint64 `json:"width,omitempty"` // Consumable extension, not defined by Adzerk + Height uint64 `json:"height,omitempty"` // Consumable extension, not defined by Adzerk } type contents struct { @@ -241,23 +243,13 @@ func (a *ConsumableAdapter) MakeBids( for impID, decision := range serverResponse.Decisions { if decision.Pricing != nil && decision.Pricing.ClearPrice != nil { - - imp := getImp(impID, internalRequest.Imp) - if imp == nil { - errors = append(errors, &errortypes.BadServerResponse{ - Message: fmt.Sprintf( - "ignoring bid id=%s, request doesn't contain any impression with id=%s", internalRequest.ID, impID), - }) - continue - } - bid := openrtb.Bid{} bid.ID = internalRequest.ID bid.ImpID = impID bid.Price = *decision.Pricing.ClearPrice bid.AdM = retrieveAd(decision) - bid.W = imp.Banner.Format[0].W // TODO: Review to check if this is correct behaviour - bid.H = imp.Banner.Format[0].H + bid.W = decision.Width + bid.H = decision.Height bid.CrID = strconv.FormatInt(decision.AdID, 10) bid.Exp = 30 // TODO: Check this is intention of TTL @@ -279,15 +271,6 @@ func (a *ConsumableAdapter) MakeBids( return bidderResponse, errors } -func getImp(impId string, imps []openrtb.Imp) *openrtb.Imp { - for _, imp := range imps { - if imp.ID == impId { - return &imp - } - } - return nil -} - func extractExtensions(impression openrtb.Imp) (*adapters.ExtImpBidder, *openrtb_ext.ExtImpConsumable, []error) { var bidderExt adapters.ExtImpBidder if err := json.Unmarshal(impression.Ext, &bidderExt); err != nil { From 1f8749789d261d23858db3724d01aaad6417c503 Mon Sep 17 00:00:00 2001 From: guscarreon Date: Thu, 27 Aug 2020 13:35:37 -0400 Subject: [PATCH 183/381] Panics happen when left with zero length []Imp (#1462) --- adapters/info.go | 8 +++ adapters/info_test.go | 142 +++++++++++++++++++++++++++++++----------- 2 files changed, 114 insertions(+), 36 deletions(-) diff --git a/adapters/info.go b/adapters/info.go index 732ae85589b..982827c90fb 100644 --- a/adapters/info.go +++ b/adapters/info.go @@ -37,6 +37,7 @@ type InfoAwareBidder struct { func (i *InfoAwareBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *ExtraRequestInfo) ([]*RequestData, []error) { var allowedMediaTypes parsedSupports + if request.Site != nil { if !i.info.site.enabled { return nil, []error{BadInput("this bidder does not support site requests")} @@ -56,7 +57,14 @@ func (i *InfoAwareBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *Ext // To avoid allocating new arrays and copying in the normal case, we'll make one pass to // see if any imps need to be removed, and another to do the removing if necessary. numToFilter, errs := i.pruneImps(request.Imp, allowedMediaTypes) + + // If all imps in bid request come with unsupported media types, exit + if numToFilter == len(request.Imp) { + return nil, append(errs, BadInput("Bid request didn't contain media types supported by the bidder")) + } + if numToFilter != 0 { + // Filter out imps with unsupported media types filteredImps, newErrs := i.filterImps(request.Imp, numToFilter) request.Imp = filteredImps errs = append(errs, newErrs...) diff --git a/adapters/info_test.go b/adapters/info_test.go index 9c0dd16babb..ce0f88696db 100644 --- a/adapters/info_test.go +++ b/adapters/info_test.go @@ -24,6 +24,7 @@ func TestAppNotSupported(t *testing.T) { } constrained := adapters.EnforceBidderInfo(bidder, info) bids, errs := constrained.MakeRequests(&openrtb.BidRequest{ + Imp: []openrtb.Imp{{ID: "imp-1", Banner: &openrtb.Banner{}}}, App: &openrtb.App{}, }, &adapters.ExtraRequestInfo{}) if !assert.Len(t, errs, 1) { @@ -45,6 +46,7 @@ func TestSiteNotSupported(t *testing.T) { } constrained := adapters.EnforceBidderInfo(bidder, info) bids, errs := constrained.MakeRequests(&openrtb.BidRequest{ + Imp: []openrtb.Imp{{ID: "imp-1", Banner: &openrtb.Banner{}}}, Site: &openrtb.Site{}, }, &adapters.ExtraRequestInfo{}) if !assert.Len(t, errs, 1) { @@ -69,48 +71,111 @@ func TestImpFiltering(t *testing.T) { } constrained := adapters.EnforceBidderInfo(bidder, info) - _, errs := constrained.MakeRequests(&openrtb.BidRequest{ - Imp: []openrtb.Imp{ - { - ID: "imp-1", - Video: &openrtb.Video{}, + + testCases := []struct { + description string + inBidRequest *openrtb.BidRequest + expectedErrors []error + expectedImpLen int + }{ + { + description: "Empty Imp array. MakeRequest() call not expected", + inBidRequest: &openrtb.BidRequest{ + Imp: []openrtb.Imp{}, + Site: &openrtb.Site{}, }, - { - Native: &openrtb.Native{}, + expectedErrors: []error{ + &errortypes.BadInput{Message: "Bid request didn't contain media types supported by the bidder"}, }, - { - ID: "imp-2", - Video: &openrtb.Video{}, - Native: &openrtb.Native{}, + expectedImpLen: 0, + }, + { + description: "Sole imp in bid request is of wrong media type. MakeRequest() call not expected", + inBidRequest: &openrtb.BidRequest{ + Imp: []openrtb.Imp{{ID: "imp-1", Video: &openrtb.Video{}}}, + App: &openrtb.App{}, }, - { - Banner: &openrtb.Banner{}, + expectedErrors: []error{ + &errortypes.BadInput{Message: "request.imp[0] uses video, but this bidder doesn't support it"}, + &errortypes.BadInput{Message: "Bid request didn't contain media types supported by the bidder"}, }, + expectedImpLen: 0, + }, + { + description: "All imps in bid request of wrong media type, MakeRequest() call not expected", + inBidRequest: &openrtb.BidRequest{ + Imp: []openrtb.Imp{ + {ID: "imp-1", Video: &openrtb.Video{}}, + {ID: "imp-2", Native: &openrtb.Native{}}, + {ID: "imp-3", Audio: &openrtb.Audio{}}, + }, + App: &openrtb.App{}, + }, + expectedErrors: []error{ + &errortypes.BadInput{Message: "request.imp[0] uses video, but this bidder doesn't support it"}, + &errortypes.BadInput{Message: "request.imp[1] uses native, but this bidder doesn't support it"}, + &errortypes.BadInput{Message: "request.imp[2] uses audio, but this bidder doesn't support it"}, + &errortypes.BadInput{Message: "Bid request didn't contain media types supported by the bidder"}, + }, + expectedImpLen: 0, + }, + { + description: "Some imps with correct media type, MakeRequest() call expected", + inBidRequest: &openrtb.BidRequest{ + Imp: []openrtb.Imp{ + { + ID: "imp-1", + Video: &openrtb.Video{}, + }, + { + Native: &openrtb.Native{}, + }, + { + ID: "imp-2", + Video: &openrtb.Video{}, + Native: &openrtb.Native{}, + }, + { + Banner: &openrtb.Banner{}, + }, + }, + Site: &openrtb.Site{}, + }, + expectedErrors: []error{ + &errortypes.BadInput{Message: "request.imp[1] uses native, but this bidder doesn't support it"}, + &errortypes.BadInput{Message: "request.imp[2] uses native, but this bidder doesn't support it"}, + &errortypes.BadInput{Message: "request.imp[3] uses banner, but this bidder doesn't support it"}, + &errortypes.BadInput{Message: "request.imp[1] has no supported MediaTypes. It will be ignored"}, + &errortypes.BadInput{Message: "request.imp[3] has no supported MediaTypes. It will be ignored"}, + }, + expectedImpLen: 2, + }, + { + description: "All imps with correct media type, MakeRequest() call expected", + inBidRequest: &openrtb.BidRequest{ + Imp: []openrtb.Imp{ + {ID: "imp-1", Video: &openrtb.Video{}}, + {ID: "imp-2", Video: &openrtb.Video{}}, + }, + Site: &openrtb.Site{}, + }, + expectedErrors: nil, + expectedImpLen: 2, }, - Site: &openrtb.Site{}, - }, &adapters.ExtraRequestInfo{}) - if !assert.Len(t, errs, 6) { - return } - assert.EqualError(t, errs[0], "request.imp[1] uses native, but this bidder doesn't support it") - assert.EqualError(t, errs[1], "request.imp[2] uses native, but this bidder doesn't support it") - assert.EqualError(t, errs[2], "request.imp[3] uses banner, but this bidder doesn't support it") - assert.EqualError(t, errs[3], "request.imp[1] has no supported MediaTypes. It will be ignored") - assert.EqualError(t, errs[4], "request.imp[3] has no supported MediaTypes. It will be ignored") - assert.EqualError(t, errs[5], "mock MakeRequests error") - assert.IsType(t, &errortypes.BadInput{}, errs[0]) - assert.IsType(t, &errortypes.BadInput{}, errs[1]) - assert.IsType(t, &errortypes.BadInput{}, errs[2]) - assert.IsType(t, &errortypes.BadInput{}, errs[3]) - assert.IsType(t, &errortypes.BadInput{}, errs[4]) - req := bidder.gotRequest - if !assert.Len(t, req.Imp, 2) { - return + for _, test := range testCases { + actualAdapterRequests, actualErrs := constrained.MakeRequests(test.inBidRequest, &adapters.ExtraRequestInfo{}) + + // Assert the request.Imp slice was correctly filtered and if MakeRequest() was called by asserting + // the corresponding error messages were returned + for i, expectedErr := range test.expectedErrors { + assert.EqualError(t, expectedErr, actualErrs[i].Error(), "Test failed. Error[%d] in error list mismatch: %s", i, test.description) + } + + // Extra MakeRequests() call check: our mockBidder returns an adapter request for every imp + assert.Len(t, actualAdapterRequests, test.expectedImpLen, "Test failed. Incorrect lenght of filtered imps: %s", test.description) } - assert.Equal(t, "imp-1", req.Imp[0].ID) - assert.Equal(t, "imp-2", req.Imp[1].ID) - assert.Nil(t, req.Imp[1].Native) } type mockBidder struct { @@ -118,8 +183,13 @@ type mockBidder struct { } func (m *mockBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - m.gotRequest = request - return nil, []error{errors.New("mock MakeRequests error")} + var adapterRequests []*adapters.RequestData + + for i := 0; i < len(request.Imp); i++ { + adapterRequests = append(adapterRequests, &adapters.RequestData{}) + } + + return adapterRequests, nil } func (m *mockBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { From 292df1f3f5da623c6c140542510da026b1c3cbd2 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Thu, 27 Aug 2020 14:19:25 -0400 Subject: [PATCH 184/381] Add Scheme Option To External Cache URL (#1460) --- config/config.go | 14 +- config/config_test.go | 17 +++ endpoints/openrtb2/video_auction_test.go | 4 +- exchange/auction_test.go | 17 ++- exchange/exchange.go | 61 +++++--- exchange/exchange_test.go | 141 +++++++++++++++++- .../exchangetest/targeting-cache-vast.json | 2 +- .../exchangetest/targeting-cache-zero.json | 2 +- prebid_cache_client/client.go | 30 ++-- prebid_cache_client/client_test.go | 74 +++++---- 10 files changed, 284 insertions(+), 78 deletions(-) diff --git a/config/config.go b/config/config.go index e3b7d8ebda0..c2e7dfd8f3f 100755 --- a/config/config.go +++ b/config/config.go @@ -136,6 +136,10 @@ func (data *ExternalCache) validate(errs configErrors) configErrors { return errs } + if data.Scheme != "" && data.Scheme != "http" && data.Scheme != "https" { + return append(errs, errors.New("External cache Scheme must be http or https if specified")) + } + // Either host or path or both not empty, validate. if data.Host == "" && data.Path != "" || data.Host != "" && data.Path == "" { return append(errs, errors.New("External cache Host and Path must both be specified")) @@ -486,13 +490,14 @@ type DataCache struct { TTLSeconds int `mapstructure:"ttl_seconds"` } -// Data type where we store the external cache URL elements. This is completely unrelated to type Cache struct defined afterwards, because -// the latter is used for internal cache URL while the following contains information of the external cache URL. +// ExternalCache configures the externally accessible cache url. type ExternalCache struct { - Host string `mapstructure:"host"` - Path string `mapstructure:"path"` + Scheme string `mapstructure:"scheme"` + Host string `mapstructure:"host"` + Path string `mapstructure:"path"` } +// Cache configures the url used internally by Prebid Server to communicate with Prebid Cache. type Cache struct { Scheme string `mapstructure:"scheme"` Host string `mapstructure:"host"` @@ -734,6 +739,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("cache.default_ttl_seconds.video", 0) v.SetDefault("cache.default_ttl_seconds.native", 0) v.SetDefault("cache.default_ttl_seconds.audio", 0) + v.SetDefault("external_cache.scheme", "") v.SetDefault("external_cache.host", "") v.SetDefault("external_cache.path", "") v.SetDefault("recaptcha_secret", "") diff --git a/config/config_test.go b/config/config_test.go index 3da3f72137b..40589ac7b23 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -90,6 +90,21 @@ func TestExternalCacheURLValidate(t *testing.T) { data: ExternalCache{Host: "http://", Path: ""}, expErrors: 1, }, + { + desc: "Scheme Invalid", + data: ExternalCache{Scheme: "invalid", Host: "www.google.com", Path: "/path/v1"}, + expErrors: 1, + }, + { + desc: "Scheme HTTP", + data: ExternalCache{Scheme: "http", Host: "www.google.com", Path: "/path/v1"}, + expErrors: 0, + }, + { + desc: "Scheme HTTPS", + data: ExternalCache{Scheme: "https", Host: "www.google.com", Path: "/path/v1"}, + expErrors: 0, + }, } for _, test := range testCases { var errs configErrors @@ -150,6 +165,7 @@ cache: host: prebidcache.net query: uuid=%PBS_CACHE_UUID% external_cache: + scheme: https host: www.externalprebidcache.net path: /endpoints/cache http_client: @@ -307,6 +323,7 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "cache.scheme", cfg.CacheURL.Scheme, "http") cmpStrings(t, "cache.host", cfg.CacheURL.Host, "prebidcache.net") cmpStrings(t, "cache.query", cfg.CacheURL.Query, "uuid=%PBS_CACHE_UUID%") + cmpStrings(t, "external_cache.scheme", cfg.ExtCacheURL.Scheme, "https") cmpStrings(t, "external_cache.host", cfg.ExtCacheURL.Host, "www.externalprebidcache.net") cmpStrings(t, "external_cache.path", cfg.ExtCacheURL.Path, "/endpoints/cache") cmpInts(t, "http_client.max_connections_per_host", cfg.Client.MaxConnsPerHost, 10) diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 534db3c79e2..78715f5c87d 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1259,8 +1259,8 @@ func (m *mockCacheClient) PutJson(ctx context.Context, values []prebid_cache_cli return []string{}, []error{} } -func (m *mockCacheClient) GetExtCacheData() (string, string) { - return "", "" +func (m *mockCacheClient) GetExtCacheData() (scheme string, host string, path string) { + return "", "", "" } type mockVideoStoredReqFetcher struct { diff --git a/exchange/auction_test.go b/exchange/auction_test.go index e23c45cb494..9f24682bfc3 100644 --- a/exchange/auction_test.go +++ b/exchange/auction_test.go @@ -298,22 +298,27 @@ type pbsBid struct { Bidder openrtb_ext.BidderName `json:"bidder"` } -type mockCache struct { - items []prebid_cache_client.Cacheable -} - type cacheComparator struct { freq int expectedKeys []string actualKeys []string } -func (c *mockCache) GetExtCacheData() (string, string) { - return "", "" +type mockCache struct { + scheme string + host string + path string + items []prebid_cache_client.Cacheable +} + +func (c *mockCache) GetExtCacheData() (scheme string, host string, path string) { + return c.scheme, c.host, c.path } + func (c *mockCache) GetPutUrl() string { return "" } + func (c *mockCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) { c.items = values return []string{"", "", "", "", ""}, nil diff --git a/exchange/exchange.go b/exchange/exchange.go index 1fbdfe8ea67..59e876697cf 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -8,6 +8,7 @@ import ( "fmt" "math/rand" "net/http" + "net/url" "runtime/debug" "sort" "strconv" @@ -107,7 +108,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque shouldCacheBids, shouldCacheVAST := getExtCacheInfo(requestExt) targData := getExtTargetData(requestExt, shouldCacheBids, shouldCacheVAST) if targData != nil { - targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() + _, targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() } debugInfo := getDebugInfo(bidRequest, requestExt) @@ -814,24 +815,50 @@ func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName, a // If bid got cached inside `(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)`, // a UUID should be found inside `a.cacheIds` or `a.vastCacheIds`. This function returns the UUID along with the internal cache URL -func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auc *auction) (openrtb_ext.ExtBidPrebidCacheBids, bool) { - var cacheInfo openrtb_ext.ExtBidPrebidCacheBids - var cacheUUID string - var found bool = false - - if auc != nil { - var extCacheHost, extCachePath string - if cacheUUID, found = auc.cacheIds[bid.bid]; found { - cacheInfo.CacheId = cacheUUID - extCacheHost, extCachePath = e.cache.GetExtCacheData() - cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID - } else if cacheUUID, found = auc.vastCacheIds[bid.bid]; found { - cacheInfo.CacheId = cacheUUID - extCacheHost, extCachePath = e.cache.GetExtCacheData() - cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID +func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auction *auction) (cacheInfo openrtb_ext.ExtBidPrebidCacheBids, found bool) { + uuid, found := findCacheID(bid, auction) + + if found { + cacheInfo.CacheId = uuid + cacheInfo.Url = buildCacheURL(e.cache, uuid) + } + + return +} + +func findCacheID(bid *pbsOrtbBid, auction *auction) (string, bool) { + if bid != nil && bid.bid != nil && auction != nil { + if id, found := auction.cacheIds[bid.bid]; found { + return id, true + } + + if id, found := auction.vastCacheIds[bid.bid]; found { + return id, true } } - return cacheInfo, found + + return "", false +} + +func buildCacheURL(cache prebid_cache_client.Client, uuid string) string { + scheme, host, path := cache.GetExtCacheData() + + if host == "" || path == "" { + return "" + } + + query := url.Values{"uuid": []string{uuid}} + cacheURL := url.URL{ + Scheme: scheme, + Host: host, + Path: path, + RawQuery: query.Encode(), + } + cacheURL.Query() + + // URLs without a scheme will begin with //, in which case we + // want to trim it off to keep compatbile with current behavior. + return strings.TrimPrefix(cacheURL.String(), "//") } func listBiddersWithRequests(cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest) []openrtb_ext.BidderName { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index d1531237688..efabb845211 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -274,9 +274,10 @@ func TestDebugBehaviour(t *testing.T) { } } -func TestGetBidCacheInfo(t *testing.T) { +func TestGetBidCacheInfoEndToEnd(t *testing.T) { testUUID := "CACHE_UUID_1234" - testExternalCacheHost := "https://www.externalprebidcache.net" + testExternalCacheScheme := "https" + testExternalCacheHost := "www.externalprebidcache.net" testExternalCachePath := "endpoints/cache" /* 1) An adapter */ @@ -292,8 +293,9 @@ func TestGetBidCacheInfo(t *testing.T) { Host: "www.internalprebidcache.net", }, ExtCacheURL: config.ExternalCache{ - Host: testExternalCacheHost, - Path: testExternalCachePath, + Scheme: testExternalCacheScheme, + Host: testExternalCacheHost, + Path: testExternalCachePath, }, } adapterList := make([]openrtb_ext.BidderName, 0, 2) @@ -421,7 +423,7 @@ func TestGetBidCacheInfo(t *testing.T) { Seat: string(bidderName), Bid: []openrtb.Bid{ { - Ext: json.RawMessage(`{ "prebid": { "cache": { "bids": { "cacheId": "` + testUUID + `", "url": "` + testExternalCacheHost + `/` + testExternalCachePath + `?uuid=` + testUUID + `" }, "key": "", "url": "" }`), + Ext: json.RawMessage(`{ "prebid": { "cache": { "bids": { "cacheId": "` + testUUID + `", "url": "` + testExternalCacheScheme + `://` + testExternalCacheHost + `/` + testExternalCachePath + `?uuid=` + testUUID + `" }, "key": "", "url": "" }`), }, }, }, @@ -446,6 +448,131 @@ func TestGetBidCacheInfo(t *testing.T) { assert.Equal(t, expCacheURL, cacheURL, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheURL) } +func TestGetBidCacheInfo(t *testing.T) { + bid := &openrtb.Bid{ID: "42"} + testCases := []struct { + description string + scheme string + host string + path string + bid *pbsOrtbBid + auction *auction + expectedFound bool + expectedCacheID string + expectedCacheURL string + }{ + { + description: "JSON Cache ID", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "https://prebid.org/cache?uuid=anyID", + }, + { + description: "VAST Cache ID", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{vastCacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "https://prebid.org/cache?uuid=anyID", + }, + { + description: "Cache ID Not Found", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{}, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + { + description: "Scheme Not Provided", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "prebid.org/cache?uuid=anyID", + }, + { + description: "Host And Path Not Provided - Without Scheme", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "", + }, + { + description: "Host And Path Not Provided - With Scheme", + scheme: "https", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "", + }, + { + description: "Nil Bid", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: nil, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + { + description: "Nil Embedded Bid", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: nil}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + { + description: "Nil Auction", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: nil, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + } + + for _, test := range testCases { + exchange := &exchange{ + cache: &mockCache{ + scheme: test.scheme, + host: test.host, + path: test.path, + }, + } + + cacheInfo, found := exchange.getBidCacheInfo(test.bid, test.auction) + + assert.Equal(t, test.expectedFound, found, test.description+":found") + assert.Equal(t, test.expectedCacheID, cacheInfo.CacheId, test.description+":id") + assert.Equal(t, test.expectedCacheURL, cacheInfo.Url, test.description+":url") + } +} + func TestBidResponseCurrency(t *testing.T) { // Init objects cfg := &config.Configuration{Adapters: make(map[string]config.Adapter, 1)} @@ -2176,8 +2303,8 @@ func mockSlowHandler(delay time.Duration, statusCode int, body string) http.Hand type wellBehavedCache struct{} -func (c *wellBehavedCache) GetExtCacheData() (string, string) { - return "www.pbcserver.com", "/pbcache/endpoint" +func (c *wellBehavedCache) GetExtCacheData() (scheme string, host string, path string) { + return "https", "www.pbcserver.com", "/pbcache/endpoint" } func (c *wellBehavedCache) PutJson(ctx context.Context, values []pbc.Cacheable) ([]string, []error) { diff --git a/exchange/exchangetest/targeting-cache-vast.json b/exchange/exchangetest/targeting-cache-vast.json index f348dd1b29d..53a48c4ec69 100644 --- a/exchange/exchangetest/targeting-cache-vast.json +++ b/exchange/exchangetest/targeting-cache-vast.json @@ -67,7 +67,7 @@ "cache": { "bids": { "cacheId": "0", - "url": "www.pbcserver.com/pbcache/endpoint?uuid=0" + "url": "https://www.pbcserver.com/pbcache/endpoint?uuid=0" }, "key": "", "url": "" diff --git a/exchange/exchangetest/targeting-cache-zero.json b/exchange/exchangetest/targeting-cache-zero.json index 5130153026a..0048ea10917 100644 --- a/exchange/exchangetest/targeting-cache-zero.json +++ b/exchange/exchangetest/targeting-cache-zero.json @@ -70,7 +70,7 @@ "cache": { "bids": { "cacheId": "0", - "url": "www.pbcserver.com/pbcache/endpoint?uuid=0" + "url": "https://www.pbcserver.com/pbcache/endpoint?uuid=0" }, "key": "", "url": "" diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go index a5730ce7914..1ad788300a9 100644 --- a/prebid_cache_client/client.go +++ b/prebid_cache_client/client.go @@ -29,8 +29,8 @@ type Client interface { // logging any relevant errors to the app logs PutJson(ctx context.Context, values []Cacheable) ([]string, []error) - // Serves the purpose of a getter that returns the host and the cache of the prebid-server URL - GetExtCacheData() (string, string) + // GetExtCacheData gets the scheme, host, and path of the externally accessible cache url. + GetExtCacheData() (scheme string, host string, path string) } type PayloadType string @@ -49,23 +49,25 @@ type Cacheable struct { func NewClient(httpClient *http.Client, conf *config.Cache, extCache *config.ExternalCache, metrics pbsmetrics.MetricsEngine) Client { return &clientImpl{ - httpClient: httpClient, - putUrl: conf.GetBaseURL() + "/cache", - externalCacheHost: extCache.Host, - externalCachePath: extCache.Path, - metrics: metrics, + httpClient: httpClient, + putUrl: conf.GetBaseURL() + "/cache", + externalCacheScheme: extCache.Scheme, + externalCacheHost: extCache.Host, + externalCachePath: extCache.Path, + metrics: metrics, } } type clientImpl struct { - httpClient *http.Client - putUrl string - externalCacheHost string - externalCachePath string - metrics pbsmetrics.MetricsEngine + httpClient *http.Client + putUrl string + externalCacheScheme string + externalCacheHost string + externalCachePath string + metrics pbsmetrics.MetricsEngine } -func (c *clientImpl) GetExtCacheData() (string, string) { +func (c *clientImpl) GetExtCacheData() (string, string, string) { path := c.externalCachePath if path == "/" { // Only the slash for the path, remove it to empty @@ -75,7 +77,7 @@ func (c *clientImpl) GetExtCacheData() (string, string) { path = "/" + path } - return c.externalCacheHost, path + return c.externalCacheScheme, c.externalCacheHost, path } func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []string, errs []error) { diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go index 5840d4ea564..72fd2761731 100644 --- a/prebid_cache_client/client_test.go +++ b/prebid_cache_client/client_test.go @@ -186,58 +186,80 @@ func TestEncodeValueToBuffer(t *testing.T) { func TestStripCacheHostAndPath(t *testing.T) { inCacheURL := config.Cache{ExpectedTimeMillis: 10} type aTest struct { - inExtCacheURL config.ExternalCache - expectedHost string - expectedPath string + inExtCacheURL config.ExternalCache + expectedScheme string + expectedHost string + expectedPath string } testInput := []aTest{ { inExtCacheURL: config.ExternalCache{ - Host: "prebid-server.prebid.org", - Path: "/pbcache/endpoint", + Scheme: "", + Host: "prebid-server.prebid.org", + Path: "/pbcache/endpoint", }, - expectedHost: "prebid-server.prebid.org", - expectedPath: "/pbcache/endpoint", + expectedScheme: "", + expectedHost: "prebid-server.prebid.org", + expectedPath: "/pbcache/endpoint", }, { inExtCacheURL: config.ExternalCache{ - Host: "prebidcache.net", - Path: "", + Scheme: "https", + Host: "prebid-server.prebid.org", + Path: "/pbcache/endpoint", }, - expectedHost: "prebidcache.net", - expectedPath: "", + expectedScheme: "https", + expectedHost: "prebid-server.prebid.org", + expectedPath: "/pbcache/endpoint", }, { inExtCacheURL: config.ExternalCache{ - Host: "", - Path: "", + Scheme: "", + Host: "prebidcache.net", + Path: "", }, - expectedHost: "", - expectedPath: "", + expectedScheme: "", + expectedHost: "prebidcache.net", + expectedPath: "", }, { inExtCacheURL: config.ExternalCache{ - Host: "prebid-server.prebid.org", - Path: "pbcache/endpoint", + Scheme: "", + Host: "", + Path: "", }, - expectedHost: "prebid-server.prebid.org", - expectedPath: "/pbcache/endpoint", + expectedScheme: "", + expectedHost: "", + expectedPath: "", }, { inExtCacheURL: config.ExternalCache{ - Host: "prebidcache.net", - Path: "/", + Scheme: "", + Host: "prebid-server.prebid.org", + Path: "pbcache/endpoint", }, - expectedHost: "prebidcache.net", - expectedPath: "", + expectedScheme: "", + expectedHost: "prebid-server.prebid.org", + expectedPath: "/pbcache/endpoint", + }, + { + inExtCacheURL: config.ExternalCache{ + Scheme: "", + Host: "prebidcache.net", + Path: "/", + }, + expectedScheme: "", + expectedHost: "prebidcache.net", + expectedPath: "", }, } for _, test := range testInput { cacheClient := NewClient(&http.Client{}, &inCacheURL, &test.inExtCacheURL, &metricsConf.DummyMetricsEngine{}) - cHost, cPath := cacheClient.GetExtCacheData() + scheme, host, path := cacheClient.GetExtCacheData() - assert.Equal(t, test.expectedHost, cHost) - assert.Equal(t, test.expectedPath, cPath) + assert.Equal(t, test.expectedScheme, scheme) + assert.Equal(t, test.expectedHost, host) + assert.Equal(t, test.expectedPath, path) } } From 5d13c8595b343893c2652ad135207bf0f58263c5 Mon Sep 17 00:00:00 2001 From: GammaSSP <35954362+gammassp@users.noreply.github.com> Date: Fri, 28 Aug 2020 22:54:11 +0700 Subject: [PATCH 185/381] Update gamma adapter (#1447) * Gamma SSP Adapter * Add Gamma SSP server adapter * increase coverage * Fix conflict with base master * Add check MediaType for Imp * Implement Multi Imps request * Changes requested * remove bad-request * increase coverage * Remove duplicate test file * Update gamma.go * Update gamma.go * Update gamma.go * Update config.go Remove Gamma User Sync Url from config * Gamma SSP Adapter * Add Gamma SSP server adapter * increase coverage * Fix conflict with base master * Add check MediaType for Imp * Implement Multi Imps request * Changes requested * remove bad-request * increase coverage * Remove duplicate test file * Update gamma.go * Update gamma.go * update gamma adapter * return nil when have No-Bid Signaling * add missing-adm.json * discard the bid that's missing adm * discard the bid that's missing adm * escape vast instead of encoded it * expand test coverage Co-authored-by: Easy Life --- adapters/gamma/gamma.go | 77 ++++++++++++++++--- .../exemplary/banner-and-video-and-audio.json | 15 ++-- .../exemplary/valid-full-params.json | 2 +- .../gammatest/supplemental/bad-request.json | 2 +- .../gammatest/supplemental/missing-adm.json | 76 ++++++++++++++++++ .../gammatest/supplemental/missing-param.json | 2 +- .../gammatest/supplemental/missing-zone.json | 2 +- .../supplemental/nobid-signaling.json | 56 ++++++++++++++ .../supplemental/status-forbidden.json | 2 +- .../supplemental/status-no-content.json | 2 +- 10 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 adapters/gamma/gammatest/supplemental/missing-adm.json create mode 100644 adapters/gamma/gammatest/supplemental/nobid-signaling.json diff --git a/adapters/gamma/gamma.go b/adapters/gamma/gamma.go index c011954fff9..3756723598f 100644 --- a/adapters/gamma/gamma.go +++ b/adapters/gamma/gamma.go @@ -17,6 +17,27 @@ type GammaAdapter struct { URI string } +type gammaBid struct { + openrtb.Bid //base + VastXML string `json:"vastXml,omitempty"` + VastURL string `json:"vastUrl,omitempty"` +} + +type gammaSeatBid struct { + Bid []gammaBid `json:"bid"` + Group int8 `json:"group,omitempty"` + Ext json.RawMessage `json:"ext,omitempty"` +} +type gammaBidResponse struct { + ID string `json:"id"` + SeatBid []gammaSeatBid `json:"seatbid,omitempty"` + BidID string `json:"bidid,omitempty"` + Cur string `json:"cur,omitempty"` + CustomData string `json:"customdata,omitempty"` + NBR *openrtb.NoBidReasonCode `json:"nbr,omitempty"` + Ext json.RawMessage `json:"ext,omitempty"` +} + func checkParams(gammaExt openrtb_ext.ExtImpGamma) error { if gammaExt.PartnerID == "" { return &errortypes.BadInput{ @@ -180,6 +201,28 @@ func (a *GammaAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapte return adapterRequests, errs } +func convertBid(gBid gammaBid, mediaType openrtb_ext.BidType) *openrtb.Bid { + var bid openrtb.Bid + bid = gBid.Bid + + if mediaType == openrtb_ext.BidTypeVideo { + //Return inline VAST XML Document (Section 6.4.2) + if len(gBid.VastXML) > 0 { + if len(gBid.VastURL) > 0 { + bid.NURL = gBid.VastURL + } + bid.AdM = gBid.VastXML + } else { + return nil + } + } else { + if len(gBid.Bid.AdM) == 0 { + return nil + } + } + return &bid +} + func (a *GammaAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { if response.StatusCode == http.StatusNoContent { return nil, nil @@ -197,24 +240,38 @@ func (a *GammaAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalReq }} } - var bidResp openrtb.BidResponse - if err := json.Unmarshal(response.Body, &bidResp); err != nil { + var gammaResp gammaBidResponse + if err := json.Unmarshal(response.Body, &gammaResp); err != nil { return nil, []error{&errortypes.BadServerResponse{ Message: fmt.Sprintf("bad server response: %d. ", err), }} } - bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) - for _, sb := range bidResp.SeatBid { + //(Section 7.1 No-Bid Signaling) + if len(gammaResp.SeatBid) == 0 { + return nil, nil + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(gammaResp.SeatBid[0].Bid)) + errs := make([]error, 0, len(gammaResp.SeatBid[0].Bid)) + for _, sb := range gammaResp.SeatBid { for i := range sb.Bid { - bid := sb.Bid[i] - bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ - Bid: &bid, - BidType: getMediaTypeForImp(bidResp.ID, internalRequest.Imp), - }) + mediaType := getMediaTypeForImp(gammaResp.ID, internalRequest.Imp) + bid := convertBid(sb.Bid[i], mediaType) + if bid != nil { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: bid, + BidType: mediaType, + }) + } else { + err := &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Missing Ad Markup. Run with request.debug = 1 for more info"), + } + errs = append(errs, err) + } } } - return bidResponse, nil + return bidResponse, errs } //Adding header fields to request header diff --git a/adapters/gamma/gammatest/exemplary/banner-and-video-and-audio.json b/adapters/gamma/gammatest/exemplary/banner-and-video-and-audio.json index 1c92ab96ffe..4282ac32e4d 100644 --- a/adapters/gamma/gammatest/exemplary/banner-and-video-and-audio.json +++ b/adapters/gamma/gammatest/exemplary/banner-and-video-and-audio.json @@ -80,7 +80,7 @@ "mockResponse": { "status": 200, "body": { - "id": "test-request-id", + "id": "test-imp-video-id", "cur": "USD", "seatbid": [ { @@ -89,7 +89,10 @@ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-video-id", "price": 0.500000, - "adm": "some-test-ad", + "adm": "", + "adomain": ["sample.com"], + "vastXml": "some-test-ad", + "vastUrl": "some-test-url", "crid": "29484110", "w": 640, "h": 480 @@ -122,13 +125,15 @@ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-video-id", "price": 0.5, - "adm": "some-test-ad", + "adm": "", + "vastXml": "some-test-ad", + "vastUrl": "some-test-url", "adid": "29484110", "adomain": ["sample.com"], "cid": "958", "crid": "29484110", - "w": 1024, - "h": 576 + "w": 640, + "h": 480 }, "type": "video" } diff --git a/adapters/gamma/gammatest/exemplary/valid-full-params.json b/adapters/gamma/gammatest/exemplary/valid-full-params.json index 8fa1d3e700a..25d51a646ff 100644 --- a/adapters/gamma/gammatest/exemplary/valid-full-params.json +++ b/adapters/gamma/gammatest/exemplary/valid-full-params.json @@ -2,7 +2,7 @@ "mockBidRequest": { "id": "test-request-id", "app":{ - "id":"test-app-id", + "id":"test-app-id", "name":"test-app-name", "bundle":"test-app-bundle" }, diff --git a/adapters/gamma/gammatest/supplemental/bad-request.json b/adapters/gamma/gammatest/supplemental/bad-request.json index d460550c198..8b533f91de0 100644 --- a/adapters/gamma/gammatest/supplemental/bad-request.json +++ b/adapters/gamma/gammatest/supplemental/bad-request.json @@ -2,7 +2,7 @@ "mockBidRequest": { "id": "test-request-id", "app":{ - "id":"test-app-id", + "id":"test-app-id", "name":"test-app-name", "bundle":"test-app-bundle" }, diff --git a/adapters/gamma/gammatest/supplemental/missing-adm.json b/adapters/gamma/gammatest/supplemental/missing-adm.json new file mode 100644 index 00000000000..6b8ff64c04a --- /dev/null +++ b/adapters/gamma/gammatest/supplemental/missing-adm.json @@ -0,0 +1,76 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-video-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "id": "sample-id", + "zid": "sample-zone-id", + "wid": "sample-web-id" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hb.gammaplatform.com/adx/request/?id=sample-id&zid=sample-zone-id&wid=sample-web-id&bidid=test-imp-video-id&hb=pbmobile" + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "gamma", + "bid": [ + { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-video-id", + "price": 0.500000, + "crid": "29484110", + "w": 640, + "h": 360 + } + ] + } + ] + } + } + } + ], + "expectedBids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-video-id", + "price": 0.500000, + "crid": "29484110", + "w": 640, + "h": 360 + }, + "type": "video" + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Missing Ad Markup. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/gamma/gammatest/supplemental/missing-param.json b/adapters/gamma/gammatest/supplemental/missing-param.json index 32e4f9eae6d..9dac0936bd7 100644 --- a/adapters/gamma/gammatest/supplemental/missing-param.json +++ b/adapters/gamma/gammatest/supplemental/missing-param.json @@ -20,7 +20,7 @@ "bidder": { "zid": "sample-zone-id", "wid": "sample-web-id" - + } } diff --git a/adapters/gamma/gammatest/supplemental/missing-zone.json b/adapters/gamma/gammatest/supplemental/missing-zone.json index b53ddc83f98..5c84ef229ee 100644 --- a/adapters/gamma/gammatest/supplemental/missing-zone.json +++ b/adapters/gamma/gammatest/supplemental/missing-zone.json @@ -20,7 +20,7 @@ "bidder": { "id": "sample-id", "wid": "sample-web-id" - + } } diff --git a/adapters/gamma/gammatest/supplemental/nobid-signaling.json b/adapters/gamma/gammatest/supplemental/nobid-signaling.json new file mode 100644 index 00000000000..4055ad70249 --- /dev/null +++ b/adapters/gamma/gammatest/supplemental/nobid-signaling.json @@ -0,0 +1,56 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app":{ + "id":"test-app-id", + "name":"test-app-name", + "bundle":"test-app-bundle" + }, + + "device":{ + "ua":"test-device-ua", + "ip":"test-device-ip", + "ifa":"test-device-ifa", + "model":"test-device-model", + "os":"test-device-os", + "dnt":1 + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext":{ + "bidder":{ + "id": "sample-id", + "zid": "sample-zone-id", + "wid": "sample-web-id" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hb.gammaplatform.com/adx/request/?id=sample-id&zid=sample-zone-id&wid=sample-web-id&bidid=test-imp-id&hb=pbmobile&device_ip=test-device-ip&device_model=test-device-model&device_os=test-device-os&device_ua=test-device-ua&device_ifa=test-device-ifa&app_id=test-app-id&app_bundle=test-app-bundle&app_name=test-app-name" + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + ] + } + } + } + ], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/gamma/gammatest/supplemental/status-forbidden.json b/adapters/gamma/gammatest/supplemental/status-forbidden.json index 950a45ec0c9..3a30b210f4f 100644 --- a/adapters/gamma/gammatest/supplemental/status-forbidden.json +++ b/adapters/gamma/gammatest/supplemental/status-forbidden.json @@ -2,7 +2,7 @@ "mockBidRequest": { "id": "test-request-id", "app":{ - "id":"test-app-id", + "id":"test-app-id", "name":"test-app-name", "bundle":"test-app-bundle" }, diff --git a/adapters/gamma/gammatest/supplemental/status-no-content.json b/adapters/gamma/gammatest/supplemental/status-no-content.json index 6c7878a86c9..045fb939ced 100644 --- a/adapters/gamma/gammatest/supplemental/status-no-content.json +++ b/adapters/gamma/gammatest/supplemental/status-no-content.json @@ -2,7 +2,7 @@ "mockBidRequest": { "id": "test-request-id", "app":{ - "id":"test-app-id", + "id":"test-app-id", "name":"test-app-name", "bundle":"test-app-bundle" }, From ebdf997cc1c010cb35d5df90b2241f03c074882d Mon Sep 17 00:00:00 2001 From: gpolaert Date: Fri, 28 Aug 2020 19:40:28 +0200 Subject: [PATCH 186/381] fix: avoid unexpected EOF on gz writer (#1449) --- .../pubstack/eventchannel/eventchannel.go | 10 ++--- .../eventchannel/eventchannel_test.go | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/analytics/pubstack/eventchannel/eventchannel.go b/analytics/pubstack/eventchannel/eventchannel.go index b8dc4dd8e28..d9c2bc4117c 100644 --- a/analytics/pubstack/eventchannel/eventchannel.go +++ b/analytics/pubstack/eventchannel/eventchannel.go @@ -93,10 +93,13 @@ func (c *EventChannel) flush() { return } + // reset buffers and writers + defer c.reset() + // finish writing gzip header - err := c.gz.Flush() + err := c.gz.Close() if err != nil { - glog.Warning("[pubstack] fail to flush gzipped buffer") + glog.Warning("[pubstack] fail to close gzipped buffer") return } @@ -108,9 +111,6 @@ func (c *EventChannel) flush() { return } - // reset buffers and writers - c.reset() - // send events (async) go c.send(payload) } diff --git a/analytics/pubstack/eventchannel/eventchannel_test.go b/analytics/pubstack/eventchannel/eventchannel_test.go index 9fdcfe976a6..792e15e151e 100644 --- a/analytics/pubstack/eventchannel/eventchannel_test.go +++ b/analytics/pubstack/eventchannel/eventchannel_test.go @@ -134,3 +134,42 @@ func TestEventChannel_Push(t *testing.T) { assert.Equal(t, string(data), "onetwothreefourfivesixseven") } + +func TestEventChannel_OutputFormat(t *testing.T) { + + toGzip := func(payload string) []byte { + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + + if _, err := zw.Write([]byte(payload)); err != nil { + assert.Fail(t, err.Error()) + } + + if err := zw.Close(); err != nil { + assert.Fail(t, err.Error()) + } + return buf.Bytes() + } + + data := make([]byte, 0) + send := func(payload []byte) error { + data = append(data, payload...) + return nil + } + + eventChannel := NewEventChannel(send, 15000, 10, 2*time.Minute) + + eventChannel.Push([]byte("one")) + eventChannel.flush() + eventChannel.Push([]byte("two")) + eventChannel.Push([]byte("three")) + + eventChannel.Close() + + time.Sleep(10 * time.Millisecond) + + expected := append(toGzip("one"), toGzip("twothree")...) + + assert.Equal(t, expected, data) + +} From d8dc27f245d603af9b3f58fb5897b61e417b2d3c Mon Sep 17 00:00:00 2001 From: Stephan Brosinski Date: Tue, 1 Sep 2020 01:02:15 +0200 Subject: [PATCH 187/381] Smaato adapter: support for video mediaType (#1463) Co-authored-by: vikram --- adapters/smaato/smaato.go | 59 ++++-- .../smaato/smaatotest/exemplary/video.json | 187 ++++++++++++++++++ .../supplemental/bad-adm-response.json | 8 +- static/bidder-info/smaato.yaml | 4 +- 4 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 adapters/smaato/smaatotest/exemplary/video.json diff --git a/adapters/smaato/smaato.go b/adapters/smaato/smaato.go index 06678d77a61..b48851bb4d2 100644 --- a/adapters/smaato/smaato.go +++ b/adapters/smaato/smaato.go @@ -20,6 +20,7 @@ type adMarkupType string const ( smtAdTypeImg adMarkupType = "Img" smtAdTypeRichmedia adMarkupType = "Richmedia" + smtAdTypeVideo adMarkupType = "Video" ) // SmaatoAdapter describes a Smaato prebid server adapter. @@ -63,7 +64,7 @@ func (a *SmaatoAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt } // Use bidRequestExt of first imp to retrieve params which are valid for all imps, e.g. publisherId - publisherId, err := jsonparser.GetString(request.Imp[0].Ext, "bidder", "publisherId") + publisherID, err := jsonparser.GetString(request.Imp[0].Ext, "bidder", "publisherId") if err != nil { errs = append(errs, err) return nil, errs @@ -80,7 +81,7 @@ func (a *SmaatoAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapt } if request.Site != nil { siteCopy := *request.Site - siteCopy.Publisher = &openrtb.Publisher{ID: publisherId} + siteCopy.Publisher = &openrtb.Publisher{ID: publisherID} if request.Site.Ext != nil { var siteExt siteExt @@ -178,16 +179,25 @@ func (a *SmaatoAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRe for i := 0; i < len(sb.Bid); i++ { bid := sb.Bid[i] + markupType, markupTypeErr := getAdMarkupType(response, bid.AdM) + if markupTypeErr != nil { + return nil, []error{markupTypeErr} + } + var markupError error - bid.AdM, markupError = renderAdMarkup(getAdMarkupType(response, bid.AdM), bid.AdM) + bid.AdM, markupError = renderAdMarkup(markupType, bid.AdM) if markupError != nil { - fmt.Println(markupError) - continue // no bid when broken ad markup + return nil, []error{markupError} + } + + bidType, bidTypeErr := markupTypeToBidType(markupType) + if bidTypeErr != nil { + return nil, []error{bidTypeErr} } bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &bid, - BidType: openrtb_ext.BidTypeBanner, + BidType: bidType, }) } } @@ -202,23 +212,41 @@ func renderAdMarkup(adMarkupType adMarkupType, adMarkup string) (string, error) adm, markupError = extractAdmImage(adMarkup) case smtAdTypeRichmedia: adm, markupError = extractAdmRichMedia(adMarkup) + case smtAdTypeVideo: + adm, markupError = adMarkup, nil default: return "", fmt.Errorf("Unknown markup type %s", adMarkupType) } return adm, markupError } -func getAdMarkupType(response *adapters.ResponseData, adMarkup string) adMarkupType { +func markupTypeToBidType(markupType adMarkupType) (openrtb_ext.BidType, error) { + switch markupType { + case smtAdTypeImg: + return openrtb_ext.BidTypeBanner, nil + case smtAdTypeRichmedia: + return openrtb_ext.BidTypeBanner, nil + case smtAdTypeVideo: + return openrtb_ext.BidTypeVideo, nil + default: + return "", fmt.Errorf("Invalid markupType %s", markupType) + } +} + +func getAdMarkupType(response *adapters.ResponseData, adMarkup string) (adMarkupType, error) { if admType := adMarkupType(response.Headers.Get("X-SMT-ADTYPE")); admType != "" { - return admType + return admType, nil } if strings.HasPrefix(adMarkup, `{"image":`) { - return smtAdTypeImg + return smtAdTypeImg, nil } if strings.HasPrefix(adMarkup, `{"richmedia":`) { - return smtAdTypeRichmedia + return smtAdTypeRichmedia, nil + } + if strings.HasPrefix(adMarkup, `", + "adomain": [ + "smaato.com" + ], + "bidderName": "smaato", + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768 + } + ] + } + ], + "bidid": "04db8629-179d-4bcd-acce-e54722969006", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "adm": "", + "adomain": [ + "smaato.com" + ], + "cid": "CM6523", + "crid": "CR69381", + "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd", + "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C", + "iurl": "https://iurl", + "nurl": "https://nurl", + "price": 0.01, + "w": 1024, + "h": 768 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smaato/smaatotest/supplemental/bad-adm-response.json b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json index 6d4990e9ea4..1fce58f0dfe 100644 --- a/adapters/smaato/smaatotest/supplemental/bad-adm-response.json +++ b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json @@ -162,5 +162,11 @@ } } ], - "expectedBidResponses": [] + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Invalid ad markup {\"badmedia\":{\"mediadata\":{\"content\":\"

\", \"w\":350,\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}", + "comparison": "literal" + } + ] } \ No newline at end of file diff --git a/static/bidder-info/smaato.yaml b/static/bidder-info/smaato.yaml index 662603febdb..db3e61e5cc6 100644 --- a/static/bidder-info/smaato.yaml +++ b/static/bidder-info/smaato.yaml @@ -4,6 +4,8 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - - banner \ No newline at end of file + - banner + - video From ebe98c795ed936bf8c3cf6784acdd0a2e9da8796 Mon Sep 17 00:00:00 2001 From: Shriprasad Date: Tue, 1 Sep 2020 18:06:41 +0530 Subject: [PATCH 188/381] Resolved merge issues --- endpoints/openrtb2/ctv/response/adpod_generator.go | 10 +++++----- endpoints/openrtb2/ctv_auction.go | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/endpoints/openrtb2/ctv/response/adpod_generator.go b/endpoints/openrtb2/ctv/response/adpod_generator.go index 5c69063bd55..059b2774bee 100644 --- a/endpoints/openrtb2/ctv/response/adpod_generator.go +++ b/endpoints/openrtb2/ctv/response/adpod_generator.go @@ -25,7 +25,7 @@ type filteredBid struct { reasonCode constant.FilterReasonCode } type highestCombination struct { - bids []*Bid + bids []*types.Bid bidIDs []string durations []int price float64 @@ -48,7 +48,7 @@ type AdPodGenerator struct { } //NewAdPodGenerator will generate adpod based on configuration -func NewAdPodGenerator(request *openrtb.BidRequest, impIndex int, buckets BidsBuckets, comb ICombination, adpod *openrtb_ext.VideoAdPod, met pbsmetrics.MetricsEngine) *AdPodGenerator { +func NewAdPodGenerator(request *openrtb.BidRequest, impIndex int, buckets types.BidsBuckets, comb combination.ICombination, adpod *openrtb_ext.VideoAdPod, met pbsmetrics.MetricsEngine) *AdPodGenerator { return &AdPodGenerator{ request: request, impIndex: impIndex, @@ -83,7 +83,7 @@ func (o *AdPodGenerator) cleanup(wg *sync.WaitGroup, responseCh chan *highestCom func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombination { start := time.Now() - defer TimeTrack(start, fmt.Sprintf("Tid:%v ImpId:%v getAdPodBids", o.request.ID, o.request.Imp[o.impIndex].ID)) + defer util.TimeTrack(start, fmt.Sprintf("Tid:%v ImpId:%v getAdPodBids", o.request.ID, o.request.Imp[o.impIndex].ID)) maxRoutines := 3 isTimedOutORReceivedAllResponses := false @@ -145,14 +145,14 @@ func (o *AdPodGenerator) getAdPodBids(timeout time.Duration) []*highestCombinati defer ticker.Stop() labels := pbsmetrics.PodLabels{ - AlgorithmName: string(CombinationGeneratorV1), + AlgorithmName: string(constant.CombinationGeneratorV1), NoOfCombinations: new(int), } *labels.NoOfCombinations = combinationCount o.met.RecordPodCombGenTime(labels, time.Duration(totalTimeByCombGen)) compExclLabels := pbsmetrics.PodLabels{ - AlgorithmName: string(CompetitiveExclusionV1), + AlgorithmName: string(constant.CompetitiveExclusionV1), NoOfResponseBids: new(int), } *compExclLabels.NoOfResponseBids = 0 diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index 1f460fcc3dc..ec313a575bd 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -16,7 +16,6 @@ import ( "github.com/PubMatic-OpenWrap/openrtb" "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/endpoints/openrtb2/ctv/combination" "github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/constant" "github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/impressions" @@ -422,7 +421,7 @@ func (deps *ctvEndpointDeps) getAllAdPodImpsConfigs() { } //getAdPodImpsConfigs will return number of impressions configurations within adpod -func (deps *ctvEndpointDeps) getAdPodImpsConfigs(imp *openrtb.Imp, adpod *openrtb_ext.VideoAdPod) []*ctv.ImpAdPodConfig { +func (deps *ctvEndpointDeps) getAdPodImpsConfigs(imp *openrtb.Imp, adpod *openrtb_ext.VideoAdPod) []*types.ImpAdPodConfig { selectedAlgorithm := impressions.MinMaxAlgorithm labels := pbsmetrics.PodLabels{AlgorithmName: impressions.MonitorKey[selectedAlgorithm], NoOfImpressions: new(int)} @@ -433,7 +432,7 @@ func (deps *ctvEndpointDeps) getAdPodImpsConfigs(imp *openrtb.Imp, adpod *openrt *labels.NoOfImpressions = len(impRanges) deps.metricsEngine.RecordPodImpGenTime(labels, start) - config := make([]*ctv.ImpAdPodConfig, len(impRanges)) + config := make([]*types.ImpAdPodConfig, len(impRanges)) for i, value := range impRanges { config[i] = &types.ImpAdPodConfig{ ImpID: util.GetCTVImpressionID(imp.ID, i+1), From 412e0fcf4fa43f07a3aec843373e5c093093f43b Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Tue, 1 Sep 2020 19:02:39 +0300 Subject: [PATCH 189/381] Rubicon liveramp param (#1466) Add liveramp mapping to user.ext should translate the "liveramp.com" id from the "user.ext.eids" array to "user.ext.liveramp_idl" as follows: ``` { "user": { "ext": { "eids": [{ "source": 'liveramp.com', "uids": [{ "id": "T7JiRRvsRAmh88" }] }] } } } ``` to XAPI: ``` { "user": { "ext": { "liveramp_idl": "T7JiRRvsRAmh88" } } } ``` --- adapters/rubicon/rubicon.go | 60 ++++++++++++++++++++++---------- adapters/rubicon/rubicon_test.go | 14 +++++++- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index a69530de831..56ae7b2f792 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -93,11 +93,12 @@ type rubiconExtUserTpID struct { } type rubiconUserExt struct { - Consent string `json:"consent,omitempty"` - DigiTrust *openrtb_ext.ExtUserDigiTrust `json:"digitrust"` - Eids []openrtb_ext.ExtUserEid `json:"eids,omitempty"` - TpID []rubiconExtUserTpID `json:"tpid,omitempty"` - RP rubiconUserExtRP `json:"rp"` + Consent string `json:"consent,omitempty"` + DigiTrust *openrtb_ext.ExtUserDigiTrust `json:"digitrust"` + Eids []openrtb_ext.ExtUserEid `json:"eids,omitempty"` + TpID []rubiconExtUserTpID `json:"tpid,omitempty"` + RP rubiconUserExtRP `json:"rp"` + LiverampIdl string `json:"liveramp_idl,omitempty"` } type rubiconSiteExtRP struct { @@ -247,6 +248,12 @@ type rubiconUserExtEidUidExt struct { RtiPartner string `json:"rtiPartner,omitempty"` } +type mappedRubiconUidsParam struct { + tpIds []rubiconExtUserTpID + segments []string + liverampIdl string +} + //MAS algorithm func findPrimary(alt []int) (int, []int) { min, pos, primary := 0, 0, 0 @@ -683,13 +690,18 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap // set user.ext.tpid if len(userExt.Eids) > 0 { - if tpIds, segments, errors := getTpIdsAndSegments(userExt.Eids); len(errors) > 0 { + mappedRubiconUidsParam, errors := getTpIdsAndSegments(userExt.Eids) + if len(errors) > 0 { errs = append(errs, errors...) continue - } else if err := updateUserExtWithTpIdsAndSegments(&userExtRP, tpIds, segments); err != nil { + } + + if err := updateUserExtWithTpIdsAndSegments(&userExtRP, mappedRubiconUidsParam); err != nil { errs = append(errs, err) continue } + + userExtRP.LiverampIdl = mappedRubiconUidsParam.liverampIdl } } @@ -793,9 +805,11 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap return requestData, errs } -func getTpIdsAndSegments(eids []openrtb_ext.ExtUserEid) ([]rubiconExtUserTpID, []string, []error) { - tpIds := make([]rubiconExtUserTpID, 0) - segments := make([]string, 0) +func getTpIdsAndSegments(eids []openrtb_ext.ExtUserEid) (mappedRubiconUidsParam, []error) { + rubiconUidsParam := mappedRubiconUidsParam{ + tpIds: make([]rubiconExtUserTpID, 0), + segments: make([]string, 0), + } errs := make([]error, 0) for _, eid := range eids { @@ -815,7 +829,7 @@ func getTpIdsAndSegments(eids []openrtb_ext.ExtUserEid) ([]rubiconExtUserTpID, [ } if eidUidExt.RtiPartner == "TDID" { - tpIds = append(tpIds, rubiconExtUserTpID{Source: "tdid", UID: uid.ID}) + rubiconUidsParam.tpIds = append(rubiconUidsParam.tpIds, rubiconExtUserTpID{Source: "tdid", UID: uid.ID}) } } } @@ -824,7 +838,7 @@ func getTpIdsAndSegments(eids []openrtb_ext.ExtUserEid) ([]rubiconExtUserTpID, [ if len(uids) > 0 { uidId := uids[0].ID if uidId != "" { - tpIds = append(tpIds, rubiconExtUserTpID{Source: "liveintent.com", UID: uidId}) + rubiconUidsParam.tpIds = append(rubiconUidsParam.tpIds, rubiconExtUserTpID{Source: "liveintent.com", UID: uidId}) } if eid.Ext != nil { @@ -835,20 +849,28 @@ func getTpIdsAndSegments(eids []openrtb_ext.ExtUserEid) ([]rubiconExtUserTpID, [ }) continue } - segments = eidExt.Segments + rubiconUidsParam.segments = eidExt.Segments + } + } + case "liveramp.com": + uids := eid.Uids + if len(uids) > 0 { + uidId := uids[0].ID + if uidId != "" && rubiconUidsParam.liverampIdl == "" { + rubiconUidsParam.liverampIdl = uidId } } } } - return tpIds, segments, errs + return rubiconUidsParam, errs } -func updateUserExtWithTpIdsAndSegments(userExtRP *rubiconUserExt, tpIds []rubiconExtUserTpID, segments []string) error { - if len(tpIds) > 0 { - userExtRP.TpID = tpIds +func updateUserExtWithTpIdsAndSegments(userExtRP *rubiconUserExt, rubiconUidsParam mappedRubiconUidsParam) error { + if len(rubiconUidsParam.tpIds) > 0 { + userExtRP.TpID = rubiconUidsParam.tpIds - if segments != nil { + if rubiconUidsParam.segments != nil { userExtRPTarget := make(map[string]interface{}) if userExtRP.RP.Target != nil { @@ -857,7 +879,7 @@ func updateUserExtWithTpIdsAndSegments(userExtRP *rubiconUserExt, tpIds []rubico } } - userExtRPTarget["LIseg"] = segments + userExtRPTarget["LIseg"] = rubiconUidsParam.segments if target, err := json.Marshal(&userExtRPTarget); err != nil { return &errortypes.BadInput{Message: err.Error()} diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index 0489797561b..5ec78ccf4f3 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -1219,6 +1219,15 @@ func TestOpenRTBRequestWithSpecificExtUserEids(t *testing.T) { "ext": { "segments": ["999","888"] } + }, + { + "source": "liveramp.com", + "uids": [{ + "id": "LIVERAMPID" + }], + "ext": { + "segments": ["111","222"] + } } ]}`), }, @@ -1239,7 +1248,7 @@ func TestOpenRTBRequestWithSpecificExtUserEids(t *testing.T) { } assert.NotNil(t, userExt.Eids) - assert.Equal(t, 3, len(userExt.Eids), "Eids values are not as expected!") + assert.Equal(t, 4, len(userExt.Eids), "Eids values are not as expected!") assert.NotNil(t, userExt.TpID) assert.Equal(t, 2, len(userExt.TpID), "TpID values are not as expected!") @@ -1250,6 +1259,9 @@ func TestOpenRTBRequestWithSpecificExtUserEids(t *testing.T) { // liveintent.com assert.Equal(t, "liveintent.com", userExt.TpID[1].Source, "TpID source value is not as expected!") + // liveramp.com + assert.Equal(t, "LIVERAMPID", userExt.LiverampIdl, "Liveramp_idl value is not as expected!") + userExtRPTarget := make(map[string]interface{}) if err := json.Unmarshal(userExt.RP.Target, &userExtRPTarget); err != nil { t.Fatal("Error unmarshalling request.user.ext.rp.target object.") From 754de04fee53656f669bf5cc52251c688f5a36fa Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Tue, 1 Sep 2020 13:09:32 -0700 Subject: [PATCH 190/381] Consolidate StoredRequest configs, add validation for all data types (#1453) --- config/config.go | 51 ++++-- config/config_test.go | 47 ++++- config/stored_requests.go | 253 ++++++++------------------ config/stored_requests_test.go | 84 ++++++++- stored_requests/config/config.go | 97 +++------- stored_requests/config/config_test.go | 108 +++-------- 6 files changed, 284 insertions(+), 356 deletions(-) diff --git a/config/config.go b/config/config.go index c2e7dfd8f3f..e443a48ec1d 100755 --- a/config/config.go +++ b/config/config.go @@ -29,18 +29,19 @@ type Configuration struct { EnableGzip bool `mapstructure:"enable_gzip"` // StatusResponse is the string which will be returned by the /status endpoint when things are OK. // If empty, it will return a 204 with no content. - StatusResponse string `mapstructure:"status_response"` - AuctionTimeouts AuctionTimeouts `mapstructure:"auction_timeouts_ms"` - CacheURL Cache `mapstructure:"cache"` - ExtCacheURL ExternalCache `mapstructure:"external_cache"` - RecaptchaSecret string `mapstructure:"recaptcha_secret"` - HostCookie HostCookie `mapstructure:"host_cookie"` - Metrics Metrics `mapstructure:"metrics"` - DataCache DataCache `mapstructure:"datacache"` - StoredRequests StoredRequests `mapstructure:"stored_requests"` - CategoryMapping StoredRequestsSlim `mapstructure:"category_mapping"` + StatusResponse string `mapstructure:"status_response"` + AuctionTimeouts AuctionTimeouts `mapstructure:"auction_timeouts_ms"` + CacheURL Cache `mapstructure:"cache"` + ExtCacheURL ExternalCache `mapstructure:"external_cache"` + RecaptchaSecret string `mapstructure:"recaptcha_secret"` + HostCookie HostCookie `mapstructure:"host_cookie"` + Metrics Metrics `mapstructure:"metrics"` + DataCache DataCache `mapstructure:"datacache"` + StoredRequests StoredRequests `mapstructure:"stored_requests"` + StoredRequestsAMP StoredRequests `mapstructure:"stored_amp_req"` + CategoryMapping StoredRequests `mapstructure:"category_mapping"` // Note that StoredVideo refers to stored video requests, and has nothing to do with caching video creatives. - StoredVideo StoredRequestsSlim `mapstructure:"stored_video_req"` + StoredVideo StoredRequests `mapstructure:"stored_video_req"` // Adapters should have a key for every openrtb_ext.BidderName, converted to lower-case. // Se also: https://github.com/spf13/viper/issues/371#issuecomment-335388559 @@ -103,7 +104,10 @@ func (c configErrors) Error() string { func (cfg *Configuration) validate() configErrors { var errs configErrors errs = cfg.AuctionTimeouts.validate(errs) - errs = cfg.StoredRequests.validate(errs) + errs = cfg.StoredRequests.validate("stored_req", errs) + errs = cfg.StoredRequestsAMP.validate("stored_amp_req", errs) + errs = cfg.CategoryMapping.validate("categories", errs) + errs = cfg.StoredVideo.validate("stored_video_req", errs) errs = cfg.Metrics.validate(errs) if cfg.MaxRequestSize < 0 { errs = append(errs, fmt.Errorf("cfg.max_request_size must be >= 0. Got %d", cfg.MaxRequestSize)) @@ -604,6 +608,9 @@ func New(v *viper.Viper) (*Configuration, error) { c.BlacklistedAcctMap[c.BlacklistedAccts[i]] = true } + // Migrate combo stored request config to separate stored_reqs and amp stored_reqs configs. + resolvedStoredRequestsConfig(&c) + glog.Info("Logging the resolved configuration:") logGeneral(reflect.ValueOf(c), " \t") if errs := c.validate(); len(errs) > 0 { @@ -721,6 +728,7 @@ func SetupViper(v *viper.Viper, filename string) { v.AddConfigPath(".") v.AddConfigPath("/etc/config") } + // Fixes #475: Some defaults will be set just so they are accessible via environment variables // (basically so viper knows they exist) v.SetDefault("external_url", "http://localhost:8000") @@ -779,7 +787,8 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("category_mapping.filesystem.enabled", true) v.SetDefault("category_mapping.filesystem.directorypath", "./static/category-mapping") v.SetDefault("category_mapping.http.endpoint", "") - v.SetDefault("stored_requests.filesystem", false) + v.SetDefault("stored_requests.filesystem.enabled", false) + v.SetDefault("stored_requests.filesystem.directorypath", "./stored_requests/data/by_id") v.SetDefault("stored_requests.directorypath", "./stored_requests/data/by_id") v.SetDefault("stored_requests.postgres.connection.dbname", "") v.SetDefault("stored_requests.postgres.connection.host", "") @@ -995,6 +1004,22 @@ func SetupViper(v *viper.Viper, filename string) { v.SetEnvPrefix("PBS") v.AutomaticEnv() v.ReadInConfig() + + // Migrate config settings to maintain compatibility with old configs + migrateConfig(v) +} + +func migrateConfig(v *viper.Viper) { + // if stored_requests.filesystem is not a map in conf file as expected from defaults, + // means we have old-style settings; migrate them to new filesystem map to avoid breaking viper + if _, ok := v.Get("stored_requests.filesystem").(map[string]interface{}); !ok { + glog.Warning("stored_requests.filesystem should be changed to stored_requests.filesystem.enabled") + glog.Warning("stored_requests.directorypath should be changed to stored_requests.filesystem.directorypath") + m := v.GetStringMap("stored_requests.filesystem") + m["enabled"] = v.GetBool("stored_requests.filesystem") + m["directorypath"] = v.GetString("stored_requests.directorypath") + v.Set("stored_requests.filesystem", m) + } } func setBidderDefaults(v *viper.Viper, bidder string) { diff --git a/config/config_test.go b/config/config_test.go index 40589ac7b23..b23ddd6f614 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -3,6 +3,7 @@ package config import ( "bytes" "net" + "os" "strings" "testing" "time" @@ -135,6 +136,8 @@ func TestDefaults(t *testing.T) { cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false) cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true) cmpStrings(t, "certificates_file", cfg.PemCertsFile, "") + cmpBools(t, "stored_requests.filesystem.enabled", false, cfg.StoredRequests.Files.Enabled) + cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path) } var fullConfig = []byte(` @@ -287,6 +290,12 @@ adapters: usersync_url: http:\\tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r= `) +var oldStoredRequestsConfig = []byte(` +stored_requests: + filesystem: true + directorypath: "/somepath" +`) + func cmpStrings(t *testing.T, key string, a string, b string) { t.Helper() assert.Equal(t, a, b, "%s: %s != %s", key, a, b) @@ -440,17 +449,53 @@ func TestUnmarshalAdapterExtraInfo(t *testing.T) { func TestValidConfig(t *testing.T) { cfg := Configuration{ StoredRequests: StoredRequests{ - Files: true, + Files: FileFetcherConfig{Enabled: true}, + InMemoryCache: InMemoryCache{ + Type: "none", + }, + }, + StoredVideo: StoredRequests{ + Files: FileFetcherConfig{Enabled: true}, InMemoryCache: InMemoryCache{ Type: "none", }, }, + CategoryMapping: StoredRequests{ + Files: FileFetcherConfig{Enabled: true}, + }, } + resolvedStoredRequestsConfig(&cfg) err := cfg.validate() assert.Nil(t, err, "OpenRTB filesystem config should work. %v", err) } +func TestMigrateConfig(t *testing.T) { + v := viper.New() + SetupViper(v, "") + v.SetConfigType("yaml") + v.ReadConfig(bytes.NewBuffer(oldStoredRequestsConfig)) + migrateConfig(v) + cfg, err := New(v) + assert.NoError(t, err, "Setting up config should work but it doesn't") + cmpBools(t, "stored_requests.filesystem.enabled", true, cfg.StoredRequests.Files.Enabled) + cmpStrings(t, "stored_requests.filesystem.path", "/somepath", cfg.StoredRequests.Files.Path) +} + +func TestMigrateConfigFromEnv(t *testing.T) { + if oldval, ok := os.LookupEnv("PBS_STORED_REQUESTS_FILESYSTEM"); ok { + defer os.Setenv("PBS_STORED_REQUESTS_FILESYSTEM", oldval) + } else { + defer os.Unsetenv("PBS_STORED_REQUESTS_FILESYSTEM") + } + os.Setenv("PBS_STORED_REQUESTS_FILESYSTEM", "true") + v := viper.New() + SetupViper(v, "") + cfg, err := New(v) + assert.NoError(t, err, "Setting up config should work but it doesn't") + cmpBools(t, "stored_requests.filesystem.enabled", true, cfg.StoredRequests.Files.Enabled) +} + func TestInvalidAdapterEndpointConfig(t *testing.T) { v := viper.New() SetupViper(v, "") diff --git a/config/stored_requests.go b/config/stored_requests.go index 04e400f9b7c..b63073fede7 100644 --- a/config/stored_requests.go +++ b/config/stored_requests.go @@ -2,7 +2,6 @@ package config import ( "bytes" - "errors" "fmt" "strconv" "strings" @@ -11,44 +10,35 @@ import ( "github.com/golang/glog" ) -// StoredRequests configures the backend used to store requests on the server. -type StoredRequests struct { - // Files should be true if Stored Requests should be loaded from the filesystem. - Files bool `mapstructure:"filesystem"` - //If data should be loaded from file system, path should be specified in configuration - Path string `mapstructure:"directorypath"` - // Postgres configures Fetchers and EventProducers which read from a Postgres DB. - // Fetchers are in stored_requests/backends/db_fetcher/postgres.go - // EventProducers are in stored_requests/events/postgres - Postgres PostgresConfig `mapstructure:"postgres"` - // HTTP configures an instance of stored_requests/backends/http/http_fetcher.go. - // If non-nil, Stored Requests will be fetched from the endpoint described there. - HTTP HTTPFetcherConfig `mapstructure:"http"` - // InMemoryCache configures an instance of stored_requests/caches/memory/cache.go. - // If non-nil, Stored Requests will be saved in an in-memory cache. - InMemoryCache InMemoryCache `mapstructure:"in_memory_cache"` - // CacheEventsAPI configures an instance of stored_requests/events/api/api.go. - // If non-nil, Stored Request Caches can be updated or invalidated through API endpoints. - // This is intended to be a useful development tool and not recommended for a production environment. - // It should not be exposed to public networks without authentication. - CacheEventsAPI bool `mapstructure:"cache_events_api"` - // HTTPEvents configures an instance of stored_requests/events/http/http.go. - // If non-nil, the server will use those endpoints to populate and update the cache. - HTTPEvents HTTPEventsConfig `mapstructure:"http_events"` +// DataType constants +type DataType string + +const ( + RequestDataType DataType = "Request" + CategoryDataType DataType = "Category" + VideoDataType DataType = "Video" + AMPRequestDataType DataType = "AMP Request" +) + +func (sr *StoredRequests) DataType() DataType { + return sr.dataType } -// StoredRequestsSlim struct defines options for stored requests from a single endpoint -type StoredRequestsSlim struct { +// StoredRequests struct defines options for stored requests for each data type +// including some amp stored_requests options +type StoredRequests struct { + // dataType is a tag pushed from upstream indicating the type of object fetched here + dataType DataType // Files should be used if Stored Requests should be loaded from the filesystem. // Fetchers are in stored_requests/backends/file_system/fetcher.go Files FileFetcherConfig `mapstructure:"filesystem"` // Postgres configures Fetchers and EventProducers which read from a Postgres DB. // Fetchers are in stored_requests/backends/db_fetcher/postgres.go // EventProducers are in stored_requests/events/postgres - Postgres PostgresConfigSlim `mapstructure:"postgres"` + Postgres PostgresConfig `mapstructure:"postgres"` // HTTP configures an instance of stored_requests/backends/http/http_fetcher.go. // If non-nil, Stored Requests will be fetched from the endpoint described there. - HTTP HTTPFetcherConfigSlim `mapstructure:"http"` + HTTP HTTPFetcherConfig `mapstructure:"http"` // InMemoryCache configures an instance of stored_requests/caches/memory/cache.go. // If non-nil, Stored Requests will be saved in an in-memory cache. InMemoryCache InMemoryCache `mapstructure:"in_memory_cache"` @@ -57,14 +47,7 @@ type StoredRequestsSlim struct { CacheEvents CacheEventsConfig `mapstructure:"cache_events"` // HTTPEvents configures an instance of stored_requests/events/http/http.go. // If non-nil, the server will use those endpoints to populate and update the cache. - HTTPEvents HTTPEventsConfigSlim `mapstructure:"http_events"` -} - -// HTTPEventsConfigSlim configures stored_requests/events/http/http.go -type HTTPEventsConfigSlim struct { - Endpoint string `mapstructure:"endpoint"` - RefreshRate int64 `mapstructure:"refresh_rate_seconds"` - Timeout int `mapstructure:"timeout_ms"` + HTTPEvents HTTPEventsConfig `mapstructure:"http_events"` } // HTTPEventsConfig configures stored_requests/events/http/http.go @@ -83,14 +66,6 @@ func (cfg HTTPEventsConfig) RefreshRateDuration() time.Duration { return time.Duration(cfg.RefreshRate) * time.Second } -func (cfg HTTPEventsConfigSlim) TimeoutDuration() time.Duration { - return time.Duration(cfg.Timeout) * time.Millisecond -} - -func (cfg HTTPEventsConfigSlim) RefreshRateDuration() time.Duration { - return time.Duration(cfg.RefreshRate) * time.Second -} - // CacheEventsConfig configured stored_requests/events/api/api.go type CacheEventsConfig struct { // Enabled should be true to enable the events api endpoint @@ -107,48 +82,64 @@ type FileFetcherConfig struct { Path string `mapstructure:"directorypath"` } -// HTTPFetcherConfigSlim configures a stored_requests/backends/http_fetcher/fetcher.go -type HTTPFetcherConfigSlim struct { - Endpoint string `mapstructure:"endpoint"` -} - // HTTPFetcherConfig configures a stored_requests/backends/http_fetcher/fetcher.go type HTTPFetcherConfig struct { Endpoint string `mapstructure:"endpoint"` AmpEndpoint string `mapstructure:"amp_endpoint"` } -func (cfg *StoredRequests) validate(errs configErrors) configErrors { +// Migrate combined stored_requests+amp configuration to separate simple config sections +func resolvedStoredRequestsConfig(cfg *Configuration) { + sr := &cfg.StoredRequests + amp := &cfg.StoredRequestsAMP + + sr.CacheEvents.Endpoint = "/storedrequests/openrtb2" // why is this here and not SetDefault ? + + // Amp uses the same config but some fields get replaced by Amp* version of similar fields + cfg.StoredRequestsAMP = cfg.StoredRequests + amp.Postgres.FetcherQueries.QueryTemplate = sr.Postgres.FetcherQueries.AmpQueryTemplate + amp.Postgres.CacheInitialization.Query = sr.Postgres.CacheInitialization.AmpQuery + amp.Postgres.PollUpdates.Query = sr.Postgres.PollUpdates.AmpQuery + amp.HTTP.Endpoint = sr.HTTP.AmpEndpoint + amp.CacheEvents.Endpoint = "/storedrequests/amp" + amp.HTTPEvents.Endpoint = sr.HTTPEvents.AmpEndpoint + + // Set data types for each section + cfg.StoredRequests.dataType = RequestDataType + cfg.StoredRequestsAMP.dataType = AMPRequestDataType + cfg.StoredVideo.dataType = VideoDataType + cfg.CategoryMapping.dataType = CategoryDataType + return +} + +func (cfg *StoredRequests) validate(section string, errs configErrors) configErrors { + errs = cfg.Postgres.validate(section, errs) + + // Categories do not use cache so none of the following checks apply + if cfg.dataType == CategoryDataType { + return errs + } + if cfg.InMemoryCache.Type == "none" { - if cfg.CacheEventsAPI { - errs = append(errs, errors.New("stored_requests.cache_events_api must be false if stored_requests.in_memory_cache=none")) + if cfg.CacheEvents.Enabled { + errs = append(errs, fmt.Errorf("%s: cache_events must be disabled if in_memory_cache=none", section)) } if cfg.HTTPEvents.RefreshRate != 0 { - errs = append(errs, errors.New("stored_requests.http_events.refresh_rate_seconds must be 0 if stored_requests.in_memory_cache=none")) + errs = append(errs, fmt.Errorf("%s: http_events.refresh_rate_seconds must be 0 if in_memory_cache=none", section)) } if cfg.Postgres.PollUpdates.Query != "" { - errs = append(errs, errors.New("stored_requests.postgres.poll_for_updates.query must be empty if stored_requests.in_memory_cache=none")) + errs = append(errs, fmt.Errorf("%s: postgres.poll_for_updates.query must be empty if in_memory_cache=none", section)) } if cfg.Postgres.CacheInitialization.Query != "" { - errs = append(errs, errors.New("stored_requests.postgres.initialize_caches.query must be empty if stored_requests.in_memory_cache=none")) + errs = append(errs, fmt.Errorf("%s: postgres.initialize_caches.query must be empty if in_memory_cache=none", section)) } } - errs = cfg.InMemoryCache.validate(errs) - errs = cfg.Postgres.validate(errs) + errs = cfg.InMemoryCache.validate(section, errs) return errs } -// PostgresConfigSlim configures the Stored Request ecosystem to use Postgres. This must include a Fetcher, -// and may optionally include some EventProducers to populate and refresh the caches. -type PostgresConfigSlim struct { - ConnectionInfo PostgresConnection `mapstructure:"connection"` - FetcherQueries PostgresFetcherQueriesSlim `mapstructure:"fetcher"` - CacheInitialization PostgresCacheInitializerSlim `mapstructure:"initialize_caches"` - PollUpdates PostgresUpdatePollingSlim `mapstructure:"poll_for_updates"` -} - // PostgresConfig configures the Stored Request ecosystem to use Postgres. This must include a Fetcher, // and may optionally include some EventProducers to populate and refresh the caches. type PostgresConfig struct { @@ -158,12 +149,12 @@ type PostgresConfig struct { PollUpdates PostgresUpdatePolling `mapstructure:"poll_for_updates"` } -func (cfg *PostgresConfig) validate(errs configErrors) configErrors { +func (cfg *PostgresConfig) validate(section string, errs configErrors) configErrors { if cfg.ConnectionInfo.Database == "" { return errs } - return cfg.PollUpdates.validate(errs) + return cfg.PollUpdates.validate(section, errs) } // PostgresConnection has options which put types to the Postgres Connection string. See: @@ -242,32 +233,6 @@ type PostgresFetcherQueries struct { AmpQueryTemplate string `mapstructure:"amp_query"` } -type PostgresFetcherQueriesSlim struct { - // QueryTemplate is the Postgres Query which can be used to fetch configs from the database. - // It is a Template, rather than a full Query, because a single HTTP request may reference multiple Stored Requests. - // - // In the simplest case, this could be something like: - // SELECT id, requestData, 'request' as type - // FROM stored_requests - // WHERE id in %REQUEST_ID_LIST% - // UNION ALL - // SELECT id, impData, 'imp' as type - // FROM stored_imps - // WHERE id in %IMP_ID_LIST% - // - // The MakeQuery function will transform this query into: - // SELECT id, requestData, 'request' as type - // FROM stored_requests - // WHERE id in ($1) - // UNION ALL - // SELECT id, impData, 'imp' as type - // FROM stored_imps - // WHERE id in ($2, $3, $4, ...) - // - // ... where the number of "$x" args depends on how many IDs are nested within the HTTP request. - QueryTemplate string `mapstructure:"query"` -} - type PostgresCacheInitializer struct { Timeout int `mapstructure:"timeout_ms"` // Query should be something like: @@ -284,69 +249,19 @@ type PostgresCacheInitializer struct { AmpQuery string `mapstructure:"amp_query"` } -type PostgresCacheInitializerSlim struct { - Timeout int `mapstructure:"timeout_ms"` - // Query should be something like: - // - // SELECT id, requestData, 'request' AS type FROM stored_requests - // UNION ALL - // SELECT id, impData, 'imp' AS type FROM stored_imps - // - // This query will be run once on startup to fetch _all_ known Stored Request data from the database. - // - // For more details on the expected format of requestData and impData, see stored_requests/events/postgres/polling.go - Query string `mapstructure:"query"` -} - -func (cfg *PostgresCacheInitializerSlim) validate(errs configErrors) configErrors { +func (cfg *PostgresCacheInitializer) validate(section string, errs configErrors) configErrors { if cfg.Query == "" { return errs } if cfg.Timeout <= 0 { - errs = append(errs, errors.New("stored_requests.postgres.initialize_caches.timeout_ms must be positive")) + errs = append(errs, fmt.Errorf("%s: postgres.initialize_caches.timeout_ms must be positive", section)) } if strings.Contains(cfg.Query, "$") { - errs = append(errs, errors.New("stored_requests.postgres.initialize_caches.query should not contain any wildcards (e.g. $1)")) - } - return errs -} - -func (cfg *PostgresCacheInitializer) validate(errs configErrors) configErrors { - if cfg.Query == "" && cfg.AmpQuery == "" { - return errs - } - - slim := &PostgresCacheInitializerSlim{Timeout: cfg.Timeout, Query: cfg.Query} - errs = slim.validate(errs) - - if strings.Contains(cfg.AmpQuery, "$") { - errs = append(errs, errors.New("stored_requests.postgres.initialize_caches.amp_query should not contain any wildcards (e.g. $1)")) + errs = append(errs, fmt.Errorf("%s: postgres.initialize_caches.query should not contain any wildcards (e.g. $1)", section)) } - return errs } -type PostgresUpdatePollingSlim struct { - // RefreshRate determines how frequently the Query and AmpQuery are run. - RefreshRate int `mapstructure:"refresh_rate_seconds"` - - // Timeout is the amount of time before a call to the database is aborted. - Timeout int `mapstructure:"timeout_ms"` - - // An example UpdateQuery is: - // - // SELECT id, requestData, 'request' AS type - // FROM stored_requests - // WHERE last_updated > $1 - // UNION ALL - // SELECT id, requestData, 'imp' AS type - // FROM stored_imps - // WHERE last_updated > $1 - // - // The code will be run periodically to fetch updates from the database. - Query string `mapstructure:"query"` -} - type PostgresUpdatePolling struct { // RefreshRate determines how frequently the Query and AmpQuery are run. RefreshRate int `mapstructure:"refresh_rate_seconds"` @@ -370,49 +285,31 @@ type PostgresUpdatePolling struct { AmpQuery string `mapstructure:"amp_query"` } -func (cfg *PostgresUpdatePollingSlim) validate(errs configErrors) configErrors { +func (cfg *PostgresUpdatePolling) validate(section string, errs configErrors) configErrors { if cfg.Query == "" { return errs } if cfg.RefreshRate <= 0 { - errs = append(errs, errors.New("stored_requests.postgres.poll_for_updates.refresh_rate_seconds must be > 0")) + errs = append(errs, fmt.Errorf("%s: postgres.poll_for_updates.refresh_rate_seconds must be > 0", section)) } if cfg.Timeout <= 0 { - errs = append(errs, errors.New("stored_requests.postgres.poll_for_updates.timeout_ms must be > 0")) + errs = append(errs, fmt.Errorf("%s: postgres.poll_for_updates.timeout_ms must be > 0", section)) } if !strings.Contains(cfg.Query, "$1") || strings.Contains(cfg.Query, "$2") { - errs = append(errs, errors.New("stored_requests.postgres.poll_for_updates.query must contain exactly one wildcard")) - } - return errs -} - -func (cfg *PostgresUpdatePolling) validate(errs configErrors) configErrors { - if cfg.Query == "" && cfg.AmpQuery == "" { - return errs - } - slim := &PostgresUpdatePollingSlim{RefreshRate: cfg.RefreshRate, Timeout: cfg.Timeout, Query: cfg.Query} - errs = slim.validate(errs) - - if !strings.Contains(cfg.AmpQuery, "$1") || strings.Contains(cfg.AmpQuery, "$2") { - errs = append(errs, errors.New("stored_requests.postgres.poll_for_updates.amp_query must contain exactly one wildcard")) + errs = append(errs, fmt.Errorf("%s: postgres.poll_for_updates.query must contain exactly one wildcard", section)) } return errs } // 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) { +func (cfg *PostgresFetcherQueries) MakeQuery(numReqs int, numImps int) (query string) { return resolve(cfg.QueryTemplate, numReqs, numImps) } -// MakeAmpQuery is the equivalent of MakeQuery() for AMP. -func (cfg *PostgresFetcherQueries) MakeAmpQuery(numReqs int, numImps int) string { - return resolve(cfg.AmpQueryTemplate, numReqs, numImps) -} - func resolve(template string, numReqs int, numImps int) (query string) { numReqs = ensureNonNegative("Request", numReqs) numImps = ensureNonNegative("Imp", numImps) @@ -473,29 +370,29 @@ type InMemoryCache struct { ImpCacheSize int `mapstructure:"imp_cache_size_bytes"` } -func (cfg *InMemoryCache) validate(errs configErrors) configErrors { +func (cfg *InMemoryCache) validate(section string, errs configErrors) configErrors { switch cfg.Type { case "none": // No errors for no config options case "unbounded": if cfg.TTL != 0 { - errs = append(errs, fmt.Errorf("stored_requests.in_memory_cache must be 0 for unbounded caches. Got %d", cfg.TTL)) + errs = append(errs, fmt.Errorf("%s: in_memory_cache must be 0 for unbounded caches. Got %d", section, cfg.TTL)) } if cfg.RequestCacheSize != 0 { - errs = append(errs, fmt.Errorf("stored_requests.in_memory_cache.request_cache_size_bytes must be 0 for unbounded caches. Got %d", cfg.RequestCacheSize)) + errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes must be 0 for unbounded caches. Got %d", section, cfg.RequestCacheSize)) } if cfg.ImpCacheSize != 0 { - errs = append(errs, fmt.Errorf("stored_requests.in_memory_cache.imp_cache_size_bytes must be 0 for unbounded caches. Got %d", cfg.ImpCacheSize)) + errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes must be 0 for unbounded caches. Got %d", section, cfg.ImpCacheSize)) } case "lru": if cfg.RequestCacheSize <= 0 { - errs = append(errs, fmt.Errorf("stored_requests.in_memory_cache.request_cache_size_bytes must be >= 0 when stored_requests.in_memory_cache.type=lru. Got %d", cfg.RequestCacheSize)) + errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.RequestCacheSize)) } if cfg.ImpCacheSize <= 0 { - errs = append(errs, fmt.Errorf("stored_requests.in_memory_cache.imp_cache_size_bytes must be >= 0 when stored_requests.in_memory_cache.type=lru. Got %d", cfg.ImpCacheSize)) + errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.ImpCacheSize)) } default: - errs = append(errs, fmt.Errorf("stored_requests.in_memory_cache.type %s is invalid", cfg.Type)) + errs = append(errs, fmt.Errorf("%s: in_memory_cache.type %s is invalid", section, cfg.Type)) } return errs } diff --git a/config/stored_requests_test.go b/config/stored_requests_test.go index 3e01dd3c853..36a5e3793ed 100644 --- a/config/stored_requests_test.go +++ b/config/stored_requests_test.go @@ -78,40 +78,40 @@ func TestPostgressConnString(t *testing.T) { func TestInMemoryCacheValidation(t *testing.T) { assertNoErrs(t, (&InMemoryCache{ Type: "unbounded", - }).validate(nil)) + }).validate("Test", nil)) assertNoErrs(t, (&InMemoryCache{ Type: "none", - }).validate(nil)) + }).validate("Test", nil)) assertNoErrs(t, (&InMemoryCache{ Type: "lru", RequestCacheSize: 1000, ImpCacheSize: 1000, - }).validate(nil)) + }).validate("Test", nil)) assertErrsExist(t, (&InMemoryCache{ Type: "unrecognized", - }).validate(nil)) + }).validate("Test", nil)) assertErrsExist(t, (&InMemoryCache{ Type: "unbounded", ImpCacheSize: 1000, - }).validate(nil)) + }).validate("Test", nil)) assertErrsExist(t, (&InMemoryCache{ Type: "unbounded", RequestCacheSize: 1000, - }).validate(nil)) + }).validate("Test", nil)) assertErrsExist(t, (&InMemoryCache{ Type: "unbounded", TTL: 500, - }).validate(nil)) + }).validate("Test", nil)) assertErrsExist(t, (&InMemoryCache{ Type: "lru", RequestCacheSize: 0, ImpCacheSize: 1000, - }).validate(nil)) + }).validate("Test", nil)) assertErrsExist(t, (&InMemoryCache{ Type: "lru", RequestCacheSize: 1000, ImpCacheSize: 0, - }).validate(nil)) + }).validate("Test", nil)) } func assertErrsExist(t *testing.T, err configErrors) { @@ -140,7 +140,7 @@ func assertHasValue(t *testing.T, m map[string]string, key string, val string) { } func buildQuery(template string, numReqs int, numImps int) string { - cfg := PostgresFetcherQueriesSlim{} + cfg := PostgresFetcherQueries{} cfg.QueryTemplate = template return cfg.MakeQuery(numReqs, numImps) @@ -152,3 +152,67 @@ func assertStringsEqual(t *testing.T, actual string, expected string) { } } + +func TestResolveConfig(t *testing.T) { + cfg := &Configuration{ + StoredRequests: StoredRequests{ + Files: FileFetcherConfig{ + Enabled: true, + Path: "/test-path"}, + Postgres: PostgresConfig{ + ConnectionInfo: PostgresConnection{ + Database: "db", + Host: "pghost", + Port: 5, + Username: "user", + Password: "pass", + }, + FetcherQueries: PostgresFetcherQueries{ + AmpQueryTemplate: "amp-fetcher-query", + }, + CacheInitialization: PostgresCacheInitializer{ + AmpQuery: "amp-cache-init-query", + }, + PollUpdates: PostgresUpdatePolling{ + AmpQuery: "amp-poll-query", + }, + }, + HTTP: HTTPFetcherConfig{ + AmpEndpoint: "amp-http-fetcher-endpoint", + }, + InMemoryCache: InMemoryCache{ + Type: "none", + TTL: 50, + RequestCacheSize: 1, + ImpCacheSize: 2, + }, + CacheEvents: CacheEventsConfig{ + Enabled: true, + }, + HTTPEvents: HTTPEventsConfig{ + AmpEndpoint: "amp-http-events-endpoint", + }, + }, + } + + cfg.StoredRequests.Postgres.FetcherQueries.QueryTemplate = "auc-fetcher-query" + cfg.StoredRequests.Postgres.CacheInitialization.Query = "auc-cache-init-query" + cfg.StoredRequests.Postgres.PollUpdates.Query = "auc-poll-query" + cfg.StoredRequests.HTTP.Endpoint = "auc-http-fetcher-endpoint" + cfg.StoredRequests.HTTPEvents.Endpoint = "auc-http-events-endpoint" + + resolvedStoredRequestsConfig(cfg) + auc := &cfg.StoredRequests + amp := &cfg.StoredRequestsAMP + + // Auction should have the non-amp values in it + assertStringsEqual(t, auc.CacheEvents.Endpoint, "/storedrequests/openrtb2") + + // Amp should have the amp values in it + assertStringsEqual(t, amp.Postgres.FetcherQueries.QueryTemplate, cfg.StoredRequests.Postgres.FetcherQueries.AmpQueryTemplate) + assertStringsEqual(t, amp.Postgres.CacheInitialization.Query, cfg.StoredRequests.Postgres.CacheInitialization.AmpQuery) + assertStringsEqual(t, amp.Postgres.PollUpdates.Query, cfg.StoredRequests.Postgres.PollUpdates.AmpQuery) + assertStringsEqual(t, amp.HTTP.Endpoint, cfg.StoredRequests.HTTP.AmpEndpoint) + assertStringsEqual(t, amp.HTTPEvents.Endpoint, cfg.StoredRequests.HTTPEvents.AmpEndpoint) + assertStringsEqual(t, amp.CacheEvents.Endpoint, "/storedrequests/amp") +} diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go index e8f6b0bdf46..8f06efcb32b 100644 --- a/stored_requests/config/config.go +++ b/stored_requests/config/config.go @@ -41,25 +41,26 @@ type dbConnection struct { // // As a side-effect, it will add some endpoints to the router if the config calls for it. // In the future we should look for ways to simplify this so that it's not doing two things. -func CreateStoredRequests(cfg *config.StoredRequestsSlim, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router, dbc *dbConnection) (fetcher stored_requests.AllFetcher, shutdown func()) { +func CreateStoredRequests(cfg *config.StoredRequests, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router, dbc *dbConnection) (fetcher stored_requests.AllFetcher, shutdown func()) { // Create database connection if given options for one if cfg.Postgres.ConnectionInfo.Database != "" { conn := cfg.Postgres.ConnectionInfo.ConnString() if dbc.conn == "" { - glog.Infof("Connecting to Postgres for Stored Requests. DB=%s, host=%s, port=%d, user=%s", + glog.Infof("Connecting to Postgres for Stored %s. DB=%s, host=%s, port=%d, user=%s", + cfg.DataType(), cfg.Postgres.ConnectionInfo.Database, cfg.Postgres.ConnectionInfo.Host, cfg.Postgres.ConnectionInfo.Port, cfg.Postgres.ConnectionInfo.Username) - db := newPostgresDB(cfg.Postgres.ConnectionInfo) + db := newPostgresDB(cfg.DataType(), cfg.Postgres.ConnectionInfo) dbc.conn = conn dbc.db = db } // Error out if config is trying to use multiple database connections for different stored requests (not supported yet) if conn != dbc.conn { - glog.Fatal("Multiple database connection settings found in Stored Requests config, only a single database connection is currently supported.") + glog.Fatal("Multiple database connection settings found in config, only a single database connection is currently supported.") } } @@ -106,9 +107,6 @@ func CreateStoredRequests(cfg *config.StoredRequestsSlim, metricsEngine pbsmetri // As a side-effect, it will add some endpoints to the router if the config calls for it. // In the future we should look for ways to simplify this so that it's not doing two things. func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router) (db *sql.DB, shutdown func(), fetcher stored_requests.Fetcher, ampFetcher stored_requests.Fetcher, categoriesFetcher stored_requests.CategoryFetcher, videoFetcher stored_requests.Fetcher) { - // Build individual slim options from combined config struct - slimAuction, slimAmp := resolvedStoredRequestsConfig(cfg) - // TODO: Switch this to be set in config defaults //if cfg.CategoryMapping.CacheEvents.Enabled && cfg.CategoryMapping.CacheEvents.Endpoint == "" { // cfg.CategoryMapping.CacheEvents.Endpoint = "/storedrequest/categorymapping" @@ -116,8 +114,8 @@ func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.Metri var dbc dbConnection - fetcher1, shutdown1 := CreateStoredRequests(&slimAuction, metricsEngine, client, router, &dbc) - fetcher2, shutdown2 := CreateStoredRequests(&slimAmp, metricsEngine, client, router, &dbc) + fetcher1, shutdown1 := CreateStoredRequests(&cfg.StoredRequests, metricsEngine, client, router, &dbc) + fetcher2, shutdown2 := CreateStoredRequests(&cfg.StoredRequestsAMP, metricsEngine, client, router, &dbc) fetcher3, shutdown3 := CreateStoredRequests(&cfg.CategoryMapping, metricsEngine, client, router, &dbc) fetcher4, shutdown4 := CreateStoredRequests(&cfg.StoredVideo, metricsEngine, client, router, &dbc) @@ -138,48 +136,6 @@ func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.Metri return } -func resolvedStoredRequestsConfig(cfg *config.Configuration) (auc, amp config.StoredRequestsSlim) { - sr := &cfg.StoredRequests - - // Auction endpoint uses non-Amp fields so can just copy the slin data - auc.Files.Enabled = sr.Files - auc.Files.Path = sr.Path - auc.Postgres.ConnectionInfo = sr.Postgres.ConnectionInfo - auc.Postgres.FetcherQueries.QueryTemplate = sr.Postgres.FetcherQueries.QueryTemplate - auc.Postgres.CacheInitialization.Timeout = sr.Postgres.CacheInitialization.Timeout - auc.Postgres.CacheInitialization.Query = sr.Postgres.CacheInitialization.Query - auc.Postgres.PollUpdates.RefreshRate = sr.Postgres.PollUpdates.RefreshRate - auc.Postgres.PollUpdates.Timeout = sr.Postgres.PollUpdates.Timeout - auc.Postgres.PollUpdates.Query = sr.Postgres.PollUpdates.Query - auc.HTTP.Endpoint = sr.HTTP.Endpoint - auc.InMemoryCache = sr.InMemoryCache - auc.CacheEvents.Enabled = sr.CacheEventsAPI - auc.CacheEvents.Endpoint = "/storedrequests/openrtb2" - auc.HTTPEvents.RefreshRate = sr.HTTPEvents.RefreshRate - auc.HTTPEvents.Timeout = sr.HTTPEvents.Timeout - auc.HTTPEvents.Endpoint = sr.HTTPEvents.Endpoint - - // Amp endpoint uses all the slim data but some fields get replacyed by Amp* version of similar fields - amp.Files.Enabled = sr.Files - amp.Files.Path = sr.Path - amp.Postgres.ConnectionInfo = sr.Postgres.ConnectionInfo - amp.Postgres.FetcherQueries.QueryTemplate = sr.Postgres.FetcherQueries.AmpQueryTemplate - amp.Postgres.CacheInitialization.Timeout = sr.Postgres.CacheInitialization.Timeout - amp.Postgres.CacheInitialization.Query = sr.Postgres.CacheInitialization.AmpQuery - amp.Postgres.PollUpdates.RefreshRate = sr.Postgres.PollUpdates.RefreshRate - amp.Postgres.PollUpdates.Timeout = sr.Postgres.PollUpdates.Timeout - amp.Postgres.PollUpdates.Query = sr.Postgres.PollUpdates.AmpQuery - amp.HTTP.Endpoint = sr.HTTP.AmpEndpoint - amp.InMemoryCache = sr.InMemoryCache - amp.CacheEvents.Enabled = sr.CacheEventsAPI - amp.CacheEvents.Endpoint = "/storedrequests/amp" - amp.HTTPEvents.RefreshRate = sr.HTTPEvents.RefreshRate - amp.HTTPEvents.Timeout = sr.HTTPEvents.Timeout - amp.HTTPEvents.Endpoint = sr.HTTPEvents.AmpEndpoint - - return -} - func addListeners(cache stored_requests.Cache, eventProducers []events.EventProducer) (shutdown func()) { listeners := make([]*events.EventListener, 0, len(eventProducers)) @@ -196,36 +152,36 @@ func addListeners(cache stored_requests.Cache, eventProducers []events.EventProd } } -func newFetcher(cfg *config.StoredRequestsSlim, client *http.Client, db *sql.DB) (fetcher stored_requests.AllFetcher) { +func newFetcher(cfg *config.StoredRequests, client *http.Client, db *sql.DB) (fetcher stored_requests.AllFetcher) { idList := make(stored_requests.MultiFetcher, 0, 3) if cfg.Files.Enabled { - fFetcher := newFilesystem(cfg.Files.Path) + fFetcher := newFilesystem(cfg.DataType(), cfg.Files.Path) idList = append(idList, fFetcher) } if cfg.Postgres.FetcherQueries.QueryTemplate != "" { - glog.Infof("Loading Stored Requests via Postgres.\nQuery: %s", cfg.Postgres.FetcherQueries.QueryTemplate) + glog.Infof("Loading Stored %s data via Postgres.\nQuery: %s", cfg.DataType(), cfg.Postgres.FetcherQueries.QueryTemplate) idList = append(idList, db_fetcher.NewFetcher(db, cfg.Postgres.FetcherQueries.MakeQuery)) } if cfg.HTTP.Endpoint != "" { - glog.Infof("Loading Stored Requests via HTTP. endpoint=%s", cfg.HTTP.Endpoint) + glog.Infof("Loading Stored %s data via HTTP. endpoint=%s", cfg.DataType(), cfg.HTTP.Endpoint) idList = append(idList, http_fetcher.NewFetcher(client, cfg.HTTP.Endpoint)) } - fetcher = consolidate(idList) + fetcher = consolidate(cfg.DataType(), idList) return } -func newCache(cfg *config.StoredRequestsSlim) stored_requests.Cache { +func newCache(cfg *config.StoredRequests) stored_requests.Cache { if cfg.InMemoryCache.Type == "none" { - glog.Info("No Stored Request cache configured. The Fetcher backend will be used for all Stored Requests.") + glog.Infof("No Stored %s cache configured. The %s Fetcher backend will be used for all data requests", cfg.DataType(), cfg.DataType()) return &nil_cache.NilCache{} } return memory.NewCache(&cfg.InMemoryCache) } -func newEventProducers(cfg *config.StoredRequestsSlim, client *http.Client, db *sql.DB, router *httprouter.Router) (eventProducers []events.EventProducer) { +func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, router *httprouter.Router) (eventProducers []events.EventProducer) { if cfg.CacheEvents.Enabled { eventProducers = append(eventProducers, newEventsAPI(router, cfg.CacheEvents.Endpoint)) } @@ -247,7 +203,7 @@ func newEventProducers(cfg *config.StoredRequestsSlim, client *http.Client, db * return } -func newPostgresPolling(cfg config.PostgresUpdatePollingSlim, db *sql.DB, startTime time.Time) events.EventProducer { +func newPostgresPolling(cfg config.PostgresUpdatePolling, db *sql.DB, startTime time.Time) events.EventProducer { timeout := time.Duration(cfg.Timeout) * time.Millisecond ctxProducer := func() (ctx context.Context, canceller func()) { return context.WithTimeout(context.Background(), timeout) @@ -269,32 +225,37 @@ func newHttpEvents(client *http.Client, timeout time.Duration, refreshRate time. return httpEvents.NewHTTPEvents(client, endpoint, ctxProducer, refreshRate) } -func newFilesystem(configPath string) stored_requests.AllFetcher { - glog.Infof("Loading Stored Requests from filesystem at path %s", configPath) +func newFilesystem(dataType config.DataType, configPath string) stored_requests.AllFetcher { + glog.Infof("Loading Stored %s data from filesystem at path %s", dataType, configPath) fetcher, err := file_fetcher.NewFileFetcher(configPath) if err != nil { - glog.Fatalf("Failed to create a FileFetcher: %v", err) + glog.Fatalf("Failed to create a %s FileFetcher: %v", dataType, err) } return fetcher } -func newPostgresDB(cfg config.PostgresConnection) *sql.DB { +func newPostgresDB(dataType config.DataType, cfg config.PostgresConnection) *sql.DB { db, err := sql.Open("postgres", cfg.ConnString()) if err != nil { - glog.Fatalf("Failed to open postgres connection: %v", err) + glog.Fatalf("Failed to open %s postgres connection: %v", dataType, err) } if err := db.Ping(); err != nil { - glog.Fatalf("Failed to ping postgres: %v", err) + glog.Fatalf("Failed to ping %s postgres: %v", dataType, err) } return db } // consolidate returns a single Fetcher from an array of fetchers of any size. -func consolidate(fetchers []stored_requests.AllFetcher) stored_requests.AllFetcher { +func consolidate(dataType config.DataType, fetchers []stored_requests.AllFetcher) stored_requests.AllFetcher { if len(fetchers) == 0 { - glog.Warning("No Stored Request support configured. request.imp[i].ext.prebid.storedrequest will be ignored. If you need this, check your app config") + switch dataType { + case config.RequestDataType: + glog.Warning("No Stored Request support configured. request.imp[i].ext.prebid.storedrequest will be ignored. If you need this, check your app config") + default: + glog.Warningf("No Stored %s support configured. If you need this, check your app config", dataType) + } return empty_fetcher.EmptyFetcher{} } else if len(fetchers) == 1 { return fetchers[0] diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go index 11748a59966..4c3943ea5be 100644 --- a/stored_requests/config/config_test.go +++ b/stored_requests/config/config_test.go @@ -19,8 +19,8 @@ import ( ) func TestNewEmptyFetcher(t *testing.T) { - fetcher := newFetcher(&config.StoredRequestsSlim{}, nil, nil) - ampFetcher := newFetcher(&config.StoredRequestsSlim{}, nil, nil) + fetcher := newFetcher(&config.StoredRequests{}, nil, nil) + ampFetcher := newFetcher(&config.StoredRequests{}, nil, nil) if fetcher == nil || ampFetcher == nil { t.Errorf("The fetchers should be non-nil, even with an empty config.") } @@ -33,13 +33,13 @@ func TestNewEmptyFetcher(t *testing.T) { } func TestNewHTTPFetcher(t *testing.T) { - fetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ + fetcher := newFetcher(&config.StoredRequests{ + HTTP: config.HTTPFetcherConfig{ Endpoint: "stored-requests.prebid.com", }, }, nil, nil) - ampFetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ + ampFetcher := newFetcher(&config.StoredRequests{ + HTTP: config.HTTPFetcherConfig{ Endpoint: "stored-requests.prebid.com?type=amp", }, }, nil, nil) @@ -60,13 +60,13 @@ func TestNewHTTPFetcher(t *testing.T) { } func TestNewHTTPFetcherNoAmp(t *testing.T) { - fetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ + fetcher := newFetcher(&config.StoredRequests{ + HTTP: config.HTTPFetcherConfig{ Endpoint: "stored-requests.prebid.com", }, }, nil, nil) - ampFetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ + ampFetcher := newFetcher(&config.StoredRequests{ + HTTP: config.HTTPFetcherConfig{ Endpoint: "", }, }, nil, nil) @@ -82,78 +82,14 @@ func TestNewHTTPFetcherNoAmp(t *testing.T) { } } -func TestResolveConfig(t *testing.T) { - cfg := &config.Configuration{ - StoredRequests: config.StoredRequests{ - Files: true, - Path: "/test-path", - Postgres: config.PostgresConfig{ - ConnectionInfo: config.PostgresConnection{ - Database: "db", - Host: "pghost", - Port: 5, - Username: "user", - Password: "pass", - }, - FetcherQueries: config.PostgresFetcherQueries{ - AmpQueryTemplate: "amp-fetcher-query", - }, - CacheInitialization: config.PostgresCacheInitializer{ - AmpQuery: "amp-cache-init-query", - }, - PollUpdates: config.PostgresUpdatePolling{ - AmpQuery: "amp-poll-query", - }, - }, - HTTP: config.HTTPFetcherConfig{ - AmpEndpoint: "amp-http-fetcher-endpoint", - }, - InMemoryCache: config.InMemoryCache{ - Type: "none", - TTL: 50, - RequestCacheSize: 1, - ImpCacheSize: 2, - }, - CacheEventsAPI: true, - HTTPEvents: config.HTTPEventsConfig{ - AmpEndpoint: "amp-http-events-endpoint", - }, - }, - } - - cfg.StoredRequests.Postgres.FetcherQueries.QueryTemplate = "auc-fetcher-query" - cfg.StoredRequests.Postgres.CacheInitialization.Query = "auc-cache-init-query" - cfg.StoredRequests.Postgres.PollUpdates.Query = "auc-poll-query" - cfg.StoredRequests.HTTP.Endpoint = "auc-http-fetcher-endpoint" - cfg.StoredRequests.HTTPEvents.Endpoint = "auc-http-events-endpoint" - - auc, amp := resolvedStoredRequestsConfig(cfg) - - // Auction slim should have the non-amp values in it - assertStringsEqual(t, auc.Postgres.FetcherQueries.QueryTemplate, cfg.StoredRequests.Postgres.FetcherQueries.QueryTemplate) - assertStringsEqual(t, auc.Postgres.CacheInitialization.Query, cfg.StoredRequests.Postgres.CacheInitialization.Query) - assertStringsEqual(t, auc.Postgres.PollUpdates.Query, cfg.StoredRequests.Postgres.PollUpdates.Query) - assertStringsEqual(t, auc.HTTP.Endpoint, cfg.StoredRequests.HTTP.Endpoint) - assertStringsEqual(t, auc.HTTPEvents.Endpoint, cfg.StoredRequests.HTTPEvents.Endpoint) - assertStringsEqual(t, auc.CacheEvents.Endpoint, "/storedrequests/openrtb2") - - // Amp slim should have the amp values in it - assertStringsEqual(t, amp.Postgres.FetcherQueries.QueryTemplate, cfg.StoredRequests.Postgres.FetcherQueries.AmpQueryTemplate) - assertStringsEqual(t, amp.Postgres.CacheInitialization.Query, cfg.StoredRequests.Postgres.CacheInitialization.AmpQuery) - assertStringsEqual(t, amp.Postgres.PollUpdates.Query, cfg.StoredRequests.Postgres.PollUpdates.AmpQuery) - assertStringsEqual(t, amp.HTTP.Endpoint, cfg.StoredRequests.HTTP.AmpEndpoint) - assertStringsEqual(t, amp.HTTPEvents.Endpoint, cfg.StoredRequests.HTTPEvents.AmpEndpoint) - assertStringsEqual(t, amp.CacheEvents.Endpoint, "/storedrequests/amp") -} - func TestNewHTTPEvents(t *testing.T) { handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) } server1 := httptest.NewServer(http.HandlerFunc(handler)) - cfg := &config.StoredRequestsSlim{ - HTTPEvents: config.HTTPEventsConfigSlim{ + cfg := &config.StoredRequests{ + HTTPEvents: config.HTTPEventsConfig{ Endpoint: server1.URL, RefreshRate: 100, Timeout: 1000, @@ -165,7 +101,7 @@ func TestNewHTTPEvents(t *testing.T) { } func TestNewEmptyCache(t *testing.T) { - cache := newCache(&config.StoredRequestsSlim{InMemoryCache: config.InMemoryCache{Type: "none"}}) + cache := newCache(&config.StoredRequests{InMemoryCache: config.InMemoryCache{Type: "none"}}) cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}, nil) reqs, _ := cache.Get(context.Background(), []string{"foo"}, nil) if len(reqs) != 0 { @@ -174,7 +110,7 @@ func TestNewEmptyCache(t *testing.T) { } func TestNewInMemoryCache(t *testing.T) { - cache := newCache(&config.StoredRequestsSlim{ + cache := newCache(&config.StoredRequests{ InMemoryCache: config.InMemoryCache{ TTL: 60, RequestCacheSize: 100, @@ -189,26 +125,26 @@ func TestNewInMemoryCache(t *testing.T) { } func TestNewPostgresEventProducers(t *testing.T) { - cfg := &config.StoredRequestsSlim{ - Postgres: config.PostgresConfigSlim{ - CacheInitialization: config.PostgresCacheInitializerSlim{ + cfg := &config.StoredRequests{ + Postgres: config.PostgresConfig{ + CacheInitialization: config.PostgresCacheInitializer{ Timeout: 50, Query: "SELECT id, requestData, type FROM stored_data", }, - PollUpdates: config.PostgresUpdatePollingSlim{ + PollUpdates: config.PostgresUpdatePolling{ RefreshRate: 20, Timeout: 50, Query: "SELECT id, requestData, type FROM stored_data WHERE last_updated > $1", }, }, } - ampCfg := &config.StoredRequestsSlim{ - Postgres: config.PostgresConfigSlim{ - CacheInitialization: config.PostgresCacheInitializerSlim{ + ampCfg := &config.StoredRequests{ + Postgres: config.PostgresConfig{ + CacheInitialization: config.PostgresCacheInitializer{ Timeout: 50, Query: "SELECT id, requestData, type FROM stored_amp_data", }, - PollUpdates: config.PostgresUpdatePollingSlim{ + PollUpdates: config.PostgresUpdatePolling{ RefreshRate: 20, Timeout: 50, Query: "SELECT id, requestData, type FROM stored_amp_data WHERE last_updated > $1", From f350cdab662a6feca661226fec0cd10f69363ab8 Mon Sep 17 00:00:00 2001 From: guscarreon Date: Wed, 2 Sep 2020 12:51:09 -0400 Subject: [PATCH 191/381] Fix Test TestEventChannel_OutputFormat (#1468) --- analytics/pubstack/eventchannel/eventchannel_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/analytics/pubstack/eventchannel/eventchannel_test.go b/analytics/pubstack/eventchannel/eventchannel_test.go index 792e15e151e..f450fb61fe1 100644 --- a/analytics/pubstack/eventchannel/eventchannel_test.go +++ b/analytics/pubstack/eventchannel/eventchannel_test.go @@ -3,11 +3,12 @@ package eventchannel import ( "bytes" "compress/gzip" - "github.com/stretchr/testify/assert" "io/ioutil" "sync" "testing" "time" + + "github.com/stretchr/testify/assert" ) var maxByteSize = int64(15) @@ -160,16 +161,21 @@ func TestEventChannel_OutputFormat(t *testing.T) { eventChannel := NewEventChannel(send, 15000, 10, 2*time.Minute) eventChannel.Push([]byte("one")) + time.Sleep(1 * time.Millisecond) + eventChannel.flush() + eventChannel.Push([]byte("two")) + time.Sleep(1 * time.Millisecond) + eventChannel.Push([]byte("three")) + time.Sleep(1 * time.Millisecond) eventChannel.Close() - time.Sleep(10 * time.Millisecond) + time.Sleep(1 * time.Millisecond) expected := append(toGzip("one"), toGzip("twothree")...) assert.Equal(t, expected, data) - } From c867e6f7edbd8fbe197b16441f1792a05642973b Mon Sep 17 00:00:00 2001 From: Mansi Nahar Date: Wed, 2 Sep 2020 16:24:49 -0400 Subject: [PATCH 192/381] Add ability to randomly generate source.TID if empty and set publisher.ID to resolved account ID (#1439) --- config/config.go | 3 + config/config_test.go | 3 + endpoints/openrtb2/amp_auction.go | 35 ++++-- endpoints/openrtb2/amp_auction_test.go | 79 +++++++++++++ endpoints/openrtb2/auction.go | 31 ++++- endpoints/openrtb2/auction_test.go | 158 +++++++++++++++++++++++-- endpoints/openrtb2/video_auction.go | 4 +- exchange/utils.go | 7 +- 8 files changed, 293 insertions(+), 27 deletions(-) diff --git a/config/config.go b/config/config.go index e443a48ec1d..23cc35719db 100755 --- a/config/config.go +++ b/config/config.go @@ -73,6 +73,8 @@ type Configuration struct { Debug Debug `mapstructure:"debug"` // RequestValidation specifies the request validation options. RequestValidation RequestValidation `mapstructure:"request_validation"` + // When true, PBS will assign a randomly generated UUID to req.Source.TID if it is empty + AutoGenSourceTID bool `mapstructure:"auto_gen_source_tid"` } const MIN_COOKIE_SIZE_BYTES = 500 @@ -975,6 +977,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("blacklisted_accts", []string{""}) v.SetDefault("account_required", false) v.SetDefault("certificates_file", "") + v.SetDefault("auto_gen_source_tid", true) v.SetDefault("request_timeout_headers.request_time_in_queue", "") v.SetDefault("request_timeout_headers.request_timeout_in_queue", "") diff --git a/config/config_test.go b/config/config_test.go index b23ddd6f614..23764d216ef 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -138,6 +138,7 @@ func TestDefaults(t *testing.T) { cmpStrings(t, "certificates_file", cfg.PemCertsFile, "") cmpBools(t, "stored_requests.filesystem.enabled", false, cfg.StoredRequests.Files.Enabled) cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path) + cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, true) } var fullConfig = []byte(` @@ -224,6 +225,7 @@ adapters: usersync_url: https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r= blacklisted_apps: ["spamAppID","sketchy-app-id"] account_required: true +auto_gen_source_tid: false certificates_file: /etc/ssl/cert.pem request_validation: ipv4_private_networks: ["1.1.1.0/24"] @@ -406,6 +408,7 @@ func TestFullConfig(t *testing.T) { cmpStrings(t, "adapters.rhythmone.endpoint", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].Endpoint, "http://tag.1rx.io/rmp") cmpStrings(t, "adapters.rhythmone.usersync_url", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].UserSyncURL, "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=http%3A%2F%2Fprebid-server.prebid.org%2F%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BRX_UUID%5D") cmpBools(t, "account_required", cfg.AccountRequired, true) + cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, false) cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true) cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true) cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem") diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 8efba5a926c..f5b72e029e0 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -154,7 +154,8 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h } else { labels.CookieFlag = pbsmetrics.CookieFlagYes } - labels.PubID = effectivePubID(req.Site.Publisher) + labels.PubID = getAccountID(req.Site.Publisher) + // Blacklist account now that we have resolved the value if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { errL = append(errL, acctIdErr) @@ -388,13 +389,7 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope setAmpExt(req.Site, "1") - account := httpRequest.FormValue("account") - if account != "" { - if req.Site.Publisher == nil { - req.Site.Publisher = &openrtb.Publisher{} - } - req.Site.Publisher.ID = account - } + setEffectiveAmpPubID(req, httpRequest.URL.Query()) slot := httpRequest.FormValue("slot") if slot != "" { @@ -564,3 +559,27 @@ func readConsent(url *url.URL) string { // Fallback to 'gdpr_consent' for compatability until it's no longer used by AMP. return url.Query().Get("gdpr_consent") } + +// Sets the effective publisher ID for amp request +func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) { + var pub *openrtb.Publisher + if req.App != nil { + if req.App.Publisher == nil { + req.App.Publisher = new(openrtb.Publisher) + } + pub = req.App.Publisher + } else if req.Site != nil { + if req.Site.Publisher == nil { + req.Site.Publisher = new(openrtb.Publisher) + } + pub = req.Site.Publisher + } + + if pub.ID == "" { + // For amp requests, the publisher ID could be sent via the account + // query string + if acc := urlQueryParams.Get("account"); acc != "" && acc != "ACCOUNT_ID" { + pub.ID = acc + } + } +} diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 692d3fb0c5d..be6735c4c39 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -1042,3 +1042,82 @@ func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, return json.Marshal(bidRequest) } + +func TestSetEffectiveAmpPubID(t *testing.T) { + testPubID := "test-pub" + testURLQueryParams := url.Values{} + testURLQueryParams.Add("account", testPubID) + + testCases := []struct { + description string + req *openrtb.BidRequest + urlQueryParams url.Values + expectedPubID string + }{ + { + description: "No publisher ID provided", + req: &openrtb.BidRequest{ + App: &openrtb.App{ + Publisher: nil, + }, + }, + expectedPubID: "", + }, + { + description: "Publisher ID present in req.App.Publisher.ID", + req: &openrtb.BidRequest{ + App: &openrtb.App{ + Publisher: &openrtb.Publisher{ + ID: testPubID, + }, + }, + }, + expectedPubID: testPubID, + }, + { + description: "Publisher ID present in req.Site.Publisher.ID", + req: &openrtb.BidRequest{ + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: testPubID, + }, + }, + }, + expectedPubID: testPubID, + }, + { + description: "Publisher ID present in account query parameter", + req: &openrtb.BidRequest{ + App: &openrtb.App{ + Publisher: &openrtb.Publisher{ + ID: "", + }, + }, + }, + urlQueryParams: testURLQueryParams, + expectedPubID: testPubID, + }, + { + description: "req.Site.Publisher present but ID set to empty string", + req: &openrtb.BidRequest{ + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "", + }, + }, + }, + expectedPubID: "", + }, + } + + for _, test := range testCases { + setEffectiveAmpPubID(test.req, test.urlQueryParams) + if test.req.Site != nil { + assert.Equal(t, test.expectedPubID, test.req.Site.Publisher.ID, + "should return the expected Publisher ID for test case: %s", test.description) + } else { + assert.Equal(t, test.expectedPubID, test.req.App.Publisher.ID, + "should return the expected Publisher ID for test case: %s", test.description) + } + } +} diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 86186fa8373..bad2f8aae5b 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -15,6 +15,7 @@ import ( "github.com/buger/jsonparser" jsonpatch "github.com/evanphx/json-patch" + "github.com/gofrs/uuid" "github.com/golang/glog" "github.com/julienschmidt/httprouter" "github.com/mssola/user_agent" @@ -141,7 +142,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http if req.App != nil { labels.Source = pbsmetrics.DemandApp labels.RType = pbsmetrics.ReqTypeORTB2App - labels.PubID = effectivePubID(req.App.Publisher) + labels.PubID = getAccountID(req.App.Publisher) } else { //req.Site != nil labels.Source = pbsmetrics.DemandWeb if usersyncs.LiveSyncCount() == 0 { @@ -149,7 +150,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http } else { labels.CookieFlag = pbsmetrics.CookieFlagYes } - labels.PubID = effectivePubID(req.Site.Publisher) + labels.PubID = getAccountID(req.Site.Publisher) } if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { @@ -283,6 +284,14 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { errL = append(errL, &errortypes.Warning{Message: fmt.Sprintf("A prebid request can only process one currency. Taking the first currency in the list, %s, as the active currency", req.Cur[0])}) } + // If automatically filling source TID is enabled then validate that + // source.TID exists and If it doesn't, fill it with a randomly generated UUID + if deps.cfg.AutoGenSourceTID { + if err := validateAndFillSourceTID(req); err != nil { + return []error{err} + } + } + var aliases map[string]string if bidExt, err := deps.parseBidExt(req.Ext); err != nil { return []error{err} @@ -358,6 +367,20 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { return errL } +func validateAndFillSourceTID(req *openrtb.BidRequest) error { + if req.Source == nil { + req.Source = &openrtb.Source{} + } + if req.Source.TID == "" { + if rawUUID, err := uuid.NewV4(); err == nil { + req.Source.TID = rawUUID.String() + } else { + return errors.New("req.Source.TID missing in the req and error creating a random UID") + } + } + return nil +} + func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases map[string]string) error { for bidderToAdjust, adjustmentFactor := range adjustmentFactors { if adjustmentFactor <= 0 { @@ -1265,8 +1288,8 @@ func writeError(errs []error, w http.ResponseWriter, labels *pbsmetrics.Labels) return rc } -// Returns the effective publisher ID -func effectivePubID(pub *openrtb.Publisher) string { +// Returns the account ID for the request +func getAccountID(pub *openrtb.Publisher) string { if pub != nil { if pub.Ext != nil { var pubExt openrtb_ext.ExtPublisher diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 957760c61c9..6baab8d500f 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1112,16 +1112,6 @@ func TestValidateImpExtDisabledBidder(t *testing.T) { assert.Equal(t, []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, errs) } -func TestEffectivePubID(t *testing.T) { - var pub openrtb.Publisher - assert.Equal(t, pbsmetrics.PublisherUnknown, effectivePubID(nil), "effectivePubID failed for nil Publisher.") - assert.Equal(t, pbsmetrics.PublisherUnknown, effectivePubID(&pub), "effectivePubID failed for empty Publisher.") - pub.ID = "123" - assert.Equal(t, "123", effectivePubID(&pub), "effectivePubID failed for standard Publisher.") - pub.Ext = json.RawMessage(`{"prebid": {"parentAccount": "abc"} }`) - assert.Equal(t, "abc", effectivePubID(&pub), "effectivePubID failed for parentAccount.") -} - func validRequest(t *testing.T, filename string) string { requestData, err := ioutil.ReadFile("sample-requests/valid-whole/supplementary/" + filename) if err != nil { @@ -1222,6 +1212,54 @@ func TestCCPAInvalid(t *testing.T) { assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") } +func TestValidateSourceTID(t *testing.T) { + cfg := &config.Configuration{ + AutoGenSourceTID: true, + } + + deps := &endpointDeps{ + &nobidExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + cfg, + pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + nil, + nil, + hardcodedResponseIPValidator{response: true}, + } + + ui := uint64(1) + req := openrtb.BidRequest{ + ID: "someID", + Imp: []openrtb.Imp{ + { + ID: "imp-ID", + Banner: &openrtb.Banner{ + W: &ui, + H: &ui, + }, + Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`), + }, + }, + Site: &openrtb.Site{ + ID: "myID", + }, + Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"invalid by length"}`), + }, + } + + deps.validateRequest(&req) + assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID") +} + func TestSChainInvalid(t *testing.T) { deps := &endpointDeps{ &nobidExchange{}, @@ -1269,6 +1307,63 @@ func TestSChainInvalid(t *testing.T) { assert.ElementsMatch(t, errL, []error{expectedError}) } +func TestGetAccountID(t *testing.T) { + testPubID := "test-pub" + testParentAccount := "test-account" + testPubExt := openrtb_ext.ExtPublisher{ + Prebid: &openrtb_ext.ExtPublisherPrebid{ + ParentAccount: &testParentAccount, + }, + } + testPubExtJSON, err := json.Marshal(testPubExt) + assert.NoError(t, err) + + testCases := []struct { + description string + pub *openrtb.Publisher + expectedAccID string + }{ + { + description: "Publisher.ID and Publisher.Ext.Prebid.ParentAccount both present", + pub: &openrtb.Publisher{ + ID: testPubID, + Ext: testPubExtJSON, + }, + expectedAccID: testParentAccount, + }, + { + description: "Only Publisher.Ext.Prebid.ParentAccount present", + pub: &openrtb.Publisher{ + ID: "", + Ext: testPubExtJSON, + }, + expectedAccID: testParentAccount, + }, + { + description: "Only Publisher.ID present", + pub: &openrtb.Publisher{ + ID: testPubID, + }, + expectedAccID: testPubID, + }, + { + description: "Neither Publisher.ID or Publisher.Ext.Prebid.ParentAccount present", + pub: &openrtb.Publisher{}, + expectedAccID: pbsmetrics.PublisherUnknown, + }, + { + description: "Publisher is nil", + pub: nil, + expectedAccID: pbsmetrics.PublisherUnknown, + }, + } + + for _, test := range testCases { + acc := getAccountID(test.pub) + assert.Equal(t, test.expectedAccID, acc, "getAccountID should return expected account for test case: %s", test.description) + } +} + func TestSanitizeRequest(t *testing.T) { testCases := []struct { description string @@ -1344,6 +1439,49 @@ func TestSanitizeRequest(t *testing.T) { } } +func TestValidateAndFillSourceTID(t *testing.T) { + testTID := "some-tid" + testCases := []struct { + description string + req *openrtb.BidRequest + expectRandTID bool + expectedTID string + }{ + { + description: "req.Source not present. Expecting a randomly generated TID value", + req: &openrtb.BidRequest{}, + expectRandTID: true, + }, + { + description: "req.Source.TID not present. Expecting a randomly generated TID value", + req: &openrtb.BidRequest{ + Source: &openrtb.Source{}, + }, + expectRandTID: true, + }, + { + description: "req.Source.TID present. Expecting no change", + req: &openrtb.BidRequest{ + Source: &openrtb.Source{ + TID: testTID, + }, + }, + expectRandTID: false, + expectedTID: testTID, + }, + } + + for _, test := range testCases { + _ = validateAndFillSourceTID(test.req) + if test.expectRandTID { + assert.NotEmpty(t, test.req.Source.TID, test.description) + assert.NotEqual(t, test.expectedTID, test.req.Source.TID, test.description) + } else { + assert.Equal(t, test.expectedTID, test.req.Source.TID, test.description) + } + } +} + // nobidExchange is a well-behaved exchange which always bids "no bid". type nobidExchange struct { gotRequest *openrtb.BidRequest diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index a6ca527874a..a8e4c28b167 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -242,7 +242,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re usersyncs := usersync.ParsePBSCookieFromRequest(r, &(deps.cfg.HostCookie)) if bidReq.App != nil { labels.Source = pbsmetrics.DemandApp - labels.PubID = effectivePubID(bidReq.App.Publisher) + labels.PubID = getAccountID(bidReq.App.Publisher) } else { // both bidReq.App == nil and bidReq.Site != nil are true labels.Source = pbsmetrics.DemandWeb if usersyncs.LiveSyncCount() == 0 { @@ -250,7 +250,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } else { labels.CookieFlag = pbsmetrics.CookieFlagYes } - labels.PubID = effectivePubID(bidReq.Site.Publisher) + labels.PubID = getAccountID(bidReq.Site.Publisher) } if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { diff --git a/exchange/utils.go b/exchange/utils.go index 2131aac5f41..2e9e4dc8f80 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -206,14 +206,15 @@ func prepareSource(req *openrtb.BidRequest, bidder string, sChainsByBidder map[s } // set source - var source openrtb.Source + if req.Source == nil { + req.Source = &openrtb.Source{} + } schain := openrtb_ext.ExtRequestPrebidSChain{ SChain: *selectedSChain, } sourceExt, err := json.Marshal(schain) if err == nil { - source.Ext = sourceExt - req.Source = &source + req.Source.Ext = sourceExt } } From 0c96441954e9443d11a591daa62da86cba14cde1 Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Thu, 3 Sep 2020 07:50:18 -0700 Subject: [PATCH 193/381] Add support for Account configuration (PBID-727, #1395) (#1426) --- analytics/core.go | 2 + config/accounts.go | 8 + config/config.go | 44 ++++- config/config_test.go | 17 ++ config/stored_requests.go | 36 +++- endpoints/openrtb2/amp_auction.go | 34 ++-- endpoints/openrtb2/amp_auction_test.go | 14 +- endpoints/openrtb2/auction.go | 74 ++++++-- endpoints/openrtb2/auction_benchmark_test.go | 1 + endpoints/openrtb2/auction_test.go | 172 +++++++++++++++--- .../account-required/valid-acct.json | 67 +++++++ endpoints/openrtb2/video_auction.go | 14 +- endpoints/openrtb2/video_auction_test.go | 7 +- exchange/exchange.go | 8 +- exchange/exchange_test.go | 8 +- exchange/targeting_test.go | 2 +- router/router.go | 8 +- .../backends/db_fetcher/fetcher.go | 4 + .../backends/empty_fetcher/fetcher.go | 5 + .../backends/file_fetcher/fetcher.go | 15 ++ .../backends/file_fetcher/fetcher_test.go | 14 ++ .../file_fetcher/test/accounts/valid.json | 4 + .../backends/http_fetcher/fetcher.go | 4 + stored_requests/config/config.go | 5 +- stored_requests/data/by_id/accounts/test.json | 14 ++ stored_requests/fetcher.go | 10 + stored_requests/fetcher_test.go | 5 + stored_requests/multifetcher.go | 15 ++ stored_requests/multifetcher_test.go | 51 ++++++ 29 files changed, 576 insertions(+), 86 deletions(-) create mode 100644 config/accounts.go create mode 100644 endpoints/openrtb2/sample-requests/account-required/valid-acct.json create mode 100644 stored_requests/backends/file_fetcher/test/accounts/valid.json create mode 100644 stored_requests/data/by_id/accounts/test.json diff --git a/analytics/core.go b/analytics/core.go index 6fd5139fd3d..737d133487e 100644 --- a/analytics/core.go +++ b/analytics/core.go @@ -2,6 +2,7 @@ package analytics import ( "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/usersync" ) @@ -28,6 +29,7 @@ type AuctionObject struct { Errors []error Request *openrtb.BidRequest Response *openrtb.BidResponse + Account *config.Account } //Loggable object of a transaction at /openrtb2/amp endpoint diff --git a/config/accounts.go b/config/accounts.go new file mode 100644 index 00000000000..11c2e10eb1b --- /dev/null +++ b/config/accounts.go @@ -0,0 +1,8 @@ +package config + +// Account represents a publisher account configuration +type Account struct { + ID string `mapstructure:"id" json:"id"` + Disabled bool `mapstructure:"disabled" json:"disabled"` + CacheTTL DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"` +} diff --git a/config/config.go b/config/config.go index 23cc35719db..59ba55ebe26 100755 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "bytes" + "encoding/json" "errors" "fmt" "net/url" @@ -40,6 +41,7 @@ type Configuration struct { StoredRequests StoredRequests `mapstructure:"stored_requests"` StoredRequestsAMP StoredRequests `mapstructure:"stored_amp_req"` CategoryMapping StoredRequests `mapstructure:"category_mapping"` + Accounts StoredRequests `mapstructure:"accounts"` // Note that StoredVideo refers to stored video requests, and has nothing to do with caching video creatives. StoredVideo StoredRequests `mapstructure:"stored_video_req"` @@ -65,6 +67,11 @@ type Configuration struct { BlacklistedAcctMap map[string]bool // Is publisher/account ID required to be submitted in the OpenRTB2 request AccountRequired bool `mapstructure:"account_required"` + // AccountDefaults defines default settings for valid accounts that are partially defined + // and provides a way to set global settings that can be overridden at account level. + AccountDefaults Account `mapstructure:"account_defaults"` + // accountDefaultsJSON is the internal serialized form of AccountDefaults used for json merge + accountDefaultsJSON json.RawMessage // Local private file containing SSL certificates PemCertsFile string `mapstructure:"certificates_file"` // Custom headers to handle request timeouts from queueing infrastructure @@ -106,10 +113,11 @@ func (c configErrors) Error() string { func (cfg *Configuration) validate() configErrors { var errs configErrors errs = cfg.AuctionTimeouts.validate(errs) - errs = cfg.StoredRequests.validate("stored_req", errs) - errs = cfg.StoredRequestsAMP.validate("stored_amp_req", errs) - errs = cfg.CategoryMapping.validate("categories", errs) - errs = cfg.StoredVideo.validate("stored_video_req", errs) + errs = cfg.StoredRequests.validate(errs) + errs = cfg.StoredRequestsAMP.validate(errs) + errs = cfg.Accounts.validate(errs) + errs = cfg.CategoryMapping.validate(errs) + errs = cfg.StoredVideo.validate(errs) errs = cfg.Metrics.validate(errs) if cfg.MaxRequestSize < 0 { errs = append(errs, fmt.Errorf("cfg.max_request_size must be >= 0. Got %d", cfg.MaxRequestSize)) @@ -119,6 +127,9 @@ func (cfg *Configuration) validate() configErrors { errs = validateAdapters(cfg.Adapters, errs) errs = cfg.Debug.validate(errs) errs = cfg.ExtCacheURL.validate(errs) + if cfg.AccountDefaults.Disabled { + glog.Warning(`With account_defaults.disabled=true, host-defined accounts must exist and have "disabled":false. All other requests will be rejected.`) + } return errs } @@ -589,6 +600,12 @@ func New(v *viper.Viper) (*Configuration, error) { return nil, err } + // Update account defaults and generate base json for patch + c.AccountDefaults.CacheTTL = c.CacheURL.DefaultTTLs // comment this out to set explicitly in config + if err := c.MarshalAccountDefaults(); err != nil { + return nil, err + } + // To look for a request's publisher_id in the NonStandardPublishers list in // O(1) time, we fill this hash table located in the NonStandardPublisherMap field of GDPR c.GDPR.NonStandardPublisherMap = make(map[string]int) @@ -622,6 +639,20 @@ func New(v *viper.Viper) (*Configuration, error) { return &c, nil } +// MarshalAccountDefaults compiles AccountDefaults into the JSON format used for merge patch +func (cfg *Configuration) MarshalAccountDefaults() error { + var err error + if cfg.accountDefaultsJSON, err = json.Marshal(cfg.AccountDefaults); err != nil { + glog.Warningf("converting %+v to json: %v", cfg.AccountDefaults, err) + } + return err +} + +// AccountDefaultsJSON returns the precompiled JSON form of account_defaults +func (cfg *Configuration) AccountDefaultsJSON() json.RawMessage { + return cfg.accountDefaultsJSON +} + //Allows for protocol relative URL if scheme is empty func (cfg *Cache) GetBaseURL() string { cfg.Scheme = strings.ToLower(cfg.Scheme) @@ -843,6 +874,10 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("stored_video_req.http_events.refresh_rate_seconds", 0) v.SetDefault("stored_video_req.http_events.timeout_ms", 0) + v.SetDefault("accounts.filesystem.enabled", false) + v.SetDefault("accounts.filesystem.directorypath", "./stored_requests/data/by_id") + v.SetDefault("accounts.in_memory_cache.type", "none") + for _, bidder := range openrtb_ext.BidderMap { setBidderDefaults(v, strings.ToLower(string(bidder))) } @@ -976,6 +1011,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("blacklisted_apps", []string{""}) v.SetDefault("blacklisted_accts", []string{""}) v.SetDefault("account_required", false) + v.SetDefault("account_defaults.disabled", false) v.SetDefault("certificates_file", "") v.SetDefault("auto_gen_source_tid", true) diff --git a/config/config_test.go b/config/config_test.go index 23764d216ef..d2e80c7e14c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "bytes" + "errors" "net" "os" "strings" @@ -466,6 +467,10 @@ func TestValidConfig(t *testing.T) { CategoryMapping: StoredRequests{ Files: FileFetcherConfig{Enabled: true}, }, + Accounts: StoredRequests{ + Files: FileFetcherConfig{Enabled: true}, + InMemoryCache: InMemoryCache{Type: "none"}, + }, } resolvedStoredRequestsConfig(&cfg) @@ -640,6 +645,18 @@ func TestValidateDebug(t *testing.T) { 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 TestValidateAccountsConfigRestrictions(t *testing.T) { + cfg := newDefaultConfig(t) + cfg.Accounts.Files.Enabled = true + cfg.Accounts.HTTP.Endpoint = "http://localhost" + cfg.Accounts.Postgres.ConnectionInfo.Database = "accounts" + + errs := cfg.validate() + assert.Len(t, errs, 2) + assert.Contains(t, errs, errors.New("accounts.http: retrieving accounts via http not available, use accounts.files")) + assert.Contains(t, errs, errors.New("accounts.postgres: retrieving accounts via postgres not available, use accounts.files")) +} + func newDefaultConfig(t *testing.T) *Configuration { v := viper.New() SetupViper(v, "") diff --git a/config/stored_requests.go b/config/stored_requests.go index b63073fede7..61db7eb03d0 100644 --- a/config/stored_requests.go +++ b/config/stored_requests.go @@ -18,8 +18,20 @@ const ( CategoryDataType DataType = "Category" VideoDataType DataType = "Video" AMPRequestDataType DataType = "AMP Request" + AccountDataType DataType = "Account" ) +// Section returns the config section this type is defined in +func (sr *StoredRequests) Section() string { + return map[DataType]string{ + RequestDataType: "stored_requests", + CategoryDataType: "categories", + VideoDataType: "stored_video_req", + AMPRequestDataType: "stored_amp_req", + AccountDataType: "accounts", + }[sr.dataType] +} + func (sr *StoredRequests) DataType() DataType { return sr.dataType } @@ -109,34 +121,42 @@ func resolvedStoredRequestsConfig(cfg *Configuration) { cfg.StoredRequestsAMP.dataType = AMPRequestDataType cfg.StoredVideo.dataType = VideoDataType cfg.CategoryMapping.dataType = CategoryDataType + cfg.Accounts.dataType = AccountDataType return } -func (cfg *StoredRequests) validate(section string, errs configErrors) configErrors { - errs = cfg.Postgres.validate(section, errs) +func (cfg *StoredRequests) validate(errs configErrors) configErrors { + if cfg.DataType() == AccountDataType && cfg.HTTP.Endpoint != "" { + errs = append(errs, fmt.Errorf("%s.http: retrieving accounts via http not available, use accounts.files", cfg.Section())) + } + if cfg.DataType() == AccountDataType && cfg.Postgres.ConnectionInfo.Database != "" { + errs = append(errs, fmt.Errorf("%s.postgres: retrieving accounts via postgres not available, use accounts.files", cfg.Section())) + } else { + errs = cfg.Postgres.validate(cfg.Section(), errs) + } // Categories do not use cache so none of the following checks apply - if cfg.dataType == CategoryDataType { + if cfg.DataType() == CategoryDataType { return errs } if cfg.InMemoryCache.Type == "none" { if cfg.CacheEvents.Enabled { - errs = append(errs, fmt.Errorf("%s: cache_events must be disabled if in_memory_cache=none", section)) + errs = append(errs, fmt.Errorf("%s: cache_events must be disabled if in_memory_cache=none", cfg.Section())) } if cfg.HTTPEvents.RefreshRate != 0 { - errs = append(errs, fmt.Errorf("%s: http_events.refresh_rate_seconds must be 0 if in_memory_cache=none", section)) + errs = append(errs, fmt.Errorf("%s: http_events.refresh_rate_seconds must be 0 if in_memory_cache=none", cfg.Section())) } if cfg.Postgres.PollUpdates.Query != "" { - errs = append(errs, fmt.Errorf("%s: postgres.poll_for_updates.query must be empty if in_memory_cache=none", section)) + errs = append(errs, fmt.Errorf("%s: postgres.poll_for_updates.query must be empty if in_memory_cache=none", cfg.Section())) } if cfg.Postgres.CacheInitialization.Query != "" { - errs = append(errs, fmt.Errorf("%s: postgres.initialize_caches.query must be empty if in_memory_cache=none", section)) + errs = append(errs, fmt.Errorf("%s: postgres.initialize_caches.query must be empty if in_memory_cache=none", cfg.Section())) } } - errs = cfg.InMemoryCache.validate(section, errs) + errs = cfg.InMemoryCache.validate(cfg.Section(), errs) return errs } diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index f5b72e029e0..54f4706902d 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -44,6 +44,7 @@ func NewAmpEndpoint( ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, + accounts stored_requests.AccountFetcher, categories stored_requests.CategoryFetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, @@ -53,7 +54,7 @@ func NewAmpEndpoint( bidderMap map[string]openrtb_ext.BidderName, ) (httprouter.Handle, error) { - if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { + if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewAmpEndpoint requires non-nil arguments.") } @@ -69,6 +70,7 @@ func NewAmpEndpoint( validator, requestsById, empty_fetcher.EmptyFetcher{}, + accounts, categories, cfg, met, @@ -155,26 +157,30 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h labels.CookieFlag = pbsmetrics.CookieFlagYes } labels.PubID = getAccountID(req.Site.Publisher) - - // Blacklist account now that we have resolved the value - if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL = append(errL, acctIdErr) - errCode := errortypes.ReadCode(acctIdErr) - if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.BlacklistedAcctErrorCode { - w.WriteHeader(http.StatusServiceUnavailable) - labels.RequestStatus = pbsmetrics.RequestStatusBlacklisted - } else { - w.WriteHeader(http.StatusBadRequest) - labels.RequestStatus = pbsmetrics.RequestStatusBadInput + // Look up account now that we have resolved the pubID value + account, acctIDErrs := deps.getAccount(ctx, labels.PubID) + if len(acctIDErrs) > 0 { + errL = append(errL, acctIDErrs...) + httpStatus := http.StatusBadRequest + metricsStatus := pbsmetrics.RequestStatusBadInput + for _, er := range errL { + errCode := errortypes.ReadCode(er) + if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.BlacklistedAcctErrorCode { + httpStatus = http.StatusServiceUnavailable + metricsStatus = pbsmetrics.RequestStatusBlacklisted + break + } } + w.WriteHeader(httpStatus) + labels.RequestStatus = metricsStatus for _, err := range errortypes.FatalOnly(errL) { w.Write([]byte(fmt.Sprintf("Invalid request format: %s\n", err.Error()))) } - ao.Errors = append(ao.Errors, acctIdErr) + ao.Errors = append(ao.Errors, acctIDErrs...) return } - response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories, nil) + response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, account, &deps.categories, nil) ao.AuctionResponse = response if err != nil { diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index be6735c4c39..57e0a7b447d 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -47,6 +47,7 @@ func TestGoodAmpRequests(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{goodRequests}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -100,6 +101,7 @@ func TestAMPPageInfo(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -205,6 +207,7 @@ func TestGDPRConsent(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -358,6 +361,7 @@ func TestCCPAConsent(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -417,6 +421,7 @@ func TestNoConsent(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -463,6 +468,7 @@ func TestInvalidConsent(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -547,6 +553,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -599,6 +606,7 @@ func TestAMPSiteExt(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{stored}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -639,6 +647,7 @@ func TestAmpBadRequests(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{badRequests}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -670,6 +679,7 @@ func TestAmpDebug(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -743,6 +753,7 @@ func TestQueryParamOverrides(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -895,6 +906,7 @@ func (s formatOverrideSpec) execute(t *testing.T) { newParamsValidator(t), &mockAmpStoredReqFetcher{requests}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -952,7 +964,7 @@ var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBi }, } -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) { +func (m *mockAmpExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { m.lastRequest = bidRequest response := &openrtb.BidResponse{ diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index bad2f8aae5b..bc0cd90073f 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -46,9 +46,9 @@ var ( dntEnabled int8 = 1 ) -func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById 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 NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, accounts stored_requests.AccountFetcher, 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) { - if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { + if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewEndpoint requires non-nil arguments.") } @@ -64,6 +64,7 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato validator, requestsById, empty_fetcher.EmptyFetcher{}, + accounts, categories, cfg, met, @@ -82,6 +83,7 @@ type endpointDeps struct { paramsValidator openrtb_ext.BidderParamValidator storedReqFetcher stored_requests.Fetcher videoFetcher stored_requests.Fetcher + accounts stored_requests.AccountFetcher categories stored_requests.CategoryFetcher cfg *config.Configuration metricsEngine pbsmetrics.MetricsEngine @@ -153,15 +155,18 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http labels.PubID = getAccountID(req.Site.Publisher) } - if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL = append(errL, acctIdErr) + // Look up account now that we have resolved the pubID value + account, acctIDErrs := deps.getAccount(ctx, labels.PubID) + if len(acctIDErrs) > 0 { + errL = append(errL, acctIDErrs...) writeError(errL, w, &labels) return } - response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories, nil) + response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, account, &deps.categories, nil) ao.Request = req ao.Response = response + ao.Account = account if err != nil { labels.RequestStatus = pbsmetrics.RequestStatusErr w.WriteHeader(http.StatusInternalServerError) @@ -1305,14 +1310,55 @@ func getAccountID(pub *openrtb.Publisher) string { return pbsmetrics.PublisherUnknown } -func validateAccount(cfg *config.Configuration, pubID string) error { - var err error = nil - if cfg.AccountRequired && pubID == pbsmetrics.PublisherUnknown { - // If specified in the configuration, discard requests that don't come with an account ID. - err = error(&errortypes.AcctRequired{Message: fmt.Sprintf("Prebid-server has been configured to discard requests that don't come with an Account ID. Please reach out to the prebid server host.")}) - } else if _, found := cfg.BlacklistedAcctMap[pubID]; found { - // Blacklist account now that we have resolved the value - err = error(&errortypes.BlacklistedAcct{Message: fmt.Sprintf("Prebid-server has blacklisted Account ID: %s, please reach out to the prebid server host.", pubID)}) +func (deps *endpointDeps) getAccount(ctx context.Context, pubID string) (account *config.Account, errs []error) { + // Check BlacklistedAcctMap until we have deprecated it + if _, found := deps.cfg.BlacklistedAcctMap[pubID]; found { + return nil, []error{&errortypes.BlacklistedAcct{ + Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", pubID), + }} + } + if deps.cfg.AccountRequired && pubID == pbsmetrics.PublisherUnknown { + return nil, []error{&errortypes.AcctRequired{ + Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."), + }} + } + if accountJSON, accErrs := deps.accounts.FetchAccount(ctx, pubID); len(accErrs) > 0 || accountJSON == nil { + // pubID does not reference a valid account + if len(accErrs) > 0 { + errs = append(errs, errs...) + } + if deps.cfg.AccountRequired && deps.cfg.AccountDefaults.Disabled { + errs = append(errs, &errortypes.AcctRequired{ + Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."), + }) + return nil, errs + } + // Make a copy of AccountDefaults instead of taking a reference, + // to preserve original pubID in case is needed to check NonStandardPublisherMap + pubAccount := deps.cfg.AccountDefaults + pubAccount.ID = pubID + account = &pubAccount + } else { + // pubID resolved to a valid account, merge with AccountDefaults for a complete config + account = &config.Account{} + completeJSON, err := jsonpatch.MergePatch(deps.cfg.AccountDefaultsJSON(), accountJSON) + if err == nil { + err = json.Unmarshal(completeJSON, account) + } + if err != nil { + errs = append(errs, err) + return nil, errs + } + // Fill in ID if needed, so it can be left out of account definition + if len(account.ID) == 0 { + account.ID = pubID + } } - return err + if account.Disabled { + errs = append(errs, &errortypes.BlacklistedAcct{ + Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", pubID), + }) + return nil, errs + } + return } diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go index fba0daecea8..09af23af103 100644 --- a/endpoints/openrtb2/auction_benchmark_test.go +++ b/endpoints/openrtb2/auction_benchmark_test.go @@ -83,6 +83,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) { paramValidator, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 6baab8d500f..72da2a36953 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -38,16 +38,17 @@ const maxSize = 1024 * 256 // Struct of data for the general purpose auction tester type getResponseFromDirectory struct { - dir string - file string - payloadGetter func(*testing.T, []byte) []byte - messageGetter func(*testing.T, []byte) []byte - expectedCode int - aliased bool - disabledBidders []string - adaptersConfig map[string]config.Adapter - accountReq bool - description string + dir string + file string + payloadGetter func(*testing.T, []byte) []byte + messageGetter func(*testing.T, []byte) []byte + expectedCode int + aliased bool + disabledBidders []string + adaptersConfig map[string]config.Adapter + accountReq bool + accountDefaultDisabled bool + description string } // TestExplicitUserId makes sure that the cookie's ID doesn't override an explicit value sent in the request. @@ -103,7 +104,7 @@ func TestExplicitUserId(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{}) - endpoint, _ := NewEndpoint(ex, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + endpoint, _ := NewEndpoint(ex, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) endpoint(httptest.NewRecorder(), request, nil) @@ -255,7 +256,7 @@ func TestRejectAccountRequired(t *testing.T) { accountReq: true, }, { - // Account is required, was provided and is not in the blacklisted accounts map + // Account is required, was provided, not blacklisted, is not defined by host dir: "sample-requests/account-required", file: "with-acct.json", payloadGetter: getRequestPayload, @@ -264,6 +265,28 @@ func TestRejectAccountRequired(t *testing.T) { aliased: true, accountReq: true, }, + { + // Account is required, was provided, not blacklisted, is not defined by host + // but strict validation is in force because default account settings are disabled. + dir: "sample-requests/account-required", + file: "with-acct.json", + payloadGetter: getRequestPayload, + messageGetter: nilReturner, + expectedCode: http.StatusBadRequest, + aliased: true, + accountReq: true, + accountDefaultDisabled: true, + }, + { + // Account is required, was provided, not blacklisted and is a valid account + dir: "sample-requests/account-required", + file: "valid-acct.json", + payloadGetter: getRequestPayload, + messageGetter: nilReturner, + expectedCode: http.StatusOK, + aliased: true, + accountReq: true, + }, { // Account is required, was provided in request and is found in the blacklisted accounts map dir: "sample-requests/blacklisted", @@ -346,12 +369,23 @@ func (gr *getResponseFromDirectory) doRequest(t *testing.T, requestData []byte) // 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{}) + cfg := config.Configuration{ + MaxRequestSize: maxSize, + BlacklistedApps: []string{"spam_app"}, + BlacklistedAppMap: map[string]bool{"spam_app": true}, + BlacklistedAccts: []string{"bad_acct"}, + BlacklistedAcctMap: map[string]bool{"bad_acct": true}, + AccountRequired: gr.accountReq, + AccountDefaults: config.Account{Disabled: gr.accountDefaultDisabled}, + } + assert.NoError(t, cfg.MarshalAccountDefaults()) endpoint, _ := NewEndpoint( &nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, + &mockAccountFetcher{}, empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize, BlacklistedApps: []string{"spam_app"}, BlacklistedAppMap: map[string]bool{"spam_app": true}, BlacklistedAccts: []string{"bad_acct"}, BlacklistedAcctMap: map[string]bool{"bad_acct": true}, AccountRequired: gr.accountReq}, + &cfg, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), disabledBidders, @@ -390,7 +424,7 @@ func doBadAliasRequest(t *testing.T, filename string, expectMsg string) { // 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{}) - endpoint, _ := NewEndpoint(&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), disabledBidders, aliasJSON, bidderMap) + endpoint, _ := NewEndpoint(&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), disabledBidders, aliasJSON, bidderMap) request := httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(requestData)) recorder := httptest.NewRecorder() @@ -454,7 +488,7 @@ func TestNilExchange(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{}) - _, err := NewEndpoint(nil, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + _, err := NewEndpoint(nil, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil Exchange.") } @@ -465,7 +499,7 @@ func TestNilValidator(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{}) - _, err := NewEndpoint(&nobidExchange{}, nil, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + _, err := NewEndpoint(&nobidExchange{}, nil, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil BidderParamValidator.") } @@ -476,7 +510,7 @@ func TestExchangeError(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{}) - endpoint, _ := NewEndpoint(&brokenExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + endpoint, _ := NewEndpoint(&brokenExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -588,7 +622,7 @@ func TestImplicitIPsEndToEnd(t *testing.T) { IPv6PrivateNetworksParsed: test.privateNetworksIPv6, }, } - endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) httpReq.Header.Set("X-Forwarded-For", test.xForwardedForHeader) @@ -773,7 +807,7 @@ func TestImplicitDNTEndToEnd(t *testing.T) { metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) for _, test := range testCases { exchange := &nobidExchange{} - endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) httpReq.Header.Set("DNT", test.dntHeader) @@ -846,6 +880,7 @@ func TestStoredRequests(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -885,6 +920,7 @@ func TestOversizedRequest(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: int64(len(reqBody) - 1)}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -920,6 +956,7 @@ func TestRequestSizeEdgeCase(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: int64(len(reqBody))}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -953,6 +990,7 @@ func TestNoEncoding(t *testing.T) { newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1028,6 +1066,7 @@ func TestContentType(t *testing.T) { newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1058,6 +1097,7 @@ func TestDisabledBidder(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{ MaxRequestSize: int64(len(reqBody)), }, @@ -1096,6 +1136,7 @@ func TestValidateImpExtDisabledBidder(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: int64(8096)}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1127,6 +1168,7 @@ func TestCurrencyTrunc(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1171,6 +1213,7 @@ func TestCCPAInvalid(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1223,6 +1266,7 @@ func TestValidateSourceTID(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1267,6 +1311,7 @@ func TestSChainInvalid(t *testing.T) { &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1487,7 +1532,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, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { +func (e *nobidExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { e.gotRequest = bidRequest return &openrtb.BidResponse{ ID: bidRequest.ID, @@ -1498,7 +1543,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, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { +func (e *brokenExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { return nil, errors.New("Critical, unrecoverable error.") } @@ -1854,11 +1899,27 @@ func (cf mockStoredReqFetcher) FetchRequests(ctx context.Context, requestIDs []s return testStoredRequestData, testStoredImpData, nil } +var mockAccountData = map[string]json.RawMessage{ + "valid_acct": json.RawMessage(`{"disabled":false}`), + "disabled_acct": json.RawMessage(`{"disabled":true}`), +} + +type mockAccountFetcher struct { +} + +func (af mockAccountFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + if account, ok := mockAccountData[accountID]; ok { + return account, nil + } else { + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} + } +} + 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, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { +func (m *mockExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { m.lastRequest = bidRequest return &openrtb.BidResponse{ SeatBid: []openrtb.SeatBid{{ @@ -1913,3 +1974,70 @@ type hardcodedResponseIPValidator struct { func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool { return v.response } + +func TestGetAccount(t *testing.T) { + unknown := pbsmetrics.PublisherUnknown + testCases := []struct { + accountID string + // account_required + required bool + // account_defaults.disabled + disabled bool + // expected error, or nil if account should be found + err error + }{ + // Blacklisted account is always rejected even in permissive setup + {accountID: "bad_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}}, + + // empty pubID + {accountID: unknown, required: false, disabled: false, err: nil}, + {accountID: unknown, required: true, disabled: false, err: &errortypes.AcctRequired{}}, + {accountID: unknown, required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, + {accountID: unknown, required: true, disabled: true, err: &errortypes.AcctRequired{}}, + + // pubID given but is not a valid host account (does not exist) + {accountID: "not_bad_acct", required: false, disabled: false, err: nil}, + {accountID: "not_bad_acct", required: true, disabled: false, err: nil}, + {accountID: "not_bad_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, + {accountID: "not_bad_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}}, + + // pubID given and matches a valid host account with Disabled: false + {accountID: "valid_acct", required: false, disabled: false, err: nil}, + {accountID: "valid_acct", required: true, disabled: false, err: nil}, + {accountID: "valid_acct", required: false, disabled: true, err: nil}, + {accountID: "valid_acct", required: true, disabled: true, err: nil}, + + // pubID given and matches a host account explicitly disabled (Disabled: true on account json) + {accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}}, + {accountID: "disabled_acct", required: true, disabled: false, err: &errortypes.BlacklistedAcct{}}, + {accountID: "disabled_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, + {accountID: "disabled_acct", required: true, disabled: true, err: &errortypes.BlacklistedAcct{}}, + } + + for _, test := range testCases { + description := fmt.Sprintf(`ID=%s/required=%t/disabled=%t`, test.accountID, test.required, test.disabled) + t.Run(description, func(t *testing.T) { + deps := &endpointDeps{ + cfg: &config.Configuration{ + BlacklistedAcctMap: map[string]bool{"bad_acct": true}, + AccountRequired: test.required, + AccountDefaults: config.Account{Disabled: test.disabled}, + }, + accounts: &mockAccountFetcher{}, + } + assert.NoError(t, deps.cfg.MarshalAccountDefaults()) + + account, errors := deps.getAccount(context.Background(), test.accountID) + + if test.err == nil { + assert.Empty(t, errors) + assert.Equal(t, test.accountID, account.ID, "account.ID must match requested ID") + assert.Equal(t, false, account.Disabled, "returned account must not be disabled") + } else { + assert.NotEmpty(t, errors, "expected errors but got success") + assert.Nil(t, account, "return account must be nil on error") + assert.IsType(t, test.err, errors[0], "error is of unexpected type") + } + }) + } +} diff --git a/endpoints/openrtb2/sample-requests/account-required/valid-acct.json b/endpoints/openrtb2/sample-requests/account-required/valid-acct.json new file mode 100644 index 00000000000..87d9d1eac37 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/account-required/valid-acct.json @@ -0,0 +1,67 @@ +{ + "description": "This request comes with a valid account id", + "message": "", + + "requestPayload": { + "id": "some-request-id", + "site": { + "publisher": { "id": "valid_acct"}, + "page": "test.somepage.com" + }, + "user": { }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + }, + "bidadjustmentfactors": { + "appnexus": 1.01, + "districtm": 0.98, + "rubicon": 0.99 + }, + "cache": { + "bids": {} + }, + "targeting": { + "includewinners": false, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.10 + } + ] + } + } + } + } + } + } + diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index a8e4c28b167..f5494751cc2 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -35,9 +35,9 @@ 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, cache prebid_cache_client.Client) (httprouter.Handle, error) { +func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, videoFetcher stored_requests.Fetcher, accounts stored_requests.AccountFetcher, 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 { + if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewVideoEndpoint requires non-nil arguments.") } @@ -55,6 +55,7 @@ func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamVal validator, requestsById, videoFetcher, + accounts, categories, cfg, met, @@ -253,13 +254,14 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re labels.PubID = getAccountID(bidReq.Site.Publisher) } - if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL := []error{err} - handleError(&labels, w, errL, &vo, &debugLog) + // Look up account now that we have resolved the pubID value + account, acctIDErrs := deps.getAccount(ctx, labels.PubID) + if len(acctIDErrs) > 0 { + handleError(&labels, w, acctIDErrs, &vo, &debugLog) return } //execute auction logic - response, err := deps.ex.HoldAuction(ctx, bidReq, usersyncs, labels, &deps.categories, &debugLog) + response, err := deps.ex.HoldAuction(ctx, bidReq, usersyncs, labels, account, &deps.categories, &debugLog) vo.Request = bidReq vo.Response = response if err != nil { diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 78715f5c87d..a4e9bfbb61e 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1168,6 +1168,7 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *p &mockVideoStoredReqFetcher{}, &mockVideoStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, mockModule, @@ -1210,6 +1211,7 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { &mockVideoStoredReqFetcher{}, &mockVideoStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1233,6 +1235,7 @@ func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps { &mockVideoStoredReqFetcher{}, &mockVideoStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), @@ -1275,7 +1278,7 @@ type mockExchangeVideo struct { cache *mockCacheClient } -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) { +func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { m.lastRequest = bidRequest if debugLog != nil && debugLog.Enabled { m.cache.called = true @@ -1311,7 +1314,7 @@ type mockExchangeVideoNoBids struct { cache *mockCacheClient } -func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { +func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { m.lastRequest = bidRequest return &openrtb.BidResponse{ SeatBid: []openrtb.SeatBid{{}}, diff --git a/exchange/exchange.go b/exchange/exchange.go index 59e876697cf..9d1cd20affa 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -37,7 +37,7 @@ const DebugContextKey = ContextKey("debugInfo") // 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, debugLog *DebugLog) (*openrtb.BidResponse, error) + HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) } // IdFetcher can find the user's ID for a specific Bidder. @@ -54,7 +54,6 @@ type exchange struct { gDPR gdpr.Permissions currencyConverter *currencies.RateConverter UsersyncIfAmbiguous bool - defaultTTLs config.DefaultTTLs privacyConfig config.Privacy eeaCountries map[string]struct{} } @@ -89,7 +88,6 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con e.gDPR = gDPR e.currencyConverter = currencyConverter e.UsersyncIfAmbiguous = cfg.GDPR.UsersyncIfAmbiguous - e.defaultTTLs = cfg.CacheURL.DefaultTTLs e.privacyConfig = config.Privacy{ CCPA: cfg.CCPA, GDPR: cfg.GDPR, @@ -98,7 +96,7 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con return e } -func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) { +func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) { requestExt, err := extractBidRequestExt(bidRequest) if err != nil { @@ -203,7 +201,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque } } - cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &e.defaultTTLs, bidCategory, debugLog) + cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &account.CacheTTL, bidCategory, debugLog) if len(cacheErrs) > 0 { errs = append(errs, cacheErrs...) } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index efabb845211..21003b669ed 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -248,7 +248,7 @@ func TestDebugBehaviour(t *testing.T) { } // Run test - outBidResponse, err := e.HoldAuction(context.Background(), bidRequest, &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) + outBidResponse, err := e.HoldAuction(context.Background(), bidRequest, &emptyUsersync{}, pbsmetrics.Labels{}, &config.Account{}, &categoriesFetcher, nil) // Assert no HoldAuction error assert.NoErrorf(t, err, "%s. ex.HoldAuction returned an error: %v \n", test.desc, err) @@ -752,7 +752,7 @@ func TestRaceIntegration(t *testing.T) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) ex := NewExchange(server.Client(), &wellBehavedCache{}, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter) - _, err := ex.HoldAuction(context.Background(), newRaceCheckingRequest(t), &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) + _, err := ex.HoldAuction(context.Background(), newRaceCheckingRequest(t), &emptyUsersync{}, pbsmetrics.Labels{}, &config.Account{}, &categoriesFetcher, nil) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) } @@ -939,7 +939,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, nil) + _, err := e.HoldAuction(context.Background(), request, &emptyUsersync{}, pbsmetrics.Labels{}, &config.Account{}, &categoriesFetcher, nil) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) } @@ -1055,7 +1055,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { *debugLog = *spec.DebugLog debugLog.Regexp = regexp.MustCompile(`[<>]`) } - bid, err := ex.HoldAuction(context.Background(), &spec.IncomingRequest.OrtbRequest, mockIdFetcher(spec.IncomingRequest.Usersyncs), pbsmetrics.Labels{}, &categoriesFetcher, debugLog) + bid, err := ex.HoldAuction(context.Background(), &spec.IncomingRequest.OrtbRequest, mockIdFetcher(spec.IncomingRequest.Usersyncs), pbsmetrics.Labels{}, &config.Account{}, &categoriesFetcher, debugLog) responseTimes := extractResponseTimes(t, filename, bid) for _, bidderName := range biddersInAuction { if _, ok := responseTimes[bidderName]; !ok { diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index e596e5aa215..aaa75411ee4 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -108,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, nil) + bidResp, err := ex.HoldAuction(context.Background(), req, &mockFetcher{}, pbsmetrics.Labels{}, &config.Account{}, &categoriesFetcher, nil) if err != nil { t.Fatalf("Unexpected errors running auction: %v", err) diff --git a/router/router.go b/router/router.go index 30936705a22..4826f0e5feb 100644 --- a/router/router.go +++ b/router/router.go @@ -211,7 +211,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r // Metrics engine r.MetricsEngine = metricsConf.NewMetricsEngine(cfg, legacyBidderList) - db, shutdown, fetcher, ampFetcher, categoriesFetcher, videoFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, generalHttpClient, r.Router) + db, shutdown, fetcher, ampFetcher, accounts, categoriesFetcher, videoFetcher := storedRequestsConf.NewStoredRequests(cfg, r.MetricsEngine, generalHttpClient, r.Router) // todo(zachbadgett): better shutdown r.Shutdown = shutdown @@ -243,19 +243,19 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r cacheClient := pbc.NewClient(cacheHttpClient, &cfg.CacheURL, &cfg.ExtCacheURL, r.MetricsEngine) theExchange := exchange.NewExchange(generalHttpClient, cacheClient, cfg, r.MetricsEngine, bidderInfos, gdprPerms, rateConvertor) - openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, categoriesFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap) + openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, accounts, 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) + ampEndpoint, err := openrtb2.NewAmpEndpoint(theExchange, paramsValidator, ampFetcher, accounts, 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) + videoEndpoint, err := openrtb2.NewVideoEndpoint(theExchange, paramsValidator, fetcher, videoFetcher, accounts, categoriesFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap, cacheClient) if err != nil { glog.Fatalf("Failed to create the video endpoint handler. %v", err) } diff --git a/stored_requests/backends/db_fetcher/fetcher.go b/stored_requests/backends/db_fetcher/fetcher.go index 223067c917e..d8cf132d25b 100644 --- a/stored_requests/backends/db_fetcher/fetcher.go +++ b/stored_requests/backends/db_fetcher/fetcher.go @@ -93,6 +93,10 @@ func (fetcher *dbFetcher) FetchRequests(ctx context.Context, requestIDs []string return storedRequestData, storedImpData, errs } +func (fetcher *dbFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} +} + func (fetcher *dbFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } diff --git a/stored_requests/backends/empty_fetcher/fetcher.go b/stored_requests/backends/empty_fetcher/fetcher.go index 25e8ead434b..ee6b98b3b2e 100644 --- a/stored_requests/backends/empty_fetcher/fetcher.go +++ b/stored_requests/backends/empty_fetcher/fetcher.go @@ -3,6 +3,7 @@ package empty_fetcher import ( "context" "encoding/json" + "github.com/prebid/prebid-server/stored_requests" ) @@ -27,6 +28,10 @@ func (fetcher EmptyFetcher) FetchRequests(ctx context.Context, requestIDs []stri return } +func (fetcher EmptyFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} +} + func (fetcher EmptyFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } diff --git a/stored_requests/backends/file_fetcher/fetcher.go b/stored_requests/backends/file_fetcher/fetcher.go index 60853f65da7..2d3b00657b9 100644 --- a/stored_requests/backends/file_fetcher/fetcher.go +++ b/stored_requests/backends/file_fetcher/fetcher.go @@ -33,6 +33,21 @@ func (fetcher *eagerFetcher) FetchRequests(ctx context.Context, requestIDs []str return storedRequests, storedImpressions, errs } +// FetchAccount fetches the host account configuration for a publisher +func (fetcher *eagerFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + if len(accountID) == 0 { + return nil, []error{fmt.Errorf("Cannot look up an empty accountID")} + } + accountJSON, ok := fetcher.FileSystem.Directories["accounts"].Files[accountID] + if !ok { + return nil, []error{stored_requests.NotFoundError{ + ID: accountID, + DataType: "Account", + }} + } + return accountJSON, nil +} + func (fetcher *eagerFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { fileName := primaryAdServer diff --git a/stored_requests/backends/file_fetcher/fetcher_test.go b/stored_requests/backends/file_fetcher/fetcher_test.go index 2429a77cd25..a145a3b43a2 100644 --- a/stored_requests/backends/file_fetcher/fetcher_test.go +++ b/stored_requests/backends/file_fetcher/fetcher_test.go @@ -24,6 +24,20 @@ func TestFileFetcher(t *testing.T) { validateImp(t, storedImps) } +func TestAccountFetcher(t *testing.T) { + fetcher, err := NewFileFetcher("./test") + assert.NoError(t, err, "Failed to create test fetcher") + + account, errs := fetcher.FetchAccount(context.Background(), "valid") + assertErrorCount(t, 0, errs) + assert.JSONEq(t, `{"disabled":false, "id":"valid"}`, string(account)) + + account, errs = fetcher.FetchAccount(context.Background(), "nonexistent") + assertErrorCount(t, 1, errs) + assert.Error(t, errs[0]) + assert.Equal(t, stored_requests.NotFoundError{"nonexistent", "Account"}, errs[0]) +} + func TestInvalidDirectory(t *testing.T) { _, err := NewFileFetcher("./nonexistant-directory") if err == nil { diff --git a/stored_requests/backends/file_fetcher/test/accounts/valid.json b/stored_requests/backends/file_fetcher/test/accounts/valid.json new file mode 100644 index 00000000000..2c8bd12af3c --- /dev/null +++ b/stored_requests/backends/file_fetcher/test/accounts/valid.json @@ -0,0 +1,4 @@ +{ + "id": "valid", + "disabled": false +} diff --git a/stored_requests/backends/http_fetcher/fetcher.go b/stored_requests/backends/http_fetcher/fetcher.go index b7e42c9e6cf..d533d5315ab 100644 --- a/stored_requests/backends/http_fetcher/fetcher.go +++ b/stored_requests/backends/http_fetcher/fetcher.go @@ -81,6 +81,10 @@ func (fetcher *HttpFetcher) FetchRequests(ctx context.Context, requestIDs []stri return } +func (fetcher *HttpFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} +} + func (fetcher *HttpFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { if fetcher.Categories == nil { fetcher.Categories = make(map[string]map[string]stored_requests.Category) diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go index 8f06efcb32b..e81d9667a73 100644 --- a/stored_requests/config/config.go +++ b/stored_requests/config/config.go @@ -106,7 +106,7 @@ func CreateStoredRequests(cfg *config.StoredRequests, metricsEngine pbsmetrics.M // // As a side-effect, it will add some endpoints to the router if the config calls for it. // In the future we should look for ways to simplify this so that it's not doing two things. -func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router) (db *sql.DB, shutdown func(), fetcher stored_requests.Fetcher, ampFetcher stored_requests.Fetcher, categoriesFetcher stored_requests.CategoryFetcher, videoFetcher stored_requests.Fetcher) { +func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router) (db *sql.DB, shutdown func(), fetcher stored_requests.Fetcher, ampFetcher stored_requests.Fetcher, accountsFetcher stored_requests.AccountFetcher, categoriesFetcher stored_requests.CategoryFetcher, videoFetcher stored_requests.Fetcher) { // TODO: Switch this to be set in config defaults //if cfg.CategoryMapping.CacheEvents.Enabled && cfg.CategoryMapping.CacheEvents.Endpoint == "" { // cfg.CategoryMapping.CacheEvents.Endpoint = "/storedrequest/categorymapping" @@ -118,6 +118,7 @@ func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.Metri fetcher2, shutdown2 := CreateStoredRequests(&cfg.StoredRequestsAMP, metricsEngine, client, router, &dbc) fetcher3, shutdown3 := CreateStoredRequests(&cfg.CategoryMapping, metricsEngine, client, router, &dbc) fetcher4, shutdown4 := CreateStoredRequests(&cfg.StoredVideo, metricsEngine, client, router, &dbc) + fetcher5, shutdown5 := CreateStoredRequests(&cfg.Accounts, metricsEngine, client, router, &dbc) db = dbc.db @@ -125,12 +126,14 @@ func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.Metri ampFetcher = fetcher2.(stored_requests.Fetcher) categoriesFetcher = fetcher3.(stored_requests.CategoryFetcher) videoFetcher = fetcher4.(stored_requests.Fetcher) + accountsFetcher = fetcher5.(stored_requests.AccountFetcher) shutdown = func() { shutdown1() shutdown2() shutdown3() shutdown4() + shutdown5() } return diff --git a/stored_requests/data/by_id/accounts/test.json b/stored_requests/data/by_id/accounts/test.json new file mode 100644 index 00000000000..76bafff7f1c --- /dev/null +++ b/stored_requests/data/by_id/accounts/test.json @@ -0,0 +1,14 @@ +{ + "id": "test", + "name": "test account", + "disabled": true, + "cache_ttl": { + "banner": 600, + "video": 3600, + "native": 3600, + "audio": 3600 + }, + "events": { + "enabled": true + } +} diff --git a/stored_requests/fetcher.go b/stored_requests/fetcher.go index 23fdb6b4925..a31b9989bd0 100644 --- a/stored_requests/fetcher.go +++ b/stored_requests/fetcher.go @@ -25,6 +25,11 @@ type Fetcher interface { FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) } +type AccountFetcher interface { + // FetchAccount fetches the host account configuration for a publisher + FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) +} + type CategoryFetcher interface { // FetchCategories fetches the ad-server/publisher specific category for the given IAB category FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) @@ -33,6 +38,7 @@ type CategoryFetcher interface { // 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) + FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) } @@ -181,6 +187,10 @@ func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []strin return } +func (f *fetcherWithCache) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + return f.fetcher.FetchAccount(ctx, accountID) +} + func (f *fetcherWithCache) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } diff --git a/stored_requests/fetcher_test.go b/stored_requests/fetcher_test.go index c1040acdb90..1928d1165db 100644 --- a/stored_requests/fetcher_test.go +++ b/stored_requests/fetcher_test.go @@ -215,6 +215,11 @@ func (f *mockFetcher) FetchRequests(ctx context.Context, requestIDs []string, im return args.Get(0).(map[string]json.RawMessage), args.Get(1).(map[string]json.RawMessage), args.Get(2).([]error) } +func (a *mockFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + args := a.Called(ctx, accountID) + return args.Get(0).(json.RawMessage), args.Get(1).([]error) +} + func (f *mockFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } diff --git a/stored_requests/multifetcher.go b/stored_requests/multifetcher.go index 24cf848448c..2d08fd45337 100644 --- a/stored_requests/multifetcher.go +++ b/stored_requests/multifetcher.go @@ -36,6 +36,21 @@ func (mf MultiFetcher) FetchRequests(ctx context.Context, requestIDs []string, i return } +func (mf MultiFetcher) FetchAccount(ctx context.Context, accountID string) (account json.RawMessage, errs []error) { + for _, f := range mf { + if af, ok := f.(AccountFetcher); ok { + if account, accErrs := af.FetchAccount(ctx, accountID); len(accErrs) == 0 { + return account, nil + } else { + accErrs = dropMissingIDs(accErrs) + errs = append(errs, accErrs...) + } + } + } + errs = append(errs, NotFoundError{accountID, "Account"}) + return nil, errs +} + func (mf MultiFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { for _, f := range mf { if cf, ok := f.(CategoryFetcher); ok { diff --git a/stored_requests/multifetcher_test.go b/stored_requests/multifetcher_test.go index e703c2c9dcc..5035cfba82e 100644 --- a/stored_requests/multifetcher_test.go +++ b/stored_requests/multifetcher_test.go @@ -125,3 +125,54 @@ func TestOtherError(t *testing.T) { assert.JSONEq(t, `{"req_id": "def"}`, string(reqData["def"]), "MultiFetcher should return the right request data") assert.JSONEq(t, `{"imp_id": "imp-1"}`, string(impData["imp-1"]), "MultiFetcher should return the right imp data") } + +func TestMultiFetcherAccountFoundInFirstFetcher(t *testing.T) { + f1 := &mockFetcher{} + f2 := &mockFetcher{} + fetcher := &MultiFetcher{f1, f2} + ctx := context.Background() + + f1.On("FetchAccount", ctx, "ONE").Once().Return(json.RawMessage(`{"id": "ONE"}`), []error{}) + + account, errs := fetcher.FetchAccount(ctx, "ONE") + + f1.AssertExpectations(t) + f2.AssertNotCalled(t, "FetchAccount") + assert.Empty(t, errs) + assert.JSONEq(t, `{"id": "ONE"}`, string(account)) +} + +func TestMultiFetcherAccountFoundInSecondFetcher(t *testing.T) { + f1 := &mockFetcher{} + f2 := &mockFetcher{} + fetcher := &MultiFetcher{f1, f2} + ctx := context.Background() + + f1.On("FetchAccount", ctx, "TWO").Once().Return(json.RawMessage(``), []error{NotFoundError{"TWO", "Account"}}) + f2.On("FetchAccount", ctx, "TWO").Once().Return(json.RawMessage(`{"id": "TWO"}`), []error{}) + + account, errs := fetcher.FetchAccount(ctx, "TWO") + + f1.AssertExpectations(t) + f2.AssertExpectations(t) + assert.Empty(t, errs) + assert.JSONEq(t, `{"id": "TWO"}`, string(account)) +} + +func TestMultiFetcherAccountNotFound(t *testing.T) { + f1 := &mockFetcher{} + f2 := &mockFetcher{} + fetcher := &MultiFetcher{f1, f2} + ctx := context.Background() + + f1.On("FetchAccount", ctx, "MISSING").Once().Return(json.RawMessage(``), []error{NotFoundError{"TWO", "Account"}}) + f2.On("FetchAccount", ctx, "MISSING").Once().Return(json.RawMessage(``), []error{NotFoundError{"TWO", "Account"}}) + + account, errs := fetcher.FetchAccount(ctx, "MISSING") + + f1.AssertExpectations(t) + f2.AssertExpectations(t) + assert.Len(t, errs, 1) + assert.Nil(t, account) + assert.EqualError(t, errs[0], NotFoundError{"MISSING", "Account"}.Error()) +} From 44310b6f329813cc0f01747ea2e5c7db4f3975f9 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 8 Sep 2020 10:19:44 -0400 Subject: [PATCH 194/381] Minor changes to accounts test coverage (#1475) --- endpoints/openrtb2/auction_test.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 72da2a36953..53fea2e0500 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -279,13 +279,14 @@ func TestRejectAccountRequired(t *testing.T) { }, { // Account is required, was provided, not blacklisted and is a valid account - dir: "sample-requests/account-required", - file: "valid-acct.json", - payloadGetter: getRequestPayload, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - aliased: true, - accountReq: true, + dir: "sample-requests/account-required", + file: "valid-acct.json", + payloadGetter: getRequestPayload, + messageGetter: nilReturner, + expectedCode: http.StatusOK, + aliased: true, + accountReq: true, + accountDefaultDisabled: true, }, { // Account is required, was provided in request and is found in the blacklisted accounts map @@ -1996,10 +1997,10 @@ func TestGetAccount(t *testing.T) { {accountID: unknown, required: true, disabled: true, err: &errortypes.AcctRequired{}}, // pubID given but is not a valid host account (does not exist) - {accountID: "not_bad_acct", required: false, disabled: false, err: nil}, - {accountID: "not_bad_acct", required: true, disabled: false, err: nil}, - {accountID: "not_bad_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, - {accountID: "not_bad_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}}, + {accountID: "doesnt_exist_acct", required: false, disabled: false, err: nil}, + {accountID: "doesnt_exist_acct", required: true, disabled: false, err: nil}, + {accountID: "doesnt_exist_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, + {accountID: "doesnt_exist_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}}, // pubID given and matches a valid host account with Disabled: false {accountID: "valid_acct", required: false, disabled: false, err: nil}, From d75df46922283b7cbeef8c14774f5bc8163a4c63 Mon Sep 17 00:00:00 2001 From: smithaammassamveettil <39389834+smithaammassamveettil@users.noreply.github.com> Date: Tue, 8 Sep 2020 10:53:49 -0700 Subject: [PATCH 195/381] Brightroll adapter - adding config support (#1461) --- adapters/brightroll/brightroll.go | 81 +++++++++++++++---- adapters/brightroll/brightroll_test.go | 28 ++++++- .../exemplary/banner-native-audio.json | 23 ++++-- .../exemplary/banner-video-native.json | 28 +++++-- .../exemplary/banner-video.json | 24 ++++-- .../exemplary/simple-banner.json | 15 +++- .../exemplary/simple-video.json | 15 +++- .../exemplary/valid-extension.json | 15 +++- .../exemplary/video-and-audio.json | 19 +++-- .../brightrolltest/params/race/banner.json | 2 +- .../brightrolltest/params/race/video.json | 2 +- .../supplemental/invalid-imp.json | 2 +- .../supplemental/invalid-publisher.json | 34 ++++++++ exchange/adapter_map.go | 2 +- 14 files changed, 236 insertions(+), 54 deletions(-) create mode 100644 adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json diff --git a/adapters/brightroll/brightroll.go b/adapters/brightroll/brightroll.go index 0ae95dd303a..83b253a0996 100644 --- a/adapters/brightroll/brightroll.go +++ b/adapters/brightroll/brightroll.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "github.com/golang/glog" "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" @@ -13,7 +14,20 @@ import ( ) type BrightrollAdapter struct { - URI string + URI string + extraInfo ExtraInfo +} + +type ExtraInfo struct { + Accounts []Account `json:"accounts"` +} + +type Account struct { + ID string `json:"id"` + Badv []string `json:"badv"` + Bcat []string `json:"bcat"` + Battr []int8 `json:"battr"` + BidFloor float64 `json:"bidfloor"` } func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { @@ -55,6 +69,23 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo errors = append(errors, err) return nil, errors } + + var account *Account + for _, a := range a.extraInfo.Accounts { + if a.ID == brightrollExt.Publisher { + account = &a + break + } + } + + if account == nil { + err = &errortypes.BadInput{ + Message: "Invalid publisher", + } + errors = append(errors, err) + return nil, errors + } + validImpExists := false for i := 0; i < len(request.Imp); i++ { //Brightroll supports only banner and video impressions as of now @@ -65,9 +96,9 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo bannerCopy.W = &(firstFormat.W) bannerCopy.H = &(firstFormat.H) } - if brightrollExt.Publisher == "adthrive" { - bannerCopy.BAttr = getBlockedCreativetypesForAdThrive() + if len(account.Battr) > 0 { + bannerCopy.BAttr = getBlockedCreativetypes(account.Battr) } request.Imp[i].Banner = &bannerCopy validImpExists = true @@ -75,10 +106,15 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo validImpExists = true if brightrollExt.Publisher == "adthrive" { videoCopy := *request.Imp[i].Video - videoCopy.BAttr = getBlockedCreativetypesForAdThrive() + if len(account.Battr) > 0 { + videoCopy.BAttr = getBlockedCreativetypes(account.Battr) + } request.Imp[i].Video = &videoCopy } } + if validImpExists && request.Imp[i].BidFloor == 0 && account.BidFloor > 0 { + request.Imp[i].BidFloor = account.BidFloor + } } if !validImpExists { err := &errortypes.BadInput{ @@ -90,8 +126,12 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo request.AT = 1 //Defaulting to first price auction for all prebid requests - if brightrollExt.Publisher == "adthrive" { - request.BCat = getBlockedCategoriesForAdthrive() + if len(account.Bcat) > 0 { + request.BCat = account.Bcat + } + + if len(account.Badv) > 0 { + request.BAdv = account.Badv } reqJSON, err := json.Marshal(request) if err != nil { @@ -159,13 +199,12 @@ func (a *BrightrollAdapter) MakeBids(internalRequest *openrtb.BidRequest, extern return bidResponse, nil } -//customized request, need following blocked categories -func getBlockedCategoriesForAdthrive() []string { - return []string{"IAB8-5", "IAB8-18", "IAB15-1", "IAB7-30", "IAB14-1", "IAB22-1", "IAB3-7", "IAB7-3", "IAB14-3", "IAB11", "IAB11-1", "IAB11-2", "IAB11-3", "IAB11-4", "IAB11-5", "IAB23", "IAB23-1", "IAB23-2", "IAB23-3", "IAB23-4", "IAB23-5", "IAB23-6", "IAB23-7", "IAB23-8", "IAB23-9", "IAB23-10", "IAB7-39", "IAB9-30", "IAB7-44", "IAB25", "IAB25-1", "IAB25-2", "IAB25-3", "IAB25-4", "IAB25-5", "IAB25-6", "IAB25-7", "IAB26", "IAB26-1", "IAB26-2", "IAB26-3", "IAB26-4"} -} - -func getBlockedCreativetypesForAdThrive() []openrtb.CreativeAttribute { - return []openrtb.CreativeAttribute{openrtb.CreativeAttribute(1), openrtb.CreativeAttribute(2), openrtb.CreativeAttribute(3), openrtb.CreativeAttribute(6), openrtb.CreativeAttribute(9), openrtb.CreativeAttribute(10)} +func getBlockedCreativetypes(attr []int8) []openrtb.CreativeAttribute { + var creativeAttr []openrtb.CreativeAttribute + for i := 0; i < len(attr); i++ { + creativeAttr = append(creativeAttr, openrtb.CreativeAttribute(attr[i])) + } + return creativeAttr } //Adding header fields to request header @@ -189,8 +228,18 @@ func getMediaTypeForImp(impId string, imps []openrtb.Imp) openrtb_ext.BidType { return mediaType } -func NewBrightrollBidder(endpoint string) *BrightrollAdapter { - return &BrightrollAdapter{ - URI: endpoint, +func NewBrightrollBidder(endpoint string, extraAdapterInfo string) *BrightrollAdapter { + + var extraInfo ExtraInfo + + if len(extraAdapterInfo) == 0 { + extraAdapterInfo = "{\"accounts\":[]}" + } + err := json.Unmarshal([]byte(extraAdapterInfo), &extraInfo) + + if err != nil { + glog.Fatalf("Invalid Brightroll extra adapter info: " + err.Error()) + return nil } + return &BrightrollAdapter{URI: endpoint, extraInfo: extraInfo} } diff --git a/adapters/brightroll/brightroll_test.go b/adapters/brightroll/brightroll_test.go index 0a6c2c44567..5f1c64fd16f 100644 --- a/adapters/brightroll/brightroll_test.go +++ b/adapters/brightroll/brightroll_test.go @@ -1,11 +1,37 @@ package brightroll import ( + "github.com/stretchr/testify/assert" "testing" "github.com/prebid/prebid-server/adapters/adapterstest" ) +func TestEmptyConfig(t *testing.T) { + output := NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs", "") + ex := ExtraInfo{ + Accounts: []Account{}, + } + expected := &BrightrollAdapter{ + URI: "http://test-bid.ybp.yahoo.com/bid/appnexuspbs", + extraInfo: ex, + } + assert.Equal(t, expected, output, "") +} + +func TestNonEmptyConfig(t *testing.T) { + output := NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs", "{\"accounts\": [{\"id\": \"test\",\"bidfloor\":0.1}]}") + ex := ExtraInfo{ + Accounts: []Account{{ID: "test", BidFloor: 0.1}}, + } + + expected := &BrightrollAdapter{ + URI: "http://test-bid.ybp.yahoo.com/bid/appnexuspbs", + extraInfo: ex, + } + assert.Equal(t, expected, output, "") +} + func TestJsonSamples(t *testing.T) { - adapterstest.RunJSONBidderTest(t, "brightrolltest", NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs")) + adapterstest.RunJSONBidderTest(t, "brightrolltest", NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs", "{\"accounts\": [{\"id\": \"adthrive\",\"badv\": [], \"bcat\": [\"IAB8-5\",\"IAB8-18\"],\"battr\": [1,2,3], \"bidfloor\":0.0}]}")) } diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json b/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json index e8ef5b688f6..e67a1485e54 100644 --- a/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json +++ b/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json @@ -18,7 +18,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -30,7 +30,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -43,7 +43,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } @@ -52,14 +52,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom", + "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", "body": { "id": "test-request-id", "at":1, + "bcat": [ + "IAB8-5", + "IAB8-18" + ], "imp": [ { "id": "test-imp-id", "banner": { + "battr": [ + 1, + 2, + 3 + ], "format": [ { "w": 300, @@ -75,7 +84,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -87,7 +96,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -100,7 +109,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json b/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json index 4df7ab5df10..0fffbd2fac5 100644 --- a/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json +++ b/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json @@ -18,7 +18,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -30,7 +30,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -44,7 +44,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } @@ -53,14 +53,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom", + "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", "body": { "id": "test-request-id", "at":1, + "bcat": [ + "IAB8-5", + "IAB8-18" + ], "imp": [ { "id": "test-imp-id", "banner": { + "battr": [ + 1, + 2, + 3 + ], "format": [ { "w": 300, @@ -76,7 +85,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -88,13 +97,18 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, { "id": "test-imp-video-id", "video": { + "battr": [ + 1, + 2, + 3 + ], "mimes": ["video/mp4"], "protocols": [2, 5], "w": 1024, @@ -102,7 +116,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-video.json b/adapters/brightroll/brightrolltest/exemplary/banner-video.json index 50053102d08..f58dd9b1395 100644 --- a/adapters/brightroll/brightrolltest/exemplary/banner-video.json +++ b/adapters/brightroll/brightrolltest/exemplary/banner-video.json @@ -18,7 +18,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -32,7 +32,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } @@ -41,14 +41,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom", + "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", "body": { "id": "test-request-id", "at":1, + "bcat": [ + "IAB8-5", + "IAB8-18" + ], "imp": [ { "id": "test-imp-id", "banner": { + "battr": [ + 1, + 2, + 3 + ], "format": [ { "w": 300, @@ -64,13 +73,18 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, { "id": "test-imp-video-id", "video": { + "battr": [ + 1, + 2, + 3 + ], "mimes": ["video/mp4"], "protocols": [2, 5], "w": 1024, @@ -78,7 +92,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } diff --git a/adapters/brightroll/brightrolltest/exemplary/simple-banner.json b/adapters/brightroll/brightrolltest/exemplary/simple-banner.json index 96fa0cbc9f3..66bc06cf696 100644 --- a/adapters/brightroll/brightrolltest/exemplary/simple-banner.json +++ b/adapters/brightroll/brightrolltest/exemplary/simple-banner.json @@ -18,7 +18,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } @@ -28,14 +28,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom", + "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", "body": { "id": "test-request-id", "at":1, + "bcat": [ + "IAB8-5", + "IAB8-18" + ], "imp": [ { "id": "test-imp-id", "banner": { + "battr": [ + 1, + 2, + 3 + ], "format": [ { "w": 300, @@ -51,7 +60,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } diff --git a/adapters/brightroll/brightrolltest/exemplary/simple-video.json b/adapters/brightroll/brightrolltest/exemplary/simple-video.json index f2466b2fd95..3f9b809182d 100644 --- a/adapters/brightroll/brightrolltest/exemplary/simple-video.json +++ b/adapters/brightroll/brightrolltest/exemplary/simple-video.json @@ -12,7 +12,7 @@ }, "ext":{ "bidder":{ - "publisher": "cafemom" + "publisher": "adthrive" } } } @@ -22,14 +22,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom", + "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", "body": { "id": "test-request-id", "at":1, + "bcat": [ + "IAB8-5", + "IAB8-18" + ], "imp": [ { "id": "test-imp-id", "video": { + "battr": [ + 1, + 2, + 3 + ], "mimes": ["video/mp4"], "protocols": [2, 5], "w": 1024, @@ -37,7 +46,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } diff --git a/adapters/brightroll/brightrolltest/exemplary/valid-extension.json b/adapters/brightroll/brightrolltest/exemplary/valid-extension.json index 970b4ade63a..da38c62be58 100644 --- a/adapters/brightroll/brightrolltest/exemplary/valid-extension.json +++ b/adapters/brightroll/brightrolltest/exemplary/valid-extension.json @@ -12,7 +12,7 @@ }, "ext":{ "bidder":{ - "publisher": "cafemom" + "publisher": "adthrive" } } } @@ -22,14 +22,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom", + "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", "body": { "id": "test-request-id", "at":1, + "bcat": [ + "IAB8-5", + "IAB8-18" + ], "imp": [ { "id": "test-imp-id", "video": { + "battr": [ + 1, + 2, + 3 + ], "mimes": ["video/mp4"], "protocols": [2, 5], "w": 1024, @@ -37,7 +46,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } diff --git a/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json b/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json index 9f24a471b31..d3295e5bffd 100644 --- a/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json +++ b/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json @@ -12,7 +12,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -25,7 +25,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } @@ -34,14 +34,23 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom", + "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive", "body": { "id": "test-request-id", "at":1, + "bcat": [ + "IAB8-5", + "IAB8-18" + ], "imp": [ { "id": "test-imp-video-id", "video": { + "battr": [ + 1, + 2, + 3 + ], "mimes": ["video/mp4"], "protocols": [2, 5], "w": 1024, @@ -49,7 +58,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, @@ -62,7 +71,7 @@ }, "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } } diff --git a/adapters/brightroll/brightrolltest/params/race/banner.json b/adapters/brightroll/brightrolltest/params/race/banner.json index 91517e36fd8..6eb4ec6a337 100644 --- a/adapters/brightroll/brightrolltest/params/race/banner.json +++ b/adapters/brightroll/brightrolltest/params/race/banner.json @@ -1,3 +1,3 @@ { - "publisher": "cafemom" + "publisher": "adthrive" } diff --git a/adapters/brightroll/brightrolltest/params/race/video.json b/adapters/brightroll/brightrolltest/params/race/video.json index 91517e36fd8..6eb4ec6a337 100644 --- a/adapters/brightroll/brightrolltest/params/race/video.json +++ b/adapters/brightroll/brightrolltest/params/race/video.json @@ -1,3 +1,3 @@ { - "publisher": "cafemom" + "publisher": "adthrive" } diff --git a/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json b/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json index a6362c40adb..01beec712c7 100644 --- a/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json +++ b/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json @@ -3,7 +3,7 @@ "id": "test-request-id", "ext": { "bidder": { - "publisher": "cafemom" + "publisher": "adthrive" } } }, diff --git a/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json b/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json new file mode 100644 index 00000000000..da48108af0b --- /dev/null +++ b/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json @@ -0,0 +1,34 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-missing-req-param-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "publisher":"test" + } + } + + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Invalid publisher", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index d056de664b7..a160e87aad7 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -117,7 +117,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderBrightroll: brightroll.NewBrightrollBidder(cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderBrightroll)].ExtraAdapterInfo), 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), From 480d2a22042597a3179c3504b39e54d7a715fe00 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Tue, 8 Sep 2020 17:14:52 -0400 Subject: [PATCH 196/381] Refactor TCF 1/2 Vendor List Fetcher Tests (#1441) --- gdpr/gdpr.go | 9 +- gdpr/impl_test.go | 183 +++--- gdpr/vendorlist-fetching.go | 118 ++-- gdpr/vendorlist-fetching_test.go | 954 ++++++++++++++++++++++--------- 4 files changed, 851 insertions(+), 413 deletions(-) diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 04db8cb92ed..6d447beb438 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -29,9 +29,10 @@ type Permissions interface { AMPException() bool } +// Versions of the GDPR TCF technical specification. const ( - tCF1 uint8 = 1 - tCF2 uint8 = 2 + tcf1SpecVersion uint8 = 1 + tcf2SpecVersion uint8 = 2 ) // NewPermissions gets an instance of the Permissions for use elsewhere in the project. @@ -45,8 +46,8 @@ func NewPermissions(ctx context.Context, cfg config.GDPR, vendorIDs map[openrtb_ 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)}, + tcf1SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tcf1SpecVersion), + tcf2SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tcf2SpecVersion)}, } } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 053e87536ab..d5114454f06 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -23,8 +23,8 @@ func TestNoConsentButAllowByDefault(t *testing.T) { }, vendorIDs: nil, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: failedListFetcher, - tCF2: failedListFetcher, + tcf1SpecVersion: failedListFetcher, + tcf2SpecVersion: failedListFetcher, }, } allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "") @@ -43,8 +43,8 @@ func TestNoConsentAndRejectByDefault(t *testing.T) { }, vendorIDs: nil, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: failedListFetcher, - tCF2: failedListFetcher, + tcf1SpecVersion: failedListFetcher, + tcf2SpecVersion: failedListFetcher, }, } allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "") @@ -56,12 +56,11 @@ func TestNoConsentAndRejectByDefault(t *testing.T) { } func TestAllowedSyncs(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, - }, - 3: { - purposes: []int{1}, + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, + {ID: 3, Purposes: []int{1}}, }, }) perms := permissionsImpl{ @@ -73,10 +72,10 @@ func TestAllowedSyncs(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), }, @@ -92,12 +91,11 @@ func TestAllowedSyncs(t *testing.T) { } func TestProhibitedPurposes(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, // cookie reads/writes - }, - 3: { - purposes: []int{3}, // ad personalization + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, // cookie reads/writes + {ID: 3, Purposes: []int{3}}, // ad personalization }, }) perms := permissionsImpl{ @@ -109,10 +107,10 @@ func TestProhibitedPurposes(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), }, @@ -128,12 +126,11 @@ func TestProhibitedPurposes(t *testing.T) { } func TestProhibitedVendors(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, // cookie reads/writes - }, - 3: { - purposes: []int{3}, // ad personalization + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, // cookie reads/writes + {ID: 3, Purposes: []int{3}}, // ad personalization }, }) perms := permissionsImpl{ @@ -145,10 +142,10 @@ func TestProhibitedVendors(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), }, @@ -169,8 +166,8 @@ func TestMalformedConsent(t *testing.T) { HostVendorID: 2, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(nil), - tCF2: listFetcher(nil), + tcf1SpecVersion: listFetcher(nil), + tcf2SpecVersion: listFetcher(nil), }, } @@ -180,12 +177,11 @@ func TestMalformedConsent(t *testing.T) { } func TestAllowPersonalInfo(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, // cookie reads/writes - }, - 3: { - purposes: []int{1, 3}, // ad personalization + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, // cookie reads/writes + {ID: 3, Purposes: []int{1, 3}}, // ad personalization }, }) perms := permissionsImpl{ @@ -197,10 +193,10 @@ func TestAllowPersonalInfo(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), }, @@ -222,24 +218,39 @@ func TestAllowPersonalInfo(t *testing.T) { 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}}, +func buildTCF2VendorList34() tcf2VendorList { + return tcf2VendorList{ + VendorListVersion: 2, + Vendors: map[string]*tcf2Vendor{ + "2": { + ID: 2, + Purposes: []int{1}, + }, + "6": { + ID: 6, + Purposes: []int{1, 2, 4}, + LegIntPurposes: []int{7}, + SpecialPurposes: []int{1}, + FlexiblePurposes: []int{1, 2, 4, 7}, + }, + "8": { + ID: 8, + Purposes: []int{1, 7}, + LegIntPurposes: []int{2, 4}, + }, + "10": { + ID: 10, + Purposes: []int{2, 4, 7}, + SpecialPurposes: []int{1}, + }, + "32": { + ID: 32, + Purposes: []int{1, 2, 4, 7}, + }, + }, + } } + var tcf2Config = config.GDPR{ HostVendorID: 2, TCF2: config.TCF2{ @@ -261,7 +272,7 @@ type tcf2TestDef struct { } func TestAllowPersonalInfoTCF2(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -270,8 +281,8 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -316,7 +327,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { } func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -325,8 +336,8 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -338,11 +349,10 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { assert.EqualValuesf(t, true, allowPI, "AllowPI failure") assert.EqualValuesf(t, true, allowGeo, "AllowGeo failure") assert.EqualValuesf(t, true, allowID, "AllowID failure") - } func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -351,8 +361,8 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 15: parseVendorListDataV2(t, vendorListData), }), }, @@ -397,7 +407,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { } func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -406,8 +416,8 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -453,7 +463,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { } func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -462,8 +472,8 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -510,7 +520,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { } func TestAllowSyncTCF2(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -519,8 +529,8 @@ func TestAllowSyncTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -537,9 +547,9 @@ func TestAllowSyncTCF2(t *testing.T) { } func TestProhibitedPurposeSyncTCF2(t *testing.T) { - basicPurposes := tcf2BasicPurposes - basicPurposes[8] = &purposes{purposes: []int{7}} - vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + tcf2VendorList34 := buildTCF2VendorList34() + tcf2VendorList34.Vendors["8"].Purposes = []int{7} + vendorListData := tcf2MarshalVendorList(tcf2VendorList34) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -548,15 +558,15 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: 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 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for 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") @@ -567,9 +577,7 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) { } func TestProhibitedVendorSyncTCF2(t *testing.T) { - basicPurposes := tcf2BasicPurposes - basicPurposes[10] = &purposes{purposes: []int{1}} - vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -579,20 +587,21 @@ func TestProhibitedVendorSyncTCF2(t *testing.T) { openrtb_ext.BidderOpenx: 10, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: 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 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for 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") + // Permission disallowed due to consent string not including vendor 10. + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderOpenx, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure") } diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index 1442f81c3ba..66a3f4ad2d6 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -26,67 +26,83 @@ type saveVendors func(uint16, api.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, uint8) string, TCFVer uint8) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { - var fallbackVL api.VendorList = nil - - if TCFVer == tCF1 && len(cfg.TCF1.FallbackGVLPath) > 0 { - fallbackVL = loadFallbackGVL(cfg.TCF1.FallbackGVLPath) - } - - // If we are not going to try fetching the GVL dynamically, we have a simple fetcher - if !cfg.TCF1.FetchGVL && TCFVer == tCF1 && fallbackVL != nil { - return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { - return fallbackVL, nil +func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16, uint8) string, tcfSpecVersion uint8) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { + var fallback api.VendorList + if tcfSpecVersion == tcf1SpecVersion && len(cfg.TCF1.FallbackGVLPath) > 0 { + fallback = loadFallbackGVL(cfg.TCF1.FallbackGVLPath) + } + + // If we are not going to try fetching the GVL dynamically, we have a simple fetcher. + if !cfg.TCF1.FetchGVL && tcfSpecVersion == tcf1SpecVersion { + if fallback != nil { + return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { + return fallback, nil + } + } + return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { + return nil, makeVendorListNotFoundError(vendorListVersion) } } - // These save and load functions can be used to store & retrieve lists from our cache. - save, load := newVendorListCache(fallbackVL) - withTimeout, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) - defer cancel() - populateCache(withTimeout, client, urlMaker, save, TCFVer) + cacheSave, cacheLoad := newVendorListCache(fallback) - saveOneSometimes := newOccasionalSaver(cfg.Timeouts.ActiveTimeout(), TCFVer) + preloadContext, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) + defer cancel() + preloadCache(preloadContext, client, urlMaker, cacheSave, tcfSpecVersion) - return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { - list := load(id) - if list != nil { + saveOneRateLimited := newOccasionalSaver(cfg.Timeouts.ActiveTimeout(), tcfSpecVersion) + return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { + // Attempt To Load From Cache + if list := cacheLoad(vendorListVersion); list != nil { return list, nil } - saveOneSometimes(ctx, client, urlMaker(id, TCFVer), save) - list = load(id) - if list != nil { + + // Attempt To Download + // - May not add to cache immediately. + saveOneRateLimited(ctx, client, urlMaker(vendorListVersion, tcfSpecVersion), cacheSave) + + // Attempt To Load From Cache Again + // - May have been added by the call to saveOneRateLimited. + if list := cacheLoad(vendorListVersion); list != nil { return list, nil } - if fallbackVL != nil { - return fallbackVL, nil + + // Attempt To Use Hardcoded Fallback + if fallback != nil { + return fallback, nil } - return nil, fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", id) + + // Give Up + return nil, makeVendorListNotFoundError(vendorListVersion) } } -// populateCache saves all the known versions of the vendor list for future use. -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) +func makeVendorListNotFoundError(vendorListVersion uint16) error { + return fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", vendorListVersion) +} + +// preloadCache saves all the known versions of the vendor list for future use. +func preloadCache(ctx context.Context, client *http.Client, urlMaker func(uint16, uint8) string, saver saveVendors, tcfSpecVersion uint8) { + latestVersion := saveOne(ctx, client, urlMaker(0, tcfSpecVersion), saver, tcfSpecVersion) for i := uint16(1); i < latestVersion; i++ { - saveOne(ctx, client, urlMaker(i, TCFVer), saver, TCFVer) + saveOne(ctx, client, urlMaker(i, tcfSpecVersion), saver, tcfSpecVersion) } } // 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, TCFVer uint8) string { - if TCFVer == 2 { - if version == 0 { +func vendorListURLMaker(vendorListVersion uint16, tcfSpecVersion uint8) string { + if tcfSpecVersion == tcf2SpecVersion { + if vendorListVersion == 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" + return "https://vendorlist.consensu.org/v2/archives/vendor-list-v" + strconv.Itoa(int(vendorListVersion)) + ".json" } - if version == 0 { + if vendorListVersion == 0 { return "https://vendorlist.consensu.org/vendorlist.json" } - return "https://vendorlist.consensu.org/v-" + strconv.Itoa(int(version)) + "/vendorlist.json" + return "https://vendorlist.consensu.org/v-" + strconv.Itoa(int(vendorListVersion)) + "/vendorlist.json" } // newOccasionalSaver returns a wrapped version of saveOne() which only activates every few minutes. @@ -94,22 +110,24 @@ func vendorListURLMaker(version uint16, TCFVer uint8) 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, TCFVer uint8) func(ctx context.Context, client *http.Client, url string, saver saveVendors) { +func newOccasionalSaver(timeout time.Duration, tcfSpecVersion uint8) func(ctx context.Context, client *http.Client, url string, saver saveVendors) { lastSaved := &atomic.Value{} lastSaved.Store(time.Time{}) return func(ctx context.Context, client *http.Client, url string, saver saveVendors) { now := time.Now() - if now.Sub(lastSaved.Load().(time.Time)).Minutes() > 10 { + timeSinceLastSave := now.Sub(lastSaved.Load().(time.Time)) + + if timeSinceLastSave.Minutes() > 10 { withTimeout, cancel := context.WithTimeout(ctx, timeout) defer cancel() - saveOne(withTimeout, client, url, saver, TCFVer) + saveOne(withTimeout, client, url, saver, tcfSpecVersion) lastSaved.Store(now) } } } -func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors, cTFVer uint8) uint16 { +func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors, tcfSpecVersion 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) @@ -133,7 +151,7 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen return 0 } var newList api.VendorList - if cTFVer == 2 { + if tcfSpecVersion == tcf2SpecVersion { newList, err = vendorlist2.ParseEagerly(respBody) } else { newList, err = vendorlist.ParseEagerly(respBody) @@ -147,14 +165,15 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen return newList.Version() } -func newVendorListCache(fallbackVL api.VendorList) (save func(id uint16, list api.VendorList), load func(id uint16) api.VendorList) { +func newVendorListCache(fallbackVL api.VendorList) (save func(vendorListVersion uint16, list api.VendorList), load func(vendorListVersion uint16) api.VendorList) { cache := &sync.Map{} - save = func(id uint16, list api.VendorList) { - cache.Store(id, list) + save = func(vendorListVersion uint16, list api.VendorList) { + cache.Store(vendorListVersion, list) } - load = func(id uint16) api.VendorList { - list, ok := cache.Load(id) + + load = func(vendorListVersion uint16) api.VendorList { + list, ok := cache.Load(vendorListVersion) if ok { return list.(vendorlist.VendorList) } @@ -164,13 +183,14 @@ func newVendorListCache(fallbackVL api.VendorList) (save func(id uint16, list ap } func loadFallbackGVL(fallbackGVLPath string) vendorlist.VendorList { - fallbackVLbody, err := ioutil.ReadFile(fallbackGVLPath) + fallbackContents, err := ioutil.ReadFile(fallbackGVLPath) if err != nil { glog.Fatalf("Error reading from file %s: %v", fallbackGVLPath, err) } - fallbackVL, err := vendorlist.ParseEagerly(fallbackVLbody) + + fallback, err := vendorlist.ParseEagerly(fallbackContents) if err != nil { glog.Fatalf("Error processing default GVL from %s: %v", fallbackGVLPath, err) } - return fallbackVL + return fallback } diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index 484a0a54b41..e5ad8793b4f 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -7,233 +7,697 @@ import ( "net/http/httptest" "strconv" "testing" - "time" "github.com/stretchr/testify/assert" + "github.com/prebid/go-gdpr/consentconstants" "github.com/prebid/prebid-server/config" ) -func TestVendorFetch(t *testing.T) { - vendorListOne := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, - }, - }) - vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2, 3}, +func TestTCF1FetcherInitialLoad(t *testing.T) { + // Loads two vendor lists during initialization by setting the latest vendor list version to 2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 2, + vendorLists: map[int]string{ + 1: tcf1VendorList1, + 2: tcf1VendorList2, }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ - 1: vendorListOne, - 2: vendorListTwo, }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - list, err := fetcher(context.Background(), 1) - assertNilErr(t, err) - vendor := list.Vendor(32) - assertBoolsEqual(t, true, vendor.Purpose(1)) - assertBoolsEqual(t, false, vendor.Purpose(3)) - assertBoolsEqual(t, false, vendor.Purpose(4)) - - list, err = fetcher(context.Background(), 2) - assertNilErr(t, err) - vendor = list.Vendor(32) - assertBoolsEqual(t, true, vendor.Purpose(1)) - assertBoolsEqual(t, true, vendor.Purpose(3)) -} - -func TestLazyFetch(t *testing.T) { - firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, - }, - }) - secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ - 3: { - purposes: []int{1}, - }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: firstVendorList, - 2: secondVendorList, + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } + + for _, test := range testCases { + runTest(t, test, tcf1SpecVersion, server) + } +} + +func TestTCF2FetcherInitialLoad(t *testing.T) { + // Loads two vendor lists during initialization by setting the latest vendor list version to 2. + // Ensures TCF1 fetch settings have no effect on TCF2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 2, + vendorLists: map[int]string{ + 1: tcf2VendorList1, + 2: tcf2VendorList2, + }, }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - list, err := fetcher(context.Background(), 2) - assertNilErr(t, err) + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + } - vendor := list.Vendor(3) - assertBoolsEqual(t, true, vendor.Purpose(1)) - assertBoolsEqual(t, false, vendor.Purpose(2)) + for _, test := range testCases { + runTest(t, test, tcf2SpecVersion, server) + } } -func TestInitialTimeout(t *testing.T) { - list := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, +func TestTCF1FetcherDynamicLoadListExists(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor lists will be dynamically loaded. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf1VendorList1, + 2: tcf1VendorList2, }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: list, }))) defer server.Close() - ctx, cancel := context.WithDeadline(context.Background(), time.Time{}) - defer cancel() - 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) + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } + + for _, test := range testCases { + runTest(t, test, tcf1SpecVersion, server) + } } -func TestFetchThrottling(t *testing.T) { - vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, - }, - }) - vendorListThree := mockVendorListData(t, 3, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, +func TestTCF2FetcherDynamicLoadListExists(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor lists will be dynamically loaded. + // Ensures TCF1 fetch settings have no effect on TCF2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf2VendorList1, + 2: tcf2VendorList2, }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: "{}", - 2: vendorListTwo, - 3: vendorListThree, }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - _, err := fetcher(context.Background(), 2) - assertNilErr(t, err) - _, err = fetcher(context.Background(), 3) - assertErr(t, err, false) + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + } + + for _, test := range testCases { + runTest(t, test, tcf2SpecVersion, server) + } } -func TestMalformedVendorlistFetch(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"}))) +func TestTCF1FetcherDynamicLoadListDoesntExist(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor list load attempts will be done dynamically. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf1VendorList1, + }, + }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - _, err := fetcher(context.Background(), 1) - assertErr(t, err, false) + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } + + for _, test := range testCases { + runTest(t, test, 1, server) + } } -func TestMissingVendorlistFetch(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"}))) +func TestTCF2FetcherDynamicLoadListDoesntExist(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor list load attempts will be done dynamically. + // Ensures TCF1 fetch settings have no effect on TCF2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf2VendorList1, + }, + }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - _, err := fetcher(context.Background(), 2) - assertErr(t, err, false) -} + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "No Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "No Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } -func TestVendorListMaker(t *testing.T) { - 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)) + for _, test := range testCases { + runTest(t, test, tcf2SpecVersion, server) + } } -func TestDefaultVendorList(t *testing.T) { - firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, - }, - }) - secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ - 12: { - purposes: []int{2}, +func TestTCF1FetcherThrottling(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{1}}}, + }), + 2: tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 2, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{1, 2}}}, + }), + 3: tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 3, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{1, 2, 3}}}, + }), }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ - 1: firstVendorList, - 2: secondVendorList, }))) defer server.Close() - testcfg := testConfig() - testcfg.TCF1.FetchGVL = true - testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" - fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion) - list, err := fetcher(context.Background(), 12) - assert.NoError(t, err, "Error with fetching default vendorlist: %v", err) - assert.Equal(t, uint16(215), list.Version(), "Expected to fetch default version 215, got %d", list.Version()) + // Dynamically Load List 2 Successfully + _, errList1 := fetcher(context.Background(), 2) + assert.NoError(t, errList1) - // Testing that we got the default vendorlist data, and not the version off the server. - vendor := list.Vendor(12) - assert.Equal(t, true, vendor.Purpose(1)) - assert.Equal(t, false, vendor.Purpose(2)) + // Fail To Load List 3 Due To Rate Limiting + // - The request is rate limited after dynamically list 2. + _, errList2 := fetcher(context.Background(), 3) + assert.EqualError(t, errList2, "gdpr vendor list version 3 does not exist, or has not been loaded yet. Try again in a few minutes") } -func TestFallbackVendorListPassthrough(t *testing.T) { - firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, +func TestTCF2FetcherThrottling(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 1, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1}}}, + }), + 2: tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 2, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2}}}, + }), + 3: tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 3, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}}, + }), }, - }) - secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ - 12: { - purposes: []int{2}, + }))) + defer server.Close() + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion) + + // Dynamically Load List 2 Successfully + _, errList1 := fetcher(context.Background(), 2) + assert.NoError(t, errList1) + + // Fail To Load List 3 Due To Rate Limiting + // - The request is rate limited after dynamically list 2. + _, errList2 := fetcher(context.Background(), 3) + assert.EqualError(t, errList2, "gdpr vendor list version 3 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF1MalformedVendorlist(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: "malformed", }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: firstVendorList, - 2: secondVendorList, }))) defer server.Close() - testcfg := testConfig() - testcfg.TCF1.FetchGVL = true - testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" - fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1) - list, err := fetcher(context.Background(), 2) - assert.NoError(t, err, "Error with fetching existing vendorlist: %v", err) - assert.Equal(t, uint16(2), list.Version(), "Expected to fetch mock list version 2, got version %d", list.Version()) - - // Testing that we got the testing vendorlist data, and not the default. - vendor := list.Vendor(12) - assert.Equal(t, false, vendor.Purpose(1)) - assert.Equal(t, true, vendor.Purpose(2)) -} - -func TestFallbackVendorListNoFetch(t *testing.T) { - firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, - }, - }) - secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ - 12: { - purposes: []int{2}, - }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: firstVendorList, - 2: secondVendorList, + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion) + _, err := fetcher(context.Background(), 1) + + // Fetching should fail since vendor list could not be unmarshalled. + assert.Error(t, err) +} + +func TestTCF2MalformedVendorlist(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: "malformed", + }, }))) defer server.Close() - testcfg := testConfig() - testcfg.TCF1.FetchGVL = false - testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" - fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1) - list, err := fetcher(context.Background(), 2) - assert.NoError(t, err, "Error with fetching default vendorlist: %v", err) - assert.Equal(t, uint16(215), list.Version(), "Expected to fetch default version 215, got %d", list.Version()) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion) + _, err := fetcher(context.Background(), 1) + + // Fetching should fail since vendor list could not be unmarshalled. + assert.Error(t, err) +} + +func TestTCF1ServerUrlInvalid(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + invalidURLGenerator := func(uint16, uint8) string { return " http://invalid-url-has-leading-whitespace" } - // Testing that we got the default vendorlist data, and not the version off the server. - vendor := list.Vendor(12) - assert.Equal(t, true, vendor.Purpose(1)) - assert.Equal(t, false, vendor.Purpose(2)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator, tcf1SpecVersion) + _, err := fetcher(context.Background(), 1) + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF2ServerUrlInvalid(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + invalidURLGenerator := func(uint16, uint8) string { return " http://invalid-url-has-leading-whitespace" } + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator, tcf2SpecVersion) + _, err := fetcher(context.Background(), 1) + + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF1ServerUnavailable(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion) + _, err := fetcher(context.Background(), 1) + + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF2ServerUnavailable(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion) + _, err := fetcher(context.Background(), 1) + + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestVendorListURLMaker(t *testing.T) { + testCases := []struct { + description string + tcfSpecVersion uint8 + vendorListVersion uint16 + expectedURL string + }{ + { + description: "TCF1 - Latest", + tcfSpecVersion: 1, + vendorListVersion: 0, // Forces latest version. + expectedURL: "https://vendorlist.consensu.org/vendorlist.json", + }, + { + description: "TCF1 - Specific", + tcfSpecVersion: 1, + vendorListVersion: 42, + expectedURL: "https://vendorlist.consensu.org/v-42/vendorlist.json", + }, + { + description: "TCF2 - Latest", + tcfSpecVersion: 2, + vendorListVersion: 0, // Forces latest version. + expectedURL: "https://vendorlist.consensu.org/v2/vendor-list.json", + }, + { + description: "TCF2 - Specific", + tcfSpecVersion: 2, + vendorListVersion: 42, + expectedURL: "https://vendorlist.consensu.org/v2/archives/vendor-list-v42.json", + }, + } + + for _, test := range testCases { + result := vendorListURLMaker(test.vendorListVersion, test.tcfSpecVersion) + assert.Equal(t, test.expectedURL, result) + } +} + +var tcf1VendorList1 = tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{2}}}, +}) + +var tcf2VendorList1 = tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 1, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{2}}}, +}) + +var vendorList1Expected = testExpected{ + vendorListVersion: 1, + vendorID: 12, + vendorPurposes: map[int]bool{1: false, 2: true, 3: false}, +} + +var tcf1VendorList2 = tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 2, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{2, 3}}}, +}) + +var tcf2VendorList2 = tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 2, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{2, 3}}}, +}) + +var vendorList2Expected = testExpected{ + vendorListVersion: 2, + vendorID: 12, + vendorPurposes: map[int]bool{1: false, 2: true, 3: true}, +} + +var vendorListFallbackExpected = testExpected{ + vendorListVersion: 215, // Values from hardcoded fallback file. + vendorID: 12, + vendorPurposes: map[int]bool{1: true, 2: false, 3: true}, +} + +type tcf1VendorList struct { + VendorListVersion uint16 `json:"vendorListVersion"` + Vendors []tcf1Vendor `json:"vendors"` +} + +type tcf1Vendor struct { + ID uint16 `json:"id"` + Purposes []int `json:"purposeIds"` +} + +func tcf1MarshalVendorList(vendorList tcf1VendorList) string { + json, _ := json.Marshal(vendorList) + return string(json) +} + +type tcf2VendorList struct { + VendorListVersion uint16 `json:"vendorListVersion"` + Vendors map[string]*tcf2Vendor `json:"vendors"` +} + +type tcf2Vendor struct { + ID uint16 `json:"id"` + Purposes []int `json:"purposes"` + LegIntPurposes []int `json:"legIntPurposes"` + FlexiblePurposes []int `json:"flexiblePurposes"` + SpecialPurposes []int `json:"specialPurposes"` +} + +func tcf2MarshalVendorList(vendorList tcf2VendorList) string { + json, _ := json.Marshal(vendorList) + return string(json) +} + +type serverSettings struct { + vendorListLatestVersion int + vendorLists map[int]string } // mockServer returns a handler which returns the given response for each global vendor list version. @@ -247,129 +711,74 @@ func TestFallbackVendorListNoFetch(t *testing.T) { // // If the "version" query param points to a version which doesn't exist, it returns a 403. // Don't ask why... that's just what the official page is doing. See https://vendorlist.consensu.org/v-9999/vendorlist.json -func mockServer(latestVersion int, responses map[int]string) func(http.ResponseWriter, *http.Request) { +func mockServer(settings serverSettings) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, req *http.Request) { - version := req.URL.Query().Get("version") - versionInt, err := strconv.Atoi(version) + vendorListVersion := req.URL.Query().Get("version") + vendorListVersionInt, err := strconv.Atoi(vendorListVersion) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Request had invalid version: " + version)) + w.Write([]byte("Request had invalid version: " + vendorListVersion)) return } - if versionInt == 0 { - versionInt = latestVersion + if vendorListVersionInt == 0 { + vendorListVersionInt = settings.vendorListLatestVersion } - response, ok := responses[versionInt] + response, ok := settings.vendorLists[vendorListVersionInt] if !ok { w.WriteHeader(http.StatusForbidden) - w.Write([]byte("Version not found: " + version)) + w.Write([]byte("Version not found: " + vendorListVersion)) return } w.Write([]byte(response)) } } -func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purposes) string { - type vendorContract struct { - ID uint16 `json:"id"` - Purposes []int `json:"purposeIds"` - } - - type vendorListContract struct { - Version uint16 `json:"vendorListVersion"` - Vendors []vendorContract `json:"vendors"` - } - - buildVendors := func(input map[uint16]*purposes) []vendorContract { - vendors := make([]vendorContract, 0, len(input)) - for id, purpose := range input { - vendors = append(vendors, vendorContract{ - ID: id, - Purposes: purpose.purposes, - }) - } - return vendors - } - - obj := vendorListContract{ - Version: version, - Vendors: buildVendors(vendors), - } - data, err := json.Marshal(obj) - assertNilErr(t, err) - return string(data) +type test struct { + description string + setup testSetup + expected testExpected } -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 - } +type testSetup struct { + enableTCF1Fetch bool + enableTCF1Fallback bool + vendorListVersion uint16 +} - 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 - } +type testExpected struct { + errorMessage string + vendorListVersion uint16 + vendorID uint16 + vendorPurposes map[int]bool +} - 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 +func runTest(t *testing.T, test test, tcfSpecVersion uint8, server *httptest.Server) { + config := testConfig() + config.TCF1.FetchGVL = test.setup.enableTCF1Fetch + if test.setup.enableTCF1Fallback { + config.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" } - for id, purpose := range specialPurposes { - sid := strconv.Itoa(int(id)) - vendor, ok := vendors[sid] - if !ok { - vendor = vendorContract{ID: id} + fetcher := newVendorListFetcher(context.Background(), config, server.Client(), testURLMaker(server), tcfSpecVersion) + vendorList, err := fetcher(context.Background(), test.setup.vendorListVersion) + + if test.expected.errorMessage != "" { + assert.EqualError(t, err, test.expected.errorMessage, test.description+":error") + } else { + assert.NoError(t, err, test.description+":vendorlist") + assert.Equal(t, test.expected.vendorListVersion, vendorList.Version(), test.description+":vendorlistid") + vendor := vendorList.Vendor(test.expected.vendorID) + for id, expected := range test.expected.vendorPurposes { + result := vendor.Purpose(consentconstants.Purpose(id)) + assert.Equalf(t, expected, result, "%s:vendor-%d:purpose-%d", test.description, vendorList.Version(), 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, TCFVer uint8) string { - return url + "?version=" + strconv.Itoa(int(version)) + return func(vendorListVersion uint16, tcfSpecVersion uint8) string { + return url + "?version=" + strconv.Itoa(int(vendorListVersion)) } } @@ -379,9 +788,8 @@ func testConfig() config.GDPR { InitVendorlistFetch: 60 * 1000, ActiveVendorlistFetch: 1000 * 5, }, + TCF1: config.TCF1{ + FetchGVL: true, + }, } } - -type purposes struct { - purposes []int -} From dc4d192c177b2220d9a41fa8146ddef57216f3db Mon Sep 17 00:00:00 2001 From: Viral Vala Date: Wed, 9 Sep 2020 16:04:20 +0530 Subject: [PATCH 197/381] BugID:OTT-17 First Commit --- endpoints/openrtb2/ctv_auction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index 00fe0c4a5e2..c8d8c07b100 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -692,7 +692,7 @@ func (deps *ctvEndpointDeps) getBidResponseExt(resp *openrtb.BidResponse) (data if nil != imp.Bid && len(imp.Bid.Bids) > 0 { for _, bid := range imp.Bid.Bids { //update adm - bid.AdM = constant.VASTDefaultTag + //bid.AdM = constant.VASTDefaultTag //add duration value raw, err := jsonparser.Set(bid.Ext, []byte(strconv.Itoa(int(bid.Duration))), "prebid", "video", "duration") From 420da24edd3a6316e669ae969dcba10edb57fcaa Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Wed, 9 Sep 2020 12:01:33 -0700 Subject: [PATCH 198/381] Add validation checker for PRs and merges with github actions (#1476) --- .github/workflows/validate.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000000..d7bb50fbabf --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,28 @@ +on: + push: + branches: + - master + pull_request: + release: + types: + - created +name: Validate +jobs: + Go: + strategy: + matrix: + go-version: [1.13.x, 1.14.x, 1.15.x] + os: [ubuntu-18.04] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Validate + run: | + ./validate.sh --nofmt --cov --race 10 + env: + GO111MODULE: "on" From 22c454c9e1d2617b109b1577c172a82da6a1358e Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Thu, 10 Sep 2020 10:17:40 -0700 Subject: [PATCH 199/381] Cache refactor (#1431) Reason: Cache has Fetcher-like functionality to handle both requests and imps at a time. Internally, it still uses two caches configured and searched separately, causing some code repetition. Reusing this code to cache other objects like accounts is not easy. Keeping the req/imp repetition in fetcher and out of cache allows for a reusable simpler cache, preserving existing fetcher functionality. Changes in this set: Cache is now a simple generic id->RawMessage store fetcherWithCache handles the separate req and imp caches ComposedCache handles single caches - but it does not appear to be used Removed cache overlap tests since they do not apply now Slightly less code --- stored_requests/caches/cachestest/reliable.go | 65 ++---------- stored_requests/caches/memory/cache.go | 60 ++++------- stored_requests/caches/memory/cache_test.go | 44 +++------ stored_requests/caches/nil_cache/nil_cache.go | 9 +- stored_requests/config/config.go | 7 +- stored_requests/config/config_test.go | 8 +- stored_requests/events/api/api_test.go | 31 +++--- stored_requests/events/events.go | 6 +- stored_requests/events/events_test.go | 21 ++-- stored_requests/fetcher.go | 57 ++++++----- stored_requests/fetcher_test.go | 99 ++++++++++--------- 11 files changed, 169 insertions(+), 238 deletions(-) diff --git a/stored_requests/caches/cachestest/reliable.go b/stored_requests/caches/cachestest/reliable.go index e08a20e9cdb..7fbaf7238af 100644 --- a/stored_requests/caches/cachestest/reliable.go +++ b/stored_requests/caches/cachestest/reliable.go @@ -11,8 +11,6 @@ import ( const ( reqCacheKey = "known-req" reqCacheVal = `{"req":true}` - impCacheKey = "known-imp" - impCacheVal = `{"imp":true}` ) // AssertCacheRobustness runs tests which can be used to validate any Cache that is 100% reliable. @@ -20,84 +18,41 @@ const ( // // The cacheSupplier should be a function which returns a new Cache (with no data inside) on every call. // This will be called from separate Goroutines to make sure that different tests don't conflict. -func AssertCacheRobustness(t *testing.T, cacheSupplier func() stored_requests.Cache) { +func AssertCacheRobustness(t *testing.T, cacheSupplier func() stored_requests.CacheJSON) { t.Run("TestCacheMiss", cacheMissTester(cacheSupplier())) t.Run("TestCacheHit", cacheHitTester(cacheSupplier())) - t.Run("TestCacheMixed", cacheMixedTester(cacheSupplier())) - t.Run("TestCacheOverlap", cacheOverlapTester(cacheSupplier())) t.Run("TestCacheSaveInvalidate", cacheSaveInvalidateTester(cacheSupplier())) } -func cacheMissTester(cache stored_requests.Cache) func(*testing.T) { +func cacheMissTester(cache stored_requests.CacheJSON) func(*testing.T) { return func(t *testing.T) { - storedReqs, storedImps := cache.Get(context.Background(), []string{"unknown"}, nil) - assertMapLength(t, 0, storedReqs) - assertMapLength(t, 0, storedImps) + storedData := cache.Get(context.Background(), []string{"unknown"}) + assertMapLength(t, 0, storedData) } } -func cacheHitTester(cache stored_requests.Cache) func(*testing.T) { +func cacheHitTester(cache stored_requests.CacheJSON) func(*testing.T) { return func(t *testing.T) { cache.Save(context.Background(), map[string]json.RawMessage{ reqCacheKey: json.RawMessage(reqCacheVal), - }, map[string]json.RawMessage{ - impCacheKey: json.RawMessage(impCacheVal), }) - reqData, impData := cache.Get(context.Background(), []string{reqCacheKey}, []string{impCacheKey}) - if len(reqData) != 1 { - t.Errorf("The cache should have returned the data.") - } + reqData := cache.Get(context.Background(), []string{reqCacheKey}) assertMapLength(t, 1, reqData) assertHasValue(t, reqData, reqCacheKey, reqCacheVal) - - assertMapLength(t, 1, impData) - assertHasValue(t, impData, impCacheKey, impCacheVal) - } -} - -func cacheMixedTester(cache stored_requests.Cache) func(*testing.T) { - return func(t *testing.T) { - cache.Save(context.Background(), map[string]json.RawMessage{ - reqCacheKey: json.RawMessage(reqCacheVal), - }, nil) - reqData, impData := cache.Get(context.Background(), []string{reqCacheKey, "unknown-req"}, nil) - assertMapLength(t, 1, reqData) - assertHasValue(t, reqData, reqCacheKey, reqCacheVal) - assertMapLength(t, 0, impData) } } -func cacheOverlapTester(cache stored_requests.Cache) func(*testing.T) { - commonKey := "id" +func cacheSaveInvalidateTester(cache stored_requests.CacheJSON) func(*testing.T) { return func(t *testing.T) { cache.Save(context.Background(), map[string]json.RawMessage{ - commonKey: json.RawMessage(reqCacheVal), - }, map[string]json.RawMessage{ - commonKey: json.RawMessage(impCacheVal), - }) - reqData, impData := cache.Get(context.Background(), []string{commonKey}, []string{commonKey}) - assertMapLength(t, 1, reqData) - assertHasValue(t, reqData, commonKey, reqCacheVal) - assertMapLength(t, 1, impData) - assertHasValue(t, impData, commonKey, impCacheVal) - } -} - -func cacheSaveInvalidateTester(cache stored_requests.Cache) func(*testing.T) { - return func(t *testing.T) { - cache.Save(context.Background(), map[string]json.RawMessage{ - reqCacheKey: json.RawMessage(reqCacheVal), - }, map[string]json.RawMessage{ reqCacheKey: json.RawMessage(reqCacheVal), }) - reqData, impData := cache.Get(context.Background(), []string{reqCacheKey}, []string{reqCacheKey}) + reqData := cache.Get(context.Background(), []string{reqCacheKey}) assertMapLength(t, 1, reqData) - assertMapLength(t, 1, impData) - cache.Invalidate(context.Background(), []string{reqCacheKey}, []string{reqCacheKey}) - reqData, impData = cache.Get(context.Background(), []string{reqCacheKey}, []string{reqCacheKey}) + cache.Invalidate(context.Background(), []string{reqCacheKey}) + reqData = cache.Get(context.Background(), []string{reqCacheKey}) assertMapLength(t, 0, reqData) - assertMapLength(t, 0, impData) } } diff --git a/stored_requests/caches/memory/cache.go b/stored_requests/caches/memory/cache.go index c5702080ef9..aea087e6d19 100644 --- a/stored_requests/caches/memory/cache.go +++ b/stored_requests/caches/memory/cache.go @@ -7,7 +7,6 @@ import ( "github.com/coocood/freecache" "github.com/golang/glog" - "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/stored_requests" ) @@ -17,68 +16,51 @@ import ( // 2. The cache is too large. This will cause the least recently used items to be evicted. // // For no TTL, use ttlSeconds <= 0 -func NewCache(cfg *config.InMemoryCache) stored_requests.Cache { - return &cache{ - requestDataCache: newCacheForWithLimits(cfg.RequestCacheSize, cfg.TTL, "Request"), - impDataCache: newCacheForWithLimits(cfg.ImpCacheSize, cfg.TTL, "Imp"), - } -} - -func newCacheForWithLimits(size int, ttl int, dataType string) mapLike { +func NewCache(size int, ttl int, dataType string) stored_requests.CacheJSON { if ttl > 0 && size <= 0 { - glog.Fatal("No in-memory caches defined with a finite TTL but unbounded size. Config validation should have caught this. Failing fast because something is buggy.") + glog.Fatalf("No in-memory %s caches defined with a finite TTL but unbounded size. Config validation should have caught this. Failing fast because something is buggy.", dataType) } if size > 0 { glog.Infof("Using a Stored %s in-memory cache. Max size: %d bytes. TTL: %d seconds.", dataType, size, ttl) - return &pbsLRUCache{ - Cache: freecache.NewCache(size), - ttlSeconds: ttl, + return &cache{ + dataType: dataType, + cache: &pbsLRUCache{ + Cache: freecache.NewCache(size), + ttlSeconds: ttl, + }, } } else { glog.Infof("Using an unbounded Stored %s in-memory cache.", dataType) - return &pbsSyncMap{&sync.Map{}} + return &cache{ + dataType: dataType, + cache: &pbsSyncMap{&sync.Map{}}, + } } } type cache struct { - requestDataCache mapLike - impDataCache mapLike + dataType string + cache mapLike } -func (c *cache) Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) { - requestData = doGet(c.requestDataCache, requestIDs) - impData = doGet(c.impDataCache, impIDs) - return -} - -func doGet(cache mapLike, ids []string) (data map[string]json.RawMessage) { +func (c *cache) Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) { data = make(map[string]json.RawMessage, len(ids)) for _, id := range ids { - if val, ok := cache.Get(id); ok { + if val, ok := c.cache.Get(id); ok { data[id] = val } } return } -func (c *cache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) { - c.doSave(c.requestDataCache, storedRequests) - c.doSave(c.impDataCache, storedImps) -} - -func (c *cache) doSave(cache mapLike, values map[string]json.RawMessage) { - for id, data := range values { - cache.Set(id, data) +func (c *cache) Save(ctx context.Context, data map[string]json.RawMessage) { + for id, data := range data { + c.cache.Set(id, data) } } -func (c *cache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { - doInvalidate(c.requestDataCache, requestIDs) - doInvalidate(c.impDataCache, impIDs) -} - -func doInvalidate(cache mapLike, ids []string) { +func (c *cache) Invalidate(ctx context.Context, ids []string) { for _, id := range ids { - cache.Delete(id) + c.cache.Delete(id) } } diff --git a/stored_requests/caches/memory/cache_test.go b/stored_requests/caches/memory/cache_test.go index ba4703ef89a..b89bd5af26f 100644 --- a/stored_requests/caches/memory/cache_test.go +++ b/stored_requests/caches/memory/cache_test.go @@ -7,52 +7,34 @@ import ( "strconv" "testing" - "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/caches/cachestest" ) func TestLRURobustness(t *testing.T) { - cachestest.AssertCacheRobustness(t, func() stored_requests.Cache { - return NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) + cachestest.AssertCacheRobustness(t, func() stored_requests.CacheJSON { + return NewCache(256*1024, -1, "TestData") }) } func TestUnboundedRobustness(t *testing.T) { - cachestest.AssertCacheRobustness(t, func() stored_requests.Cache { - return NewCache(&config.InMemoryCache{ - RequestCacheSize: 0, - ImpCacheSize: 0, - TTL: -1, - }) + cachestest.AssertCacheRobustness(t, func() stored_requests.CacheJSON { + return NewCache(0, -1, "TestData") }) } func TestRaceLRUConcurrency(t *testing.T) { - cache := NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) - + cache := NewCache(256*1024, -1, "TestData") doRaceTest(t, cache) } func TestRaceUnboundedConcurrency(t *testing.T) { - cache := NewCache(&config.InMemoryCache{ - RequestCacheSize: 0, - ImpCacheSize: 0, - TTL: -1, - }) + cache := NewCache(0, -1, "TestData") doRaceTest(t, cache) } -func doRaceTest(t *testing.T, cache stored_requests.Cache) { +func doRaceTest(t *testing.T, cache stored_requests.CacheJSON) { done := make(chan struct{}) sets := [][]int{rand.Perm(100), rand.Perm(100), rand.Perm(100)} @@ -70,26 +52,26 @@ func doRaceTest(t *testing.T, cache stored_requests.Cache) { } } -func readLots(cache stored_requests.Cache, done chan<- struct{}, reads []int) { +func readLots(cache stored_requests.CacheJSON, done chan<- struct{}, reads []int) { var s struct{} for _, i := range reads { - cache.Get(context.Background(), sliceForVal(i), sliceForVal(-i)) + cache.Get(context.Background(), sliceForVal(i)) } done <- s } -func writeLots(cache stored_requests.Cache, done chan<- struct{}, writes []int) { +func writeLots(cache stored_requests.CacheJSON, done chan<- struct{}, writes []int) { var s struct{} for _, i := range writes { - cache.Save(context.Background(), mapForVal(i), mapForVal(-i)) + cache.Save(context.Background(), mapForVal(i)) } done <- s } -func invalidateLots(cache stored_requests.Cache, done chan<- struct{}, invalidates []int) { +func invalidateLots(cache stored_requests.CacheJSON, done chan<- struct{}, invalidates []int) { var s struct{} for _, i := range invalidates { - cache.Invalidate(context.Background(), sliceForVal(i), sliceForVal(-i)) + cache.Invalidate(context.Background(), sliceForVal(i)) } done <- s } diff --git a/stored_requests/caches/nil_cache/nil_cache.go b/stored_requests/caches/nil_cache/nil_cache.go index de29156e3c9..d043ae55c96 100644 --- a/stored_requests/caches/nil_cache/nil_cache.go +++ b/stored_requests/caches/nil_cache/nil_cache.go @@ -8,13 +8,14 @@ import ( // NilCache is a no-op cache which does nothing useful. type NilCache struct{} -func (c *NilCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage) { - return nil, nil +func (c *NilCache) Get(ctx context.Context, ids []string) map[string]json.RawMessage { + return make(map[string]json.RawMessage) } -func (c *NilCache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) { + +func (c *NilCache) Save(ctx context.Context, data map[string]json.RawMessage) { return } -func (c *NilCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { +func (c *NilCache) Invalidate(ctx context.Context, ids []string) { return } diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go index e81d9667a73..6c663cbf0a2 100644 --- a/stored_requests/config/config.go +++ b/stored_requests/config/config.go @@ -178,10 +178,13 @@ func newFetcher(cfg *config.StoredRequests, client *http.Client, db *sql.DB) (fe func newCache(cfg *config.StoredRequests) stored_requests.Cache { if cfg.InMemoryCache.Type == "none" { glog.Infof("No Stored %s cache configured. The %s Fetcher backend will be used for all data requests", cfg.DataType(), cfg.DataType()) - return &nil_cache.NilCache{} + return stored_requests.Cache{&nil_cache.NilCache{}, &nil_cache.NilCache{}} } - return memory.NewCache(&cfg.InMemoryCache) + return stored_requests.Cache{ + Requests: memory.NewCache(cfg.InMemoryCache.RequestCacheSize, cfg.InMemoryCache.TTL, "Requests"), + Imps: memory.NewCache(cfg.InMemoryCache.ImpCacheSize, cfg.InMemoryCache.TTL, "Imps"), + } } func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, router *httprouter.Router) (eventProducers []events.EventProducer) { diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go index 4c3943ea5be..af6f4a4f514 100644 --- a/stored_requests/config/config_test.go +++ b/stored_requests/config/config_test.go @@ -102,8 +102,8 @@ func TestNewHTTPEvents(t *testing.T) { func TestNewEmptyCache(t *testing.T) { cache := newCache(&config.StoredRequests{InMemoryCache: config.InMemoryCache{Type: "none"}}) - cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}, nil) - reqs, _ := cache.Get(context.Background(), []string{"foo"}, nil) + cache.Requests.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}) + reqs := cache.Requests.Get(context.Background(), []string{"foo"}) if len(reqs) != 0 { t.Errorf("The newCache method should return an empty cache if the config asks for it.") } @@ -117,8 +117,8 @@ func TestNewInMemoryCache(t *testing.T) { ImpCacheSize: 100, }, }) - cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}, nil) - reqs, _ := cache.Get(context.Background(), []string{"foo"}, nil) + cache.Requests.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}) + reqs := cache.Requests.Get(context.Background(), []string{"foo"}) if len(reqs) != 1 { t.Errorf("The newCache method should return an in-memory cache if the config asks for it.") } diff --git a/stored_requests/events/api/api_test.go b/stored_requests/events/api/api_test.go index 64cf68b0a91..d2db4557573 100644 --- a/stored_requests/events/api/api_test.go +++ b/stored_requests/events/api/api_test.go @@ -9,22 +9,21 @@ import ( "strings" "testing" - "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/caches/memory" "github.com/prebid/prebid-server/stored_requests/events" ) func TestGoodRequests(t *testing.T) { - cache := memory.NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) - + cache := stored_requests.Cache{ + Requests: memory.NewCache(256*1024, -1, "Requests"), + Imps: memory.NewCache(256*1024, -1, "Imps"), + } id := "1" config := fmt.Sprintf(`{"id": "%s"}`, id) initialValue := map[string]json.RawMessage{id: json.RawMessage(config)} - cache.Save(context.Background(), initialValue, initialValue) + cache.Requests.Save(context.Background(), initialValue) + cache.Imps.Save(context.Background(), initialValue) apiEvents, endpoint := NewEventsAPI() @@ -51,7 +50,8 @@ func TestGoodRequests(t *testing.T) { } <-updateOccurred - reqData, impData := cache.Get(context.Background(), []string{id}, []string{id}) + reqData := cache.Requests.Get(context.Background(), []string{id}) + impData := cache.Imps.Get(context.Background(), []string{id}) assertHasValue(t, reqData, id, config) assertHasValue(t, impData, id, config) @@ -66,18 +66,17 @@ func TestGoodRequests(t *testing.T) { } <-invalidateOccurred - reqData, impData = cache.Get(context.Background(), []string{id}, []string{id}) + reqData = cache.Requests.Get(context.Background(), []string{id}) + impData = cache.Imps.Get(context.Background(), []string{id}) assertMapLength(t, 0, reqData) assertMapLength(t, 0, impData) } func TestBadRequests(t *testing.T) { - cache := memory.NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) - + cache := stored_requests.Cache{ + Requests: memory.NewCache(256*1024, -1, "Requests"), + Imps: memory.NewCache(256*1024, -1, "Imps"), + } apiEvents, endpoint := NewEventsAPI() listener := events.SimpleEventListener() go listener.Listen(cache, apiEvents) diff --git a/stored_requests/events/events.go b/stored_requests/events/events.go index ea67e8eeeb9..ba08f13d65b 100644 --- a/stored_requests/events/events.go +++ b/stored_requests/events/events.go @@ -61,12 +61,14 @@ func (e *EventListener) Listen(cache stored_requests.Cache, events EventProducer for { select { case save := <-events.Saves(): - cache.Save(context.Background(), save.Requests, save.Imps) + cache.Requests.Save(context.Background(), save.Requests) + cache.Imps.Save(context.Background(), save.Imps) if e.onSave != nil { e.onSave() } case invalidation := <-events.Invalidations(): - cache.Invalidate(context.Background(), invalidation.Requests, invalidation.Imps) + cache.Requests.Invalidate(context.Background(), invalidation.Requests) + cache.Imps.Invalidate(context.Background(), invalidation.Imps) if e.onInvalidate != nil { e.onInvalidate() } diff --git a/stored_requests/events/events_test.go b/stored_requests/events/events_test.go index 240a697592a..74263d14b93 100644 --- a/stored_requests/events/events_test.go +++ b/stored_requests/events/events_test.go @@ -7,7 +7,7 @@ import ( "reflect" "testing" - "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/caches/memory" ) @@ -16,12 +16,10 @@ func TestListen(t *testing.T) { saves: make(chan Save), invalidations: make(chan Invalidation), } - - cache := memory.NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) + cache := stored_requests.Cache{ + Requests: memory.NewCache(256*1024, -1, "Requests"), + Imps: memory.NewCache(256*1024, -1, "Imps"), + } // create channels to synchronize saveOccurred := make(chan struct{}) @@ -42,7 +40,8 @@ func TestListen(t *testing.T) { Requests: data, Imps: data, } - cache.Save(context.Background(), save.Requests, save.Imps) + cache.Requests.Save(context.Background(), save.Requests) + cache.Requests.Save(context.Background(), save.Imps) config = fmt.Sprintf(`{"id": "%s", "updated": true}`, id) data = map[string]json.RawMessage{id: json.RawMessage(config)} @@ -54,7 +53,8 @@ func TestListen(t *testing.T) { ep.saves <- save <-saveOccurred - requestData, impData := cache.Get(context.Background(), idSlice, idSlice) + requestData := cache.Requests.Get(context.Background(), idSlice) + impData := cache.Imps.Get(context.Background(), idSlice) if !reflect.DeepEqual(requestData, data) || !reflect.DeepEqual(impData, data) { t.Error("Update failed") } @@ -67,7 +67,8 @@ func TestListen(t *testing.T) { ep.invalidations <- invalidation <-invalidateOccurred - requestData, impData = cache.Get(context.Background(), idSlice, idSlice) + requestData = cache.Requests.Get(context.Background(), idSlice) + impData = cache.Imps.Get(context.Background(), idSlice) if len(requestData) > 0 || len(impData) > 0 { t.Error("Invalidate failed") } diff --git a/stored_requests/fetcher.go b/stored_requests/fetcher.go index a31b9989bd0..e9716e08a23 100644 --- a/stored_requests/fetcher.go +++ b/stored_requests/fetcher.go @@ -37,9 +37,9 @@ type CategoryFetcher interface { // 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) - FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) - FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) + Fetcher + AccountFetcher + CategoryFetcher } // NotFoundError is an error type to flag that an ID was not found by the Fetcher. @@ -62,7 +62,11 @@ func (e NotFoundError) Error() string { // Cache is an intermediate layer which can be used to create more complex Fetchers by composition. // Implementations must be safe for concurrent access by multiple goroutines. // To add a Cache layer in front of a Fetcher, see WithCache() -type Cache interface { +type Cache struct { + Requests CacheJSON + Imps CacheJSON +} +type CacheJSON interface { // Get works much like Fetcher.FetchRequests, with a few exceptions: // // 1. Any (actionable) errors should be logged by the implementation, rather than returned. @@ -73,37 +77,33 @@ type Cache interface { // // Nil slices and empty strings are treated as "no ops". That is, a nil requestID will always produce a nil // "stored request data" in the response. - Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) + Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) // Invalidate will ensure that all values associated with the given IDs // are no longer returned by the cache until new values are saved via Update - Invalidate(ctx context.Context, requestIDs []string, impIDs []string) + Invalidate(ctx context.Context, ids []string) // Save will add or overwrite the data in the cache at the given keys - Save(ctx context.Context, requestData map[string]json.RawMessage, impData map[string]json.RawMessage) + Save(ctx context.Context, data map[string]json.RawMessage) } // ComposedCache creates an interface to treat a slice of caches as a single cache -type ComposedCache []Cache +type ComposedCache []CacheJSON // Get will attempt to Get from the caches in the order in which they are in the slice, // stopping as soon as a value is found (or when all caches have been exhausted) -func (c ComposedCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) { - requestData = make(map[string]json.RawMessage, len(requestIDs)) - impData = make(map[string]json.RawMessage, len(impIDs)) +func (c ComposedCache) Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) { + data = make(map[string]json.RawMessage, len(ids)) - remainingReqIDs := requestIDs - remainingImpIDs := impIDs + remainingIDs := ids for _, cache := range c { - cachedReqData, cachedImpData := cache.Get(ctx, remainingReqIDs, remainingImpIDs) - - requestData, remainingReqIDs = updateFromCache(requestData, remainingReqIDs, cachedReqData) - impData, remainingImpIDs = updateFromCache(impData, remainingImpIDs, cachedImpData) + cachedData := cache.Get(ctx, remainingIDs) + data, remainingIDs = updateFromCache(data, remainingIDs, cachedData) - // return if all ids filled - if len(remainingReqIDs) == 0 && len(remainingImpIDs) == 0 { - return + // finish early if all ids filled + if len(remainingIDs) == 0 { + break } } @@ -129,16 +129,16 @@ func updateFromCache(data map[string]json.RawMessage, ids []string, newData map[ } // Invalidate will propagate invalidations to all underlying caches -func (c ComposedCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { +func (c ComposedCache) Invalidate(ctx context.Context, ids []string) { for _, cache := range c { - cache.Invalidate(ctx, requestIDs, impIDs) + cache.Invalidate(ctx, ids) } } // Save will propagate saves to all underlying caches -func (c ComposedCache) Save(ctx context.Context, requestData map[string]json.RawMessage, impData map[string]json.RawMessage) { +func (c ComposedCache) Save(ctx context.Context, data map[string]json.RawMessage) { for _, cache := range c { - cache.Save(ctx, requestData, impData) + cache.Save(ctx, data) } } @@ -148,7 +148,7 @@ type fetcherWithCache struct { metricsEngine pbsmetrics.MetricsEngine } -// WithCache returns a Fetcher which uses the given Cache before delegating to the original. +// WithCache returns a Fetcher which uses the given Caches before delegating to the original. // This can be called multiple times to compose Cache layers onto the backing Fetcher, though // it is usually more desirable to first compose caches with Compose, ensuring propagation of updates // and invalidations through all cache layers. @@ -161,7 +161,9 @@ func WithCache(fetcher AllFetcher, cache Cache, metricsEngine pbsmetrics.Metrics } func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) { - requestData, impData = f.cache.Get(ctx, requestIDs, impIDs) + + requestData = f.cache.Requests.Get(ctx, requestIDs) + impData = f.cache.Imps.Get(ctx, impIDs) // Fixes #311 leftoverImps := findLeftovers(impIDs, impData) @@ -178,7 +180,8 @@ func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []strin fetcherReqData, fetcherImpData, fetcherErrs := f.fetcher.FetchRequests(ctx, leftoverReqs, leftoverImps) errs = fetcherErrs - f.cache.Save(ctx, fetcherReqData, fetcherImpData) + f.cache.Requests.Save(ctx, fetcherReqData) + f.cache.Imps.Save(ctx, fetcherImpData) requestData = mergeData(requestData, fetcherReqData) impData = mergeData(impData, fetcherImpData) diff --git a/stored_requests/fetcher_test.go b/stored_requests/fetcher_test.go index 1928d1165db..396ba3d04b2 100644 --- a/stored_requests/fetcher_test.go +++ b/stored_requests/fetcher_test.go @@ -12,25 +12,27 @@ import ( "github.com/stretchr/testify/mock" ) -func setupFetcherWithCacheDeps() (*mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) { - cache := &mockCache{} +func setupFetcherWithCacheDeps() (*mockCache, *mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) { + reqCache := &mockCache{} + impCache := &mockCache{} metricsEngine := &pbsmetrics.MetricsEngineMock{} fetcher := &mockFetcher{} - afetcherWithCache := WithCache(fetcher, cache, metricsEngine) + afetcherWithCache := WithCache(fetcher, Cache{reqCache, impCache}, metricsEngine) - return cache, fetcher, afetcherWithCache, metricsEngine + return reqCache, impCache, fetcher, afetcherWithCache, metricsEngine } func TestPerfectCache(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"known"} reqIDs := []string{"req-id"} ctx := context.Background() - cache.On("Get", ctx, reqIDs, impIDs).Return( + reqCache.On("Get", ctx, reqIDs).Return( map[string]json.RawMessage{ "req-id": json.RawMessage(`{"req":true}`), - }, + }) + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{ "known": json.RawMessage(`{}`), }) @@ -41,7 +43,8 @@ func TestPerfectCache(t *testing.T) { reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, reqIDs, impIDs) - cache.AssertExpectations(t) + reqCache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.JSONEq(t, `{"req":true}`, string(reqData["req-id"]), "Fetch requests should fetch the right request data") @@ -50,15 +53,16 @@ func TestPerfectCache(t *testing.T) { } func TestImperfectCache(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"cached", "uncached"} ctx := context.Background() - cache.On("Get", ctx, []string(nil), impIDs).Return( - map[string]json.RawMessage{}, + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{ "cached": json.RawMessage(`true`), }) + reqCache.On("Get", ctx, []string(nil)).Return( + map[string]json.RawMessage{}) fetcher.On("FetchRequests", ctx, []string{}, []string{"uncached"}).Return( map[string]json.RawMessage{}, @@ -67,11 +71,11 @@ func TestImperfectCache(t *testing.T) { }, []error{}, ) - cache.On("Save", ctx, - map[string]json.RawMessage{}, + impCache.On("Save", ctx, map[string]json.RawMessage{ "uncached": json.RawMessage(`false`), }) + reqCache.On("Save", ctx, map[string]json.RawMessage{}) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0) metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 1) @@ -79,7 +83,7 @@ func TestImperfectCache(t *testing.T) { reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, impIDs) - cache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, reqData, 0, "Fetch requests should return nil if no request IDs were passed") @@ -89,14 +93,15 @@ func TestImperfectCache(t *testing.T) { } func TestMissingData(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"unknown"} ctx := context.Background() - cache.On("Get", ctx, []string(nil), impIDs).Return( - map[string]json.RawMessage{}, + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{}, ) + reqCache.On("Get", ctx, []string(nil)).Return( + map[string]json.RawMessage{}) fetcher.On("FetchRequests", ctx, []string{}, impIDs).Return( map[string]json.RawMessage{}, map[string]json.RawMessage{}, @@ -104,8 +109,10 @@ func TestMissingData(t *testing.T) { errors.New("Data not found"), }, ) - cache.On("Save", ctx, + impCache.On("Save", ctx, map[string]json.RawMessage{}, + ) + reqCache.On("Save", ctx, map[string]json.RawMessage{}, ) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0) @@ -115,7 +122,8 @@ func TestMissingData(t *testing.T) { reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, impIDs) - cache.AssertExpectations(t) + reqCache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, errs, 1, "FetchRequests for missing data should return an error") @@ -125,15 +133,16 @@ func TestMissingData(t *testing.T) { // Prevents #311 func TestCacheSaves(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"abc", "abc"} ctx := context.Background() - cache.On("Get", ctx, []string(nil), impIDs).Return( - map[string]json.RawMessage{}, + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{ "abc": json.RawMessage(`{}`), }) + reqCache.On("Get", ctx, []string(nil)).Return( + map[string]json.RawMessage{}) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0) metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 2) @@ -141,7 +150,7 @@ func TestCacheSaves(t *testing.T) { _, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, []string{"abc", "abc"}) - cache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, impData, 1, "FetchRequests should return data only once for duplicate requests") @@ -154,38 +163,34 @@ func TestComposedCache(t *testing.T) { c2 := &mockCache{} c3 := &mockCache{} c4 := &mockCache{} - cache := ComposedCache{c1, c2, c3, c4} + impCache := &mockCache{} + cache := Cache{ + Requests: ComposedCache{c1, c2, c3, c4}, + Imps: impCache, + } metricsEngine := &pbsmetrics.MetricsEngineMock{} fetcher := &mockFetcher{} aFetcherWithCache := WithCache(fetcher, cache, metricsEngine) - impIDs := []string{"1", "2", "3"} reqIDs := []string{"1", "2", "3"} + impIDs := []string{} ctx := context.Background() - c1.On("Get", ctx, reqIDs, impIDs).Return( - map[string]json.RawMessage{ - "1": json.RawMessage(`{"id": "1"}`), - }, + c1.On("Get", ctx, reqIDs).Return( map[string]json.RawMessage{ "1": json.RawMessage(`{"id": "1"}`), }) - c2.On("Get", ctx, []string{"2", "3"}, []string{"2", "3"}).Return( - map[string]json.RawMessage{ - "2": json.RawMessage(`{"id": "2"}`), - }, + c2.On("Get", ctx, []string{"2", "3"}).Return( map[string]json.RawMessage{ "2": json.RawMessage(`{"id": "2"}`), }) - c3.On("Get", ctx, []string{"3"}, []string{"3"}).Return( - map[string]json.RawMessage{ - "3": json.RawMessage(`{"id": "3"}`), - }, + c3.On("Get", ctx, []string{"3"}).Return( map[string]json.RawMessage{ "3": json.RawMessage(`{"id": "3"}`), }) + impCache.On("Get", ctx, []string{}).Return(map[string]json.RawMessage{}) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 3) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0) - metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 3) + metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 0) metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheMiss, 0) reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, reqIDs, impIDs) @@ -193,14 +198,12 @@ func TestComposedCache(t *testing.T) { c1.AssertExpectations(t) c2.AssertExpectations(t) c3.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, reqData, len(reqIDs), "FetchRequests should be able to return all request data from a composed cache") assert.Len(t, impData, len(impIDs), "FetchRequests should be able to return all imp data from a composed cache") assert.Len(t, errs, 0, "FetchRequests shouldn't return an error when trying to use a composed cache") - assert.JSONEq(t, `{"id": "1"}`, string(impData["1"]), "FetchRequests should fetch the right imp data") - assert.JSONEq(t, `{"id": "2"}`, string(impData["2"]), "FetchRequests should fetch the right imp data") - assert.JSONEq(t, `{"id": "3"}`, string(impData["3"]), "FetchRequests should fetch the right imp data") assert.JSONEq(t, `{"id": "1"}`, string(reqData["1"]), "FetchRequests should fetch the right req data") assert.JSONEq(t, `{"id": "2"}`, string(reqData["2"]), "FetchRequests should fetch the right req data") assert.JSONEq(t, `{"id": "3"}`, string(reqData["3"]), "FetchRequests should fetch the right req data") @@ -228,15 +231,15 @@ type mockCache struct { mock.Mock } -func (c *mockCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage) { - args := c.Called(ctx, requestIDs, impIDs) - return args.Get(0).(map[string]json.RawMessage), args.Get(1).(map[string]json.RawMessage) +func (c *mockCache) Get(ctx context.Context, ids []string) map[string]json.RawMessage { + args := c.Called(ctx, ids) + return args.Get(0).(map[string]json.RawMessage) } -func (c *mockCache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) { - c.Called(ctx, storedRequests, storedImps) +func (c *mockCache) Save(ctx context.Context, data map[string]json.RawMessage) { + c.Called(ctx, data) } -func (c *mockCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { - c.Called(ctx, requestIDs, impIDs) +func (c *mockCache) Invalidate(ctx context.Context, ids []string) { + c.Called(ctx, ids) } From 42e676570cf64c244b08b48efdec4de7d4995ec1 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Thu, 10 Sep 2020 13:26:43 -0400 Subject: [PATCH 200/381] Pass Through First Party Context Data (#1479) --- endpoints/openrtb2/auction.go | 16 +- endpoints/openrtb2/auction_test.go | 95 +++++++++- ...valid-context-allowed-with-ext-bidder.json | 32 ++++ ...id-context-allowed-with-prebid-bidder.json | 36 ++++ ...rstpartydata-imp-ext-multiple-bidders.json | 173 +++++++++++++++++ ...ydata-imp-ext-multiple-prebid-bidders.json | 179 ++++++++++++++++++ .../firstpartydata-imp-ext-one-bidder.json | 103 ++++++++++ ...stpartydata-imp-ext-one-prebid-bidder.json | 108 +++++++++++ exchange/utils.go | 38 ++-- openrtb_ext/bidders.go | 3 + openrtb_ext/bidders_test.go | 5 + openrtb_ext/request.go | 4 + 12 files changed, 769 insertions(+), 23 deletions(-) create mode 100644 endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json create mode 100644 endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json create mode 100644 exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json create mode 100644 exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json create mode 100644 exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json create mode 100644 exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index bc0cd90073f..41c1c1677a5 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -765,8 +765,8 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st } // Also accept bidder exts within imp[...].ext.prebid.bidder - // 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} + // NOTE: This is not part of the official API yet, so we are not expecting clients + // to migrate from imp[...].ext.${BIDDER} to imp[...].ext.prebid.bidder.${BIDDER} // at this time // https://github.com/prebid/prebid-server/pull/846#issuecomment-476352224 if rawPrebidExt, ok := bidderExts[openrtb_ext.PrebidExtKey]; ok { @@ -785,7 +785,7 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st /* Process all the bidder exts in the request */ disabledBidders := []string{} for bidder, ext := range bidderExts { - if bidder != openrtb_ext.PrebidExtKey { + if isBidderToValidate(bidder) { coreBidder := bidder if tmp, isAlias := aliases[bidder]; isAlias { coreBidder = tmp @@ -820,12 +820,20 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st // TODO #713 Fix this here if len(bidderExts) < 1 { errL = append(errL, fmt.Errorf("request.imp[%d].ext must contain at least one bidder", impIndex)) - return errL } return errL } +func isBidderToValidate(bidder string) bool { + // PrebidExtKey is a special case for the prebid config section and is not considered a bidder. + + // FirstPartyDataContextExtKey is a special case for the first party data context section + // and is not considered a bidder. + + return bidder != openrtb_ext.PrebidExtKey && bidder != openrtb_ext.FirstPartyDataContextExtKey +} + func (deps *endpointDeps) parseBidExt(ext json.RawMessage) (*openrtb_ext.ExtRequest, error) { if len(ext) < 1 { return nil, nil diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 53fea2e0500..7dc244a28c3 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -141,6 +141,16 @@ func TestGoodRequests(t *testing.T) { supplementary.assert(t) } +func TestFirstPartyDataRequests(t *testing.T) { + validRequests := &getResponseFromDirectory{ + dir: "sample-requests/first-party-data", + payloadGetter: getRequestPayload, + messageGetter: nilReturner, + expectedCode: http.StatusOK, + } + validRequests.assert(t) +} + // TestGoodNativeRequests makes sure we return 200s on well-formed Native requests. func TestGoodNativeRequests(t *testing.T) { tests := &getResponseFromDirectory{ @@ -1127,10 +1137,73 @@ func TestDisabledBidder(t *testing.T) { } } -func TestValidateImpExtDisabledBidder(t *testing.T) { - imp := &openrtb.Imp{ - Ext: json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}`), +func TestValidateImpExt(t *testing.T) { + testCases := []struct { + description string + impExt json.RawMessage + expectedImpExt string + expectedErrs []error + }{ + { + description: "Empty", + impExt: nil, + expectedImpExt: "", + expectedErrs: []error{errors.New("request.imp[0].ext is required")}, + }, + { + description: "Valid Bidder", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555}}`), + expectedImpExt: `{"appnexus":{"placement_id":555}}`, + expectedErrs: []error{}, + }, + { + description: "Valid Bidder + Disabled Bidder", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}`), + expectedImpExt: `{"appnexus":{"placement_id":555}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, + }, + { + description: "Valid Bidder + Disabled Bidder + First Party Data Context", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, + }, + { + description: "Valid Bidder + First Party Data Context", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{}, + }, + { + description: "Valid Prebid Ext Bidder", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`, + expectedErrs: []error{}, + // request.imp[x].ext.prebid.bidder.{biddername} is only promoted/copied to request.ext.{biddername} if there is at least one disabled bidder. + }, + { + description: "Valid Prebid Ext Bidder + First Party Data Context", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}} ,"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{}, + // request.imp[x].ext.prebid.bidder.{biddername} is only promoted/copied to request.ext.{biddername} if there is at least one disabled bidder. + }, + { + description: "Valid Prebid Ext Bidder + Disabled Bidder", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555},"unknownbidder":{"foo":"bar"}}},"appnexus":{"placement_id":555}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, + // request.imp[x].ext.prebid.bidder.{biddername} disabled bidders are not removed. if there is a disabled bidder, the valid ones are promoted/copied to request.ext.{biddername}. + }, + { + description: "Valid Prebid Ext Bidder + Disabled Bidder + First Party Data Context", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555},"unknownbidder":{"foo":"bar"}}},"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, + // request.imp[x].ext.prebid.bidder.{biddername} disabled bidders are not removed. if there is a disabled bidder, the valid ones are promoted/copied to request.ext.{biddername}. + }, } + deps := &endpointDeps{ &nobidExchange{}, newParamsValidator(t), @@ -1149,9 +1222,19 @@ func TestValidateImpExtDisabledBidder(t *testing.T) { nil, hardcodedResponseIPValidator{response: true}, } - errs := deps.validateImpExt(imp, nil, 0) - assert.JSONEq(t, `{"appnexus":{"placement_id":555}}`, string(imp.Ext)) - assert.Equal(t, []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, errs) + + for _, test := range testCases { + imp := &openrtb.Imp{Ext: test.impExt} + + errs := deps.validateImpExt(imp, nil, 0) + + if len(test.expectedImpExt) > 0 { + assert.JSONEq(t, test.expectedImpExt, string(imp.Ext)) + } else { + assert.Empty(t, imp.Ext) + } + assert.Equal(t, test.expectedErrs, errs) + } } func validRequest(t *testing.T, filename string) string { diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json new file mode 100644 index 00000000000..aa205fc55ce --- /dev/null +++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json @@ -0,0 +1,32 @@ +{ + "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.", + + "requestPayload": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } +} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json new file mode 100644 index 00000000000..1616e84b416 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json @@ -0,0 +1,36 @@ +{ + "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.", + + "requestPayload": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + } + } + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json new file mode 100644 index 00000000000..8004c3c2646 --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json @@ -0,0 +1,173 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }, { + "seat": "rubicon", + "bid": [{ + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json new file mode 100644 index 00000000000..d62afccf426 --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json @@ -0,0 +1,179 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "prebid": {}, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + }, + "prebid": {}, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }, { + "seat": "rubicon", + "bid": [{ + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json new file mode 100644 index 00000000000..6f0bab9529c --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json @@ -0,0 +1,103 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "appnexus": { + "placementId": 1 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json new file mode 100644 index 00000000000..1610b9ea47e --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json @@ -0,0 +1,108 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "prebid": {}, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/utils.go b/exchange/utils.go index 2e9e4dc8f80..5863f6c8530 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -273,13 +273,18 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) { imp := imps[i] impExt := impExts[i] + var firstPartyDataContext json.RawMessage + if context, exists := impExt[openrtb_ext.FirstPartyDataContextExtKey]; exists { + firstPartyDataContext = context + } + rawPrebidExt, ok := impExt[openrtb_ext.PrebidExtKey] if ok { var prebidExt openrtb_ext.ExtImpPrebid if err := json.Unmarshal(rawPrebidExt, &prebidExt); err == nil && prebidExt.Bidder != nil { - if errs := sanitizedImpCopy(&imp, prebidExt.Bidder, rawPrebidExt, &splitImps); errs != nil { + if errs := sanitizedImpCopy(&imp, prebidExt.Bidder, rawPrebidExt, firstPartyDataContext, &splitImps); errs != nil { errList = append(errList, errs...) } @@ -287,7 +292,7 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) { } } - if errs := sanitizedImpCopy(&imp, impExt, rawPrebidExt, &splitImps); errs != nil { + if errs := sanitizedImpCopy(&imp, impExt, rawPrebidExt, firstPartyDataContext, &splitImps); errs != nil { errList = append(errList, errs...) } } @@ -295,35 +300,38 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) { return splitImps, nil } -// sanitizedImpCopy returns a copy of imp with its ext filtered so that only "prebid" and bidder params exist. +// sanitizedImpCopy returns a copy of imp with its ext filtered so that only "prebid", "context", and bidder params exist. // It will not mutate the input imp. // This function will write the new imps to the output map passed in func sanitizedImpCopy(imp *openrtb.Imp, bidderExts map[string]json.RawMessage, rawPrebidExt json.RawMessage, + firstPartyDataContext json.RawMessage, out *map[string][]openrtb.Imp) []error { var prebidExt map[string]json.RawMessage var errs []error - // We don't want to include other demand partners' bidder params - // in the sanitized imp if err := json.Unmarshal(rawPrebidExt, &prebidExt); err == nil { - delete(prebidExt, "bidder") - - var err error - if rawPrebidExt, err = json.Marshal(prebidExt); err != nil { - errs = append(errs, err) + // Remove the entire bidder field. We will already have the content we need in bidderExts. We + // don't want to include other demand partners' bidder params in the sanitized imp. + if _, hasBidderField := prebidExt["bidder"]; hasBidderField { + delete(prebidExt, "bidder") + + var err error + if rawPrebidExt, err = json.Marshal(prebidExt); err != nil { + errs = append(errs, err) + } } } for bidder, ext := range bidderExts { - if bidder == openrtb_ext.PrebidExtKey { + if bidder == openrtb_ext.PrebidExtKey || bidder == openrtb_ext.FirstPartyDataContextExtKey { continue } impCopy := *imp - newExt := make(map[string]json.RawMessage, 2) + newExt := make(map[string]json.RawMessage, 3) newExt["bidder"] = ext @@ -331,6 +339,10 @@ func sanitizedImpCopy(imp *openrtb.Imp, newExt[openrtb_ext.PrebidExtKey] = rawPrebidExt } + if len(firstPartyDataContext) > 0 { + newExt[openrtb_ext.FirstPartyDataContextExtKey] = firstPartyDataContext + } + rawExt, err := json.Marshal(newExt) if err != nil { errs = append(errs, err) @@ -392,7 +404,7 @@ func resolveBidder(bidder string, aliases map[string]string) openrtb_ext.BidderN } // parseImpExts does a partial-unmarshal of the imp[].Ext field. -// The keys in the returned map are expected to be "prebid", core BidderNames, or Aliases for this request. +// The keys in the returned map are expected to be "prebid", "context", core BidderNames, or Aliases for this request. func parseImpExts(imps []openrtb.Imp) ([]map[string]json.RawMessage, error) { exts := make([]map[string]json.RawMessage, len(imps)) // Loop over every impression in the request diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 761f53d441e..876eeab86bd 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -20,6 +20,9 @@ type BidderName string // BidderNameGeneral is reserved for non-bidder specific messages when using a map keyed on the bidder name. const BidderNameGeneral = BidderName("general") +// BidderNameContext is reserved for first party data. +const BidderNameContext = BidderName("context") + // 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. diff --git a/openrtb_ext/bidders_test.go b/openrtb_ext/bidders_test.go index d49b23237ed..9f05f526905 100644 --- a/openrtb_ext/bidders_test.go +++ b/openrtb_ext/bidders_test.go @@ -61,3 +61,8 @@ func TestBidderListDoesNotDefineGeneral(t *testing.T) { bidders := BidderList() assert.NotContains(t, bidders, BidderNameGeneral) } + +func TestBidderListDoesNotDefineContext(t *testing.T) { + bidders := BidderList() + assert.NotContains(t, bidders, BidderNameContext) +} diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index d6edf47f939..42ac9d9d4b9 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -5,6 +5,10 @@ import ( "errors" ) +// FirstPartyDataContextExtKey defines the field name within bidrequest.ext reserved +// for first party data support. +const FirstPartyDataContextExtKey string = "context" + // ExtRequest defines the contract for bidrequest.ext type ExtRequest struct { Prebid ExtRequestPrebid `json:"prebid"` From fa23f5c226df99a9a4ef318100fdb7d84d3e40fa Mon Sep 17 00:00:00 2001 From: hdeodhar <35999856+hdeodhar@users.noreply.github.com> Date: Thu, 10 Sep 2020 22:33:14 +0100 Subject: [PATCH 201/381] Added new size 640x360 (Id: 198) (#1490) --- adapters/rubicon/rubicon.go | 1 + 1 file changed, 1 insertion(+) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 56ae7b2f792..7d6e0e12039 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -236,6 +236,7 @@ var rubiSizeMap = map[rubiSize]int{ {w: 800, h: 250}: 125, {w: 200, h: 600}: 126, {w: 640, h: 320}: 156, + {w: 640, h: 360}: 198, } // defines the contract for bidrequest.user.ext.eids[i].ext From 65c6c3608d0cca20168a69c8cf14d043a0d39a45 Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Mon, 14 Sep 2020 07:19:40 -0700 Subject: [PATCH 202/381] Refactor: move getAccount to accounts package (from openrtb2) (#1483) --- account/account.go | 69 +++++++++++++++++++++ account/account_test.go | 94 +++++++++++++++++++++++++++++ endpoints/openrtb2/amp_auction.go | 3 +- endpoints/openrtb2/auction.go | 56 +---------------- endpoints/openrtb2/auction_test.go | 70 +-------------------- endpoints/openrtb2/video_auction.go | 3 +- 6 files changed, 170 insertions(+), 125 deletions(-) create mode 100644 account/account.go create mode 100644 account/account_test.go diff --git a/account/account.go b/account/account.go new file mode 100644 index 00000000000..2f27b61efab --- /dev/null +++ b/account/account.go @@ -0,0 +1,69 @@ +package account + +import ( + "context" + "encoding/json" + "fmt" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/pbsmetrics" + "github.com/prebid/prebid-server/stored_requests" +) + +// GetAccount looks up the config.Account object referenced by the given accountID, with access rules applied +func GetAccount(ctx context.Context, cfg *config.Configuration, fetcher stored_requests.AccountFetcher, accountID string) (account *config.Account, errs []error) { + // Check BlacklistedAcctMap until we have deprecated it + if _, found := cfg.BlacklistedAcctMap[accountID]; found { + return nil, []error{&errortypes.BlacklistedAcct{ + Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", accountID), + }} + } + if cfg.AccountRequired && accountID == pbsmetrics.PublisherUnknown { + return nil, []error{&errortypes.AcctRequired{ + Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."), + }} + } + if accountJSON, accErrs := fetcher.FetchAccount(ctx, accountID); len(accErrs) > 0 || accountJSON == nil { + // accountID does not reference a valid account + for _, e := range accErrs { + if _, ok := e.(stored_requests.NotFoundError); !ok { + errs = append(errs, e) + } + } + if cfg.AccountRequired && cfg.AccountDefaults.Disabled { + errs = append(errs, &errortypes.AcctRequired{ + Message: fmt.Sprintf("Prebid-server could not verify the Account ID. Please reach out to the prebid server host."), + }) + return nil, errs + } + // Make a copy of AccountDefaults instead of taking a reference, + // to preserve original accountID in case is needed to check NonStandardPublisherMap + pubAccount := cfg.AccountDefaults + pubAccount.ID = accountID + account = &pubAccount + } else { + // accountID resolved to a valid account, merge with AccountDefaults for a complete config + account = &config.Account{} + completeJSON, err := jsonpatch.MergePatch(cfg.AccountDefaultsJSON(), accountJSON) + if err == nil { + err = json.Unmarshal(completeJSON, account) + } + if err != nil { + errs = append(errs, err) + return nil, errs + } + // Fill in ID if needed, so it can be left out of account definition + if len(account.ID) == 0 { + account.ID = accountID + } + } + if account.Disabled { + errs = append(errs, &errortypes.BlacklistedAcct{ + Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", accountID), + }) + return nil, errs + } + return account, nil +} diff --git a/account/account_test.go b/account/account_test.go new file mode 100644 index 00000000000..0d192f18510 --- /dev/null +++ b/account/account_test.go @@ -0,0 +1,94 @@ +package account + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/pbsmetrics" + "github.com/prebid/prebid-server/stored_requests" + "github.com/stretchr/testify/assert" +) + +var mockAccountData = map[string]json.RawMessage{ + "valid_acct": json.RawMessage(`{"disabled":false}`), + "disabled_acct": json.RawMessage(`{"disabled":true}`), +} + +type mockAccountFetcher struct { +} + +func (af mockAccountFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + if account, ok := mockAccountData[accountID]; ok { + return account, nil + } + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} +} + +func TestGetAccount(t *testing.T) { + unknown := pbsmetrics.PublisherUnknown + testCases := []struct { + accountID string + // account_required + required bool + // account_defaults.disabled + disabled bool + // expected error, or nil if account should be found + err error + }{ + // Blacklisted account is always rejected even in permissive setup + {accountID: "bad_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}}, + + // empty pubID + {accountID: unknown, required: false, disabled: false, err: nil}, + {accountID: unknown, required: true, disabled: false, err: &errortypes.AcctRequired{}}, + {accountID: unknown, required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, + {accountID: unknown, required: true, disabled: true, err: &errortypes.AcctRequired{}}, + + // pubID given but is not a valid host account (does not exist) + {accountID: "doesnt_exist_acct", required: false, disabled: false, err: nil}, + {accountID: "doesnt_exist_acct", required: true, disabled: false, err: nil}, + {accountID: "doesnt_exist_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, + {accountID: "doesnt_exist_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}}, + + // pubID given and matches a valid host account with Disabled: false + {accountID: "valid_acct", required: false, disabled: false, err: nil}, + {accountID: "valid_acct", required: true, disabled: false, err: nil}, + {accountID: "valid_acct", required: false, disabled: true, err: nil}, + {accountID: "valid_acct", required: true, disabled: true, err: nil}, + + // pubID given and matches a host account explicitly disabled (Disabled: true on account json) + {accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}}, + {accountID: "disabled_acct", required: true, disabled: false, err: &errortypes.BlacklistedAcct{}}, + {accountID: "disabled_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, + {accountID: "disabled_acct", required: true, disabled: true, err: &errortypes.BlacklistedAcct{}}, + } + + for _, test := range testCases { + description := fmt.Sprintf(`ID=%s/required=%t/disabled=%t`, test.accountID, test.required, test.disabled) + t.Run(description, func(t *testing.T) { + cfg := &config.Configuration{ + BlacklistedAcctMap: map[string]bool{"bad_acct": true}, + AccountRequired: test.required, + AccountDefaults: config.Account{Disabled: test.disabled}, + } + fetcher := &mockAccountFetcher{} + assert.NoError(t, cfg.MarshalAccountDefaults()) + + account, errors := GetAccount(context.Background(), cfg, fetcher, test.accountID) + + if test.err == nil { + assert.Empty(t, errors) + assert.Equal(t, test.accountID, account.ID, "account.ID must match requested ID") + assert.Equal(t, false, account.Disabled, "returned account must not be disabled") + } else { + assert.NotEmpty(t, errors, "expected errors but got success") + assert.Nil(t, account, "return account must be nil on error") + assert.IsType(t, test.err, errors[0], "error is of unexpected type") + } + }) + } +} diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 54f4706902d..1e92569e260 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -16,6 +16,7 @@ import ( "github.com/golang/glog" "github.com/julienschmidt/httprouter" "github.com/mxmCherry/openrtb" + accountService "github.com/prebid/prebid-server/account" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" @@ -158,7 +159,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h } labels.PubID = getAccountID(req.Site.Publisher) // Look up account now that we have resolved the pubID value - account, acctIDErrs := deps.getAccount(ctx, labels.PubID) + account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID) if len(acctIDErrs) > 0 { errL = append(errL, acctIDErrs...) httpStatus := http.StatusBadRequest diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 41c1c1677a5..d6cbc2285fb 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -22,6 +22,7 @@ import ( "github.com/mxmCherry/openrtb" "github.com/mxmCherry/openrtb/native" nativeRequests "github.com/mxmCherry/openrtb/native/request" + accountService "github.com/prebid/prebid-server/account" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" @@ -156,7 +157,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http } // Look up account now that we have resolved the pubID value - account, acctIDErrs := deps.getAccount(ctx, labels.PubID) + account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID) if len(acctIDErrs) > 0 { errL = append(errL, acctIDErrs...) writeError(errL, w, &labels) @@ -1317,56 +1318,3 @@ func getAccountID(pub *openrtb.Publisher) string { } return pbsmetrics.PublisherUnknown } - -func (deps *endpointDeps) getAccount(ctx context.Context, pubID string) (account *config.Account, errs []error) { - // Check BlacklistedAcctMap until we have deprecated it - if _, found := deps.cfg.BlacklistedAcctMap[pubID]; found { - return nil, []error{&errortypes.BlacklistedAcct{ - Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", pubID), - }} - } - if deps.cfg.AccountRequired && pubID == pbsmetrics.PublisherUnknown { - return nil, []error{&errortypes.AcctRequired{ - Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."), - }} - } - if accountJSON, accErrs := deps.accounts.FetchAccount(ctx, pubID); len(accErrs) > 0 || accountJSON == nil { - // pubID does not reference a valid account - if len(accErrs) > 0 { - errs = append(errs, errs...) - } - if deps.cfg.AccountRequired && deps.cfg.AccountDefaults.Disabled { - errs = append(errs, &errortypes.AcctRequired{ - Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."), - }) - return nil, errs - } - // Make a copy of AccountDefaults instead of taking a reference, - // to preserve original pubID in case is needed to check NonStandardPublisherMap - pubAccount := deps.cfg.AccountDefaults - pubAccount.ID = pubID - account = &pubAccount - } else { - // pubID resolved to a valid account, merge with AccountDefaults for a complete config - account = &config.Account{} - completeJSON, err := jsonpatch.MergePatch(deps.cfg.AccountDefaultsJSON(), accountJSON) - if err == nil { - err = json.Unmarshal(completeJSON, account) - } - if err != nil { - errs = append(errs, err) - return nil, errs - } - // Fill in ID if needed, so it can be left out of account definition - if len(account.ID) == 0 { - account.ID = pubID - } - } - if account.Disabled { - errs = append(errs, &errortypes.BlacklistedAcct{ - Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", pubID), - }) - return nil, errs - } - return -} diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 7dc244a28c3..925cffcebeb 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1984,8 +1984,7 @@ func (cf mockStoredReqFetcher) FetchRequests(ctx context.Context, requestIDs []s } var mockAccountData = map[string]json.RawMessage{ - "valid_acct": json.RawMessage(`{"disabled":false}`), - "disabled_acct": json.RawMessage(`{"disabled":true}`), + "valid_acct": json.RawMessage(`{"disabled":false}`), } type mockAccountFetcher struct { @@ -2058,70 +2057,3 @@ type hardcodedResponseIPValidator struct { func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool { return v.response } - -func TestGetAccount(t *testing.T) { - unknown := pbsmetrics.PublisherUnknown - testCases := []struct { - accountID string - // account_required - required bool - // account_defaults.disabled - disabled bool - // expected error, or nil if account should be found - err error - }{ - // Blacklisted account is always rejected even in permissive setup - {accountID: "bad_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}}, - - // empty pubID - {accountID: unknown, required: false, disabled: false, err: nil}, - {accountID: unknown, required: true, disabled: false, err: &errortypes.AcctRequired{}}, - {accountID: unknown, required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, - {accountID: unknown, required: true, disabled: true, err: &errortypes.AcctRequired{}}, - - // pubID given but is not a valid host account (does not exist) - {accountID: "doesnt_exist_acct", required: false, disabled: false, err: nil}, - {accountID: "doesnt_exist_acct", required: true, disabled: false, err: nil}, - {accountID: "doesnt_exist_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, - {accountID: "doesnt_exist_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}}, - - // pubID given and matches a valid host account with Disabled: false - {accountID: "valid_acct", required: false, disabled: false, err: nil}, - {accountID: "valid_acct", required: true, disabled: false, err: nil}, - {accountID: "valid_acct", required: false, disabled: true, err: nil}, - {accountID: "valid_acct", required: true, disabled: true, err: nil}, - - // pubID given and matches a host account explicitly disabled (Disabled: true on account json) - {accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}}, - {accountID: "disabled_acct", required: true, disabled: false, err: &errortypes.BlacklistedAcct{}}, - {accountID: "disabled_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}}, - {accountID: "disabled_acct", required: true, disabled: true, err: &errortypes.BlacklistedAcct{}}, - } - - for _, test := range testCases { - description := fmt.Sprintf(`ID=%s/required=%t/disabled=%t`, test.accountID, test.required, test.disabled) - t.Run(description, func(t *testing.T) { - deps := &endpointDeps{ - cfg: &config.Configuration{ - BlacklistedAcctMap: map[string]bool{"bad_acct": true}, - AccountRequired: test.required, - AccountDefaults: config.Account{Disabled: test.disabled}, - }, - accounts: &mockAccountFetcher{}, - } - assert.NoError(t, deps.cfg.MarshalAccountDefaults()) - - account, errors := deps.getAccount(context.Background(), test.accountID) - - if test.err == nil { - assert.Empty(t, errors) - assert.Equal(t, test.accountID, account.ID, "account.ID must match requested ID") - assert.Equal(t, false, account.Disabled, "returned account must not be disabled") - } else { - assert.NotEmpty(t, errors, "expected errors but got success") - assert.Nil(t, account, "return account must be nil on error") - assert.IsType(t, test.err, errors[0], "error is of unexpected type") - } - }) - } -} diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index f5494751cc2..ab5634c7853 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -23,6 +23,7 @@ import ( "github.com/golang/glog" "github.com/julienschmidt/httprouter" "github.com/mxmCherry/openrtb" + accountService "github.com/prebid/prebid-server/account" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/exchange" @@ -255,7 +256,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } // Look up account now that we have resolved the pubID value - account, acctIDErrs := deps.getAccount(ctx, labels.PubID) + account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID) if len(acctIDErrs) > 0 { handleError(&labels, w, acctIDErrs, &vo, &debugLog) return From 58b356d5ad53b9eee1a40086eccb55ae00e36d1e Mon Sep 17 00:00:00 2001 From: ShriprasadM Date: Tue, 15 Sep 2020 12:37:55 +0530 Subject: [PATCH 203/381] OTT-18: moved VideoAuction to selector pattern. This required for mocking PBS response (#76) Co-authored-by: Shriprasad --- main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 0fa4454026b..802714590e0 100644 --- a/main.go +++ b/main.go @@ -48,9 +48,9 @@ func main() { */ func InitPrebidServer(configFile string) { - //init contents + //init contents rand.Seed(time.Now().UnixNano()) - + //main contents cfg, err := loadConfig(configFile) if err != nil { @@ -96,7 +96,7 @@ func OrtbAuction(w http.ResponseWriter, r *http.Request) error { return router.OrtbAuctionEndpointWrapper(w, r) } -func VideoAuction(w http.ResponseWriter, r *http.Request) error { +var VideoAuction = func(w http.ResponseWriter, r *http.Request) error { return router.VideoAuctionEndpointWrapper(w, r) } From e7d0babd32833e6fba144d020b4a09c0aacdeb50 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Tue, 15 Sep 2020 10:21:18 -0400 Subject: [PATCH 204/381] Fixed TCF2 Geo Only Enforcement (#1492) --- exchange/utils.go | 4 +- privacy/enforcement.go | 30 +++-- privacy/enforcement_test.go | 131 ++++++++++------------ privacy/scrubber.go | 52 +++++++-- privacy/scrubber_test.go | 218 +++++++++++++++--------------------- 5 files changed, 213 insertions(+), 222 deletions(-) diff --git a/exchange/utils.go b/exchange/utils.go index 5863f6c8530..22b28adcacb 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -107,12 +107,10 @@ func cleanOpenRTBRequests(ctx context.Context, coreBidder := resolveBidder(bidder.String(), aliases) var publisherID = labels.PubID - ok, geo, id, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) - privacyEnforcement.GDPR = !ok && err == nil + _, geo, id, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) privacyEnforcement.GDPRGeo = !geo && err == nil privacyEnforcement.GDPRID = !id && err == nil } else { - privacyEnforcement.GDPR = false privacyEnforcement.GDPRGeo = false privacyEnforcement.GDPRID = false } diff --git a/privacy/enforcement.go b/privacy/enforcement.go index 9c23c320680..3f157329cf6 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -8,7 +8,6 @@ import ( type Enforcement struct { CCPA bool COPPA bool - GDPR bool GDPRGeo bool GDPRID bool LMT bool @@ -16,7 +15,7 @@ type Enforcement struct { // Any returns true if at least one privacy policy requires enforcement. func (e Enforcement) Any() bool { - return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo || e.GDPRID || e.LMT + return e.CCPA || e.COPPA || e.GDPRGeo || e.GDPRID || e.LMT } // Apply cleans personally identifiable information from an OpenRTB bid request. @@ -26,17 +25,33 @@ func (e Enforcement) Apply(bidRequest *openrtb.BidRequest, ampGDPRException bool func (e Enforcement) apply(bidRequest *openrtb.BidRequest, ampGDPRException bool, scrubber Scrubber) { if bidRequest != nil && e.Any() { - bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) + bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(ampGDPRException), e.getGeoScrubStrategy()) } } +func (e Enforcement) getDeviceIDScrubStrategy() ScrubStrategyDeviceID { + if e.COPPA || e.GDPRID || e.CCPA || e.LMT { + return ScrubStrategyDeviceIDAll + } + + return ScrubStrategyDeviceIDNone +} + +func (e Enforcement) getIPv4ScrubStrategy() ScrubStrategyIPV4 { + if e.COPPA || e.GDPRGeo || e.CCPA || e.LMT { + return ScrubStrategyIPV4Lowest8 + } + + return ScrubStrategyIPV4None +} + func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 { if e.COPPA { return ScrubStrategyIPV6Lowest32 } - if e.GDPR || e.CCPA || e.LMT { + if e.GDPRGeo || e.CCPA || e.LMT { return ScrubStrategyIPV6Lowest16 } @@ -60,12 +75,11 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs return ScrubStrategyUserIDAndDemographic } - if e.GDPR && ampGDPRException { - return ScrubStrategyUserNone + if e.CCPA || e.LMT { + return ScrubStrategyUserID } - // If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above) - if e.CCPA || e.GDPRID || e.LMT { + if e.GDPRID && !ampGDPRException { return ScrubStrategyUserID } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index ef02e28147a..0cf36a614c4 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -19,7 +19,6 @@ func TestAny(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: false, GDPRGeo: false, GDPRID: false, LMT: false, @@ -31,7 +30,6 @@ func TestAny(t *testing.T) { enforcement: Enforcement{ CCPA: true, COPPA: true, - GDPR: true, GDPRGeo: true, GDPRID: true, LMT: true, @@ -43,7 +41,6 @@ func TestAny(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: true, - GDPR: false, GDPRGeo: false, GDPRID: false, LMT: true, @@ -63,6 +60,8 @@ func TestApply(t *testing.T) { description string enforcement Enforcement ampGDPRException bool + expectedDeviceID ScrubStrategyDeviceID + expectedDeviceIPv4 ScrubStrategyIPV4 expectedDeviceIPv6 ScrubStrategyIPV6 expectedDeviceGeo ScrubStrategyGeo expectedUser ScrubStrategyUser @@ -73,12 +72,12 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: true, COPPA: true, - GDPR: true, GDPRGeo: true, GDPRID: true, LMT: true, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, expectedUser: ScrubStrategyUserIDAndDemographic, @@ -89,12 +88,12 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: true, COPPA: false, - GDPR: false, GDPRGeo: false, GDPRID: false, LMT: false, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserID, @@ -105,124 +104,97 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: true, - GDPR: false, GDPRGeo: false, GDPRID: false, LMT: false, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoFull, }, { - description: "GDPR Only", + description: "GDPR Only - Full", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: true, GDPRGeo: true, GDPRID: true, LMT: false, }, ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserID, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { - description: "GDPR Only, ampGDPRException", + description: "GDPR Only - Full - AMP Exception", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: true, GDPRGeo: true, GDPRID: true, LMT: false, }, ampGDPRException: true, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserNone, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { - description: "CCPA Only, ampGDPRException", - enforcement: Enforcement{ - CCPA: true, - COPPA: false, - GDPR: false, - GDPRGeo: false, - GDPRID: false, - LMT: false, - }, - ampGDPRException: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserID, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - }, - { - description: "COPPA and GDPR, ampGDPRException", - enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: true, - GDPRGeo: true, - GDPRID: true, - LMT: false, - }, - ampGDPRException: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, - expectedDeviceGeo: ScrubStrategyGeoFull, - expectedUser: ScrubStrategyUserIDAndDemographic, - expectedUserGeo: ScrubStrategyGeoFull, - }, - { - description: "GDPR Only, no Geo", + description: "GDPR Only - ID Only", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: true, GDPRGeo: false, GDPRID: true, LMT: false, }, ampGDPRException: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4None, + expectedDeviceIPv6: ScrubStrategyIPV6None, expectedDeviceGeo: ScrubStrategyGeoNone, expectedUser: ScrubStrategyUserID, expectedUserGeo: ScrubStrategyGeoNone, }, { - description: "GDPR Only, Geo only", + description: "GDPR Only - ID Only - AMP Exception", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: false, - GDPRGeo: true, - GDPRID: false, + GDPRGeo: false, + GDPRID: true, LMT: false, }, - ampGDPRException: false, + ampGDPRException: true, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4None, expectedDeviceIPv6: ScrubStrategyIPV6None, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + expectedDeviceGeo: ScrubStrategyGeoNone, expectedUser: ScrubStrategyUserNone, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, + expectedUserGeo: ScrubStrategyGeoNone, }, { - description: "GDPR Only, ID exception", + description: "GDPR Only - Geo Only", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: true, GDPRGeo: true, GDPRID: false, LMT: false, }, ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDNone, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserNone, @@ -233,32 +205,50 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: false, GDPRGeo: false, GDPRID: false, LMT: true, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserID, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { - description: "LMT Only, ampGDPRException", + description: "Interactions: COPPA Only + AMP Exception", enforcement: Enforcement{ CCPA: false, - COPPA: false, - GDPR: false, + COPPA: true, GDPRGeo: false, GDPRID: false, - LMT: true, + LMT: false, }, ampGDPRException: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserID, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceGeo: ScrubStrategyGeoFull, + expectedUser: ScrubStrategyUserIDAndDemographic, + expectedUserGeo: ScrubStrategyGeoFull, + }, + { + description: "Interactions: COPPA + GDPR Full + AMP Exception", + enforcement: Enforcement{ + CCPA: false, + COPPA: true, + GDPRGeo: true, + GDPRID: true, + LMT: false, + }, + ampGDPRException: true, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceGeo: ScrubStrategyGeoFull, + expectedUser: ScrubStrategyUserIDAndDemographic, + expectedUserGeo: ScrubStrategyGeoFull, }, } @@ -271,7 +261,7 @@ func TestApply(t *testing.T) { replacedUser := &openrtb.User{} m := &mockScrubber{} - m.On("ScrubDevice", req.Device, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(replacedDevice).Once() + m.On("ScrubDevice", req.Device, test.expectedDeviceID, test.expectedDeviceIPv4, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(replacedDevice).Once() m.On("ScrubUser", req.User, test.expectedUser, test.expectedUserGeo).Return(replacedUser).Once() test.enforcement.apply(req, test.ampGDPRException, m) @@ -290,7 +280,6 @@ func TestApplyNoneApplicable(t *testing.T) { enforcement := Enforcement{ CCPA: false, COPPA: false, - GDPR: false, GDPRGeo: false, GDPRID: false, LMT: false, @@ -315,8 +304,8 @@ type mockScrubber struct { mock.Mock } -func (m *mockScrubber) ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { - args := m.Called(device, ipv6, geo) +func (m *mockScrubber) ScrubDevice(device *openrtb.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { + args := m.Called(device, id, ipv4, ipv6, geo) return args.Get(0).(*openrtb.Device) } diff --git a/privacy/scrubber.go b/privacy/scrubber.go index 655436838e6..8771c8b3282 100644 --- a/privacy/scrubber.go +++ b/privacy/scrubber.go @@ -7,6 +7,17 @@ import ( "github.com/mxmCherry/openrtb" ) +// ScrubStrategyIPV4 defines the approach to scrub PII from an IPV4 address. +type ScrubStrategyIPV4 int + +const ( + // ScrubStrategyIPV4None does not remove any part of an IPV4 address. + ScrubStrategyIPV4None ScrubStrategyIPV4 = iota + + // ScrubStrategyIPV4Lowest8 zeroes out the last 8 bits of an IPV4 address. + ScrubStrategyIPV4Lowest8 +) + // ScrubStrategyIPV6 defines the approach to scrub PII from an IPV6 address. type ScrubStrategyIPV6 int @@ -49,9 +60,20 @@ const ( ScrubStrategyUserID ) +// ScrubStrategyDeviceID defines the approach to remove hardware id and device id data. +type ScrubStrategyDeviceID int + +const ( + // ScrubStrategyDeviceIDNone does not remove hardware id and device id data. + ScrubStrategyDeviceIDNone ScrubStrategyDeviceID = iota + + // ScrubStrategyDeviceIDAll removes all hardware and device id data (ifa, mac hashes device id hashes) + ScrubStrategyDeviceIDAll +) + // Scrubber removes PII from parts of an OpenRTB request. type Scrubber interface { - ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device + ScrubDevice(device *openrtb.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User } @@ -62,20 +84,28 @@ func NewScrubber() Scrubber { return scrubber{} } -func (scrubber) ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { +func (scrubber) ScrubDevice(device *openrtb.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, 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) + + switch id { + case ScrubStrategyDeviceIDAll: + deviceCopy.DIDMD5 = "" + deviceCopy.DIDSHA1 = "" + deviceCopy.DPIDMD5 = "" + deviceCopy.DPIDSHA1 = "" + deviceCopy.IFA = "" + deviceCopy.MACMD5 = "" + deviceCopy.MACSHA1 = "" + } + + switch ipv4 { + case ScrubStrategyIPV4Lowest8: + deviceCopy.IP = scrubIPV4Lowest8(device.IP) + } switch ipv6 { case ScrubStrategyIPV6Lowest16: @@ -124,7 +154,7 @@ func (scrubber) ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo Sc return &userCopy } -func scrubIPV4(ip string) string { +func scrubIPV4Lowest8(ip string) string { i := strings.LastIndex(ip, ".") if i == -1 { return "" diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go index 67241019317..e0a2cb86f64 100644 --- a/privacy/scrubber_test.go +++ b/privacy/scrubber_test.go @@ -31,28 +31,21 @@ func TestScrubDevice(t *testing.T) { testCases := []struct { description string expected *openrtb.Device + id ScrubStrategyDeviceID + ipv4 ScrubStrategyIPV4 ipv6 ScrubStrategyIPV6 geo ScrubStrategyGeo }{ { - description: "IPv6 Lowest 32 & Geo Full", - expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: &openrtb.Geo{}, - }, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoFull, + description: "All Strageties - None", + expected: device, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 16 & Geo Full", + description: "All Strageties - Strictest", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -62,14 +55,16 @@ func TestScrubDevice(t *testing.T) { MACMD5: "", IFA: "", IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", + IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", Geo: &openrtb.Geo{}, }, - ipv6: ScrubStrategyIPV6Lowest16, + id: ScrubStrategyDeviceIDAll, + ipv4: ScrubStrategyIPV4Lowest8, + ipv6: ScrubStrategyIPV6Lowest32, geo: ScrubStrategyGeoFull, }, { - description: "IPv6 None & Geo Full", + description: "Isolated - ID - All", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -78,161 +73,126 @@ func TestScrubDevice(t *testing.T) { MACSHA1: "", MACMD5: "", IFA: "", - IP: "1.2.3.0", + IP: "1.2.3.4", IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", - Geo: &openrtb.Geo{}, + Geo: device.Geo, }, + id: ScrubStrategyDeviceIDAll, + ipv4: ScrubStrategyIPV4None, ipv6: ScrubStrategyIPV6None, - geo: ScrubStrategyGeoFull, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 32 & Geo Reduced", + description: "Isolated - IPv4 - Lowest 8", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: &openrtb.Geo{ - Lat: 123.46, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + Geo: device.Geo, }, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoReducedPrecision, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4Lowest8, + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 16 & Geo Reduced", + description: "Isolated - IPv6 - Lowest 16", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", 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", - }, + Geo: device.Geo, }, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, 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, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 32 & Geo None", + description: "Isolated - IPv6 - Lowest 32", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: &openrtb.Geo{ - Lat: 123.456, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, + Geo: device.Geo, }, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, ipv6: ScrubStrategyIPV6Lowest32, geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 16 & Geo None", + description: "Isolated - Geo - Reduced Precision", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", Geo: &openrtb.Geo{ - Lat: 123.456, + Lat: 123.46, Lon: 678.89, Metro: "some metro", City: "some city", ZIP: "some zip", }, }, - ipv6: ScrubStrategyIPV6Lowest16, - geo: ScrubStrategyGeoNone, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoReducedPrecision, }, { - description: "IPv6 None & Geo None", + description: "Isolated - Geo - Full", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", 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", - }, + Geo: &openrtb.Geo{}, }, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, ipv6: ScrubStrategyIPV6None, - geo: ScrubStrategyGeoNone, + geo: ScrubStrategyGeoFull, }, } for _, test := range testCases { - result := NewScrubber().ScrubDevice(device, test.ipv6, test.geo) + result := NewScrubber().ScrubDevice(device, test.id, test.ipv4, test.ipv6, test.geo) assert.Equal(t, test.expected, result, test.description) } } func TestScrubDeviceNil(t *testing.T) { - result := NewScrubber().ScrubDevice(nil, ScrubStrategyIPV6None, ScrubStrategyGeoNone) + result := NewScrubber().ScrubDevice(nil, ScrubStrategyDeviceIDNone, ScrubStrategyIPV4None, ScrubStrategyIPV6None, ScrubStrategyGeoNone) assert.Nil(t, result) } @@ -458,7 +418,7 @@ func TestScrubIPV4(t *testing.T) { } for _, test := range testCases { - result := scrubIPV4(test.IP) + result := scrubIPV4Lowest8(test.IP) assert.Equal(t, test.cleanedIP, result, test.description) } } From d3ba8a94e3830af798b26a5c97c10ff0d94b44de Mon Sep 17 00:00:00 2001 From: Bill Newman Date: Tue, 15 Sep 2020 17:21:59 +0300 Subject: [PATCH 205/381] New colossus adapter [Clean branch] (#1495) Co-authored-by: Aiholkin --- adapters/colossus/colossus.go | 137 ++++++++++++++++++ adapters/colossus/colossus_test.go | 12 ++ .../colossustest/exemplary/simple-banner.json | 132 +++++++++++++++++ .../colossustest/exemplary/simple-video.json | 119 +++++++++++++++ .../exemplary/simple-web-banner.json | 133 +++++++++++++++++ .../colossus/colossustest/params/banner.json | 3 + .../colossustest/params/race/banner.json | 3 + .../colossustest/params/race/video.json | 3 + .../colossus/colossustest/params/video.json | 3 + .../supplemental/bad-imp-ext.json | 42 ++++++ .../supplemental/bad_response.json | 85 +++++++++++ .../supplemental/bad_status_code.json | 79 ++++++++++ .../supplemental/empty_imp_ext.json | 38 +++++ .../supplemental/imp_ext_empty_object.json | 39 +++++ .../supplemental/imp_ext_string.json | 39 +++++ .../colossustest/supplemental/status-204.json | 79 ++++++++++ .../colossustest/supplemental/status-404.json | 85 +++++++++++ .../supplemental/string_imp_ext.json | 39 +++++ adapters/colossus/params_test.go | 46 ++++++ adapters/colossus/usersync.go | 13 ++ adapters/colossus/usersync_test.go | 35 +++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_colossus.go | 6 + static/bidder-info/colossus.yaml | 11 ++ static/bidder-params/colossus.json | 14 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 29 files changed, 1204 insertions(+) create mode 100644 adapters/colossus/colossus.go create mode 100644 adapters/colossus/colossus_test.go create mode 100644 adapters/colossus/colossustest/exemplary/simple-banner.json create mode 100644 adapters/colossus/colossustest/exemplary/simple-video.json create mode 100644 adapters/colossus/colossustest/exemplary/simple-web-banner.json create mode 100644 adapters/colossus/colossustest/params/banner.json create mode 100644 adapters/colossus/colossustest/params/race/banner.json create mode 100644 adapters/colossus/colossustest/params/race/video.json create mode 100644 adapters/colossus/colossustest/params/video.json create mode 100644 adapters/colossus/colossustest/supplemental/bad-imp-ext.json create mode 100644 adapters/colossus/colossustest/supplemental/bad_response.json create mode 100644 adapters/colossus/colossustest/supplemental/bad_status_code.json create mode 100644 adapters/colossus/colossustest/supplemental/empty_imp_ext.json create mode 100644 adapters/colossus/colossustest/supplemental/imp_ext_empty_object.json create mode 100644 adapters/colossus/colossustest/supplemental/imp_ext_string.json create mode 100644 adapters/colossus/colossustest/supplemental/status-204.json create mode 100644 adapters/colossus/colossustest/supplemental/status-404.json create mode 100644 adapters/colossus/colossustest/supplemental/string_imp_ext.json create mode 100644 adapters/colossus/params_test.go create mode 100644 adapters/colossus/usersync.go create mode 100644 adapters/colossus/usersync_test.go create mode 100644 openrtb_ext/imp_colossus.go create mode 100644 static/bidder-info/colossus.yaml create mode 100644 static/bidder-params/colossus.json diff --git a/adapters/colossus/colossus.go b/adapters/colossus/colossus.go new file mode 100644 index 00000000000..89cd49d2881 --- /dev/null +++ b/adapters/colossus/colossus.go @@ -0,0 +1,137 @@ +package colossus + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/buger/jsonparser" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type ColossusAdapter struct { + URI string +} + +// NewColossusBidder Initializes the Bidder +func NewColossusBidder(endpoint string) *ColossusAdapter { + return &ColossusAdapter{ + URI: endpoint, + } +} + +// MakeRequests create bid request for colossus demand +func (a *ColossusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var err error + var tagID string + + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb.Imp{imp} + + tagID, err = jsonparser.GetString(reqCopy.Imp[0].Ext, "bidder", "TagID") + if err != nil { + errs = append(errs, err) + continue + } + + reqCopy.Imp[0].TagID = tagID + + adapterReq, errors := a.makeRequest(&reqCopy) + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + errs = append(errs, errors...) + } + return adapterRequests, errs +} + +func (a *ColossusAdapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, []error) { + + var errs []error + + reqJSON, err := json.Marshal(request) + + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.URI, + Body: reqJSON, + Headers: headers, + }, errs +} + +// MakeBids makes the bids +func (a *ColossusAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + if response.StatusCode == http.StatusNotFound { + return nil, []error{&errortypes.BadServerResponse{ + 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 { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, errs +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) (openrtb_ext.BidType, error) { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impID { + if imp.Banner == nil && imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } + return mediaType, nil + } + } + + // This shouldnt happen. Lets handle it just incase by returning an error. + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression \"%s\" ", impID), + } +} diff --git a/adapters/colossus/colossus_test.go b/adapters/colossus/colossus_test.go new file mode 100644 index 00000000000..f4fd12f3fab --- /dev/null +++ b/adapters/colossus/colossus_test.go @@ -0,0 +1,12 @@ +package colossus + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + colossusAdapter := NewColossusBidder("http://example.com/?c=o&m=rtb") + adapterstest.RunJSONBidderTest(t, "colossustest", colossusAdapter) +} diff --git a/adapters/colossus/colossustest/exemplary/simple-banner.json b/adapters/colossus/colossustest/exemplary/simple-banner.json new file mode 100644 index 00000000000..1adc7010ed8 --- /dev/null +++ b/adapters/colossus/colossustest/exemplary/simple-banner.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "61317", + "ext": { + "bidder": { + "TagID": "61317" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } +}, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=rtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "61317", + "ext": { + "bidder": { + "TagID": "61317" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/colossus/colossustest/exemplary/simple-video.json b/adapters/colossus/colossustest/exemplary/simple-video.json new file mode 100644 index 00000000000..78516fcef31 --- /dev/null +++ b/adapters/colossus/colossustest/exemplary/simple-video.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "TagID": "61318" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=rtb", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "tagid": "61318", + "ext": { + "bidder": { + "TagID": "61318" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "colossus" + } + ], + "cur": "USD" + } + } + } + ], + + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/colossus/colossustest/exemplary/simple-web-banner.json b/adapters/colossus/colossustest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..37baf3d97dd --- /dev/null +++ b/adapters/colossus/colossustest/exemplary/simple-web-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=rtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "1", + "ext": { + "bidder": { + "TagID": "1" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "colossus" + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/colossus/colossustest/params/banner.json b/adapters/colossus/colossustest/params/banner.json new file mode 100644 index 00000000000..7c2643d4901 --- /dev/null +++ b/adapters/colossus/colossustest/params/banner.json @@ -0,0 +1,3 @@ +{ + "TagID": "61317" +} diff --git a/adapters/colossus/colossustest/params/race/banner.json b/adapters/colossus/colossustest/params/race/banner.json new file mode 100644 index 00000000000..7c2643d4901 --- /dev/null +++ b/adapters/colossus/colossustest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "TagID": "61317" +} diff --git a/adapters/colossus/colossustest/params/race/video.json b/adapters/colossus/colossustest/params/race/video.json new file mode 100644 index 00000000000..56f865c71d9 --- /dev/null +++ b/adapters/colossus/colossustest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "TagID": "61318" +} diff --git a/adapters/colossus/colossustest/params/video.json b/adapters/colossus/colossustest/params/video.json new file mode 100644 index 00000000000..56f865c71d9 --- /dev/null +++ b/adapters/colossus/colossustest/params/video.json @@ -0,0 +1,3 @@ +{ + "TagID": "61318" +} diff --git a/adapters/colossus/colossustest/supplemental/bad-imp-ext.json b/adapters/colossus/colossustest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..13656337e5e --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/bad-imp-ext.json @@ -0,0 +1,42 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "61317", + "ext": { + "colossus": { + "TagID": "61317" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } +}, +"expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } +] +} diff --git a/adapters/colossus/colossustest/supplemental/bad_response.json b/adapters/colossus/colossustest/supplemental/bad_response.json new file mode 100644 index 00000000000..c69b00c8e6e --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/?c=o&m=rtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/colossus/colossustest/supplemental/bad_status_code.json b/adapters/colossus/colossustest/supplemental/bad_status_code.json new file mode 100644 index 00000000000..f5b6a5748af --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/bad_status_code.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": {} + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/?c=o&m=rtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": {} + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/colossus/colossustest/supplemental/empty_imp_ext.json b/adapters/colossus/colossustest/supplemental/empty_imp_ext.json new file mode 100644 index 00000000000..00e1cf60fb7 --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/empty_imp_ext.json @@ -0,0 +1,38 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "61317", + "ext": {} + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/colossus/colossustest/supplemental/imp_ext_empty_object.json b/adapters/colossus/colossustest/supplemental/imp_ext_empty_object.json new file mode 100644 index 00000000000..e9c1f257aba --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/imp_ext_empty_object.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "61317", + "ext": {} + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/colossus/colossustest/supplemental/imp_ext_string.json b/adapters/colossus/colossustest/supplemental/imp_ext_string.json new file mode 100644 index 00000000000..362a8fa4df8 --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/imp_ext_string.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "61317", + "ext": "" + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/colossus/colossustest/supplemental/status-204.json b/adapters/colossus/colossustest/supplemental/status-204.json new file mode 100644 index 00000000000..73f8bc71f23 --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/status-204.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/?c=o&m=rtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }] +} diff --git a/adapters/colossus/colossustest/supplemental/status-404.json b/adapters/colossus/colossustest/supplemental/status-404.json new file mode 100644 index 00000000000..676eb8bb2f4 --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/status-404.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://example.com/?c=o&m=rtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/colossus/colossustest/supplemental/string_imp_ext.json b/adapters/colossus/colossustest/supplemental/string_imp_ext.json new file mode 100644 index 00000000000..362a8fa4df8 --- /dev/null +++ b/adapters/colossus/colossustest/supplemental/string_imp_ext.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "61317", + "ext": "" + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/colossus/params_test.go b/adapters/colossus/params_test.go new file mode 100644 index 00000000000..2883de2f53e --- /dev/null +++ b/adapters/colossus/params_test.go @@ -0,0 +1,46 @@ +package colossus + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// TestValidParams makes sure that the colossus 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.BidderColossus, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected colossus params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the colossus 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.BidderColossus, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"TagID": "61317"}`, +} + +var invalidParams = []string{ + `{"id": "123"}`, + `{"tagid": "123"}`, + `{"TagID": 16}`, +} diff --git a/adapters/colossus/usersync.go b/adapters/colossus/usersync.go new file mode 100644 index 00000000000..a4e82ee3bde --- /dev/null +++ b/adapters/colossus/usersync.go @@ -0,0 +1,13 @@ +package colossus + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +// NewColossusSyncer returns colossus syncer +func NewColossusSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("colossus", 0, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/colossus/usersync_test.go b/adapters/colossus/usersync_test.go new file mode 100644 index 00000000000..79d5483d528 --- /dev/null +++ b/adapters/colossus/usersync_test.go @@ -0,0 +1,35 @@ +package colossus + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestColossusSyncer(t *testing.T) { + syncURL := "https://sync.colossusssp.com/pbs.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dcolossus%26uid%3D%5BUID%5D" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewColossusSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + Consent: "A", + }, + CCPA: ccpa.Policy{ + Value: "1-YY", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://sync.colossusssp.com/pbs.gif?gdpr=0&gdpr_consent=A&us_privacy=1-YY&redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dcolossus%26uid%3D%5BUID%5D", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 59ba55ebe26..5731b65d567 100755 --- a/config/config.go +++ b/config/config.go @@ -693,6 +693,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderBeachfront, "https://sync.bfmio.com/sync_s2s?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbrightroll%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderColossus, "https://sync.colossusssp.com/pbs.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dcolossus%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConsumable, "https://e.serverbid.com/udb/9969/match?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dconsumable%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderConversant, "https://prebid-match.dotomi.com/match/bounce/current?version=1&networkId=72582&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dcpmstar%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -911,6 +912,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.colossus.endpoint", "http://colossusssp.com/?c=o&m=rtb") v.SetDefault("adapters.consumable.endpoint", "https://e.serverbid.com/api/v2") v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25") v.SetDefault("adapters.cpmstar.endpoint", "https://server.cpmstar.com/openrtbbidrq.aspx") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index a160e87aad7..5bb788b63b9 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -31,6 +31,7 @@ import ( "github.com/prebid/prebid-server/adapters/beachfront" "github.com/prebid/prebid-server/adapters/beintoo" "github.com/prebid/prebid-server/adapters/brightroll" + "github.com/prebid/prebid-server/adapters/colossus" "github.com/prebid/prebid-server/adapters/consumable" "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/cpmstar" @@ -118,6 +119,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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, cfg.Adapters[string(openrtb_ext.BidderBrightroll)].ExtraAdapterInfo), + openrtb_ext.BidderColossus: colossus.NewColossusBidder(cfg.Adapters[string(openrtb_ext.BidderColossus)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 876eeab86bd..221f97c9697 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -49,6 +49,7 @@ const ( BidderBeachfront BidderName = "beachfront" BidderBeintoo BidderName = "beintoo" BidderBrightroll BidderName = "brightroll" + BidderColossus BidderName = "colossus" BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" BidderCpmstar BidderName = "cpmstar" @@ -133,6 +134,7 @@ var BidderMap = map[string]BidderName{ "beachfront": BidderBeachfront, "beintoo": BidderBeintoo, "brightroll": BidderBrightroll, + "colossus": BidderColossus, "consumable": BidderConsumable, "conversant": BidderConversant, "cpmstar": BidderCpmstar, diff --git a/openrtb_ext/imp_colossus.go b/openrtb_ext/imp_colossus.go new file mode 100644 index 00000000000..8969000558d --- /dev/null +++ b/openrtb_ext/imp_colossus.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpColossus defines colossus specifiec param +type ExtImpColossus struct { + TagID string `json:"TagID"` +} diff --git a/static/bidder-info/colossus.yaml b/static/bidder-info/colossus.yaml new file mode 100644 index 00000000000..901c824c603 --- /dev/null +++ b/static/bidder-info/colossus.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "support@huddledmasses.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-params/colossus.json b/static/bidder-params/colossus.json new file mode 100644 index 00000000000..f2732fa0854 --- /dev/null +++ b/static/bidder-params/colossus.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Colossus Adapter Params", + "description": "A schema which validates params accepted by the Colossus adapter", + + "type": "object", + "properties": { + "TagID": { + "type": "string", + "description": "An ID which identifies the colossus ad tag" + } + }, + "required" : [ "TagID" ] + } diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 89540ea205b..c6ae984efc9 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -23,6 +23,7 @@ import ( "github.com/prebid/prebid-server/adapters/beachfront" "github.com/prebid/prebid-server/adapters/beintoo" "github.com/prebid/prebid-server/adapters/brightroll" + "github.com/prebid/prebid-server/adapters/colossus" "github.com/prebid/prebid-server/adapters/consumable" "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/cpmstar" @@ -99,6 +100,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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.BidderColossus, colossus.NewColossusSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderConsumable, consumable.NewConsumableSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderConversant, conversant.NewConversantSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderCpmstar, cpmstar.NewCpmstarSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 9197ed9507d..2cf0b2513c5 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -31,6 +31,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderBeachfront): syncConfig, string(openrtb_ext.BidderBeintoo): syncConfig, string(openrtb_ext.BidderBrightroll): syncConfig, + string(openrtb_ext.BidderColossus): syncConfig, string(openrtb_ext.BidderConsumable): syncConfig, string(openrtb_ext.BidderConversant): syncConfig, string(openrtb_ext.BidderCpmstar): syncConfig, From 7b59a4bd49e502b05f81624bcc908259aec757cf Mon Sep 17 00:00:00 2001 From: Daniel Lawrence Date: Tue, 15 Sep 2020 09:12:57 -0700 Subject: [PATCH 206/381] New: InMobi Prebid Server Adapter (#1489) * Adding InMobi adapter * code review feedback, also explicitly working with Imp[0], as we don't support multiple impressions * less tolerant bidder params due to sneaky 1.13 -> 1.14+ change --- adapters/inmobi/inmobi.go | 127 ++++++++++++++++++ adapters/inmobi/inmobi_test.go | 10 ++ .../inmobitest/exemplary/simple-banner.json | 107 +++++++++++++++ .../inmobitest/exemplary/simple-video.json | 109 +++++++++++++++ .../inmobi/inmobitest/params/race/banner.json | 3 + .../inmobi/inmobitest/params/race/video.json | 3 + .../inmobi/inmobitest/supplemental/204.json | 61 +++++++++ .../inmobi/inmobitest/supplemental/400.json | 67 +++++++++ .../supplemental/banner-format-coersion.json | 113 ++++++++++++++++ .../supplemental/ext-unmarshal-err.json | 28 ++++ .../supplemental/missing-plc-error.json | 28 ++++ .../inmobitest/supplemental/no-imp-error.json | 13 ++ config/config.go | 1 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_inmobi.go | 5 + static/bidder-info/inmobi.yaml | 8 ++ static/bidder-params/inmobi.json | 13 ++ usersync/usersyncers/syncer_test.go | 1 + 19 files changed, 701 insertions(+) create mode 100644 adapters/inmobi/inmobi.go create mode 100644 adapters/inmobi/inmobi_test.go create mode 100644 adapters/inmobi/inmobitest/exemplary/simple-banner.json create mode 100644 adapters/inmobi/inmobitest/exemplary/simple-video.json create mode 100644 adapters/inmobi/inmobitest/params/race/banner.json create mode 100644 adapters/inmobi/inmobitest/params/race/video.json create mode 100644 adapters/inmobi/inmobitest/supplemental/204.json create mode 100644 adapters/inmobi/inmobitest/supplemental/400.json create mode 100644 adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json create mode 100644 adapters/inmobi/inmobitest/supplemental/ext-unmarshal-err.json create mode 100644 adapters/inmobi/inmobitest/supplemental/missing-plc-error.json create mode 100644 adapters/inmobi/inmobitest/supplemental/no-imp-error.json create mode 100644 openrtb_ext/imp_inmobi.go create mode 100644 static/bidder-info/inmobi.yaml create mode 100644 static/bidder-params/inmobi.json diff --git a/adapters/inmobi/inmobi.go b/adapters/inmobi/inmobi.go new file mode 100644 index 00000000000..4d46ffb8f1e --- /dev/null +++ b/adapters/inmobi/inmobi.go @@ -0,0 +1,127 @@ +package inmobi + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" +) + +type InMobiAdapter struct { + endPoint string +} + +func NewInMobiAdapter(endpoint string) *InMobiAdapter { + return &InMobiAdapter{ + endPoint: endpoint, + } +} + +func (a *InMobiAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: "No impression in the request", + }} + } + + if err := preprocess(&request.Imp[0]); err != nil { + errs = append(errs, err) + return nil, errs + } + + reqJson, err := json.Marshal(request) + if err != nil { + errs = append(errs, err) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: a.endPoint, + Body: reqJson, + Headers: headers, + }}, errs +} + +func (a *InMobiAdapter) 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 { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected http status code: %d", response.StatusCode), + }} + } + + var serverBidResponse openrtb.BidResponse + if err := json.Unmarshal(response.Body, &serverBidResponse); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + + for _, sb := range serverBidResponse.SeatBid { + for i := range sb.Bid { + mediaType := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp) + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: mediaType, + }) + } + } + + return bidResponse, nil +} + +func preprocess(imp *openrtb.Imp) error { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + + var inMobiExt openrtb_ext.ExtImpInMobi + if err := json.Unmarshal(bidderExt.Bidder, &inMobiExt); err != nil { + return &errortypes.BadInput{Message: "bad InMobi bidder ext"} + } + + if len(inMobiExt.Plc) == 0 { + return &errortypes.BadInput{Message: "'plc' is a required attribute for InMobi's bidder ext"} + } + + if imp.Banner != nil { + banner := *imp.Banner + imp.Banner = &banner + if (banner.W == nil || banner.H == nil || *banner.W == 0 || *banner.H == 0) && len(banner.Format) > 0 { + format := banner.Format[0] + banner.W = &format.W + banner.H = &format.H + } + } + + return nil +} + +func getMediaTypeForImp(impId string, imps []openrtb.Imp) openrtb_ext.BidType { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impId { + if imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } + break + } + } + return mediaType +} diff --git a/adapters/inmobi/inmobi_test.go b/adapters/inmobi/inmobi_test.go new file mode 100644 index 00000000000..6aa58d97222 --- /dev/null +++ b/adapters/inmobi/inmobi_test.go @@ -0,0 +1,10 @@ +package inmobi + +import ( + "github.com/prebid/prebid-server/adapters/adapterstest" + "testing" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "inmobitest", NewInMobiAdapter("https://api.w.inmobi.com/showad/openrtb/bidder/prebid")) +} diff --git a/adapters/inmobi/inmobitest/exemplary/simple-banner.json b/adapters/inmobi/inmobitest/exemplary/simple-banner.json new file mode 100644 index 00000000000..4345ef8ff66 --- /dev/null +++ b/adapters/inmobi/inmobitest/exemplary/simple-banner.json @@ -0,0 +1,107 @@ +{ + "mockBidRequest": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "seatbid": [ + { + "bid": [ + { + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + }, + "nurl": "https://some.event.url/params", + "crid": "123456789", + "adomain": [], + "price": 2.0, + "id": "1234", + "adm": "bannerhtml", + "impid": "imp-id" + } + ] + } + ] + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "1234", + "impid": "imp-id", + "price": 2.0, + "adm": "bannerhtml", + "crid": "123456789", + "nurl": "https://some.event.url/params", + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + } + }, + "type": "banner" + }] + }] +} diff --git a/adapters/inmobi/inmobitest/exemplary/simple-video.json b/adapters/inmobi/inmobitest/exemplary/simple-video.json new file mode 100644 index 00000000000..20b3c0cc810 --- /dev/null +++ b/adapters/inmobi/inmobitest/exemplary/simple-video.json @@ -0,0 +1,109 @@ +{ + "mockBidRequest": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1598991608990" + } + }, + "video": { + "w": 640, + "h": 360, + "mimes": ["video/mp4"] + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1598991608990" + } + }, + "video": { + "w": 640, + "h": 360, + "mimes": ["video/mp4"] + }, + "id": "imp-id" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "seatbid": [ + { + "bid": [ + { + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + }, + "nurl": "https://some.event.url/params", + "crid": "123456789", + "adomain": [], + "price": 2.0, + "id": "1234", + "adm": " ", + "impid": "imp-id" + } + ] + } + ] + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "1234", + "impid": "imp-id", + "price": 2.0, + "adm": " ", + "crid": "123456789", + "nurl": "https://some.event.url/params", + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + } + }, + "type": "video" + }] + }] +} diff --git a/adapters/inmobi/inmobitest/params/race/banner.json b/adapters/inmobi/inmobitest/params/race/banner.json new file mode 100644 index 00000000000..7791393fc99 --- /dev/null +++ b/adapters/inmobi/inmobitest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "plc": "1596825400965" +} diff --git a/adapters/inmobi/inmobitest/params/race/video.json b/adapters/inmobi/inmobitest/params/race/video.json new file mode 100644 index 00000000000..74a44b6e6f9 --- /dev/null +++ b/adapters/inmobi/inmobitest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "plc": "1598991608990" +} diff --git a/adapters/inmobi/inmobitest/supplemental/204.json b/adapters/inmobi/inmobitest/supplemental/204.json new file mode 100644 index 00000000000..c811763678c --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/204.json @@ -0,0 +1,61 @@ +{ + "mockBidRequest": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }] +} diff --git a/adapters/inmobi/inmobitest/supplemental/400.json b/adapters/inmobi/inmobitest/supplemental/400.json new file mode 100644 index 00000000000..2df5c85aaca --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/400.json @@ -0,0 +1,67 @@ +{ + "mockBidRequest": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d", + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected http status code: 400", + "comparison": "literal" + } + ] +} diff --git a/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json b/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json new file mode 100644 index 00000000000..514f86817c9 --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "device-ifa", + "ip": "1.1.1.1", + "ua": "device-ua" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "format": [{ + "w": 320, + "h": 50 + }] + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "app": { + "bundle": "com.example.app" + }, + "id": "req-id", + "device": { + "ifa": "device-ifa", + "ip": "1.1.1.1", + "ua": "device-ua" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1596825400965" + } + }, + "banner": { + "format": [{ + "w": 320, + "h": 50 + }], + "w": 320, + "h": 50 + }, + "id": "imp-id" + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "seatbid": [ + { + "bid": [ + { + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + }, + "nurl": "https://some.event.url/params", + "crid": "123456789", + "adomain": [], + "price": 2.0, + "id": "1234", + "adm": "bannerhtml", + "impid": "imp-id" + } + ] + } + ] + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "1234", + "impid": "imp-id", + "price": 2.0, + "adm": "bannerhtml", + "crid": "123456789", + "nurl": "https://some.event.url/params", + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + } + }, + "type": "banner" + }] + }] +} diff --git a/adapters/inmobi/inmobitest/supplemental/ext-unmarshal-err.json b/adapters/inmobi/inmobitest/supplemental/ext-unmarshal-err.json new file mode 100644 index 00000000000..957a2f6f952 --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/ext-unmarshal-err.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "req-id", + "imp": [ + { + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "plc": true + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "bad InMobi bidder ext", + "comparison": "literal" + } + ] +} diff --git a/adapters/inmobi/inmobitest/supplemental/missing-plc-error.json b/adapters/inmobi/inmobitest/supplemental/missing-plc-error.json new file mode 100644 index 00000000000..52697cf90c6 --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/missing-plc-error.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "req-id", + "imp": [ + { + "banner": { + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "a": 1 + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "'plc' is a required attribute for InMobi's bidder ext", + "comparison": "literal" + } + ] +} diff --git a/adapters/inmobi/inmobitest/supplemental/no-imp-error.json b/adapters/inmobi/inmobitest/supplemental/no-imp-error.json new file mode 100644 index 00000000000..6c6c363425a --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/no-imp-error.json @@ -0,0 +1,13 @@ +{ + "mockBidRequest": { + "id": "req-id", + "imp": [ + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "No impression in the request", + "comparison": "literal" + } + ] +} diff --git a/config/config.go b/config/config.go index 5731b65d567..53daf117fdf 100755 --- a/config/config.go +++ b/config/config.go @@ -926,6 +926,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.inmobi.endpoint", "https://api.w.inmobi.com/showad/openrtb/bidder/prebid") 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", "https://kssp.kbntx.ch/prebid") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 5bb788b63b9..d428168921a 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -45,6 +45,7 @@ import ( "github.com/prebid/prebid-server/adapters/grid" "github.com/prebid/prebid-server/adapters/gumgum" "github.com/prebid/prebid-server/adapters/improvedigital" + "github.com/prebid/prebid-server/adapters/inmobi" "github.com/prebid/prebid-server/adapters/ix" "github.com/prebid/prebid-server/adapters/kidoz" "github.com/prebid/prebid-server/adapters/kubient" @@ -135,6 +136,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderInMobi: inmobi.NewInMobiAdapter(cfg.Adapters[string(openrtb_ext.BidderInMobi)].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), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 221f97c9697..dcfd663ebc7 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -64,6 +64,7 @@ const ( BidderGrid BidderName = "grid" BidderGumGum BidderName = "gumgum" BidderImprovedigital BidderName = "improvedigital" + BidderInMobi BidderName = "inmobi" BidderIx BidderName = "ix" BidderKidoz BidderName = "kidoz" BidderKubient BidderName = "kubient" @@ -149,6 +150,7 @@ var BidderMap = map[string]BidderName{ "grid": BidderGrid, "gumgum": BidderGumGum, "improvedigital": BidderImprovedigital, + "inmobi": BidderInMobi, "ix": BidderIx, "kidoz": BidderKidoz, "kubient": BidderKubient, diff --git a/openrtb_ext/imp_inmobi.go b/openrtb_ext/imp_inmobi.go new file mode 100644 index 00000000000..d74e3cac8b0 --- /dev/null +++ b/openrtb_ext/imp_inmobi.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpInMobi struct { + Plc string `json:"plc"` +} diff --git a/static/bidder-info/inmobi.yaml b/static/bidder-info/inmobi.yaml new file mode 100644 index 00000000000..3f8cdd8cb91 --- /dev/null +++ b/static/bidder-info/inmobi.yaml @@ -0,0 +1,8 @@ +maintainer: + email: "prebid-support@inmobi.com" + +capabilities: + app: + mediaTypes: + - banner + - video diff --git a/static/bidder-params/inmobi.json b/static/bidder-params/inmobi.json new file mode 100644 index 00000000000..631b3137b72 --- /dev/null +++ b/static/bidder-params/inmobi.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "InMobi Adapter Params", + "description": "A schema which validates params accepted by the InMobi adapter", + "type": "object", + "properties": { + "plc": { + "type": ["string"], + "description": "An ID corresponding to the placement selling the impression" + } + }, + "required": ["plc"] +} diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 2cf0b2513c5..bd250489fdd 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -89,6 +89,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderAdhese: true, openrtb_ext.BidderAdoppler: true, openrtb_ext.BidderApplogy: true, + openrtb_ext.BidderInMobi: true, openrtb_ext.BidderKidoz: true, openrtb_ext.BidderKubient: true, openrtb_ext.BidderMobileFuse: true, From ab653bc8a3ea0e7bf814a1741a0c7eab2b1e5139 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 16 Sep 2020 18:04:48 -0400 Subject: [PATCH 207/381] Revert "Added new size 640x360 (Id: 198) (#1490)" (#1501) This reverts commit fa23f5c226df99a9a4ef318100fdb7d84d3e40fa. --- adapters/rubicon/rubicon.go | 1 - 1 file changed, 1 deletion(-) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 7d6e0e12039..56ae7b2f792 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -236,7 +236,6 @@ var rubiSizeMap = map[rubiSize]int{ {w: 800, h: 250}: 125, {w: 200, h: 600}: 126, {w: 640, h: 320}: 156, - {w: 640, h: 360}: 198, } // defines the contract for bidrequest.user.ext.eids[i].ext From f6624b7acc924f6e66b014825bf07d2edb072eb9 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Thu, 17 Sep 2020 01:48:17 -0400 Subject: [PATCH 208/381] CCPA Publisher No Sale Relationships (#1465) --- adapters/33across/usersync_test.go | 2 +- adapters/adkernel/usersync_test.go | 2 +- adapters/adkernelAdn/usersync_test.go | 2 +- adapters/adman/usersync_test.go | 2 +- adapters/admixer/usersync_test.go | 7 +- adapters/adtarget/usersync_test.go | 5 +- adapters/aja/usersync_test.go | 5 +- adapters/avocet/usersync_test.go | 2 +- adapters/beachfront/usersync_test.go | 2 +- adapters/beintoo/usersync_test.go | 2 +- adapters/consumable/consumable.go | 13 +- adapters/consumable/usersync_test.go | 2 +- adapters/datablocks/usersync_test.go | 2 +- adapters/emx_digital/usersync_test.go | 2 +- adapters/engagebdr/usersync_test.go | 2 +- adapters/gamoshi/usersync_test.go | 2 +- adapters/gumgum/usersync_test.go | 2 +- adapters/improvedigital/usersync_test.go | 2 +- adapters/marsmedia/usersync_test.go | 2 +- adapters/nanointeractive/usersync_test.go | 24 +- adapters/pubmatic/usersync_test.go | 2 +- adapters/rhythmone/usersync_test.go | 2 +- adapters/sharethrough/butler.go | 15 +- adapters/smartadserver/usersync_test.go | 2 +- adapters/syncer.go | 2 +- adapters/syncer_test.go | 2 +- adapters/unruly/usersync_test.go | 2 +- adapters/valueimpression/usersync_test.go | 2 +- adapters/visx/usersync_test.go | 2 +- adapters/zeroclickfraud/usersync_test.go | 2 +- endpoints/auction.go | 6 +- endpoints/auction_test.go | 5 +- endpoints/cookie_sync.go | 50 +- endpoints/openrtb2/amp_auction.go | 41 +- endpoints/openrtb2/auction.go | 35 +- endpoints/openrtb2/auction_test.go | 48 ++ .../exchangetest/ccpa-nosale-any-bidder.json | 75 +++ .../ccpa-nosale-specific-bidder.json | 75 +++ exchange/utils.go | 70 +- exchange/utils_test.go | 80 ++- openrtb_ext/request.go | 5 + privacy/ccpa/consentwriter.go | 25 + privacy/ccpa/consentwriter_test.go | 51 ++ privacy/ccpa/parsedpolicy.go | 137 ++++ privacy/ccpa/parsedpolicy_test.go | 391 +++++++++++ privacy/ccpa/policy.go | 213 +++--- privacy/ccpa/policy_test.go | 630 +++++++++++------- privacy/enforcer.go | 43 ++ privacy/enforcer_test.go | 18 + privacy/gdpr/consentwriter.go | 44 ++ privacy/gdpr/consentwriter_test.go | 101 +++ privacy/gdpr/policy.go | 40 +- privacy/gdpr/policy_test.go | 113 +--- privacy/lmt/policy.go | 14 +- privacy/lmt/policy_test.go | 68 +- privacy/policies.go | 52 +- privacy/policies_test.go | 119 ---- privacy/writer.go | 18 + privacy/writer_test.go | 25 + 59 files changed, 1962 insertions(+), 747 deletions(-) create mode 100644 exchange/exchangetest/ccpa-nosale-any-bidder.json create mode 100644 exchange/exchangetest/ccpa-nosale-specific-bidder.json create mode 100644 privacy/ccpa/consentwriter.go create mode 100644 privacy/ccpa/consentwriter_test.go create mode 100644 privacy/ccpa/parsedpolicy.go create mode 100644 privacy/ccpa/parsedpolicy_test.go create mode 100644 privacy/enforcer.go create mode 100644 privacy/enforcer_test.go create mode 100644 privacy/gdpr/consentwriter.go create mode 100644 privacy/gdpr/consentwriter_test.go delete mode 100644 privacy/policies_test.go create mode 100644 privacy/writer.go create mode 100644 privacy/writer_test.go diff --git a/adapters/33across/usersync_test.go b/adapters/33across/usersync_test.go index a5e301b1082..a9eb4e57908 100644 --- a/adapters/33across/usersync_test.go +++ b/adapters/33across/usersync_test.go @@ -23,7 +23,7 @@ func Test33AcrossSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/adkernel/usersync_test.go b/adapters/adkernel/usersync_test.go index 0d539d11ee0..aeacf00b7f0 100644 --- a/adapters/adkernel/usersync_test.go +++ b/adapters/adkernel/usersync_test.go @@ -23,7 +23,7 @@ func TestAdkernelAdnSyncer(t *testing.T) { Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/adkernelAdn/usersync_test.go b/adapters/adkernelAdn/usersync_test.go index ecc759bdf70..92d688e6117 100644 --- a/adapters/adkernelAdn/usersync_test.go +++ b/adapters/adkernelAdn/usersync_test.go @@ -23,7 +23,7 @@ func TestAdkernelAdnSyncer(t *testing.T) { Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/adman/usersync_test.go b/adapters/adman/usersync_test.go index 55a6e2cec97..25da77db7ed 100644 --- a/adapters/adman/usersync_test.go +++ b/adapters/adman/usersync_test.go @@ -23,7 +23,7 @@ func TestAdmanSyncer(t *testing.T) { Consent: "ANDFJDS", }, CCPA: ccpa.Policy{ - Value: "1-YY", + Consent: "1-YY", }, }) diff --git a/adapters/admixer/usersync_test.go b/adapters/admixer/usersync_test.go index a5715c64a46..d31f7b10fb1 100644 --- a/adapters/admixer/usersync_test.go +++ b/adapters/admixer/usersync_test.go @@ -1,12 +1,13 @@ package admixer import ( + "testing" + "text/template" + "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/privacy/ccpa" "github.com/prebid/prebid-server/privacy/gdpr" "github.com/stretchr/testify/assert" - "testing" - "text/template" ) func TestAdmixerSyncer(t *testing.T) { @@ -22,7 +23,7 @@ func TestAdmixerSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/adtarget/usersync_test.go b/adapters/adtarget/usersync_test.go index 3ab2ed5b5df..ddba9e7a720 100644 --- a/adapters/adtarget/usersync_test.go +++ b/adapters/adtarget/usersync_test.go @@ -2,10 +2,11 @@ package adtarget import ( "fmt" - "github.com/prebid/prebid-server/privacy/ccpa" "testing" "text/template" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/privacy/gdpr" "github.com/stretchr/testify/assert" @@ -25,7 +26,7 @@ func TestAdtargetSyncer(t *testing.T) { Consent: "123", }, CCPA: ccpa.Policy{ - Value: "1-YY", + Consent: "1-YY", }, }) diff --git a/adapters/aja/usersync_test.go b/adapters/aja/usersync_test.go index dbb66cc9ae2..4b6c90ef141 100644 --- a/adapters/aja/usersync_test.go +++ b/adapters/aja/usersync_test.go @@ -1,10 +1,11 @@ package aja import ( - "github.com/prebid/prebid-server/privacy/ccpa" "testing" "text/template" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/privacy/gdpr" "github.com/stretchr/testify/assert" @@ -23,7 +24,7 @@ func TestAJASyncer(t *testing.T) { Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/avocet/usersync_test.go b/adapters/avocet/usersync_test.go index 8fba403f1b1..3df39b77fce 100644 --- a/adapters/avocet/usersync_test.go +++ b/adapters/avocet/usersync_test.go @@ -23,7 +23,7 @@ func TestAvocetSyncer(t *testing.T) { Consent: "ConsentString", }, CCPA: ccpa.Policy{ - Value: "PrivacyString", + Consent: "PrivacyString", }, }) diff --git a/adapters/beachfront/usersync_test.go b/adapters/beachfront/usersync_test.go index 0267ac05eb7..db4d825eb5a 100644 --- a/adapters/beachfront/usersync_test.go +++ b/adapters/beachfront/usersync_test.go @@ -23,7 +23,7 @@ func TestBeachfrontSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/beintoo/usersync_test.go b/adapters/beintoo/usersync_test.go index 2cfca010226..880d6a84cee 100644 --- a/adapters/beintoo/usersync_test.go +++ b/adapters/beintoo/usersync_test.go @@ -23,7 +23,7 @@ func TestBeintooSyncer(t *testing.T) { Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/consumable/consumable.go b/adapters/consumable/consumable.go index ff7451f15f7..18ece8d4c4a 100644 --- a/adapters/consumable/consumable.go +++ b/adapters/consumable/consumable.go @@ -3,15 +3,16 @@ package consumable import ( "encoding/json" "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/privacy/ccpa" - "net/http" - "net/url" - "strconv" - "strings" ) type ConsumableAdapter struct { @@ -136,9 +137,9 @@ func (a *ConsumableAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *a gdpr := bidGdpr{} - ccpaPolicy, err := ccpa.ReadPolicy(request) + ccpaPolicy, err := ccpa.ReadFromRequest(request) if err == nil { - body.CCPA = ccpaPolicy.Value + body.CCPA = ccpaPolicy.Consent } // TODO: Replace with gdpr.ReadPolicy when it is available diff --git a/adapters/consumable/usersync_test.go b/adapters/consumable/usersync_test.go index 017cb72975b..ef71c0b18c7 100644 --- a/adapters/consumable/usersync_test.go +++ b/adapters/consumable/usersync_test.go @@ -23,7 +23,7 @@ func TestConsumableSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/datablocks/usersync_test.go b/adapters/datablocks/usersync_test.go index f8500ab9b03..a7518e9b226 100644 --- a/adapters/datablocks/usersync_test.go +++ b/adapters/datablocks/usersync_test.go @@ -23,7 +23,7 @@ func TestDatablocksSyncer(t *testing.T) { Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/emx_digital/usersync_test.go b/adapters/emx_digital/usersync_test.go index 0e76936cea4..59d66d87808 100644 --- a/adapters/emx_digital/usersync_test.go +++ b/adapters/emx_digital/usersync_test.go @@ -23,7 +23,7 @@ func TestEMXDigitalSyncer(t *testing.T) { Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/engagebdr/usersync_test.go b/adapters/engagebdr/usersync_test.go index 45e1e41e196..3a6c179addf 100644 --- a/adapters/engagebdr/usersync_test.go +++ b/adapters/engagebdr/usersync_test.go @@ -23,7 +23,7 @@ func TestEngageBDRSyncer(t *testing.T) { Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/gamoshi/usersync_test.go b/adapters/gamoshi/usersync_test.go index b8e3e327e44..43dc88a4953 100644 --- a/adapters/gamoshi/usersync_test.go +++ b/adapters/gamoshi/usersync_test.go @@ -18,7 +18,7 @@ func TestGamoshiSyncer(t *testing.T) { syncer := NewGamoshiSyncer(syncURLTemplate) syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ CCPA: ccpa.Policy{ - Value: "anyValue", + Consent: "anyValue", }, }) diff --git a/adapters/gumgum/usersync_test.go b/adapters/gumgum/usersync_test.go index 3606f6ae04c..9c6dc420600 100644 --- a/adapters/gumgum/usersync_test.go +++ b/adapters/gumgum/usersync_test.go @@ -23,7 +23,7 @@ func TestGumGumSyncer(t *testing.T) { Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/improvedigital/usersync_test.go b/adapters/improvedigital/usersync_test.go index c928ebf123d..35ea89cf894 100644 --- a/adapters/improvedigital/usersync_test.go +++ b/adapters/improvedigital/usersync_test.go @@ -23,7 +23,7 @@ func TestImprovedigitalSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/marsmedia/usersync_test.go b/adapters/marsmedia/usersync_test.go index 67276a35fb6..f019c014516 100644 --- a/adapters/marsmedia/usersync_test.go +++ b/adapters/marsmedia/usersync_test.go @@ -23,7 +23,7 @@ func TestMarsmediaSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/nanointeractive/usersync_test.go b/adapters/nanointeractive/usersync_test.go index ec9787bc20d..fa78664928f 100644 --- a/adapters/nanointeractive/usersync_test.go +++ b/adapters/nanointeractive/usersync_test.go @@ -1,11 +1,12 @@ package nanointeractive import ( - "github.com/prebid/prebid-server/privacy/ccpa" - "github.com/prebid/prebid-server/privacy/gdpr" "testing" "text/template" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/prebid/prebid-server/privacy" "github.com/stretchr/testify/assert" ) @@ -17,16 +18,15 @@ func TestNewNanoInteractiveSyncer(t *testing.T) { ) userSync := NewNanoInteractiveSyncer(syncURLTemplate) - syncInfo, err := userSync.GetUsersyncInfo( - privacy.Policies{ - GDPR: gdpr.Policy{ - Signal: "1", - Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", - }, - CCPA: ccpa.Policy{ - Value: "1NYN", - }, - }) + syncInfo, err := userSync.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "1", + Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", + }, + CCPA: ccpa.Policy{ + Consent: "1NYN", + }, + }) assert.NoError(t, err) assert.Equal(t, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr=1&consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw&us_privacy=1NYN&redirectUri=http%3A%2F%2Flocalhost%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D1%26gdpr_consent%3DBONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw%26uid%3D%24UID", syncInfo.URL) diff --git a/adapters/pubmatic/usersync_test.go b/adapters/pubmatic/usersync_test.go index dd4a086c453..d6cd9f78af7 100644 --- a/adapters/pubmatic/usersync_test.go +++ b/adapters/pubmatic/usersync_test.go @@ -23,7 +23,7 @@ func TestPubmaticSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/rhythmone/usersync_test.go b/adapters/rhythmone/usersync_test.go index cee6e9b0259..85ecba2a8ab 100644 --- a/adapters/rhythmone/usersync_test.go +++ b/adapters/rhythmone/usersync_test.go @@ -23,7 +23,7 @@ func TestRhythmoneSyncer(t *testing.T) { Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/sharethrough/butler.go b/adapters/sharethrough/butler.go index 522bbc4967e..36af79c4534 100644 --- a/adapters/sharethrough/butler.go +++ b/adapters/sharethrough/butler.go @@ -3,16 +3,17 @@ package sharethrough import ( "encoding/json" "fmt" - "github.com/mxmCherry/openrtb" - "github.com/prebid/prebid-server/adapters" - "github.com/prebid/prebid-server/errortypes" - "github.com/prebid/prebid-server/openrtb_ext" - "github.com/prebid/prebid-server/privacy/ccpa" "net/http" "net/url" "regexp" "strconv" "time" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/privacy/ccpa" ) const defaultTmax = 10000 // 10 sec @@ -97,8 +98,8 @@ func (s StrOpenRTBTranslator) requestFromOpenRTB(imp openrtb.Imp, request *openr } usPolicySignal := "" - if usPolicy, err := ccpa.ReadPolicy(request); err == nil { - usPolicySignal = usPolicy.Value + if usPolicy, err := ccpa.ReadFromRequest(request); err == nil { + usPolicySignal = usPolicy.Consent } return &adapters.RequestData{ diff --git a/adapters/smartadserver/usersync_test.go b/adapters/smartadserver/usersync_test.go index e279b49e017..c4e6660693f 100644 --- a/adapters/smartadserver/usersync_test.go +++ b/adapters/smartadserver/usersync_test.go @@ -23,7 +23,7 @@ func TestSmartadserverSyncer(t *testing.T) { Consent: "COyASAoOyASAoAfAAAENAfCAAAAAAAAAAAAAAAAAAAAA", }, CCPA: ccpa.Policy{ - Value: "1YNN", + Consent: "1YNN", }, }) diff --git a/adapters/syncer.go b/adapters/syncer.go index c212a4366c9..122bcc7ed38 100644 --- a/adapters/syncer.go +++ b/adapters/syncer.go @@ -46,7 +46,7 @@ func (s *Syncer) GetUsersyncInfo(privacyPolicies privacy.Policies) (*usersync.Us syncURL, err := macros.ResolveMacros(*s.urlTemplate, macros.UserSyncTemplateParams{ GDPR: privacyPolicies.GDPR.Signal, GDPRConsent: privacyPolicies.GDPR.Consent, - USPrivacy: privacyPolicies.CCPA.Value, + USPrivacy: privacyPolicies.CCPA.Consent, }) if err != nil { return nil, err diff --git a/adapters/syncer_test.go b/adapters/syncer_test.go index 9be523091dd..ca33a9a130d 100644 --- a/adapters/syncer_test.go +++ b/adapters/syncer_test.go @@ -17,7 +17,7 @@ func TestGetUsersyncInfo(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, } diff --git a/adapters/unruly/usersync_test.go b/adapters/unruly/usersync_test.go index bdab254f370..2f0e07d813a 100644 --- a/adapters/unruly/usersync_test.go +++ b/adapters/unruly/usersync_test.go @@ -23,7 +23,7 @@ func TestUnrulySyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/valueimpression/usersync_test.go b/adapters/valueimpression/usersync_test.go index 63f123055a9..ffb3f372bd7 100644 --- a/adapters/valueimpression/usersync_test.go +++ b/adapters/valueimpression/usersync_test.go @@ -23,7 +23,7 @@ func TestValueImpressionSyncer(t *testing.T) { Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/adapters/visx/usersync_test.go b/adapters/visx/usersync_test.go index a77136c9240..b410cda6061 100644 --- a/adapters/visx/usersync_test.go +++ b/adapters/visx/usersync_test.go @@ -23,7 +23,7 @@ func TestVisxSyncer(t *testing.T) { Consent: "B", }, CCPA: ccpa.Policy{ - Value: "C", + Consent: "C", }, }) diff --git a/adapters/zeroclickfraud/usersync_test.go b/adapters/zeroclickfraud/usersync_test.go index 30ade771a4c..5e8f8fdf111 100644 --- a/adapters/zeroclickfraud/usersync_test.go +++ b/adapters/zeroclickfraud/usersync_test.go @@ -23,7 +23,7 @@ func TestZeroClickFraudSyncer(t *testing.T) { Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw", }, CCPA: ccpa.Policy{ - Value: "1NYN", + Consent: "1NYN", }, }) diff --git a/endpoints/auction.go b/endpoints/auction.go index bf592e43b02..c6fd57123c7 100644 --- a/endpoints/auction.go +++ b/endpoints/auction.go @@ -24,7 +24,7 @@ import ( "github.com/prebid/prebid-server/pbsmetrics" pbc "github.com/prebid/prebid-server/prebid_cache_client" "github.com/prebid/prebid-server/privacy" - gdprPolicy "github.com/prebid/prebid-server/privacy/gdpr" + gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr" "github.com/prebid/prebid-server/usersync" ) @@ -190,7 +190,7 @@ func (a *auction) recoverSafely(inner func(*pbs.PBSBidder, pbsmetrics.AdapterLab } } -func (a *auction) shouldUsersync(ctx context.Context, bidder openrtb_ext.BidderName, gdprPrivacyPolicy gdprPolicy.Policy) bool { +func (a *auction) shouldUsersync(ctx context.Context, bidder openrtb_ext.BidderName, gdprPrivacyPolicy gdprPrivacy.Policy) bool { switch gdprPrivacyPolicy.Signal { case "0": return true @@ -511,7 +511,7 @@ func (a *auction) processUserSync(req *pbs.PBSRequest, bidder *pbs.PBSBidder, bl if uid == "" { bidder.NoCookie = true privacyPolicies := privacy.Policies{ - GDPR: gdprPolicy.Policy{ + GDPR: gdprPrivacy.Policy{ Signal: req.ParseGDPR(), Consent: req.ParseConsent(), }, diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index 028f119640a..1e41b02aaa2 100644 --- a/endpoints/auction_test.go +++ b/endpoints/auction_test.go @@ -387,11 +387,12 @@ func TestShouldUsersync(t *testing.T) { }, metricsEngine: nil, } - privacyPolicy := gdprPolicy.Policy{ + gdprPrivacyPolicy := gdprPolicy.Policy{ Signal: gdprApplies, Consent: consent, } - allowSyncs := deps.shouldUsersync(context.Background(), openrtb_ext.BidderAdform, privacyPolicy) + + allowSyncs := deps.shouldUsersync(context.Background(), openrtb_ext.BidderAdform, gdprPrivacyPolicy) if allowSyncs != expectAllow { t.Errorf("Expected syncs: %t, allowed syncs: %t", expectAllow, allowSyncs) } diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index 9787a8f78f2..60da4f0bd16 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -20,7 +20,7 @@ import ( "github.com/prebid/prebid-server/pbsmetrics" "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/privacy/ccpa" - gdprPolicy "github.com/prebid/prebid-server/privacy/gdpr" + gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr" "github.com/prebid/prebid-server/usersync" ) @@ -105,24 +105,30 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h } } + parsedReq.filterExistingSyncs(deps.syncers, userSyncCookie, needSyncupForSameSite) + + adapterSyncs := make(map[openrtb_ext.BidderName]bool) + // assume all bidders will be privacy blocked + for _, b := range parsedReq.Bidders { + adapterSyncs[openrtb_ext.BidderName(b)] = true + } + privacyPolicy := privacy.Policies{ - GDPR: gdprPolicy.Policy{ + GDPR: gdprPrivacy.Policy{ Signal: gdprToString(parsedReq.GDPR), Consent: parsedReq.Consent, }, CCPA: ccpa.Policy{ - Value: parsedReq.USPrivacy, + Consent: parsedReq.USPrivacy, }, } - parsedReq.filterExistingSyncs(deps.syncers, userSyncCookie, needSyncupForSameSite) + parsedReq.filterForGDPR(deps.syncPermissions) - adapterSyncs := make(map[openrtb_ext.BidderName]bool) - // assume all bidders will be privacy blocked - for _, b := range parsedReq.Bidders { - adapterSyncs[openrtb_ext.BidderName(b)] = true + if deps.enforceCCPA { + parsedReq.filterForCCPA() } - parsedReq.filterForPrivacy(deps.syncPermissions, privacyPolicy, deps.enforceCCPA) + // surviving bidders are not privacy blocked for _, b := range parsedReq.Bidders { adapterSyncs[openrtb_ext.BidderName(b)] = false @@ -223,12 +229,7 @@ func (req *cookieSyncRequest) filterExistingSyncs(valid map[openrtb_ext.BidderNa } } -func (req *cookieSyncRequest) filterForPrivacy(permissions gdpr.Permissions, privacyPolicies privacy.Policies, enforceCCPA bool) { - if enforceCCPA && privacyPolicies.CCPA.ShouldEnforce() { - req.Bidders = nil - return - } - +func (req *cookieSyncRequest) filterForGDPR(permissions gdpr.Permissions) { if req.GDPR != nil && *req.GDPR == 0 { return } @@ -246,6 +247,25 @@ func (req *cookieSyncRequest) filterForPrivacy(permissions gdpr.Permissions, pri } } +func (req *cookieSyncRequest) filterForCCPA() { + validBidders := make(map[string]struct{}) + for _, v := range openrtb_ext.BidderMap { + validBidders[v.String()] = struct{}{} + } + + ccpaPolicy := &ccpa.Policy{Consent: req.USPrivacy} + ccpaParsedPolicy, err := ccpaPolicy.Parse(validBidders) + + if err == nil { + for i := 0; i < len(req.Bidders); i++ { + if ccpaParsedPolicy.ShouldEnforce(req.Bidders[i]) { + req.Bidders = append(req.Bidders[:i], req.Bidders[i+1:]...) + i-- + } + } + } +} + // filterToLimit will enforce a max limit on cookiesyncs supplied, picking a random subset of syncs to get to the limit if over. func (req *cookieSyncRequest) filterToLimit() { if req.Limit <= 0 { diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 1e92569e260..d7442f5ecba 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -24,6 +24,8 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/ccpa" + "github.com/prebid/prebid-server/privacy/gdpr" "github.com/prebid/prebid-server/stored_requests" "github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher" "github.com/prebid/prebid-server/usersync" @@ -403,17 +405,12 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope req.Imp[0].TagID = slot } - 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), - }} - } + policyWriter, policyWriterErr := readPolicyFromUrl(httpRequest.URL) + if policyWriterErr != nil { + return []error{policyWriterErr} + } + if err := policyWriter.Write(req); err != nil { + return []error{err} } if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil { @@ -558,7 +555,27 @@ func setAmpExt(site *openrtb.Site, value string) { } } -func readConsent(url *url.URL) string { +func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) { + consent := readConsentFromURL(url) + + if len(consent) == 0 { + return privacy.NilPolicyWriter{}, nil + } + + if gdpr.ValidateConsent(consent) { + return gdpr.ConsentWriter{consent}, nil + } + + if ccpa.ValidateConsent(consent) { + return ccpa.ConsentWriter{consent}, nil + } + + return privacy.NilPolicyWriter{}, &errortypes.InvalidPrivacyConsent{ + Message: fmt.Sprintf("Consent '%s' is not recognized as either CCPA or GDPR TCF.", consent), + } +} + +func readConsentFromURL(url *url.URL) string { if v := url.Query().Get("consent_string"); v != "" { return v } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index d6cbc2285fb..b02b57861bd 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -318,39 +318,36 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { } if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) { - errL = append(errL, errors.New("request.site or request.app must be defined, but not both.")) - return errL + return append(errL, errors.New("request.site or request.app must be defined, but not both.")) } if err := deps.validateSite(req.Site); err != nil { - errL = append(errL, err) - return errL + return append(errL, err) } if err := deps.validateApp(req.App); err != nil { - errL = append(errL, err) - return errL + return append(errL, err) } if err := validateUser(req.User, aliases); err != nil { - errL = append(errL, err) - return errL + return append(errL, err) } if err := validateRegs(req.Regs); err != nil { - errL = append(errL, err) - return errL + return append(errL, err) } - if policy, err := ccpa.ReadPolicy(req); err != nil { - errL = append(errL, errL...) - return errL - } else if err := policy.Validate(); err != nil { - errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)}) - - policy.Value = "" - if err := policy.Write(req); err != nil { - errL = append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err)) + if ccpaPolicy, err := ccpa.ReadFromRequest(req); err != nil { + return append(errL, err) + } else if _, err := ccpaPolicy.Parse(exchange.GetValidBidders(aliases)); err != nil { + if _, invalidConsent := err.(*errortypes.InvalidPrivacyConsent); invalidConsent { + errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)}) + consentWriter := ccpa.ConsentWriter{""} + if err := consentWriter.Write(req); err != nil { + return append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err)) + } + } else { + return append(errL, err) } } diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 925cffcebeb..58913bb58d6 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1339,6 +1339,54 @@ func TestCCPAInvalid(t *testing.T) { assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") } +func TestNoSaleInvalid(t *testing.T) { + deps := &endpointDeps{ + &nobidExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{}, + pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + nil, + nil, + hardcodedResponseIPValidator{response: true}, + } + + ui := uint64(1) + req := openrtb.BidRequest{ + ID: "someID", + Imp: []openrtb.Imp{ + { + ID: "imp-ID", + Banner: &openrtb.Banner{ + W: &ui, + H: &ui, + }, + Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`), + }, + }, + Site: &openrtb.Site{ + ID: "myID", + }, + Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"1NYN"}`), + }, + Ext: json.RawMessage(`{"prebid":{"nosale":["*", "appnexus"]}}`), + } + + errL := deps.validateRequest(&req) + + expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided") + assert.ElementsMatch(t, errL, []error{expectedError}) +} + func TestValidateSourceTID(t *testing.T) { cfg := &config.Configuration{ AutoGenSourceTID: true, diff --git a/exchange/exchangetest/ccpa-nosale-any-bidder.json b/exchange/exchangetest/ccpa-nosale-any-bidder.json new file mode 100644 index 00000000000..f7abd91f512 --- /dev/null +++ b/exchange/exchangetest/ccpa-nosale-any-bidder.json @@ -0,0 +1,75 @@ +{ + "enforceCcpa": 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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["*"] + } + }, + "user": { + "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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["*"] + } + }, + "user": { + "buyeruid": "some-buyer-id" + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/ccpa-nosale-specific-bidder.json b/exchange/exchangetest/ccpa-nosale-specific-bidder.json new file mode 100644 index 00000000000..b89e29aea01 --- /dev/null +++ b/exchange/exchangetest/ccpa-nosale-specific-bidder.json @@ -0,0 +1,75 @@ +{ + "enforceCcpa": 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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["appnexus"] + } + }, + "user": { + "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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["appnexus"] + } + }, + "user": { + "buyeruid": "some-buyer-id" + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/utils.go b/exchange/utils.go index 22b28adcacb..1e49b7acc6a 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -19,6 +19,8 @@ import ( "github.com/prebid/prebid-server/privacy/lmt" ) +const unknownBidder string = "" + func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) @@ -65,31 +67,32 @@ func cleanOpenRTBRequests(ctx context.Context, requestsByBidder, errs = splitBidRequest(orig, requestExt, impsByBidder, aliases, usersyncs, blables, labels) + if len(requestsByBidder) == 0 { + return + } + gdpr := extractGDPR(orig, usersyncIfAmbiguous) consent := extractConsent(orig) ampGDPRException := (labels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException() - var ccpaPolicy ccpa.Policy - if privacyConfig.CCPA.Enforce { - ccpaPolicy, _ = ccpa.ReadPolicy(orig) + ccpaEnforcer, err := extractCCPA(orig, privacyConfig, aliases) + if err != nil { + errs = append(errs, err) + return } - var lmtPolicy lmt.Policy - if privacyConfig.LMT.Enforce { - lmtPolicy = lmt.ReadPolicy(orig) - } + lmtEnforcer := extractLMT(orig, privacyConfig) // request level privacy policies privacyEnforcement := privacy.Enforcement{ - CCPA: ccpaPolicy.ShouldEnforce(), COPPA: orig.Regs != nil && orig.Regs.COPPA == 1, - LMT: lmtPolicy.ShouldEnforce(), + LMT: lmtEnforcer.ShouldEnforce(unknownBidder), } - privacyLabels.CCPAProvided = ccpaPolicy.Value != "" - privacyLabels.CCPAEnforced = privacyEnforcement.CCPA + privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce() + privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder) privacyLabels.COPPAEnforced = privacyEnforcement.COPPA - privacyLabels.LMTEnforced = privacyEnforcement.LMT + privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder) if gdpr == 1 { privacyLabels.GDPREnforced = true @@ -102,7 +105,10 @@ func cleanOpenRTBRequests(ctx context.Context, // bidder level privacy policies for bidder, bidReq := range requestsByBidder { + // CCPA + privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidder.String()) + // GDPR if gdpr == 1 { coreBidder := resolveBidder(bidder.String(), aliases) @@ -121,6 +127,32 @@ func cleanOpenRTBRequests(ctx context.Context, return } +func extractCCPA(orig *openrtb.BidRequest, privacyConfig config.Privacy, aliases map[string]string) (privacy.PolicyEnforcer, error) { + ccpaPolicy, err := ccpa.ReadFromRequest(orig) + if err != nil { + return privacy.NilPolicyEnforcer{}, err + } + + validBidders := GetValidBidders(aliases) + ccpaParsedPolicy, err := ccpaPolicy.Parse(validBidders) + if err != nil { + return privacy.NilPolicyEnforcer{}, err + } + + ccpaEnforcer := privacy.EnabledPolicyEnforcer{ + Enabled: privacyConfig.CCPA.Enforce, + PolicyEnforcer: ccpaParsedPolicy, + } + return ccpaEnforcer, nil +} + +func extractLMT(orig *openrtb.BidRequest, privacyConfig config.Privacy) privacy.PolicyEnforcer { + return privacy.EnabledPolicyEnforcer{ + Enabled: privacyConfig.LMT.Enforce, + PolicyEnforcer: lmt.ReadFromRequest(orig), + } +} + func splitBidRequest(req *openrtb.BidRequest, requestExt *openrtb_ext.ExtRequest, impsByBidder map[string][]openrtb.Imp, @@ -429,6 +461,20 @@ func parseAliases(orig *openrtb.BidRequest) (map[string]string, []error) { return aliases, nil } +func GetValidBidders(aliases map[string]string) map[string]struct{} { + validBidders := make(map[string]struct{}) + + for _, v := range openrtb_ext.BidderMap { + validBidders[v.String()] = struct{}{} + } + + for k := range aliases { + validBidders[k] = struct{}{} + } + + return validBidders +} + // Quick little randomizer for a list of strings. Stuffing it in utils to keep other files clean func randomizeList(list []openrtb_ext.BidderName) { l := len(list) diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 528e875ab16..0dd6c0311ab 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -3,11 +3,13 @@ package exchange import ( "context" "encoding/json" + "errors" "fmt" "testing" "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbsmetrics" "github.com/stretchr/testify/assert" @@ -93,6 +95,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { func TestCleanOpenRTBRequestsCCPA(t *testing.T) { testCases := []struct { description string + reqExt json.RawMessage ccpaConsent string enforceCCPA bool expectDataScrub bool @@ -118,13 +121,46 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { CCPAEnforced: false, }, }, + { + description: "Feature Flag Enabled - No Sale Star - Doesn't Scrub", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*"]}}`), + ccpaConsent: "1-Y-", + enforceCCPA: true, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: false, + }, + }, + { + description: "Feature Flag Enabled - No Sale Specific Bidder - Doesn't Scrub", + reqExt: json.RawMessage(`{"prebid":{"nosale":["appnexus"]}}`), + ccpaConsent: "1-Y-", + enforceCCPA: true, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, + }, + { + description: "Feature Flag Enabled - No Sale Different Bidder - Scrubs", + reqExt: json.RawMessage(`{"prebid":{"nosale":["rubicon"]}}`), + ccpaConsent: "1-Y-", + enforceCCPA: true, + expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, + }, { description: "Feature Flag Disabled", ccpaConsent: "1-Y-", enforceCCPA: false, expectDataScrub: false, expectPrivacyLabels: pbsmetrics.PrivacyLabels{ - CCPAProvided: false, + CCPAProvided: true, CCPAEnforced: false, }, }, @@ -132,6 +168,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { for _, test := range testCases { req := newBidRequest(t) + req.Ext = test.reqExt req.Regs = &openrtb.Regs{ Ext: json.RawMessage(`{"us_privacy":"` + test.ccpaConsent + `"}`), } @@ -157,6 +194,47 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { } } +func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { + testCases := []struct { + description string + reqExt json.RawMessage + reqRegsExt json.RawMessage + expectError error + }{ + { + description: "Invalid Consent", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*"]}}`), + reqRegsExt: json.RawMessage(`{"us_privacy":"malformed"}`), + expectError: &errortypes.InvalidPrivacyConsent{"request.regs.ext.us_privacy must contain 4 characters"}, + }, + { + description: "Invalid No Sale Bidders", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*", "another"]}}`), + reqRegsExt: json.RawMessage(`{"us_privacy":"1NYN"}`), + expectError: errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided"), + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.Ext = test.reqExt + req.Regs = &openrtb.Regs{Ext: test.reqRegsExt} + + var reqExtStruct openrtb_ext.ExtRequest + err := json.Unmarshal(req.Ext, &reqExtStruct) + assert.NoError(t, err, test.description+":marshal_ext") + + privacyConfig := config.Privacy{ + CCPA: config.CCPA{ + Enforce: true, + }, + } + _, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &reqExtStruct, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + + assert.ElementsMatch(t, []error{test.expectError}, errs, test.description) + } +} + func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { testCases := []struct { description string diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 42ac9d9d4b9..894be6763c6 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -24,6 +24,11 @@ type ExtRequestPrebid struct { Targeting *ExtRequestTargeting `json:"targeting,omitempty"` SupportDeals bool `json:"supportdeals,omitempty"` Debug bool `json:"debug,omitempty"` + + // NoSale specifies bidders with whom the publisher has a legal relationship where the + // passing of personally identifiable information doesn't constitute a sale per CCPA law. + // The array may contain a single sstar ('*') entry to represent all bidders. + NoSale []string `json:"nosale,omitempty"` } // ExtRequestPrebid defines the contract for bidrequest.ext.prebid.schains diff --git a/privacy/ccpa/consentwriter.go b/privacy/ccpa/consentwriter.go new file mode 100644 index 00000000000..4856655402b --- /dev/null +++ b/privacy/ccpa/consentwriter.go @@ -0,0 +1,25 @@ +package ccpa + +import ( + "github.com/mxmCherry/openrtb" +) + +// ConsentWriter implements the PolicyWriter interface for CCPA. +type ConsentWriter struct { + Consent string +} + +// Write mutates an OpenRTB bid request with the CCPA consent string. +func (c ConsentWriter) Write(req *openrtb.BidRequest) error { + if req == nil { + return nil + } + + regs, err := buildRegs(c.Consent, req.Regs) + if err != nil { + return err + } + req.Regs = regs + + return nil +} diff --git a/privacy/ccpa/consentwriter_test.go b/privacy/ccpa/consentwriter_test.go new file mode 100644 index 00000000000..57a7f8f4ddf --- /dev/null +++ b/privacy/ccpa/consentwriter_test.go @@ -0,0 +1,51 @@ +package ccpa + +import ( + "encoding/json" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestConsentWriter(t *testing.T) { + consent := "anyConsent" + testCases := []struct { + description string + request *openrtb.BidRequest + expected *openrtb.BidRequest + expectedError bool + }{ + { + description: "Nil Request", + request: nil, + expected: nil, + }, + { + description: "Success", + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + }, + }, + { + description: "Error With Regs.Ext - Does Not Mutate", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + expectedError: true, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + }, + } + + for _, test := range testCases { + writer := ConsentWriter{consent} + + err := writer.Write(test.request) + + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, test.request, test.description) + } +} diff --git a/privacy/ccpa/parsedpolicy.go b/privacy/ccpa/parsedpolicy.go new file mode 100644 index 00000000000..3c934e67822 --- /dev/null +++ b/privacy/ccpa/parsedpolicy.go @@ -0,0 +1,137 @@ +package ccpa + +import ( + "errors" + "fmt" + + "github.com/prebid/prebid-server/errortypes" +) + +const ( + ccpaVersion1 = '1' + ccpaYes = 'Y' + ccpaNo = 'N' + ccpaNotApplicable = '-' +) + +const ( + indexVersion = 0 + indexExplicitNotice = 1 + indexOptOutSale = 2 + indexLSPACoveredTransaction = 3 +) + +const allBiddersMarker = "*" + +// ValidateConsent returns true if the consent string is empty or valid per the IAB CCPA spec. +func ValidateConsent(consent string) bool { + _, err := parseConsent(consent) + return err == nil +} + +// ParsedPolicy represents parsed and validated CCPA regulatory information. Use this struct +// to make enforcement decisions. +type ParsedPolicy struct { + consentSpecified bool + consentOptOutSale bool + noSaleForAllBidders bool + noSaleSpecificBidders map[string]struct{} +} + +// Parse returns a parsed and validated ParsedPolicy intended for use in enforcement decisions. +func (p Policy) Parse(validBidders map[string]struct{}) (ParsedPolicy, error) { + consentOptOut, err := parseConsent(p.Consent) + if err != nil { + msg := fmt.Sprintf("request.regs.ext.us_privacy %s", err.Error()) + return ParsedPolicy{}, &errortypes.InvalidPrivacyConsent{Message: msg} + } + + noSaleForAllBidders, noSaleSpecificBidders, err := parseNoSaleBidders(p.NoSaleBidders, validBidders) + if err != nil { + return ParsedPolicy{}, fmt.Errorf("request.ext.prebid.nosale is invalid: %s", err.Error()) + } + + return ParsedPolicy{ + consentSpecified: p.Consent != "", + consentOptOutSale: consentOptOut, + noSaleForAllBidders: noSaleForAllBidders, + noSaleSpecificBidders: noSaleSpecificBidders, + }, nil +} + +func parseConsent(consent string) (consentOptOutSale bool, err error) { + if consent == "" { + return false, nil + } + + if len(consent) != 4 { + return false, errors.New("must contain 4 characters") + } + + if consent[indexVersion] != ccpaVersion1 { + return false, errors.New("must specify version 1") + } + + var c byte + + c = consent[indexExplicitNotice] + if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable { + return false, errors.New("must specify 'N', 'Y', or '-' for the explicit notice") + } + + c = consent[indexOptOutSale] + if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable { + return false, errors.New("must specify 'N', 'Y', or '-' for the opt-out sale") + } + + c = consent[indexLSPACoveredTransaction] + if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable { + return false, errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement") + } + + return consent[indexOptOutSale] == ccpaYes, nil +} + +func parseNoSaleBidders(noSaleBidders []string, validBidders map[string]struct{}) (noSaleForAllBidders bool, noSaleSpecificBidders map[string]struct{}, err error) { + noSaleSpecificBidders = make(map[string]struct{}) + + if len(noSaleBidders) == 1 && noSaleBidders[0] == allBiddersMarker { + noSaleForAllBidders = true + return + } + + for _, bidder := range noSaleBidders { + if bidder == allBiddersMarker { + err = errors.New("can only specify all bidders if no other bidders are provided") + return + } + + if _, exists := validBidders[bidder]; exists { + noSaleSpecificBidders[bidder] = struct{}{} + } else { + err = fmt.Errorf("unrecognized bidder '%s'", bidder) + return + } + } + + return +} + +// CanEnforce returns true when consent is specifically provided by the publisher, as opposed to an empty string. +func (p ParsedPolicy) CanEnforce() bool { + return p.consentSpecified +} + +func (p ParsedPolicy) isNoSaleForBidder(bidder string) bool { + if p.noSaleForAllBidders { + return true + } + + _, exists := p.noSaleSpecificBidders[bidder] + return exists +} + +// ShouldEnforce returns true when the opt-out signal is explicitly detected. +func (p ParsedPolicy) ShouldEnforce(bidder string) bool { + return !p.isNoSaleForBidder(bidder) && p.consentOptOutSale +} diff --git a/privacy/ccpa/parsedpolicy_test.go b/privacy/ccpa/parsedpolicy_test.go new file mode 100644 index 00000000000..2f7e8bfd683 --- /dev/null +++ b/privacy/ccpa/parsedpolicy_test.go @@ -0,0 +1,391 @@ +package ccpa + +import ( + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestValidateConsent(t *testing.T) { + testCases := []struct { + description string + consent string + expected bool + }{ + { + description: "Empty String", + consent: "", + expected: true, + }, + { + description: "Valid Consent With Opt Out", + consent: "1NYN", + expected: true, + }, + { + description: "Valid Consent Without Opt Out", + consent: "1NNN", + expected: true, + }, + { + description: "Invalid", + consent: "malformed", + expected: false, + }, + } + + for _, test := range testCases { + result := ValidateConsent(test.consent) + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestParse(t *testing.T) { + validBidders := map[string]struct{}{"a": {}} + + testCases := []struct { + description string + consent string + noSaleBidders []string + expectedPolicy ParsedPolicy + expectedError string + }{ + { + description: "Consent Error", + consent: "malformed", + noSaleBidders: []string{}, + expectedPolicy: ParsedPolicy{}, + expectedError: "request.regs.ext.us_privacy must contain 4 characters", + }, + { + description: "No Sale Error", + consent: "1NYN", + noSaleBidders: []string{"b"}, + expectedPolicy: ParsedPolicy{}, + expectedError: "request.ext.prebid.nosale is invalid: unrecognized bidder 'b'", + }, + { + description: "Success", + consent: "1NYN", + noSaleBidders: []string{"a"}, + expectedPolicy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"a": {}}, + }, + }, + } + + for _, test := range testCases { + policy := Policy{test.consent, test.noSaleBidders} + + result, err := policy.Parse(validBidders) + + if test.expectedError == "" { + assert.NoError(t, err, test.description) + } else { + assert.EqualError(t, err, test.expectedError, test.description) + } + + assert.Equal(t, test.expectedPolicy, result, test.description) + } +} + +func TestParseConsent(t *testing.T) { + testCases := []struct { + description string + consent string + expectedResult bool + expectedError string + }{ + { + description: "Valid", + consent: "1NYN", + expectedResult: true, + }, + { + description: "Valid - Not Sale", + consent: "1NNN", + expectedResult: false, + }, + { + description: "Valid - Not Applicable", + consent: "1---", + expectedResult: false, + }, + { + description: "Valid - Empty", + consent: "", + expectedResult: false, + }, + { + description: "Wrong Length", + consent: "1NY", + expectedResult: false, + expectedError: "must contain 4 characters", + }, + { + description: "Wrong Version", + consent: "2---", + expectedResult: false, + expectedError: "must specify version 1", + }, + { + description: "Explicit Notice Char", + consent: "1X--", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + }, + { + description: "Invalid Explicit Notice Case", + consent: "1y--", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + }, + { + description: "Invalid Opt-Out Sale Char", + consent: "1-X-", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + }, + { + description: "Invalid Opt-Out Sale Case", + consent: "1-y-", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + }, + { + description: "Invalid LSPA Char", + consent: "1--X", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + }, + { + description: "Invalid LSPA Case", + consent: "1--y", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + }, + } + + for _, test := range testCases { + result, err := parseConsent(test.consent) + + if test.expectedError == "" { + assert.NoError(t, err, test.description) + } else { + assert.EqualError(t, err, test.expectedError, test.description) + } + + assert.Equal(t, test.expectedResult, result, test.description) + } +} + +func TestParseNoSaleBidders(t *testing.T) { + testCases := []struct { + description string + noSaleBidders []string + validBidders []string + expectedNoSaleForAllBidders bool + expectedNoSaleSpecificBidders map[string]struct{} + expectedError string + }{ + { + description: "Valid - No Bidders", + noSaleBidders: []string{}, + validBidders: []string{"a"}, + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "Valid - 1 Bidder", + noSaleBidders: []string{"a"}, + validBidders: []string{"a"}, + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{"a": {}}, + }, + { + description: "Valid - 1+ Bidders", + noSaleBidders: []string{"a", "b"}, + validBidders: []string{"a", "b"}, + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{"a": {}, "b": {}}, + }, + { + description: "Valid - All Bidders", + noSaleBidders: []string{"*"}, + validBidders: []string{"a"}, + expectedNoSaleForAllBidders: true, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "Bidder Not Valid", + noSaleBidders: []string{"b"}, + validBidders: []string{"a"}, + expectedError: "unrecognized bidder 'b'", + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "All Bidder Mixed With Other Bidders Is Invalid", + noSaleBidders: []string{"*", "a"}, + validBidders: []string{"a"}, + expectedError: "can only specify all bidders if no other bidders are provided", + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "Valid Bidders Case Sensitive", + noSaleBidders: []string{"a"}, + validBidders: []string{"A"}, + expectedError: "unrecognized bidder 'a'", + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + } + + for _, test := range testCases { + validBiddersMap := make(map[string]struct{}) + for _, v := range test.validBidders { + validBiddersMap[v] = struct{}{} + } + + resultNoSaleForAllBidders, resultNoSaleSpecificBidders, err := parseNoSaleBidders(test.noSaleBidders, validBiddersMap) + + if test.expectedError == "" { + assert.NoError(t, err, test.description+":err") + } else { + assert.EqualError(t, err, test.expectedError, test.description+":err") + } + + assert.Equal(t, test.expectedNoSaleForAllBidders, resultNoSaleForAllBidders, test.description+":allBidders") + assert.Equal(t, test.expectedNoSaleSpecificBidders, resultNoSaleSpecificBidders, test.description+":specificBidders") + } +} + +func TestCanEnforce(t *testing.T) { + testCases := []struct { + description string + policy ParsedPolicy + expected bool + }{ + { + description: "Specified", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + expected: true, + }, + { + description: "Not Specified", + policy: ParsedPolicy{ + consentSpecified: false, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + expected: false, + }, + } + + for _, test := range testCases { + result := test.policy.CanEnforce() + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestShouldEnforce(t *testing.T) { + testCases := []struct { + description string + policy ParsedPolicy + bidder string + expected bool + }{ + { + description: "Not Enforced - All Bidders No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: true, + noSaleSpecificBidders: map[string]struct{}{}, + }, + bidder: "a", + expected: false, + }, + { + description: "Not Enforced - Specific Bidders No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"a": {}}, + }, + bidder: "a", + expected: false, + }, + { + description: "Not Enforced - No Bidder No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + bidder: "a", + expected: false, + }, + { + description: "Not Enforced - No Sale Case Sensitive", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"A": {}}, + }, + bidder: "a", + expected: false, + }, + { + description: "Enforced - No Bidder No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + bidder: "a", + expected: true, + }, + { + description: "Enforced - No Sale Case Sensitive", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"A": {}}, + }, + bidder: "a", + expected: true, + }, + } + + for _, test := range testCases { + result := test.policy.ShouldEnforce(test.bidder) + assert.Equal(t, test.expected, result, test.description) + } +} + +type mockPolicWriter struct { + mock.Mock +} + +func (m *mockPolicWriter) Write(req *openrtb.BidRequest) error { + args := m.Called(req) + return args.Error(0) +} diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index 11ac434595a..a9f1c49e47d 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -9,139 +9,190 @@ import ( "github.com/prebid/prebid-server/openrtb_ext" ) -// Policy represents the CCPA regulation for an OpenRTB bid request. +// Policy represents the CCPA regulatory information from an OpenRTB bid request. type Policy struct { - Value string + Consent string + NoSaleBidders []string } -// ReadPolicy extracts the CCPA regulation policy from an OpenRTB request. -func ReadPolicy(req *openrtb.BidRequest) (Policy, error) { - policy := Policy{} +// ReadFromRequest extracts the CCPA regulatory information from an OpenRTB bid request. +func ReadFromRequest(req *openrtb.BidRequest) (Policy, error) { + var consent string + var noSaleBidders []string - if req != nil && req.Regs != nil && len(req.Regs.Ext) > 0 { + if req == nil { + return Policy{}, nil + } + + // Read consent from request.regs.ext + if req.Regs != nil && len(req.Regs.Ext) > 0 { var ext openrtb_ext.ExtRegs if err := json.Unmarshal(req.Regs.Ext, &ext); err != nil { - return policy, err + return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err) } - policy.Value = ext.USPrivacy + consent = ext.USPrivacy } - return policy, nil + // Read no sale bidders from request.ext.prebid + if len(req.Ext) > 0 { + var ext openrtb_ext.ExtRequest + if err := json.Unmarshal(req.Ext, &ext); err != nil { + return Policy{}, fmt.Errorf("error reading request.ext.prebid: %s", err) + } + noSaleBidders = ext.Prebid.NoSale + } + + return Policy{consent, noSaleBidders}, nil } -// Write mutates an OpenRTB bid request with the context of the CCPA policy. +// Write mutates an OpenRTB bid request with the CCPA regulatory information. func (p Policy) Write(req *openrtb.BidRequest) error { - if p.Value == "" { - return clearPolicy(req) - } - if req == nil { return nil } - if req.Regs == nil { - req.Regs = &openrtb.Regs{} + regs, err := buildRegs(p.Consent, req.Regs) + if err != nil { + return err } - - if req.Regs.Ext == nil { - ext, err := json.Marshal(openrtb_ext.ExtRegs{USPrivacy: p.Value}) - if err == nil { - req.Regs.Ext = ext - } + ext, err := buildExt(p.NoSaleBidders, req.Ext) + if err != nil { 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 - } + req.Regs = regs + req.Ext = ext + return nil +} + +func buildRegs(consent string, regs *openrtb.Regs) (*openrtb.Regs, error) { + if consent == "" { + return buildRegsClear(regs) } - return err + return buildRegsWrite(consent, regs) } -func clearPolicy(req *openrtb.BidRequest) error { - if req == nil { - return nil +func buildRegsClear(regs *openrtb.Regs) (*openrtb.Regs, error) { + if regs == nil || len(regs.Ext) == 0 { + return regs, nil } - if req.Regs == nil { - return nil + var extMap map[string]interface{} + if err := json.Unmarshal(regs.Ext, &extMap); err != nil { + return nil, err } - if len(req.Regs.Ext) == 0 { - return nil + delete(extMap, "us_privacy") + + // Remove entire ext if it's now empty + if len(extMap) == 0 { + regsResult := *regs + regsResult.Ext = nil + return ®sResult, nil } - var extMap map[string]interface{} - err := json.Unmarshal(req.Regs.Ext, &extMap) + // Marshal ext if there are still other fields + var regsResult openrtb.Regs + ext, err := json.Marshal(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 - } + regsResult = *regs + regsResult.Ext = ext } - - return err + return ®sResult, err } -// Validate returns an error if the CCPA policy does not adhere to the IAB spec. -func (p Policy) Validate() error { - if err := ValidateConsent(p.Value); err != nil { - return fmt.Errorf("request.regs.ext.us_privacy %s", err.Error()) +func buildRegsWrite(consent string, regs *openrtb.Regs) (*openrtb.Regs, error) { + if regs == nil { + return marshalRegsExt(openrtb.Regs{}, openrtb_ext.ExtRegs{USPrivacy: consent}) } - return nil + if regs.Ext == nil { + return marshalRegsExt(*regs, openrtb_ext.ExtRegs{USPrivacy: consent}) + } + + var extMap map[string]interface{} + if err := json.Unmarshal(regs.Ext, &extMap); err != nil { + return nil, err + } + + extMap["us_privacy"] = consent + return marshalRegsExt(*regs, extMap) } -// 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 +func marshalRegsExt(regs openrtb.Regs, ext interface{}) (*openrtb.Regs, error) { + extJSON, err := json.Marshal(ext) + if err == nil { + regs.Ext = extJSON } + return ®s, err +} - if len(consent) != 4 { - return errors.New("must contain 4 characters") +func buildExt(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) { + if len(noSaleBidders) == 0 { + return buildExtClear(ext) } + return buildExtWrite(noSaleBidders, ext) +} - if consent[0] != '1' { - return errors.New("must specify version 1") +func buildExtClear(ext json.RawMessage) (json.RawMessage, error) { + if len(ext) == 0 { + return ext, nil } - var c byte + var extMap map[string]interface{} + if err := json.Unmarshal(ext, &extMap); err != nil { + return nil, err + } - c = consent[1] - if c != 'N' && c != 'Y' && c != '-' { - return errors.New("must specify 'N', 'Y', or '-' for the explicit notice") + prebidExt, exists := extMap["prebid"] + if !exists { + return ext, nil } - c = consent[2] - if c != 'N' && c != 'Y' && c != '-' { - return errors.New("must specify 'N', 'Y', or '-' for the opt-out sale") + // Verify prebid is an object + prebidExtMap, ok := prebidExt.(map[string]interface{}) + if !ok { + return nil, errors.New("request.ext.prebid is not a json object") } - c = consent[3] - if c != 'N' && c != 'Y' && c != '-' { - return errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement") + // Remove no sale member + delete(prebidExtMap, "nosale") + if len(prebidExtMap) == 0 { + delete(extMap, "prebid") } - return nil + // Remove entire ext if it's empty + if len(extMap) == 0 { + return nil, nil + } + + return json.Marshal(extMap) } -// ShouldEnforce returns true when the opt-out signal is explicitly detected. -func (p Policy) ShouldEnforce() bool { - if err := p.Validate(); err != nil { - return false +func buildExtWrite(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) { + if len(ext) == 0 { + return json.Marshal(openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{NoSale: noSaleBidders}}) + } + + var extMap map[string]interface{} + if err := json.Unmarshal(ext, &extMap); err != nil { + return nil, err + } + + var prebidExt map[string]interface{} + if prebidExtInterface, exists := extMap["prebid"]; exists { + // Reference Existing Prebid Ext Map + if prebidExtMap, ok := prebidExtInterface.(map[string]interface{}); ok { + prebidExt = prebidExtMap + } else { + return nil, errors.New("request.ext.prebid is not a json object") + } + } else { + // Create New Empty Prebid Ext Map + prebidExt = make(map[string]interface{}) + extMap["prebid"] = prebidExt } - return p.Value != "" && p.Value[2] == 'Y' + prebidExt["nosale"] = noSaleBidders + return json.Marshal(extMap) } diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go index e9b4c4525b1..7ff896e9ebf 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRead(t *testing.T) { +func TestReadFromRequest(t *testing.T) { testCases := []struct { description string request *openrtb.BidRequest @@ -18,83 +18,146 @@ func TestRead(t *testing.T) { { description: "Success", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"ABC"}`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "ABC", + Consent: "ABC", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Empty - No Request", + description: "Nil Request", request: nil, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: nil, }, }, { - description: "Empty - No Regs", + description: "Nil Regs", request: &openrtb.BidRequest{ Regs: nil, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Empty - No Ext", + description: "Nil Regs.Ext", request: &openrtb.BidRequest{ Regs: &openrtb.Regs{}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Empty - No Value", + description: "Empty Regs.Ext", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"anythingElse":"42"}`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Serialization Issue", + description: "Missing Regs.Ext USPrivacy Value", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`malformed`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"anythingElse":"42"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + }, + expectedPolicy: Policy{ + Consent: "", + NoSaleBidders: []string{"a", "b"}, + }, + }, + { + description: "Malformed Regs.Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + }, + expectedError: true, + }, + { + description: "Invalid Regs.Ext Type", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + }, + expectedError: true, + }, + { + description: "Nil Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: nil, + }, + expectedPolicy: Policy{ + Consent: "ABC", + NoSaleBidders: nil, + }, + }, + { + description: "Empty Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{}`), + }, + expectedPolicy: Policy{ + Consent: "ABC", + NoSaleBidders: nil, + }, + }, + { + description: "Missing Ext.Prebid No Sale Value", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{"anythingElse":"42"}`), + }, + expectedPolicy: Policy{ + Consent: "ABC", + NoSaleBidders: nil, + }, + }, + { + description: "Malformed Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`malformed`), + }, + expectedError: true, + }, + { + description: "Invalid Ext.Prebid.NoSale Type", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":"wrongtype"}}`), }, expectedError: true, }, { description: "Injection Attack", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`)}, }, expectedPolicy: Policy{ - Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", + Consent: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", }, }, } for _, test := range testCases { - - p, e := ReadPolicy(test.request) - - if test.expectedError { - assert.Error(t, e, test.description) - } else { - assert.NoError(t, e, test.description) - } - - assert.Equal(t, test.expectedPolicy, p, test.description) + result, err := ReadFromRequest(test.request) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expectedPolicy, result, test.description) } } @@ -107,313 +170,422 @@ func TestWrite(t *testing.T) { expectedError bool }{ { - description: "Disabled", - policy: Policy{Value: ""}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{}, - }, - { - description: "Disabled - Nil Request", - policy: Policy{Value: ""}, + description: "Nil Request", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, 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: "Success", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, }, { - 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: "Error Regs.Ext - No Partial Update To Request", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + expectedError: true, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, }, { - 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: "Error Ext - No Partial Update To Request", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, + request: &openrtb.BidRequest{ + Ext: json.RawMessage(`malformed}`), + }, + expectedError: true, + expected: &openrtb.BidRequest{ + Ext: json.RawMessage(`malformed}`), + }, }, + } + + for _, test := range testCases { + err := test.policy.Write(test.request) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, test.request, test.description) + } +} + +func TestBuildRegs(t *testing.T) { + testCases := []struct { + description string + consent string + regs *openrtb.Regs + expected *openrtb.Regs + expectedError bool + }{ { - 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: "Clear", + consent: "", + regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"ABC"}`), + }, + expected: &openrtb.Regs{}, }, { - description: "Enabled - Nil Request", - policy: Policy{Value: "anyValue"}, - request: nil, - expected: nil, + description: "Clear - Error", + consent: "", + regs: &openrtb.Regs{ + Ext: json.RawMessage(`malformed`), + }, + expectedError: true, }, { - description: "Enabled With Nil Request Regs Object", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}}, + description: "Write", + consent: "anyConsent", + regs: nil, + expected: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`), + }, }, { - description: "Enabled With Nil Request Regs Ext Object", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{}}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}}, + description: "Write - Error", + consent: "anyConsent", + regs: &openrtb.Regs{ + Ext: json.RawMessage(`malformed`), + }, + expectedError: true, }, + } + + for _, test := range testCases { + result, err := buildRegs(test.consent, test.regs) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestBuildRegsClear(t *testing.T) { + testCases := []struct { + description string + regs *openrtb.Regs + expected *openrtb.Regs + expectedError bool + }{ { - description: "Enabled With Existing Request Regs Ext Object - Doesn't Overwrite", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any"}`)}}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}}, + description: "Nil Regs", + regs: nil, + expected: nil, }, { - description: "Enabled With Existing Request Regs Ext Object - Overwrites", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeOverwritten"}`)}}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}}, + description: "Nil Regs.Ext", + regs: &openrtb.Regs{Ext: nil}, + expected: &openrtb.Regs{Ext: nil}, }, { - description: "Enabled With Existing Malformed Request Regs Ext Object", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`malformed`)}}, - expectedError: true, + description: "Empty Regs.Ext", + regs: &openrtb.Regs{Ext: json.RawMessage(`{}`)}, + expected: &openrtb.Regs{}, }, { - 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: "Removes Regs.Ext Entirely", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + expected: &openrtb.Regs{}, + }, + { + description: "Leaves Other Regs.Ext Values", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC", "other":"any"}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"other":"any"}`)}, }, { - 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: "Invalid Regs.Ext Type - Still Cleared", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expected: &openrtb.Regs{}, }, { - 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\":\""}`), - }}, + description: "Malformed Regs.Ext", + regs: &openrtb.Regs{Ext: json.RawMessage(`malformed`)}, + expectedError: true, }, } for _, test := range testCases { - err := test.policy.Write(test.request) - - if test.expectedError { - assert.Error(t, err, test.description) - } else { - assert.NoError(t, err, test.description) - assert.Equal(t, test.expected, test.request, test.description) - } + result, err := buildRegsClear(test.regs) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) } } -func TestValidate(t *testing.T) { +func TestBuildRegsWrite(t *testing.T) { testCases := []struct { description string - policy Policy - expectedError string + consent string + regs *openrtb.Regs + expected *openrtb.Regs + expectedError bool }{ { - description: "Valid", - policy: Policy{Value: "1NYN"}, - expectedError: "", + description: "Nil Regs", + consent: "anyConsent", + regs: nil, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Valid - Not Applicable", - policy: Policy{Value: "1---"}, - expectedError: "", + description: "Nil Regs.Ext", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: nil}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Valid - Empty", - policy: Policy{Value: ""}, - expectedError: "", + description: "Empty Regs.Ext", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Invalid Length", - policy: Policy{Value: "1NY"}, - expectedError: "request.regs.ext.us_privacy must contain 4 characters", + description: "Overwrites Existing", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Invalid Version", - policy: Policy{Value: "2---"}, - expectedError: "request.regs.ext.us_privacy must specify version 1", + description: "Leaves Other Ext Values", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"other":"any"}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"other":"any","us_privacy":"anyConsent"}`)}, }, { - 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 Regs.Ext Type - Still Overwrites", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - 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: "Malformed Regs.Ext", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`malformed`)}, + expectedError: true, }, + } + + for _, test := range testCases { + result, err := buildRegsWrite(test.consent, test.regs) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestBuildExt(t *testing.T) { + testCases := []struct { + description string + noSaleBidders []string + ext json.RawMessage + expected json.RawMessage + expectedError bool + }{ { - 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: "Clear - Nil", + noSaleBidders: nil, + ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + expected: nil, }, { - 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: "Clear - Empty", + noSaleBidders: []string{}, + ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + expected: nil, }, { - 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: "Clear - Error", + noSaleBidders: []string{}, + ext: json.RawMessage(`malformed`), + expectedError: true, }, { - 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", + description: "Write", + noSaleBidders: []string{"a", "b"}, + ext: nil, + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Write - Error", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`malformed`), + expectedError: true, }, } for _, test := range testCases { - result := test.policy.Validate() - - if test.expectedError == "" { - assert.NoError(t, result, test.description) - } else { - assert.EqualError(t, result, test.expectedError, test.description) - } + result, err := buildExt(test.noSaleBidders, test.ext) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) } } -func TestValidateConsent(t *testing.T) { +func TestBuildExtClear(t *testing.T) { testCases := []struct { description string - consent string - expectedError string + ext json.RawMessage + expected json.RawMessage + expectedError bool }{ { - description: "Valid", - consent: "1NYN", - expectedError: "", + description: "Nil Ext", + ext: nil, + expected: nil, }, { - description: "Valid - Not Applicable", - consent: "1---", - expectedError: "", + description: "Empty Ext", + ext: json.RawMessage(``), + expected: json.RawMessage(``), }, { - description: "Invalid Empty", - consent: "", - expectedError: "", + description: "Empty Ext Object", + ext: json.RawMessage(`{}`), + expected: json.RawMessage(`{}`), }, { - description: "Invalid Length", - consent: "1NY", - expectedError: "must contain 4 characters", + description: "Empty Ext.Prebid", + ext: json.RawMessage(`{"prebid":{}}`), + expected: nil, }, { - description: "Invalid Version", - consent: "2---", - expectedError: "must specify version 1", + description: "Removes Ext Entirely", + ext: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + expected: nil, }, { - description: "Invalid Explicit Notice Char", - consent: "1X--", - expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + description: "Leaves Other Ext Values", + ext: json.RawMessage(`{"other":"any","prebid":{"nosale":["a","b"]}}`), + expected: json.RawMessage(`{"other":"any"}`), }, { - description: "Invalid Explicit Notice Case", - consent: "1y--", - expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + description: "Leaves Other Ext.Prebid Values", + ext: json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`), + expected: json.RawMessage(`{"prebid":{"other":"any"}}`), }, { - description: "Invalid Opt-Out Sale Char", - consent: "1-X-", - expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + description: "Leaves All Other Values", + ext: json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`), + expected: json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`), }, { - description: "Invalid Opt-Out Sale Case", - consent: "1-y-", - expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + description: "Malformed Ext", + ext: json.RawMessage(`malformed`), + expectedError: true, }, { - description: "Invalid LSPA Char", - consent: "1--X", - expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + description: "Malformed Ext.Prebid", + ext: json.RawMessage(`{"prebid":malformed}`), + expectedError: true, }, { - description: "Invalid LSPA Case", - consent: "1--y", - expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + description: "Invalid Ext.Prebid Type", + ext: json.RawMessage(`{"prebid":123}`), + expectedError: true, }, } for _, test := range testCases { - result := ValidateConsent(test.consent) - - if test.expectedError == "" { - assert.NoError(t, result, test.description) - } else { - assert.EqualError(t, result, test.expectedError, test.description) - } + result, err := buildExtClear(test.ext) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) } } -func TestShouldEnforce(t *testing.T) { +func TestBuildExtWrite(t *testing.T) { testCases := []struct { - description string - policy Policy - expected bool + description string + noSaleBidders []string + ext json.RawMessage + expected json.RawMessage + expectedError bool }{ { - description: "Enforceable", - policy: Policy{Value: "1-Y-"}, - expected: true, + description: "Nil Ext", + noSaleBidders: []string{"a", "b"}, + ext: nil, + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Empty Ext", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(``), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Not Enforceable - Not Present", - policy: Policy{Value: ""}, - expected: false, + description: "Empty Ext Object", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Not Enforceable - Opt-Out Unknown", - policy: Policy{Value: "1---"}, - expected: false, + description: "Empty Ext.Prebid", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Not Enforceable - Opt-Out Explicitly No", - policy: Policy{Value: "1-N-"}, - expected: false, + description: "Overwrites Existing", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{"nosale":["x","y"]}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Invalid", - policy: Policy{Value: "2---"}, - expected: false, + description: "Leaves Other Ext Values", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"other":"any"}`), + expected: json.RawMessage(`{"other":"any","prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Leaves Other Ext.Prebid Values", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{"other":"any"}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`), + }, + { + description: "Leaves All Other Values", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`), + expected: json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`), + }, + { + description: "Invalid Ext.Prebid No Sale Type - Still Overrides", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{"nosale":123}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Invalid Ext.Prebid Type ", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":"wrongtype"}`), + expectedError: true, + }, + { + description: "Malformed Ext", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{malformed`), + expectedError: true, + }, + { + description: "Malformed Ext.Prebid", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":malformed}`), + expectedError: true, }, } for _, test := range testCases { - result := test.policy.ShouldEnforce() + result, err := buildExtWrite(test.noSaleBidders, test.ext) + assertError(t, test.expectedError, err, test.description) assert.Equal(t, test.expected, result, test.description) } } + +func assertError(t *testing.T, expectError bool, err error, description string) { + t.Helper() + if expectError { + assert.Error(t, err, description) + } else { + assert.NoError(t, err, description) + } +} diff --git a/privacy/enforcer.go b/privacy/enforcer.go new file mode 100644 index 00000000000..0d5ecad5309 --- /dev/null +++ b/privacy/enforcer.go @@ -0,0 +1,43 @@ +package privacy + +// PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy. +type PolicyEnforcer interface { + // CanEnforce returns true when policy information is specifically provided by the publisher. + CanEnforce() bool + + // ShouldEnforce returns true when the OpenRTB request should have personally identifiable + // information (PII) removed or anonymized per the policy. + ShouldEnforce(bidder string) bool +} + +// NilPolicyEnforcer implements the PolicyEnforcer interface but will always return false. +type NilPolicyEnforcer struct{} + +// CanEnforce is hardcoded to always return false. +func (NilPolicyEnforcer) CanEnforce() bool { + return false +} + +// ShouldEnforce is hardcoded to always return false. +func (NilPolicyEnforcer) ShouldEnforce(bidder string) bool { + return false +} + +// EnabledPolicyEnforcer decorates a PolicyEnforcer with an enabled flag. +type EnabledPolicyEnforcer struct { + Enabled bool + PolicyEnforcer PolicyEnforcer +} + +// CanEnforce returns true when the PolicyEnforcer can enforce. +func (p EnabledPolicyEnforcer) CanEnforce() bool { + return p.PolicyEnforcer.CanEnforce() +} + +// ShouldEnforce returns true when the enforcer is enabled the PolicyEnforcer allows enforcement. +func (p EnabledPolicyEnforcer) ShouldEnforce(bidder string) bool { + if p.Enabled { + return p.PolicyEnforcer.ShouldEnforce(bidder) + } + return false +} diff --git a/privacy/enforcer_test.go b/privacy/enforcer_test.go new file mode 100644 index 00000000000..b0c4032c714 --- /dev/null +++ b/privacy/enforcer_test.go @@ -0,0 +1,18 @@ +package privacy + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNilEnforcerCanEnforce(t *testing.T) { + nilEnforcer := &NilPolicyEnforcer{} + assert.False(t, nilEnforcer.CanEnforce()) +} + +func TestNilEnforcerShouldEnforce(t *testing.T) { + nilEnforcer := &NilPolicyEnforcer{} + assert.False(t, nilEnforcer.ShouldEnforce("")) + assert.False(t, nilEnforcer.ShouldEnforce("anyBidder")) +} diff --git a/privacy/gdpr/consentwriter.go b/privacy/gdpr/consentwriter.go new file mode 100644 index 00000000000..040bbd6c94b --- /dev/null +++ b/privacy/gdpr/consentwriter.go @@ -0,0 +1,44 @@ +package gdpr + +import ( + "encoding/json" + + "github.com/prebid/prebid-server/openrtb_ext" + + "github.com/mxmCherry/openrtb" +) + +// ConsentWriter implements the PolicyWriter interface for GDPR TCF. +type ConsentWriter struct { + Consent string +} + +// Write mutates an OpenRTB bid request with the GDPR TCF consent. +func (c ConsentWriter) Write(req *openrtb.BidRequest) error { + if c.Consent == "" { + return nil + } + + if req.User == nil { + req.User = &openrtb.User{} + } + + if req.User.Ext == nil { + ext, err := json.Marshal(openrtb_ext.ExtUser{Consent: c.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"] = c.Consent + ext, err := json.Marshal(extMap) + if err == nil { + req.User.Ext = ext + } + } + return err +} diff --git a/privacy/gdpr/consentwriter_test.go b/privacy/gdpr/consentwriter_test.go new file mode 100644 index 00000000000..77fbdf88d92 --- /dev/null +++ b/privacy/gdpr/consentwriter_test.go @@ -0,0 +1,101 @@ +package gdpr + +import ( + "encoding/json" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestConsentWriter(t *testing.T) { + testCases := []struct { + description string + consent string + request *openrtb.BidRequest + expected *openrtb.BidRequest + expectedError bool + }{ + { + description: "Empty", + consent: "", + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{}, + }, + { + description: "Enabled With Nil Request User Object", + consent: "anyConsent", + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, + }, + { + description: "Enabled With Nil Request User Ext Object", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{}}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, + }, + { + description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"existing":"any"}`)}}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, + }, + { + description: "Enabled With Existing Request User Ext Object - Overwrites", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, + }, + { + description: "Enabled With Existing Malformed Request User Ext Object", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`malformed`)}}, + expectedError: true, + }, + { + description: "Injection Attack With Nil Request User Object", + 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", + 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", + 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 { + writer := ConsentWriter{test.consent} + err := writer.Write(test.request) + + if test.expectedError { + assert.Error(t, err, test.description) + } else { + assert.NoError(t, err, test.description) + assert.Equal(t, test.expected, test.request, test.description) + } + } +} diff --git a/privacy/gdpr/policy.go b/privacy/gdpr/policy.go index 4733e1edd38..0464a9ff979 100644 --- a/privacy/gdpr/policy.go +++ b/privacy/gdpr/policy.go @@ -1,10 +1,6 @@ package gdpr import ( - "encoding/json" - "github.com/prebid/prebid-server/openrtb_ext" - - "github.com/mxmCherry/openrtb" "github.com/prebid/go-gdpr/vendorconsent" ) @@ -14,38 +10,8 @@ type Policy struct { Consent string } -// Write mutates an OpenRTB bid request with the context of the GDPR policy. -func (p Policy) Write(req *openrtb.BidRequest) error { - if p.Consent == "" { - return nil - } - - if req.User == nil { - req.User = &openrtb.User{} - } - - if req.User.Ext == 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 -} - -// ValidateConsent returns an error if the GDPR consent string does not adhere to the IAB TCF spec. -func ValidateConsent(consent string) error { +// ValidateConsent returns true if the consent string is empty or valid per the IAB TCF spec. +func ValidateConsent(consent string) bool { _, err := vendorconsent.ParseString(consent) - return err + return err == nil } diff --git a/privacy/gdpr/policy_test.go b/privacy/gdpr/policy_test.go index c9bf10cd24a..dc8f56425c5 100644 --- a/privacy/gdpr/policy_test.go +++ b/privacy/gdpr/policy_test.go @@ -1,129 +1,36 @@ package gdpr import ( - "encoding/json" "testing" - "github.com/mxmCherry/openrtb" "github.com/stretchr/testify/assert" ) -func TestWrite(t *testing.T) { - testCases := []struct { - description string - policy Policy - request *openrtb.BidRequest - expected *openrtb.BidRequest - expectedError bool - }{ - { - description: "Disabled", - policy: Policy{Consent: ""}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{}, - }, - { - description: "Enabled With Nil Request User Object", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, - }, - { - description: "Enabled With Nil Request User Ext Object", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{}}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, - }, - { - description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"existing":"any"}`)}}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, - }, - { - description: "Enabled With Existing Request User Ext Object - Overwrites", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, - }, - { - description: "Enabled With Existing Malformed Request User Ext Object", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{ - 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 { - err := test.policy.Write(test.request) - - if test.expectedError { - assert.Error(t, err, test.description) - } else { - assert.NoError(t, err, test.description) - assert.Equal(t, test.expected, test.request, test.description) - } - } -} - func TestValidateConsent(t *testing.T) { testCases := []struct { description string consent string - expectError bool + expected bool }{ { description: "Invalid", consent: "", - expectError: true, + expected: false, }, { - description: "Valid", + description: "TCF1 Valid", consent: "BONV8oqONXwgmADACHENAO7pqzAAppY", - expectError: false, + expected: true, + }, + { + description: "TCF2 Valid", + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + expected: true, }, } for _, test := range testCases { result := ValidateConsent(test.consent) - - if test.expectError { - assert.Error(t, result, test.description) - } else { - assert.NoError(t, result, test.description) - } + assert.Equal(t, test.expected, result, test.description) } } diff --git a/privacy/lmt/policy.go b/privacy/lmt/policy.go index 79425bf59f7..295dcc46469 100644 --- a/privacy/lmt/policy.go +++ b/privacy/lmt/policy.go @@ -15,19 +15,21 @@ type Policy struct { SignalProvided bool } -// ReadPolicy extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request. -func ReadPolicy(req *openrtb.BidRequest) Policy { - policy := Policy{} - +// ReadFromRequest extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request. +func ReadFromRequest(req *openrtb.BidRequest) (policy Policy) { if req != nil && req.Device != nil && req.Device.Lmt != nil { policy.Signal = int(*req.Device.Lmt) policy.SignalProvided = true } + return +} - return policy +// CanEnforce returns true the LMT (Limit Ad Tracking) signal is provided by the publisher. +func (p Policy) CanEnforce() bool { + return p.SignalProvided } // ShouldEnforce returns true when the LMT (Limit Ad Tracking) policy is in effect. -func (p Policy) ShouldEnforce() bool { +func (p Policy) ShouldEnforce(bidder string) bool { return p.SignalProvided && p.Signal == trackingRestricted } diff --git a/privacy/lmt/policy_test.go b/privacy/lmt/policy_test.go index 45de219a9bf..3027414fd02 100644 --- a/privacy/lmt/policy_test.go +++ b/privacy/lmt/policy_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRead(t *testing.T) { +func TestReadFromRequest(t *testing.T) { var one int8 = 1 testCases := []struct { @@ -60,11 +60,73 @@ func TestRead(t *testing.T) { } for _, test := range testCases { - p := ReadPolicy(test.request) + p := ReadFromRequest(test.request) assert.Equal(t, test.expectedPolicy, p, test.description) } } +func TestCanEnforce(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: true, + }, + { + description: "Signal Provided - One", + policy: Policy{ + Signal: 1, + SignalProvided: true, + }, + expected: true, + }, + { + description: "Signal Provided - Other", + policy: Policy{ + Signal: 42, + SignalProvided: true, + }, + expected: true, + }, + } + + for _, test := range testCases { + result := test.policy.CanEnforce() + assert.Equal(t, test.expected, result, test.description) + } +} + func TestShouldEnforce(t *testing.T) { testCases := []struct { description string @@ -122,7 +184,7 @@ func TestShouldEnforce(t *testing.T) { } for _, test := range testCases { - result := test.policy.ShouldEnforce() + result := test.policy.ShouldEnforce("") assert.Equal(t, test.expected, result, test.description) } } diff --git a/privacy/policies.go b/privacy/policies.go index cb11c6d03a6..bc844a4e463 100644 --- a/privacy/policies.go +++ b/privacy/policies.go @@ -1,60 +1,14 @@ package privacy import ( - "github.com/mxmCherry/openrtb" - "github.com/prebid/prebid-server/privacy/ccpa" "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/prebid/prebid-server/privacy/lmt" ) // Policies represents the privacy regulations for an OpenRTB bid request. type Policies struct { - GDPR gdpr.Policy CCPA ccpa.Policy -} - -type policyWriter interface { - Write(req *openrtb.BidRequest) error -} - -// Write mutates an OpenRTB bid request with the policies applied. -func (p Policies) Write(req *openrtb.BidRequest) error { - return writePolicies(req, []policyWriter{ - p.GDPR, p.CCPA, - }) -} - -func writePolicies(req *openrtb.BidRequest, writers []policyWriter) error { - for _, writer := range writers { - if err := writer.Write(req); err != nil { - return err - } - } - - 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 + GDPR gdpr.Policy + LMT lmt.Policy } diff --git a/privacy/policies_test.go b/privacy/policies_test.go deleted file mode 100644 index 34fbe52d0e9..00000000000 --- a/privacy/policies_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package privacy - -import ( - "errors" - "testing" - - "github.com/mxmCherry/openrtb" - "github.com/prebid/prebid-server/privacy/ccpa" - "github.com/prebid/prebid-server/privacy/gdpr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestWritePoliciesNone(t *testing.T) { - request := &openrtb.BidRequest{} - policyWriters := []policyWriter{} - - err := writePolicies(request, policyWriters) - - assert.NoError(t, err) -} - -func TestWritePoliciesOne(t *testing.T) { - request := &openrtb.BidRequest{} - mockWriter := new(mockPolicyWriter) - policyWriters := []policyWriter{ - mockWriter, - } - - mockWriter.On("Write", request).Return(nil).Once() - - err := writePolicies(request, policyWriters) - - assert.NoError(t, err) - mockWriter.AssertExpectations(t) -} - -func TestWritePoliciesMany(t *testing.T) { - request := &openrtb.BidRequest{} - mockWriter1 := new(mockPolicyWriter) - mockWriter2 := new(mockPolicyWriter) - policyWriters := []policyWriter{ - mockWriter1, mockWriter2, - } - - mockWriter1.On("Write", request).Return(nil).Once() - mockWriter2.On("Write", request).Return(nil).Once() - - err := writePolicies(request, policyWriters) - - assert.NoError(t, err) - mockWriter1.AssertExpectations(t) - mockWriter2.AssertExpectations(t) -} - -func TestWritePoliciesError(t *testing.T) { - request := &openrtb.BidRequest{} - mockWriter := new(mockPolicyWriter) - policyWriters := []policyWriter{ - mockWriter, - } - - expectedErr := errors.New("anyError") - mockWriter.On("Write", request).Return(expectedErr).Once() - - err := writePolicies(request, policyWriters) - - assert.Error(t, err, expectedErr) - mockWriter.AssertExpectations(t) -} - -type mockPolicyWriter struct { - mock.Mock -} - -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/writer.go b/privacy/writer.go new file mode 100644 index 00000000000..c61767f16c8 --- /dev/null +++ b/privacy/writer.go @@ -0,0 +1,18 @@ +package privacy + +import ( + "github.com/mxmCherry/openrtb" +) + +// PolicyWriter mutates an OpenRTB bid request with a policy's regulatory information. +type PolicyWriter interface { + Write(req *openrtb.BidRequest) error +} + +// NilPolicyWriter implements the PolicyWriter interface but performs no action. +type NilPolicyWriter struct{} + +// Write is hardcoded to perform no action with the OpenRTB bid request. +func (NilPolicyWriter) Write(req *openrtb.BidRequest) error { + return nil +} diff --git a/privacy/writer_test.go b/privacy/writer_test.go new file mode 100644 index 00000000000..79170cfc451 --- /dev/null +++ b/privacy/writer_test.go @@ -0,0 +1,25 @@ +package privacy + +import ( + "encoding/json" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestNilWriter(t *testing.T) { + request := &openrtb.BidRequest{ + ID: "anyID", + Ext: json.RawMessage(`{"anyJson":"anyValue"}`), + } + expectedRequest := &openrtb.BidRequest{ + ID: "anyID", + Ext: json.RawMessage(`{"anyJson":"anyValue"}`), + } + + nilWriter := &NilPolicyWriter{} + nilWriter.Write(request) + + assert.Equal(t, expectedRequest, request) +} From 472c7a00105cb986c072047dd416af299b7f7a1b Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Thu, 17 Sep 2020 11:05:24 -0400 Subject: [PATCH 209/381] Fix Merge Conflict (#1502) --- adapters/colossus/usersync_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/colossus/usersync_test.go b/adapters/colossus/usersync_test.go index 79d5483d528..52eb6389b1c 100644 --- a/adapters/colossus/usersync_test.go +++ b/adapters/colossus/usersync_test.go @@ -23,7 +23,7 @@ func TestColossusSyncer(t *testing.T) { Consent: "A", }, CCPA: ccpa.Policy{ - Value: "1-YY", + Consent: "1-YY", }, }) From 97be47dc634d8b2fbf70c15fa1d73dc7cbf9d05d Mon Sep 17 00:00:00 2001 From: johnwier <49074029+johnwier@users.noreply.github.com> Date: Thu, 17 Sep 2020 08:29:14 -0700 Subject: [PATCH 210/381] Update conversant adapter for new prebid-server interface (#1484) --- adapters/conversant/cnvr_legacy.go | 291 ++++++ adapters/conversant/cnvr_legacy_test.go | 853 ++++++++++++++++++ adapters/conversant/conversant.go | 359 +++----- adapters/conversant/conversant_test.go | 849 +---------------- .../conversanttest/exemplary/banner.json | 113 +++ .../conversanttest/exemplary/simple_app.json | 114 +++ .../conversanttest/exemplary/video.json | 138 +++ .../supplemental/missing_cnvrext.json | 32 + .../supplemental/missing_ext.json | 30 + .../supplemental/missing_siteid.json | 35 + .../supplemental/server_badresponse.json | 57 ++ .../supplemental/server_nocontent.json | 51 ++ .../supplemental/server_unknownstatus.json | 55 ++ exchange/adapter_map.go | 3 +- openrtb_ext/imp_conversant.go | 13 + static/bidder-params/conversant.json | 4 - 16 files changed, 1908 insertions(+), 1089 deletions(-) create mode 100644 adapters/conversant/cnvr_legacy.go create mode 100644 adapters/conversant/cnvr_legacy_test.go create mode 100644 adapters/conversant/conversanttest/exemplary/banner.json create mode 100644 adapters/conversant/conversanttest/exemplary/simple_app.json create mode 100644 adapters/conversant/conversanttest/exemplary/video.json create mode 100644 adapters/conversant/conversanttest/supplemental/missing_cnvrext.json create mode 100644 adapters/conversant/conversanttest/supplemental/missing_ext.json create mode 100644 adapters/conversant/conversanttest/supplemental/missing_siteid.json create mode 100644 adapters/conversant/conversanttest/supplemental/server_badresponse.json create mode 100644 adapters/conversant/conversanttest/supplemental/server_nocontent.json create mode 100644 adapters/conversant/conversanttest/supplemental/server_unknownstatus.json create mode 100644 openrtb_ext/imp_conversant.go diff --git a/adapters/conversant/cnvr_legacy.go b/adapters/conversant/cnvr_legacy.go new file mode 100644 index 00000000000..bd121f098c0 --- /dev/null +++ b/adapters/conversant/cnvr_legacy.go @@ -0,0 +1,291 @@ +package conversant + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/pbs" + "golang.org/x/net/context/ctxhttp" +) + +type ConversantLegacyAdapter struct { + http *adapters.HTTPAdapter + URI string +} + +// Corresponds to the bidder name in cookies and requests +func (a *ConversantLegacyAdapter) Name() string { + return "conversant" +} + +// Return true so no request will be sent unless user has been sync'ed. +func (a *ConversantLegacyAdapter) SkipNoCookies() bool { + return true +} + +type conversantParams struct { + SiteID string `json:"site_id"` + Secure *int8 `json:"secure"` + TagID string `json:"tag_id"` + Position *int8 `json:"position"` + BidFloor float64 `json:"bidfloor"` + Mobile *int8 `json:"mobile"` + MIMEs []string `json:"mimes"` + API []int8 `json:"api"` + Protocols []int8 `json:"protocols"` + MaxDuration *int64 `json:"maxduration"` +} + +func (a *ConversantLegacyAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.PBSBidder) (pbs.PBSBidSlice, error) { + mediaTypes := []pbs.MediaType{pbs.MEDIA_TYPE_BANNER, pbs.MEDIA_TYPE_VIDEO} + cnvrReq, err := adapters.MakeOpenRTBGeneric(req, bidder, a.Name(), mediaTypes) + + if err != nil { + return nil, err + } + + // Create a map of impression objects for both request creation + // and response parsing. + + impMap := make(map[string]*openrtb.Imp, len(cnvrReq.Imp)) + for idx := range cnvrReq.Imp { + impMap[cnvrReq.Imp[idx].ID] = &cnvrReq.Imp[idx] + } + + // Fill in additional info from custom params + + for _, unit := range bidder.AdUnits { + var params conversantParams + + imp := impMap[unit.Code] + if imp == nil { + // Skip ad units that do not have corresponding impressions. + continue + } + + err := json.Unmarshal(unit.Params, ¶ms) + if err != nil { + return nil, &errortypes.BadInput{ + Message: err.Error(), + } + } + + // Fill in additional Site info + if params.SiteID != "" { + if cnvrReq.Site != nil { + cnvrReq.Site.ID = params.SiteID + } + if cnvrReq.App != nil { + cnvrReq.App.ID = params.SiteID + } + } + + if params.Mobile != nil && !(cnvrReq.Site == nil) { + cnvrReq.Site.Mobile = *params.Mobile + } + + // Fill in additional impression info + + imp.DisplayManager = "prebid-s2s" + imp.DisplayManagerVer = "1.0.1" + imp.BidFloor = params.BidFloor + imp.TagID = params.TagID + + var position *openrtb.AdPosition + if params.Position != nil { + position = openrtb.AdPosition(*params.Position).Ptr() + } + + if imp.Banner != nil { + imp.Banner.Pos = position + } else if imp.Video != nil { + imp.Video.Pos = position + + if len(params.API) > 0 { + imp.Video.API = make([]openrtb.APIFramework, 0, len(params.API)) + for _, api := range params.API { + imp.Video.API = append(imp.Video.API, openrtb.APIFramework(api)) + } + } + + // Include protocols, mimes, and max duration if specified + // These properties can also be specified in ad unit's video object, + // but are overridden if the custom params object also contains them. + + if len(params.Protocols) > 0 { + imp.Video.Protocols = make([]openrtb.Protocol, 0, len(params.Protocols)) + for _, protocol := range params.Protocols { + imp.Video.Protocols = append(imp.Video.Protocols, openrtb.Protocol(protocol)) + } + } + + if len(params.MIMEs) > 0 { + imp.Video.MIMEs = make([]string, len(params.MIMEs)) + copy(imp.Video.MIMEs, params.MIMEs) + } + + if params.MaxDuration != nil { + imp.Video.MaxDuration = *params.MaxDuration + } + } + + // Take care not to override the global secure flag + + if (imp.Secure == nil || *imp.Secure == 0) && params.Secure != nil { + imp.Secure = params.Secure + } + } + + // Do a quick check on required parameters + + if cnvrReq.Site != nil && cnvrReq.Site.ID == "" { + return nil, &errortypes.BadInput{ + Message: "Missing site id", + } + } + + if cnvrReq.App != nil && cnvrReq.App.ID == "" { + return nil, &errortypes.BadInput{ + Message: "Missing app id", + } + } + + // Start capturing debug info + + debug := &pbs.BidderDebug{ + RequestURI: a.URI, + } + + if cnvrReq.Device == nil { + cnvrReq.Device = &openrtb.Device{} + } + + // Convert request to json to be sent over http + + j, _ := json.Marshal(cnvrReq) + + if req.IsDebug { + debug.RequestBody = string(j) + bidder.Debug = append(bidder.Debug, debug) + } + + httpReq, err := http.NewRequest("POST", a.URI, bytes.NewBuffer(j)) + httpReq.Header.Add("Content-Type", "application/json") + httpReq.Header.Add("Accept", "application/json") + + resp, err := ctxhttp.Do(ctx, a.http.Client, httpReq) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if req.IsDebug { + debug.StatusCode = resp.StatusCode + } + + if resp.StatusCode == 204 { + return nil, nil + } + + body, err := ioutil.ReadAll(resp.Body) + + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusBadRequest { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)), + } + } + + if resp.StatusCode != 200 { + return nil, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)), + } + } + + if req.IsDebug { + debug.ResponseBody = string(body) + } + + var bidResp openrtb.BidResponse + + err = json.Unmarshal(body, &bidResp) + if err != nil { + return nil, &errortypes.BadServerResponse{ + Message: err.Error(), + } + } + + bids := make(pbs.PBSBidSlice, 0) + + for _, seatbid := range bidResp.SeatBid { + for _, bid := range seatbid.Bid { + if bid.Price <= 0 { + continue + } + + imp := impMap[bid.ImpID] + if imp == nil { + // All returned bids should have a matching impression + return nil, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unknown impression id '%s'", bid.ImpID), + } + } + + bidID := bidder.LookupBidID(bid.ImpID) + if bidID == "" { + return nil, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unknown ad unit code '%s'", bid.ImpID), + } + } + + pbsBid := pbs.PBSBid{ + BidID: bidID, + AdUnitCode: bid.ImpID, + Price: bid.Price, + Creative_id: bid.CrID, + BidderCode: bidder.BidderCode, + } + + if imp.Video != nil { + pbsBid.CreativeMediaType = "video" + pbsBid.NURL = bid.AdM // Assign to NURL so it'll be interpreted as a vastUrl + pbsBid.Width = imp.Video.W + pbsBid.Height = imp.Video.H + } else { + pbsBid.CreativeMediaType = "banner" + pbsBid.NURL = bid.NURL + pbsBid.Adm = bid.AdM + pbsBid.Width = bid.W + pbsBid.Height = bid.H + } + + bids = append(bids, &pbsBid) + } + } + + if len(bids) == 0 { + return nil, nil + } + + return bids, nil +} + +func NewConversantAdapter(config *adapters.HTTPAdapterConfig, uri string) *ConversantLegacyAdapter { + a := adapters.NewHTTPAdapter(config) + + return &ConversantLegacyAdapter{ + http: a, + URI: uri, + } +} diff --git a/adapters/conversant/cnvr_legacy_test.go b/adapters/conversant/cnvr_legacy_test.go new file mode 100644 index 00000000000..b958e320dc7 --- /dev/null +++ b/adapters/conversant/cnvr_legacy_test.go @@ -0,0 +1,853 @@ +package conversant + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/cache/dummycache" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/pbs" + "github.com/prebid/prebid-server/usersync" +) + +// Constants + +const ExpectedSiteID string = "12345" +const ExpectedDisplayManager string = "prebid-s2s" +const ExpectedBuyerUID string = "AQECT_o7M1FLbQJK8QFmAQEBAQE" +const ExpectedNURL string = "http://test.dotomi.com" +const ExpectedAdM string = "" +const ExpectedCrID string = "98765" + +const DefaultParam = `{"site_id": "12345"}` + +// Test properties of Adapter interface + +func TestConversantProperties(t *testing.T) { + an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl") + + assertNotEqual(t, an.Name(), "", "Missing family name") + assertTrue(t, an.SkipNoCookies(), "SkipNoCookies should be true") +} + +// Test empty bid requests + +func TestConversantEmptyBid(t *testing.T) { + an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl") + + ctx := context.TODO() + pbReq := pbs.PBSRequest{} + pbBidder := pbs.PBSBidder{} + _, err := an.Call(ctx, &pbReq, &pbBidder) + assertTrue(t, err != nil, "No error received for an invalid request") +} + +// Test required parameters, which is just the site id for now + +func TestConversantRequiredParameters(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent) + }), + ) + defer server.Close() + + an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, server.URL) + ctx := context.TODO() + + testParams := func(params ...string) (pbs.PBSBidSlice, error) { + req, err := CreateBannerRequest(params...) + if err != nil { + return nil, err + } + return an.Call(ctx, req, req.Bidders[0]) + } + + var err error + + if _, err = testParams(`{}`); err == nil { + t.Fatal("Failed to catch missing site id") + } +} + +// Test handling of 404 + +func TestConversantBadStatus(t *testing.T) { + // Create a test http server that returns after 2 milliseconds + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + }), + ) + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + ctx := context.TODO() + pbReq, err := CreateBannerRequest(DefaultParam) + if err != nil { + t.Fatal("Failed to create a banner request", err) + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + assertTrue(t, err != nil, "Failed to catch 404 error") +} + +// Test handling of HTTP timeout + +func TestConversantTimeout(t *testing.T) { + // Create a test http server that returns after 2 milliseconds + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + <-time.After(2 * time.Millisecond) + }), + ) + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + // Create a context that expires before http returns + + ctx, cancel := context.WithTimeout(context.Background(), 0) + defer cancel() + + // Create a basic request + pbReq, err := CreateBannerRequest(DefaultParam) + if err != nil { + t.Fatal("Failed to create a banner request", err) + } + + // Attempt to process the request, which should hit a timeout + // immediately + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err == nil || err != context.DeadlineExceeded { + t.Fatal("No timeout recevied for timed out request", err) + } +} + +// Test handling of 204 + +func TestConversantNoBid(t *testing.T) { + // Create a test http server that returns after 2 milliseconds + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent) + }), + ) + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + ctx := context.TODO() + pbReq, err := CreateBannerRequest(DefaultParam) + if err != nil { + t.Fatal("Failed to create a banner request", err) + } + + resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0]) + if resp != nil || err != nil { + t.Fatal("Failed to handle empty bid", err) + } +} + +// Verify an outgoing openrtp request is created correctly + +func TestConversantRequestDefault(t *testing.T) { + server, lastReq := CreateServer() + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + ctx := context.TODO() + pbReq, err := CreateBannerRequest(DefaultParam) + if err != nil { + t.Fatal("Failed to create a banner request", err) + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err != nil { + t.Fatal("Failed to retrieve bids", err) + } + + assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") + imp := &lastReq.Imp[0] + + assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") + assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") + assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") + assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") + assertTrue(t, imp.Video == nil, "Request video should be nil") + assertEqual(t, int(*imp.Secure), 0, "Request secure") + assertEqual(t, imp.BidFloor, 0.0, "Request bid floor") + assertEqual(t, imp.TagID, "", "Request tag id") + assertTrue(t, imp.Banner.Pos == nil, "Request pos") + assertEqual(t, int(*imp.Banner.W), 300, "Request width") + assertEqual(t, int(*imp.Banner.H), 250, "Request height") +} + +// Verify inapp video request +func TestConversantInappVideoRequest(t *testing.T) { + server, lastReq := CreateServer() + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + requestParam := `{"secure": 1, "site_id": "12345"}` + appParam := `{ "bundle": "com.naver.linewebtoon" }` + videoParam := `{ "mimes": ["video/x-ms-wmv"], + "protocols": [1, 2], + "maxduration": 90 }` + + ctx := context.TODO() + pbReq := CreateRequest(requestParam) + pbReq, err := ConvertToVideoRequest(pbReq, videoParam) + if err != nil { + t.Fatal("failed to parse request") + } + pbReq, err = ConvertToAppRequest(pbReq, appParam) + if err != nil { + t.Fatal("failed to parse request") + } + pbReq, err = ParseRequest(pbReq) + if err != nil { + t.Fatal("failed to parse request") + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + + imp := &lastReq.Imp[0] + assertEqual(t, int(imp.Video.W), 300, "Request width") + assertEqual(t, int(imp.Video.H), 250, "Request height") + assertEqual(t, lastReq.App.ID, "12345", "App Id") +} + +// Verify inapp video request +func TestConversantInappBannerRequest(t *testing.T) { + server, lastReq := CreateServer() + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + param := `{ "secure": 1, + "site_id": "12345", + "tag_id": "top", + "position": 2, + "bidfloor": 1.01 }` + appParam := `{ "bundle": "com.naver.linewebtoon" }` + + ctx := context.TODO() + pbReq, _ := CreateBannerRequest(param) + pbReq, err := ConvertToAppRequest(pbReq, appParam) + if err != nil { + t.Fatal("failed to parse request") + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + + imp := &lastReq.Imp[0] + assertEqual(t, lastReq.App.ID, "12345", "App Id") + assertEqual(t, int(*imp.Banner.W), 300, "Request width") + assertEqual(t, int(*imp.Banner.H), 250, "Request height") +} + +// Verify an outgoing openrtp request with additional conversant parameters is +// processed correctly + +func TestConversantRequest(t *testing.T) { + server, lastReq := CreateServer() + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + param := `{ "site_id": "12345", + "secure": 1, + "tag_id": "top", + "position": 2, + "bidfloor": 1.01, + "mobile": 1 }` + + ctx := context.TODO() + pbReq, err := CreateBannerRequest(param) + if err != nil { + t.Fatal("Failed to create a banner request", err) + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err != nil { + t.Fatal("Failed to retrieve bids", err) + } + + assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") + imp := &lastReq.Imp[0] + + assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") + assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") + assertEqual(t, int(lastReq.Site.Mobile), 1, "Request site mobile flag") + assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") + assertTrue(t, imp.Video == nil, "Request video should be nil") + assertEqual(t, int(*imp.Secure), 1, "Request secure") + assertEqual(t, imp.BidFloor, 1.01, "Request bid floor") + assertEqual(t, imp.TagID, "top", "Request tag id") + assertEqual(t, int(*imp.Banner.Pos), 2, "Request pos") + assertEqual(t, int(*imp.Banner.W), 300, "Request width") + assertEqual(t, int(*imp.Banner.H), 250, "Request height") +} + +// Verify openrtp responses are converted correctly + +func TestConversantResponse(t *testing.T) { + prices := []float64{0.01, 0.0, 2.01} + server, lastReq := CreateServer(prices...) + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + param := `{ "site_id": "12345", + "secure": 1, + "tag_id": "top", + "position": 2, + "bidfloor": 1.01, + "mobile" : 1}` + + ctx := context.TODO() + pbReq, err := CreateBannerRequest(param, param, param) + if err != nil { + t.Fatal("Failed to create a banner request", err) + } + + resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err != nil { + t.Fatal("Failed to retrieve bids", err) + } + + prices, imps := FilterZeroPrices(prices, lastReq.Imp) + + assertEqual(t, len(resp), len(prices), "Bad number of responses") + + for i, bid := range resp { + assertEqual(t, bid.Price, prices[i], "Bad price in response") + assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response") + + if bid.Price > 0 { + assertEqual(t, bid.Adm, ExpectedAdM, "Bad ad markup in response") + assertEqual(t, bid.NURL, ExpectedNURL, "Bad notification url in response") + assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response") + assertEqual(t, bid.Width, *imps[i].Banner.W, "Bad width in response") + assertEqual(t, bid.Height, *imps[i].Banner.H, "Bad height in response") + } + } +} + +// Test video request + +func TestConversantBasicVideoRequest(t *testing.T) { + server, lastReq := CreateServer() + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + param := `{ "site_id": "12345", + "tag_id": "bottom left", + "position": 3, + "bidfloor": 1.01 }` + + ctx := context.TODO() + pbReq, err := CreateVideoRequest(param) + if err != nil { + t.Fatal("Failed to create a video request", err) + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err != nil { + t.Fatal("Failed to retrieve bids", err) + } + + assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") + imp := &lastReq.Imp[0] + + assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") + assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") + assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") + assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") + assertTrue(t, imp.Banner == nil, "Request banner should be nil") + assertEqual(t, int(*imp.Secure), 0, "Request secure") + assertEqual(t, imp.BidFloor, 1.01, "Request bid floor") + assertEqual(t, imp.TagID, "bottom left", "Request tag id") + assertEqual(t, int(*imp.Video.Pos), 3, "Request pos") + assertEqual(t, int(imp.Video.W), 300, "Request width") + assertEqual(t, int(imp.Video.H), 250, "Request height") + + assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries") + assertEqual(t, imp.Video.MIMEs[0], "video/mp4", "Requst video MIMEs type") + assertTrue(t, imp.Video.Protocols == nil, "Request video protocols") + assertEqual(t, imp.Video.MaxDuration, int64(0), "Request video 0 max duration") + assertTrue(t, imp.Video.API == nil, "Request video api should be nil") +} + +// Test video request with parameters in custom params object + +func TestConversantVideoRequestWithParams(t *testing.T) { + server, lastReq := CreateServer() + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + param := `{ "site_id": "12345", + "tag_id": "bottom left", + "position": 3, + "bidfloor": 1.01, + "mimes": ["video/x-ms-wmv"], + "protocols": [1, 2], + "api": [1, 2], + "maxduration": 90 }` + + ctx := context.TODO() + pbReq, err := CreateVideoRequest(param) + if err != nil { + t.Fatal("Failed to create a video request", err) + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err != nil { + t.Fatal("Failed to retrieve bids", err) + } + + assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") + imp := &lastReq.Imp[0] + + assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") + assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") + assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") + assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") + assertTrue(t, imp.Banner == nil, "Request banner should be nil") + assertEqual(t, int(*imp.Secure), 0, "Request secure") + assertEqual(t, imp.BidFloor, 1.01, "Request bid floor") + assertEqual(t, imp.TagID, "bottom left", "Request tag id") + assertEqual(t, int(*imp.Video.Pos), 3, "Request pos") + assertEqual(t, int(imp.Video.W), 300, "Request width") + assertEqual(t, int(imp.Video.H), 250, "Request height") + + assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries") + assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type") + assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols") + assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1") + assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2") + assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration") + assertEqual(t, len(imp.Video.API), 2, "Request video api should be nil") + assertEqual(t, imp.Video.API[0], openrtb.APIFramework(1), "Request video api 1") + assertEqual(t, imp.Video.API[1], openrtb.APIFramework(2), "Request video api 2") +} + +// Test video request with parameters in the video object + +func TestConversantVideoRequestWithParams2(t *testing.T) { + server, lastReq := CreateServer() + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + param := `{ "site_id": "12345" }` + videoParam := `{ "mimes": ["video/x-ms-wmv"], + "protocols": [1, 2], + "maxduration": 90 }` + + ctx := context.TODO() + pbReq := CreateRequest(param) + pbReq, err := ConvertToVideoRequest(pbReq, videoParam) + if err != nil { + t.Fatal("Failed to convert to a video request", err) + } + pbReq, err = ParseRequest(pbReq) + if err != nil { + t.Fatal("Failed to parse video request", err) + } + + _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err != nil { + t.Fatal("Failed to retrieve bids", err) + } + + assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") + imp := &lastReq.Imp[0] + + assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") + assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") + assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") + assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") + assertTrue(t, imp.Banner == nil, "Request banner should be nil") + assertEqual(t, int(*imp.Secure), 0, "Request secure") + assertEqual(t, imp.BidFloor, 0.0, "Request bid floor") + assertEqual(t, int(imp.Video.W), 300, "Request width") + assertEqual(t, int(imp.Video.H), 250, "Request height") + + assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries") + assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type") + assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols") + assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1") + assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2") + assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration") +} + +// Test video responses + +func TestConversantVideoResponse(t *testing.T) { + prices := []float64{0.01, 0.0, 2.01} + server, lastReq := CreateServer(prices...) + if server == nil { + t.Fatal("server not created") + } + + defer server.Close() + + // Create a adapter to test + + conf := *adapters.DefaultHTTPAdapterConfig + an := NewConversantAdapter(&conf, server.URL) + + param := `{ "site_id": "12345", + "secure": 1, + "tag_id": "top", + "position": 2, + "bidfloor": 1.01, + "mobile" : 1}` + + ctx := context.TODO() + pbReq, err := CreateVideoRequest(param, param, param) + if err != nil { + t.Fatal("Failed to create a video request", err) + } + + resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0]) + if err != nil { + t.Fatal("Failed to retrieve bids", err) + } + + prices, imps := FilterZeroPrices(prices, lastReq.Imp) + + assertEqual(t, len(resp), len(prices), "Bad number of responses") + + for i, bid := range resp { + assertEqual(t, bid.Price, prices[i], "Bad price in response") + assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response") + + if bid.Price > 0 { + assertEqual(t, bid.Adm, "", "Bad ad markup in response") + assertEqual(t, bid.NURL, ExpectedAdM, "Bad notification url in response") + assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response") + assertEqual(t, bid.Width, imps[i].Video.W, "Bad width in response") + assertEqual(t, bid.Height, imps[i].Video.H, "Bad height in response") + } + } +} + +// Helpers to create a banner and video requests + +func CreateRequest(params ...string) *pbs.PBSRequest { + num := len(params) + + req := pbs.PBSRequest{ + Tid: "t-000", + AccountID: "1", + AdUnits: make([]pbs.AdUnit, num), + } + + for i := 0; i < num; i++ { + req.AdUnits[i] = pbs.AdUnit{ + Code: fmt.Sprintf("au-%03d", i), + Sizes: []openrtb.Format{ + { + W: 300, + H: 250, + }, + }, + Bids: []pbs.Bids{ + { + BidderCode: "conversant", + BidID: fmt.Sprintf("b-%03d", i), + Params: json.RawMessage(params[i]), + }, + }, + } + } + + return &req +} + +// Convert a request to a video request by adding required properties + +func ConvertToVideoRequest(req *pbs.PBSRequest, videoParams ...string) (*pbs.PBSRequest, error) { + for i := 0; i < len(req.AdUnits); i++ { + video := pbs.PBSVideo{} + if i < len(videoParams) { + err := json.Unmarshal([]byte(videoParams[i]), &video) + if err != nil { + return nil, err + } + } + + if video.Mimes == nil { + video.Mimes = []string{"video/mp4"} + } + + req.AdUnits[i].Video = video + req.AdUnits[i].MediaTypes = []string{"video"} + } + + return req, nil +} + +// Convert a request to an app request by adding required properties +func ConvertToAppRequest(req *pbs.PBSRequest, appParams string) (*pbs.PBSRequest, error) { + app := new(openrtb.App) + err := json.Unmarshal([]byte(appParams), &app) + if err == nil { + req.App = app + } + + return req, nil +} + +// Feed the request thru the prebid parser so user id and +// other private properties are defined + +func ParseRequest(req *pbs.PBSRequest) (*pbs.PBSRequest, error) { + body := new(bytes.Buffer) + _ = json.NewEncoder(body).Encode(req) + + // Need to pass the conversant user id thru uid cookie + + httpReq := httptest.NewRequest("POST", "/foo", body) + cookie := usersync.NewPBSCookie() + _ = cookie.TrySync("conversant", ExpectedBuyerUID) + httpReq.Header.Set("Cookie", cookie.ToHTTPCookie(90*24*time.Hour).String()) + httpReq.Header.Add("Referer", "http://example.com") + cache, _ := dummycache.New() + hcc := config.HostCookie{} + + parsedReq, err := pbs.ParsePBSRequest(httpReq, &config.AuctionTimeouts{ + Default: 2000, + Max: 2000, + }, cache, &hcc) + + return parsedReq, err +} + +// A helper to create a banner request + +func CreateBannerRequest(params ...string) (*pbs.PBSRequest, error) { + req := CreateRequest(params...) + req, err := ParseRequest(req) + return req, err +} + +// A helper to create a video request + +func CreateVideoRequest(params ...string) (*pbs.PBSRequest, error) { + req := CreateRequest(params...) + req, err := ConvertToVideoRequest(req) + if err != nil { + return nil, err + } + req, err = ParseRequest(req) + return req, err +} + +// Helper to create a test http server that receives and generate openrtb requests and responses + +func CreateServer(prices ...float64) (*httptest.Server, *openrtb.BidRequest) { + var lastBidRequest openrtb.BidRequest + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var bidReq openrtb.BidRequest + var price float64 + var bids []openrtb.Bid + var bid openrtb.Bid + + err = json.Unmarshal(body, &bidReq) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + lastBidRequest = bidReq + + for i, imp := range bidReq.Imp { + if i < len(prices) { + price = prices[i] + } else { + price = 0 + } + + if price > 0 { + bid = openrtb.Bid{ + ID: imp.ID, + ImpID: imp.ID, + Price: price, + NURL: ExpectedNURL, + AdM: ExpectedAdM, + CrID: ExpectedCrID, + } + + if imp.Banner != nil { + bid.W = *imp.Banner.W + bid.H = *imp.Banner.H + } else if imp.Video != nil { + bid.W = imp.Video.W + bid.H = imp.Video.H + } + } else { + bid = openrtb.Bid{ + ID: imp.ID, + ImpID: imp.ID, + Price: 0, + } + } + + bids = append(bids, bid) + } + + if len(bids) == 0 { + w.WriteHeader(http.StatusNoContent) + } else { + js, _ := json.Marshal(openrtb.BidResponse{ + ID: bidReq.ID, + SeatBid: []openrtb.SeatBid{ + { + Bid: bids, + }, + }, + }) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(js) + } + }), + ) + + return server, &lastBidRequest +} + +// Helper to remove impressions with $0 bids + +func FilterZeroPrices(prices []float64, imps []openrtb.Imp) ([]float64, []openrtb.Imp) { + prices2 := make([]float64, 0) + imps2 := make([]openrtb.Imp, 0) + + for i := range prices { + if prices[i] > 0 { + prices2 = append(prices2, prices[i]) + imps2 = append(imps2, imps[i]) + } + } + + return prices2, imps2 +} + +// Helpers to test equality + +func assertEqual(t *testing.T, actual interface{}, expected interface{}, msg string) { + if expected != actual { + msg = fmt.Sprintf("%s: act(%v) != exp(%v)", msg, actual, expected) + t.Fatal(msg) + } +} + +func assertNotEqual(t *testing.T, actual interface{}, expected interface{}, msg string) { + if expected == actual { + msg = fmt.Sprintf("%s: act(%v) == exp(%v)", msg, actual, expected) + t.Fatal(msg) + } +} + +func assertTrue(t *testing.T, val bool, msg string) { + if val == false { + msg = fmt.Sprintf("%s: is false but should be true", msg) + t.Fatal(msg) + } +} + +func assertFalse(t *testing.T, val bool, msg string) { + if val == true { + msg = fmt.Sprintf("%s: is true but should be false", msg) + t.Fatal(msg) + } +} diff --git a/adapters/conversant/conversant.go b/adapters/conversant/conversant.go index b248e2e9dc1..64ddc38bf1a 100644 --- a/adapters/conversant/conversant.go +++ b/adapters/conversant/conversant.go @@ -1,291 +1,176 @@ package conversant import ( - "bytes" - "context" "encoding/json" "fmt" - "io/ioutil" - "net/http" - "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" - "github.com/prebid/prebid-server/pbs" - "golang.org/x/net/context/ctxhttp" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" ) type ConversantAdapter struct { - http *adapters.HTTPAdapter - URI string -} - -// Corresponds to the bidder name in cookies and requests -func (a *ConversantAdapter) Name() string { - return "conversant" + URI string } -// Return true so no request will be sent unless user has been sync'ed. -func (a *ConversantAdapter) SkipNoCookies() bool { - return true -} - -type conversantParams struct { - SiteID string `json:"site_id"` - Secure *int8 `json:"secure"` - TagID string `json:"tag_id"` - Position *int8 `json:"position"` - BidFloor float64 `json:"bidfloor"` - Mobile *int8 `json:"mobile"` - MIMEs []string `json:"mimes"` - API []int8 `json:"api"` - Protocols []int8 `json:"protocols"` - MaxDuration *int64 `json:"maxduration"` -} - -func (a *ConversantAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.PBSBidder) (pbs.PBSBidSlice, error) { - mediaTypes := []pbs.MediaType{pbs.MEDIA_TYPE_BANNER, pbs.MEDIA_TYPE_VIDEO} - cnvrReq, err := adapters.MakeOpenRTBGeneric(req, bidder, a.Name(), mediaTypes) - - if err != nil { - return nil, err - } - - // Create a map of impression objects for both request creation - // and response parsing. - - impMap := make(map[string]*openrtb.Imp, len(cnvrReq.Imp)) - for idx := range cnvrReq.Imp { - impMap[cnvrReq.Imp[idx].ID] = &cnvrReq.Imp[idx] - } - - // Fill in additional info from custom params - - for _, unit := range bidder.AdUnits { - var params conversantParams - - imp := impMap[unit.Code] - if imp == nil { - // Skip ad units that do not have corresponding impressions. - continue - } - - err := json.Unmarshal(unit.Params, ¶ms) - if err != nil { - return nil, &errortypes.BadInput{ - Message: err.Error(), - } +func (c ConversantAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + for i := 0; i < len(request.Imp); i++ { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(request.Imp[i].Ext, &bidderExt); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Impression[%d] missing ext object", i), + }} } - // Fill in additional Site info - if params.SiteID != "" { - if cnvrReq.Site != nil { - cnvrReq.Site.ID = params.SiteID - } - if cnvrReq.App != nil { - cnvrReq.App.ID = params.SiteID - } + var cnvrExt openrtb_ext.ExtImpConversant + if err := json.Unmarshal(bidderExt.Bidder, &cnvrExt); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Impression[%d] missing ext.bidder object", i), + }} } - if params.Mobile != nil && !(cnvrReq.Site == nil) { - cnvrReq.Site.Mobile = *params.Mobile + if cnvrExt.SiteID == "" { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Impression[%d] requires ext.bidder.site_id", i), + }} } - // Fill in additional impression info - - imp.DisplayManager = "prebid-s2s" - imp.DisplayManagerVer = "1.0.1" - imp.BidFloor = params.BidFloor - imp.TagID = params.TagID - - var position *openrtb.AdPosition - if params.Position != nil { - position = openrtb.AdPosition(*params.Position).Ptr() + if i == 0 { + if request.Site != nil { + tmpSite := *request.Site + request.Site = &tmpSite + request.Site.ID = cnvrExt.SiteID + } else if request.App != nil { + tmpApp := *request.App + request.App = &tmpApp + request.App.ID = cnvrExt.SiteID + } } + parseCnvrParams(&request.Imp[i], cnvrExt) + } - if imp.Banner != nil { - imp.Banner.Pos = position - } else if imp.Video != nil { - imp.Video.Pos = position + //create the request body + 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") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: c.URI, + Body: data, + Headers: headers, + }}, nil +} - if len(params.API) > 0 { - imp.Video.API = make([]openrtb.APIFramework, 0, len(params.API)) - for _, api := range params.API { - imp.Video.API = append(imp.Video.API, openrtb.APIFramework(api)) - } - } +func parseCnvrParams(imp *openrtb.Imp, cnvrExt openrtb_ext.ExtImpConversant) { + imp.DisplayManager = "prebid-s2s" + imp.DisplayManagerVer = "2.0.0" + imp.BidFloor = cnvrExt.BidFloor + imp.TagID = cnvrExt.TagID - // Include protocols, mimes, and max duration if specified - // These properties can also be specified in ad unit's video object, - // but are overridden if the custom params object also contains them. + // Take care not to override the global secure flag + if (imp.Secure == nil || *imp.Secure == 0) && cnvrExt.Secure != nil { + imp.Secure = cnvrExt.Secure + } - if len(params.Protocols) > 0 { - imp.Video.Protocols = make([]openrtb.Protocol, 0, len(params.Protocols)) - for _, protocol := range params.Protocols { - imp.Video.Protocols = append(imp.Video.Protocols, openrtb.Protocol(protocol)) - } - } + var position *openrtb.AdPosition + if cnvrExt.Position != nil { + position = openrtb.AdPosition(*cnvrExt.Position).Ptr() + } + if imp.Banner != nil { + tmpBanner := *imp.Banner + imp.Banner = &tmpBanner + imp.Banner.Pos = position - if len(params.MIMEs) > 0 { - imp.Video.MIMEs = make([]string, len(params.MIMEs)) - copy(imp.Video.MIMEs, params.MIMEs) - } + } else if imp.Video != nil { + tmpVideo := *imp.Video + imp.Video = &tmpVideo + imp.Video.Pos = position - if params.MaxDuration != nil { - imp.Video.MaxDuration = *params.MaxDuration + if len(cnvrExt.API) > 0 { + imp.Video.API = make([]openrtb.APIFramework, 0, len(cnvrExt.API)) + for _, api := range cnvrExt.API { + imp.Video.API = append(imp.Video.API, openrtb.APIFramework(api)) } } - // Take care not to override the global secure flag + // Include protocols, mimes, and max duration if specified + // These properties can also be specified in ad unit's video object, + // but are overridden if the custom params object also contains them. - if (imp.Secure == nil || *imp.Secure == 0) && params.Secure != nil { - imp.Secure = params.Secure + if len(cnvrExt.Protocols) > 0 { + imp.Video.Protocols = make([]openrtb.Protocol, 0, len(cnvrExt.Protocols)) + for _, protocol := range cnvrExt.Protocols { + imp.Video.Protocols = append(imp.Video.Protocols, openrtb.Protocol(protocol)) + } } - } - - // Do a quick check on required parameters - if cnvrReq.Site != nil && cnvrReq.Site.ID == "" { - return nil, &errortypes.BadInput{ - Message: "Missing site id", + if len(cnvrExt.MIMEs) > 0 { + imp.Video.MIMEs = make([]string, len(cnvrExt.MIMEs)) + copy(imp.Video.MIMEs, cnvrExt.MIMEs) } - } - if cnvrReq.App != nil && cnvrReq.App.ID == "" { - return nil, &errortypes.BadInput{ - Message: "Missing app id", + if cnvrExt.MaxDuration != nil { + imp.Video.MaxDuration = *cnvrExt.MaxDuration } } +} - // Start capturing debug info - - debug := &pbs.BidderDebug{ - RequestURI: a.URI, - } - - if cnvrReq.Device == nil { - cnvrReq.Device = &openrtb.Device{} - } - - // Convert request to json to be sent over http - - j, _ := json.Marshal(cnvrReq) - - if req.IsDebug { - debug.RequestBody = string(j) - bidder.Debug = append(bidder.Debug, debug) - } - - httpReq, err := http.NewRequest("POST", a.URI, bytes.NewBuffer(j)) - httpReq.Header.Add("Content-Type", "application/json") - httpReq.Header.Add("Accept", "application/json") - - resp, err := ctxhttp.Do(ctx, a.http.Client, httpReq) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - if req.IsDebug { - debug.StatusCode = resp.StatusCode - } - - if resp.StatusCode == 204 { - return nil, nil - } - - body, err := ioutil.ReadAll(resp.Body) - - if err != nil { - return nil, err +func (c ConversantAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { + return nil, nil // no bid response } - if resp.StatusCode == http.StatusBadRequest { - return nil, &errortypes.BadInput{ - Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)), - } + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d", response.StatusCode), + }} } - if resp.StatusCode != 200 { - return nil, &errortypes.BadServerResponse{ - Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)), - } + var resp openrtb.BidResponse + if err := json.Unmarshal(response.Body, &resp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("bad server response: %d. ", err), + }} } - if req.IsDebug { - debug.ResponseBody = string(body) + if len(resp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Empty bid request"), + }} } - var bidResp openrtb.BidResponse - - err = json.Unmarshal(body, &bidResp) - if err != nil { - return nil, &errortypes.BadServerResponse{ - Message: err.Error(), - } + bids := resp.SeatBid[0].Bid + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bids)) + for i := 0; i < len(bids); i++ { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bids[i], + BidType: getBidType(bids[i].ImpID, internalRequest.Imp), + }) } + return bidResponse, nil +} - bids := make(pbs.PBSBidSlice, 0) - - for _, seatbid := range bidResp.SeatBid { - for _, bid := range seatbid.Bid { - if bid.Price <= 0 { - continue - } - - imp := impMap[bid.ImpID] - if imp == nil { - // All returned bids should have a matching impression - return nil, &errortypes.BadServerResponse{ - Message: fmt.Sprintf("Unknown impression id '%s'", bid.ImpID), - } - } - - bidID := bidder.LookupBidID(bid.ImpID) - if bidID == "" { - return nil, &errortypes.BadServerResponse{ - Message: fmt.Sprintf("Unknown ad unit code '%s'", bid.ImpID), - } - } - - pbsBid := pbs.PBSBid{ - BidID: bidID, - AdUnitCode: bid.ImpID, - Price: bid.Price, - Creative_id: bid.CrID, - BidderCode: bidder.BidderCode, - } - +func getBidType(impId string, imps []openrtb.Imp) openrtb_ext.BidType { + bidType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impId { if imp.Video != nil { - pbsBid.CreativeMediaType = "video" - pbsBid.NURL = bid.AdM // Assign to NURL so it'll be interpreted as a vastUrl - pbsBid.Width = imp.Video.W - pbsBid.Height = imp.Video.H - } else { - pbsBid.CreativeMediaType = "banner" - pbsBid.NURL = bid.NURL - pbsBid.Adm = bid.AdM - pbsBid.Width = bid.W - pbsBid.Height = bid.H + bidType = openrtb_ext.BidTypeVideo } - - bids = append(bids, &pbsBid) + break } } - - if len(bids) == 0 { - return nil, nil - } - - return bids, nil + return bidType } -func NewConversantAdapter(config *adapters.HTTPAdapterConfig, uri string) *ConversantAdapter { - a := adapters.NewHTTPAdapter(config) - - return &ConversantAdapter{ - http: a, - URI: uri, - } +func NewConversantBidder(endpoint string) *ConversantAdapter { + return &ConversantAdapter{URI: endpoint} } diff --git a/adapters/conversant/conversant_test.go b/adapters/conversant/conversant_test.go index b958e320dc7..e3275030e83 100644 --- a/adapters/conversant/conversant_test.go +++ b/adapters/conversant/conversant_test.go @@ -1,853 +1,10 @@ package conversant import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" + "github.com/prebid/prebid-server/adapters/adapterstest" "testing" - "time" - - "github.com/mxmCherry/openrtb" - "github.com/prebid/prebid-server/adapters" - "github.com/prebid/prebid-server/cache/dummycache" - "github.com/prebid/prebid-server/config" - "github.com/prebid/prebid-server/pbs" - "github.com/prebid/prebid-server/usersync" ) -// Constants - -const ExpectedSiteID string = "12345" -const ExpectedDisplayManager string = "prebid-s2s" -const ExpectedBuyerUID string = "AQECT_o7M1FLbQJK8QFmAQEBAQE" -const ExpectedNURL string = "http://test.dotomi.com" -const ExpectedAdM string = "" -const ExpectedCrID string = "98765" - -const DefaultParam = `{"site_id": "12345"}` - -// Test properties of Adapter interface - -func TestConversantProperties(t *testing.T) { - an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl") - - assertNotEqual(t, an.Name(), "", "Missing family name") - assertTrue(t, an.SkipNoCookies(), "SkipNoCookies should be true") -} - -// Test empty bid requests - -func TestConversantEmptyBid(t *testing.T) { - an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl") - - ctx := context.TODO() - pbReq := pbs.PBSRequest{} - pbBidder := pbs.PBSBidder{} - _, err := an.Call(ctx, &pbReq, &pbBidder) - assertTrue(t, err != nil, "No error received for an invalid request") -} - -// Test required parameters, which is just the site id for now - -func TestConversantRequiredParameters(t *testing.T) { - server := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent) - }), - ) - defer server.Close() - - an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, server.URL) - ctx := context.TODO() - - testParams := func(params ...string) (pbs.PBSBidSlice, error) { - req, err := CreateBannerRequest(params...) - if err != nil { - return nil, err - } - return an.Call(ctx, req, req.Bidders[0]) - } - - var err error - - if _, err = testParams(`{}`); err == nil { - t.Fatal("Failed to catch missing site id") - } -} - -// Test handling of 404 - -func TestConversantBadStatus(t *testing.T) { - // Create a test http server that returns after 2 milliseconds - - server := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - }), - ) - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - ctx := context.TODO() - pbReq, err := CreateBannerRequest(DefaultParam) - if err != nil { - t.Fatal("Failed to create a banner request", err) - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - assertTrue(t, err != nil, "Failed to catch 404 error") -} - -// Test handling of HTTP timeout - -func TestConversantTimeout(t *testing.T) { - // Create a test http server that returns after 2 milliseconds - - server := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - <-time.After(2 * time.Millisecond) - }), - ) - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - // Create a context that expires before http returns - - ctx, cancel := context.WithTimeout(context.Background(), 0) - defer cancel() - - // Create a basic request - pbReq, err := CreateBannerRequest(DefaultParam) - if err != nil { - t.Fatal("Failed to create a banner request", err) - } - - // Attempt to process the request, which should hit a timeout - // immediately - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err == nil || err != context.DeadlineExceeded { - t.Fatal("No timeout recevied for timed out request", err) - } -} - -// Test handling of 204 - -func TestConversantNoBid(t *testing.T) { - // Create a test http server that returns after 2 milliseconds - - server := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent) - }), - ) - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - ctx := context.TODO() - pbReq, err := CreateBannerRequest(DefaultParam) - if err != nil { - t.Fatal("Failed to create a banner request", err) - } - - resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0]) - if resp != nil || err != nil { - t.Fatal("Failed to handle empty bid", err) - } -} - -// Verify an outgoing openrtp request is created correctly - -func TestConversantRequestDefault(t *testing.T) { - server, lastReq := CreateServer() - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - ctx := context.TODO() - pbReq, err := CreateBannerRequest(DefaultParam) - if err != nil { - t.Fatal("Failed to create a banner request", err) - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err != nil { - t.Fatal("Failed to retrieve bids", err) - } - - assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") - imp := &lastReq.Imp[0] - - assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") - assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") - assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") - assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") - assertTrue(t, imp.Video == nil, "Request video should be nil") - assertEqual(t, int(*imp.Secure), 0, "Request secure") - assertEqual(t, imp.BidFloor, 0.0, "Request bid floor") - assertEqual(t, imp.TagID, "", "Request tag id") - assertTrue(t, imp.Banner.Pos == nil, "Request pos") - assertEqual(t, int(*imp.Banner.W), 300, "Request width") - assertEqual(t, int(*imp.Banner.H), 250, "Request height") -} - -// Verify inapp video request -func TestConversantInappVideoRequest(t *testing.T) { - server, lastReq := CreateServer() - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - requestParam := `{"secure": 1, "site_id": "12345"}` - appParam := `{ "bundle": "com.naver.linewebtoon" }` - videoParam := `{ "mimes": ["video/x-ms-wmv"], - "protocols": [1, 2], - "maxduration": 90 }` - - ctx := context.TODO() - pbReq := CreateRequest(requestParam) - pbReq, err := ConvertToVideoRequest(pbReq, videoParam) - if err != nil { - t.Fatal("failed to parse request") - } - pbReq, err = ConvertToAppRequest(pbReq, appParam) - if err != nil { - t.Fatal("failed to parse request") - } - pbReq, err = ParseRequest(pbReq) - if err != nil { - t.Fatal("failed to parse request") - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - - imp := &lastReq.Imp[0] - assertEqual(t, int(imp.Video.W), 300, "Request width") - assertEqual(t, int(imp.Video.H), 250, "Request height") - assertEqual(t, lastReq.App.ID, "12345", "App Id") -} - -// Verify inapp video request -func TestConversantInappBannerRequest(t *testing.T) { - server, lastReq := CreateServer() - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - param := `{ "secure": 1, - "site_id": "12345", - "tag_id": "top", - "position": 2, - "bidfloor": 1.01 }` - appParam := `{ "bundle": "com.naver.linewebtoon" }` - - ctx := context.TODO() - pbReq, _ := CreateBannerRequest(param) - pbReq, err := ConvertToAppRequest(pbReq, appParam) - if err != nil { - t.Fatal("failed to parse request") - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - - imp := &lastReq.Imp[0] - assertEqual(t, lastReq.App.ID, "12345", "App Id") - assertEqual(t, int(*imp.Banner.W), 300, "Request width") - assertEqual(t, int(*imp.Banner.H), 250, "Request height") -} - -// Verify an outgoing openrtp request with additional conversant parameters is -// processed correctly - -func TestConversantRequest(t *testing.T) { - server, lastReq := CreateServer() - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - param := `{ "site_id": "12345", - "secure": 1, - "tag_id": "top", - "position": 2, - "bidfloor": 1.01, - "mobile": 1 }` - - ctx := context.TODO() - pbReq, err := CreateBannerRequest(param) - if err != nil { - t.Fatal("Failed to create a banner request", err) - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err != nil { - t.Fatal("Failed to retrieve bids", err) - } - - assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") - imp := &lastReq.Imp[0] - - assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") - assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") - assertEqual(t, int(lastReq.Site.Mobile), 1, "Request site mobile flag") - assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") - assertTrue(t, imp.Video == nil, "Request video should be nil") - assertEqual(t, int(*imp.Secure), 1, "Request secure") - assertEqual(t, imp.BidFloor, 1.01, "Request bid floor") - assertEqual(t, imp.TagID, "top", "Request tag id") - assertEqual(t, int(*imp.Banner.Pos), 2, "Request pos") - assertEqual(t, int(*imp.Banner.W), 300, "Request width") - assertEqual(t, int(*imp.Banner.H), 250, "Request height") -} - -// Verify openrtp responses are converted correctly - -func TestConversantResponse(t *testing.T) { - prices := []float64{0.01, 0.0, 2.01} - server, lastReq := CreateServer(prices...) - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - param := `{ "site_id": "12345", - "secure": 1, - "tag_id": "top", - "position": 2, - "bidfloor": 1.01, - "mobile" : 1}` - - ctx := context.TODO() - pbReq, err := CreateBannerRequest(param, param, param) - if err != nil { - t.Fatal("Failed to create a banner request", err) - } - - resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err != nil { - t.Fatal("Failed to retrieve bids", err) - } - - prices, imps := FilterZeroPrices(prices, lastReq.Imp) - - assertEqual(t, len(resp), len(prices), "Bad number of responses") - - for i, bid := range resp { - assertEqual(t, bid.Price, prices[i], "Bad price in response") - assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response") - - if bid.Price > 0 { - assertEqual(t, bid.Adm, ExpectedAdM, "Bad ad markup in response") - assertEqual(t, bid.NURL, ExpectedNURL, "Bad notification url in response") - assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response") - assertEqual(t, bid.Width, *imps[i].Banner.W, "Bad width in response") - assertEqual(t, bid.Height, *imps[i].Banner.H, "Bad height in response") - } - } -} - -// Test video request - -func TestConversantBasicVideoRequest(t *testing.T) { - server, lastReq := CreateServer() - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - param := `{ "site_id": "12345", - "tag_id": "bottom left", - "position": 3, - "bidfloor": 1.01 }` - - ctx := context.TODO() - pbReq, err := CreateVideoRequest(param) - if err != nil { - t.Fatal("Failed to create a video request", err) - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err != nil { - t.Fatal("Failed to retrieve bids", err) - } - - assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") - imp := &lastReq.Imp[0] - - assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") - assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") - assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") - assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") - assertTrue(t, imp.Banner == nil, "Request banner should be nil") - assertEqual(t, int(*imp.Secure), 0, "Request secure") - assertEqual(t, imp.BidFloor, 1.01, "Request bid floor") - assertEqual(t, imp.TagID, "bottom left", "Request tag id") - assertEqual(t, int(*imp.Video.Pos), 3, "Request pos") - assertEqual(t, int(imp.Video.W), 300, "Request width") - assertEqual(t, int(imp.Video.H), 250, "Request height") - - assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries") - assertEqual(t, imp.Video.MIMEs[0], "video/mp4", "Requst video MIMEs type") - assertTrue(t, imp.Video.Protocols == nil, "Request video protocols") - assertEqual(t, imp.Video.MaxDuration, int64(0), "Request video 0 max duration") - assertTrue(t, imp.Video.API == nil, "Request video api should be nil") -} - -// Test video request with parameters in custom params object - -func TestConversantVideoRequestWithParams(t *testing.T) { - server, lastReq := CreateServer() - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - param := `{ "site_id": "12345", - "tag_id": "bottom left", - "position": 3, - "bidfloor": 1.01, - "mimes": ["video/x-ms-wmv"], - "protocols": [1, 2], - "api": [1, 2], - "maxduration": 90 }` - - ctx := context.TODO() - pbReq, err := CreateVideoRequest(param) - if err != nil { - t.Fatal("Failed to create a video request", err) - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err != nil { - t.Fatal("Failed to retrieve bids", err) - } - - assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") - imp := &lastReq.Imp[0] - - assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") - assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") - assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") - assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") - assertTrue(t, imp.Banner == nil, "Request banner should be nil") - assertEqual(t, int(*imp.Secure), 0, "Request secure") - assertEqual(t, imp.BidFloor, 1.01, "Request bid floor") - assertEqual(t, imp.TagID, "bottom left", "Request tag id") - assertEqual(t, int(*imp.Video.Pos), 3, "Request pos") - assertEqual(t, int(imp.Video.W), 300, "Request width") - assertEqual(t, int(imp.Video.H), 250, "Request height") - - assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries") - assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type") - assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols") - assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1") - assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2") - assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration") - assertEqual(t, len(imp.Video.API), 2, "Request video api should be nil") - assertEqual(t, imp.Video.API[0], openrtb.APIFramework(1), "Request video api 1") - assertEqual(t, imp.Video.API[1], openrtb.APIFramework(2), "Request video api 2") -} - -// Test video request with parameters in the video object - -func TestConversantVideoRequestWithParams2(t *testing.T) { - server, lastReq := CreateServer() - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - param := `{ "site_id": "12345" }` - videoParam := `{ "mimes": ["video/x-ms-wmv"], - "protocols": [1, 2], - "maxduration": 90 }` - - ctx := context.TODO() - pbReq := CreateRequest(param) - pbReq, err := ConvertToVideoRequest(pbReq, videoParam) - if err != nil { - t.Fatal("Failed to convert to a video request", err) - } - pbReq, err = ParseRequest(pbReq) - if err != nil { - t.Fatal("Failed to parse video request", err) - } - - _, err = an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err != nil { - t.Fatal("Failed to retrieve bids", err) - } - - assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions") - imp := &lastReq.Imp[0] - - assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value") - assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id") - assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag") - assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid") - assertTrue(t, imp.Banner == nil, "Request banner should be nil") - assertEqual(t, int(*imp.Secure), 0, "Request secure") - assertEqual(t, imp.BidFloor, 0.0, "Request bid floor") - assertEqual(t, int(imp.Video.W), 300, "Request width") - assertEqual(t, int(imp.Video.H), 250, "Request height") - - assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries") - assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type") - assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols") - assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1") - assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2") - assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration") -} - -// Test video responses - -func TestConversantVideoResponse(t *testing.T) { - prices := []float64{0.01, 0.0, 2.01} - server, lastReq := CreateServer(prices...) - if server == nil { - t.Fatal("server not created") - } - - defer server.Close() - - // Create a adapter to test - - conf := *adapters.DefaultHTTPAdapterConfig - an := NewConversantAdapter(&conf, server.URL) - - param := `{ "site_id": "12345", - "secure": 1, - "tag_id": "top", - "position": 2, - "bidfloor": 1.01, - "mobile" : 1}` - - ctx := context.TODO() - pbReq, err := CreateVideoRequest(param, param, param) - if err != nil { - t.Fatal("Failed to create a video request", err) - } - - resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0]) - if err != nil { - t.Fatal("Failed to retrieve bids", err) - } - - prices, imps := FilterZeroPrices(prices, lastReq.Imp) - - assertEqual(t, len(resp), len(prices), "Bad number of responses") - - for i, bid := range resp { - assertEqual(t, bid.Price, prices[i], "Bad price in response") - assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response") - - if bid.Price > 0 { - assertEqual(t, bid.Adm, "", "Bad ad markup in response") - assertEqual(t, bid.NURL, ExpectedAdM, "Bad notification url in response") - assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response") - assertEqual(t, bid.Width, imps[i].Video.W, "Bad width in response") - assertEqual(t, bid.Height, imps[i].Video.H, "Bad height in response") - } - } -} - -// Helpers to create a banner and video requests - -func CreateRequest(params ...string) *pbs.PBSRequest { - num := len(params) - - req := pbs.PBSRequest{ - Tid: "t-000", - AccountID: "1", - AdUnits: make([]pbs.AdUnit, num), - } - - for i := 0; i < num; i++ { - req.AdUnits[i] = pbs.AdUnit{ - Code: fmt.Sprintf("au-%03d", i), - Sizes: []openrtb.Format{ - { - W: 300, - H: 250, - }, - }, - Bids: []pbs.Bids{ - { - BidderCode: "conversant", - BidID: fmt.Sprintf("b-%03d", i), - Params: json.RawMessage(params[i]), - }, - }, - } - } - - return &req -} - -// Convert a request to a video request by adding required properties - -func ConvertToVideoRequest(req *pbs.PBSRequest, videoParams ...string) (*pbs.PBSRequest, error) { - for i := 0; i < len(req.AdUnits); i++ { - video := pbs.PBSVideo{} - if i < len(videoParams) { - err := json.Unmarshal([]byte(videoParams[i]), &video) - if err != nil { - return nil, err - } - } - - if video.Mimes == nil { - video.Mimes = []string{"video/mp4"} - } - - req.AdUnits[i].Video = video - req.AdUnits[i].MediaTypes = []string{"video"} - } - - return req, nil -} - -// Convert a request to an app request by adding required properties -func ConvertToAppRequest(req *pbs.PBSRequest, appParams string) (*pbs.PBSRequest, error) { - app := new(openrtb.App) - err := json.Unmarshal([]byte(appParams), &app) - if err == nil { - req.App = app - } - - return req, nil -} - -// Feed the request thru the prebid parser so user id and -// other private properties are defined - -func ParseRequest(req *pbs.PBSRequest) (*pbs.PBSRequest, error) { - body := new(bytes.Buffer) - _ = json.NewEncoder(body).Encode(req) - - // Need to pass the conversant user id thru uid cookie - - httpReq := httptest.NewRequest("POST", "/foo", body) - cookie := usersync.NewPBSCookie() - _ = cookie.TrySync("conversant", ExpectedBuyerUID) - httpReq.Header.Set("Cookie", cookie.ToHTTPCookie(90*24*time.Hour).String()) - httpReq.Header.Add("Referer", "http://example.com") - cache, _ := dummycache.New() - hcc := config.HostCookie{} - - parsedReq, err := pbs.ParsePBSRequest(httpReq, &config.AuctionTimeouts{ - Default: 2000, - Max: 2000, - }, cache, &hcc) - - return parsedReq, err -} - -// A helper to create a banner request - -func CreateBannerRequest(params ...string) (*pbs.PBSRequest, error) { - req := CreateRequest(params...) - req, err := ParseRequest(req) - return req, err -} - -// A helper to create a video request - -func CreateVideoRequest(params ...string) (*pbs.PBSRequest, error) { - req := CreateRequest(params...) - req, err := ConvertToVideoRequest(req) - if err != nil { - return nil, err - } - req, err = ParseRequest(req) - return req, err -} - -// Helper to create a test http server that receives and generate openrtb requests and responses - -func CreateServer(prices ...float64) (*httptest.Server, *openrtb.BidRequest) { - var lastBidRequest openrtb.BidRequest - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - var bidReq openrtb.BidRequest - var price float64 - var bids []openrtb.Bid - var bid openrtb.Bid - - err = json.Unmarshal(body, &bidReq) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - lastBidRequest = bidReq - - for i, imp := range bidReq.Imp { - if i < len(prices) { - price = prices[i] - } else { - price = 0 - } - - if price > 0 { - bid = openrtb.Bid{ - ID: imp.ID, - ImpID: imp.ID, - Price: price, - NURL: ExpectedNURL, - AdM: ExpectedAdM, - CrID: ExpectedCrID, - } - - if imp.Banner != nil { - bid.W = *imp.Banner.W - bid.H = *imp.Banner.H - } else if imp.Video != nil { - bid.W = imp.Video.W - bid.H = imp.Video.H - } - } else { - bid = openrtb.Bid{ - ID: imp.ID, - ImpID: imp.ID, - Price: 0, - } - } - - bids = append(bids, bid) - } - - if len(bids) == 0 { - w.WriteHeader(http.StatusNoContent) - } else { - js, _ := json.Marshal(openrtb.BidResponse{ - ID: bidReq.ID, - SeatBid: []openrtb.SeatBid{ - { - Bid: bids, - }, - }, - }) - - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(js) - } - }), - ) - - return server, &lastBidRequest -} - -// Helper to remove impressions with $0 bids - -func FilterZeroPrices(prices []float64, imps []openrtb.Imp) ([]float64, []openrtb.Imp) { - prices2 := make([]float64, 0) - imps2 := make([]openrtb.Imp, 0) - - for i := range prices { - if prices[i] > 0 { - prices2 = append(prices2, prices[i]) - imps2 = append(imps2, imps[i]) - } - } - - return prices2, imps2 -} - -// Helpers to test equality - -func assertEqual(t *testing.T, actual interface{}, expected interface{}, msg string) { - if expected != actual { - msg = fmt.Sprintf("%s: act(%v) != exp(%v)", msg, actual, expected) - t.Fatal(msg) - } -} - -func assertNotEqual(t *testing.T, actual interface{}, expected interface{}, msg string) { - if expected == actual { - msg = fmt.Sprintf("%s: act(%v) == exp(%v)", msg, actual, expected) - t.Fatal(msg) - } -} - -func assertTrue(t *testing.T, val bool, msg string) { - if val == false { - msg = fmt.Sprintf("%s: is false but should be true", msg) - t.Fatal(msg) - } -} - -func assertFalse(t *testing.T, val bool, msg string) { - if val == true { - msg = fmt.Sprintf("%s: is true but should be false", msg) - t.Fatal(msg) - } +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "conversanttest", NewConversantBidder("")) } diff --git a/adapters/conversant/conversanttest/exemplary/banner.json b/adapters/conversant/conversanttest/exemplary/banner.json new file mode 100644 index 00000000000..472e18f712d --- /dev/null +++ b/adapters/conversant/conversanttest/exemplary/banner.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "testauction", + "imp": [ + { + "id": "1", + "banner": { + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "site_id": "108060", + "bidfloor": 0.01, + "tag_id": "mytag", + "secure": 1 + } + } + } + ], + "device": { + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", + "ip": "192.168.1.1", + "dnt": 1 + }, + "site": { + "domain": "www.mypage.com" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "", + "body": { + "id": "testauction", + "site": { + "id": "108060", + "domain": "www.mypage.com" + }, + "imp": [ + { + "id": "1", + "tagid": "mytag", + "secure": 1, + "bidfloor": 0.01, + "displaymanager": "prebid-s2s", + "displaymanagerver": "2.0.0", + "banner": { + "format": [{"w": 300, "h": 250}] + }, + "ext": { + "bidder": { + "site_id": "108060", + "bidfloor": 0.01, + "tag_id": "mytag", + "secure": 1 + } + } + } + ], + "device": { + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", + "ip": "192.168.1.1", + "dnt": 1 + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "testauction", + "bidid": "c8d95f4b-bcbb-4a6c-adbb-4c7f33af3c24", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "1", + "price": 0.0340, + "nurl": "https:\/\/event.ad.cpe.dotomi.com\/cvx\/event\/imp?enc=eyJ1c2VyaWQiOiI3MTI3MDUzNzM3NTM3MTAzMjIiLCJwYXJ0bmVyVHhpZCI6ImUyZWUzNjZlLWEyMjgtNDI0Mi1hNjJlLTk4ODk3ODhiYzgxNCIsInR4aWQiOiI3MTE1NzQwNDg3NTczODUwMDIiLCJuZXR3b3JrUmVxdWVzdElkIjoiNzExNTc0MDQ4NzU3Mzg1ODc0Iiwic2lkIjoxMTgwOTgsImRpdmlzaW9uSWQiOjgsInRpZCI6OCwibW9iaWxlRGF0YSI6IjU5IiwiYmlkUHJpY2UiOjAuMDY4MCwicHViQ29zdCI6MC4wMzQwLCJwYXJ0bmVyRmVlIjowLjAxMzYsImlwU3RyaW5nIjoiNzMuMTE4LjEzMC4xODYiLCJzdXBwbHlUeXBlIjoxLCJpbnRlZ3JhdGlvblR5cGUiOjQsIm1lZGlhdGlvblR5cGUiOjEyNiwicGxhY2VtZW50SWQiOiIxMTY5ODcwIiwiaGVhZGVyQmlkIjoxLCJpc0RpcmVjdFB1Ymxpc2hlciI6MCwiaGFzQ29uc2VudCI6MSwib3BlcmF0aW9uIjoiQ0xJRU5UX0hFQURFUl8yNSIsImlzQ29yZVNoaWVsZCI6MCwicGFydG5lckNyZWF0aXZlSWQiOiIyNDk2NDRfMzAweDI1MCIsInBhcnRuZXJEb21haW5zIjpbIndhbG1hcnQuY29tIl0sInNlbGxlclJlcXVlc3RJZCI6ImE3ODcyMWQ3LWE2ZmUtNGJiNS1hNjFkLTFhMDg1MzkxZTVlZCIsInNlbGxlckltcElkIjoiMzAwNDIxZDY0NWY2ZjRjOWMifQ&", + "adm": "", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }] + }, + { + "seat": "45678", + "bid": [{ + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + } + ] + }], + "cur": "USD" + } + } + }], + + "expectedBids": [ + { + "bid": { + "adm": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }, + "type": "banner" + }, + { + "bid": { + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + }, + "type": "video" + } + ] +} + \ No newline at end of file diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json new file mode 100644 index 00000000000..c2b20cf1c5d --- /dev/null +++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json @@ -0,0 +1,200 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [{ + "id": "some_test_ad_id_1", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + } + }, + { + "id": "some_test_ad_id_2", + "video":{ + "mimes": [ + "video/mp4", + "application/javascript" + ], + "protocols":[ + 2, + 3, + 5, + 6 + ], + "w":640, + "h":480 + }, + "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://hb.emxdgt.com?t=1000&ts=2060541160", + "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_1", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + }, + "tagid": "25251", + "secure": 0 + }, + { + "id": "some_test_ad_id_2", + "video":{ + "mimes": [ + "video/mp4", + "application/javascript" + ], + "protocols":[ + 2, + 3, + 5, + 6 + ], + "w":640, + "h":480 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + }, + "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": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }] + }, + { + "seat": "45678", + "bid": [{ + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + } + ] + }], + "cur": "USD" + } + } + }], + + "expectedBids": [{ + "bid": { + "adm": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }, + "type": "banner" + }, + { + "bid": { + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + }, + "type": "video" + } + ] +} + \ No newline at end of file diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json new file mode 100644 index 00000000000..8de90f52192 --- /dev/null +++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json @@ -0,0 +1,119 @@ +{ + "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 + }, + "app": { + "domain": "www.publisher.com", + "storeurl": "http://www.publisher.com/awesome/site?with=some¶meters=here" + } + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "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 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + }, + "tagid": "25251", + "secure": 0 + }], + "app": { + "domain": "www.publisher.com", + "storeurl": "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": "
", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 300, + "w": 250 + }] + }], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 300, + "w": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/amx/amxtest/exemplary/video-simple.json b/adapters/amx/amxtest/exemplary/video-simple.json new file mode 100644 index 00000000000..8fb3baa26d0 --- /dev/null +++ b/adapters/amx/amxtest/exemplary/video-simple.json @@ -0,0 +1,245 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "boxingallowed": 1, + "linearity": 1, + "maxduration": 90, + "minduration": 6, + "mimes": ["video/mp4"], + "placement": 1, + "playbackmethod": [2], + "protocols": [1,2,3,4,5,6,7,8], + "skip": 1, + "skipafter": 5, + "startdelay": 0, + "h": 300, + "pos": 1, + "w": 640 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw", + "adUnitId": "tagid-override" + } + }, + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "unused_publisher_id" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "boxingallowed": 1, + "linearity": 1, + "maxduration": 90, + "minduration": 6, + "mimes": ["video/mp4"], + "placement": 1, + "playbackmethod": [2], + "protocols": [1,2,3,4,5,6,7,8], + "skip": 1, + "skipafter": 5, + "startdelay": 0, + "h": 300, + "pos": 1, + "w": 640 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw", + "adUnitId": "tagid-override" + } + }, + "tagid": "tagid-override", + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "cHJlYmlkLm9yZw" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "WQ5V2DWVTMNXABDD", + "seatbid": [{ + "bid": [{ + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "00:00:15", + "nurl": "https://example.com/nurl", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 600, + "w": 300, + "ext": { + "himp": ["https://example.com/imp-tracker/pixel.gif?param=1¶m2=2"], + "startdelay": 0 + } + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "00:00:15", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "ext": { + "himp": ["https://example.com/imp-tracker/pixel.gif?param=1¶m2=2"], + "startdelay": 0 + }, + "h": 600, + "w": 300 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/amx/amxtest/exemplary/web-simple.json b/adapters/amx/amxtest/exemplary/web-simple.json new file mode 100644 index 00000000000..74854f912ae --- /dev/null +++ b/adapters/amx/amxtest/exemplary/web-simple.json @@ -0,0 +1,246 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "banner": { + "format": [ + { + "h": 600, + "w": 300 + } + ], + "h": 600, + "pos": 1, + "w": 300 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw" + } + }, + "tagid": "example-tag-id", + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "unused_publisher_id" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + }, + { + "source": "adserver.org", + "uids": [ + { + "id": "1234567", + "ext": { + "rtiPartner": "TDID" + } + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "tagid": "example-tag-id", + "banner": { + "format": [ + { + "h": 600, + "w": 300 + } + ], + "h": 600, + "pos": 1, + "w": 300 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw" + } + }, + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "cHJlYmlkLm9yZw" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + }, + { + "source": "adserver.org", + "uids": [ + { + "id": "1234567", + "ext": { + "rtiPartner": "TDID" + } + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "WQ5V2DWVTMNXABDD", + "seatbid": [{ + "bid": [{ + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 600, + "w": 300 + }] + }], + "cur": "USD" + } + } + }], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 600, + "w": 300 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/amx/amxtest/params/race/display.json b/adapters/amx/amxtest/params/race/display.json new file mode 100644 index 00000000000..bd101e95a25 --- /dev/null +++ b/adapters/amx/amxtest/params/race/display.json @@ -0,0 +1 @@ +{"tagId":"sample345", "adUnitId": "sampleAdUnitID"} \ No newline at end of file diff --git a/adapters/amx/amxtest/params/race/video.json b/adapters/amx/amxtest/params/race/video.json new file mode 100644 index 00000000000..d2f11bf80b4 --- /dev/null +++ b/adapters/amx/amxtest/params/race/video.json @@ -0,0 +1 @@ +{"tagId": "sample123", "adUnitId": "sampleAdUnitID"} \ No newline at end of file diff --git a/adapters/amx/amxtest/supplemental/204-response.json b/adapters/amx/amxtest/supplemental/204-response.json new file mode 100644 index 00000000000..09571a03569 --- /dev/null +++ b/adapters/amx/amxtest/supplemental/204-response.json @@ -0,0 +1,109 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300 + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "mimes": null, + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300 + } + }, + "mockResponse": { + "status": 204, + "headers": { + "X-Nbr": [ + "3b" + ] + }, + "body": {} + } + }], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/amx/amxtest/supplemental/400-response.json b/adapters/amx/amxtest/supplemental/400-response.json new file mode 100644 index 00000000000..f10cea89718 --- /dev/null +++ b/adapters/amx/amxtest/supplemental/400-response.json @@ -0,0 +1,114 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300 + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "mimes": null, + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300 + } + }, + "mockResponse": { + "status": 400, + "headers": { + "X-Nbr": [ + "3b" + ] + }, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Invalid Request: 400. Error Code: 3b", + "comparison": "literal" + } + ] +} diff --git a/adapters/amx/amxtest/supplemental/500-response.json b/adapters/amx/amxtest/supplemental/500-response.json new file mode 100644 index 00000000000..fe5d89930c8 --- /dev/null +++ b/adapters/amx/amxtest/supplemental/500-response.json @@ -0,0 +1,114 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300 + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "mimes": null, + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300 + } + }, + "mockResponse": { + "status": 500, + "headers": { + "X-Nbr": [ + "7a" + ] + }, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected response: 500. Error Code: 7a", + "comparison": "literal" + } + ] +} diff --git a/adapters/amx/params_test.go b/adapters/amx/params_test.go new file mode 100644 index 00000000000..89e9a3adeb4 --- /dev/null +++ b/adapters/amx/params_test.go @@ -0,0 +1,47 @@ +package amx + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +var validBidParams = []string{ + `{"tagId":"sampleTagId", "adUnitId": "sampleAdUnitId"}`, + `{"tagId":"sampleTagId", "adUnitId": ""}`, + `{"adUnitId": ""}`, + `{"adUnitId": "sampleAdUnitId"}`, + `{"tagId":"sampleTagId"}`, + `{"tagId":""}`, + `{}`, + `{"otherValue": "ignored"}`, + `{"tagId": "sampleTagId", "otherValue": "ignored"}`, + `{"otherValue": "ignored", "adUnitId": "sampleAdUnitId"}`, +} + +var invalidBidParams = []string{ + `{"tagId":1234}`, + `{"tagId": true}`, + `{"adUnitId": true}`, + `{"adUnitId": null}`, + `{"adUnitId": null, "tagId": "sampleTagId"}`, + `{"adUnitId": 1234, "tagId": "sampleTagId"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + assert.Nil(t, err) + for _, params := range validBidParams { + assert.Nil(t, validator.Validate(openrtb_ext.BidderAMX, json.RawMessage(params))) + } +} + +func TestInValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + assert.Nil(t, err) + for _, params := range invalidBidParams { + assert.NotNil(t, validator.Validate(openrtb_ext.BidderAMX, json.RawMessage(params))) + } +} diff --git a/adapters/amx/usersync.go b/adapters/amx/usersync.go new file mode 100644 index 00000000000..28e6ac0ed79 --- /dev/null +++ b/adapters/amx/usersync.go @@ -0,0 +1,13 @@ +package amx + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +// NewAMXSyncer produces an AMX RTB usersyncer +func NewAMXSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("amx", 737, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/amx/usersync_test.go b/adapters/amx/usersync_test.go new file mode 100644 index 00000000000..20a47c33b69 --- /dev/null +++ b/adapters/amx/usersync_test.go @@ -0,0 +1,23 @@ +package amx + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/stretchr/testify/assert" +) + +func TestAMXSyncer(t *testing.T) { + syncURL := "http://pbs.amxrtb.com/cchain/0?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&cb=localhost%2Fsetuid%3Fbidder%3Damx%26uid%3D" + syncURLTemplate := template.Must(template.New("sync-template").Parse(syncURL)) + + syncer := NewAMXSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{}) + + assert.NoError(t, err) + assert.Equal(t, "http://pbs.amxrtb.com/cchain/0?gdpr=&gdpr_consent=&cb=localhost%2Fsetuid%3Fbidder%3Damx%26uid%3D", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 737, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/adapters/dmx/dmx.go b/adapters/dmx/dmx.go index de33bd390e5..dfb97f33abb 100644 --- a/adapters/dmx/dmx.go +++ b/adapters/dmx/dmx.go @@ -4,13 +4,14 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "net/url" + "strings" + "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/openrtb_ext" - "net/http" - "net/url" - "strings" ) type DmxAdapter struct { diff --git a/config/config.go b/config/config.go index 6d8fdbc070d..c2db7b7d03c 100755 --- a/config/config.go +++ b/config/config.go @@ -712,6 +712,7 @@ func (cfg *Configuration) setDerivedDefaults() { // 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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Daja%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25s") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAMX, "https://prebid.a-mo.net/cchain/0?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Damx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAppnexus, "https://ib.adnxs.com/getuid?"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadnxs%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAvocet, "https://ads.avct.cloud/getuid?&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dbeachfront%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bio_cid%5D") @@ -939,6 +940,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.amx.endpoint", "http://pbs.amxrtb.com/auction/openrtb") 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") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index 30d8d45641f..ff72165bcb3 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -25,6 +25,7 @@ import ( "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" "github.com/prebid/prebid-server/adapters/aja" + "github.com/prebid/prebid-server/adapters/amx" "github.com/prebid/prebid-server/adapters/applogy" "github.com/prebid/prebid-server/adapters/appnexus" "github.com/prebid/prebid-server/adapters/audienceNetwork" @@ -122,6 +123,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderAMX: amx.NewAMXBidder(cfg.Adapters[string(openrtb_ext.BidderAMX)].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), openrtb_ext.BidderAvocet: avocet.NewAvocetAdapter(cfg.Adapters[string(openrtb_ext.BidderAvocet)].Endpoint), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index dd482a2ea44..b98c80ae9af 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -43,6 +43,7 @@ const ( BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderAJA BidderName = "aja" + BidderAMX BidderName = "amx" BidderApplogy BidderName = "applogy" BidderAppnexus BidderName = "appnexus" BidderAdoppler BidderName = "adoppler" @@ -136,6 +137,7 @@ var BidderMap = map[string]BidderName{ "adtelligent": BidderAdtelligent, "advangelists": BidderAdvangelists, "aja": BidderAJA, + "amx": BidderAMX, "applogy": BidderApplogy, "appnexus": BidderAppnexus, "adoppler": BidderAdoppler, diff --git a/openrtb_ext/imp_amx.go b/openrtb_ext/imp_amx.go new file mode 100644 index 00000000000..d4439d05f60 --- /dev/null +++ b/openrtb_ext/imp_amx.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpAMX is the imp.ext format for the AMX bidder +type ExtImpAMX struct { + TagID string `json:"tagId,omitempty"` + AdUnitID string `json:"adUnitId,omitempty"` +} diff --git a/static/bidder-info/amx.yaml b/static/bidder-info/amx.yaml new file mode 100644 index 00000000000..3e20d2095f6 --- /dev/null +++ b/static/bidder-info/amx.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "prebid@amxrtb.com" +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-params/amx.json b/static/bidder-params/amx.json new file mode 100644 index 00000000000..f9b1b26b3db --- /dev/null +++ b/static/bidder-params/amx.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AMX RTB Adapter Params", + "description": "A schema to validate params accepted by the AMX adapter", + "type": "object", + "properties": { + "tagId" : { + "type": "string", + "description": "Set a tagId (overrides site.publisher.id, or app.publisher.id)" + }, + "adUnitId": { + "type": "string", + "description": "Override imp.tagid value to provide a custom value in AMX ad unit ID reporting" + } + } +} diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index a9d909db9a1..d6b3092a56d 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -18,6 +18,7 @@ import ( "github.com/prebid/prebid-server/adapters/adtelligent" "github.com/prebid/prebid-server/adapters/advangelists" "github.com/prebid/prebid-server/adapters/aja" + "github.com/prebid/prebid-server/adapters/amx" "github.com/prebid/prebid-server/adapters/appnexus" "github.com/prebid/prebid-server/adapters/audienceNetwork" "github.com/prebid/prebid-server/adapters/avocet" @@ -102,6 +103,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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.BidderAMX, amx.NewAMXSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAppnexus, appnexus.NewAppnexusSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAvocet, avocet.NewAvocetSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderBeachfront, beachfront.NewBeachfrontSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 8ae8581aa6e..7582055fa46 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -27,6 +27,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderAdtelligent): syncConfig, string(openrtb_ext.BidderAdvangelists): syncConfig, string(openrtb_ext.BidderAJA): syncConfig, + string(openrtb_ext.BidderAMX): syncConfig, string(openrtb_ext.BidderAppnexus): syncConfig, string(openrtb_ext.BidderAvocet): syncConfig, string(openrtb_ext.BidderBeachfront): syncConfig, From ec7a2f70ce7b7c4b1680c13283599452d6f2c5d3 Mon Sep 17 00:00:00 2001 From: shalmali-patil Date: Fri, 6 Nov 2020 12:08:02 +0530 Subject: [PATCH 261/381] UOE-5745 adding a test input file for rewarded video case --- .../exemplary/video-rewarded.json | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 adapters/pubmatic/pubmatictest/exemplary/video-rewarded.json diff --git a/adapters/pubmatic/pubmatictest/exemplary/video-rewarded.json b/adapters/pubmatic/pubmatictest/exemplary/video-rewarded.json new file mode 100644 index 00000000000..af4220bd23e --- /dev/null +++ b/adapters/pubmatic/pubmatictest/exemplary/video-rewarded.json @@ -0,0 +1,174 @@ +{ + "mockBidRequest": { + "id": "test-video-request", + "imp": [{ + "id": "test-video-imp", + "video": { + "w":640, + "h":480, + "mimes": ["video/mp4", "video/x-flv"], + "minduration": 5, + "maxduration": 30, + "startdelay": 5, + "playbackmethod": [1, 3], + "api": [1, 2], + "protocols": [2, 3], + "battr": [13, 14], + "linearity": 1, + "placement": 2, + "minbitrate": 10, + "maxbitrate": 10 + }, + "ext": { + "prebid": { + "is_rewarded_inventory": 1 + }, + "bidder": { + "adSlot": "AdTag_Div1@0x0", + "publisherId": "999", + "keywords": [{ + "key": "pmZoneID", + "value": ["Zone1", "Zone2"] + } + ], + "wrapper": { + "version": 1, + "profile": 5123 + } + } + } + }], + "device":{ + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "1234" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server", + "body": { + "id": "test-video-request", + "imp": [ + { + "id": "test-video-imp", + "tagid":"AdTag_Div1", + "video": { + "w":640, + "h":480, + "mimes": ["video/mp4", "video/x-flv"], + "minduration": 5, + "maxduration": 30, + "startdelay": 5, + "playbackmethod": [1, 3], + "api": [1, 2], + "protocols": [2, 3], + "battr": [13, 14], + "linearity": 1, + "placement": 2, + "minbitrate": 10, + "maxbitrate": 10 + }, + "ext": { + "pmZoneID": "Zone1,Zone2", + "reward": 1 + } + } + ], + "device":{ + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" + }, + "site": { + "id": "siteID", + "publisher": { + "id": "999" + } + }, + "ext": { + "wrapper": { + "profile": 5123, + "version":1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-video-request", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-video-imp", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "crid": "29681110", + "h": 250, + "w": 300, + "dealid":"test deal", + "cat" : ["IAB-1", "IAB-2"], + "ext": { + "dspid": 6, + "deal_channel": 1, + "BidType": 1, + "video" : { + "duration" : 5 + } + } + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-video-imp", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["pubmatic.com"], + "cat": [ + "IAB-1" + ], + "crid": "29681110", + "w": 300, + "h": 250, + "dealid":"test deal", + "ext": { + "dspid": 6, + "deal_channel": 1, + "BidType": 1, + "video" : { + "duration" : 5 + } + } + }, + "type": "video", + "video" :{ + "duration" : 5 + } + } + ] + } + ] + } From 494e3de77b9899ef80185e92a036471b4f2e39ca Mon Sep 17 00:00:00 2001 From: shalmali-patil Date: Fri, 6 Nov 2020 14:47:10 +0530 Subject: [PATCH 262/381] UOE-5745: removing commented code --- adapters/pubmatic/pubmatic.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go index fbb020f8321..3a39e83d091 100644 --- a/adapters/pubmatic/pubmatic.go +++ b/adapters/pubmatic/pubmatic.go @@ -666,21 +666,6 @@ func populateKeywordsInExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, impExtM } } -/*func makeKeywordStr(keywords []*openrtb_ext.ExtImpPubmaticKeyVal) string { - eachKv := make([]string, 0, len(keywords)) - for _, keyVal := range keywords { - if len(keyVal.Values) == 0 { - logf("No values present for key = %s", keyVal.Key) - continue - } else { - eachKv = append(eachKv, fmt.Sprintf("\"%s\":\"%s\"", keyVal.Key, strings.Join(keyVal.Values[:], ","))) - } - } - - kvStr := strings.Join(eachKv, ",") - return kvStr -}*/ - func prepareImpressionExt(keywords map[string]string) string { eachKv := make([]string, 0, len(keywords)) From 63f5bcfb9f310f62299ecaac5ef053dee31041fc Mon Sep 17 00:00:00 2001 From: htang555 Date: Tue, 10 Nov 2020 13:37:04 -0500 Subject: [PATCH 263/381] update Datablocks usersync.go (#1572) --- adapters/datablocks/usersync.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/datablocks/usersync.go b/adapters/datablocks/usersync.go index c8ec92aa857..2b47b259e39 100644 --- a/adapters/datablocks/usersync.go +++ b/adapters/datablocks/usersync.go @@ -7,8 +7,8 @@ import ( "github.com/prebid/prebid-server/usersync" ) -const datablocksGDPRVendorID = uint16(14) +const datablocksGDPRVendorID = uint16(0) func NewDatablocksSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("datablocks", 14, temp, adapters.SyncTypeRedirect) + return adapters.NewSyncer("datablocks", datablocksGDPRVendorID, temp, adapters.SyncTypeRedirect) } From 6d37afcdc109a4671644a18a66181c1bd75e6568 Mon Sep 17 00:00:00 2001 From: Aparna Rao Date: Wed, 11 Nov 2020 03:59:50 -0500 Subject: [PATCH 264/381] 33Across: Add video support in adapter (#1557) --- adapters/33across/33across.go | 77 +++++++++++- adapters/33across/33across_test.go | 2 +- .../exemplary/bidresponse-defaults.json | 100 ++++++++++++++++ .../exemplary/instream-video-defaults.json | 108 +++++++++++++++++ .../33acrosstest/exemplary/multi-format.json | 103 ++++++++++++++++ .../exemplary/optional-params.json | 0 .../exemplary/outstream-video-defaults.json | 107 +++++++++++++++++ .../exemplary/simple-banner.json | 14 ++- .../33acrosstest/exemplary/simple-video.json | 110 ++++++++++++++++++ .../params/race/banner.json | 0 .../33acrosstest/params/race/video.json | 6 + .../supplemental/status-not-ok.json | 67 +++++++++++ .../supplemental/video-validation-fail.json | 32 +++++ static/bidder-info/33across.yaml | 2 + 14 files changed, 724 insertions(+), 4 deletions(-) create mode 100644 adapters/33across/33acrosstest/exemplary/bidresponse-defaults.json create mode 100644 adapters/33across/33acrosstest/exemplary/instream-video-defaults.json create mode 100644 adapters/33across/33acrosstest/exemplary/multi-format.json rename adapters/33across/{33across => 33acrosstest}/exemplary/optional-params.json (100%) create mode 100644 adapters/33across/33acrosstest/exemplary/outstream-video-defaults.json rename adapters/33across/{33across => 33acrosstest}/exemplary/simple-banner.json (85%) create mode 100644 adapters/33across/33acrosstest/exemplary/simple-video.json rename adapters/33across/{33across => 33acrosstest}/params/race/banner.json (100%) create mode 100644 adapters/33across/33acrosstest/params/race/video.json create mode 100644 adapters/33across/33acrosstest/supplemental/status-not-ok.json create mode 100644 adapters/33across/33acrosstest/supplemental/video-validation-fail.json diff --git a/adapters/33across/33across.go b/adapters/33across/33across.go index 5c1b31eeb8c..40099a204e0 100644 --- a/adapters/33across/33across.go +++ b/adapters/33across/33across.go @@ -24,6 +24,14 @@ type ext struct { Zoneid string `json:"zoneid,omitempty"` } +type bidExt struct { + Ttx bidTtxExt `json:"ttx,omitempty"` +} + +type bidTtxExt struct { + MediaType string `json:mediaType,omitempty` +} + // MakeRequests create the object for TTX Reqeust. func (a *TtxAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var errs []error @@ -49,6 +57,14 @@ func (a *TtxAdapter) makeRequest(request *openrtb.BidRequest) (*adapters.Request errs = append(errs, err) } + if reqCopy.Imp[0].Banner == nil && reqCopy.Imp[0].Video == nil { + errs = append(errs, &errortypes.BadInput{ + Message: "At least one of [banner, video] formats must be defined in Imp. None found", + }) + + return nil, errs + } + // Last Step reqJSON, err := json.Marshal(reqCopy) if err != nil { @@ -104,6 +120,19 @@ func preprocess(request *openrtb.BidRequest) error { siteCopy.ID = ttxExt.SiteId request.Site = &siteCopy + // Validate Video if it exists + if imp.Video != nil { + videoCopy, err := validateVideoParams(imp.Video, impExt.Ttx.Prod) + + imp.Video = videoCopy + + if err != nil { + return &errortypes.BadInput{ + Message: err.Error(), + } + } + } + return nil } @@ -135,9 +164,18 @@ func (a *TtxAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalReque for _, sb := range bidResp.SeatBid { for i := range sb.Bid { + var bidExt bidExt + var bidType openrtb_ext.BidType + + if err := json.Unmarshal(sb.Bid[i].Ext, &bidExt); err != nil { + bidType = openrtb_ext.BidTypeBanner + } else { + bidType = getBidType(bidExt) + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &sb.Bid[i], - BidType: "banner", + BidType: bidType, }) } } @@ -145,6 +183,43 @@ func (a *TtxAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalReque } +func validateVideoParams(video *openrtb.Video, prod string) (*openrtb.Video, error) { + videoCopy := video + if videoCopy.W == 0 || + videoCopy.H == 0 || + videoCopy.Protocols == nil || + videoCopy.MIMEs == nil || + videoCopy.PlaybackMethod == nil { + + return nil, &errortypes.BadInput{ + Message: "One or more invalid or missing video field(s) w, h, protocols, mimes, playbackmethod", + } + } + + if videoCopy.Placement == 0 { + videoCopy.Placement = 2 + } + + if prod == "instream" { + videoCopy.Placement = 1 + + if videoCopy.StartDelay == nil { + videoCopy.StartDelay = openrtb.StartDelay.Ptr(0) + } + } + + return videoCopy, nil +} + +func getBidType(ext bidExt) openrtb_ext.BidType { + if ext.Ttx.MediaType == "video" { + return openrtb_ext.BidTypeVideo + } + + return openrtb_ext.BidTypeBanner +} + +// New33AcrossBidder configures bidder endpoint func New33AcrossBidder(endpoint string) *TtxAdapter { return &TtxAdapter{ endpoint: endpoint, diff --git a/adapters/33across/33across_test.go b/adapters/33across/33across_test.go index ccdcbf9cc2c..9856f8f9f6a 100644 --- a/adapters/33across/33across_test.go +++ b/adapters/33across/33across_test.go @@ -7,5 +7,5 @@ import ( ) func TestJsonSamples(t *testing.T) { - adapterstest.RunJSONBidderTest(t, "33across", New33AcrossBidder("http://ssc.33across.com")) + adapterstest.RunJSONBidderTest(t, "33acrosstest", New33AcrossBidder("http://ssc.33across.com")) } diff --git a/adapters/33across/33acrosstest/exemplary/bidresponse-defaults.json b/adapters/33across/33acrosstest/exemplary/bidresponse-defaults.json new file mode 100644 index 00000000000..bb0e6585fd0 --- /dev/null +++ b/adapters/33across/33acrosstest/exemplary/bidresponse-defaults.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "placement": 1, + "startdelay": -2, + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "bidder": { + "siteId": "fake-site-id", + "productId": "instream" + } + } + } + ], + "site": {} + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ssc.33across.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "placement": 1, + "startdelay": -2, + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "ttx": { + "prod": "instream" + } + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "ttx", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "h": 90, + "w": 728 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/33across/33acrosstest/exemplary/instream-video-defaults.json b/adapters/33across/33acrosstest/exemplary/instream-video-defaults.json new file mode 100644 index 00000000000..479b197077a --- /dev/null +++ b/adapters/33across/33acrosstest/exemplary/instream-video-defaults.json @@ -0,0 +1,108 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "bidder": { + "siteId": "fake-site-id", + "productId": "instream" + } + } + } + ], + "site": {} + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ssc.33across.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "placement": 1, + "startdelay": 0, + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "ttx": { + "prod": "instream" + } + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "ttx", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "h": 90, + "w": 728, + "ext": { + "ttx": { + "mediaType": "video" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "w": 728, + "h": 90, + "ext": { + "ttx": { + "mediaType": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/33across/33acrosstest/exemplary/multi-format.json b/adapters/33across/33acrosstest/exemplary/multi-format.json new file mode 100644 index 00000000000..db15955ca87 --- /dev/null +++ b/adapters/33across/33acrosstest/exemplary/multi-format.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "bidder": { + "siteId": "fake-site-id", + "productId": "inview" + } + } + } + ], + "site": {} + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ssc.33across.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "placement": 2, + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "ttx": { + "prod": "inview" + } + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "ttx", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "crid_10", + "h": 90, + "w": 728 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "crid_10", + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/33across/33across/exemplary/optional-params.json b/adapters/33across/33acrosstest/exemplary/optional-params.json similarity index 100% rename from adapters/33across/33across/exemplary/optional-params.json rename to adapters/33across/33acrosstest/exemplary/optional-params.json diff --git a/adapters/33across/33acrosstest/exemplary/outstream-video-defaults.json b/adapters/33across/33acrosstest/exemplary/outstream-video-defaults.json new file mode 100644 index 00000000000..c0c31168684 --- /dev/null +++ b/adapters/33across/33acrosstest/exemplary/outstream-video-defaults.json @@ -0,0 +1,107 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "bidder": { + "siteId": "fake-site-id", + "productId": "siab" + } + } + } + ], + "site": {} + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ssc.33across.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "placement": 2, + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "ttx": { + "prod": "siab" + } + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "ttx", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "h": 90, + "w": 728, + "ext": { + "ttx": { + "mediaType": "video" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "w": 728, + "h": 90, + "ext": { + "ttx": { + "mediaType": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/33across/33across/exemplary/simple-banner.json b/adapters/33across/33acrosstest/exemplary/simple-banner.json similarity index 85% rename from adapters/33across/33across/exemplary/simple-banner.json rename to adapters/33across/33acrosstest/exemplary/simple-banner.json index 074badade07..d8c215c06ae 100644 --- a/adapters/33across/33across/exemplary/simple-banner.json +++ b/adapters/33across/33acrosstest/exemplary/simple-banner.json @@ -56,7 +56,12 @@ "adm": "some-test-ad", "crid": "crid_10", "h": 90, - "w": 728 + "w": 728, + "ext": { + "ttx": { + "mediaType": "banner" + } + } }] } ], @@ -78,7 +83,12 @@ "adm": "some-test-ad", "crid": "crid_10", "w": 728, - "h": 90 + "h": 90, + "ext": { + "ttx": { + "mediaType": "banner" + } + } }, "type": "banner" } diff --git a/adapters/33across/33acrosstest/exemplary/simple-video.json b/adapters/33across/33acrosstest/exemplary/simple-video.json new file mode 100644 index 00000000000..55337b92827 --- /dev/null +++ b/adapters/33across/33acrosstest/exemplary/simple-video.json @@ -0,0 +1,110 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "placement": 1, + "startdelay": -2, + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "bidder": { + "siteId": "fake-site-id", + "productId": "instream" + } + } + } + ], + "site": {} + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ssc.33across.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 728, + "h": 90, + "protocols": [2], + "placement": 1, + "startdelay": -2, + "playbackmethod": [2], + "mimes": ["foo", "bar"] + }, + "ext": { + "ttx": { + "prod": "instream" + } + } + } + ], + "site": { + "id": "fake-site-id" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "ttx", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "h": 90, + "w": 728, + "ext": { + "ttx": { + "mediaType": "video" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-vast-ad", + "crid": "crid_10", + "w": 728, + "h": 90, + "ext": { + "ttx": { + "mediaType": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/33across/33across/params/race/banner.json b/adapters/33across/33acrosstest/params/race/banner.json similarity index 100% rename from adapters/33across/33across/params/race/banner.json rename to adapters/33across/33acrosstest/params/race/banner.json diff --git a/adapters/33across/33acrosstest/params/race/video.json b/adapters/33across/33acrosstest/params/race/video.json new file mode 100644 index 00000000000..9df849ad94b --- /dev/null +++ b/adapters/33across/33acrosstest/params/race/video.json @@ -0,0 +1,6 @@ +{ + "productId": "siab", + "siteId": "33across", + "zoneId": "33AcrossZone" + } + \ No newline at end of file diff --git a/adapters/33across/33acrosstest/supplemental/status-not-ok.json b/adapters/33across/33acrosstest/supplemental/status-not-ok.json new file mode 100644 index 00000000000..98fe01c2e50 --- /dev/null +++ b/adapters/33across/33acrosstest/supplemental/status-not-ok.json @@ -0,0 +1,67 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "bidder": { + "siteId": "fake-invalid-site-id", + "productId": "inview" + } + } + } + ], + "site": {} + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ssc.33across.com", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [{"w": 728, "h": 90}] + }, + "ext": { + "ttx": { + "prod": "inview" + } + } + } + ], + "site": { + "id": "fake-invalid-site-id" + } + } + }, + "mockResponse": { + "status": 400, + "body": { + "error": { + "message": "Validation failed", + "details": [ + { + "message": "site.id is invalid" + } + ] + } + } + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/33across/33acrosstest/supplemental/video-validation-fail.json b/adapters/33across/33acrosstest/supplemental/video-validation-fail.json new file mode 100644 index 00000000000..97cb79bd26c --- /dev/null +++ b/adapters/33across/33acrosstest/supplemental/video-validation-fail.json @@ -0,0 +1,32 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 728, + "h": 90 + }, + "ext": { + "bidder": { + "siteId": "fake-site-id", + "productId": "siab" + } + } + } + ], + "site": {} + }, + + "expectedMakeRequestsErrors": [ + { + "value": "One or more invalid or missing video field(s) w, h, protocols, mimes, playbackmethod", + "comparison": "literal" + }, + { + "value": "At least one of [banner, video] formats must be defined in Imp. None found", + "comparison": "literal" + } + ] +} diff --git a/static/bidder-info/33across.yaml b/static/bidder-info/33across.yaml index 84ba6d68611..67e6996accf 100644 --- a/static/bidder-info/33across.yaml +++ b/static/bidder-info/33across.yaml @@ -4,6 +4,8 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner + - video From c481f56a9f3ca5b405a3d623e107f7389c34dc18 Mon Sep 17 00:00:00 2001 From: silvermob <73727464+silvermob@users.noreply.github.com> Date: Wed, 11 Nov 2020 21:25:30 +0700 Subject: [PATCH 265/381] SilverMob adapter (#1561) * SilverMob adapter * Fixes andchanges according to notes in PR * Remaining fixes: multibids, expectedMakeRequestsErrors * removed log * removed log * Multi-bid test * Removed unnesesary block Co-authored-by: Anton Nikityuk --- adapters/silvermob/params_test.go | 56 ++++ adapters/silvermob/silvermob.go | 188 +++++++++++ adapters/silvermob/silvermob_test.go | 11 + .../silvermobtest/exemplary/banner-app.json | 163 ++++++++++ .../exemplary/banner-multi-app.json | 293 ++++++++++++++++++ .../silvermobtest/exemplary/native-app.json | 159 ++++++++++ .../silvermobtest/exemplary/video-app.json | 171 ++++++++++ .../silvermobtest/params/race/banner.json | 4 + .../silvermobtest/params/race/native.json | 4 + .../silvermobtest/params/race/video.json | 4 + .../supplemental/empty-seatbid-array.json | 139 +++++++++ .../supplemental/invalid-response.json | 118 +++++++ .../invalid-silvermob-ext-object.json | 28 ++ .../supplemental/status-code-bad-request.json | 99 ++++++ .../supplemental/status-code-no-content.json | 83 +++++ .../supplemental/status-code-other-error.json | 87 ++++++ .../status-code-service-unavailable.json | 87 ++++++ config/config.go | 1 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_silvermob.go | 7 + static/bidder-info/silvermob.yaml | 8 + static/bidder-params/silvermob.json | 17 + usersync/usersyncers/syncer_test.go | 1 + 24 files changed, 1732 insertions(+) create mode 100644 adapters/silvermob/params_test.go create mode 100644 adapters/silvermob/silvermob.go create mode 100644 adapters/silvermob/silvermob_test.go create mode 100644 adapters/silvermob/silvermobtest/exemplary/banner-app.json create mode 100644 adapters/silvermob/silvermobtest/exemplary/banner-multi-app.json create mode 100644 adapters/silvermob/silvermobtest/exemplary/native-app.json create mode 100644 adapters/silvermob/silvermobtest/exemplary/video-app.json create mode 100644 adapters/silvermob/silvermobtest/params/race/banner.json create mode 100644 adapters/silvermob/silvermobtest/params/race/native.json create mode 100644 adapters/silvermob/silvermobtest/params/race/video.json create mode 100644 adapters/silvermob/silvermobtest/supplemental/empty-seatbid-array.json create mode 100644 adapters/silvermob/silvermobtest/supplemental/invalid-response.json create mode 100644 adapters/silvermob/silvermobtest/supplemental/invalid-silvermob-ext-object.json create mode 100644 adapters/silvermob/silvermobtest/supplemental/status-code-bad-request.json create mode 100644 adapters/silvermob/silvermobtest/supplemental/status-code-no-content.json create mode 100644 adapters/silvermob/silvermobtest/supplemental/status-code-other-error.json create mode 100644 adapters/silvermob/silvermobtest/supplemental/status-code-service-unavailable.json create mode 100644 openrtb_ext/imp_silvermob.go create mode 100644 static/bidder-info/silvermob.yaml create mode 100644 static/bidder-params/silvermob.json diff --git a/adapters/silvermob/params_test.go b/adapters/silvermob/params_test.go new file mode 100644 index 00000000000..13009f6a08b --- /dev/null +++ b/adapters/silvermob/params_test.go @@ -0,0 +1,56 @@ +package silvermob + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// TestValidParams makes sure that the silvermob 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.BidderSilverMob, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected silvermob params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the silvermob 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.BidderSilverMob, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"zoneid": "16", "host": "us"}`, + `{"zoneid": "16", "host": "eu"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"ZoneID": "asd", "Host": "123"}`, + `{}`, + `{"ZoneID": "asd"}`, + `{"Host": "111"}`, + `{"zoneid": 16, "host": 111}`, +} diff --git a/adapters/silvermob/silvermob.go b/adapters/silvermob/silvermob.go new file mode 100644 index 00000000000..be7a1762d23 --- /dev/null +++ b/adapters/silvermob/silvermob.go @@ -0,0 +1,188 @@ +package silvermob + +import ( + "encoding/json" + "fmt" + "github.com/golang/glog" + "net/http" + "text/template" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/macros" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type SilverMobAdapter struct { + endpoint template.Template +} + +func NewSilverMobBidder(endpointTemplate string) *SilverMobAdapter { + template, err := template.New("endpointTemplate").Parse(endpointTemplate) + if err != nil { + glog.Fatal("Unable to parse endpoint url template") + return nil + } + return &SilverMobAdapter{endpoint: *template} +} + +func GetHeaders(request *openrtb.BidRequest) *http.Header { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + + if request.Device != nil { + if len(request.Device.UA) > 0 { + headers.Add("User-Agent", request.Device.UA) + } + + if len(request.Device.IPv6) > 0 { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Forwarded-For", request.Device.IP) + } + } + + return &headers +} + +func (a *SilverMobAdapter) MakeRequests( + openRTBRequest *openrtb.BidRequest, + reqInfo *adapters.ExtraRequestInfo, +) ( + []*adapters.RequestData, + []error, +) { + requestCopy := *openRTBRequest + impCount := len(openRTBRequest.Imp) + requestData := make([]*adapters.RequestData, 0, impCount) + errs := []error{} + + var err error + + for _, imp := range openRTBRequest.Imp { + var silvermobExt *openrtb_ext.ExtSilverMob + + silvermobExt, err = a.getImpressionExt(&imp) + + if err != nil { + errs = append(errs, err) + continue + } + + url, err := a.buildEndpointURL(silvermobExt) + if err != nil { + errs = append(errs, err) + continue + } + + requestCopy.Imp = []openrtb.Imp{imp} + reqJSON, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + reqData := &adapters.RequestData{ + Method: http.MethodPost, + Body: reqJSON, + Uri: url, + Headers: *GetHeaders(&requestCopy), + } + + requestData = append(requestData, reqData) + } + + return requestData, errs +} + +func (a *SilverMobAdapter) getImpressionExt(imp *openrtb.Imp) (*openrtb_ext.ExtSilverMob, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("error unmarshaling imp.ext: %s", err.Error()), + } + } + var silvermobExt openrtb_ext.ExtSilverMob + if err := json.Unmarshal(bidderExt.Bidder, &silvermobExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("error unmarshaling imp.ext.bidder: %s", err.Error()), + } + } + return &silvermobExt, nil +} + +func (a *SilverMobAdapter) buildEndpointURL(params *openrtb_ext.ExtSilverMob) (string, error) { + endpointParams := macros.EndpointTemplateParams{ZoneID: params.ZoneID, Host: params.Host} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *SilverMobAdapter) MakeBids( + openRTBRequest *openrtb.BidRequest, + requestToBidder *adapters.RequestData, + bidderRawResponse *adapters.ResponseData, +) ( + bidderResponse *adapters.BidderResponse, + errs []error, +) { + + if bidderRawResponse.StatusCode == http.StatusNoContent { + return nil, nil + } + + if bidderRawResponse.StatusCode == http.StatusBadRequest { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Bad Request status code: %d. Run with request.debug = 1 for more info", bidderRawResponse.StatusCode), + }} + } + + if bidderRawResponse.StatusCode != http.StatusOK { + return nil, []error{fmt.Errorf("Unexpected status code: %d. Run with request.debug = 1 for more info", bidderRawResponse.StatusCode)} + } + + responseBody := bidderRawResponse.Body + var bidResp openrtb.BidResponse + if err := json.Unmarshal(responseBody, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Error unmarshaling server Response: %s", err), + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Empty SeatBid array", + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + + for _, sb := range bidResp.SeatBid { + for _, bid := range sb.Bid { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: getMediaTypeForImp(bid.ImpID, openRTBRequest.Imp), + }) + } + } + + return bidResponse, nil +} + +func getMediaTypeForImp(impId string, imps []openrtb.Imp) openrtb_ext.BidType { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impId { + if imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } else if imp.Native != nil { + mediaType = openrtb_ext.BidTypeNative + } + return mediaType + } + } + return mediaType +} diff --git a/adapters/silvermob/silvermob_test.go b/adapters/silvermob/silvermob_test.go new file mode 100644 index 00000000000..f75b16fe3c2 --- /dev/null +++ b/adapters/silvermob/silvermob_test.go @@ -0,0 +1,11 @@ +package silvermob + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "silvermobtest", NewSilverMobBidder("http://{{.Host}}.example.com/api/dsp/bid/{{.ZoneID}}")) +} diff --git a/adapters/silvermob/silvermobtest/exemplary/banner-app.json b/adapters/silvermob/silvermobtest/exemplary/banner-app.json new file mode 100644 index 00000000000..7a2a4fef26e --- /dev/null +++ b/adapters/silvermob/silvermobtest/exemplary/banner-app.json @@ -0,0 +1,163 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w":320, + "h":50 + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w":320, + "h":50 + } + ], + "type": "banner", + "seat": "silvermob" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "silvermob": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid":{ + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w":320, + "h":50 + }, + "type": "banner" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/silvermob/silvermobtest/exemplary/banner-multi-app.json b/adapters/silvermob/silvermobtest/exemplary/banner-multi-app.json new file mode 100644 index 00000000000..531704c5c29 --- /dev/null +++ b/adapters/silvermob/silvermobtest/exemplary/banner-multi-app.json @@ -0,0 +1,293 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + }, + { + "id": "another-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":480 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w":320, + "h":50 + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w":320, + "h":50 + } + ], + "type": "banner", + "seat": "silvermob" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "silvermob": 154 + }, + "tmaxrequest": 1000 + } + } + } + }, + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://eu.example.com/api/dsp/bid/1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "another-impression-id", + "banner": { + "w":320, + "h":480 + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "1" + } + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "another-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "21", + "w":320, + "h":480 + } + ], + "type": "banner", + "seat": "silvermob" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "silvermob": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + + { + "bids":[ + { + "bid":{ + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w":320, + "h":50 + }, + "type": "banner" + } + ] + }, + { + "bids": [ + { + "bid":{ + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "another-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "21", + "w":320, + "h":480 + }, + "type": "banner" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/silvermob/silvermobtest/exemplary/native-app.json b/adapters/silvermob/silvermobtest/exemplary/native-app.json new file mode 100644 index 00000000000..49934bf75ab --- /dev/null +++ b/adapters/silvermob/silvermobtest/exemplary/native-app.json @@ -0,0 +1,159 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20" + } + ], + "type": "native", + "seat": "silvermob" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "silvermob": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "asesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ] + }, + "type": "native" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/silvermob/silvermobtest/exemplary/video-app.json b/adapters/silvermob/silvermobtest/exemplary/video-app.json new file mode 100644 index 00000000000..c80d5ab900a --- /dev/null +++ b/adapters/silvermob/silvermobtest/exemplary/video-app.json @@ -0,0 +1,171 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "asesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720 + } + ], + "seat": "silvermob" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "silvermob": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "asesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 1280, + "h": 720 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/silvermob/silvermobtest/params/race/banner.json b/adapters/silvermob/silvermobtest/params/race/banner.json new file mode 100644 index 00000000000..9b6ca9d749b --- /dev/null +++ b/adapters/silvermob/silvermobtest/params/race/banner.json @@ -0,0 +1,4 @@ +{ + "zoneid": "0", + "host": "eu" +} diff --git a/adapters/silvermob/silvermobtest/params/race/native.json b/adapters/silvermob/silvermobtest/params/race/native.json new file mode 100644 index 00000000000..f63a4842b6d --- /dev/null +++ b/adapters/silvermob/silvermobtest/params/race/native.json @@ -0,0 +1,4 @@ +{ + "zoneid": "0", + "host": "eu" +} \ No newline at end of file diff --git a/adapters/silvermob/silvermobtest/params/race/video.json b/adapters/silvermob/silvermobtest/params/race/video.json new file mode 100644 index 00000000000..f63a4842b6d --- /dev/null +++ b/adapters/silvermob/silvermobtest/params/race/video.json @@ -0,0 +1,4 @@ +{ + "zoneid": "0", + "host": "eu" +} \ No newline at end of file diff --git a/adapters/silvermob/silvermobtest/supplemental/empty-seatbid-array.json b/adapters/silvermob/silvermobtest/supplemental/empty-seatbid-array.json new file mode 100644 index 00000000000..be95abeaa2f --- /dev/null +++ b/adapters/silvermob/silvermobtest/supplemental/empty-seatbid-array.json @@ -0,0 +1,139 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "silvermob": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Empty SeatBid array", + "comparison": "literal" + } + ] +} diff --git a/adapters/silvermob/silvermobtest/supplemental/invalid-response.json b/adapters/silvermob/silvermobtest/supplemental/invalid-response.json new file mode 100644 index 00000000000..d2a1e890df0 --- /dev/null +++ b/adapters/silvermob/silvermobtest/supplemental/invalid-response.json @@ -0,0 +1,118 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "X-Openrtb-Version": [ + "2.5" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ] + }, + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w":320, + "h":50 + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": "invalid response" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Error unmarshaling server Response: json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/silvermob/silvermobtest/supplemental/invalid-silvermob-ext-object.json b/adapters/silvermob/silvermobtest/supplemental/invalid-silvermob-ext-object.json new file mode 100644 index 00000000000..090d7aff5b6 --- /dev/null +++ b/adapters/silvermob/silvermobtest/supplemental/invalid-silvermob-ext-object.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": "Awesome" + } + ], + "site": { + "page": "test.com" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "error unmarshaling imp.ext: json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] +} diff --git a/adapters/silvermob/silvermobtest/supplemental/status-code-bad-request.json b/adapters/silvermob/silvermobtest/supplemental/status-code-bad-request.json new file mode 100644 index 00000000000..e93f249030d --- /dev/null +++ b/adapters/silvermob/silvermobtest/supplemental/status-code-bad-request.json @@ -0,0 +1,99 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID", + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 400 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Bad Request status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/silvermob/silvermobtest/supplemental/status-code-no-content.json b/adapters/silvermob/silvermobtest/supplemental/status-code-no-content.json new file mode 100644 index 00000000000..a29710bfa85 --- /dev/null +++ b/adapters/silvermob/silvermobtest/supplemental/status-code-no-content.json @@ -0,0 +1,83 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "id": "app_001", + "bundle": "com.awesome.app", + "publisher": { + "id": "2" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "app_001", + "bundle": "com.awesome.app", + "publisher": { + "id": "2" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/silvermob/silvermobtest/supplemental/status-code-other-error.json b/adapters/silvermob/silvermobtest/supplemental/status-code-other-error.json new file mode 100644 index 00000000000..a32af01e55f --- /dev/null +++ b/adapters/silvermob/silvermobtest/supplemental/status-code-other-error.json @@ -0,0 +1,87 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "id": "app_001", + "bundle": "com.awesome.app", + "publisher": { + "id": "2" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "app_001", + "bundle": "com.awesome.app", + "publisher": { + "id": "2" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 306 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 306. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/silvermob/silvermobtest/supplemental/status-code-service-unavailable.json b/adapters/silvermob/silvermobtest/supplemental/status-code-service-unavailable.json new file mode 100644 index 00000000000..5772a86ee8a --- /dev/null +++ b/adapters/silvermob/silvermobtest/supplemental/status-code-service-unavailable.json @@ -0,0 +1,87 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "id": "app_001", + "bundle": "com.awesome.app", + "publisher": { + "id": "2" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://eu.example.com/api/dsp/bid/0", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "host": "eu", + "zoneid": "0" + } + } + } + ], + "app": { + "id": "app_001", + "bundle": "com.awesome.app", + "publisher": { + "id": "2" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 503 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 503. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/config/config.go b/config/config.go index c2db7b7d03c..7d1fac3c633 100755 --- a/config/config.go +++ b/config/config.go @@ -992,6 +992,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.rubicon.disabled", true) 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.silvermob.endpoint", "http://{{.Host}}.silvermob.com/marketplace/api/dsp/bid/{{.ZoneID}}") v.SetDefault("adapters.smaato.endpoint", "https://prebid.ad.smaato.net/oapi/prebid") v.SetDefault("adapters.smartadserver.endpoint", "https://ssb-global.smartadserver.com") v.SetDefault("adapters.smartrtb.endpoint", "http://market-east.smrtb.com/json/publisher/rtb?pubid={{.PublisherID}}") diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index ff72165bcb3..9800ccd7e28 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -74,6 +74,7 @@ import ( "github.com/prebid/prebid-server/adapters/rtbhouse" "github.com/prebid/prebid-server/adapters/rubicon" "github.com/prebid/prebid-server/adapters/sharethrough" + "github.com/prebid/prebid-server/adapters/silvermob" "github.com/prebid/prebid-server/adapters/smaato" "github.com/prebid/prebid-server/adapters/smartadserver" "github.com/prebid/prebid-server/adapters/smartrtb" @@ -176,6 +177,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.BidderSilverMob: silvermob.NewSilverMobBidder(cfg.Adapters[string(openrtb_ext.BidderSilverMob)].Endpoint), openrtb_ext.BidderSmaato: smaato.NewSmaatoBidder(cfg.Adapters[string(openrtb_ext.BidderSmaato)].Endpoint), openrtb_ext.BidderSmartadserver: smartadserver.NewSmartadserverBidder(cfg.Adapters[string(openrtb_ext.BidderSmartadserver)].Endpoint), openrtb_ext.BidderSmartRTB: smartrtb.NewSmartRTBBidder(cfg.Adapters[string(openrtb_ext.BidderSmartRTB)].Endpoint), diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index b98c80ae9af..0fac0800ee3 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -93,6 +93,7 @@ const ( BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSharethrough BidderName = "sharethrough" + BidderSilverMob BidderName = "silvermob" BidderSmaato BidderName = "smaato" BidderSmartadserver BidderName = "smartadserver" BidderSmartRTB BidderName = "smartrtb" @@ -187,6 +188,7 @@ var BidderMap = map[string]BidderName{ "rtbhouse": BidderRTBHouse, "rubicon": BidderRubicon, "sharethrough": BidderSharethrough, + "silvermob": BidderSilverMob, "smaato": BidderSmaato, "smartadserver": BidderSmartadserver, "smartrtb": BidderSmartRTB, diff --git a/openrtb_ext/imp_silvermob.go b/openrtb_ext/imp_silvermob.go new file mode 100644 index 00000000000..9b2465534ca --- /dev/null +++ b/openrtb_ext/imp_silvermob.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtSilverMob defines the contract for bidrequest.imp[i].ext.silvermob +type ExtSilverMob struct { + ZoneID string `json:"zoneid"` + Host string `json:"host"` +} diff --git a/static/bidder-info/silvermob.yaml b/static/bidder-info/silvermob.yaml new file mode 100644 index 00000000000..5f1e4809dd3 --- /dev/null +++ b/static/bidder-info/silvermob.yaml @@ -0,0 +1,8 @@ +maintainer: + email: "support@silvermob.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-params/silvermob.json b/static/bidder-params/silvermob.json new file mode 100644 index 00000000000..8ebc85a2ab7 --- /dev/null +++ b/static/bidder-params/silvermob.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "SilverMob Adapter Params", + "description": "A schema which validates params accepted by the SilverMob adapter", + "type": "object", + "properties": { + "zoneid": { + "type": "string", + "description": "Zone ID" + }, + "host": { + "type": "string", + "description": "Host" + } + }, + "required": ["zoneid", "host"] + } \ No newline at end of file diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 7582055fa46..a6e74966ad7 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -102,6 +102,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderMobileFuse: true, openrtb_ext.BidderOrbidder: true, openrtb_ext.BidderPubnative: true, + openrtb_ext.BidderSilverMob: true, openrtb_ext.BidderSmaato: true, openrtb_ext.BidderTappx: true, openrtb_ext.BidderYeahmobi: true, From 9a3f2a042aff31127037d5f80531edee6fd560d3 Mon Sep 17 00:00:00 2001 From: Seba Perez Date: Wed, 11 Nov 2020 15:28:33 -0300 Subject: [PATCH 266/381] Updated ePlanning GVL ID (#1574) --- adapters/eplanning/usersync.go | 2 +- adapters/eplanning/usersync_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/eplanning/usersync.go b/adapters/eplanning/usersync.go index 252c106a77c..faa7fa82a19 100644 --- a/adapters/eplanning/usersync.go +++ b/adapters/eplanning/usersync.go @@ -8,5 +8,5 @@ import ( ) func NewEPlanningSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("eplanning", 0, temp, adapters.SyncTypeIframe) + return adapters.NewSyncer("eplanning", 90, temp, adapters.SyncTypeIframe) } diff --git a/adapters/eplanning/usersync_test.go b/adapters/eplanning/usersync_test.go index 890832bafc3..85770689024 100644 --- a/adapters/eplanning/usersync_test.go +++ b/adapters/eplanning/usersync_test.go @@ -20,6 +20,6 @@ func TestEPlanningSyncer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "https://ads.us.e-planning.net/uspd/1/?du=https%3A%2F%2Fads.us.e-planning.net%2Fgetuid%2F1%2F5a1ad71d2d53a0f5%3Flocalhost%2Fsetuid%3Fbidder%3Deplanning%26gdpr%3D%26gdpr_consent%3D%26uid%3D%24UID", syncInfo.URL) assert.Equal(t, "iframe", syncInfo.Type) - assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.EqualValues(t, 90, syncer.GDPRVendorID()) assert.Equal(t, false, syncInfo.SupportCORS) } From aaecdfada00fa657fcb4237ea2c4caf3b2f664e6 Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 11 Nov 2020 22:02:59 +0100 Subject: [PATCH 267/381] update adpone google vendor id (#1577) --- adapters/adpone/usersync.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/adpone/usersync.go b/adapters/adpone/usersync.go index 480ecb82f3f..67d4c998275 100644 --- a/adapters/adpone/usersync.go +++ b/adapters/adpone/usersync.go @@ -7,7 +7,7 @@ import ( "github.com/prebid/prebid-server/usersync" ) -const adponeGDPRVendorID = uint16(16) +const adponeGDPRVendorID = uint16(799) const adponeFamilyName = "adpone" func NewadponeSyncer(urlTemplate *template.Template) usersync.Usersyncer { From 70600225899ce1b55e1ea19a6c07d1172b1bd642 Mon Sep 17 00:00:00 2001 From: Gena Date: Thu, 12 Nov 2020 17:43:05 +0200 Subject: [PATCH 268/381] ADtelligent gvlid (#1581) --- adapters/adtelligent/usersync.go | 2 +- adapters/adtelligent/usersync_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapters/adtelligent/usersync.go b/adapters/adtelligent/usersync.go index cda3b62a071..087b5bdd22d 100644 --- a/adapters/adtelligent/usersync.go +++ b/adapters/adtelligent/usersync.go @@ -8,5 +8,5 @@ import ( ) func NewAdtelligentSyncer(temp *template.Template) usersync.Usersyncer { - return adapters.NewSyncer("adtelligent", 0, temp, adapters.SyncTypeRedirect) + return adapters.NewSyncer("adtelligent", 410, temp, adapters.SyncTypeRedirect) } diff --git a/adapters/adtelligent/usersync_test.go b/adapters/adtelligent/usersync_test.go index 1cc5dfe4627..fa157d226c5 100644 --- a/adapters/adtelligent/usersync_test.go +++ b/adapters/adtelligent/usersync_test.go @@ -25,6 +25,6 @@ func TestAdtelligentSyncer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "//sync.adtelligent.com/csync?t=p&ep=0&redir=localhost%2Fsetuid%3Fbidder%3Dadtelligent%26gdpr%3D0%26gdpr_consent%3D%26uid%3D%7Buid%7D", syncInfo.URL) assert.Equal(t, "redirect", syncInfo.Type) - assert.EqualValues(t, 0, syncer.GDPRVendorID()) + assert.EqualValues(t, 410, syncer.GDPRVendorID()) assert.Equal(t, false, syncInfo.SupportCORS) } From 12d96a6ef68161e0db498215d99410d5d64cc331 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Thu, 12 Nov 2020 13:54:35 -0500 Subject: [PATCH 269/381] Add account/ host GDPR enabled flags & account per request type GDPR enabled flags (#1564) * Add account level request type specific and general GDPR enabled flags * Clean up test TestAccountLevelGDPREnabled * Add host-level GDPR enabled flag * Move account GDPR enable check as receiver method on accountGDPR * Remove mapstructure annotations on account structs * Minor test updates * Re-add mapstructure annotations on account structs * Change RequestType to IntegrationType and struct annotation formatting * Update comment * Update account IntegrationType comments * Remove extra space in config/accounts.go via gofmt --- config/accounts.go | 52 ++++++++ config/accounts_test.go | 116 +++++++++++++++++ config/config.go | 2 + exchange/exchange.go | 2 +- exchange/exchange_test.go | 2 + .../exchangetest/gdpr-geo-eu-off-device.json | 1 + exchange/exchangetest/gdpr-geo-eu-off.json | 1 + .../gdpr-geo-eu-on-featureflag-off.json | 62 +++++++++ exchange/exchangetest/gdpr-geo-eu-on.json | 1 + exchange/utils.go | 23 +++- exchange/utils_test.go | 123 ++++++++++++++---- 11 files changed, 358 insertions(+), 27 deletions(-) create mode 100644 config/accounts_test.go create mode 100644 exchange/exchangetest/gdpr-geo-eu-on-featureflag-off.json diff --git a/config/accounts.go b/config/accounts.go index 162818eb95d..5ec818b843f 100644 --- a/config/accounts.go +++ b/config/accounts.go @@ -1,9 +1,61 @@ package config +// IntegrationType enumerates the values of integrations Prebid Server can configure for an account +type IntegrationType string + +// Possible values of integration types Prebid Server can configure for an account +const ( + IntegrationTypeAMP IntegrationType = "amp" + IntegrationTypeApp IntegrationType = "app" + IntegrationTypeVideo IntegrationType = "video" + IntegrationTypeWeb IntegrationType = "web" +) + // Account represents a publisher account configuration type Account struct { ID string `mapstructure:"id" json:"id"` Disabled bool `mapstructure:"disabled" json:"disabled"` CacheTTL DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"` EventsEnabled bool `mapstructure:"events_enabled" json:"events_enabled"` + GDPR AccountGDPR `mapstructure:"gdpr" json:"gdpr"` +} + +// AccountGDPR represents account-specific GDPR configuration +type AccountGDPR struct { + Enabled *bool `mapstructure:"enabled" json:"enabled,omitempty"` + IntegrationEnabled AccountGDPRIntegration `mapstructure:"integration_enabled" json:"integration_enabled"` +} + +// EnabledForIntegrationType indicates whether GDPR is turned on at the account level for the specified integration type +// by using the integration type setting if defined or the general GDPR setting if defined; otherwise it returns nil +func (a *AccountGDPR) EnabledForIntegrationType(integrationType IntegrationType) *bool { + var integrationEnabled *bool + + switch integrationType { + case IntegrationTypeAMP: + integrationEnabled = a.IntegrationEnabled.AMP + case IntegrationTypeApp: + integrationEnabled = a.IntegrationEnabled.App + case IntegrationTypeVideo: + integrationEnabled = a.IntegrationEnabled.Video + case IntegrationTypeWeb: + integrationEnabled = a.IntegrationEnabled.Web + } + + if integrationEnabled != nil { + return integrationEnabled + } + if a.Enabled != nil { + return a.Enabled + } + + return nil +} + +// AccountGDPRIntegration indicates whether GDPR is enabled for each integration type +type AccountGDPRIntegration struct { + AMP *bool `mapstructure:"amp" json:"amp,omitempty"` + App *bool `mapstructure:"app" json:"app,omitempty"` + Video *bool `mapstructure:"video" json:"video,omitempty"` + Web *bool `mapstructure:"web" json:"web,omitempty"` } diff --git a/config/accounts_test.go b/config/accounts_test.go new file mode 100644 index 00000000000..ca7e893835f --- /dev/null +++ b/config/accounts_test.go @@ -0,0 +1,116 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAccountGDPREnabledForIntegrationType(t *testing.T) { + trueValue, falseValue := true, false + + tests := []struct { + description string + giveIntegrationType IntegrationType + giveGDPREnabled *bool + giveAMPGDPREnabled *bool + giveAppGDPREnabled *bool + giveVideoGDPREnabled *bool + giveWebGDPREnabled *bool + wantEnabled *bool + }{ + { + description: "GDPR AMP integration enabled, general GDPR disabled", + giveIntegrationType: IntegrationTypeAMP, + giveGDPREnabled: &falseValue, + giveAMPGDPREnabled: &trueValue, + wantEnabled: &trueValue, + }, + { + description: "GDPR App integration enabled, general GDPR disabled", + giveIntegrationType: IntegrationTypeApp, + giveGDPREnabled: &falseValue, + giveAppGDPREnabled: &trueValue, + wantEnabled: &trueValue, + }, + { + description: "GDPR Video integration enabled, general GDPR disabled", + giveIntegrationType: IntegrationTypeVideo, + giveGDPREnabled: &falseValue, + giveVideoGDPREnabled: &trueValue, + wantEnabled: &trueValue, + }, + { + description: "GDPR Web integration enabled, general GDPR disabled", + giveIntegrationType: IntegrationTypeWeb, + giveGDPREnabled: &falseValue, + giveWebGDPREnabled: &trueValue, + wantEnabled: &trueValue, + }, + { + description: "Web integration enabled, general GDPR unspecified", + giveIntegrationType: IntegrationTypeWeb, + giveGDPREnabled: nil, + giveWebGDPREnabled: &trueValue, + wantEnabled: &trueValue, + }, + { + description: "GDPR Web integration disabled, general GDPR enabled", + giveIntegrationType: IntegrationTypeWeb, + giveGDPREnabled: &trueValue, + giveWebGDPREnabled: &falseValue, + wantEnabled: &falseValue, + }, + { + description: "GDPR Web integration disabled, general GDPR unspecified", + giveIntegrationType: IntegrationTypeWeb, + giveGDPREnabled: nil, + giveWebGDPREnabled: &falseValue, + wantEnabled: &falseValue, + }, + { + description: "GDPR Web integration unspecified, general GDPR disabled", + giveIntegrationType: IntegrationTypeWeb, + giveGDPREnabled: &falseValue, + giveWebGDPREnabled: nil, + wantEnabled: &falseValue, + }, + { + description: "GDPR Web integration unspecified, general GDPR enabled", + giveIntegrationType: IntegrationTypeWeb, + giveGDPREnabled: &trueValue, + giveWebGDPREnabled: nil, + wantEnabled: &trueValue, + }, + { + description: "GDPR Web integration unspecified, general GDPR unspecified", + giveIntegrationType: IntegrationTypeWeb, + giveGDPREnabled: nil, + giveWebGDPREnabled: nil, + wantEnabled: nil, + }, + } + + for _, tt := range tests { + account := Account{ + GDPR: AccountGDPR{ + Enabled: tt.giveGDPREnabled, + IntegrationEnabled: AccountGDPRIntegration{ + AMP: tt.giveAMPGDPREnabled, + App: tt.giveAppGDPREnabled, + Video: tt.giveVideoGDPREnabled, + Web: tt.giveWebGDPREnabled, + }, + }, + } + + enabled := account.GDPR.EnabledForIntegrationType(tt.giveIntegrationType) + + if tt.wantEnabled == nil { + assert.Nil(t, enabled, tt.description) + } else { + assert.NotNil(t, enabled, tt.description) + assert.Equal(t, *tt.wantEnabled, *enabled, tt.description) + } + } +} diff --git a/config/config.go b/config/config.go index 7d1fac3c633..045aea3c7ab 100755 --- a/config/config.go +++ b/config/config.go @@ -210,6 +210,7 @@ type Privacy struct { } type GDPR struct { + Enabled bool `mapstructure:"enabled"` HostVendorID int `mapstructure:"host_vendor_id"` UsersyncIfAmbiguous bool `mapstructure:"usersync_if_ambiguous"` Timeouts GDPRTimeouts `mapstructure:"timeouts_ms"` @@ -1028,6 +1029,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("analytics.pubstack.buffers.count", 100) v.SetDefault("analytics.pubstack.buffers.timeout", "900s") v.SetDefault("amp_timeout_adjustment_ms", 0) + v.SetDefault("gdpr.enabled", true) v.SetDefault("gdpr.host_vendor_id", 0) v.SetDefault("gdpr.usersync_if_ambiguous", false) v.SetDefault("gdpr.timeouts_ms.init_vendorlist_fetches", 0) diff --git a/exchange/exchange.go b/exchange/exchange.go index 2e8fe76fcfa..3a5f785bbc8 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -122,7 +122,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, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig) + cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig, account) e.me.RecordRequestPrivacy(privacyLabels) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index ae8bc3c8c9d..bd2fa1147ef 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -1305,6 +1305,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { Enforce: spec.EnforceLMT, }, GDPR: config.GDPR{ + Enabled: spec.GDPREnabled, UsersyncIfAmbiguous: !spec.AssumeGDPRApplies, EEACountriesMap: eeac, }, @@ -2444,6 +2445,7 @@ func TestUpdateHbPbCatDur(t *testing.T) { } type exchangeSpec struct { + GDPREnabled bool `json:"gdpr_enabled"` IncomingRequest exchangeRequest `json:"incomingRequest"` OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` Response exchangeResponse `json:"response,omitempty"` diff --git a/exchange/exchangetest/gdpr-geo-eu-off-device.json b/exchange/exchangetest/gdpr-geo-eu-off-device.json index fc655de8162..f704cdd5c8e 100644 --- a/exchange/exchangetest/gdpr-geo-eu-off-device.json +++ b/exchange/exchangetest/gdpr-geo-eu-off-device.json @@ -1,5 +1,6 @@ { "assume_gdpr_applies": false, + "gdpr_enabled": true, "incomingRequest": { "ortbRequest": { "id": "some-request-id", diff --git a/exchange/exchangetest/gdpr-geo-eu-off.json b/exchange/exchangetest/gdpr-geo-eu-off.json index 27a030f11fc..24357eb7eec 100644 --- a/exchange/exchangetest/gdpr-geo-eu-off.json +++ b/exchange/exchangetest/gdpr-geo-eu-off.json @@ -1,5 +1,6 @@ { "assume_gdpr_applies": false, + "gdpr_enabled": true, "incomingRequest": { "ortbRequest": { "id": "some-request-id", diff --git a/exchange/exchangetest/gdpr-geo-eu-on-featureflag-off.json b/exchange/exchangetest/gdpr-geo-eu-on-featureflag-off.json new file mode 100644 index 00000000000..6c6ca3edc62 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-on-featureflag-off.json @@ -0,0 +1,62 @@ +{ + "assume_gdpr_applies": true, + "gdpr_enabled": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-on.json b/exchange/exchangetest/gdpr-geo-eu-on.json index 4ec42fc6c70..eb42a17c936 100644 --- a/exchange/exchangetest/gdpr-geo-eu-on.json +++ b/exchange/exchangetest/gdpr-geo-eu-on.json @@ -1,5 +1,6 @@ { "assume_gdpr_applies": true, + "gdpr_enabled": true, "incomingRequest": { "ortbRequest": { "id": "some-request-id", diff --git a/exchange/utils.go b/exchange/utils.go index 63e56e841c3..f6fcc711e42 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -19,6 +19,13 @@ import ( "github.com/prebid/prebid-server/privacy/lmt" ) +var integrationTypeMap = map[pbsmetrics.RequestType]config.IntegrationType{ + pbsmetrics.ReqTypeAMP: config.IntegrationTypeAMP, + pbsmetrics.ReqTypeORTB2App: config.IntegrationTypeApp, + pbsmetrics.ReqTypeVideo: config.IntegrationTypeVideo, + pbsmetrics.ReqTypeORTB2Web: config.IntegrationTypeWeb, +} + const unknownBidder string = "" func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { @@ -53,7 +60,8 @@ func cleanOpenRTBRequests(ctx context.Context, labels pbsmetrics.Labels, gDPR gdpr.Permissions, usersyncIfAmbiguous bool, - privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, privacyLabels pbsmetrics.PrivacyLabels, errs []error) { + privacyConfig config.Privacy, + account *config.Account) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, privacyLabels pbsmetrics.PrivacyLabels, errs []error) { impsByBidder, errs := splitImps(orig.Imp) if len(errs) > 0 { @@ -94,7 +102,9 @@ func cleanOpenRTBRequests(ctx context.Context, privacyLabels.COPPAEnforced = privacyEnforcement.COPPA privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder) - if gdpr == 1 { + gdprEnabled := gdprEnabled(account, privacyConfig, integrationTypeMap[labels.RType]) + + if gdpr == 1 && gdprEnabled { privacyLabels.GDPREnforced = true parsedConsent, err := vendorconsent.ParseString(consent) if err == nil { @@ -109,7 +119,7 @@ func cleanOpenRTBRequests(ctx context.Context, privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidder.String()) // GDPR - if gdpr == 1 { + if gdpr == 1 && gdprEnabled { coreBidder := resolveBidder(bidder.String(), aliases) var publisherID = labels.PubID @@ -127,6 +137,13 @@ func cleanOpenRTBRequests(ctx context.Context, return } +func gdprEnabled(account *config.Account, privacyConfig config.Privacy, integrationType config.IntegrationType) bool { + if accountEnabled := account.GDPR.EnabledForIntegrationType(integrationType); accountEnabled != nil { + return *accountEnabled + } + return privacyConfig.GDPR.Enabled +} + func extractCCPA(orig *openrtb.BidRequest, privacyConfig config.Privacy, aliases map[string]string) (privacy.PolicyEnforcer, error) { ccpaPolicy, err := ccpa.ReadFromRequest(orig) if err != nil { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 94975e82a46..14293ae20a3 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -82,7 +82,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { } for _, test := range testCases { - reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{}) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -179,7 +179,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { }, } - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{}) result := results["appnexus"] assert.Nil(t, errs) @@ -229,7 +229,7 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { Enforce: true, }, } - _, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &reqExtStruct, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + _, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &reqExtStruct, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{}) assert.ElementsMatch(t, []error{test.expectError}, errs, test.description) } @@ -264,7 +264,7 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { req := newBidRequest(t) req.Regs = &openrtb.Regs{COPPA: test.coppa} - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}, &config.Account{}) result := results["appnexus"] assert.Nil(t, errs) @@ -366,7 +366,7 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { extRequest = unmarshaledExt } - results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, extRequest, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{}) + results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, extRequest, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{}, &config.Account{}) result := results["appnexus"] if test.hasError == true { @@ -943,7 +943,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { }, } - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{}) result := results["appnexus"] assert.Nil(t, errs) @@ -959,8 +959,12 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { } func TestCleanOpenRTBRequestsGDPR(t *testing.T) { + trueValue, falseValue := true, false + testCases := []struct { description string + gdprAccountEnabled *bool + gdprHostEnabled bool gdpr string gdprConsent string gdprScrub bool @@ -968,40 +972,96 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { expectPrivacyLabels pbsmetrics.PrivacyLabels }{ { - description: "Enforce - TCF Invalid", - gdpr: "1", - gdprConsent: "malformed", - gdprScrub: false, + description: "Enforce - TCF Invalid", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "malformed", + gdprScrub: false, expectPrivacyLabels: pbsmetrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: "", }, }, { - description: "Enforce - TCF 1", - gdpr: "1", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", - gdprScrub: true, + description: "Enforce - TCF 1", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, expectPrivacyLabels: pbsmetrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: pbsmetrics.TCFVersionV1, }, }, { - description: "Enforce - TCF 2", - gdpr: "1", - gdprConsent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", - gdprScrub: true, + description: "Enforce - TCF 2", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + gdprScrub: true, expectPrivacyLabels: pbsmetrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: pbsmetrics.TCFVersionV2, }, }, { - description: "Not Enforce - TCF 1", - gdpr: "0", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", - gdprScrub: false, + description: "Not Enforce - TCF 1", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "0", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + }, + { + description: "Enforce - TCF 1; account GDPR enabled, host GDPR setting disregarded", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: false, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }, + }, + { + description: "Not Enforce - TCF 1; account GDPR disabled, host GDPR setting disregarded", + gdprAccountEnabled: &falseValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + }, + { + description: "Enforce - TCF 1; account GDPR not specified, host GDPR enabled", + gdprAccountEnabled: nil, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }, + }, + { + description: "Not Enforce - TCF 1; account GDPR not specified, host GDPR disabled", + gdprAccountEnabled: nil, + gdprHostEnabled: false, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, expectPrivacyLabels: pbsmetrics.PrivacyLabels{ GDPREnforced: false, GDPRTCFVersion: "", @@ -1018,13 +1078,30 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { privacyConfig := config.Privacy{ GDPR: config.GDPR{ + Enabled: test.gdprHostEnabled, TCF2: config.TCF2{ Enabled: true, }, }, } - results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: !test.gdprScrub}, true, privacyConfig) + accountConfig := config.Account{ + GDPR: config.AccountGDPR{ + Enabled: test.gdprAccountEnabled, + }, + } + + results, _, privacyLabels, errs := cleanOpenRTBRequests( + context.Background(), + req, + nil, + &emptyUsersync{}, + map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, + pbsmetrics.Labels{}, + &permissionsMock{personalInfoAllowed: !test.gdprScrub}, + true, + privacyConfig, + &accountConfig) result := results["appnexus"] assert.Nil(t, errs) From ada88b4f72dc3c298eec61242e527427c071a4fc Mon Sep 17 00:00:00 2001 From: Steve Alliance Date: Mon, 16 Nov 2020 07:47:51 -0500 Subject: [PATCH 270/381] DMX Bidfloor fix (#1579) --- adapters/dmx/dmx.go | 27 ++-- .../exemplary/imp-populated-banner.json | 139 ++++++++++++++++++ 2 files changed, 150 insertions(+), 16 deletions(-) create mode 100644 adapters/dmx/dmxtest/exemplary/imp-populated-banner.json diff --git a/adapters/dmx/dmx.go b/adapters/dmx/dmx.go index dfb97f33abb..6da7823a75f 100644 --- a/adapters/dmx/dmx.go +++ b/adapters/dmx/dmx.go @@ -168,6 +168,7 @@ func (adapter *DmxAdapter) MakeRequests(request *openrtb.BidRequest, req *adapte Body: oJson, Headers: headers, } + reqsBidder = append(reqsBidder, reqBidder) return } @@ -221,35 +222,29 @@ func (adapter *DmxAdapter) MakeBids(request *openrtb.BidRequest, externalRequest } func fetchParams(params dmxExt, inst openrtb.Imp, ins openrtb.Imp, imps []openrtb.Imp, banner *openrtb.Banner, video *openrtb.Video, intVal int8) []openrtb.Imp { + var tempimp openrtb.Imp + tempimp = inst if params.Bidder.TagId != "" { - ins = openrtb.Imp{ - ID: inst.ID, - TagID: params.Bidder.TagId, - Ext: inst.Ext, - Secure: &intVal, - } + tempimp.TagID = params.Bidder.TagId + tempimp.Secure = &intVal } if params.Bidder.DmxId != "" { - ins = openrtb.Imp{ - ID: inst.ID, - TagID: params.Bidder.DmxId, - Ext: inst.Ext, - Secure: &intVal, - } + tempimp.TagID = params.Bidder.DmxId + tempimp.Secure = &intVal } if banner != nil { - ins.Banner = banner + tempimp.Banner = banner } if video != nil { - ins.Video = video + tempimp.Video = video } - if ins.TagID == "" { + if tempimp.TagID == "" { return imps } - imps = append(imps, ins) + imps = append(imps, tempimp) return imps } diff --git a/adapters/dmx/dmxtest/exemplary/imp-populated-banner.json b/adapters/dmx/dmxtest/exemplary/imp-populated-banner.json new file mode 100644 index 00000000000..f2b30bb400b --- /dev/null +++ b/adapters/dmx/dmxtest/exemplary/imp-populated-banner.json @@ -0,0 +1,139 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app":{ + "bundle":"302324249", + "id":"ed6207cefff74c14878963566683c070", + "name":"Skout - iOS Match Buy", + "publisher":{ + "id":"10400" + }, + "storeurl":"https://itunes.apple.com/app/id302324249" + }, + "imp": [ + { + "bidfloor": 0.35, + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "dmxid": "123454", + "publisher_id": "10400" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "", + "body": { + "id": "test-request-id", + "app":{ + "bundle":"302324249", + "id":"ed6207cefff74c14878963566683c070", + "name":"Skout - iOS Match Buy", + "publisher":{ + "id":"10400" + }, + "storeurl":"https://itunes.apple.com/app/id302324249" + }, + "imp": [ + { + "bidfloor": 0.35, + "id": "test-imp-id", + "tagid": "123454", + "secure": 1, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisher_id": "10400", + "dmxid": "123454" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.75, + "adid": "29681110", + "adm": "
banner-ads
", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300 + }] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 1.75, + "adm": "
banner-ads
", + "adid": "29681110", + "adomain": ["dmx.districtm.io"], + "iurl": "https://dmx.districtm.io/b/v2", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250 + + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file From acf889e881afdf1085a76e45e61879e69f931320 Mon Sep 17 00:00:00 2001 From: Jurij Sinickij Date: Tue, 17 Nov 2020 18:39:18 +0200 Subject: [PATCH 271/381] adform bidder video bid response support (#1573) --- adapters/adform/adform.go | 25 +++-- adapters/adform/adform_test.go | 67 ++++++++++--- .../exemplary/multiformat-impression.json | 99 +++++++++++++++++++ .../exemplary/single-banner-impression.json | 64 ++++++++++++ .../exemplary/single-video-impression.json | 60 +++++++++++ .../adform/adformtest/params/race/video.json | 3 + static/bidder-info/adform.yaml | 2 + 7 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 adapters/adform/adformtest/exemplary/multiformat-impression.json create mode 100644 adapters/adform/adformtest/exemplary/single-banner-impression.json create mode 100644 adapters/adform/adformtest/exemplary/single-video-impression.json create mode 100644 adapters/adform/adformtest/params/race/video.json diff --git a/adapters/adform/adform.go b/adapters/adform/adform.go index 5881f4ab86e..d7e5dedac22 100644 --- a/adapters/adform/adform.go +++ b/adapters/adform/adform.go @@ -79,6 +79,7 @@ type adformBid struct { Height uint64 `json:"height,omitempty"` DealId string `json:"deal_id,omitempty"` CreativeId string `json:"win_crid,omitempty"` + VastContent string `json:"vast_content,omitempty"` } const priceTypeGross = "gross" @@ -241,7 +242,8 @@ func toPBSBidSlice(adformBids []*adformBid, r *adformRequest) pbs.PBSBidSlice { bids := make(pbs.PBSBidSlice, 0) for i, bid := range adformBids { - if bid.Banner == "" || bid.ResponseType != "banner" { + adm, bidType := getAdAndType(bid) + if adm == "" { continue } pbsBid := pbs.PBSBid{ @@ -249,12 +251,12 @@ func toPBSBidSlice(adformBids []*adformBid, r *adformRequest) pbs.PBSBidSlice { AdUnitCode: r.adUnits[i].adUnitCode, BidderCode: r.bidderCode, Price: bid.Price, - Adm: bid.Banner, + Adm: adm, Width: bid.Width, Height: bid.Height, DealId: bid.DealId, Creative_id: bid.CreativeId, - CreativeMediaType: string(openrtb_ext.BidTypeBanner), + CreativeMediaType: string(bidType), } bids = append(bids, &pbsBid) @@ -632,21 +634,23 @@ func toOpenRtbBidResponse(adformBids []*adformBid, r *openrtb.BidRequest) *adapt } for i, bid := range adformBids { - if bid.Banner == "" || bid.ResponseType != "banner" { + adm, bidType := getAdAndType(bid) + if adm == "" { continue } + openRtbBid := openrtb.Bid{ ID: r.Imp[i].ID, ImpID: r.Imp[i].ID, Price: bid.Price, - AdM: bid.Banner, + AdM: adm, W: bid.Width, H: bid.Height, DealID: bid.DealId, CrID: bid.CreativeId, } - bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &openRtbBid, BidType: openrtb_ext.BidTypeBanner}) + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &openRtbBid, BidType: bidType}) currency = bid.Currency } @@ -654,3 +658,12 @@ func toOpenRtbBidResponse(adformBids []*adformBid, r *openrtb.BidRequest) *adapt return bidResponse } + +func getAdAndType(bid *adformBid) (string, openrtb_ext.BidType) { + if bid.ResponseType == "banner" { + return bid.Banner, openrtb_ext.BidTypeBanner + } else if bid.ResponseType == "vast_content" { + return bid.VastContent, openrtb_ext.BidTypeVideo + } + return "", "" +} diff --git a/adapters/adform/adform_test.go b/adapters/adform/adform_test.go index f227776207d..0bfb95b6369 100644 --- a/adapters/adform/adform_test.go +++ b/adapters/adform/adform_test.go @@ -107,6 +107,16 @@ func createAdformServerResponse(testData aBidInfo) ([]byte, error) { DealId: testData.tags[2].dealId, CreativeId: testData.tags[2].creativeId, }, + { + ResponseType: "vast_content", + VastContent: testData.tags[3].content, + Price: testData.tags[3].price, + Currency: "EUR", + Width: testData.width, + Height: testData.height, + DealId: testData.tags[3].dealId, + CreativeId: testData.tags[3].creativeId, + }, } adformServerResponse, err := json.Marshal(bids) return adformServerResponse, err @@ -123,10 +133,21 @@ func TestAdformBasicResponse(t *testing.T) { if err != nil { t.Fatalf("Should not have gotten adapter error: %v", err) } - if len(bids) != 2 { - t.Fatalf("Received %d bids instead of 2", len(bids)) + if len(bids) != 3 { + t.Fatalf("Received %d bids instead of 3", len(bids)) + } + expectedTypes := []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeVideo, } - for _, bid := range bids { + + for i, bid := range bids { + + if bid.CreativeMediaType != string(expectedTypes[i]) { + t.Errorf("Expected a %s bid. Got: %s", expectedTypes[i], bid.CreativeMediaType) + } + matched := false for _, tag := range adformTestData.tags { if bid.AdUnitCode == tag.code { @@ -224,7 +245,7 @@ func preparePrebidRequest(serverUrl string, t *testing.T) *pbs.PBSRequest { func preparePrebidRequestBody(requestData aBidInfo, t *testing.T) *bytes.Buffer { prebidRequest := pbs.PBSRequest{ - AdUnits: make([]pbs.AdUnit, 3), + AdUnits: make([]pbs.AdUnit, 4), Device: &openrtb.Device{ UA: requestData.deviceUA, IP: requestData.deviceIP, @@ -326,6 +347,7 @@ func createTestData(secure bool) aBidInfo { {mid: 32344, keyValues: "color:red,age:30-40", keyWords: "red,blue", cdims: "300x300,400x200", priceType: "gross", code: "code1", price: 1.23, content: "banner-content1", dealId: "dealId1", creativeId: "creativeId1"}, {mid: 32345, priceType: "net", code: "code2", minp: 23.1, cdims: "300x200"}, // no bid for ad unit {mid: 32346, code: "code3", price: 1.24, content: "banner-content2", dealId: "dealId2", url: "https://adform.com?a=b"}, + {mid: 32347, code: "code4", content: "vast-xml"}, }, secure: secure, currency: "EUR", @@ -379,6 +401,11 @@ func createOpenRtbRequest(testData *aBidInfo) *openrtb.BidRequest { func TestOpenRTBStandardResponse(t *testing.T) { testData := createTestData(true) request := createOpenRtbRequest(&testData) + expectedTypes := []openrtb_ext.BidType{ + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeBanner, + openrtb_ext.BidTypeVideo, + } responseBody, err := createAdformServerResponse(testData) if err != nil { @@ -390,16 +417,17 @@ func TestOpenRTBStandardResponse(t *testing.T) { bidder := new(AdformAdapter) bidResponse, errs := bidder.MakeBids(request, nil, httpResponse) - if len(bidResponse.Bids) != 2 { - t.Fatalf("Expected 2 bids. Got %d", len(bidResponse.Bids)) + if len(bidResponse.Bids) != 3 { + t.Fatalf("Expected 3 bids. Got %d", len(bidResponse.Bids)) } if len(errs) != 0 { t.Errorf("Expected 0 errors. Got %d", len(errs)) } - for _, typeBid := range bidResponse.Bids { - if typeBid.BidType != openrtb_ext.BidTypeBanner { - t.Errorf("Expected a banner bid. Got: %s", bidResponse.Bids[0].BidType) + for i, typeBid := range bidResponse.Bids { + + if typeBid.BidType != expectedTypes[i] { + t.Errorf("Expected a %s bid. Got: %s", expectedTypes[i], typeBid.BidType) } bid := typeBid.Bid matched := false @@ -561,10 +589,10 @@ func assertAdformServerRequest(testData aBidInfo, r *http.Request, isOpenRtb boo var midsWithCurrency = "" var queryString = "" if isOpenRtb { - midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9RVVSJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZSZjZGltcz0zMDB4MzAwLDQwMHgyMDA&bWlkPTMyMzQ1JnJjdXI9RVVSJmNkaW1zPTMwMHgyMDAmbWlucD0yMy4xMA&bWlkPTMyMzQ2JnJjdXI9RVVS" + midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9RVVSJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZSZjZGltcz0zMDB4MzAwLDQwMHgyMDA&bWlkPTMyMzQ1JnJjdXI9RVVSJmNkaW1zPTMwMHgyMDAmbWlucD0yMy4xMA&bWlkPTMyMzQ2JnJjdXI9RVVS&bWlkPTMyMzQ3JnJjdXI9RVVS" queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&eids=eyJ0ZXN0LmNvbSI6eyJvdGhlcl91c2VyX2lkIjpbMF0sInNvbWVfdXNlcl9pZCI6WzFdfSwidGVzdDIub3JnIjp7Im90aGVyX3VzZXJfaWQiOlsyXX19&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&url=https%3A%2F%2Fadform.com%3Fa%3Db&" + midsWithCurrency } else { - midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZSZjZGltcz0zMDB4MzAwLDQwMHgyMDA&bWlkPTMyMzQ1JnJjdXI9VVNEJmNkaW1zPTMwMHgyMDAmbWlucD0yMy4xMA&bWlkPTMyMzQ2JnJjdXI9VVNE" // no way to pass currency in legacy adapter + midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZSZjZGltcz0zMDB4MzAwLDQwMHgyMDA&bWlkPTMyMzQ1JnJjdXI9VVNEJmNkaW1zPTMwMHgyMDAmbWlucD0yMy4xMA&bWlkPTMyMzQ2JnJjdXI9VVNE&bWlkPTMyMzQ3JnJjdXI9VVNE" // no way to pass currency in legacy adapter queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&" + midsWithCurrency } @@ -646,7 +674,7 @@ func TestPriceTypeUrlParameterCreation(t *testing.T) { // Asserts that toOpenRtbBidResponse() creates a *adapters.BidderResponse with // the currency of the last valid []*adformBid element and the expected number of bids func TestToOpenRtbBidResponse(t *testing.T) { - expectedBids := 3 + expectedBids := 4 lastCurrency, anotherCurrency, emptyCurrency := "EUR", "USD", "" request := &openrtb.BidRequest{ @@ -672,6 +700,11 @@ func TestToOpenRtbBidResponse(t *testing.T) { Ext: json.RawMessage(`{"bidder1": { "mid": "32344" }}`), Banner: &openrtb.Banner{}, }, + { + ID: "video-imp-no4", + Ext: json.RawMessage(`{"bidder1": { "mid": "32345" }}`), + Banner: &openrtb.Banner{}, + }, }, Device: &openrtb.Device{UA: "ua", IP: "ip"}, User: &openrtb.User{BuyerUID: "buyerUID"}, @@ -703,6 +736,16 @@ func TestToOpenRtbBidResponse(t *testing.T) { ResponseType: "banner", Banner: "banner-content4", Price: 1.25, + Currency: emptyCurrency, + Width: 300, + Height: 200, + DealId: "dealId4", + CreativeId: "creativeId4", + }, + { + ResponseType: "vast_content", + VastContent: "vast-content", + Price: 1.25, Currency: lastCurrency, Width: 300, Height: 200, diff --git a/adapters/adform/adformtest/exemplary/multiformat-impression.json b/adapters/adform/adformtest/exemplary/multiformat-impression.json new file mode 100644 index 00000000000..efd4aca63e2 --- /dev/null +++ b/adapters/adform/adformtest/exemplary/multiformat-impression.json @@ -0,0 +1,99 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "banner-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "mid": 12345 + } + } + }, + { + "id": "video-imp-id", + "video": { + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "mid": 54321 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adx.adform.net/adx?CC=1&fd=1&gdpr=&gdpr_consent=&ip=&rp=4&stid=&bWlkPTEyMzQ1JnJjdXI9VVNE&bWlkPTU0MzIxJnJjdXI9VVNE" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "response": "banner", + "banner": "", + "win_bid": 0.5, + "win_cur": "USD", + "width": 300, + "height": 250, + "deal_id": null, + "win_crid": "20078830" + }, + { + "response": "vast_content", + "vast_content": "", + "win_bid": 0.7, + "win_cur": "USD", + "width": 640, + "height": 480, + "deal_id": "DID-123-22", + "win_crid": "20078831" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "banner-imp-id", + "impid": "banner-imp-id", + "price": 0.5, + "adm": "", + "crid": "20078830", + "w": 300, + "h": 250 + }, + "type": "banner" + }, + { + "bid": { + "id": "video-imp-id", + "impid": "video-imp-id", + "price": 0.7, + "adm": "", + "crid": "20078831", + "dealid": "DID-123-22", + "w": 640, + "h": 480 + }, + "type": "video" + } + ] + } + ] + } diff --git a/adapters/adform/adformtest/exemplary/single-banner-impression.json b/adapters/adform/adformtest/exemplary/single-banner-impression.json new file mode 100644 index 00000000000..fd7f3cde526 --- /dev/null +++ b/adapters/adform/adformtest/exemplary/single-banner-impression.json @@ -0,0 +1,64 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "mid": 12345 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adx.adform.net/adx?CC=1&fd=1&gdpr=&gdpr_consent=&ip=&rp=4&stid=&bWlkPTEyMzQ1JnJjdXI9VVNE" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "response": "banner", + "banner": "", + "win_bid": 0.5, + "win_cur": "USD", + "width": 300, + "height": 250, + "deal_id": null, + "win_crid": "20078830" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-imp-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "", + "crid": "20078830", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/adform/adformtest/exemplary/single-video-impression.json b/adapters/adform/adformtest/exemplary/single-video-impression.json new file mode 100644 index 00000000000..e22977e6523 --- /dev/null +++ b/adapters/adform/adformtest/exemplary/single-video-impression.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "mid": 54321 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adx.adform.net/adx?CC=1&fd=1&gdpr=&gdpr_consent=&ip=&rp=4&stid=&bWlkPTU0MzIxJnJjdXI9VVNE" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "response": "vast_content", + "vast_content": "", + "win_bid": 0.5, + "win_cur": "USD", + "width": 640, + "height": 480, + "deal_id": null, + "win_crid": "20078830" + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-imp-id", + "impid": "test-imp-id", + "price": 0.5, + "adm": "", + "crid": "20078830", + "w": 640, + "h": 480 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adform/adformtest/params/race/video.json b/adapters/adform/adformtest/params/race/video.json new file mode 100644 index 00000000000..51f8f1b94d2 --- /dev/null +++ b/adapters/adform/adformtest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "mid": "858300" +} diff --git a/static/bidder-info/adform.yaml b/static/bidder-info/adform.yaml index 8aafd9f6815..4dce10b9af8 100644 --- a/static/bidder-info/adform.yaml +++ b/static/bidder-info/adform.yaml @@ -4,6 +4,8 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner + - video From 17f50208439141bc14aa02fd34edc25b98a94375 Mon Sep 17 00:00:00 2001 From: Mansi Nahar Date: Tue, 17 Nov 2020 11:41:50 -0500 Subject: [PATCH 272/381] Fix Beachfront JSON tests (#1578) --- .../exemplary/minimal-banner.json | 43 ++-- .../exemplary/simple-adm-video.json | 100 ++++---- .../beachfronttest/exemplary/simple-mix.json | 136 ++++++----- .../exemplary/simple-nurl-video.json | 130 ++++++----- .../minimal-banner-empty_array-200.json | 2 +- .../supplemental/minimal-mobile-video.json | 112 ++++----- .../supplemental/minimal-site-banner.json | 171 +++++++------- .../supplemental/mobile-banner.json | 28 ++- .../supplemental/multi-banner.json | 24 +- .../supplemental/multi-video.json | 214 ++++++++---------- .../supplemental/unmarshal-error-banner.json | 2 +- ...nmarshal-error-but-another-good-video.json | 2 +- .../supplemental/unmarshal-error-video.json | 2 +- 13 files changed, 462 insertions(+), 504 deletions(-) diff --git a/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json b/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json index 51ce4e9295e..6672e2af91d 100644 --- a/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json +++ b/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json @@ -24,7 +24,6 @@ } ] }, - "httpCalls": [ { "expectedRequest": { @@ -57,38 +56,40 @@ "ua": "", "adapterName": "BF_PREBID_S2S", "adapterVersion": "0.9.0", - "user": { - } + "user": {} } }, "mockResponse": { "status": 200, "body": [ { - "crid":"crid_1", - "price":2.942808, - "w":300, - "h":250, - "slot":"div-gpt-ad-1460505748561-0", - "adm":"
", - "id": "some_test_ad_id_1", - "impid": "some_test_ad_id_1", - "ttl": 300, - "crid": "94395500", - "w": 300, - "price": 2.942808, - "adid": "94395500", - "h": 250 - }, - "type": "banner" - }, + "expectedBidResponses": [ { - "bid": { - "adm": "00:00:15", - "id": "some_test_ad_id_2", - "impid": "some_test_ad_id_2", - "ttl": 300, - "crid": "9999999", - "w": 1020, - "price": 1, - "adid": "9999999", - "h": 1000 + "bids": [{ + "bid": { + "adm": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }, + "type": "banner" }, - "type": "video" + { + "bid": { + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + }, + "type": "video" + }] } ] } diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json index c2b20cf1c5d..f47fd66784e 100644 --- a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json +++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json @@ -167,33 +167,33 @@ } }], - "expectedBids": [{ - "bid": { - "adm": "
", - "id": "some_test_ad_id_1", - "impid": "some_test_ad_id_1", - "ttl": 300, - "crid": "94395500", - "w": 300, - "price": 2.942808, - "adid": "94395500", - "h": 250 - }, - "type": "banner" - }, - { - "bid": { - "adm": "00:00:15", - "id": "some_test_ad_id_2", - "impid": "some_test_ad_id_2", - "ttl": 300, - "crid": "9999999", - "w": 1020, - "price": 1, - "adid": "9999999", - "h": 1000 + "expectedBidResponses": [{ + "bids": [{ + "bid": { + "adm": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }, + "type": "banner" }, - "type": "video" + { + "bid": { + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + }, + "type": "video" + }] } ] } diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json index 8de90f52192..20c62402fc1 100644 --- a/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json +++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json @@ -102,18 +102,19 @@ } }], - "expectedBids": [{ - "bid": { - "adm": "
", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 300, + "w": 250 + }] + }], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 300, + "w": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/amx/amxtest/exemplary/video-simple.json b/adapters/amx/amxtest/exemplary/video-simple.json new file mode 100644 index 00000000000..8fb3baa26d0 --- /dev/null +++ b/adapters/amx/amxtest/exemplary/video-simple.json @@ -0,0 +1,245 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "boxingallowed": 1, + "linearity": 1, + "maxduration": 90, + "minduration": 6, + "mimes": ["video/mp4"], + "placement": 1, + "playbackmethod": [2], + "protocols": [1,2,3,4,5,6,7,8], + "skip": 1, + "skipafter": 5, + "startdelay": 0, + "h": 300, + "pos": 1, + "w": 640 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw", + "adUnitId": "tagid-override" + } + }, + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "unused_publisher_id" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "boxingallowed": 1, + "linearity": 1, + "maxduration": 90, + "minduration": 6, + "mimes": ["video/mp4"], + "placement": 1, + "playbackmethod": [2], + "protocols": [1,2,3,4,5,6,7,8], + "skip": 1, + "skipafter": 5, + "startdelay": 0, + "h": 300, + "pos": 1, + "w": 640 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw", + "adUnitId": "tagid-override" + } + }, + "tagid": "tagid-override", + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "cHJlYmlkLm9yZw" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "WQ5V2DWVTMNXABDD", + "seatbid": [{ + "bid": [{ + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "00:00:15", + "nurl": "https://example.com/nurl", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 600, + "w": 300, + "ext": { + "himp": ["https://example.com/imp-tracker/pixel.gif?param=1¶m2=2"], + "startdelay": 0 + } + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "00:00:15", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "ext": { + "himp": ["https://example.com/imp-tracker/pixel.gif?param=1¶m2=2"], + "startdelay": 0 + }, + "h": 600, + "w": 300 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/amx/amxtest/exemplary/web-simple.json b/adapters/amx/amxtest/exemplary/web-simple.json new file mode 100644 index 00000000000..74854f912ae --- /dev/null +++ b/adapters/amx/amxtest/exemplary/web-simple.json @@ -0,0 +1,246 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "banner": { + "format": [ + { + "h": 600, + "w": 300 + } + ], + "h": 600, + "pos": 1, + "w": 300 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw" + } + }, + "tagid": "example-tag-id", + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "unused_publisher_id" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + }, + { + "source": "adserver.org", + "uids": [ + { + "id": "1234567", + "ext": { + "rtiPartner": "TDID" + } + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "tagid": "example-tag-id", + "banner": { + "format": [ + { + "h": 600, + "w": 300 + } + ], + "h": 600, + "pos": 1, + "w": 300 + }, + "ext": { + "bidder": { + "tagId": "cHJlYmlkLm9yZw" + } + }, + "id": "1", + "secure": 1 + } + ], + "regs": { + "ext": { + "gdpr": 0, + "us_privacy": "1---" + } + }, + "site": { + "domain": "www.example.com", + "ext": { + "amp": 0 + }, + "publisher": { + "id": "cHJlYmlkLm9yZw" + }, + "page": "https://www.example.com/es6/es6_objects.htm", + "ref": "https://www.example.com/es6/es6_objects.htm" + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300, + "user": { + "ext": { + "eids": [ + { + "source": "amxid", + "uids": [ + { + "atype": 1, + "id": "88de601e-3d98-48e7-81d7-00000000" + } + ] + }, + { + "source": "adserver.org", + "uids": [ + { + "id": "1234567", + "ext": { + "rtiPartner": "TDID" + } + } + ] + } + ], + "gdpr": 0, + "us_privacy": "1---" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "WQ5V2DWVTMNXABDD", + "seatbid": [{ + "bid": [{ + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 600, + "w": 300 + }] + }], + "cur": "USD" + } + } + }], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "TEST", + "impid": "1", + "price": 10.0, + "adid": "1", + "adm": "", + "adomain": ["amxrtb.com"], + "iurl": "https://assets.a-mo.net/300x250.v2.png", + "cid": "1", + "crid": "1", + "h": 600, + "w": 300 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/amx/amxtest/params/race/display.json b/adapters/amx/amxtest/params/race/display.json new file mode 100644 index 00000000000..bd101e95a25 --- /dev/null +++ b/adapters/amx/amxtest/params/race/display.json @@ -0,0 +1 @@ +{"tagId":"sample345", "adUnitId": "sampleAdUnitID"} \ No newline at end of file diff --git a/adapters/amx/amxtest/params/race/video.json b/adapters/amx/amxtest/params/race/video.json new file mode 100644 index 00000000000..d2f11bf80b4 --- /dev/null +++ b/adapters/amx/amxtest/params/race/video.json @@ -0,0 +1 @@ +{"tagId": "sample123", "adUnitId": "sampleAdUnitID"} \ No newline at end of file diff --git a/adapters/amx/amxtest/supplemental/204-response.json b/adapters/amx/amxtest/supplemental/204-response.json new file mode 100644 index 00000000000..09571a03569 --- /dev/null +++ b/adapters/amx/amxtest/supplemental/204-response.json @@ -0,0 +1,109 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300 + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "mimes": null, + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300 + } + }, + "mockResponse": { + "status": 204, + "headers": { + "X-Nbr": [ + "3b" + ] + }, + "body": {} + } + }], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/amx/amxtest/supplemental/400-response.json b/adapters/amx/amxtest/supplemental/400-response.json new file mode 100644 index 00000000000..f10cea89718 --- /dev/null +++ b/adapters/amx/amxtest/supplemental/400-response.json @@ -0,0 +1,114 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300 + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "mimes": null, + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300 + } + }, + "mockResponse": { + "status": 400, + "headers": { + "X-Nbr": [ + "3b" + ] + }, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Invalid Request: 400. Error Code: 3b", + "comparison": "literal" + } + ] +} diff --git a/adapters/amx/amxtest/supplemental/500-response.json b/adapters/amx/amxtest/supplemental/500-response.json new file mode 100644 index 00000000000..fe5d89930c8 --- /dev/null +++ b/adapters/amx/amxtest/supplemental/500-response.json @@ -0,0 +1,114 @@ +{ + "mockBidRequest": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "test": 0, + "tmax": 300 + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "http://pbs-dev.amxrtb.com/auction/openrtb?v=pbs1.0", + "body": { + "device": { + "dnt": 0, + "h": 1120, + "ip": "98.249.0.0", + "language": "en", + "os": "macos", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", + "w": 1792 + }, + "id": "TL3JS6F43CKNDQFY", + "imp": [ + { + "video": { + "api": [1,2], + "mimes": null, + "h": 300, + "pos": 1, + "w": 640 + }, + "id": "1", + "secure": 1 + } + ], + "site": { + "ext": { + "amp": 0 + } + }, + "source": { + "ext": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "amxrtb.com", + "hp": 1, + "sid": "1234" + } + ], + "ver": "1.0" + } + } + }, + "tmax": 300 + } + }, + "mockResponse": { + "status": 500, + "headers": { + "X-Nbr": [ + "7a" + ] + }, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected response: 500. Error Code: 7a", + "comparison": "literal" + } + ] +} diff --git a/adapters/amx/params_test.go b/adapters/amx/params_test.go new file mode 100644 index 00000000000..ef177644b21 --- /dev/null +++ b/adapters/amx/params_test.go @@ -0,0 +1,47 @@ +package amx + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +var validBidParams = []string{ + `{"tagId":"sampleTagId", "adUnitId": "sampleAdUnitId"}`, + `{"tagId":"sampleTagId", "adUnitId": ""}`, + `{"adUnitId": ""}`, + `{"adUnitId": "sampleAdUnitId"}`, + `{"tagId":"sampleTagId"}`, + `{"tagId":""}`, + `{}`, + `{"otherValue": "ignored"}`, + `{"tagId": "sampleTagId", "otherValue": "ignored"}`, + `{"otherValue": "ignored", "adUnitId": "sampleAdUnitId"}`, +} + +var invalidBidParams = []string{ + `{"tagId":1234}`, + `{"tagId": true}`, + `{"adUnitId": true}`, + `{"adUnitId": null}`, + `{"adUnitId": null, "tagId": "sampleTagId"}`, + `{"adUnitId": 1234, "tagId": "sampleTagId"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + assert.Nil(t, err) + for _, params := range validBidParams { + assert.Nil(t, validator.Validate(openrtb_ext.BidderAMX, json.RawMessage(params))) + } +} + +func TestInValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + assert.Nil(t, err) + for _, params := range invalidBidParams { + assert.NotNil(t, validator.Validate(openrtb_ext.BidderAMX, json.RawMessage(params))) + } +} diff --git a/adapters/amx/usersync.go b/adapters/amx/usersync.go new file mode 100644 index 00000000000..d9ff10df562 --- /dev/null +++ b/adapters/amx/usersync.go @@ -0,0 +1,13 @@ +package amx + +import ( + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" +) + +// NewAMXSyncer produces an AMX RTB usersyncer +func NewAMXSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("amx", 737, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/amx/usersync_test.go b/adapters/amx/usersync_test.go new file mode 100644 index 00000000000..e6020b27570 --- /dev/null +++ b/adapters/amx/usersync_test.go @@ -0,0 +1,23 @@ +package amx + +import ( + "testing" + "text/template" + + "github.com/PubMatic-OpenWrap/prebid-server/privacy" + "github.com/stretchr/testify/assert" +) + +func TestAMXSyncer(t *testing.T) { + syncURL := "http://pbs.amxrtb.com/cchain/0?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&cb=localhost%2Fsetuid%3Fbidder%3Damx%26uid%3D" + syncURLTemplate := template.Must(template.New("sync-template").Parse(syncURL)) + + syncer := NewAMXSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{}) + + assert.NoError(t, err) + assert.Equal(t, "http://pbs.amxrtb.com/cchain/0?gdpr=&gdpr_consent=&cb=localhost%2Fsetuid%3Fbidder%3Damx%26uid%3D", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 737, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index 1b3b42295d7..145c830dbb6 100644 --- a/adapters/appnexus/appnexus.go +++ b/adapters/appnexus/appnexus.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math/rand" "net/http" "strconv" "strings" @@ -95,10 +96,11 @@ type appnexusBidExt struct { } type appnexusReqExtAppnexus struct { - IncludeBrandCategory *bool `json:"include_brand_category,omitempty"` - BrandCategoryUniqueness *bool `json:"brand_category_uniqueness,omitempty"` - IsAMP int `json:"is_amp,omitempty"` - HeaderBiddingSource int `json:"hb_source,omitempty"` + IncludeBrandCategory *bool `json:"include_brand_category,omitempty"` + BrandCategoryUniqueness *bool `json:"brand_category_uniqueness,omitempty"` + IsAMP int `json:"is_amp,omitempty"` + HeaderBiddingSource int `json:"hb_source,omitempty"` + AdPodId string `json:"adpod_id,omitempty"` } // Full request extension including appnexus extension object @@ -354,14 +356,56 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada } reqExt.Appnexus.IsAMP = isAMP reqExt.Appnexus.HeaderBiddingSource = a.hbSource + isVIDEO + + imps := request.Imp + + // For long form requests adpod_id must be sent downstream. + // Adpod id is a unique identifier for pod + // All impressions in the same pod must have the same pod id in request extension + // For this all impressions in request should belong to the same pod + // If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same + if isVIDEO == 1 { + podImps := groupByPods(imps) + + requests := make([]*adapters.RequestData, 0, len(podImps)) + for _, podImps := range podImps { + reqExt.Appnexus.AdPodId = generatePodId() + + reqs, errors := splitRequests(podImps, request, reqExt, thisURI, errs) + requests = append(requests, reqs...) + errs = append(errs, errors...) + } + return requests, errs + } + + return splitRequests(imps, request, reqExt, thisURI, errs) +} + +func generatePodId() string { + val := rand.Int63() + return fmt.Sprint(val) +} + +func groupByPods(imps []openrtb.Imp) map[string]([]openrtb.Imp) { + // find number of pods in response + podImps := make(map[string][]openrtb.Imp) + for _, imp := range imps { + pod := strings.Split(imp.ID, "_")[0] + podImps[pod] = append(podImps[pod], imp) + } + return podImps +} + +func marshalAndSetRequestExt(request *openrtb.BidRequest, requestExtension appnexusReqExt, errs []error) { var err error - request.Ext, err = json.Marshal(reqExt) + request.Ext, err = json.Marshal(requestExtension) if err != nil { errs = append(errs, err) - return nil, errs } +} + +func splitRequests(imps []openrtb.Imp, request *openrtb.BidRequest, requestExtension appnexusReqExt, uri string, errs []error) ([]*adapters.RequestData, []error) { - imps := request.Imp // Initial capacity for future array of requests, memory optimization. // Let's say there are 35 impressions and limit impressions per request equals to 10. // In this case we need to create 4 requests with 10, 10, 10 and 5 impressions. @@ -375,6 +419,8 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada headers.Add("Content-Type", "application/json;charset=utf-8") headers.Add("Accept", "application/json") + marshalAndSetRequestExt(request, requestExtension, errs) + for impsLeft { endInd := startInd + maxImpsPerReq @@ -393,7 +439,7 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *ada resArr = append(resArr, &adapters.RequestData{ Method: "POST", - Uri: thisURI, + Uri: uri, Body: reqJSON, Headers: headers, }) diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go index c6f537996b9..7468250b28d 100644 --- a/adapters/appnexus/appnexus_test.go +++ b/adapters/appnexus/appnexus_test.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "encoding/json" + "github.com/stretchr/testify/assert" "io/ioutil" "net/http" "net/http/httptest" + "regexp" "testing" "time" @@ -38,6 +40,233 @@ func TestMemberQueryParam(t *testing.T) { } } +func TestVideoSinglePod(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + result, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, result, 1, "Only one request should be returned") + + var error error + var reqData *openrtb.BidRequest + error = json.Unmarshal(result[0].Body, &reqData) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt *appnexusReqExt + error = json.Unmarshal(reqData.Ext, &reqDataExt) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + regMatch, matchErr := regexp.Match(`[0-9]19`, []byte(reqDataExt.Appnexus.AdPodId)) + assert.NoError(t, matchErr, "Regex match error should be nil") + assert.True(t, regMatch, "AdPod id doesn't present in Appnexus extension or has incorrect format") +} + +func TestVideoSinglePodManyImps(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_3", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_4", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_5", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_6", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_7", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_8", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_9", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_10", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_11", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_12", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_13", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_14", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 2, "Two requests should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId2 := reqDataExt2.Appnexus.AdPodId + + assert.Equal(t, adPodId1, adPodId2, "AdPod id is not the same for the same pod") +} + +func TestVideoTwoPods(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_2", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 2, "Two request should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId2 := reqDataExt2.Appnexus.AdPodId + + assert.NotEqual(t, adPodId1, adPodId2, "AdPod id should be different for different pods") +} + +func TestVideoTwoPodsManyImps(t *testing.T) { + var a AppNexusAdapter + a.URI = "http://test.com/openrtb2" + a.hbSource = 5 + + var reqInfo adapters.ExtraRequestInfo + reqInfo.PbsEntryPoint = "video" + + var req openrtb.BidRequest + req.ID = "test_id" + + reqExt := `{"prebid":{}}` + impExt := `{"bidder":{"placementId":123}}` + req.Ext = []byte(reqExt) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "1_2", Ext: []byte(impExt)}) + + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_0", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_1", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_2", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_3", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_4", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_5", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_6", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_7", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_8", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_9", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_10", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_11", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_12", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_13", Ext: []byte(impExt)}) + req.Imp = append(req.Imp, openrtb.Imp{ID: "2_14", Ext: []byte(impExt)}) + + res, err := a.MakeRequests(&req, &reqInfo) + + assert.Empty(t, err, "Errors array should be empty") + assert.Len(t, res, 3, "Three requests should be returned") + + var error error + var reqData1 *openrtb.BidRequest + error = json.Unmarshal(res[0].Body, &reqData1) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt1 *appnexusReqExt + error = json.Unmarshal(reqData1.Ext, &reqDataExt1) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + var reqData2 *openrtb.BidRequest + error = json.Unmarshal(res[1].Body, &reqData2) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt2 *appnexusReqExt + error = json.Unmarshal(reqData2.Ext, &reqDataExt2) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + var reqData3 *openrtb.BidRequest + error = json.Unmarshal(res[2].Body, &reqData3) + assert.NoError(t, error, "Response body unmarshalling error should be nil") + + var reqDataExt3 *appnexusReqExt + error = json.Unmarshal(reqData3.Ext, &reqDataExt3) + assert.NoError(t, error, "Response ext unmarshalling error should be nil") + + adPodId1 := reqDataExt1.Appnexus.AdPodId + adPodId2 := reqDataExt2.Appnexus.AdPodId + adPodId3 := reqDataExt3.Appnexus.AdPodId + + podIds := make(map[string]int) + podIds[adPodId1] = podIds[adPodId1] + 1 + podIds[adPodId2] = podIds[adPodId2] + 1 + podIds[adPodId3] = podIds[adPodId3] + 1 + + assert.Len(t, podIds, 2, "Incorrect number of unique pod ids") +} + // ---------------------------------------------------------------------------- // Code below this line tests the legacy, non-openrtb code flow. It can be deleted after we // clean up the existing code and make everything openrtb. diff --git a/adapters/appnexus/appnexusplatformtest/video/simple-video.json b/adapters/appnexus/appnexusplatformtest/video/simple-video.json deleted file mode 100644 index 7ee192be2c1..00000000000 --- a/adapters/appnexus/appnexusplatformtest/video/simple-video.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "minduration": 15, - "maxduration": 30, - "protocols": [2, 3, 5, 6, 7, 8], - "w": 940, - "h": 560 - }, - "ext": { - "bidder": { - "placement_id": 1 - } - } - } - ] - }, - - "httpCalls": [ - { - "expectedRequest": { - "uri": "http://ib.adnxs.com/openrtb2", - "body": { - "id": "test-request-id", - "ext": { - "appnexus": { - "hb_source": 9 - }, - "prebid": {} - }, - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": ["video/mp4"], - "minduration": 15, - "maxduration": 30, - "protocols": [2, 3, 5, 6, 7, 8], - "w": 940, - "h": 560 - }, - "ext": { - "appnexus": { - "placement_id": 1 - } - } - } - ] - } - }, - "mockResponse": { - "status": 200, - "body": { - "id": "test-request-id", - "seatbid": [ - { - "seat": "958", - "bid": [{ - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.500000, - "adid": "29681110", - "adm": "some-test-ad", - "adomain": ["appnexus.com"], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "h": 250, - "w": 300, - "cat": ["IAB9-1"], - "ext": { - "appnexus": { - "brand_id": 9, - "brand_category_id": 9, - "auction_id": 8189378542222915032, - "bid_ad_type": 1, - "bidder_id": 2, - "ranking_price": 0.000000, - "deal_priority": 5 - } - } - }] - } - ], - "bidid": "5778926625248726496", - "cur": "USD" - } - } - } - ], - - "expectedBidResponses": [ - { - "currency": "USD", - "bids": [ - { - "bid": { - "id": "7706636740145184841", - "impid": "test-imp-id", - "price": 0.5, - "adm": "some-test-ad", - "adid": "29681110", - "adomain": ["appnexus.com"], - "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", - "cid": "958", - "crid": "29681110", - "w": 300, - "h": 250, - "cat": ["IAB5-3"], - "ext": { - "appnexus": { - "brand_id": 9, - "brand_category_id": 9, - "auction_id": 8189378542222915032, - "bid_ad_type": 1, - "bidder_id": 2, - "ranking_price": 0.000000, - "deal_priority": 5 - } - } - }, - "type": "video" - } - ] - } - ] - } \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json new file mode 100644 index 00000000000..3ac62d90cd4 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json @@ -0,0 +1,116 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisherid": "123", + "placementid": "456" + } + } + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid", + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "987", + "impid": "test-imp-id", + "price": 1, + "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", + "adid": "987", + "crid": "987", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }, + "type": "banner" + }] + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json deleted file mode 100644 index f5f92515e26..00000000000 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-req-id", - "imp": [ - { - "id": "test-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ], - "w": 300, - "h": 250 - }, - "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", - "banner": { - "w": -1, - "h": 250 - }, - "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": 200, - "body": { - "id": "test-imp-id", - "seatbid": [ - { - "bid": [ - { - "id": "987", - "impid": "test-imp-id", - "price": 1.000000, - "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", - "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" - } - ] - } - ], - "bidid": "654", - "cur": "USD" - } - } - } - ], - "expectedBidResponses": [ - { - "currency": "USD", - "bids": [ - { - "bid": { - "id": "987", - "impid": "test-imp-id", - "price": 1, - "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", - "adid": "987", - "crid": "987", - "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", - "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" - }, - "type": "banner" - } - ] - } - ] -} diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json index bad228d5f18..573032c81e1 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json @@ -23,9 +23,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -64,15 +64,9 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json index 9090d80d099..08639bee013 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json @@ -16,9 +16,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -56,15 +56,9 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json index 22c62f8b821..35bdf9a443e 100644 --- a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json +++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json @@ -21,9 +21,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -66,15 +66,9 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json index 3edd6569258..450e0d9e45b 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json @@ -24,9 +24,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -64,15 +64,9 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json new file mode 100644 index 00000000000..c33807bda74 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisherid": "123", + "placementid": "456" + } + } + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid", + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "adm": "malformed", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [{ + "value": "invalid character 'm' looking for beginning of value", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json index fa9fd9132b8..b229d41a27a 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-banner-height.json @@ -22,9 +22,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json new file mode 100644 index 00000000000..68ca8044812 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json @@ -0,0 +1,40 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 15, + "maxduration": 30, + "protocols": [2, 3, 5, 6, 7, 8], + "linearity": 1, + "w": 940, + "h": 560 + }, + "instl": 1, + "ext": { + "bidder": { + "publisherid": "123", + "placementid": "456" + } + } + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "device": { + "ip": "152.193.6.74" + }, + "user": { + "id": "db089de9-a62e-4861-a881-0ff15e052516", + "buyeruid": "v4_bidder_token" + }, + "tmax": 500 + }, + "expectedMakeRequestsErrors": [{ + "value": "imp #test-imp-id: interstitial imps are only supported for banner", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json new file mode 100644 index 00000000000..50212155752 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json @@ -0,0 +1,107 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisherid": "123", + "placementid": "456" + } + } + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid", + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "adm": "{\"type\":\"ID\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}", + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [] + }], + "expectedMakeBidsErrors": [{ + "value": "bid 987 missing 'bid_id' in 'adm'", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json new file mode 100644 index 00000000000..832b16dca22 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "publisherid": "123", + "placementid": "456" + } + } + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "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", + "banner": { + "w": -1, + "h": 250 + }, + "tagid": "123_456" + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid", + "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": 200, + "body": { + "id": "test-imp-id", + "seatbid": [{ + "bid": [{ + "id": "987", + "impid": "test-imp-id", + "price": 1.000000, + "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0", + "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}" + }] + }], + "bidid": "654", + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [] + }], + "expectedMakeBidsErrors": [{ + "value": "Bid 987 missing 'adm'", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json index 016e8de0ef0..0793f990049 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-banner-height.json @@ -20,9 +20,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json index 16e8aede10c..682c33e46b8 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json @@ -41,9 +41,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -81,15 +81,9 @@ "tagid": "pub1_plmt1" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "pub1" } @@ -158,15 +152,9 @@ "tagid": "pub2_plmt2" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "pub2" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json index bb192aad76f..642e495810a 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json @@ -16,9 +16,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -56,15 +56,9 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json new file mode 100644 index 00000000000..fccdf71ca4a --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json @@ -0,0 +1,22 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "device": { + "ip": "152.193.6.74" + }, + "user": { + "id": "db089de9-a62e-4861-a881-0ff15e052516", + "buyeruid": "v4_bidder_token" + }, + "tmax": 500 + }, + "expectedMakeRequestsErrors": [{ + "value": "No impressions provided", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json index 964dcb48b48..72b4fbacdd1 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-buyeruid.json @@ -26,9 +26,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json index a9c3c23d298..f13b70e1be2 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-placementId.json @@ -25,9 +25,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json index c50f3d36378..a80a1e09b65 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/required-param-publisherId.json @@ -25,9 +25,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json new file mode 100644 index 00000000000..f0a11905cf8 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json @@ -0,0 +1,87 @@ +{ + "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" + } + } + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid" + }, + "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" + }], + "app": { + "id": "app-abc", + "bundle": "com.prebid", + "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": { + "headers": { + "X-Fb-An-Errors": [ + "someError" + ]}, + "status": 500 + } + }], + "expectedMakeBidsErrors": [{ + "value": "Unexpected status code 500 with error message 'someError'", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/site-not-supported.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/site-not-supported.json new file mode 100644 index 00000000000..9155352a192 --- /dev/null +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/site-not-supported.json @@ -0,0 +1,38 @@ +{ + "mockBidRequest": { + "id": "test-req-id", + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "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 + }, + "expectedMakeRequestsErrors": [{ + "value": "Site impressions are not supported.", + "comparison": "literal" + }] +} \ No newline at end of file diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json index 4c561c55276..45c34192ea2 100644 --- a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json +++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json @@ -21,9 +21,9 @@ } } ], - "site": { - "domain": "prebid.org", - "page": "prebid.org" + "app": { + "id": "app-abc", + "bundle": "com.prebid" }, "device": { "ip": "152.193.6.74" @@ -50,15 +50,9 @@ "tagid": "123_456" } ], - "ext": { - "appnexus": { - "hb_source": 5 - }, - "prebid": {} - }, - "site": { - "domain": "prebid.org", - "page": "prebid.org", + "app": { + "id": "app-abc", + "bundle": "com.prebid", "publisher": { "id": "123" } diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go index 3bc072a8385..0759a09d80b 100644 --- a/adapters/audienceNetwork/facebook.go +++ b/adapters/audienceNetwork/facebook.go @@ -10,16 +10,17 @@ import ( "net/http" "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/PubMatic-OpenWrap/prebid-server/util/maputil" + + "github.com/PubMatic-OpenWrap/openrtb" "github.com/buger/jsonparser" "github.com/golang/glog" ) type FacebookAdapter struct { - http *adapters.HTTPAdapter URI string nonSecureUri string platformID string @@ -35,15 +36,6 @@ var supportedBannerHeights = map[uint64]bool{ 250: true, } -// used for cookies and such -func (a *FacebookAdapter) Name() string { - return "audienceNetwork" -} - -func (a *FacebookAdapter) SkipNoCookies() bool { - return false -} - type facebookReqExt struct { PlatformID string `json:"platformid"` AuthID string `json:"authentication_id"` @@ -62,6 +54,12 @@ func (this *FacebookAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo * }} } + if request.Site != nil { + return nil, []error{&errortypes.BadInput{ + Message: "Site impressions are not supported.", + }} + } + return this.buildRequests(request) } @@ -151,10 +149,6 @@ func (this *FacebookAdapter) modifyRequest(out *openrtb.BidRequest) error { app := *out.App app.Publisher = &openrtb.Publisher{ID: pubId} out.App = &app - } else { - site := *out.Site - site.Publisher = &openrtb.Publisher{ID: pubId} - out.Site = &site } if err = this.modifyImp(imp); err != nil { @@ -178,8 +172,10 @@ func (this *FacebookAdapter) modifyImp(out *openrtb.Imp) error { } } - switch impType { - case openrtb_ext.BidTypeBanner: + if impType == openrtb_ext.BidTypeBanner { + bannerCopy := *out.Banner + out.Banner = &bannerCopy + if out.Instl == 1 { out.Banner.W = openrtb.Uint64Ptr(0) out.Banner.H = openrtb.Uint64Ptr(0) @@ -212,7 +208,6 @@ func (this *FacebookAdapter) modifyImp(out *openrtb.Imp) error { /* This will get overwritten post-serialization */ out.Banner.W = openrtb.Uint64Ptr(0) out.Banner.Format = nil - break } return nil @@ -239,102 +234,106 @@ func (this *FacebookAdapter) extractPlacementAndPublisher(out *openrtb.Imp) (str } } - placementId := fbExt.PlacementId - publisherId := fbExt.PublisherId + placementID := fbExt.PlacementId + publisherID := fbExt.PublisherId // Support the legacy path with the caller was expected to pass in just placementId // which was an underscore concantenated string with the publisherId and placementId. // The new path for callers is to pass in the placementId and publisherId independently // and the below code will prefix the placementId that we pass to FAN with the publsiherId // so that we can abstract the implementation details from the caller - toks := strings.Split(placementId, "_") + toks := strings.Split(placementID, "_") if len(toks) == 1 { - if publisherId == "" { + if publisherID == "" { return "", "", &errortypes.BadInput{ Message: "Missing publisherId param", } } - return placementId, publisherId, nil + return placementID, publisherID, nil } else if len(toks) == 2 { - publisherId = toks[0] - placementId = toks[1] + publisherID = toks[0] + placementID = toks[1] } else { return "", "", &errortypes.BadInput{ - Message: fmt.Sprintf("Invalid placementId param '%s' and publisherId param '%s'", placementId, publisherId), + Message: fmt.Sprintf("Invalid placementId param '%s' and publisherId param '%s'", placementID, publisherID), } } - return placementId, publisherId, nil + return placementID, publisherID, nil } // XXX: This entire function is just a hack to get around mxmCherry 11.0.0 limitations, without // having to fork the library and maintain our own branch -func modifyImpCustom(json []byte, imp *openrtb.Imp) ([]byte, error) { +func modifyImpCustom(jsonData []byte, imp *openrtb.Imp) ([]byte, error) { impType, ok := resolveImpType(imp) if ok == false { panic("processing an invalid impression") } - var err error + var jsonMap map[string]interface{} + err := json.Unmarshal(jsonData, &jsonMap) + if err != nil { + return jsonData, err + } + + var impMap map[string]interface{} + if impSlice, ok := maputil.ReadEmbeddedSlice(jsonMap, "imp"); !ok { + return jsonData, errors.New("unable to find imp in json data") + } else if len(impSlice) == 0 { + return jsonData, errors.New("unable to find imp[0] in json data") + } else if impMap, ok = impSlice[0].(map[string]interface{}); !ok { + return jsonData, errors.New("unexpected type for imp[0] found in json data") + } switch impType { case openrtb_ext.BidTypeBanner: - // The current version of mxmCherry (11.0.0) repesents banner.w as unsigned - // integers, so setting a value of -1 is not possible which is why we have to do it + // The current version of mxmCherry (11.0.0) represents banner.w as an unsigned + // integer, so setting a value of -1 is not possible which is why we have to do it // post-serialization - - // The above does not apply to interstitial impressions - if imp.Instl == 1 { - break - } - - json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "banner", "w") - if err != nil { - return json, err + isInterstitial := imp.Instl == 1 + if !isInterstitial { + if bannerMap, ok := maputil.ReadEmbeddedMap(impMap, "banner"); ok { + bannerMap["w"] = json.RawMessage("-1") + } else { + return jsonData, errors.New("unable to find imp[0].banner in json data") + } } - break - case openrtb_ext.BidTypeVideo: // mxmCherry omits video.w/h if set to zero, so we need to force set those // fields to zero post-serialization for the time being - json, err = jsonparser.Set(json, []byte("0"), "imp", "[0]", "video", "w") - if err != nil { - return json, err + if videoMap, ok := maputil.ReadEmbeddedMap(impMap, "video"); ok { + videoMap["w"] = json.RawMessage("0") + videoMap["h"] = json.RawMessage("0") + } else { + return jsonData, errors.New("unable to find imp[0].video in json data") } - json, err = jsonparser.Set(json, []byte("0"), "imp", "[0]", "video", "h") - if err != nil { - return json, err + case openrtb_ext.BidTypeNative: + nativeMap, ok := maputil.ReadEmbeddedMap(impMap, "native") + if !ok { + return jsonData, errors.New("unable to find imp[0].video in json data") } - break - - case openrtb_ext.BidTypeNative: // Set w/h to -1 for native impressions based on the facebook native spec. // We have to set this post-serialization since the OpenRTB protocol doesn't - // actaully support w/h in the native object - json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "native", "w") - if err != nil { - return json, err - } - - json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "native", "h") - if err != nil { - return json, err - } + // actually support w/h in the native object + nativeMap["w"] = json.RawMessage("-1") + nativeMap["h"] = json.RawMessage("-1") // The FAN adserver does not expect the native request payload, all that information // is derived server side based on the placement ID. We need to remove these pieces of // information manually since OpenRTB (and thus mxmCherry) never omit native.request - json = jsonparser.Delete(json, "imp", "[0]", "native", "ver") - json = jsonparser.Delete(json, "imp", "[0]", "native", "request") - - break + delete(nativeMap, "ver") + delete(nativeMap, "request") } - return json, nil + if jsonReEncoded, err := json.Marshal(jsonMap); err == nil { + return jsonReEncoded, nil + } else { + return nil, fmt.Errorf("unable to encode json data (%v)", err) + } } func (this *FacebookAdapter) MakeBids(request *openrtb.BidRequest, adapterRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { @@ -430,7 +429,7 @@ func resolveImpType(imp *openrtb.Imp) (openrtb_ext.BidType, bool) { return openrtb_ext.BidTypeBanner, false } -func NewFacebookBidder(client *http.Client, platformID string, appSecret string) adapters.Bidder { +func NewFacebookBidder(platformID string, appSecret string) adapters.Bidder { if platformID == "" { glog.Errorf("No facebook partnerID specified. Calls to the Audience Network will fail. Did you set adapters.facebook.platform_id in the app config?") return &adapters.MisconfiguredBidder{ @@ -447,11 +446,8 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string) } } - a := &adapters.HTTPAdapter{Client: client} - return &FacebookAdapter{ - http: a, - URI: "https://an.facebook.com/placementbid.ortb", + URI: "https://an.facebook.com/placementbid.ortb", //for AB test nonSecureUri: "http://an.facebook.com/placementbid.ortb", platformID: platformID, @@ -474,15 +470,11 @@ func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (* 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 + // The publisher ID is expected in the app object 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"), - } + return &adapters.RequestData{}, []error{ + errors.New("path app.publisher.id not found in the request"), } } diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go index b4744dce211..8ff05118a35 100644 --- a/adapters/audienceNetwork/facebook_test.go +++ b/adapters/audienceNetwork/facebook_test.go @@ -1,6 +1,7 @@ package audienceNetwork import ( + "errors" "testing" "time" @@ -40,14 +41,14 @@ type FacebookExt struct { } func TestJsonSamples(t *testing.T) { - adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret")) + adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder("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") + fba := NewFacebookBidder("test-platform-id", "test-app-secret") tb, ok := fba.(adapters.TimeoutBidder) if !ok { @@ -60,11 +61,11 @@ func TestMakeTimeoutNoticeApp(t *testing.T) { assert.Equal(t, expectedUri, toReq.Uri, "Facebook timeout notification not returning the expected URI.") } -func TestMakeTimeoutNoticeSite(t *testing.T) { +func TestMakeTimeoutNoticeBadRequest(t *testing.T) { req := adapters.RequestData{ - Body: []byte(`{"id":"1234","imp":[{"id":"1234"}],"site":{"publisher":{"id":"5678"}}}`), + Body: []byte(`{"imp":[{{"id":"1234"}}`), } - fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") + fba := NewFacebookBidder("test-platform-id", "test-app-secret") tb, ok := fba.(adapters.TimeoutBidder) if !ok { @@ -72,24 +73,29 @@ func TestMakeTimeoutNoticeSite(t *testing.T) { } 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.") + assert.Empty(t, toReq.Uri, "Facebook MakeTimeoutNotification() did not return nil", err) + assert.NotNil(t, err, "Facebook MakeTimeoutNotification() did not return an error") + } -func TestMakeTimeoutNoticeBadRequest(t *testing.T) { - req := adapters.RequestData{ - Body: []byte(`{"imp":[{{"id":"1234"}}`), - } - fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret") +func TestNewFacebookBidderMissingPlatformID(t *testing.T) { + result := NewFacebookBidder("", "anyAppSecret") - tb, ok := fba.(adapters.TimeoutBidder) - if !ok { - t.Error("Facebook adapter is not a TimeoutAdapter") + expected := &adapters.MisconfiguredBidder{ + Name: "audienceNetwork", + Error: errors.New("Audience Network is not configured properly on this Prebid Server deploy. If you believe this should work, contact the company hosting the service and tell them to check their configuration."), } - 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") + assert.Equal(t, expected, result) +} + +func TestNewFacebookBidderMissingAppSecret(t *testing.T) { + result := NewFacebookBidder("anyPlatformID", "") + + expected := &adapters.MisconfiguredBidder{ + Name: "audienceNetwork", + Error: errors.New("Audience Network is not configured properly on this Prebid Server deploy. If you believe this should work, contact the company hosting the service and tell them to check their configuration."), + } + assert.Equal(t, expected, result) } diff --git a/adapters/avocet/usersync_test.go b/adapters/avocet/usersync_test.go index be4890df91a..12b7901cc90 100644 --- a/adapters/avocet/usersync_test.go +++ b/adapters/avocet/usersync_test.go @@ -23,7 +23,7 @@ func TestAvocetSyncer(t *testing.T) { Consent: "ConsentString", }, CCPA: ccpa.Policy{ - Value: "PrivacyString", + Consent: "PrivacyString", }, }) diff --git a/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json b/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json index 51ce4e9295e..6672e2af91d 100644 --- a/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json +++ b/adapters/beachfront/beachfronttest/exemplary/minimal-banner.json @@ -24,7 +24,6 @@ } ] }, - "httpCalls": [ { "expectedRequest": { @@ -57,38 +56,40 @@ "ua": "", "adapterName": "BF_PREBID_S2S", "adapterVersion": "0.9.0", - "user": { - } + "user": {} } }, "mockResponse": { "status": 200, "body": [ { - "crid":"crid_1", - "price":2.942808, - "w":300, - "h":250, - "slot":"div-gpt-ad-1460505748561-0", - "adm":"
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }] + }, + { + "seat": "45678", + "bid": [{ + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + } + ] + }], + "cur": "USD" + } + } + }], + + "expectedBids": [ + { + "bid": { + "adm": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }, + "type": "banner" + }, + { + "bid": { + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + }, + "type": "video" + } + ] +} + \ No newline at end of file diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json new file mode 100644 index 00000000000..c2b20cf1c5d --- /dev/null +++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json @@ -0,0 +1,200 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [{ + "id": "some_test_ad_id_1", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + } + }, + { + "id": "some_test_ad_id_2", + "video":{ + "mimes": [ + "video/mp4", + "application/javascript" + ], + "protocols":[ + 2, + 3, + 5, + 6 + ], + "w":640, + "h":480 + }, + "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://hb.emxdgt.com?t=1000&ts=2060541160", + "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_1", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + }, + "tagid": "25251", + "secure": 0 + }, + { + "id": "some_test_ad_id_2", + "video":{ + "mimes": [ + "video/mp4", + "application/javascript" + ], + "protocols":[ + 2, + 3, + 5, + 6 + ], + "w":640, + "h":480 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + }, + "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": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }] + }, + { + "seat": "45678", + "bid": [{ + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + } + ] + }], + "cur": "USD" + } + } + }], + + "expectedBids": [{ + "bid": { + "adm": "
", + "id": "some_test_ad_id_1", + "impid": "some_test_ad_id_1", + "ttl": 300, + "crid": "94395500", + "w": 300, + "price": 2.942808, + "adid": "94395500", + "h": 250 + }, + "type": "banner" + }, + { + "bid": { + "adm": "00:00:15", + "id": "some_test_ad_id_2", + "impid": "some_test_ad_id_2", + "ttl": 300, + "crid": "9999999", + "w": 1020, + "price": 1, + "adid": "9999999", + "h": 1000 + }, + "type": "video" + } + ] +} + \ No newline at end of file diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json new file mode 100644 index 00000000000..8de90f52192 --- /dev/null +++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json @@ -0,0 +1,119 @@ +{ + "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 + }, + "app": { + "domain": "www.publisher.com", + "storeurl": "http://www.publisher.com/awesome/site?with=some¶meters=here" + } + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "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 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + }, + "tagid": "25251", + "secure": 0 + }], + "app": { + "domain": "www.publisher.com", + "storeurl": "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": "
", - "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 - } -} -``` - -### OpenRTB Extensions - -#### Conventions - -OpenRTB 2.5 permits exchanges to define their own extensions to any object from the spec. -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`. -The only exception here is the top-level `BidResponse`, because it's bidder-independent. - -`ext.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders. -`ext.prebid` extensions are defined by Prebid Server. - -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. - -#### Bid Adjustments - -Bidders [are encouraged](../../developers/add-new-bidder.md) to make Net bids. However, there's no way for Prebid to enforce this. -If you find that some bidders use Gross bids, publishers can adjust for it with `request.ext.prebid.bidadjustmentfactors`: - -``` -{ - "ext": { - "prebid": { - "bidadjustmentfactors": { - "appnexus": 0.8, - "rubicon": 0.7 - } - } - } -} -``` - -This may also be useful for publishers who want to account for different discrepancies with different bidders. - -#### Targeting - -Targeting refers to strings which are sent to the adserver to -[make header bidding possible](http://prebid.org/overview/intro.html#how-does-prebid-work). - -`request.ext.prebid.targeting` is an optional property which causes Prebid Server -to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.targeting`. - -**Request format** (optional param `request.ext.prebid.targeting`) - -``` -{ - "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. - -For backwards compatibility the following strings will also be allowed as price granularity definitions. There is no guarantee that these will be honored in the future. "One of ['low', 'med', 'high', 'auto', 'dense']" See [price granularity definitions](http://prebid.org/prebid-mobile/adops-price-granularity.html) - -One of "includewinners" or "includebidderkeys" must be true (both default to true if unset). If both were false, then no targeting keys would be set, which is better configured by omitting targeting altogether. - -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 - } - } -} -``` - -**Response format** (returned in `bid.ext.prebid.targeting`) - -``` -{ - "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." - } - } - } - }] - }] -} -``` - -The winning bid for each `request.imp[i]` will also contain `hb_bidder`, `hb_size`, and `hb_pb` -(with _no_ {bidderName} suffix). To prevent these keys, set `request.ext.prebid.targeting.includeWinners` to false. - -**NOTE**: Targeting keys are limited to 20 characters. If {bidderName} is too long, the returned key -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 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. - -2. The request payload can store a `buyeruid` for each Bidder by defining `request.user.ext.prebid.buyeruids` like so: - -``` -{ - "user": { - "ext": { - "prebid": { - "buyeruids": { - "appnexus": "some-appnexus-id", - "rubicon": "some-rubicon-id" - } - } - } - } -} -``` - -Prebid Server's core logic will preprocess the request so that each Bidder sees their own value in the `request.user.buyeruid` field. - -3. Prebid Server will use its Cookie to map IDs for each Bidder. - -If you're using [Prebid.js](https://github.com/prebid/Prebid.js), this is happening automatically. - -If you're using another client, you can populate the Cookie of the Prebid Server host with User IDs -for each Bidder by using the `/cookie_sync` endpoint, and calling the URLs that it returns in the response. - -#### Native Request - -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 - -Requests can define Bidder aliases if they want to refer to a Bidder by a separate name. -This can be used to request bids from the same Bidder with different params. For example: - -``` -{ - "imp": [{ - "id": "some-impression-id", - "video": { - "mimes": ["video/mp4"] - }, - "ext": { - "appnexus": { - "placementId": 123 - }, - "districtm": { - "placementId": 456 - } - } - }], - "ext": { - "prebid": { - "aliases": { - "districtm": "appnexus" - } - } - } -} -``` - -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 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. -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. - -#### Bidder Response Times - -`response.ext.responsetimemillis.{bidderName}` tells how long each bidder took to respond. -These can help quantify the performance impact of "the slowest bidder." - -#### Bidder Errors - -`response.ext.errors.{bidderName}` contains messages which describe why a request may be "suboptimal". -For example, suppose a `banner` and a `video` impression are offered to a bidder -which only supports `banner`. - -In cases like these, the bidder can ignore the `video` impression and bid on the `banner` one. -However, the publisher can improve performance by only offering impressions which the bidder supports. - -For example, a request may return this in `response.ext` - -``` -{ - "ext": { - "errors": { - "appnexus": [{ - "code": 2, - "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio." - }], - "rubicon": [{ - "code": 1, - "message": "The request exceeded the timeout allocated" - }] - } - } -} -``` - -The codes currently defined are: - -``` -0 NoErrorCode -1 TimeoutCode -2 BadInputCode -3 BadServerResponseCode -999 UnknownErrorCode -``` - -#### Debugging - -`response.ext.debug.httpcalls.{bidder}` will be populated **only if** `request.test` **was set to 1**. - -This contains info about every request and response sent by the bidder to its server. -It is only returned on `test` bids for performance reasons, but may be useful during debugging. - -`response.ext.debug.resolvedrequest` will be populated **only if** `request.test` **was set to 1**. - -This contains the request after the resolution of stored requests and implicit information (e.g. site domain, device user agent). - -#### Stored Requests - -`request.imp[i].ext.prebid.storedrequest` incorporates a [Stored Request](../../developers/stored-requests.md) from the server. - -A typical `storedrequest` value looks like this: - -``` -{ - "imp": [{ - "ext": { - "prebid": { - "storedrequest": { - "id": "some-id" - } - } - } - }] -} -``` - -For more information, see the docs for [Stored Requests](../../developers/stored-requests.md). - -#### Cache bids - -Bids can be temporarily cached on the server by sending the following data as `request.ext.prebid.cache`: - -``` -{ - "ext": { - "prebid": { - "cache": { - "bids": {}, - "vastxml": {} - } - } - } -} -``` - -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 -`bid.ext.prebid.targeting` keys: - -- `hb_cache_id`: On the highest overall Bid in each Imp. -- `hb_cache_id_{bidderName}`: On the highest Bid from {bidderName} in each Imp. - -Clients _should not assume_ that these keys will exist, just because they were requested, though. -If they exist, the value will be a UUID which can be used to fetch Bid JSON from [Prebid Cache](https://github.com/prebid/prebid-cache). -They may not exist if the host company's cache is full, having connection problems, or other issues like that. - -If `vastxml` is present, PBS will try to add analogous keys `hb_uuid` and `hb_uuid_{bidderName}`. -In addition to the caveats above, these will exist _only if the relevant Bids are for Video_. -If they exist, the values can be used to fetch the bid's VAST XML from Prebid Cache directly. - -These options are mainly intended for certain limited Prebid Mobile setups, where bids cannot be cached client-side. - -#### GDPR - -Prebid Server supports the IAB's GDPR recommendations, which can be found [here](https://iabtechlab.com/wp-content/uploads/2018/02/OpenRTB_Advisory_GDPR_2018-02.pdf). - -This adds two optional properties: - -- `request.user.ext.consent`: Is the consent string required by the IAB standards. -- `request.regs.ext.gdpr`: Is 0 if the caller believes that the user is *not* under GDPR, 1 if the user *is* under GDPR, and undefined if we're not certain. - -These fields will be forwarded to each Bidder, so they can decide how to process them. - -#### Interstitial support -Additional support for interstitials is enabled through the addition of two fields to the request: -device.ext.prebid.interstitial.minwidthperc and device.ext.interstial.minheightperc -The values will be numbers that indicate the minimum allowed size for the ad, as a percentage of the base side. For example, a width of 600 and "minwidthperc": 60 would allow ads with widths from 360 to 600 pixels inclusive. - -Example: -``` -{ - "imp": [{ - ... - "banner": { - ... - } - "instl": 1, - ... - }] - "device": { - ... - "h": 640, - "w": 320, - "ext": { - "prebid": { - "interstitial": { - "minwidthperc": 60, - "minheightperc": 60 - } - } - } - } -} -``` - -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-responses at the bidder adapter level - -**Single Stored Auction Response ID** - -When a storedauctionresponse ID is specified: - -- the rest of the ext.prebid block is irrelevant and ignored -- nothing is sent to any bidder adapter for that imp -- the response retrieved from the stored-response-id is assumed to be the entire contents of the seatbid object corresponding to that impression. - -This request: -``` -{ - "test":1, - "tmax":500, - "id": "test-auction-id", - "app": { ... }, - "ext": { - "prebid": { - "targeting": {}, - "cache": { "bids": {} } - } - }, - "imp": [ - { - "id": "a", - "ext": { "prebid": { "storedauctionresponse": { "id": "1111111111" } } } - }, - { - "id": "b", - "ext": { "prebid": { "storedauctionresponse": { "id": "22222222222" } } } - } - ] -} -``` - -Will result in this response, assuming that the ids exist in the appropriate DB table read by Prebid Server: -``` -{ - "id": "test-auction-id", - "seatbid": [ - { - // BidderA bids from storedauctionresponse=1111111111 - // BidderA bids from storedauctionresponse=22222222 - }, - { - // BidderB bids from storedauctionresponse=1111111111 - // BidderB bids from storedauctionresponse=22222222 - } - ] -} -``` - -**Multiple Stored Bid Response IDs** - -In contrast to what's outlined above, this approach lets some real auctions take place while some bidders have test responses that still exercise bidder code. For example, this request: - -``` -{ - "test":1, - "tmax":500, - "id": "test-auction-id", - "app": { ... }, - "ext": { - "prebid": { - "targeting": {}, - "cache": { "bids": {} } - } - }, - "imp": [ - { - "id": "a", - "ext": { - "prebid": { - "storedbidresponse": [ - { "bidder": "BidderA", "id": "333333" }, - { "bidder": "BidderB", "id": "444444" }, - ] - } - } - }, - { - "id": "b", - "ext": { - "prebid": { - "storedbidresponse": [ - { "bidder": "BidderA", "id": "5555555" }, - { "bidder": "BidderB", "id": "6666666" }, - ] - } - } - } - ] -} -``` -Could result in this response: - -``` -{ - "id": "test-auction-id", - "seatbid": [ - { - "bid": [ - // contents of storedbidresponse=3333333 as parsed by bidderA adapter - // contents of storedbidresponse=5555555 as parsed by bidderA adapter - ] - }, - { - // contents of storedbidresponse=4444444 as parsed by bidderB adapter - // contents of storedbidresponse=6666666 as parsed by bidderB adapter - } - ] -} -``` - -Setting up the storedresponse DB entries is the responsibility of each Prebid Server host company. - -See Prebid.org troubleshooting pages for how to utilize this feature within the context of the browser. - - -#### User IDs (PBS-Java only) - -Prebid Server adapters can support the [Prebid.js User ID modules](http://prebid.org/dev-docs/modules/userId.html) by reading the following extensions and passing them through to their server endpoints: - -``` -{ - "user": { - "ext": { - "eids": [{ - "source": "adserver.org", - "uids": [{ - "id": "111111111111", - "ext": { - "rtiPartner": "TDID" - } - }] - }, - { - "source": "pubcommon", - "id":"11111111" - } - ], - "digitrust": { - "id": "11111111111", - "keyv": 4 - } - } - } -} -``` - -#### First Party Data Support (PBS-Java only) - -This is the Prebid Server version of the Prebid.js First Party Data feature. It's a standard way for the page (or app) to supply first party data and control which bidders have access to it. - -It specifies where in the OpenRTB request non-standard attributes should be passed. For example: - -``` -{ - "ext": { - "prebid": { - "data": { "bidders": [ "rubicon", "appnexus" ] } // these are the bidders allowed to see protected data - } - }, - "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": { - 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 - } - } - ] -``` - -Prebid Server enforces the data permissioning - -So before passing the values to the bidder adapters, core will: - -1. check for ext.prebid.data.bidders -1. if it exists, store it locally, but remove it from the OpenRTB before being sent to the adapters -1. As the OpenRTB request is being sent to each adapter: - 1. if ext.prebid.data.bidders exists in the original request, and this bidder is on the list then copy site.ext.data, app.ext.data, and user.ext.data to their bidder request -- otherwise don't copy those blocks - 1. copy other objects as normal - -Each adapter must be coded to read the values from these locations and pass it to their endpoints appropriately. - -### OpenRTB Ambiguities - -This section describes the ways in which Prebid Server **implements** OpenRTB spec ambiguous parts. - -- `request.cur`: If `request.cur` is not specified in the bid request, Prebid Server will consider it as being `USD` whereas OpenRTB spec doesn't mention any default currency for bid request. -```request.cur: ['USD'] // Default value if not set``` - - -### OpenRTB Differences - -This section describes the ways in which Prebid Server **breaks** the OpenRTB spec. - -#### Allowed Bidders - -Prebid Server returns a 400 on requests which define `wseat` or `bseat`. -We may add support for these in the future, if there's compelling need. - -Instead, an impression is only offered to a bidder if `bidrequest.imp[i].ext.{bidderName}` exists. - -This supports publishers who want to sell different impressions to different bidders. - -#### Deprecated Properties - -This endpoint returns a 400 if the request contains deprecated properties (e.g. `imp.wmin`, `imp.hmax`). - -The error message in the response should describe how to "fix" the request to make it legal. -If the message is unclear, please [log an issue](https://github.com/PubMatic-OpenWrap/prebid-server/issues) -or [submit a pull request](https://github.com/PubMatic-OpenWrap/prebid-server/pulls) to improve it. - -#### Determining Bid Security (http/https) - -In the OpenRTB spec, `request.imp[i].secure` says: - -> Flag to indicate if the impression requires secure HTTPS URL creative assets and markup, -> where 0 = non-secure, 1 = secure. If omitted, the secure state is unknown, but non-secure -> HTTP support can be assumed. - -In Prebid Server, an `https` request which does not define `secure` will be forwarded to Bidders with a `1`. -Publishers who run `https` sites and want insecure ads can still set this to `0` explicitly. - -### See also - -- [The OpenRTB 2.5 spec](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf) diff --git a/docs/endpoints/setuid.md b/docs/endpoints/setuid.md deleted file mode 100644 index c1746806371..00000000000 --- a/docs/endpoints/setuid.md +++ /dev/null @@ -1,26 +0,0 @@ -# Saving User Syncs - -This endpoint is used during cookie syncs. For technical details, see the -[Cookie Sync developer docs](../developers/cookie-syncs.md). - -## `GET /setuid` - -This endpoint saves a UserID for a Bidder in the Cookie. Saved IDs will be recognized for 7 days before being considered "stale" and being re-synced. - -### Query Params - -- `bidder`: The FamilyName of the [Usersyncer](../../usersync/usersync.go) which is being synced. -- `uid`: The ID which the Bidder uses to recognize this user. If undefined, the UID for `bidder` will be deleted. -- `gdpr`: This should be `1` if GDPR is in effect, `0` if not, and undefined if the caller isn't sure -- `gdpr_consent`: This is required if `gdpr` is one, and optional (but encouraged) otherwise. If present, it should be an [unpadded base64-URL](https://tools.ietf.org/html/rfc4648#page-7) encoded [Vendor Consent String](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md#vendor-consent-string-format-). - -If the `gdpr` and `gdpr_consent` params are included, this endpoint will _not_ write a cookie unless: - -1. The Vendor ID set by the Prebid Server host company has permission to save cookies for that user. -2. The Prebid Server host company did not configure it to run with GDPR support. - -If in doubt, contact the company hosting Prebid Server and ask if they're GDPR-ready. - -### Sample request - -`GET http://prebid.site.com/setuid?bidder=adnxs&uid=12345&gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw` diff --git a/docs/endpoints/status.md b/docs/endpoints/status.md deleted file mode 100644 index 0c252397423..00000000000 --- a/docs/endpoints/status.md +++ /dev/null @@ -1,9 +0,0 @@ -## `GET /status` - -This endpoint will return a 2xx response whenever Prebid Server is ready to serve requests. -Its exact response can be [configured](../developers/configuration.md) with the `status_response` -config option. For example, in `pbs.yaml`: - -```yaml -status_response: "ok" -``` diff --git a/endpoints/auction.go b/endpoints/auction.go index dd45be8df03..ff9d8f5a0ee 100644 --- a/endpoints/auction.go +++ b/endpoints/auction.go @@ -21,7 +21,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" pbc "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" "github.com/PubMatic-OpenWrap/prebid-server/privacy" - gdprPolicy "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + gdprPrivacy "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" "github.com/PubMatic-OpenWrap/prebid-server/usersync" "github.com/golang/glog" "github.com/julienschmidt/httprouter" @@ -190,7 +190,7 @@ func (a *auction) recoverSafely(inner func(*pbs.PBSBidder, pbsmetrics.AdapterLab } } -func (a *auction) shouldUsersync(ctx context.Context, bidder openrtb_ext.BidderName, gdprPrivacyPolicy gdprPolicy.Policy) bool { +func (a *auction) shouldUsersync(ctx context.Context, bidder openrtb_ext.BidderName, gdprPrivacyPolicy gdprPrivacy.Policy) bool { switch gdprPrivacyPolicy.Signal { case "0": return true @@ -310,10 +310,7 @@ func sortBidsAddKeywordsMobile(bids pbs.PBSBidSlice, pbs_req *pbs.PBSRequest, pr // after sorting we need to add the ad targeting keywords for i, bid := range bar { // We should eventually check for the error and do something. - roundedCpm, err := exchange.GetCpmStringValue(bid.Price, openrtb_ext.PriceGranularityFromString(priceGranularitySetting)) - if err != nil { - glog.Error(err.Error()) - } + roundedCpm := exchange.GetPriceBucket(bid.Price, openrtb_ext.PriceGranularityFromString(priceGranularitySetting)) hbSize := "" if bid.Width != 0 && bid.Height != 0 { @@ -511,7 +508,7 @@ func (a *auction) processUserSync(req *pbs.PBSRequest, bidder *pbs.PBSBidder, bl if uid == "" { bidder.NoCookie = true privacyPolicies := privacy.Policies{ - GDPR: gdprPolicy.Policy{ + GDPR: gdprPrivacy.Policy{ Signal: req.ParseGDPR(), Consent: req.ParseConsent(), }, diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index 9c3b9878efa..e24e9454e12 100644 --- a/endpoints/auction_test.go +++ b/endpoints/auction_test.go @@ -387,11 +387,12 @@ func TestShouldUsersync(t *testing.T) { }, metricsEngine: nil, } - privacyPolicy := gdprPolicy.Policy{ + gdprPrivacyPolicy := gdprPolicy.Policy{ Signal: gdprApplies, Consent: consent, } - allowSyncs := deps.shouldUsersync(context.Background(), openrtb_ext.BidderAdform, privacyPolicy) + + allowSyncs := deps.shouldUsersync(context.Background(), openrtb_ext.BidderAdform, gdprPrivacyPolicy) if allowSyncs != expectAllow { t.Errorf("Expected syncs: %t, allowed syncs: %t", expectAllow, allowSyncs) } @@ -408,6 +409,7 @@ type auctionMockPermissions struct { allowHostCookies bool allowPI bool allowGeo bool + allowID bool } func (m *auctionMockPermissions) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { @@ -418,8 +420,8 @@ 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, bool, error) { - return m.allowPI, m.allowGeo, nil +func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return m.allowPI, m.allowGeo, m.allowID, nil } func (m *auctionMockPermissions) AMPException() bool { diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index b75c5d29b65..35ba3cb14a7 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -19,7 +19,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/privacy" "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" - gdprPolicy "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + gdprPrivacy "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" "github.com/PubMatic-OpenWrap/prebid-server/usersync" "github.com/buger/jsonparser" "github.com/golang/glog" @@ -110,24 +110,30 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h } } + parsedReq.filterExistingSyncs(deps.syncers, userSyncCookie, needSyncupForSameSite) + + adapterSyncs := make(map[openrtb_ext.BidderName]bool) + // assume all bidders will be privacy blocked + for _, b := range parsedReq.Bidders { + adapterSyncs[openrtb_ext.BidderName(b)] = true + } + privacyPolicy := privacy.Policies{ - GDPR: gdprPolicy.Policy{ + GDPR: gdprPrivacy.Policy{ Signal: gdprToString(parsedReq.GDPR), Consent: parsedReq.Consent, }, CCPA: ccpa.Policy{ - Value: parsedReq.USPrivacy, + Consent: parsedReq.USPrivacy, }, } - parsedReq.filterExistingSyncs(deps.syncers, userSyncCookie, needSyncupForSameSite) + parsedReq.filterForGDPR(deps.syncPermissions) - adapterSyncs := make(map[openrtb_ext.BidderName]bool) - // assume all bidders will be privacy blocked - for _, b := range parsedReq.Bidders { - adapterSyncs[openrtb_ext.BidderName(b)] = true + if deps.enforceCCPA { + parsedReq.filterForCCPA() } - parsedReq.filterForPrivacy(deps.syncPermissions, privacyPolicy, deps.enforceCCPA) + // surviving bidders are not privacy blocked for _, b := range parsedReq.Bidders { adapterSyncs[openrtb_ext.BidderName(b)] = false @@ -264,12 +270,7 @@ func (req *cookieSyncRequest) filterExistingSyncs(valid map[openrtb_ext.BidderNa } } -func (req *cookieSyncRequest) filterForPrivacy(permissions gdpr.Permissions, privacyPolicies privacy.Policies, enforceCCPA bool) { - if enforceCCPA && privacyPolicies.CCPA.ShouldEnforce() { - req.Bidders = nil - return - } - +func (req *cookieSyncRequest) filterForGDPR(permissions gdpr.Permissions) { if req.GDPR != nil && *req.GDPR == 0 { return } @@ -287,6 +288,25 @@ func (req *cookieSyncRequest) filterForPrivacy(permissions gdpr.Permissions, pri } } +func (req *cookieSyncRequest) filterForCCPA() { + validBidders := make(map[string]struct{}) + for _, v := range openrtb_ext.BidderMap { + validBidders[v.String()] = struct{}{} + } + + ccpaPolicy := &ccpa.Policy{Consent: req.USPrivacy} + ccpaParsedPolicy, err := ccpaPolicy.Parse(validBidders) + + if err == nil { + for i := 0; i < len(req.Bidders); i++ { + if ccpaParsedPolicy.ShouldEnforce(req.Bidders[i]) { + req.Bidders = append(req.Bidders[:i], req.Bidders[i+1:]...) + i-- + } + } + } +} + // filterToLimit will enforce a max limit on cookiesyncs supplied, picking a random subset of syncs to get to the limit if over. func (req *cookieSyncRequest) filterToLimit() { if req.Limit <= 0 { diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index eef441b854f..b25d369226c 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -377,8 +377,8 @@ 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, bool, error) { - return true, true, nil +func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return true, true, true, nil } func (g *gdprPerms) AMPException() bool { diff --git a/endpoints/currency_rates.go b/endpoints/currency_rates.go index 1e09f88a582..20d5ba9fc6c 100644 --- a/endpoints/currency_rates.go +++ b/endpoints/currency_rates.go @@ -24,7 +24,7 @@ type rateConverter interface { } // newCurrencyRatesInfo creates a new CurrencyRatesInfo instance. -func newCurrencyRatesInfo(rateConverter rateConverter) currencyRatesInfo { +func newCurrencyRatesInfo(rateConverter rateConverter, fetchingInterval time.Duration) currencyRatesInfo { currencyRatesInfo := currencyRatesInfo{ Active: false, @@ -44,7 +44,6 @@ func newCurrencyRatesInfo(rateConverter rateConverter) currencyRatesInfo { source := infos.Source() currencyRatesInfo.Source = &source - fetchingInterval := infos.FetchingInterval() currencyRatesInfo.FetchingInterval = &fetchingInterval lastUpdated := infos.LastUpdated() @@ -57,8 +56,8 @@ func newCurrencyRatesInfo(rateConverter rateConverter) currencyRatesInfo { } // NewCurrencyRatesEndpoint returns current currency rates applied by the PBS server. -func NewCurrencyRatesEndpoint(rateConverter rateConverter) http.HandlerFunc { - currencyRateInfo := newCurrencyRatesInfo(rateConverter) +func NewCurrencyRatesEndpoint(rateConverter rateConverter, fetchingInterval time.Duration) http.HandlerFunc { + currencyRateInfo := newCurrencyRatesInfo(rateConverter, fetchingInterval) return func(w http.ResponseWriter, _ *http.Request) { jsonOutput, err := json.Marshal(currencyRateInfo) diff --git a/endpoints/currency_rates_test.go b/endpoints/currency_rates_test.go index 5e43cec05bf..be1c3bcdf56 100644 --- a/endpoints/currency_rates_test.go +++ b/endpoints/currency_rates_test.go @@ -14,20 +14,21 @@ import ( func TestCurrencyRatesEndpoint(t *testing.T) { // Setup: var testCases = []struct { - input rateConverter - expectedBody string - expectedCode int - description string + inputConverter rateConverter + inputFetchingInterval time.Duration + expectedBody string + expectedCode int + description string }{ { nil, + time.Duration(0), `{"active": false}`, http.StatusOK, "case 1 - rate converter is nil", }, { newRateConverterMock( - 5*time.Minute, "https://sync.test.com", time.Date(2019, 3, 2, 12, 54, 56, 651387237, time.UTC), newConversionMock(&map[string]map[string]float64{ @@ -36,6 +37,7 @@ func TestCurrencyRatesEndpoint(t *testing.T) { }, }), ), + 5 * time.Minute, `{ "active": true, "source": "https://sync.test.com", @@ -52,11 +54,11 @@ func TestCurrencyRatesEndpoint(t *testing.T) { }, { newRateConverterMock( - time.Duration(0), "", time.Time{}, nil, ), + time.Duration(0), `{ "active": true, "source": "", @@ -70,12 +72,14 @@ func TestCurrencyRatesEndpoint(t *testing.T) { newRateConverterMockWithInfo( newUnmarshableConverterInfoMock(), ), + time.Duration(0), "", http.StatusInternalServerError, "case 4 - invalid rates input for marshaling", }, { newRateConverterMockWithNilInfo(), + time.Duration(0), `{ "active": true }`, @@ -86,7 +90,7 @@ func TestCurrencyRatesEndpoint(t *testing.T) { for _, tc := range testCases { - handler := NewCurrencyRatesEndpoint(tc.input) + handler := NewCurrencyRatesEndpoint(tc.inputConverter, tc.inputFetchingInterval) w := httptest.NewRecorder() // Execute: @@ -117,21 +121,16 @@ func newConversionMock(rates *map[string]map[string]float64) *conversionMock { } type converterInfoMock struct { - source string - fetchingInterval time.Duration - lastUpdated time.Time - rates *map[string]map[string]float64 - additionalInfo interface{} + source string + lastUpdated time.Time + rates *map[string]map[string]float64 + additionalInfo interface{} } func (m converterInfoMock) Source() string { return m.source } -func (m converterInfoMock) FetchingInterval() time.Duration { - return m.fetchingInterval -} - func (m converterInfoMock) LastUpdated() time.Time { return m.lastUpdated } @@ -150,10 +149,6 @@ func (m unmarshableConverterInfoMock) Source() string { return "" } -func (m unmarshableConverterInfoMock) FetchingInterval() time.Duration { - return time.Duration(0) -} - func (m unmarshableConverterInfoMock) LastUpdated() time.Time { return time.Time{} } @@ -172,7 +167,6 @@ func newUnmarshableConverterInfoMock() unmarshableConverterInfoMock { } type rateConverterMock struct { - fetchingInterval time.Duration syncSourceURL string rates *conversionMock lastUpdated time.Time @@ -197,23 +191,20 @@ func (m rateConverterMock) GetInfo() currencies.ConverterInfo { rates = m.rates.GetRates() } return converterInfoMock{ - source: m.syncSourceURL, - fetchingInterval: m.fetchingInterval, - lastUpdated: m.lastUpdated, - rates: rates, + source: m.syncSourceURL, + lastUpdated: m.lastUpdated, + rates: rates, } } func newRateConverterMock( - fetchingInterval time.Duration, syncSourceURL string, lastUpdated time.Time, rates *conversionMock) rateConverterMock { return rateConverterMock{ - fetchingInterval: fetchingInterval, - syncSourceURL: syncSourceURL, - rates: rates, - lastUpdated: lastUpdated, + syncSourceURL: syncSourceURL, + rates: rates, + lastUpdated: lastUpdated, } } diff --git a/endpoints/events/account_test.go b/endpoints/events/account_test.go new file mode 100644 index 00000000000..559b39d096c --- /dev/null +++ b/endpoints/events/account_test.go @@ -0,0 +1,161 @@ +package events + +import ( + "errors" + "fmt" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHandleAccountServiceErrors(t *testing.T) { + tests := map[string]struct { + fetcher *mockAccountsFetcher + cfg *config.Configuration + want struct { + code int + response string + } + }{ + "badRequest": { + fetcher: &mockAccountsFetcher{ + Fail: true, + Error: errors.New("some error"), + }, + cfg: &config.Configuration{ + AccountDefaults: config.Account{Disabled: true}, + AccountRequired: true, + MaxRequestSize: maxSize, + VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: false, + }, + }, + want: struct { + code int + response string + }{ + code: 400, + response: "Invalid request: some error\nInvalid request: Prebid-server could not verify the Account ID. Please reach out to the prebid server host.\n", + }, + }, + "serviceUnavailable": { + fetcher: &mockAccountsFetcher{ + Fail: false, + }, + cfg: &config.Configuration{ + BlacklistedAcctMap: map[string]bool{"testacc": true}, + MaxRequestSize: maxSize, + VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: false, + }, + }, + want: struct { + code int + response string + }{ + code: 503, + response: "Invalid request: Prebid-server has disabled Account ID: testacc, please reach out to the prebid server host.\n", + }, + }, + "timeout": { + fetcher: &mockAccountsFetcher{ + Fail: false, + DurationMS: 50, + }, + cfg: &config.Configuration{ + AccountDefaults: config.Account{Disabled: true}, + AccountRequired: true, + Event: config.Event{ + TimeoutMS: 1, + }, + MaxRequestSize: maxSize, + VTrack: config.VTrack{ + TimeoutMS: int64(1), + AllowUnknownBidder: false, + }, + }, + want: struct { + code int + response string + }{ + code: 504, + response: "Invalid request: context deadline exceeded\nInvalid request: Prebid-server could not verify the Account ID. Please reach out to the prebid server host.\n", + }, + }, + } + + for name, test := range tests { + + handlers := []struct { + name string + h httprouter.Handle + r *http.Request + }{ + vast(t, test.cfg, test.fetcher), + event(test.cfg, test.fetcher), + } + + for _, handler := range handlers { + t.Run(handler.name+"-"+name, func(t *testing.T) { + test.cfg.MarshalAccountDefaults() + + recorder := httptest.NewRecorder() + + // execute + handler.h(recorder, handler.r, nil) + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, test.want.code, recorder.Result().StatusCode, fmt.Sprintf("Expected %d", test.want.code)) + assert.Equal(t, test.want.response, string(d)) + }) + } + } +} + +func event(cfg *config.Configuration, fetcher stored_requests.AccountFetcher) struct { + name string + h httprouter.Handle + r *http.Request +} { + return struct { + name string + h httprouter.Handle + r *http.Request + }{ + name: "event", + h: NewEventEndpoint(cfg, fetcher, nil), + r: httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=testacc", strings.NewReader("")), + } +} + +func vast(t *testing.T, cfg *config.Configuration, fetcher stored_requests.AccountFetcher) struct { + name string + h httprouter.Handle + r *http.Request +} { + vtrackBody, err := getValidVTrackRequestBody(true, true) + if err != nil { + t.Fatal(err) + } + + return struct { + name string + h httprouter.Handle + r *http.Request + }{ + name: "vast", + h: NewVTrackEndpoint(cfg, fetcher, &vtrackMockCacheClient{}, adapters.BidderInfos{}), + r: httptest.NewRequest("POST", "/vtrack?a=testacc", strings.NewReader(vtrackBody)), + } +} diff --git a/endpoints/events/event.go b/endpoints/events/event.go new file mode 100644 index 00000000000..da18b16bd53 --- /dev/null +++ b/endpoints/events/event.go @@ -0,0 +1,338 @@ +package events + +import ( + "context" + "errors" + "fmt" + accountService "github.com/PubMatic-OpenWrap/prebid-server/account" + "github.com/PubMatic-OpenWrap/prebid-server/analytics" + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/julienschmidt/httprouter" + "net/http" + "net/url" + "strconv" + "time" +) + +const ( + // Required + TemplateUrl = "%v/event?t=%v&b=%v&a=%v" + TypeParameter = "t" + BidIdParameter = "b" + AccountIdParameter = "a" + + // Optional + BidderParameter = "bidder" + TimestampParameter = "ts" + FormatParameter = "f" + AnalyticsParameter = "x" +) + +var trackingPixelPng = &trackingPixel{ + Content: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, + 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, + 0x89, 0x00, 0x00, 0x00, 0x04, 0x73, 0x42, 0x49, 0x54, 0x08, 0x08, 0x08, 0x08, 0x7C, 0x08, 0x64, 0x88, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x63, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00, + 0x00, 0x05, 0x00, 0x01, 0x87, 0xA1, 0x4E, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82}, + ContentType: "image/png", +} + +type trackingPixel struct { + Content []byte `json:"content,omitempty"` + ContentType string `json:"content_type,omitempty"` +} + +type eventEndpoint struct { + Accounts stored_requests.AccountFetcher + Analytics analytics.PBSAnalyticsModule + Cfg *config.Configuration + TrackingPixel *trackingPixel +} + +func NewEventEndpoint(cfg *config.Configuration, accounts stored_requests.AccountFetcher, analytics analytics.PBSAnalyticsModule) httprouter.Handle { + ee := &eventEndpoint{ + Accounts: accounts, + Analytics: analytics, + Cfg: cfg, + TrackingPixel: trackingPixelPng, + } + + return ee.Handle +} + +func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // parse event request from http req + eventRequest, errs := ParseEventRequest(r) + + // handle possible parsing errors + if len(errs) > 0 { + w.WriteHeader(http.StatusBadRequest) + + for _, err := range errs { + w.Write([]byte(fmt.Sprintf("invalid request: %s\n", err.Error()))) + } + + return + } + + // validate account id + accountId, err := checkRequiredParameter(r, AccountIdParameter) + + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(fmt.Sprintf("Account '%s' is required query parameter and can't be empty", AccountIdParameter))) + return + } + eventRequest.AccountID = accountId + + if eventRequest.Analytics != analytics.Enabled { + w.WriteHeader(http.StatusNoContent) + return + } + + ctx := context.Background() + if e.Cfg.Event.TimeoutMS > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(e.Cfg.Event.TimeoutMS)*time.Millisecond) + defer cancel() + } + + // get account details + account, errs := accountService.GetAccount(ctx, e.Cfg, e.Accounts, eventRequest.AccountID) + if len(errs) > 0 { + status, messages := HandleAccountServiceErrors(errs) + w.WriteHeader(status) + + for _, message := range messages { + w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", message))) + } + return + } + + // account does not support events + if !account.EventsEnabled { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(fmt.Sprintf("Account '%s' doesn't support events", eventRequest.AccountID))) + return + } + + // handle notification event + e.Analytics.LogNotificationEventObject(&analytics.NotificationEvent{ + Request: eventRequest, + Account: account, + }) + + // Add tracking pixel if format == image + if eventRequest.Format == analytics.Image { + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", e.TrackingPixel.ContentType) + w.Write(e.TrackingPixel.Content) + + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// EventRequestToUrl converts an analytics.EventRequest to an URL +func EventRequestToUrl(externalUrl string, request *analytics.EventRequest) string { + s := fmt.Sprintf(TemplateUrl, externalUrl, request.Type, request.BidID, request.AccountID) + + return s + optionalParameters(request) +} + +// ParseEventRequest parses an analytics.EventRequest from an Http request +func ParseEventRequest(r *http.Request) (*analytics.EventRequest, []error) { + event := &analytics.EventRequest{} + var errs []error + // validate type + if err := readType(event, r); err != nil { + errs = append(errs, err) + } + + // validate bidid + if bidid, err := checkRequiredParameter(r, BidIdParameter); err != nil { + errs = append(errs, err) + } else { + event.BidID = bidid + } + + // validate timestamp (optional) + if err := readTimestamp(event, r); err != nil { + errs = append(errs, err) + } + + // validate format (optional) + if err := readFormat(event, r); err != nil { + errs = append(errs, err) + } + + // validate analytics (optional) + if err := readAnalytics(event, r); err != nil { + errs = append(errs, err) + } + + // Bidder + event.Bidder = r.URL.Query().Get(BidderParameter) + + return event, errs +} + +// HandleAccountServiceErrors handles account.GetAccount errors +func HandleAccountServiceErrors(errs []error) (status int, messages []string) { + messages = []string{} + status = http.StatusBadRequest + + for _, er := range errs { + if errors.Is(er, context.DeadlineExceeded) { + er = &errortypes.Timeout{ + Message: er.Error(), + } + } + + messages = append(messages, er.Error()) + + errCode := errortypes.ReadCode(er) + + if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.BlacklistedAcctErrorCode { + status = http.StatusServiceUnavailable + } + + if errCode == errortypes.TimeoutErrorCode && status == http.StatusBadRequest { + status = http.StatusGatewayTimeout + } + } + + return status, messages +} + +func optionalParameters(request *analytics.EventRequest) string { + r := url.Values{} + + // timestamp + if request.Timestamp > 0 { + r.Add(TimestampParameter, strconv.FormatInt(request.Timestamp, 10)) + } + + // bidder + if request.Bidder != "" { + r.Add(BidderParameter, request.Bidder) + } + + // format + switch request.Format { + case analytics.Blank: + r.Add(FormatParameter, string(analytics.Blank)) + case analytics.Image: + r.Add(FormatParameter, string(analytics.Image)) + } + + //analytics + switch request.Analytics { + case analytics.Enabled: + r.Add(AnalyticsParameter, string(analytics.Enabled)) + case analytics.Disabled: + r.Add(AnalyticsParameter, string(analytics.Disabled)) + } + + opt := r.Encode() + + if opt != "" { + return "&" + opt + } + + return opt +} + +// readType validates analytics.EventRequest type +func readType(er *analytics.EventRequest, httpRequest *http.Request) error { + t, err := checkRequiredParameter(httpRequest, TypeParameter) + + if err != nil { + return err + } + + switch t { + case string(analytics.Imp): + er.Type = analytics.Imp + return nil + case string(analytics.Win): + er.Type = analytics.Win + return nil + default: + return &errortypes.BadInput{Message: fmt.Sprintf("unknown type: '%s'", t)} + } +} + +// readFormat validates analytics.EventRequest format attribute +func readFormat(er *analytics.EventRequest, httpRequest *http.Request) error { + f := httpRequest.URL.Query().Get(FormatParameter) + + if f != "" { + switch f { + case string(analytics.Blank): + er.Format = analytics.Blank + return nil + case string(analytics.Image): + er.Format = analytics.Image + return nil + default: + return &errortypes.BadInput{Message: fmt.Sprintf("unknown format: '%s'", f)} + } + } + + return nil +} + +// readAnalytics validates analytics.EventRequest analytics attribute +func readAnalytics(er *analytics.EventRequest, httpRequest *http.Request) error { + a := httpRequest.URL.Query().Get(AnalyticsParameter) + + if a != "" { + switch a { + case string(analytics.Enabled): + er.Analytics = analytics.Enabled + return nil + case string(analytics.Disabled): + er.Analytics = analytics.Disabled + return nil + default: + return &errortypes.BadInput{Message: fmt.Sprintf("unknown analytics: '%s'", a)} + } + } + + er.Analytics = analytics.Enabled + return nil +} + +// readTimestamp validates analytics.EventRequest timestamp attribute +func readTimestamp(er *analytics.EventRequest, httpRequest *http.Request) error { + t := httpRequest.URL.Query().Get(TimestampParameter) + + if t != "" { + ts, err := strconv.ParseInt(t, 10, 64) + + if err != nil { + return &errortypes.BadInput{Message: fmt.Sprintf("invalid request: error parsing timestamp '%s'", t)} + } + + er.Timestamp = ts + return nil + } + + return nil +} + +// checkRequiredParameter checks if http.Request contains all required parameters +func checkRequiredParameter(httpRequest *http.Request, parameter string) (string, error) { + t := httpRequest.URL.Query().Get(parameter) + + if t == "" { + return "", &errortypes.BadInput{Message: fmt.Sprintf("parameter '%s' is required", parameter)} + } + + return t, nil +} diff --git a/endpoints/events/event_test.go b/endpoints/events/event_test.go new file mode 100644 index 00000000000..d32d01ad562 --- /dev/null +++ b/endpoints/events/event_test.go @@ -0,0 +1,664 @@ +package events + +import ( + "context" + "encoding/base64" + "encoding/json" + "github.com/PubMatic-OpenWrap/prebid-server/analytics" + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// Mock Analytics Module +type eventsMockAnalyticsModule struct { + Fail bool + Error error + Invoked bool +} + +func (e *eventsMockAnalyticsModule) LogAuctionObject(ao *analytics.AuctionObject) { + if e.Fail { + panic(e.Error) + } + return +} + +func (e *eventsMockAnalyticsModule) LogVideoObject(vo *analytics.VideoObject) { + if e.Fail { + panic(e.Error) + } + return +} + +func (e *eventsMockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) { + if e.Fail { + panic(e.Error) + } + return +} + +func (e *eventsMockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) { + if e.Fail { + panic(e.Error) + } + return +} + +func (e *eventsMockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { + if e.Fail { + panic(e.Error) + } + return +} + +func (e *eventsMockAnalyticsModule) LogNotificationEventObject(ne *analytics.NotificationEvent) { + if e.Fail { + panic(e.Error) + } + e.Invoked = true + + return +} + +// Mock Account fetcher +var mockAccountData = map[string]json.RawMessage{ + "events_enabled": json.RawMessage(`{"events_enabled":true}`), + "events_disabled": json.RawMessage(`{"events_enabled":false}`), +} + +type mockAccountsFetcher struct { + Fail bool + Error error + DurationMS int +} + +func (maf mockAccountsFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + if maf.DurationMS > 0 { + select { + case <-time.After(time.Duration(maf.DurationMS) * time.Millisecond): + break + case <-ctx.Done(): + return nil, []error{ctx.Err()} + } + } + + if account, ok := mockAccountData[accountID]; ok { + return account, nil + } + + if maf.Fail { + return nil, []error{maf.Error} + } + + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} +} + +// Tests + +func TestShouldReturnBadRequestWhenTypeIsMissing(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?b=test", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with missing type parameter") + assert.Equal(t, "invalid request: parameter 't' is required\n", string(d)) +} + +func TestShouldReturnBadRequestWhenTypeIsInvalid(t *testing.T) { + + // mock AccountsFetcher + mockAccounts := &mockAccountsFetcher{} + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=test&b=t", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccounts, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid type parameter") + assert.Equal(t, "invalid request: unknown type: 'test'\n", string(d)) +} + +func TestShouldReturnBadRequestWhenBidIdIsMissing(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with missing bidid parameter") + assert.Equal(t, "invalid request: parameter 'b' is required\n", string(d)) +} + +func TestShouldReturnBadRequestWhenTimestampIsInvalid(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=q", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid timestamp parameter") + assert.Equal(t, "invalid request: invalid request: error parsing timestamp 'q'\n", string(d)) +} + +func TestShouldReturnUnauthorizedWhenAccountIsMissing(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 401, recorder.Result().StatusCode, "Expected 401 on request with missing account id parameter") + assert.Equal(t, "Account 'a' is required query parameter and can't be empty", string(d)) +} + +func TestShouldReturnBadRequestWhenFormatValueIsInvalid(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=q", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid format parameter") + assert.Equal(t, "invalid request: unknown format: 'q'\n", string(d)) +} + +func TestShouldReturnBadRequestWhenAnalyticsValueIsInvalid(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=4", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid analytics parameter") + assert.Equal(t, "invalid request: unknown analytics: '4'\n", string(d)) +} + +func TestShouldNotPassEventToAnalyticsReporterWhenAccountNotFound(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: true, + Error: stored_requests.NotFoundError{ID: "testacc"}, + } + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=testacc", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 401, recorder.Result().StatusCode, "Expected 401 on account not found") + assert.Equal(t, "Account 'testacc' doesn't support events", string(d)) +} + +func TestShouldNotPassEventToAnalyticsReporterWhenAccountEventNotEnabled(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=events_disabled", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 401, recorder.Result().StatusCode, "Expected 401 on account with events disabled") + assert.Equal(t, "Account 'events_disabled' doesn't support events", string(d)) +} + +func TestShouldPassEventToAnalyticsReporterWhenAccountEventEnabled(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=events_enabled", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + // validate + assert.Equal(t, 204, recorder.Result().StatusCode, "Expected 204 when account has events enabled") + assert.Equal(t, true, mockAnalyticsModule.Invoked) +} + +func TestShouldNotPassEventToAnalyticsReporterWhenAnalyticsValueIsZero(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=0&a=events_enabled", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + // validate + assert.Equal(t, 204, recorder.Result().StatusCode) + assert.Equal(t, true, mockAnalyticsModule.Invoked != true) +} + +func TestShouldRespondWithPixelAndContentTypeWhenRequestFormatIsImage(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=i&x=1&a=events_enabled", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 with tracking pixel when format is imp") + assert.Equal(t, true, mockAnalyticsModule.Invoked) + assert.Equal(t, "image/png", recorder.Header().Get("Content-Type")) + assert.Equal(t, "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABHNCSVQICAgIfAhkiAAAAA1JREFUCJljYGBgYAAAAAUAAYehTtQAAAAASUVORK5CYII=", base64.URLEncoding.EncodeToString(d)) +} + +func TestShouldRespondWithNoContentWhenRequestFormatIsNotDefined(t *testing.T) { + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // mock PBS Analytics Module + mockAnalyticsModule := &eventsMockAnalyticsModule{ + Fail: false, + } + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "" + + req := httptest.NewRequest("GET", "/event?t=imp&b=test&ts=1234&x=1&a=events_enabled", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule) + + // execute + e(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 204, recorder.Result().StatusCode, "Expected 200 with empty response") + assert.Equal(t, true, mockAnalyticsModule.Invoked) + assert.Equal(t, "", recorder.Header().Get("Content-Type")) + assert.Equal(t, 0, len(d)) +} + +func TestShouldParseEventCorrectly(t *testing.T) { + + tests := map[string]struct { + req *http.Request + expected *analytics.EventRequest + }{ + "one": { + req: httptest.NewRequest("GET", "/event?t=win&b=bidId&f=b&ts=1000&x=1&a=accountId&bidder=bidder", strings.NewReader("")), + expected: &analytics.EventRequest{ + Type: analytics.Win, + BidID: "bidId", + Timestamp: 1000, + Bidder: "bidder", + AccountID: "", + Format: analytics.Blank, + Analytics: analytics.Enabled, + }, + }, + "two": { + req: httptest.NewRequest("GET", "/event?t=win&b=bidId&ts=0&a=accountId", strings.NewReader("")), + expected: &analytics.EventRequest{ + Type: analytics.Win, + BidID: "bidId", + Timestamp: 0, + Analytics: analytics.Enabled, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + + // execute + er, errs := ParseEventRequest(test.req) + + // validate + assert.Equal(t, 0, len(errs)) + assert.EqualValues(t, test.expected, er) + }) + } +} + +func TestEventRequestToUrl(t *testing.T) { + externalUrl := "http://localhost:8000" + tests := map[string]struct { + er *analytics.EventRequest + want string + }{ + "one": { + er: &analytics.EventRequest{ + Type: analytics.Imp, + BidID: "bidid", + AccountID: "accountId", + Bidder: "bidder", + Timestamp: 1234567, + Format: analytics.Blank, + Analytics: analytics.Enabled, + }, + want: "http://localhost:8000/event?t=imp&b=bidid&a=accountId&bidder=bidder&f=b&ts=1234567&x=1", + }, + "two": { + er: &analytics.EventRequest{ + Type: analytics.Win, + BidID: "bidid", + AccountID: "accountId", + Bidder: "bidder", + Timestamp: 1234567, + Format: analytics.Image, + Analytics: analytics.Disabled, + }, + want: "http://localhost:8000/event?t=win&b=bidid&a=accountId&bidder=bidder&f=i&ts=1234567&x=0", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + expected := EventRequestToUrl(externalUrl, test.er) + // validate + assert.Equal(t, test.want, expected) + }) + } +} diff --git a/endpoints/events/vtrack.go b/endpoints/events/vtrack.go new file mode 100644 index 00000000000..8a86e68edf1 --- /dev/null +++ b/endpoints/events/vtrack.go @@ -0,0 +1,300 @@ +package events + +import ( + "context" + "encoding/json" + "fmt" + accountService "github.com/PubMatic-OpenWrap/prebid-server/account" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/analytics" + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/golang/glog" + "github.com/julienschmidt/httprouter" + "io" + "io/ioutil" + "net/http" + "sort" + "strings" + "time" +) + +const ( + AccountParameter = "a" + ImpressionCloseTag = "" + ImpressionOpenTag = "" +) + +type vtrackEndpoint struct { + Cfg *config.Configuration + Accounts stored_requests.AccountFetcher + BidderInfos adapters.BidderInfos + Cache prebid_cache_client.Client +} + +type BidCacheRequest struct { + Puts []prebid_cache_client.Cacheable `json:"puts"` +} + +type BidCacheResponse struct { + Responses []CacheObject `json:"responses"` +} + +type CacheObject struct { + UUID string `json:"uuid"` +} + +func NewVTrackEndpoint(cfg *config.Configuration, accounts stored_requests.AccountFetcher, cache prebid_cache_client.Client, bidderInfos adapters.BidderInfos) httprouter.Handle { + vte := &vtrackEndpoint{ + Cfg: cfg, + Accounts: accounts, + BidderInfos: bidderInfos, + Cache: cache, + } + + return vte.Handle +} + +// /vtrack Handler +func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + + // get account id from request parameter + accountId := getAccountId(r) + + // account id is required + if accountId == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Account '%s' is required query parameter and can't be empty", AccountParameter))) + return + } + + // parse puts request from request body + req, err := ParseVTrackRequest(r, v.Cfg.MaxRequestSize+1) + + // check if there was any error while parsing puts request + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + return + } + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(v.Cfg.VTrack.TimeoutMS)*time.Millisecond)) + defer cancel() + + // get account details + account, errs := accountService.GetAccount(ctx, v.Cfg, v.Accounts, accountId) + if len(errs) > 0 { + status, messages := HandleAccountServiceErrors(errs) + w.WriteHeader(status) + + for _, message := range messages { + w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", message))) + } + return + } + + // insert impression tracking if account allows events and bidder allows VAST modification + if v.Cache != nil { + cachingResponse, errs := v.handleVTrackRequest(ctx, req, account) + + if len(errs) > 0 { + w.WriteHeader(http.StatusInternalServerError) + for _, err := range errs { + w.Write([]byte(fmt.Sprintf("Error(s) updating vast: %s\n", err.Error()))) + + return + } + } + + d, err := json.Marshal(*cachingResponse) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Error serializing pbs cache response: %s\n", err.Error()))) + + return + } + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(d) + + return + } + + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("PBS Cache client is not configured")) +} + +// GetVastUrlTracking creates a vast url tracking +func GetVastUrlTracking(externalUrl string, bidid string, bidder string, accountId string, timestamp int64) string { + + eventReq := &analytics.EventRequest{ + Type: analytics.Imp, + BidID: bidid, + AccountID: accountId, + Bidder: bidder, + Timestamp: timestamp, + Format: analytics.Blank, + } + + return EventRequestToUrl(externalUrl, eventReq) +} + +// ParseVTrackRequest parses a BidCacheRequest from an HTTP Request +func ParseVTrackRequest(httpRequest *http.Request, maxRequestSize int64) (req *BidCacheRequest, err error) { + req = &BidCacheRequest{} + err = nil + + // Pull the request body into a buffer, so we have it for later usage. + lr := &io.LimitedReader{ + R: httpRequest.Body, + N: maxRequestSize, + } + + defer httpRequest.Body.Close() + requestJson, err := ioutil.ReadAll(lr) + if err != nil { + return req, err + } + + // Check if the request size was too large + if lr.N <= 0 { + err = &errortypes.BadInput{Message: fmt.Sprintf("request size exceeded max size of %d bytes", maxRequestSize-1)} + return req, err + } + + if len(requestJson) == 0 { + err = &errortypes.BadInput{Message: "request body is empty"} + return req, err + } + + if err := json.Unmarshal(requestJson, req); err != nil { + return req, err + } + + for _, bcr := range req.Puts { + if bcr.BidID == "" { + err = error(&errortypes.BadInput{Message: fmt.Sprint("'bidid' is required field and can't be empty")}) + return req, err + } + + if bcr.Bidder == "" { + err = error(&errortypes.BadInput{Message: fmt.Sprint("'bidder' is required field and can't be empty")}) + return req, err + } + } + + return req, nil +} + +// handleVTrackRequest handles a VTrack request +func (v *vtrackEndpoint) handleVTrackRequest(ctx context.Context, req *BidCacheRequest, account *config.Account) (*BidCacheResponse, []error) { + biddersAllowingVastUpdate := getBiddersAllowingVastUpdate(req, &v.BidderInfos, v.Cfg.VTrack.AllowUnknownBidder) + // cache data + r, errs := v.cachePutObjects(ctx, req, biddersAllowingVastUpdate, account.ID) + + // handle pbs caching errors + if len(errs) != 0 { + glog.Errorf("Error(s) updating vast: %v", errs) + return nil, errs + } + + // build response + response := &BidCacheResponse{ + Responses: []CacheObject{}, + } + + for _, uuid := range r { + response.Responses = append(response.Responses, CacheObject{ + UUID: uuid, + }) + } + + return response, nil +} + +// cachePutObjects caches BidCacheRequest data +func (v *vtrackEndpoint) cachePutObjects(ctx context.Context, req *BidCacheRequest, biddersAllowingVastUpdate map[string]struct{}, accountId string) ([]string, []error) { + var cacheables []prebid_cache_client.Cacheable + + for _, c := range req.Puts { + + nc := &prebid_cache_client.Cacheable{ + Type: c.Type, + Data: c.Data, + TTLSeconds: c.TTLSeconds, + Key: c.Key, + } + + if _, ok := biddersAllowingVastUpdate[c.Bidder]; ok && nc.Data != nil { + nc.Data = modifyVastXml(v.Cfg.ExternalURL, nc.Data, c.BidID, c.Bidder, accountId, c.Timestamp) + } + + cacheables = append(cacheables, *nc) + } + + return v.Cache.PutJson(ctx, cacheables) +} + +// getBiddersAllowingVastUpdate returns a list of bidders that allow VAST XML modification +func getBiddersAllowingVastUpdate(req *BidCacheRequest, bidderInfos *adapters.BidderInfos, allowUnknownBidder bool) map[string]struct{} { + bl := map[string]struct{}{} + + for _, bcr := range req.Puts { + if _, ok := bl[bcr.Bidder]; isAllowVastForBidder(bcr.Bidder, bidderInfos, allowUnknownBidder) && !ok { + bl[bcr.Bidder] = struct{}{} + } + } + + return bl +} + +// isAllowVastForBidder checks if a bidder is active and allowed to modify vast xml data +func isAllowVastForBidder(bidder string, bidderInfos *adapters.BidderInfos, allowUnknownBidder bool) bool { + //if bidder is active and isModifyingVastXmlAllowed is true + // check if bidder is configured + if b, ok := (*bidderInfos)[bidder]; bidderInfos != nil && ok { + // check if bidder is enabled + return b.Status == adapters.StatusActive && b.ModifyingVastXmlAllowed + } + + return allowUnknownBidder +} + +// getAccountId extracts an account id from an HTTP Request +func getAccountId(httpRequest *http.Request) string { + return httpRequest.URL.Query().Get(AccountParameter) +} + +// modifyVastXml modifies BidCacheRequest element Vast XML data +func modifyVastXml(externalUrl string, data json.RawMessage, bidid string, bidder string, accountId string, timestamp int64) json.RawMessage { + c := string(data) + ci := strings.Index(c, ImpressionCloseTag) + + // no impression tag - pass it as it is + if ci == -1 { + return data + } + + vastUrlTracking := GetVastUrlTracking(externalUrl, bidid, bidder, accountId, timestamp) + impressionUrl := "" + oi := strings.Index(c, ImpressionOpenTag) + + if ci-oi == len(ImpressionOpenTag) { + return json.RawMessage(strings.Replace(c, ImpressionOpenTag, ImpressionOpenTag+impressionUrl, 1)) + } + + return json.RawMessage(strings.Replace(c, ImpressionCloseTag, ImpressionCloseTag+ImpressionOpenTag+impressionUrl+ImpressionCloseTag, 1)) +} + +func contains(s []string, e string) bool { + if len(s) == 0 { + return false + } + + i := sort.SearchStrings(s, e) + return i < len(s) && s[i] == e +} diff --git a/endpoints/events/vtrack_test.go b/endpoints/events/vtrack_test.go new file mode 100644 index 00000000000..52665e7736d --- /dev/null +++ b/endpoints/events/vtrack_test.go @@ -0,0 +1,692 @@ +package events + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http/httptest" + "strings" + "testing" +) + +const ( + maxSize = 1024 * 256 + + vastXmlWithImpressionWithContent = "prebid.org wrappercontent" + vastXmlWithImpressionWithoutContent = "prebid.org wrapper" + vastXmlWithoutImpression = "prebid.org wrapper" +) + +// Mock pbs cache client +type vtrackMockCacheClient struct { + Fail bool + Error error + Uuids []string +} + +func (m *vtrackMockCacheClient) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) { + if m.Fail { + return []string{}, []error{m.Error} + } + return m.Uuids, []error{} +} +func (m *vtrackMockCacheClient) GetExtCacheData() (scheme string, host string, path string) { + return +} + +// Test +func TestShouldRespondWithBadRequestWhenAccountParameterIsMissing(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{} + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // mock config + cfg := &config.Configuration{ + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "" + + req := httptest.NewRequest("POST", "/vtrack", strings.NewReader(reqData)) + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with missing account parameter") + assert.Equal(t, "Account 'a' is required query parameter and can't be empty", string(d)) +} + +func TestShouldRespondWithBadRequestWhenRequestBodyIsEmpty(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{} + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "" + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(reqData)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with empty body") + assert.Equal(t, "Invalid request: request body is empty\n", string(d)) +} + +func TestShouldRespondWithBadRequestWhenRequestBodyIsInvalid(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{} + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + reqData := "invalid" + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(reqData)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid body") +} + +func TestShouldRespondWithBadRequestWhenBidIdIsMissing(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{} + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + data := &BidCacheRequest{ + Puts: []prebid_cache_client.Cacheable{ + {}, + }, + } + + reqData, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(string(reqData))) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with elements missing bidid") + assert.Equal(t, "Invalid request: 'bidid' is required field and can't be empty\n", string(d)) +} + +func TestShouldRespondWithBadRequestWhenBidderIsMissing(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{} + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + data := &BidCacheRequest{ + Puts: []prebid_cache_client.Cacheable{ + { + BidID: "test", + }, + }, + } + + reqData, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(string(reqData))) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with elements missing bidder") + assert.Equal(t, "Invalid request: 'bidder' is required field and can't be empty\n", string(d)) +} + +func TestShouldRespondWithInternalServerErrorWhenPbsCacheClientFails(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{ + Fail: true, + Error: fmt.Errorf("pbs cache client failed"), + } + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{} + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: true, + }, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + data, err := getValidVTrackRequestBody(false, false) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 500, recorder.Result().StatusCode, "Expected 500 when pbs cache client fails") + assert.Equal(t, "Error(s) updating vast: pbs cache client failed\n", string(d)) +} + +func TestShouldTolerateAccountNotFound(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{} + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: true, + Error: stored_requests.NotFoundError{}, + } + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: false, + }, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + data, err := getValidVTrackRequestBody(true, false) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=1235", strings.NewReader(data)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + // validate + assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid") + assert.Equal(t, "application/json", recorder.Header().Get("Content-Type")) +} + +func TestShouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastNotAllowed(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{ + Fail: false, + Uuids: []string{"uuid1"}, + } + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: false, + }, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // bidder info + bidderInfos := make(adapters.BidderInfos) + bidderInfos["bidder"] = adapters.BidderInfo{ + Status: adapters.StatusActive, + ModifyingVastXmlAllowed: false, + } + bidderInfos["updatable_bidder"] = adapters.BidderInfo{ + Status: adapters.StatusActive, + ModifyingVastXmlAllowed: true, + } + + // prepare + data, err := getValidVTrackRequestBody(false, false) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: bidderInfos, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid") + assert.Equal(t, "{\"responses\":[{\"uuid\":\"uuid1\"}]}", string(d), "Expected 200 when account is found and request is valid") + assert.Equal(t, "application/json", recorder.Header().Get("Content-Type")) +} + +func TestShouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastAllowed(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{ + Fail: false, + Uuids: []string{"uuid1", "uuid2"}, + } + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: false, + }, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // bidder info + bidderInfos := make(adapters.BidderInfos) + bidderInfos["bidder"] = adapters.BidderInfo{ + Status: adapters.StatusActive, + ModifyingVastXmlAllowed: true, + } + bidderInfos["updatable_bidder"] = adapters.BidderInfo{ + Status: adapters.StatusActive, + ModifyingVastXmlAllowed: true, + } + + // prepare + data, err := getValidVTrackRequestBody(true, true) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: bidderInfos, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid") + assert.Equal(t, "{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}", string(d), "Expected 200 when account is found and request is valid") + assert.Equal(t, "application/json", recorder.Header().Get("Content-Type")) +} + +func TestShouldSendToCacheExpectedPutsAndUpdatableUnknownBiddersWhenUnknownBidderIsAllowed(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{ + Fail: false, + Uuids: []string{"uuid1", "uuid2"}, + } + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: true, + }, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // bidder info + bidderInfos := make(adapters.BidderInfos) + + // prepare + data, err := getValidVTrackRequestBody(true, false) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: bidderInfos, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid") + assert.Equal(t, "{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}", string(d), "Expected 200 when account is found, request has unknown bidders but allowUnknownBidders is enabled") + assert.Equal(t, "application/json", recorder.Header().Get("Content-Type")) +} + +func TestShouldReturnBadRequestWhenRequestExceedsMaxRequestSize(t *testing.T) { + // mock pbs cache client + mockCacheClient := &vtrackMockCacheClient{ + Fail: false, + Uuids: []string{"uuid1", "uuid2"}, + } + + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // config + cfg := &config.Configuration{ + MaxRequestSize: 1, + VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: true, + }, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // bidder info + bidderInfos := make(adapters.BidderInfos) + + // prepare + data, err := getValidVTrackRequestBody(true, false) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data)) + + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: bidderInfos, + Cache: mockCacheClient, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 when request exceeds max request size") + assert.Equal(t, "Invalid request: request size exceeded max size of 1 bytes\n", string(d)) +} + +func TestShouldRespondWithInternalErrorPbsCacheIsNotConfigured(t *testing.T) { + // mock AccountsFetcher + mockAccountsFetcher := &mockAccountsFetcher{ + Fail: false, + } + + // config + cfg := &config.Configuration{ + MaxRequestSize: maxSize, VTrack: config.VTrack{ + TimeoutMS: int64(2000), AllowUnknownBidder: false, + }, + AccountDefaults: config.Account{}, + } + cfg.MarshalAccountDefaults() + + // prepare + data, err := getValidVTrackRequestBody(true, true) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data)) + recorder := httptest.NewRecorder() + + e := vtrackEndpoint{ + Cfg: cfg, + BidderInfos: nil, + Cache: nil, + Accounts: mockAccountsFetcher, + } + + // execute + e.Handle(recorder, req, nil) + + d, err := ioutil.ReadAll(recorder.Result().Body) + if err != nil { + t.Fatal(err) + } + + // validate + assert.Equal(t, 500, recorder.Result().StatusCode, "Expected 500 when pbs cache is not configured") + assert.Equal(t, "PBS Cache client is not configured", string(d)) +} + +func TestVastUrlShouldReturnExpectedUrl(t *testing.T) { + url := GetVastUrlTracking("http://external-url", "bidId", "bidder", "accountId", 1000) + assert.Equal(t, "http://external-url/event?t=imp&b=bidId&a=accountId&bidder=bidder&f=b&ts=1000", url, "Invalid vast url") +} + +func getValidVTrackRequestBody(withImpression bool, withContent bool) (string, error) { + d, e := getVTrackRequestData(withImpression, withContent) + + if e != nil { + return "", e + } + + req := &BidCacheRequest{ + Puts: []prebid_cache_client.Cacheable{ + { + Type: prebid_cache_client.TypeXML, + BidID: "bidId1", + Bidder: "bidder", + Data: d, + TTLSeconds: 3600, + Timestamp: 1000, + }, + { + Type: prebid_cache_client.TypeXML, + BidID: "bidId2", + Bidder: "updatable_bidder", + Data: d, + TTLSeconds: 3600, + Timestamp: 1000, + }, + }, + } + + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + + e = enc.Encode(req) + + return buf.String(), e +} + +func getVTrackRequestData(wi bool, wic bool) (db []byte, e error) { + data := &bytes.Buffer{} + enc := json.NewEncoder(data) + enc.SetEscapeHTML(false) + + if wi && wic { + e = enc.Encode(vastXmlWithImpressionWithContent) + return data.Bytes(), e + } else if wi { + e = enc.Encode(vastXmlWithImpressionWithoutContent) + } else { + enc.Encode(vastXmlWithoutImpression) + } + + return data.Bytes(), e +} diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index cb36528417b..cd38e4c95ef 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -13,6 +13,7 @@ import ( "time" "github.com/PubMatic-OpenWrap/openrtb" + accountService "github.com/PubMatic-OpenWrap/prebid-server/account" "github.com/PubMatic-OpenWrap/prebid-server/analytics" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/errortypes" @@ -20,9 +21,12 @@ 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" + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" "github.com/buger/jsonparser" "github.com/golang/glog" "github.com/julienschmidt/httprouter" @@ -43,7 +47,7 @@ func NewAmpEndpoint( ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, - categories stored_requests.CategoryFetcher, + accounts stored_requests.AccountFetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, @@ -52,18 +56,23 @@ func NewAmpEndpoint( bidderMap map[string]openrtb_ext.BidderName, ) (httprouter.Handle, error) { - if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { + if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewAmpEndpoint requires non-nil arguments.") } defRequest := defReqJSON != nil && len(defReqJSON) > 0 + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + return httprouter.Handle((&endpointDeps{ ex, validator, requestsById, empty_fetcher.EmptyFetcher{}, - categories, + accounts, cfg, met, pbsAnalytics, @@ -72,7 +81,8 @@ func NewAmpEndpoint( defReqJSON, bidderMap, nil, - nil}).AmpAuction), nil + nil, + ipValidator}).AmpAuction), nil } @@ -132,6 +142,8 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h return } + ao.Request = req + ctx := context.Background() var cancel context.CancelFunc if req.TMax > 0 { @@ -147,26 +159,39 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h } else { labels.CookieFlag = pbsmetrics.CookieFlagYes } - labels.PubID = effectivePubID(req.Site.Publisher) - // Blacklist account now that we have resolved the value - if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL = append(errL, acctIdErr) - errCode := errortypes.ReadCode(acctIdErr) - if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.BlacklistedAcctErrorCode { - w.WriteHeader(http.StatusServiceUnavailable) - labels.RequestStatus = pbsmetrics.RequestStatusBlacklisted - } else { - w.WriteHeader(http.StatusBadRequest) - labels.RequestStatus = pbsmetrics.RequestStatusBadInput + labels.PubID = getAccountID(req.Site.Publisher) + // Look up account now that we have resolved the pubID value + account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID) + if len(acctIDErrs) > 0 { + errL = append(errL, acctIDErrs...) + httpStatus := http.StatusBadRequest + metricsStatus := pbsmetrics.RequestStatusBadInput + for _, er := range errL { + errCode := errortypes.ReadCode(er) + if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.BlacklistedAcctErrorCode { + httpStatus = http.StatusServiceUnavailable + metricsStatus = pbsmetrics.RequestStatusBlacklisted + break + } } + w.WriteHeader(httpStatus) + labels.RequestStatus = metricsStatus for _, err := range errortypes.FatalOnly(errL) { w.Write([]byte(fmt.Sprintf("Invalid request format: %s\n", err.Error()))) } - ao.Errors = append(ao.Errors, acctIdErr) + ao.Errors = append(ao.Errors, acctIDErrs...) return } - response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories, nil) + auctionRequest := exchange.AuctionRequest{ + BidRequest: req, + Account: *account, + UserSyncs: usersyncs, + RequestType: labels.RType, + LegacyLabels: labels, + } + + response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) ao.AuctionResponse = response if err != nil { @@ -381,22 +406,19 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope setAmpExt(req.Site, "1") + setEffectiveAmpPubID(req, httpRequest.URL.Query()) + slot := httpRequest.FormValue("slot") if slot != "" { req.Imp[0].TagID = slot } - 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), - }} - } + policyWriter, policyWriterErr := readPolicyFromUrl(httpRequest.URL) + if policyWriterErr != nil { + return []error{policyWriterErr} + } + if err := policyWriter.Write(req); err != nil { + return []error{err} } if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil { @@ -541,7 +563,27 @@ func setAmpExt(site *openrtb.Site, value string) { } } -func readConsent(url *url.URL) string { +func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) { + consent := readConsentFromURL(url) + + if len(consent) == 0 { + return privacy.NilPolicyWriter{}, nil + } + + if gdpr.ValidateConsent(consent) { + return gdpr.ConsentWriter{consent}, nil + } + + if ccpa.ValidateConsent(consent) { + return ccpa.ConsentWriter{consent}, nil + } + + return privacy.NilPolicyWriter{}, &errortypes.InvalidPrivacyConsent{ + Message: fmt.Sprintf("Consent '%s' is not recognized as either CCPA or GDPR TCF.", consent), + } +} + +func readConsentFromURL(url *url.URL) string { if v := url.Query().Get("consent_string"); v != "" { return v } @@ -549,3 +591,27 @@ func readConsent(url *url.URL) string { // Fallback to 'gdpr_consent' for compatability until it's no longer used by AMP. return url.Query().Get("gdpr_consent") } + +// Sets the effective publisher ID for amp request +func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) { + var pub *openrtb.Publisher + if req.App != nil { + if req.App.Publisher == nil { + req.App.Publisher = new(openrtb.Publisher) + } + pub = req.App.Publisher + } else if req.Site != nil { + if req.Site.Publisher == nil { + req.Site.Publisher = new(openrtb.Publisher) + } + pub = req.Site.Publisher + } + + if pub.ID == "" { + // For amp requests, the publisher ID could be sent via the account + // query string + if acc := urlQueryParams.Get("account"); acc != "" && acc != "ACCOUNT_ID" { + pub.ID = acc + } + } +} diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 259992dbe20..3ec5d477c22 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -11,7 +11,7 @@ import ( "strconv" "testing" - "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + "github.com/PubMatic-OpenWrap/prebid-server/analytics" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/empty_fetcher" "github.com/PubMatic-OpenWrap/openrtb" @@ -755,8 +755,9 @@ func TestQueryParamOverrides(t *testing.T) { curl := "http://example.com" slot := "1234" timeout := int64(500) + account := "12345" - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&debug=1&curl=%s&slot=%s&timeout=%d", requestID, curl, slot, timeout), nil) + request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s&debug=1&curl=%s&slot=%s&timeout=%d&account=%s", requestID, curl, slot, timeout, account), nil) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -784,6 +785,10 @@ func TestQueryParamOverrides(t *testing.T) { if resolvedRequest.Site == nil || resolvedRequest.Site.Page != curl { t.Errorf("Expected Site.Page to equal curl (%s), got: %s", curl, resolvedRequest.Site.Page) } + + if resolvedRequest.Site == nil || resolvedRequest.Site.Publisher == nil || resolvedRequest.Site.Publisher.ID != account { + t.Errorf("Expected Site.Publisher.ID to equal (%s), got: %s", account, resolvedRequest.Site.Publisher.ID) + } } func TestOverrideDimensions(t *testing.T) { @@ -876,6 +881,7 @@ type formatOverrideSpec struct { overrideWidth uint64 overrideHeight uint64 multisize string + account string expect []openrtb.Format } @@ -897,7 +903,7 @@ func (s formatOverrideSpec) execute(t *testing.T) { openrtb_ext.BidderMap, ) - url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize) + url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s&account=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize, s.account) request := httptest.NewRequest("GET", url, nil) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -946,8 +952,8 @@ var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBi }, } -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 +func (m *mockAmpExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + m.lastRequest = r.BidRequest response := &openrtb.BidResponse{ SeatBid: []openrtb.SeatBid{{ @@ -959,8 +965,8 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.B Ext: json.RawMessage(`{ "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`), } - if bidRequest.Test == 1 { - resolvedRequest, err := json.Marshal(bidRequest) + if r.BidRequest.Test == 1 { + resolvedRequest, err := json.Marshal(r.BidRequest) if err != nil { resolvedRequest = json.RawMessage("{}") } @@ -1036,3 +1042,243 @@ func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, return json.Marshal(bidRequest) } + +func TestSetEffectiveAmpPubID(t *testing.T) { + testPubID := "test-pub" + testURLQueryParams := url.Values{} + testURLQueryParams.Add("account", testPubID) + + testCases := []struct { + description string + req *openrtb.BidRequest + urlQueryParams url.Values + expectedPubID string + }{ + { + description: "No publisher ID provided", + req: &openrtb.BidRequest{ + App: &openrtb.App{ + Publisher: nil, + }, + }, + expectedPubID: "", + }, + { + description: "Publisher ID present in req.App.Publisher.ID", + req: &openrtb.BidRequest{ + App: &openrtb.App{ + Publisher: &openrtb.Publisher{ + ID: testPubID, + }, + }, + }, + expectedPubID: testPubID, + }, + { + description: "Publisher ID present in req.Site.Publisher.ID", + req: &openrtb.BidRequest{ + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: testPubID, + }, + }, + }, + expectedPubID: testPubID, + }, + { + description: "Publisher ID present in account query parameter", + req: &openrtb.BidRequest{ + App: &openrtb.App{ + Publisher: &openrtb.Publisher{ + ID: "", + }, + }, + }, + urlQueryParams: testURLQueryParams, + expectedPubID: testPubID, + }, + { + description: "req.Site.Publisher present but ID set to empty string", + req: &openrtb.BidRequest{ + Site: &openrtb.Site{ + Publisher: &openrtb.Publisher{ + ID: "", + }, + }, + }, + expectedPubID: "", + }, + } + + for _, test := range testCases { + setEffectiveAmpPubID(test.req, test.urlQueryParams) + if test.req.Site != nil { + assert.Equal(t, test.expectedPubID, test.req.Site.Publisher.ID, + "should return the expected Publisher ID for test case: %s", test.description) + } else { + assert.Equal(t, test.expectedPubID, test.req.App.Publisher.ID, + "should return the expected Publisher ID for test case: %s", test.description) + } + } +} + +type mockLogger struct { + ampObject *analytics.AmpObject +} + +func newMockLogger(ao *analytics.AmpObject) analytics.PBSAnalyticsModule { + return &mockLogger{ + ampObject: ao, + } +} + +func (logger mockLogger) LogAuctionObject(ao *analytics.AuctionObject) { + return +} +func (logger mockLogger) LogVideoObject(vo *analytics.VideoObject) { + return +} +func (logger mockLogger) LogCookieSyncObject(cookieObject *analytics.CookieSyncObject) { + return +} +func (logger mockLogger) LogSetUIDObject(uuidObj *analytics.SetUIDObject) { + return +} +func (logger mockLogger) LogNotificationEventObject(uuidObj *analytics.NotificationEvent) { + return +} +func (logger mockLogger) LogAmpObject(ao *analytics.AmpObject) { + *logger.ampObject = *ao +} + +func TestBuildAmpObject(t *testing.T) { + testCases := []struct { + description string + inTagId string + inStoredRequest json.RawMessage + expectedAmpObject *analytics.AmpObject + }{ + { + description: "Stored Amp request with nil body. Only the error gets logged", + inTagId: "test", + inStoredRequest: nil, + expectedAmpObject: &analytics.AmpObject{ + Status: http.StatusOK, + Errors: []error{fmt.Errorf("unexpected end of JSON input")}, + }, + }, + { + description: "Stored Amp request with no imps that should return error. Only the error gets logged", + inTagId: "test", + inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[],"tmax":500}`), + expectedAmpObject: &analytics.AmpObject{ + Status: http.StatusOK, + Errors: []error{fmt.Errorf("data for tag_id='test' does not define the required imp array")}, + }, + }, + { + description: "Wrong tag_id, error gets logged", + inTagId: "unknown", + inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":12883451}}}],"tmax":500}`), + expectedAmpObject: &analytics.AmpObject{ + Status: http.StatusOK, + Errors: []error{fmt.Errorf("unexpected end of JSON input")}, + }, + }, + { + description: "Valid stored Amp request, correct tag_id, a valid response should be logged", + inTagId: "test", + inStoredRequest: json.RawMessage(`{"id":"some-request-id","site":{"page":"prebid.org"},"imp":[{"id":"some-impression-id","banner":{"format":[{"w":300,"h":250}]},"ext":{"appnexus":{"placementId":12883451}}}],"tmax":500}`), + expectedAmpObject: &analytics.AmpObject{ + Status: http.StatusOK, + Errors: nil, + Request: &openrtb.BidRequest{ + ID: "some-request-id", + Device: &openrtb.Device{ + IP: "192.0.2.1", + }, + Site: &openrtb.Site{ + Page: "prebid.org", + Publisher: &openrtb.Publisher{}, + Ext: json.RawMessage(`{"amp":1}`), + }, + Imp: []openrtb.Imp{ + { + ID: "some-impression-id", + Banner: &openrtb.Banner{ + Format: []openrtb.Format{ + { + W: 300, + H: 250, + }, + }, + }, + Secure: func(val int8) *int8 { return &val }(1), //(*int8)(1), + Ext: json.RawMessage(`{"appnexus":{"placementId":12883451}}`), + }, + }, + AT: 1, + TMax: 500, + Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":null},"vastxml":null},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false}}}`), + }, + AuctionResponse: &openrtb.BidResponse{ + SeatBid: []openrtb.SeatBid{{ + Bid: []openrtb.Bid{{ + AdM: "", + Ext: json.RawMessage(`{ "prebid": {"targeting": { "hb_pb": "1.20", "hb_appnexus_pb": "1.20", "hb_cache_id": "some_id"}}}`), + }}, + Seat: "", + }}, + Ext: json.RawMessage(`{ "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`), + }, + AmpTargetingValues: map[string]string{ + "hb_appnexus_pb": "1.20", + "hb_cache_id": "some_id", + "hb_pb": "1.20", + }, + Origin: "", + }, + }, + } + + request := httptest.NewRequest("GET", "/openrtb2/auction/amp?tag_id=test", nil) + recorder := httptest.NewRecorder() + + for _, test := range testCases { + + // Set up test, declare a new mock logger every time + actualAmpObject := new(analytics.AmpObject) + + logger := newMockLogger(actualAmpObject) + + mockAmpFetcher := &mockAmpStoredReqFetcher{ + data: map[string]json.RawMessage{ + test.inTagId: json.RawMessage(test.inStoredRequest), + }, + } + + endpoint, _ := NewAmpEndpoint( + &mockAmpExchange{}, + newParamsValidator(t), + mockAmpFetcher, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), + logger, + map[string]string{}, + []byte{}, + openrtb_ext.BidderMap, + ) + + // Run test + endpoint(recorder, request, nil) + + // assert AmpObject + assert.Equalf(t, test.expectedAmpObject.Status, actualAmpObject.Status, "Amp Object Status field doesn't match expected: %s\n", test.description) + assert.Lenf(t, actualAmpObject.Errors, len(test.expectedAmpObject.Errors), "Amp Object Errors array doesn't match expected: %s\n", test.description) + assert.Equalf(t, test.expectedAmpObject.Request, actualAmpObject.Request, "Amp Object BidRequest doesn't match expected: %s\n", test.description) + assert.Equalf(t, test.expectedAmpObject.AuctionResponse, actualAmpObject.AuctionResponse, "Amp Object BidResponse doesn't match expected: %s\n", test.description) + assert.Equalf(t, test.expectedAmpObject.AmpTargetingValues, actualAmpObject.AmpTargetingValues, "Amp Object AmpTargetingValues doesn't match expected: %s\n", test.description) + assert.Equalf(t, test.expectedAmpObject.Origin, actualAmpObject.Origin, "Amp Object Origin field doesn't match expected: %s\n", test.description) + } +} diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index f8552666bc3..73bfa410441 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -16,20 +16,23 @@ import ( "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/openrtb/native" nativeRequests "github.com/PubMatic-OpenWrap/openrtb/native/request" + accountService "github.com/PubMatic-OpenWrap/prebid-server/account" "github.com/PubMatic-OpenWrap/prebid-server/analytics" "github.com/PubMatic-OpenWrap/prebid-server/config" "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" - "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" "github.com/PubMatic-OpenWrap/prebid-server/usersync" + "github.com/PubMatic-OpenWrap/prebid-server/util/httputil" + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" "github.com/buger/jsonparser" jsonpatch "github.com/evanphx/json-patch" + "github.com/gofrs/uuid" "github.com/golang/glog" "github.com/julienschmidt/httprouter" "github.com/mssola/user_agent" @@ -38,19 +41,31 @@ import ( const storedRequestTimeoutMillis = 50 -func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById 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) { +var ( + dntKey string = http.CanonicalHeaderKey("DNT") + dntDisabled int8 = 0 + dntEnabled int8 = 1 +) + +func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, accounts stored_requests.AccountFetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, disabledBidders map[string]string, defReqJSON []byte, bidderMap map[string]openrtb_ext.BidderName) (httprouter.Handle, error) { - if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil { + if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewEndpoint requires non-nil arguments.") } + defRequest := defReqJSON != nil && len(defReqJSON) > 0 + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + return httprouter.Handle((&endpointDeps{ ex, validator, requestsById, empty_fetcher.EmptyFetcher{}, - categories, + accounts, cfg, met, pbsAnalytics, @@ -59,24 +74,26 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato defReqJSON, bidderMap, nil, - nil}).Auction), nil + nil, + ipValidator}).Auction), nil } type endpointDeps struct { - ex exchange.Exchange - paramsValidator openrtb_ext.BidderParamValidator - storedReqFetcher stored_requests.Fetcher - videoFetcher stored_requests.Fetcher - categories stored_requests.CategoryFetcher - cfg *config.Configuration - metricsEngine pbsmetrics.MetricsEngine - analytics analytics.PBSAnalyticsModule - disabledBidders map[string]string - defaultRequest bool - defReqJSON []byte - bidderMap map[string]openrtb_ext.BidderName - cache prebid_cache_client.Client - debugLogRegexp *regexp.Regexp + ex exchange.Exchange + paramsValidator openrtb_ext.BidderParamValidator + storedReqFetcher stored_requests.Fetcher + videoFetcher stored_requests.Fetcher + accounts stored_requests.AccountFetcher + cfg *config.Configuration + metricsEngine pbsmetrics.MetricsEngine + analytics analytics.PBSAnalyticsModule + disabledBidders map[string]string + defaultRequest bool + defReqJSON []byte + bidderMap map[string]openrtb_ext.BidderName + cache prebid_cache_client.Client + debugLogRegexp *regexp.Regexp + privateNetworkIPValidator iputil.IPValidator } func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { @@ -126,7 +143,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http if req.App != nil { labels.Source = pbsmetrics.DemandApp labels.RType = pbsmetrics.ReqTypeORTB2App - labels.PubID = effectivePubID(req.App.Publisher) + labels.PubID = getAccountID(req.App.Publisher) } else { //req.Site != nil labels.Source = pbsmetrics.DemandWeb if usersyncs.LiveSyncCount() == 0 { @@ -134,18 +151,29 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http } else { labels.CookieFlag = pbsmetrics.CookieFlagYes } - labels.PubID = effectivePubID(req.Site.Publisher) + labels.PubID = getAccountID(req.Site.Publisher) } - if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL = append(errL, acctIdErr) + // Look up account now that we have resolved the pubID value + account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID) + if len(acctIDErrs) > 0 { + errL = append(errL, acctIDErrs...) writeError(errL, w, &labels) return } - response, err := deps.ex.HoldAuction(ctx, req, usersyncs, labels, &deps.categories, nil) + auctionRequest := exchange.AuctionRequest{ + BidRequest: req, + Account: *account, + UserSyncs: usersyncs, + RequestType: labels.RType, + LegacyLabels: labels, + } + + response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) ao.Request = req ao.Response = response + ao.Account = account if err != nil { labels.RequestStatus = pbsmetrics.RequestStatusErr w.WriteHeader(http.StatusInternalServerError) @@ -268,6 +296,14 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { errL = append(errL, &errortypes.Warning{Message: fmt.Sprintf("A prebid request can only process one currency. Taking the first currency in the list, %s, as the active currency", req.Cur[0])}) } + // If automatically filling source TID is enabled then validate that + // source.TID exists and If it doesn't, fill it with a randomly generated UUID + if deps.cfg.AutoGenSourceTID { + if err := validateAndFillSourceTID(req); err != nil { + return []error{err} + } + } + var aliases map[string]string if bidExt, err := deps.parseBidExt(req.Ext); err != nil { return []error{err} @@ -281,45 +317,43 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { if err := validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil { return []error{err} } + + if err := validateSChains(bidExt); err != nil { + return []error{err} + } } if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) { - errL = append(errL, errors.New("request.site or request.app must be defined, but not both.")) - return errL + return append(errL, errors.New("request.site or request.app must be defined, but not both.")) } if err := deps.validateSite(req.Site); err != nil { - errL = append(errL, err) - return errL + return append(errL, err) } if err := deps.validateApp(req.App); err != nil { - errL = append(errL, err) - return errL + return append(errL, err) } if err := validateUser(req.User, aliases); err != nil { - errL = append(errL, err) - return errL + return append(errL, err) } if err := validateRegs(req.Regs); err != nil { - errL = append(errL, err) - return errL - } - - ccpaPolicy, ccpaPolicyErr := ccpa.ReadPolicy(req) - if ccpaPolicyErr != nil { - errL = append(errL, ccpaPolicyErr) - return errL + return append(errL, err) } - if err := ccpaPolicy.Validate(); err != nil { - 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)) + if ccpaPolicy, err := ccpa.ReadFromRequest(req); err != nil { + return append(errL, err) + } else if _, err := ccpaPolicy.Parse(exchange.GetValidBidders(aliases)); err != nil { + if _, invalidConsent := err.(*errortypes.InvalidPrivacyConsent); invalidConsent { + errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)}) + consentWriter := ccpa.ConsentWriter{""} + if err := consentWriter.Write(req); err != nil { + return append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err)) + } + } else { + return append(errL, err) } } @@ -342,6 +376,20 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error { return errL } +func validateAndFillSourceTID(req *openrtb.BidRequest) error { + if req.Source == nil { + req.Source = &openrtb.Source{} + } + if req.Source.TID == "" { + if rawUUID, err := uuid.NewV4(); err == nil { + req.Source.TID = rawUUID.String() + } else { + return errors.New("req.Source.TID missing in the req and error creating a random UID") + } + } + return nil +} + func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases map[string]string) error { for bidderToAdjust, adjustmentFactor := range adjustmentFactors { if adjustmentFactor <= 0 { @@ -356,6 +404,11 @@ func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases return nil } +func validateSChains(req *openrtb_ext.ExtRequest) error { + _, err := exchange.BidderToPrebidSChains(req) + return err +} + func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]string, index int) []error { if imp.ID == "" { return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)} @@ -716,8 +769,8 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st } // Also accept bidder exts within imp[...].ext.prebid.bidder - // 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} + // NOTE: This is not part of the official API yet, so we are not expecting clients + // to migrate from imp[...].ext.${BIDDER} to imp[...].ext.prebid.bidder.${BIDDER} // at this time // https://github.com/PubMatic-OpenWrap/prebid-server/pull/846#issuecomment-476352224 if rawPrebidExt, ok := bidderExts[openrtb_ext.PrebidExtKey]; ok { @@ -736,8 +789,9 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st /* Process all the bidder exts in the request */ disabledBidders := []string{} validationFailedBidders := []string{} + otherExtElements := 0 for bidder, ext := range bidderExts { - if bidder != openrtb_ext.PrebidExtKey { + if isBidderToValidate(bidder) { coreBidder := bidder if tmp, isAlias := aliases[bidder]; isAlias { coreBidder = tmp @@ -757,6 +811,8 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st return []error{fmt.Errorf("request.imp[%d].ext contains unknown bidder: %s. Did you forget an alias in request.ext.prebid.aliases?", impIndex, bidder)} } } + } else { + otherExtElements++ } } @@ -773,21 +829,30 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st } } - extJSON, err := json.Marshal(bidderExts) - if err != nil { - return []error{err} + if len(disabledBidders) > 0 || len(validationFailedBidders) > 0 { + extJSON, err := json.Marshal(bidderExts) + if err != nil { + return []error{err} + } + imp.Ext = extJSON } - imp.Ext = extJSON - // TODO #713 Fix this here - if len(bidderExts) < 1 { - errL = append(errL, fmt.Errorf("request.imp[%d].ext must contain at least one bidder with valid parameters", impIndex)) - return errL + if len(bidderExts)-otherExtElements == 0 { + errL = append(errL, fmt.Errorf("request.imp[%d].ext must contain at least one bidder", impIndex)) } return errL } +func isBidderToValidate(bidder string) bool { + // PrebidExtKey is a special case for the prebid config section and is not considered a bidder. + + // FirstPartyDataContextExtKey is a special case for the first party data context section + // and is not considered a bidder. + + return bidder != openrtb_ext.PrebidExtKey && bidder != openrtb_ext.FirstPartyDataContextExtKey +} + func (deps *endpointDeps) parseBidExt(ext json.RawMessage) (*openrtb_ext.ExtRequest, error) { if len(ext) < 1 { return nil, nil @@ -925,13 +990,27 @@ func validateRegs(regs *openrtb.Regs) error { return nil } +func sanitizeRequest(r *openrtb.BidRequest, ipValidator iputil.IPValidator) { + if r.Device != nil { + if ip, ver := iputil.ParseIP(r.Device.IP); ip == nil || ver != iputil.IPv4 || !ipValidator.IsValid(ip, ver) { + r.Device.IP = "" + } + + if ip, ver := iputil.ParseIP(r.Device.IPv6); ip == nil || ver != iputil.IPv6 || !ipValidator.IsValid(ip, ver) { + r.Device.IPv6 = "" + } + } +} + // setFieldsImplicitly uses _implicit_ information from the httpReq to set values on bidReq. // This function does not consume the request body, which was set explicitly, but infers certain // OpenRTB properties from the headers and other implicit info. // // This function _should not_ override any fields which were defined explicitly by the caller in the request. func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { - setDeviceImplicitly(httpReq, bidReq) + sanitizeRequest(bidReq, deps.privateNetworkIPValidator) + + setDeviceImplicitly(httpReq, bidReq, deps.privateNetworkIPValidator) // Per the OpenRTB spec: A bid request must not contain both a Site and an App object. if bidReq.App == nil { @@ -943,9 +1022,11 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *ope } // setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device -func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { - setIPImplicitly(httpReq, bidReq) // Fixes #230 +func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidtor iputil.IPValidator) { + setIPImplicitly(httpReq, bidReq, ipValidtor) setUAImplicitly(httpReq, bidReq) + setDoNotTrackImplicitly(httpReq, bidReq) + } // setAuctionTypeImplicitly sets the auction type to 1 if it wasn't on the request, @@ -986,7 +1067,7 @@ func setSiteImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { func setImpsImplicitly(httpReq *http.Request, imps []openrtb.Imp) { secure := int8(1) for i := 0; i < len(imps); i++ { - if imps[i].Secure == nil && prebid.IsSecure(httpReq) { + if imps[i].Secure == nil && httputil.IsSecure(httpReq) { imps[i].Secure = &secure } } @@ -1143,13 +1224,21 @@ func getStoredRequestId(data []byte) (string, bool, error) { } // setIPImplicitly sets the IP address on bidReq, if it's not explicitly defined and we can figure it out. -func setIPImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { - if bidReq.Device == nil || bidReq.Device.IP == "" { - if ip := prebid.GetIP(httpReq); ip != "" { - if bidReq.Device == nil { - bidReq.Device = &openrtb.Device{} +func setIPImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidator iputil.IPValidator) { + if bidReq.Device == nil || (bidReq.Device.IP == "" && bidReq.Device.IPv6 == "") { + if ip, ver := httputil.FindIP(httpReq, ipValidator); ip != nil { + switch ver { + case iputil.IPv4: + if bidReq.Device == nil { + bidReq.Device = &openrtb.Device{} + } + bidReq.Device.IP = ip.String() + case iputil.IPv6: + if bidReq.Device == nil { + bidReq.Device = &openrtb.Device{} + } + bidReq.Device.IPv6 = ip.String() } - bidReq.Device.IP = ip } } } @@ -1166,6 +1255,24 @@ func setUAImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { } } +func setDoNotTrackImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) { + if bidReq.Device == nil || bidReq.Device.DNT == nil { + dnt := httpReq.Header.Get(dntKey) + if dnt == "0" || dnt == "1" { + if bidReq.Device == nil { + bidReq.Device = &openrtb.Device{} + } + + switch dnt { + case "0": + bidReq.Device.DNT = &dntDisabled + case "1": + bidReq.Device.DNT = &dntEnabled + } + } + } +} + // parseUserID gets this user's ID for the host machine, if it exists. func parseUserID(cfg *config.Configuration, httpReq *http.Request) (string, bool) { if hostCookie, err := httpReq.Cookie(cfg.HostCookie.CookieName); hostCookie != nil && err == nil { @@ -1213,8 +1320,8 @@ func writeError(errs []error, w http.ResponseWriter, labels *pbsmetrics.Labels) return rc } -// Returns the effective publisher ID -func effectivePubID(pub *openrtb.Publisher) string { +// Returns the account ID for the request +func getAccountID(pub *openrtb.Publisher) string { if pub != nil { if pub.Ext != nil { var pubExt openrtb_ext.ExtPublisher @@ -1229,15 +1336,3 @@ func effectivePubID(pub *openrtb.Publisher) string { } return pbsmetrics.PublisherUnknown } - -func validateAccount(cfg *config.Configuration, pubID string) error { - var err error = nil - if cfg.AccountRequired && pubID == pbsmetrics.PublisherUnknown { - // If specified in the configuration, discard requests that don't come with an account ID. - err = error(&errortypes.AcctRequired{Message: fmt.Sprintf("Prebid-server has been configured to discard requests that don't come with an Account ID. Please reach out to the prebid server host.")}) - } else if _, found := cfg.BlacklistedAcctMap[pubID]; found { - // Blacklist account now that we have resolved the value - err = error(&errortypes.BlacklistedAcct{Message: fmt.Sprintf("Prebid-server has blacklisted Account ID: %s, please reach out to the prebid server host.", pubID)}) - } - return err -} diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go index 2ac82b5c52f..ad50a02805e 100644 --- a/endpoints/openrtb2/auction_benchmark_test.go +++ b/endpoints/openrtb2/auction_benchmark_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/currencies" @@ -77,7 +78,8 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) { theMetrics, infos, gdpr.AlwaysAllow{}, - currencies.NewRateConverterDefault(), + currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), + empty_fetcher.EmptyFetcher{}, ), paramValidator, empty_fetcher.EmptyFetcher{}, diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 07d477a3730..798e59e5bf9 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -8,9 +8,11 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "testing" "time" @@ -27,6 +29,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/backends/empty_fetcher" + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" "github.com/buger/jsonparser" jsonpatch "github.com/evanphx/json-patch" "github.com/stretchr/testify/assert" @@ -34,18 +37,325 @@ import ( const maxSize = 1024 * 256 -// Struct of data for the general purpose auction tester -type getResponseFromDirectory struct { - dir string - file string - payloadGetter func(*testing.T, []byte) []byte - messageGetter func(*testing.T, []byte) []byte - expectedCode int - aliased bool - disabledBidders []string - adaptersConfig map[string]config.Adapter - accountReq bool - description string +type testCase struct { + BidRequest json.RawMessage `json:"mockBidRequest"` + Config *testConfigValues `json:"config"` + ExpectedReturnCode int `json:"expectedReturnCode,omitempty"` + ExpectedErrorMessage string `json:"expectedErrorMessage"` + ExpectedBidResponse json.RawMessage `json:"expectedBidResponse"` +} + +type testConfigValues struct { + AccountRequired bool `json:"accountRequired"` + AliasJSON string `json:"aliases"` + BlacklistedAccounts []string `json:"blacklistedAccts"` + BlacklistedApps []string `json:"blacklistedApps"` + AdapterList []string `json:"disabledAdapters"` +} + +func TestJsonSampleRequests(t *testing.T) { + testSuites := []struct { + description string + sampleRequestsSubDir string + }{ + { + "Assert 200s on all bidRequests from exemplary folder", + "valid-whole/exemplary", + }, + { + "Asserts we return 200s on well-formed Native requests.", + "valid-native", + }, + { + "Asserts we return 400s on requests that are not supposed to pass validation", + "invalid-whole", + }, + { + "Asserts we return 400s on requests with Native requests that don't pass validation", + "invalid-native", + }, + { + "Makes sure we handle (default) aliased bidders properly", + "aliased", + }, + { + "Asserts we return 503s on requests with blacklisted accounts and apps.", + "blacklisted", + }, + { + "Assert that requests that come with no user id nor app id return error if the `AccountRequired` field in the `config.Configuration` structure is set to true", + "account-required/no-account", + }, + { + "Assert requests that come with a valid user id or app id when account is required", + "account-required/with-account", + }, + { + "Tests diagnostic messages for invalid stored requests", + "invalid-stored", + }, + { + "Make sure requests with disabled bidders will fail", + "disabled/bad", + }, + { + "There are both disabled and non-disabled bidders, we expect a 200", + "disabled/good", + }, + { + "Requests with first party data context info found in imp[i].ext.prebid.bidder,context", + "first-party-data", + }, + } + for _, test := range testSuites { + testCaseFiles, err := getTestFiles(filepath.Join("sample-requests", test.sampleRequestsSubDir)) + if assert.NoError(t, err, "Test case %s. Error reading files from directory %s \n", test.description, test.sampleRequestsSubDir) { + for _, file := range testCaseFiles { + data, err := ioutil.ReadFile(file) + if assert.NoError(t, err, "Test case %s. Error reading file %s \n", test.description, file) { + runTestCase(t, data, file) + } + } + } + } +} + +func getTestFiles(dir string) ([]string, error) { + var filesToAssert []string + + fileList, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + // Append the path of every file found in `dir` to the `filesToAssert` array + for _, fileInfo := range fileList { + filesToAssert = append(filesToAssert, filepath.Join(dir, fileInfo.Name())) + } + + return filesToAssert, nil +} + +func runTestCase(t *testing.T, fileData []byte, testFile string) { + t.Helper() + + // Retrieve values from JSON file + test := parseTestFile(t, fileData, testFile) + + // Run test + actualCode, actualJsonBidResponse := doRequest(t, test) + + // Assertions + assert.Equal(t, test.ExpectedReturnCode, actualCode, "Test failed. Filename: %s \n", testFile) + + // Either assert bid response or expected error + if len(test.ExpectedErrorMessage) > 0 { + assert.True(t, strings.HasPrefix(actualJsonBidResponse, test.ExpectedErrorMessage), "Actual: %s \nExpected: %s. Filename: %s \n", actualJsonBidResponse, test.ExpectedErrorMessage, testFile) + } + + if len(test.ExpectedBidResponse) > 0 { + var expectedBidResponse openrtb.BidResponse + var actualBidResponse openrtb.BidResponse + var err error + + err = json.Unmarshal(test.ExpectedBidResponse, &expectedBidResponse) + if assert.NoError(t, err, "Could not unmarshal expected bidResponse taken from test file.\n Test file: %s\n Error:%s\n", testFile, err) { + err = json.Unmarshal([]byte(actualJsonBidResponse), &actualBidResponse) + if assert.NoError(t, err, "Could not unmarshal actual bidResponse from auction.\n Test file: %s\n Error:%s\n", testFile, err) { + assertBidResponseEqual(t, testFile, expectedBidResponse, actualBidResponse) + } + } + } +} + +func parseTestFile(t *testing.T, fileData []byte, testFile string) testCase { + t.Helper() + + parsedTestData := testCase{} + var err, errEm error + + // Get testCase values + parsedTestData.BidRequest, _, _, err = jsonparser.Get(fileData, "mockBidRequest") + assert.NoError(t, err, "Error jsonparsing root.mockBidRequest from file %s. Desc: %v.", testFile, err) + + // Get testCaseConfig values + parsedTestData.Config = &testConfigValues{} + var jsonTestConfig json.RawMessage + + jsonTestConfig, _, _, err = jsonparser.Get(fileData, "config") + if err == nil { + err = json.Unmarshal(jsonTestConfig, parsedTestData.Config) + assert.NoError(t, err, "Error unmarshaling root.config from file %s. Desc: %v.", testFile, err) + } + + // Get the return code we expect PBS to throw back given test's bidRequest and config + parsedReturnCode, err := jsonparser.GetInt(fileData, "expectedReturnCode") + assert.NoError(t, err, "Error jsonparsing root.code from file %s. Desc: %v.", testFile, err) + + // Get both bid response and error message, if any + parsedTestData.ExpectedBidResponse, _, _, err = jsonparser.Get(fileData, "expectedBidResponse") + parsedTestData.ExpectedErrorMessage, errEm = jsonparser.GetString(fileData, "expectedErrorMessage") + + assert.Falsef(t, (err == nil && errEm == nil), "Test case file can't have both a valid expectedBidResponse and a valid expectedErrorMessage, fields are mutually exclusive") + assert.Falsef(t, (err != nil && errEm != nil), "Test case file should come with either a valid expectedBidResponse or a valid expectedErrorMessage, not both.") + + parsedTestData.ExpectedReturnCode = int(parsedReturnCode) + + return parsedTestData +} + +func (tc *testConfigValues) getBlacklistedAppMap() map[string]bool { + var blacklistedAppMap map[string]bool + + if len(tc.BlacklistedApps) > 0 { + blacklistedAppMap = make(map[string]bool, len(tc.BlacklistedApps)) + for _, app := range tc.BlacklistedApps { + blacklistedAppMap[app] = true + } + } + return blacklistedAppMap +} + +func (tc *testConfigValues) getBlackListedAccountMap() map[string]bool { + var blacklistedAccountMap map[string]bool + + if len(tc.BlacklistedAccounts) > 0 { + blacklistedAccountMap = make(map[string]bool, len(tc.BlacklistedAccounts)) + for _, account := range tc.BlacklistedAccounts { + blacklistedAccountMap[account] = true + } + } + return blacklistedAccountMap +} + +func (tc *testConfigValues) getAdaptersConfigMap() map[string]config.Adapter { + var adaptersConfig map[string]config.Adapter + + if len(tc.AdapterList) > 0 { + adaptersConfig = make(map[string]config.Adapter, len(tc.AdapterList)) + for _, adapterName := range tc.AdapterList { + adaptersConfig[adapterName] = config.Adapter{Disabled: true} + } + } + return adaptersConfig +} + +// Once unmarshalled, bidResponse objects can't simply be compared with an `assert.Equalf()` call +// because tests fail if the elements inside the `bidResponse.SeatBid` and `bidResponse.SeatBid.Bid` +// arrays, if any, are not listed in the exact same order in the actual version and in the expected version. +func assertBidResponseEqual(t *testing.T, testFile string, expectedBidResponse openrtb.BidResponse, actualBidResponse openrtb.BidResponse) { + + //Assert non-array BidResponse fields + assert.Equalf(t, expectedBidResponse.ID, actualBidResponse.ID, "BidResponse.ID doesn't match expected. Test: %s\n", testFile) + assert.Equalf(t, expectedBidResponse.BidID, actualBidResponse.BidID, "BidResponse.BidID doesn't match expected. Test: %s\n", testFile) + assert.Equalf(t, expectedBidResponse.NBR, actualBidResponse.NBR, "BidResponse.NBR doesn't match expected. Test: %s\n", testFile) + + //Assert []SeatBid and their Bid elements independently of their order + assert.Len(t, actualBidResponse.SeatBid, len(expectedBidResponse.SeatBid), "BidResponse.SeatBid array doesn't match expected. Test: %s\n", testFile) + + //Given that bidResponses have the same length, compare them in an order-independent way using maps + var actualSeatBidsMap map[string]openrtb.SeatBid = make(map[string]openrtb.SeatBid, 0) + for _, seatBid := range actualBidResponse.SeatBid { + actualSeatBidsMap[seatBid.Seat] = seatBid + } + + var expectedSeatBidsMap map[string]openrtb.SeatBid = make(map[string]openrtb.SeatBid, 0) + for _, seatBid := range expectedBidResponse.SeatBid { + expectedSeatBidsMap[seatBid.Seat] = seatBid + } + + for k, expectedSeatBid := range expectedSeatBidsMap { + //Assert non-array SeatBid fields + assert.Equalf(t, expectedSeatBid.Seat, actualSeatBidsMap[k].Seat, "actualSeatBidsMap[%s].Seat doesn't match expected. Test: %s\n", k, testFile) + assert.Equalf(t, expectedSeatBid.Group, actualSeatBidsMap[k].Group, "actualSeatBidsMap[%s].Group doesn't match expected. Test: %s\n", k, testFile) + assert.Equalf(t, expectedSeatBid.Ext, actualSeatBidsMap[k].Ext, "actualSeatBidsMap[%s].Ext doesn't match expected. Test: %s\n", k, testFile) + assert.Len(t, actualSeatBidsMap[k].Bid, len(expectedSeatBid.Bid), "BidResponse.SeatBid[].Bid array doesn't match expected. Test: %s\n", testFile) + + //Assert Bid arrays + assert.ElementsMatch(t, expectedSeatBid.Bid, actualSeatBidsMap[k].Bid, "BidResponse.SeatBid array doesn't match expected. Test: %s\n", testFile) + } +} + +func TestBidRequestAssert(t *testing.T) { + appnexusB1 := openrtb.Bid{ID: "appnexus-bid-1", Price: 5.00} + appnexusB2 := openrtb.Bid{ID: "appnexus-bid-2", Price: 7.00} + rubiconB1 := openrtb.Bid{ID: "rubicon-bid-1", Price: 1.50} + rubiconB2 := openrtb.Bid{ID: "rubicon-bid-2", Price: 4.00} + + sampleSeatBids := []openrtb.SeatBid{ + { + Seat: "appnexus-bids", + Bid: []openrtb.Bid{appnexusB1, appnexusB2}, + }, + { + Seat: "rubicon-bids", + Bid: []openrtb.Bid{rubiconB1, rubiconB2}, + }, + } + + testSuites := []struct { + description string + expectedBidResponse openrtb.BidResponse + actualBidResponse openrtb.BidResponse + }{ + { + "identical SeatBids, exact same SeatBid and Bid arrays order", + openrtb.BidResponse{ID: "anId", BidID: "bidId", SeatBid: sampleSeatBids}, + openrtb.BidResponse{ID: "anId", BidID: "bidId", SeatBid: sampleSeatBids}, + }, + { + "identical SeatBids but Seatbid array elements come in different order", + openrtb.BidResponse{ID: "anId", BidID: "bidId", SeatBid: sampleSeatBids}, + openrtb.BidResponse{ID: "anId", BidID: "bidId", + SeatBid: []openrtb.SeatBid{ + { + Seat: "rubicon-bids", + Bid: []openrtb.Bid{rubiconB1, rubiconB2}, + }, + { + Seat: "appnexus-bids", + Bid: []openrtb.Bid{appnexusB1, appnexusB2}, + }, + }, + }, + }, + { + "SeatBids seem to be identical except for the different order of Bid array elements in one of them", + openrtb.BidResponse{ID: "anId", BidID: "bidId", SeatBid: sampleSeatBids}, + openrtb.BidResponse{ID: "anId", BidID: "bidId", + SeatBid: []openrtb.SeatBid{ + { + Seat: "appnexus-bids", + Bid: []openrtb.Bid{appnexusB2, appnexusB1}, + }, + { + Seat: "rubicon-bids", + Bid: []openrtb.Bid{rubiconB1, rubiconB2}, + }, + }, + }, + }, + { + "Both SeatBid elements and bid elements come in different order", + openrtb.BidResponse{ID: "anId", BidID: "bidId", SeatBid: sampleSeatBids}, + openrtb.BidResponse{ID: "anId", BidID: "bidId", + SeatBid: []openrtb.SeatBid{ + { + Seat: "rubicon-bids", + Bid: []openrtb.Bid{rubiconB2, rubiconB1}, + }, + { + Seat: "appnexus-bids", + Bid: []openrtb.Bid{appnexusB2, appnexusB1}, + }, + }, + }, + }, + } + + for _, test := range testSuites { + assertBidResponseEqual(t, test.description, test.expectedBidResponse, test.actualBidResponse) + } } // TestExplicitUserId makes sure that the cookie's ID doesn't override an explicit value sent in the request. @@ -61,7 +371,7 @@ func TestExplicitUserId(t *testing.T) { ex := &mockExchange{} request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(`{ - "id": "some-request-id", +"id": "some-request-id", "site": { "page": "test.somepage.com" }, @@ -118,198 +428,38 @@ func TestExplicitUserId(t *testing.T) { } } -// TestGoodRequests makes sure we return 200s on good requests. -func TestGoodRequests(t *testing.T) { - exemplary := &getResponseFromDirectory{ - dir: "sample-requests/valid-whole/exemplary", - payloadGetter: getRequestPayload, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - aliased: true, - } - supplementary := &getResponseFromDirectory{ - dir: "sample-requests/valid-whole/supplementary", - payloadGetter: noop, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - aliased: true, - } - exemplary.assert(t) - supplementary.assert(t) -} - -// TestGoodNativeRequests makes sure we return 200s on well-formed Native requests. -func TestGoodNativeRequests(t *testing.T) { - tests := &getResponseFromDirectory{ - dir: "sample-requests/valid-native", - payloadGetter: buildNativeRequest, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - aliased: true, - } - tests.assert(t) -} - -// TestBadRequests makes sure we return 400s on bad requests. -func TestBadRequests(t *testing.T) { - // Need to turn off aliases for bad requests as applying the alias can fail on a bad request before the expected error is reached. - tests := &getResponseFromDirectory{ - dir: "sample-requests/invalid-whole", - payloadGetter: getRequestPayload, - messageGetter: getMessage, - expectedCode: http.StatusBadRequest, - aliased: false, - } - tests.assert(t) -} - -// TestBadRequests makes sure we return 400s on requests with bad Native requests. -func TestBadNativeRequests(t *testing.T) { - tests := &getResponseFromDirectory{ - dir: "sample-requests/invalid-native", - payloadGetter: buildNativeRequest, - messageGetter: nilReturner, - expectedCode: http.StatusBadRequest, - aliased: false, - } - tests.assert(t) -} - -// TestAliasedRequests makes sure we handle (default) aliased bidders properly -func TestAliasedRequests(t *testing.T) { - tests := &getResponseFromDirectory{ - dir: "sample-requests/aliased", - payloadGetter: noop, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - aliased: true, - } - tests.assert(t) -} - -// TestDisabledBidders makes sure we don't break when encountering a disabled bidder -func TestDisabledBidders(t *testing.T) { - badTests := &getResponseFromDirectory{ - dir: "sample-requests/disabled/bad", - payloadGetter: getRequestPayload, - messageGetter: getMessage, - expectedCode: http.StatusBadRequest, - aliased: false, - disabledBidders: []string{"appnexus", "rubicon"}, - adaptersConfig: map[string]config.Adapter{ - "appnexus": {Disabled: true}, - "rubicon": {Disabled: true}, - }, - } - goodTests := &getResponseFromDirectory{ - dir: "sample-requests/disabled/good", - payloadGetter: noop, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - aliased: false, - disabledBidders: []string{"appnexus", "rubicon"}, - adaptersConfig: map[string]config.Adapter{ - "appnexus": {Disabled: true}, - "rubicon": {Disabled: true}, - }, - } - badTests.assert(t) - goodTests.assert(t) -} - -// TestBlacklistRequests makes sure we return 400s on blacklisted requests. -func TestBlacklistRequests(t *testing.T) { - // Need to turn off aliases for bad requests as applying the alias can fail on a bad request before the expected error is reached. - tests := &getResponseFromDirectory{ - dir: "sample-requests/blacklisted", - payloadGetter: getRequestPayload, - messageGetter: getMessage, - expectedCode: http.StatusServiceUnavailable, - aliased: false, - } - tests.assert(t) -} - -// TestRejectAccountRequired asserts we return a 400 code on a request that comes with no user id nor app id -// if the `AccountRequired` field in the `config.Configuration` structure is set to true -func TestRejectAccountRequired(t *testing.T) { - tests := []*getResponseFromDirectory{ - { - // Account not required and not provided in prebid request - dir: "sample-requests/account-required", - file: "no-acct.json", - payloadGetter: getRequestPayload, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - accountReq: false, - }, - { - // Account was required but not provided in prebid request - dir: "sample-requests/account-required", - file: "no-acct.json", - payloadGetter: getRequestPayload, - messageGetter: getMessage, - expectedCode: http.StatusBadRequest, - accountReq: true, - }, - { - // Account is required, was provided and is not in the blacklisted accounts map - dir: "sample-requests/account-required", - file: "with-acct.json", - payloadGetter: getRequestPayload, - messageGetter: nilReturner, - expectedCode: http.StatusOK, - aliased: true, - accountReq: true, - }, - { - // Account is required, was provided in request and is found in the blacklisted accounts map - dir: "sample-requests/blacklisted", - file: "blacklisted-acct.json", - payloadGetter: getRequestPayload, - messageGetter: getMessage, - expectedCode: http.StatusServiceUnavailable, - accountReq: true, +func doRequest(t *testing.T, test testCase) (int, string) { + disabledBidders := map[string]string{} + bidderMap := exchange.DisableBidders(getBidderInfos(test.Config.getAdaptersConfigMap(), openrtb_ext.BidderList()), disabledBidders) + + // 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. + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + + endpoint, _ := NewEndpoint( + &mockBidExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{ + MaxRequestSize: maxSize, + BlacklistedApps: test.Config.BlacklistedApps, + BlacklistedAppMap: test.Config.getBlacklistedAppMap(), + BlacklistedAccts: test.Config.BlacklistedAccounts, + BlacklistedAcctMap: test.Config.getBlackListedAccountMap(), + AccountRequired: test.Config.AccountRequired, }, - } - for _, test := range tests { - test.assert(t) - } -} + metrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + disabledBidders, + []byte(test.Config.AliasJSON), + bidderMap, + ) -// assertResponseFromDirectory makes sure that the payload from each file in dir gets the expected response status code -// from the /openrtb2/auction endpoint. -func (gr *getResponseFromDirectory) assert(t *testing.T) { - //t *testing.T, dir string, payloadGetter func(*testing.T, []byte) []byte, messageGetter func(*testing.T, []byte) []byte, expectedCode int, aliased bool) { - t.Helper() - var filesToAssert []string - if gr.file == "" { - // Append every file found in `gr.dir` to the `filesToAssert` array and test them all - for _, fileInfo := range fetchFiles(t, gr.dir) { - filesToAssert = append(filesToAssert, gr.dir+"/"+fileInfo.Name()) - } - } else { - // 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) - } - - var fileData []byte - // Test the one or more test files appended to `filesToAssert` - for _, testFile := range filesToAssert { - fileData = readFile(t, testFile) - code, msg := gr.doRequest(t, gr.payloadGetter(t, fileData)) - fmt.Printf("Processing %s\n", testFile) - assertResponseCode(t, testFile, code, gr.expectedCode, msg) - - expectMsg := gr.messageGetter(t, fileData) - if gr.description != "" { - if len(expectMsg) > 0 { - assert.Equal(t, string(expectMsg), msg, "Test failed. %s. Filename: \n", gr.description, testFile) - } else { - assert.Equal(t, string(expectMsg), msg, "file %s had bad response body", testFile) - } - } - } + request := httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(test.BidRequest)) + recorder := httptest.NewRecorder() + endpoint(recorder, request, nil) //Request comes from the unmarshalled mockBidRequest + return recorder.Code, recorder.Body.String() } // fetchFiles returns a list of the files from dir, or fails the test if an error occurs. @@ -330,39 +480,6 @@ func readFile(t *testing.T, filename string) []byte { return data } -// doRequest populates the app with mock dependencies and sends requestData to the /openrtb2/auction endpoint. -func (gr *getResponseFromDirectory) doRequest(t *testing.T, requestData []byte) (int, string) { - aliasJSON := []byte{} - if gr.aliased { - aliasJSON = []byte(`{"ext":{"prebid":{"aliases": {"test1": "appnexus", "test2": "rubicon", "test3": "openx"}}}}`) - } - 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.", - } - bidderMap := exchange.DisableBidders(getBidderInfos(gr.adaptersConfig, openrtb_ext.BidderList()), disabledBidders) - - // 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{}) - endpoint, _ := NewEndpoint( - &nobidExchange{}, - newParamsValidator(t), - &mockStoredReqFetcher{}, - empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize, BlacklistedApps: []string{"spam_app"}, BlacklistedAppMap: map[string]bool{"spam_app": true}, BlacklistedAccts: []string{"bad_acct"}, BlacklistedAcctMap: map[string]bool{"bad_acct": true}, AccountRequired: gr.accountReq}, - theMetrics, - analyticsConf.NewPBSAnalytics(&config.Analytics{}), - disabledBidders, - aliasJSON, - bidderMap, - ) - - request := httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(requestData)) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) - return recorder.Code, recorder.Body.String() -} - // TestBadAliasRequests() reuses two requests that would fail anyway. Here, we // take advantage of our knowledge that processStoredRequests() in auction.go // processes aliases before it processes stored imps. Changing that order @@ -376,7 +493,9 @@ func TestBadAliasRequests(t *testing.T) { func doBadAliasRequest(t *testing.T, filename string, expectMsg string) { t.Helper() fileData := readFile(t, filename) - requestData := getRequestPayload(t, fileData) + testBidRequest, _, _, err := jsonparser.Get(fileData, "mockBidRequest") + assert.NoError(t, err, "Error jsonparsing root.mockBidRequest from file %s. Desc: %v.", filename, err) + // aliasJSON lacks a comma after the "appnexus" entry so is bad JSON aliasJSON := []byte(`{"ext":{"prebid":{"aliases": {"test1": "appnexus" "test2": "rubicon", "test3": "openx"}}}}`) disabledBidders := map[string]string{ @@ -387,10 +506,10 @@ func doBadAliasRequest(t *testing.T, filename string, expectMsg string) { // 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{}) - endpoint, _ := NewEndpoint(&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), disabledBidders, aliasJSON, bidderMap) + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + endpoint, _ := NewEndpoint(&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), disabledBidders, aliasJSON, bidderMap) - request := httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(requestData)) + request := httptest.NewRequest("POST", "/openrtb2/auction", bytes.NewReader(testBidRequest)) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -414,29 +533,6 @@ func assertResponseCode(t *testing.T, filename string, actual int, expected int, } } -// buildNativeRequest JSON-encodes the nativeData as a string, and puts it into request.imp[0].native.request -// of a request which is valid otherwise. -func buildNativeRequest(t *testing.T, nativeData []byte) []byte { - serialized, err := json.Marshal(string(nativeData)) - if err != nil { - t.Fatalf("Failed to string-escape JSON data: %v", err) - } - - buf := bytes.NewBuffer(nil) - buf.WriteString(`{"id":"req-id","site":{"page":"some.page.com"},"tmax":500,"imp":[{"id":"some-imp","native":{"request":`) - buf.Write(serialized) - buf.WriteString(`},"ext":{"appnexus":{"placementId":12883451}}}]}`) - return buf.Bytes() -} - -func noop(t *testing.T, data []byte) []byte { - return data -} - -func nilReturner(t *testing.T, data []byte) []byte { - return nil -} - func getRequestPayload(t *testing.T, example []byte) []byte { t.Helper() if value, _, _, err := jsonparser.Get(example, "requestPayload"); err != nil { @@ -451,8 +547,8 @@ func getRequestPayload(t *testing.T, example []byte) []byte { func TestNilExchange(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{}) - _, err := NewEndpoint(nil, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + _, err := NewEndpoint(nil, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil Exchange.") } @@ -462,8 +558,8 @@ func TestNilExchange(t *testing.T) { func TestNilValidator(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{}) - _, err := NewEndpoint(&nobidExchange{}, nil, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + _, err := NewEndpoint(&nobidExchange{}, nil, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) if err == nil { t.Errorf("NewEndpoint should return an error when given a nil BidderParamValidator.") } @@ -473,8 +569,8 @@ func TestNilValidator(t *testing.T) { func TestExchangeError(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{}) - endpoint, _ := NewEndpoint(&brokenExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + endpoint, _ := NewEndpoint(&brokenExchange{}, newParamsValidator(t), empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) @@ -526,29 +622,265 @@ func TestAuctionTypeDefault(t *testing.T) { } } -// TestImplicitIPs prevents #230 -func TestImplicitIPs(t *testing.T) { - ex := &nobidExchange{} - // 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{}) - endpoint, _ := NewEndpoint(ex, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) +func TestImplicitIPsEndToEnd(t *testing.T) { + testCases := []struct { + description string + reqJSONFile string + xForwardedForHeader string + privateNetworksIPv4 []net.IPNet + privateNetworksIPv6 []net.IPNet + expectedDeviceIPv4 string + expectedDeviceIPv6 string + }{ + { + description: "IPv4", + reqJSONFile: "site.json", + xForwardedForHeader: "1.1.1.1", + expectedDeviceIPv4: "1.1.1.1", + }, + { + description: "IPv6", + reqJSONFile: "site.json", + xForwardedForHeader: "1111::", + expectedDeviceIPv6: "1111::", + }, + { + description: "IPv4 - Defined In Request", + reqJSONFile: "site-has-ipv4.json", + xForwardedForHeader: "1.1.1.1", + expectedDeviceIPv4: "8.8.8.8", // Hardcoded value in test file. + }, + { + description: "IPv6 - Defined In Request", + reqJSONFile: "site-has-ipv6.json", + xForwardedForHeader: "1111::", + expectedDeviceIPv6: "8888::", // Hardcoded value in test file. + }, + { + description: "IPv4 - Defined In Request - Private Network", + reqJSONFile: "site-has-ipv4.json", + xForwardedForHeader: "1.1.1.1", + privateNetworksIPv4: []net.IPNet{{IP: net.IP{8, 8, 8, 0}, Mask: net.CIDRMask(24, 32)}}, // Hardcoded value in test file. + expectedDeviceIPv4: "1.1.1.1", + }, + { + description: "IPv6 - Defined In Request - Private Network", + reqJSONFile: "site-has-ipv6.json", + xForwardedForHeader: "1111::", + privateNetworksIPv6: []net.IPNet{{IP: net.ParseIP("8800::"), Mask: net.CIDRMask(8, 128)}}, // Hardcoded value in test file. + expectedDeviceIPv6: "1111::", + }, + } - httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) - httpReq.Header.Set("X-Forwarded-For", "123.456.78.90") - recorder := httptest.NewRecorder() + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + for _, test := range testCases { + exchange := &nobidExchange{} + cfg := &config.Configuration{ + MaxRequestSize: maxSize, + RequestValidation: config.RequestValidation{ + IPv4PrivateNetworksParsed: test.privateNetworksIPv4, + IPv6PrivateNetworksParsed: test.privateNetworksIPv6, + }, + } + endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) - endpoint(recorder, httpReq, nil) + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) + httpReq.Header.Set("X-Forwarded-For", test.xForwardedForHeader) - if ex.gotRequest == nil { - t.Fatalf("The request never made it into the Exchange.") + endpoint(httptest.NewRecorder(), httpReq, nil) + + result := exchange.gotRequest + if !assert.NotEmpty(t, result, test.description+"Request received by the exchange.") { + t.FailNow() + } + assert.Equal(t, test.expectedDeviceIPv4, result.Device.IP, test.description+":ipv4") + assert.Equal(t, test.expectedDeviceIPv6, result.Device.IPv6, test.description+":ipv6") } +} - if ex.gotRequest.Device.IP != "123.456.78.90" { - t.Errorf("Bad device IP. Expected 123.456.78.90, got %s", ex.gotRequest.Device.IP) +func TestImplicitDNT(t *testing.T) { + var ( + disabled int8 = 0 + enabled int8 = 1 + ) + testCases := []struct { + description string + dntHeader string + request openrtb.BidRequest + expectedRequest openrtb.BidRequest + }{ + { + description: "Device Missing - Not Set In Header", + dntHeader: "", + request: openrtb.BidRequest{}, + expectedRequest: openrtb.BidRequest{}, + }, + { + description: "Device Missing - Set To 0 In Header", + dntHeader: "0", + request: openrtb.BidRequest{}, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &disabled, + }, + }, + }, + { + description: "Device Missing - Set To 1 In Header", + dntHeader: "1", + request: openrtb.BidRequest{}, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Not Set In Request - Not Set In Header", + dntHeader: "", + request: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + }, + { + description: "Not Set In Request - Set To 0 In Header", + dntHeader: "0", + request: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &disabled, + }, + }, + }, + { + description: "Not Set In Request - Set To 1 In Header", + dntHeader: "1", + request: openrtb.BidRequest{ + Device: &openrtb.Device{}, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Set In Request - Not Set In Header", + dntHeader: "", + request: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Set In Request - Set To 0 In Header", + dntHeader: "0", + request: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + { + description: "Set In Request - Set To 1 In Header", + dntHeader: "1", + request: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + expectedRequest: openrtb.BidRequest{ + Device: &openrtb.Device{ + DNT: &enabled, + }, + }, + }, + } + + for _, test := range testCases { + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", nil) + httpReq.Header.Set("DNT", test.dntHeader) + setDoNotTrackImplicitly(httpReq, &test.request) + assert.Equal(t, test.expectedRequest, test.request) } } +func TestImplicitDNTEndToEnd(t *testing.T) { + var ( + disabled int8 = 0 + enabled int8 = 1 + ) + testCases := []struct { + description string + reqJSONFile string + dntHeader string + expectedDNT *int8 + }{ + { + description: "Not Set In Request - Not Set In Header", + reqJSONFile: "site.json", + dntHeader: "", + expectedDNT: nil, + }, + { + description: "Not Set In Request - Set To 0 In Header", + reqJSONFile: "site.json", + dntHeader: "0", + expectedDNT: &disabled, + }, + { + description: "Not Set In Request - Set To 1 In Header", + reqJSONFile: "site.json", + dntHeader: "1", + expectedDNT: &enabled, + }, + { + description: "Set In Request - Not Set In Header", + reqJSONFile: "site-has-dnt.json", + dntHeader: "", + expectedDNT: &enabled, // Hardcoded value in test file. + }, + { + description: "Set In Request - Not Overwritten By Header", + reqJSONFile: "site-has-dnt.json", + dntHeader: "0", + expectedDNT: &enabled, // Hardcoded value in test file. + }, + } + + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + for _, test := range testCases { + exchange := &nobidExchange{} + endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap) + + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile))) + httpReq.Header.Set("DNT", test.dntHeader) + + endpoint(httptest.NewRecorder(), httpReq, nil) + + result := exchange.gotRequest + if !assert.NotEmpty(t, result, test.description+"Request received by the exchange.") { + t.FailNow() + } + assert.Equal(t, test.expectedDNT, result.Device.DNT, test.description+":dnt") + } +} func TestImplicitSecure(t *testing.T) { httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) httpReq.Header.Set(http.CanonicalHeaderKey("X-Forwarded-Proto"), "https") @@ -584,28 +916,31 @@ func TestRefererParsing(t *testing.T) { } } -// TestBadStoredRequests tests diagnostic messages for invalid stored requests -func TestBadStoredRequests(t *testing.T) { - // Need to turn off aliases for bad requests as applying the alias can fail on a bad request before the expected error is reached. - tests := &getResponseFromDirectory{ - dir: "sample-requests/invalid-stored", - payloadGetter: getRequestPayload, - messageGetter: getMessage, - expectedCode: http.StatusBadRequest, - aliased: false, - } - tests.assert(t) -} - // Test the stored request functionality 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, nil, nil} + metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + deps := &endpointDeps{ + &nobidExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + metrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + nil, + nil, + hardcodedResponseIPValidator{response: true}, + } for i, requestData := range testStoredRequests { - newRequest, errList := edep.processStoredRequests(context.Background(), json.RawMessage(requestData)) + newRequest, errList := deps.processStoredRequests(context.Background(), json.RawMessage(requestData)) if len(errList) != 0 { for _, err := range errList { if err != nil { @@ -640,6 +975,7 @@ func TestOversizedRequest(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -674,6 +1010,7 @@ func TestRequestSizeEdgeCase(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) @@ -756,130 +1093,425 @@ func TestExplicitAMP(t *testing.T) { return } - bidReq := openrtb.BidRequest{ - Site: &openrtb.Site{ - Ext: json.RawMessage(`{"amp":1}`), - }, + bidReq := openrtb.BidRequest{ + Site: &openrtb.Site{ + Ext: json.RawMessage(`{"amp":1}`), + }, + } + setSiteImplicitly(httpReq, &bidReq) + assert.JSONEq(t, `{"amp":1}`, string(bidReq.Site.Ext)) +} + +// TestContentType prevents #328 +func TestContentType(t *testing.T) { + endpoint, _ := NewEndpoint( + &mockExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + []byte{}, + openrtb_ext.BidderMap, + ) + request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) + recorder := httptest.NewRecorder() + endpoint(recorder, request, nil) + + if recorder.Header().Get("Content-Type") != "application/json" { + t.Errorf("Content-Type should be application/json. Got %s", recorder.Header().Get("Content-Type")) + } +} + +func TestValidateImpExt(t *testing.T) { + type testCase struct { + description string + impExt json.RawMessage + expectedImpExt string + expectedErrs []error + } + testGroups := []struct { + description string + testCases []testCase + }{ + { + "Empty", + []testCase{ + { + description: "Empty", + impExt: nil, + expectedImpExt: "", + expectedErrs: []error{errors.New("request.imp[0].ext is required")}, + }, + }, + }, + { + "Unknown bidder tests", + []testCase{ + { + description: "Unknown Bidder only", + impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555}}`), + expectedImpExt: `{"unknownbidder":{"placement_id":555}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + { + description: "Unknown Prebid Ext Bidder only", + impExt: json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`), + expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + { + description: "Unknown Prebid Ext Bidder + First Party Data Context", + impExt: json.RawMessage(`{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"prebid":{"bidder":{"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + { + description: "Unknown Bidder + First Party Data Context", + impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555} ,"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + { + description: "Unknown Bidder + Disabled Bidder", + impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`), + expectedImpExt: `{"unknownbidder":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + { + description: "Unknown Bidder + Disabled Prebid Ext Bidder", + impExt: json.RawMessage(`{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`), + expectedImpExt: `{"unknownbidder":{"placement_id":555},"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + }, + }, + { + "Disabled bidder tests", + []testCase{ + { + description: "Disabled Bidder", + impExt: json.RawMessage(`{"disabledbidder":{"foo":"bar"}}`), + expectedImpExt: `{}`, + expectedErrs: []error{ + &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, + errors.New("request.imp[0].ext must contain at least one bidder"), + }, + // if only bidder(s) found in request.imp[x].ext.{biddername} or request.imp[x].ext.prebid.bidder.{biddername} are disabled, return error + }, + { + description: "Disabled Prebid Ext Bidder", + impExt: json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`), + expectedImpExt: `{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`, + expectedErrs: []error{ + &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, + errors.New("request.imp[0].ext must contain at least one bidder"), + }, + }, + { + description: "Disabled Bidder + First Party Data Context", + impExt: json.RawMessage(`{"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{ + &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, + errors.New("request.imp[0].ext must contain at least one bidder"), + }, + }, + { + description: "Disabled Prebid Ext Bidder + First Party Data Context", + impExt: json.RawMessage(`{"prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"context":{"data":{"keywords":"prebid server example"}}, "prebid":{"bidder":{"disabledbidder":{"foo":"bar"}}}}`, + expectedErrs: []error{ + &errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}, + errors.New("request.imp[0].ext must contain at least one bidder"), + }, + }, + }, + }, + { + "First Party only", + []testCase{ + { + description: "First Party Data Context", + impExt: json.RawMessage(`{"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{ + errors.New("request.imp[0].ext must contain at least one bidder"), + }, + }, + }, + }, + { + "Valid bidder tests", + []testCase{ + { + description: "Valid bidder root ext", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555}}`), + expectedImpExt: `{"appnexus":{"placement_id":555}}`, + expectedErrs: []error{}, + }, + { + description: "Valid bidder in prebid field", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`, + expectedErrs: []error{}, + }, + { + description: "Valid Bidder + First Party Data Context", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{}, + }, + { + description: "Valid Prebid Ext Bidder + First Party Data Context", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}} ,"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{}, + }, + { + description: "Valid Bidder + Unknown Bidder", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`), + expectedImpExt: `{"appnexus":{"placement_id":555},"unknownbidder":{"placement_id":555}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + { + description: "Valid Bidder + Disabled Bidder", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}`), + expectedImpExt: `{"appnexus":{"placement_id":555}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, + }, + { + description: "Valid Bidder + Disabled Bidder + First Party Data Context", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, + }, + { + description: "Valid Bidder + Disabled Bidder + Unknown Bidder + First Party Data Context", + impExt: json.RawMessage(`{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + { + description: "Valid Prebid Ext Bidder + Disabled Bidder Ext", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555},"disabledbidder":{"foo":"bar"}}},"appnexus":{"placement_id":555}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, + }, + { + description: "Valid Prebid Ext Bidder + Disabled Ext Bidder + First Party Data Context", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555},"disabledbidder":{"foo":"bar"}}},"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`, + expectedErrs: []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'disabledbidder' has been disabled."}}, + }, + { + description: "Valid Prebid Ext Bidder + Disabled Ext Bidder + Unknown Ext + First Party Data Context", + impExt: json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`), + expectedImpExt: `{"context":{"data":{"keywords":"prebid server example"}},"prebid":{"bidder":{"appnexus":{"placement_id":555},"disabledbidder":{"foo":"bar"},"unknownbidder":{"placement_id":555}}}}`, + expectedErrs: []error{errors.New("request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?")}, + }, + }, + }, + } + + deps := &endpointDeps{ + &nobidExchange{}, + newParamsValidator(t), + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: int64(8096)}, + pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{"disabledbidder": "The bidder 'disabledbidder' has been disabled."}, + false, + []byte{}, + openrtb_ext.BidderMap, + nil, + nil, + hardcodedResponseIPValidator{response: true}, + } + + for _, group := range testGroups { + for _, test := range group.testCases { + imp := &openrtb.Imp{Ext: test.impExt} + + errs := deps.validateImpExt(imp, nil, 0) + + if len(test.expectedImpExt) > 0 { + assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description) + } else { + assert.Empty(t, imp.Ext, "imp.ext expected to be empty but was: %s. Test: %s. %s\n", string(imp.Ext), group.description, test.description) + } + assert.Equal(t, test.expectedErrs, errs, "errs slice does not match expected. Test: %s. %s\n", group.description, test.description) + } + } +} + +func validRequest(t *testing.T, filename string) string { + requestData, err := ioutil.ReadFile("sample-requests/valid-whole/supplementary/" + filename) + if err != nil { + t.Fatalf("Failed to fetch a valid request: %v", err) } - setSiteImplicitly(httpReq, &bidReq) - assert.JSONEq(t, `{"amp":1}`, string(bidReq.Site.Ext)) + testBidRequest, _, _, err := jsonparser.Get(requestData, "mockBidRequest") + assert.NoError(t, err, "Error jsonparsing root.mockBidRequest from file %s. Desc: %v.", filename, err) + + return string(testBidRequest) } -// TestContentType prevents #328 -func TestContentType(t *testing.T) { - endpoint, _ := NewEndpoint( - &mockExchange{}, +func TestCurrencyTrunc(t *testing.T) { + deps := &endpointDeps{ + &nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: maxSize}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, + false, []byte{}, openrtb_ext.BidderMap, - ) - request := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) - recorder := httptest.NewRecorder() - endpoint(recorder, request, nil) - - if recorder.Header().Get("Content-Type") != "application/json" { - t.Errorf("Content-Type should be application/json. Got %s", recorder.Header().Get("Content-Type")) + nil, + nil, + hardcodedResponseIPValidator{response: true}, } -} -// TestDisabledBidder makes sure we pass when encountering a disabled bidder in the configuration. -func TestDisabledBidder(t *testing.T) { - reqData, err := ioutil.ReadFile("sample-requests/invalid-whole/unknown-bidder.json") - if err != nil { - t.Fatalf("Failed to fetch a valid request: %v", err) + ui := uint64(1) + req := openrtb.BidRequest{ + ID: "someID", + Imp: []openrtb.Imp{ + { + ID: "imp-ID", + Banner: &openrtb.Banner{ + W: &ui, + H: &ui, + }, + Ext: json.RawMessage("{\"appnexus\": {\"placementId\": 5667}}"), + }, + }, + Site: &openrtb.Site{ + ID: "myID", + }, + Cur: []string{"USD", "EUR"}, } - reqBody := string(getRequestPayload(t, reqData)) + errL := deps.validateRequest(&req) + + expectedError := errortypes.Warning{Message: "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency"} + assert.ElementsMatch(t, errL, []error{&expectedError}) +} + +func TestCCPAInvalid(t *testing.T) { deps := &endpointDeps{ &nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, - &config.Configuration{ - MaxRequestSize: int64(len(reqBody)), - }, + &config.Configuration{}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{"unknownbidder": "The bidder 'unknownbidder' has been disabled."}, + map[string]string{}, false, []byte{}, openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } - req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody)) - recorder := httptest.NewRecorder() + ui := uint64(1) + req := openrtb.BidRequest{ + ID: "someID", + Imp: []openrtb.Imp{ + { + ID: "imp-ID", + Banner: &openrtb.Banner{ + W: &ui, + H: &ui, + }, + Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`), + }, + }, + Site: &openrtb.Site{ + ID: "myID", + }, + Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"invalid by length"}`), + }, + } - deps.Auction(recorder, req, nil) + errL := deps.validateRequest(&req) - if recorder.Code != http.StatusOK { - t.Errorf("Endpoint should return a 200 if the unknown bidder was disabled.") - } + 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}) - if bytesRead, err := req.Body.Read(make([]byte, 1)); bytesRead != 0 || err != io.EOF { - t.Errorf("The request body should have been read to completion.") - } + assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") } -func TestValidateImpExtDisabledBidder(t *testing.T) { - imp := &openrtb.Imp{ - Ext: json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}`), - } +func TestNoSaleInvalid(t *testing.T) { deps := &endpointDeps{ &nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, - &config.Configuration{MaxRequestSize: int64(8096)}, + &config.Configuration{}, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), - map[string]string{"unknownbidder": "The bidder 'unknownbidder' has been disabled."}, + map[string]string{}, false, []byte{}, openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } - errs := deps.validateImpExt(imp, nil, 0) - assert.JSONEq(t, `{"appnexus":{"placement_id":555}}`, string(imp.Ext)) - assert.Equal(t, []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, errs) -} -func TestEffectivePubID(t *testing.T) { - var pub openrtb.Publisher - assert.Equal(t, pbsmetrics.PublisherUnknown, effectivePubID(nil), "effectivePubID failed for nil Publisher.") - assert.Equal(t, pbsmetrics.PublisherUnknown, effectivePubID(&pub), "effectivePubID failed for empty Publisher.") - pub.ID = "123" - assert.Equal(t, "123", effectivePubID(&pub), "effectivePubID failed for standard Publisher.") - pub.Ext = json.RawMessage(`{"prebid": {"parentAccount": "abc"} }`) - assert.Equal(t, "abc", effectivePubID(&pub), "effectivePubID failed for parentAccount.") + ui := uint64(1) + req := openrtb.BidRequest{ + ID: "someID", + Imp: []openrtb.Imp{ + { + ID: "imp-ID", + Banner: &openrtb.Banner{ + W: &ui, + H: &ui, + }, + Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`), + }, + }, + Site: &openrtb.Site{ + ID: "myID", + }, + Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"1NYN"}`), + }, + Ext: json.RawMessage(`{"prebid":{"nosale":["*", "appnexus"]}}`), + } + + errL := deps.validateRequest(&req) + + expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided") + assert.ElementsMatch(t, errL, []error{expectedError}) } -func validRequest(t *testing.T, filename string) string { - requestData, err := ioutil.ReadFile("sample-requests/valid-whole/supplementary/" + filename) - if err != nil { - t.Fatalf("Failed to fetch a valid request: %v", err) +func TestValidateSourceTID(t *testing.T) { + cfg := &config.Configuration{ + AutoGenSourceTID: true, } - return string(requestData) -} -func TestCurrencyTrunc(t *testing.T) { deps := &endpointDeps{ &nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, - &config.Configuration{}, + cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, @@ -888,6 +1520,7 @@ func TestCurrencyTrunc(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } ui := uint64(1) @@ -900,22 +1533,22 @@ func TestCurrencyTrunc(t *testing.T) { W: &ui, H: &ui, }, - Ext: json.RawMessage("{\"appnexus\": {\"placementId\": 5667}}"), + Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`), }, }, Site: &openrtb.Site{ ID: "myID", }, - Cur: []string{"USD", "EUR"}, + Regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"invalid by length"}`), + }, } - errL := deps.validateRequest(&req) - - expectedError := errortypes.Warning{Message: "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency"} - assert.ElementsMatch(t, errL, []error{&expectedError}) + deps.validateRequest(&req) + assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID") } -func TestCCPAInvalid(t *testing.T) { +func TestSChainInvalid(t *testing.T) { deps := &endpointDeps{ &nobidExchange{}, newParamsValidator(t), @@ -931,6 +1564,7 @@ func TestCCPAInvalid(t *testing.T) { openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } ui := uint64(1) @@ -950,16 +1584,190 @@ func TestCCPAInvalid(t *testing.T) { ID: "myID", }, Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"invalid by length"}`), + Ext: json.RawMessage(`{"us_privacy":"abcd"}`), }, + Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), } errL := deps.validateRequest(&req) - 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}) + expectedError := fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.") + assert.ElementsMatch(t, errL, []error{expectedError}) +} - assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request") +func TestGetAccountID(t *testing.T) { + testPubID := "test-pub" + testParentAccount := "test-account" + testPubExt := openrtb_ext.ExtPublisher{ + Prebid: &openrtb_ext.ExtPublisherPrebid{ + ParentAccount: &testParentAccount, + }, + } + testPubExtJSON, err := json.Marshal(testPubExt) + assert.NoError(t, err) + + testCases := []struct { + description string + pub *openrtb.Publisher + expectedAccID string + }{ + { + description: "Publisher.ID and Publisher.Ext.Prebid.ParentAccount both present", + pub: &openrtb.Publisher{ + ID: testPubID, + Ext: testPubExtJSON, + }, + expectedAccID: testParentAccount, + }, + { + description: "Only Publisher.Ext.Prebid.ParentAccount present", + pub: &openrtb.Publisher{ + ID: "", + Ext: testPubExtJSON, + }, + expectedAccID: testParentAccount, + }, + { + description: "Only Publisher.ID present", + pub: &openrtb.Publisher{ + ID: testPubID, + }, + expectedAccID: testPubID, + }, + { + description: "Neither Publisher.ID or Publisher.Ext.Prebid.ParentAccount present", + pub: &openrtb.Publisher{}, + expectedAccID: pbsmetrics.PublisherUnknown, + }, + { + description: "Publisher is nil", + pub: nil, + expectedAccID: pbsmetrics.PublisherUnknown, + }, + } + + for _, test := range testCases { + acc := getAccountID(test.pub) + assert.Equal(t, test.expectedAccID, acc, "getAccountID should return expected account for test case: %s", test.description) + } +} + +func TestSanitizeRequest(t *testing.T) { + testCases := []struct { + description string + req *openrtb.BidRequest + ipValidator iputil.IPValidator + expectedIPv4 string + expectedIPv6 string + }{ + { + description: "Empty", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "", + IPv6: "", + }, + }, + expectedIPv4: "", + expectedIPv6: "", + }, + { + description: "Valid", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "1.1.1.1", + IPv6: "1111::", + }, + }, + ipValidator: hardcodedResponseIPValidator{response: true}, + expectedIPv4: "1.1.1.1", + expectedIPv6: "1111::", + }, + { + description: "Invalid", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "1.1.1.1", + IPv6: "1111::", + }, + }, + ipValidator: hardcodedResponseIPValidator{response: false}, + expectedIPv4: "", + expectedIPv6: "", + }, + { + description: "Invalid - Wrong IP Types", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "1111::", + IPv6: "1.1.1.1", + }, + }, + ipValidator: hardcodedResponseIPValidator{response: true}, + expectedIPv4: "", + expectedIPv6: "", + }, + { + description: "Malformed", + req: &openrtb.BidRequest{ + Device: &openrtb.Device{ + IP: "malformed", + IPv6: "malformed", + }, + }, + expectedIPv4: "", + expectedIPv6: "", + }, + } + + for _, test := range testCases { + sanitizeRequest(test.req, test.ipValidator) + assert.Equal(t, test.expectedIPv4, test.req.Device.IP, test.description+":ipv4") + assert.Equal(t, test.expectedIPv6, test.req.Device.IPv6, test.description+":ipv6") + } +} + +func TestValidateAndFillSourceTID(t *testing.T) { + testTID := "some-tid" + testCases := []struct { + description string + req *openrtb.BidRequest + expectRandTID bool + expectedTID string + }{ + { + description: "req.Source not present. Expecting a randomly generated TID value", + req: &openrtb.BidRequest{}, + expectRandTID: true, + }, + { + description: "req.Source.TID not present. Expecting a randomly generated TID value", + req: &openrtb.BidRequest{ + Source: &openrtb.Source{}, + }, + expectRandTID: true, + }, + { + description: "req.Source.TID present. Expecting no change", + req: &openrtb.BidRequest{ + Source: &openrtb.Source{ + TID: testTID, + }, + }, + expectRandTID: false, + expectedTID: testTID, + }, + } + + for _, test := range testCases { + _ = validateAndFillSourceTID(test.req) + if test.expectRandTID { + assert.NotEmpty(t, test.req.Source.TID, test.description) + assert.NotEqual(t, test.expectedTID, test.req.Source.TID, test.description) + } else { + assert.Equal(t, test.expectedTID, test.req.Source.TID, test.description) + } + } } // nobidExchange is a well-behaved exchange which always bids "no bid". @@ -967,28 +1775,70 @@ 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, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { - e.gotRequest = bidRequest +func (e *nobidExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + e.gotRequest = r.BidRequest return &openrtb.BidResponse{ - ID: bidRequest.ID, + ID: r.BidRequest.ID, BidID: "test bid id", NBR: openrtb.NoBidReasonCodeUnknownError.Ptr(), }, nil } -type brokenExchange struct{} - -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.") +type mockBidExchange struct { + gotRequest *openrtb.BidRequest } -func getMessage(t *testing.T, example []byte) []byte { - if value, err := jsonparser.GetString(example, "message"); err != nil { - t.Fatalf("Error parsing root.message from request: %v.", err) - } else { - return []byte(value) +// mockBidExchange is a well-behaved exchange that lists the bidders found in every bidRequest.Imp[i].Ext +// into the bidResponse.Ext to assert the bidder adapters that were not filtered out in the validation process +func (e *mockBidExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + bidResponse := &openrtb.BidResponse{ + ID: r.BidRequest.ID, + BidID: "test bid id", + NBR: openrtb.NoBidReasonCodeUnknownError.Ptr(), } - return nil + if len(r.BidRequest.Imp) > 0 { + var SeatBidMap = make(map[string]openrtb.SeatBid, 0) + for _, imp := range r.BidRequest.Imp { + var bidderExts map[string]json.RawMessage + if err := json.Unmarshal(imp.Ext, &bidderExts); err != nil { + return nil, err + } + + if rawPrebidExt, ok := bidderExts[openrtb_ext.PrebidExtKey]; ok { + var prebidExt openrtb_ext.ExtImpPrebid + if err := json.Unmarshal(rawPrebidExt, &prebidExt); err == nil && prebidExt.Bidder != nil { + for bidder, ext := range prebidExt.Bidder { + if ext == nil { + continue + } + + bidderExts[bidder] = ext + } + } + } + + for bidderNameOrAlias := range bidderExts { + if isBidderToValidate(bidderNameOrAlias) { + if val, ok := SeatBidMap[bidderNameOrAlias]; ok { + val.Bid = append(val.Bid, openrtb.Bid{ID: fmt.Sprintf("%s-bid", bidderNameOrAlias)}) + } else { + SeatBidMap[bidderNameOrAlias] = openrtb.SeatBid{Seat: fmt.Sprintf("%s-bids", bidderNameOrAlias), Bid: []openrtb.Bid{{ID: fmt.Sprintf("%s-bid", bidderNameOrAlias)}}} + } + } + } + } + for _, seatBid := range SeatBidMap { + bidResponse.SeatBid = append(bidResponse.SeatBid, seatBid) + } + } + + return bidResponse, nil +} + +type brokenExchange struct{} + +func (e *brokenExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + return nil, errors.New("Critical, unrecoverable error.") } // StoredRequest testing @@ -1000,7 +1850,7 @@ func getMessage(t *testing.T, example []byte) []byte { // second below is identical to first but with extra '}' for invalid JSON var testStoredRequestData = map[string]json.RawMessage{ "2": json.RawMessage(`{ - "tmax": 500, +"tmax": 500, "ext": { "prebid": { "targeting": { @@ -1010,15 +1860,15 @@ var testStoredRequestData = map[string]json.RawMessage{ } }`), "3": json.RawMessage(`{ - "tmax": 500, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": "low" - } - } - }} - }`), +"tmax": 500, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + } + } + }} + }`), } // Stored Imp Requests @@ -1027,7 +1877,7 @@ var testStoredRequestData = map[string]json.RawMessage{ // third below has valid JSON and matches schema var testStoredImpData = map[string]json.RawMessage{ "1": json.RawMessage(`{ - "id": "adUnit1", +"id": "adUnit1", "ext": { "appnexus": { "placementId": "abc", @@ -1040,7 +1890,7 @@ var testStoredImpData = map[string]json.RawMessage{ } }`), "7": json.RawMessage(`{ - "id": "adUnit1", +"id": "adUnit1", "ext": { "appnexus": { "placementId": 12345678, @@ -1055,7 +1905,7 @@ var testStoredImpData = map[string]json.RawMessage{ } }`), "9": json.RawMessage(`{ - "id": "adUnit1", +"id": "adUnit1", "ext": { "appnexus": { "placementId": 12345678, @@ -1334,12 +2184,27 @@ func (cf mockStoredReqFetcher) FetchRequests(ctx context.Context, requestIDs []s return testStoredRequestData, testStoredImpData, nil } +var mockAccountData = map[string]json.RawMessage{ + "valid_acct": json.RawMessage(`{"disabled":false}`), +} + +type mockAccountFetcher struct { +} + +func (af mockAccountFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + if account, ok := mockAccountData[accountID]; ok { + return account, nil + } else { + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} + } +} + 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, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { - m.lastRequest = bidRequest +func (m *mockExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + m.lastRequest = r.BidRequest return &openrtb.BidResponse{ SeatBid: []openrtb.SeatBid{{ Bid: []openrtb.Bid{{ @@ -1349,20 +2214,6 @@ func (m *mockExchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidR }, nil } -func blankAdapterConfig(bidderList []openrtb_ext.BidderName, disabledBidders []string) map[string]config.Adapter { - adapters := make(map[string]config.Adapter) - for _, b := range bidderList { - adapters[string(b)] = config.Adapter{} - } - for _, b := range disabledBidders { - tmp := adapters[b] - tmp.Disabled = true - adapters[b] = tmp - } - - return adapters -} - func getBidderInfos(cfg map[string]config.Adapter, biddersNames []openrtb_ext.BidderName) adapters.BidderInfos { biddersInfos := make(adapters.BidderInfos) for _, name := range biddersNames { @@ -1385,3 +2236,11 @@ func newBidderInfo(cfg config.Adapter) adapters.BidderInfo { Status: status, } } + +type hardcodedResponseIPValidator struct { + response bool +} + +func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool { + return v.response +} diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index a0ea8214510..551257c2599 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -14,6 +14,7 @@ import ( "github.com/PubMatic-OpenWrap/etree" "github.com/PubMatic-OpenWrap/openrtb" + accountService "github.com/PubMatic-OpenWrap/prebid-server/account" "github.com/PubMatic-OpenWrap/prebid-server/analytics" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/combination" @@ -28,6 +29,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/usersync" + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" "github.com/buger/jsonparser" uuid "github.com/gofrs/uuid" "github.com/golang/glog" @@ -55,7 +57,8 @@ func NewCTVEndpoint( validator openrtb_ext.BidderParamValidator, requestsByID stored_requests.Fetcher, videoFetcher stored_requests.Fetcher, - categories stored_requests.CategoryFetcher, + accounts stored_requests.AccountFetcher, + //categories stored_requests.CategoryFetcher, cfg *config.Configuration, met pbsmetrics.MetricsEngine, pbsAnalytics analytics.PBSAnalyticsModule, @@ -63,18 +66,23 @@ func NewCTVEndpoint( defReqJSON []byte, bidderMap map[string]openrtb_ext.BidderName) (httprouter.Handle, error) { - if ex == nil || validator == nil || requestsByID == nil || cfg == nil || met == nil { + if ex == nil || validator == nil || requestsByID == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewCTVEndpoint requires non-nil arguments.") } defRequest := defReqJSON != nil && len(defReqJSON) > 0 + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + return httprouter.Handle((&ctvEndpointDeps{ endpointDeps: endpointDeps{ ex, validator, requestsByID, videoFetcher, - categories, + accounts, cfg, met, pbsAnalytics, @@ -84,6 +92,7 @@ func NewCTVEndpoint( bidderMap, nil, nil, + ipValidator, }, }).CTVAuctionEndpoint), nil } @@ -157,7 +166,7 @@ func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.R if request.App != nil { deps.labels.Source = pbsmetrics.DemandApp deps.labels.RType = pbsmetrics.ReqTypeVideo - deps.labels.PubID = effectivePubID(request.App.Publisher) + deps.labels.PubID = getAccountID(request.App.Publisher) } else { //request.Site != nil deps.labels.Source = pbsmetrics.DemandWeb if usersyncs.LiveSyncCount() == 0 { @@ -165,18 +174,19 @@ func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.R } else { deps.labels.CookieFlag = pbsmetrics.CookieFlagYes } - deps.labels.PubID = effectivePubID(request.Site.Publisher) + deps.labels.PubID = getAccountID(request.Site.Publisher) } - //Validate Accounts - if err = validateAccount(deps.cfg, deps.labels.PubID); err != nil { - errL = append(errL, err) + deps.ctx = context.Background() + + // Look up account now that we have resolved the pubID value + account, acctIDErrs := accountService.GetAccount(deps.ctx, deps.cfg, deps.accounts, deps.labels.PubID) + if len(acctIDErrs) > 0 { + errL = append(errL, acctIDErrs...) writeError(errL, w, &deps.labels) return } - deps.ctx = context.Background() - //Setting Timeout for Request timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(request.TMax) * time.Millisecond) if timeout > 0 { @@ -185,7 +195,8 @@ func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.R defer cancel() } - response, err = deps.holdAuction(request, usersyncs) + response, err = deps.holdAuction(request, usersyncs, account) + ao.Request = request ao.Response = response if err != nil || nil == response { @@ -234,7 +245,7 @@ func (deps *ctvEndpointDeps) CTVAuctionEndpoint(w http.ResponseWriter, r *http.R } } -func (deps *ctvEndpointDeps) holdAuction(request *openrtb.BidRequest, usersyncs *usersync.PBSCookie) (*openrtb.BidResponse, error) { +func (deps *ctvEndpointDeps) holdAuction(request *openrtb.BidRequest, usersyncs *usersync.PBSCookie, account *config.Account) (*openrtb.BidResponse, error) { defer util.TimeTrack(time.Now(), fmt.Sprintf("Tid:%v CTVHoldAuction", deps.request.ID)) //Hold OpenRTB Standard Auction @@ -243,7 +254,15 @@ 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, nil) + auctionRequest := exchange.AuctionRequest{ + BidRequest: request, + Account: *account, + UserSyncs: usersyncs, + RequestType: deps.labels.RType, + LegacyLabels: deps.labels, + } + + return deps.ex.HoldAuction(deps.ctx, auctionRequest, nil) } /********************* BidRequest Processing *********************/ diff --git a/endpoints/openrtb2/sample-requests/account-required/no-account/not-required-no-acct.json b/endpoints/openrtb2/sample-requests/account-required/no-account/not-required-no-acct.json new file mode 100644 index 00000000000..c3ab09d4883 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/account-required/no-account/not-required-no-acct.json @@ -0,0 +1,84 @@ +{ + "description": "This request comes with no account id and the mock config does not make it a requirement", + "config": { + "accountRequired": false + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + }, + "bidadjustmentfactors": { + "appnexus": 1.01, + "districtm": 0.98, + "rubicon": 0.99 + }, + "cache": { + "bids": {} + }, + "targeting": { + "includewinners": false, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.10 + } + ] + } + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/account-required/no-account/required-no-acct.json b/endpoints/openrtb2/sample-requests/account-required/no-account/required-no-acct.json new file mode 100644 index 00000000000..f6c91918f13 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/account-required/no-account/required-no-acct.json @@ -0,0 +1,68 @@ +{ + "description": "This request comes with no account id and the mock config requires it. We expect an error", + "config": { + "accountRequired": true + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "user": { }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + }, + "bidadjustmentfactors": { + "appnexus": 1.01, + "districtm": 0.98, + "rubicon": 0.99 + }, + "cache": { + "bids": {} + }, + "targeting": { + "includewinners": false, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.10 + } + ] + } + } + } + } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host.\n" +} diff --git a/endpoints/openrtb2/sample-requests/account-required/no-acct.json b/endpoints/openrtb2/sample-requests/account-required/no-acct.json deleted file mode 100644 index d84d797017d..00000000000 --- a/endpoints/openrtb2/sample-requests/account-required/no-acct.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "description": "This request comes with no account id", - "message": "Invalid request: Prebid-server has been configured to discard requests that don't come with an Account ID. Please reach out to the prebid server host.\n", - - "requestPayload": { - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "user": { }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" - } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "tmax": 500, - "ext": { - "prebid": { - "aliases": { - "districtm": "appnexus" - }, - "bidadjustmentfactors": { - "appnexus": 1.01, - "districtm": 0.98, - "rubicon": 0.99 - }, - "cache": { - "bids": {} - }, - "targeting": { - "includewinners": false, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.10 - } - ] - } - } - } - } - } - } - diff --git a/endpoints/openrtb2/sample-requests/account-required/with-acct.json b/endpoints/openrtb2/sample-requests/account-required/valid-acct.json similarity index 79% rename from endpoints/openrtb2/sample-requests/account-required/with-acct.json rename to endpoints/openrtb2/sample-requests/account-required/valid-acct.json index fb4c6313051..15e72323c8e 100644 --- a/endpoints/openrtb2/sample-requests/account-required/with-acct.json +++ b/endpoints/openrtb2/sample-requests/account-required/valid-acct.json @@ -1,12 +1,12 @@ { - "description": "This request comes with no account id", - "message": "Invalid request: Prebid-server has been configured to discard requests that don't come with an Account ID. Please reach out to the prebid server host.\n", - + "description": "This request comes with a valid account id", + "message": "", + "requestPayload": { "id": "some-request-id", "site": { - "publisher": { "id": "not_bad_acct"}, - "page": "test.somepage.com" + "publisher": { "id": "valid_acct"}, + "page": "test.somepage.com" }, "user": { }, "imp": [ @@ -64,4 +64,4 @@ } } } - + diff --git a/endpoints/openrtb2/sample-requests/account-required/with-account/required-blacklisted-acct.json b/endpoints/openrtb2/sample-requests/account-required/with-account/required-blacklisted-acct.json new file mode 100644 index 00000000000..894a92fdb27 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/account-required/with-account/required-blacklisted-acct.json @@ -0,0 +1,91 @@ +{ + "description": "Account is required but request comes with a blacklisted account id", + "config": { + "accountRequired": true, + "blacklistedAccts": ["bad_acct"] + }, + "mockBidRequest": { + "id": "some-request-id", + "user": { + "ext": { + "consent": "gdpr-consent-string", + "prebid": { + "buyeruids": { + "appnexus": "override-appnexus-id-in-cookie" + } + } + } + }, + "app": { + "id": "cool_app", + "publisher": { + "id": "bad_acct" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + }, + "districtm": { + "placementId": 105 + }, + "rubicon": { + "accountId": 1001, + "siteId": 113932, + "zoneId": 535510 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + }, + "bidadjustmentfactors": { + "appnexus": 1.01, + "districtm": 0.98, + "rubicon": 0.99 + }, + "cache": { + "bids": {} + }, + "targeting": { + "includewinners": false, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.10 + } + ] + } + } + } + } + }, + "expectedReturnCode": 503, + "expectedErrorMessage": "Invalid request: Prebid-server has disabled Account ID: bad_acct, please reach out to the prebid server host.\n" +} diff --git a/endpoints/openrtb2/sample-requests/account-required/with-account/required-with-acct.json b/endpoints/openrtb2/sample-requests/account-required/with-account/required-with-acct.json new file mode 100644 index 00000000000..a72d184c81c --- /dev/null +++ b/endpoints/openrtb2/sample-requests/account-required/with-account/required-with-acct.json @@ -0,0 +1,86 @@ +{ + "description": "This request comes with an account id and which is required by the config", + "config": { + "accountRequired": true + }, + + "mockBidRequest": { + "id": "some-request-id", + "site": { + "publisher": { "id": "not_bad_acct"}, + "page": "test.somepage.com" + }, + "user": { }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + }, + "bidadjustmentfactors": { + "appnexus": 1.01, + "districtm": 0.98, + "rubicon": 0.99 + }, + "cache": { + "bids": {} + }, + "targeting": { + "includewinners": false, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.10 + } + ] + } + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/aliased/multiple-alias.json b/endpoints/openrtb2/sample-requests/aliased/multiple-alias.json new file mode 100644 index 00000000000..55e45041e6e --- /dev/null +++ b/endpoints/openrtb2/sample-requests/aliased/multiple-alias.json @@ -0,0 +1,93 @@ +{ + "description": "Imp extension comes with a valid bidder name and valid bidder aliases as defined in the config.aliases list. Given that 'alias1' refers to the 'appnexus' bidder, we only bid appnexus once.", + "config": { + "aliases": "{\"ext\":{\"prebid\":{\"aliases\":{\"alias1\":\"appnexus\",\"alias2\":\"rubicon\"}}}}" + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + }, + "alias1": { + "placementId": 12883451 + }, + "alias2": { + "accountId": 1001, + "siteId": 113932, + "zoneId": 535510 + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "alias1-bid", + "impid": "", + "price": 0 + } + ], + "seat": "alias1-bids" + }, + { + "bid": [ + { + "id": "alias2-bid", + "impid": "", + "price": 0 + } + ], + "seat": "alias2-bids" + }, + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ], + "bidid": "test bid id", + "nbr": 0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/aliased/simple.json b/endpoints/openrtb2/sample-requests/aliased/simple.json index e7f6ba21b83..a99907ab370 100644 --- a/endpoints/openrtb2/sample-requests/aliased/simple.json +++ b/endpoints/openrtb2/sample-requests/aliased/simple.json @@ -1,4 +1,9 @@ { + "description": "Imp extension doesn't come with valid bidder name but does come with valid bidder alias as defined in the mockAliases list.", + "config": { + "aliases": "{\"ext\":{\"prebid\":{\"aliases\":{\"alias1\":\"appnexus\"}}}}" + }, + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -12,10 +17,29 @@ ] }, "ext": { - "test1": { + "alias1": { "placementId": 12883451 } } } ] - } \ No newline at end of file + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "alias1-bid", + "impid": "", + "price": 0 + } + ], + "seat": "alias1-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-acct.json b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-acct.json deleted file mode 100644 index ee04a9464e9..00000000000 --- a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-acct.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "description": "This is a perfectly valid request except that it comes from a blacklisted Account", - "message": "Invalid request: Prebid-server has blacklisted Account ID: bad_acct, please reach out to the prebid server host.\n", - - "requestPayload": { - "id": "some-request-id", - "user": { - "ext": { - "consent": "gdpr-consent-string", - "prebid": { - "buyeruids": { - "appnexus": "override-appnexus-id-in-cookie" - } - } - } - }, - "app": { - "id": "cool_app", - "publisher": { - "id": "bad_acct" - } - }, - "regs": { - "ext": { - "gdpr": 1 - } - }, - "imp": [ - { - "id": "some-impression-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - }, - "districtm": { - "placementId": 105 - }, - "rubicon": { - "accountId": 1001, - "siteId": 113932, - "zoneId": 535510 - } - } - } - ], - "tmax": 500, - "ext": { - "prebid": { - "aliases": { - "districtm": "appnexus" - }, - "bidadjustmentfactors": { - "appnexus": 1.01, - "districtm": 0.98, - "rubicon": 0.99 - }, - "cache": { - "bids": {} - }, - "targeting": { - "includewinners": false, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "max": 20, - "increment": 0.10 - } - ] - } - } - } - } - } - } - diff --git a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app-publisher.json b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app-publisher.json new file mode 100644 index 00000000000..ef7a93b8bad --- /dev/null +++ b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app-publisher.json @@ -0,0 +1,90 @@ +{ + "description": "This is a perfectly valid request except that it comes with a blacklisted app publisher account", + "config": { + "blacklistedAccts": ["bad_acct"] + }, + "mockBidRequest": { + "id": "some-request-id", + "user": { + "ext": { + "consent": "gdpr-consent-string", + "prebid": { + "buyeruids": { + "appnexus": "override-appnexus-id-in-cookie" + } + } + } + }, + "app": { + "id": "cool_app", + "publisher": { + "id": "bad_acct" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + }, + "districtm": { + "placementId": 105 + }, + "rubicon": { + "accountId": 1001, + "siteId": 113932, + "zoneId": 535510 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + }, + "bidadjustmentfactors": { + "appnexus": 1.01, + "districtm": 0.98, + "rubicon": 0.99 + }, + "cache": { + "bids": {} + }, + "targeting": { + "includewinners": false, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.10 + } + ] + } + } + } + } + }, + "expectedReturnCode": 503, + "expectedErrorMessage": "Invalid request: Prebid-server has disabled Account ID: bad_acct, please reach out to the prebid server host.\n" +} diff --git a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json index 1ace4b53666..120fcec08f4 100644 --- a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json +++ b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json @@ -1,8 +1,9 @@ { "description": "This is a perfectly valid request except that it comes from a blacklisted App", - "message": "Invalid request: Prebid-server does not process requests from App ID: spam_app\n", - - "requestPayload": { + "config": { + "blacklistedApps": ["spam_app"] + }, + "mockBidRequest": { "id": "some-request-id", "user": { "ext": { @@ -80,5 +81,7 @@ } } } - } + }, + "expectedReturnCode": 503, + "expectedErrorMessage": "Invalid request: Prebid-server does not process requests from App ID: spam_app\n" } diff --git a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-site-publisher.json b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-site-publisher.json new file mode 100644 index 00000000000..cdec20d22ba --- /dev/null +++ b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-site-publisher.json @@ -0,0 +1,90 @@ +{ + "description": "This is a perfectly valid request except that it comes with a blacklisted site publisher account", + "config": { + "blacklistedAccts": ["bad_acct"] + }, + "mockBidRequest": { + "id": "some-request-id", + "user": { + "ext": { + "consent": "gdpr-consent-string", + "prebid": { + "buyeruids": { + "appnexus": "override-appnexus-id-in-cookie" + } + } + } + }, + "site": { + "id": "cool_site", + "publisher": { + "id": "bad_acct" + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + }, + "districtm": { + "placementId": 105 + }, + "rubicon": { + "accountId": 1001, + "siteId": 113932, + "zoneId": 535510 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "aliases": { + "districtm": "appnexus" + }, + "bidadjustmentfactors": { + "appnexus": 1.01, + "districtm": 0.98, + "rubicon": 0.99 + }, + "cache": { + "bids": {} + }, + "targeting": { + "includewinners": false, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.10 + } + ] + } + } + } + } + }, + "expectedReturnCode": 503, + "expectedErrorMessage": "Invalid request: Prebid-server has disabled Account ID: bad_acct, please reach out to the prebid server host.\n" +} diff --git a/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json b/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json index 096c028cfe9..f4379dc09a2 100644 --- a/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json +++ b/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json @@ -1,6 +1,9 @@ { - "message": "Invalid request: request.ext.prebid.aliases.test1 refers to unknown bidder: appnexus\n", - "requestPayload": { + "description": "Request comes with an alias to a disabled bidder, we should throw error", + "config": { + "disabledAdapters": ["appnexus", "rubicon"] + }, + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -27,5 +30,7 @@ } } } - } -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.ext.prebid.aliases.test1 refers to unknown bidder: appnexus\n" +} diff --git a/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json b/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json index 5f637b1a4fc..91e760ee41e 100644 --- a/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json +++ b/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json @@ -1,6 +1,9 @@ { - "message": "Invalid request: Bidder \"appnexus\" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.\nInvalid request: request.imp[0].ext must contain at least one bidder with valid parameters\n", - "requestPayload": { + "description": "Bid request targeted towards a disabled adapter. We expect an error.", + "config": { + "disabledAdapters": ["appnexus"] + }, + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -20,5 +23,7 @@ } } ] - } -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Bidder \"appnexus\" has been disabled on this instance of Prebid Server. Please work with the PBS host to enable this bidder again.\nInvalid request: request.imp[0].ext must contain at least one bidder\n" +} diff --git a/endpoints/openrtb2/sample-requests/disabled/good/partial.json b/endpoints/openrtb2/sample-requests/disabled/good/partial.json index fe0c492be2d..3549abaa934 100644 --- a/endpoints/openrtb2/sample-requests/disabled/good/partial.json +++ b/endpoints/openrtb2/sample-requests/disabled/good/partial.json @@ -1,4 +1,9 @@ { + "description": "Request comes with some imps directed toward disabled adapters, but there's one non-disabled adapter and we expect a successful response", + "config": { + "disabledAdapters": ["appnexus", "rubicon"] + }, + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -49,4 +54,23 @@ } } } - } \ No newline at end of file + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "openx-bid", + "impid": "", + "price": 0 + } + ], + "seat": "openx-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json new file mode 100644 index 00000000000..74dede0857f --- /dev/null +++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json @@ -0,0 +1,50 @@ +{ + "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.", + + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "expectedBidResponse": { + "id":"some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ], + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json new file mode 100644 index 00000000000..41461813c40 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json @@ -0,0 +1,54 @@ +{ + "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.", + + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 12883451 + } + } + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "expectedBidResponse": { + "id":"some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ], + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-data-invalid-type.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-data-invalid-type.json index 06eb73593b4..70bcd4bb94e 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-data-invalid-type.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-data-invalid-type.json @@ -1,11 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "data": { - "type": 364 - } + "description": "Native request with an invalid value in its imp.native.request.data.type field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"data\":{\"type\":364}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-data-no-type.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-data-no-type.json index 6dfe9c400dd..02fcd52be5a 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-data-no-type.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-data-no-type.json @@ -1,9 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "data": {} + "description": "Native request with missing imp.native.request.data.type field value", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"data\":{}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-empty.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-empty.json index 78e2feb7c79..476d1d8fef5 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-empty.json @@ -1,5 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [] -} \ No newline at end of file + "description": "Native request with an empty assets array in its imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-h-negative.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-h-negative.json index 07133c6824b..dec97928777 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-h-negative.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-h-negative.json @@ -1,12 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "img": { - "h": -30, - "w": 20 - } + "description": "Native request with a negative height for its image asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"img\":{\"h\":-30,\"w\":20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-hmin-negative.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-hmin-negative.json index d0654882937..eed0a4905e0 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-hmin-negative.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-hmin-negative.json @@ -1,12 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "img": { - "hmin": -30, - "wmin": 20 - } + "description": "Native request with a negative hmin value in its image asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"img\":{\"hmin\":-30,\"wmin\":20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-w-negative.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-w-negative.json index 8724b76ce24..b7a75f13fe2 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-w-negative.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-w-negative.json @@ -1,12 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "img": { - "h": 30, - "w": -20 - } + "description": "Native request with a negative width value in its image asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"img\":{\"h\":30,\"w\":-20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-wmin-negative.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-wmin-negative.json index 3b81cfd6cb5..ddc8d502287 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-img-wmin-negative.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-img-wmin-negative.json @@ -1,12 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "img": { - "hmin": 30, - "wmin": -20 - } + "description": "Native request with a negative wmin value in its image asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"img\":{\"hmin\":30,\"wmin\":-20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-mixed-type.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-mixed-type.json index 90642cbac18..cfe531af026 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-mixed-type.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-mixed-type.json @@ -1,15 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 140 - }, - "img": { - "wmin": 20, - "hmin": 30 - } + "description": "Native request with mixed asset type in the sole element of the assets arary in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":140},\"img\":{\"wmin\":20,\"hmin\":30}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-title-empty.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-title-empty.json new file mode 100644 index 00000000000..061af88e147 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-title-empty.json @@ -0,0 +1,25 @@ +{ + "description": "Native request with missing length property in its title asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"title\":{}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-title-no-length.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-title-no-length.json deleted file mode 100644 index 4b227e3f47f..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-title-no-length.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "context": 1, - "plcmttype": 1, - "assets": [ - { - "title": {} - } - ] -} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-mimes-empty.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-mimes-empty.json index 74be7817ceb..649f4c5268b 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-mimes-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-mimes-empty.json @@ -1,14 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "video": { - "mimes": [], - "minduration": 30, - "maxduration": 120, - "protocols": [3] - } + "description": "Native request with empty mimes array in its video asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"video\":{\"mimes\":[],\"minduration\":30,\"maxduration\":120,\"protocols\":[3]}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-maxduration.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-maxduration.json index 5b733aba518..d1faa83b2e4 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-maxduration.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-maxduration.json @@ -1,13 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "video": { - "mimes": ["video/mp4"], - "minduration": 30, - "protocols": [3] - } + "description": "Native request with missing maxduration value in a video asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":30,\"protocols\":[3]}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-mimes.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-mimes.json index 270dccb7cf3..d8792ac7ab3 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-mimes.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-mimes.json @@ -1,13 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "video": { - "minduration": 30, - "maxduration": 120, - "protocols": [3] - } + "description": "Native request with missing mimes array in a video asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"video\":{\"minduration\":30,\"maxduration\":120,\"protocols\":[3]}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-minduration.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-minduration.json index 28f5e7de1c8..d11786b6bc7 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-minduration.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-minduration.json @@ -1,13 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "video": { - "mimes": ["video/mp4"], - "maxduration": 120, - "protocols": [3] - } + "description": "Native request with missing minduration value in a video asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"video\":{\"mimes\":[\"video/mp4\"],\"maxduration\":120,\"protocols\":[3]}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-protocols.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-protocols.json index 59d6f6a5541..adaed92254f 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-protocols.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-no-protocols.json @@ -1,13 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "video": { - "mimes": ["video/mp4"], - "minduration": 30, - "maxduration": 120 - } + "description": "Native request with missing protocols array in a video asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":30,\"maxduration\":120}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-empty.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-empty.json index 4f7616f9da2..018e0168537 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-empty.json @@ -1,14 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "video": { - "mimes": ["video/mp4"], - "minduration": 30, - "maxduration": 120, - "protocols": [] - } + "description": "Native request with empty protocols array in a video asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":30,\"maxduration\":120,\"protocols\":[]}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-invalid.json b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-invalid.json index eb54c644206..1531e45f3e5 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/asset-video-protocols-invalid.json @@ -1,14 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "video": { - "mimes": ["video/mp4"], - "minduration": 30, - "maxduration": 120, - "protocols": [97] - } + "description": "Native request with an out of scope protocol value in a video asset in the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":30,\"maxduration\":120,\"protocols\":[97]}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/assets-with-dup-ids.json b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-dup-ids.json index dfece8cfb0e..91d8c51a317 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/assets-with-dup-ids.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-dup-ids.json @@ -1,18 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "id": 1, - "img": { - "wmin": 30 - } + "description": "Native request listing elements with duplicate ids in the assets array the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"id\":1,\"img\":{\"wmin\":30}},{\"id\":1,\"title\":{\"len\":20}}]}" }, - { - "id": 1, - "title": { - "len": 20 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" } diff --git a/endpoints/openrtb2/sample-requests/invalid-native/assets-with-partial-ids.json b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-partial-ids.json index 291ae8d77b1..a485532e042 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/assets-with-partial-ids.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/assets-with-partial-ids.json @@ -1,22 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "id": 1, - "img": { - "wmin": 30 - } + "description": "Native request listing some assets array elements with no id value inside the imp.native.request field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"id\":1,\"img\":{\"wmin\":30}},{\"title\":{\"len\":20}},{\"img\":{\"wmin\":50}}]}" }, - { - "title": { - "len": 20 - } - }, - { - "img": { - "wmin": 50 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" } diff --git a/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-invalid.json b/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-invalid.json index 89a9f83eae0..395e7034b0c 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-invalid.json @@ -1,31 +1,25 @@ { - "context": 1, - "contextsubtype": 21, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } + "description": "Bid request with invalid contextsubtype value inside the native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"contextsubtype\":21,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}]}" }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" } diff --git a/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-negative.json b/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-negative.json index ead42f5701e..c37edd6bb08 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-negative.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/contextsubtype-negative.json @@ -1,31 +1,25 @@ { - "context": 1, - "contextsubtype": -1, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } + "description": "Bid request with a negative contextsubtype value inside the native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"contextsubtype\":-1,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}]}" }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" } diff --git a/endpoints/openrtb2/sample-requests/invalid-native/empty-object.json b/endpoints/openrtb2/sample-requests/invalid-native/empty-object.json index 9e26dfeeb6e..3833c25746b 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/empty-object.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/empty-object.json @@ -1 +1,25 @@ -{} \ No newline at end of file +{ + "description": "Bid request with an empty native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].native.request.assets must be an array containing at least one object" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/empty.json b/endpoints/openrtb2/sample-requests/invalid-native/empty.json index e69de29bb2d..4193ebeedd2 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/empty.json @@ -0,0 +1,25 @@ +{ + "description": "Bid request with an empty native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-empty.json b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-empty.json index 8c133fe9eca..d9e10ad2179 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-empty.json @@ -1,31 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } + "description": "Bid request with empty eventtrackers array element inside the native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}],\"eventtrackers\":[{}]}" }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } - ], - "eventtrackers": [{}] + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" } diff --git a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-event-large.json b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-event-large.json new file mode 100644 index 00000000000..1cdc3c8bbc7 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-event-large.json @@ -0,0 +1,25 @@ +{ + "description": "Bid request with empty eventtrackers array inside the native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}],\"eventtrackers\":[{\"event\":5,\"methods\":[2]}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-empty.json b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-empty.json index f5a48710da7..90c49413ef5 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-empty.json @@ -1,34 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } + "description": "Bid request with empty methods array inside the native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}],\"eventtrackers\":[{\"event\":1,\"methods\":[]}]}" }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } - ], - "eventtrackers": [{ - "event": 1, - "methods": [] - }] + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" } diff --git a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-large.json b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-large.json index f5a48710da7..8b148b1fa27 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-large.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-methods-large.json @@ -1,34 +1,25 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } + "description": "Bid request with empty methods array inside the native.request field in its only imp element", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}],\"eventtrackers\":[{\"event\":1,\"methods\":[3]}]}" }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } - ], - "eventtrackers": [{ - "event": 1, - "methods": [] - }] + } + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" } diff --git a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-type-large.json b/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-type-large.json deleted file mode 100644 index d0d666ac186..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-native/eventtracker-type-large.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "context": 1, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } - }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } - } - ], - "eventtrackers": [{ - "event": 1, - "methods": [5] - }] -} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/request-context-invalid.json b/endpoints/openrtb2/sample-requests/invalid-native/request-context-invalid.json index e005815ad3e..09da5d165ae 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/request-context-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/request-context-invalid.json @@ -1,12 +1,25 @@ { - "context": 376, - "plcmttype": 2, - "assets": [ - { - "img": { - "hmin": 30, - "wmin": 20 - } + "description": "Native request with an imp.native.request.context value that doesn't match that of the context_sub_type list", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":376,\"plcmttype\":2,\"assets\":[{\"img\":{\"hmin\":30,\"wmin\":20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-native/request-plcmttype-invalid.json b/endpoints/openrtb2/sample-requests/invalid-native/request-plcmttype-invalid.json index e34b17e5d4d..c2104bcd4b5 100644 --- a/endpoints/openrtb2/sample-requests/invalid-native/request-plcmttype-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-native/request-plcmttype-invalid.json @@ -1,12 +1,25 @@ { - "context": 1, - "plcmttype": 423, - "assets": [ - { - "img": { - "hmin": 30, - "wmin": 20 - } + "description": "Native request with an imp.native.request.plcmttype value that doesn't match that of the placement_type list", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":423,\"assets\":[{\"img\":{\"hmin\":30,\"wmin\":20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json index 2a647d7d8c8..812f0664fbb 100644 --- a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json +++ b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json @@ -1,41 +1,15 @@ { - "description": "Otherwise valid request using stored request; incoming request has a comma after the closing curly brace of the second set of dimensions to yield invalid JSON", - - "message": "Invalid request: Invalid JSON in Incoming Request: invalid character ']' looking for beginning of value at offset 377\n", - "requestPayload": { - "id": "some-request-id", - "site": { - "page": "prebid.org" - }, - "imp": [ - { - "id": "some-impression-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - }, - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "tmax": 500, - "ext": { - "prebid": { - "storedrequest": { - "id": "2" - } + "description": "Otherwise valid request using stored request; incoming request has a comma after the closing curly brace of the second set of dimensions to yield invalid JSON", + "mockBidRequest": { + "imp": [{},], + "ext": { + "prebid": { + "storedrequest": { + "id": "2" } } } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Invalid JSON in Incoming Request: invalid character ']' looking for beginning of value at offset" } diff --git a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_2.json b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_2.json deleted file mode 100644 index d18a20d7a13..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_2.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "description": "Otherwise valid request using stored request; incoming request lacks a comma after first width to yield invalid JSON", - - "message": "Invalid request: Invalid JSON in Incoming Request: invalid character '\"' after object key:value pair at offset 254\n", - "requestPayload": { - "id": "some-request-id", - "site": { - "page": "prebid.org" - }, - "imp": [ - { - "id": "some-impression-id", - "banner": { - "format": [ - { - "w": 300 - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "tmax": 500, - "ext": { - "prebid": { - "storedrequest": { - "id": "2" - } - } - } - } -} diff --git a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_imp.json b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_imp.json index 0898b9da55a..e3960b17399 100644 --- a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_imp.json +++ b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_imp.json @@ -1,8 +1,7 @@ { "description": "Otherwise valid request but without comma after first width field in first imp to yield invalid JSON, using stored imp request", - "message": "Invalid request: Invalid JSON in Imp[0] of Incoming Request: invalid character '\"' after object key:value pair at offset 132\n", - "requestPayload": { + "mockBidRequest": { "id": "some-request-id", "site": { "page": "prebid.org" @@ -36,5 +35,7 @@ } ], "tmax": 500 - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Invalid JSON in Imp[0] of Incoming Request: invalid character '\"' after object key:value pair at offset 132\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_imp.json b/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_imp.json index 1847fd4108a..0fed6c32adf 100644 --- a/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_imp.json +++ b/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_imp.json @@ -1,8 +1,7 @@ { "description": "Valid request using stored imp request which has missing comma to yield invalid JSON", - "message": "Invalid request: imp.ext.prebid.storedrequest.id 7: Stored Imp has Invalid JSON: invalid character '\"' after object key:value pair at offset 185\n", - "requestPayload": { + "mockBidRequest": { "id": "some-request-id", "site": { "page": "prebid.org" @@ -32,5 +31,7 @@ } ], "tmax": 500 - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: imp.ext.prebid.storedrequest.id 7: Stored Imp has Invalid JSON" } diff --git a/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_req.json b/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_req.json index 46df45d67da..4e9d7f03352 100644 --- a/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_req.json +++ b/endpoints/openrtb2/sample-requests/invalid-stored/bad_stored_req.json @@ -1,26 +1,27 @@ { - "description": "Valid request that uses stored request with extra curly brace so stored request is not valid JSON", + "description": "Valid request that uses stored request with extra curly brace so stored request is not valid JSON", - "message": "Invalid request: ext.prebid.storedrequest.id refers to Stored Request 3 which contains Invalid JSON: invalid character '}' after top-level value at offset 293\n", - "requestPayload": { - "id": "ThisID", - "imp": [ - { - "ext": { - "prebid": { - "storedrequest": { - "id": "1" - } + "mockBidRequest": { + "id": "ThisID", + "imp": [ + { + "ext": { + "prebid": { + "storedrequest": { + "id": "1" } } } - ], - "ext": { - "prebid": { - "storedrequest": { - "id": "3" - } + } + ], + "ext": { + "prebid": { + "storedrequest": { + "id": "3" } } } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: ext.prebid.storedrequest.id refers to Stored Request 3 which contains Invalid JSON" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json b/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json index 666253ec85b..5e0aa5a989b 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.ext.prebid.aliases.appnexus defines a no-op alias. Choose a different alias, or remove this entry.\n", - "requestPayload": { + "description": "Request with invalid alias in the root extension field", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -20,10 +20,12 @@ ], "ext": { "prebid": { - "aliases": { - "appnexus": "appnexus" - } + "aliases": { + "appnexus": "appnexus" + } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.ext.prebid.aliases.appnexus defines a no-op alias. Choose a different alias, or remove this entry.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json b/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json index 6c1925d65b1..f7b5597d59f 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.ext.prebid.aliases.unknown refers to unknown bidder: other-unknown\n", - "requestPayload": { + "description": "Request targets an unknown bidder", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -11,7 +11,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "unknown": { @@ -27,5 +27,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.ext.prebid.aliases.unknown refers to unknown bidder: other-unknown\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/app-bad-ext.json b/endpoints/openrtb2/sample-requests/invalid-whole/app-bad-ext.json deleted file mode 100644 index 672b05724c8..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/app-bad-ext.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "message": "Invalid request: json: cannot unmarshal object into Go struct field ExtAppPrebid.source of type string\n", - "requestPayload": { - "id": "some-request-id", - "app": { - "ext": { - "prebid": { - "source": { - "foo": "prebid-mobile" - }, - "version": "1.0" - } - } - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" - } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ] - } -} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/array.json b/endpoints/openrtb2/sample-requests/invalid-whole/array.json deleted file mode 100644 index bbdf8c57de8..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/array.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Invalid request: json: cannot unmarshal array into Go value of type openrtb.BidRequest\n", - "requestPayload": [] -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/audio-mimes-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/audio-mimes-empty.json index 2e0e299923a..6ade27dd926 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/audio-mimes-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/audio-mimes-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].audio.mimes must contain at least one supported MIME type\n", - "requestPayload": { + "description": "Empty request.imp[0].audio.mimes array", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -13,5 +13,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].audio.mimes must contain at least one supported MIME type\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json index 515824e65d9..00e7bbbbc68 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n", - "requestPayload": { + "description": "Request invalid because banner is missing width value", + "mockBidRequest": { "id":"req-id", "site": { "id": "some-site" @@ -18,5 +18,7 @@ } } ] - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-zero.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-zero.json deleted file mode 100644 index f18f63e5e28..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-zero.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "message": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n", - "requestPayload": { - "id":"req-id", - "site": { - "id": "some-site" - }, - "imp": [ - { - "id":"imp-id", - "banner":{ - "h": 0, - "w": 250 - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ] - } -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmax.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmax.json index 1cb04fc5c1d..d97f358c328 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmax.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmax.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].banner uses unsupported property: \"hmax\". Use the \"format\" array instead.\n", - "requestPayload": { + "description": "Request comes with unsupported property 'hmax'", + "mockBidRequest": { "id":"req-id", "imp": [ { @@ -13,5 +13,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].banner uses unsupported property: \"hmax\". Use the \"format\" array instead.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmin.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmin.json index 38eeeb612e0..5e65b845bc9 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmin.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-hmin.json @@ -1,17 +1,19 @@ { - "message": "Invalid request: request.imp[0].banner uses unsupported property: \"hmin\". Use the \"format\" array instead.\n", - "requestPayload": { - "id":"req-id", - "imp": [ - { - "id":"imp-id", - "banner": { - "hmin":50 - } + "description": "Request comes with unsupported property 'hmin'", + "mockBidRequest": { + "id":"req-id", + "imp": [ + { + "id":"imp-id", + "banner": { + "hmin":50 } - ], - "app": { - "id": "app_001" } + ], + "app": { + "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].banner uses unsupported property: \"hmin\". Use the \"format\" array instead.\n" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-null.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-null.json deleted file mode 100644 index bd8beef8868..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-null.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "message": "Invalid request: request.imp[0] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"\n", - "requestPayload": { - "id":"req-id", - "imp": [ - { - "id": "imp-id", - "banner": null - } - ], - "app": { - "id": "app_001" - } - } -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json index 70739a65834..7f1abcf7de8 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n", - "requestPayload": { + "description": "Request comes with a banner that defines no 'h' value", + "mockBidRequest": { "id":"req-id", "site": { "id": "some-site" @@ -18,5 +18,7 @@ } } ] - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-zero.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-zero.json deleted file mode 100644 index b3453ab4cb7..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-zero.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "message": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n", - "requestPayload": { - "id":"req-id", - "site": { - "id": "some-site" - }, - "imp": [ - { - "id": "imp-id", - "banner": { - "h": 300, - "w": 0 - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ] - } -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmax.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmax.json index f1b7d0aeb96..3d30926ce34 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmax.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmax.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].banner uses unsupported property: \"wmax\". Use the \"format\" array instead.\n", - "requestPayload": { + "description": "Request comes with unsupported property 'wmax'", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -13,5 +13,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].banner uses unsupported property: \"wmax\". Use the \"format\" array instead.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmin.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmin.json index a39e1539ab9..235ff94a031 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmin.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-wmin.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].banner uses unsupported property: \"wmin\". Use the \"format\" array instead.\n", - "requestPayload": { + "description": "Request comes with unsupported property 'wmin'", + "mockBidRequest": { "id":"req-id", "imp": [ { @@ -13,5 +13,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].banner uses unsupported property: \"wmin\". Use the \"format\" array instead.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json index 569e16d2d20..90580bf65ff 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.ext.prebid.bidadjustmentfactors.unknown is not a known bidder or alias\n", - "requestPayload": { + "description": "Bid adjustment factor assigned to an unknown bidder", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -25,5 +25,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.ext.prebid.bidadjustmentfactors.unknown is not a known bidder or alias\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json index 4db6ee09bd8..43d51ac20cb 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.ext.prebid.bidadjustmentfactors.appnexus must be a positive number. Got -2.000000\n", - "requestPayload": { + "description": "Negative bid adjustment factor", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -25,5 +25,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.ext.prebid.bidadjustmentfactors.appnexus must be a positive number. Got -2.000000\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/boolean.json b/endpoints/openrtb2/sample-requests/invalid-whole/boolean.json deleted file mode 100644 index a807200f732..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/boolean.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Invalid request: json: cannot unmarshal bool into Go value of type openrtb.BidRequest\n", - "requestPayload": false -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json index d4b875498ae..76dc1c2bb5c 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the \"bids\" or \"vastml\" properties\n", - "requestPayload": { + "description": "Empty cache field in root level extension", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -11,7 +11,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "appnexus": { @@ -25,5 +25,7 @@ "cache": {} } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the \"bids\" or \"vastxml\" properties\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/deal-no-id.json b/endpoints/openrtb2/sample-requests/invalid-whole/deal-no-id.json index ffb3e19cfbc..5c992b49d38 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/deal-no-id.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/deal-no-id.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].pmp.deals[0] missing required field: \"id\"\n", - "requestPayload": { + "description": "Empty id field in the deals array of the pmp field", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -8,7 +8,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "pmp": { "deals": [ @@ -23,5 +23,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].pmp.deals[0] missing required field: \"id\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json b/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json index 1be93853a0b..1fb7169fced 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user contains a digitrust object that is not valid.\n", - "requestPayload": { + "description": "Invalid digitrust object in user extension", + "mockBidRequest": { "id": "request-with-invalid-digitrust-obj", "site": { "page": "test.somepage.com" @@ -40,5 +40,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user contains a digitrust object that is not valid.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/empty-object.json b/endpoints/openrtb2/sample-requests/invalid-whole/empty-object.json index a17e6d160e5..26c54e6828d 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/empty-object.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/empty-object.json @@ -1,4 +1,6 @@ { - "message": "Invalid request: request missing required field: \"id\"\n", - "requestPayload": {} + "message": "Empty bid request, expect error", + "mockBidRequest": {}, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request missing required field: \"id\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/float.json b/endpoints/openrtb2/sample-requests/invalid-whole/float.json deleted file mode 100644 index 489f258a0f2..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/float.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Invalid request: json: cannot unmarshal number into Go value of type openrtb.BidRequest\n", - "requestPayload": 6.3 -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json b/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json index 15e41cc5fb2..94722cc3253 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n", - "requestPayload": { + "description": "Banner with empty format array does not define width nor height in w and h fields", + "mockBidRequest": { "id":"req-id", "site": { "id": "some-site" @@ -18,5 +18,7 @@ } } ] - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-object.json b/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-object.json index 9117cad5d45..369722f453d 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-object.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-object.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: Request imp[0].banner.format[0] should define *either* {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero.\n", - "requestPayload": { + "description": "Banner does not define width nor height in format array element nor comes with w and h field values", + "mockBidRequest": { "id":"req-id", "imp": [ { @@ -13,5 +13,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Request imp[0].banner.format[0] should define *either* {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/format-no-height.json b/endpoints/openrtb2/sample-requests/invalid-whole/format-no-height.json index 5b5b2e64e29..919504d51ee 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/format-no-height.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/format-no-height.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: Request imp[0].banner.format[0] must define non-zero \"h\" and \"w\" properties.\n", - "requestPayload": { + "description": "Banner comes with a zero height value in format array element", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -18,5 +18,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Request imp[0].banner.format[0] must define non-zero \"h\" and \"w\" properties.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/format-no-hratio.json b/endpoints/openrtb2/sample-requests/invalid-whole/format-no-hratio.json index d5a404d2059..49756a5b708 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/format-no-hratio.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/format-no-hratio.json @@ -1,21 +1,23 @@ { - "message": "Invalid request: Request imp[0].banner.format[0] must define non-zero \"wmin\", \"wratio\", and \"hratio\" properties.\n", - "requestPayload": { - "id": "req-id", - "imp": [ - { - "id": "imp-id", - "banner": { - "format": [ - { - "wratio": 30 - } - ] - } - } - ], - "app": { - "id": "app_001" + "description": "Banner comes with a zero hratio value", + "mockBidRequest": { + "id": "req-id", + "imp": [ + { + "id": "imp-id", + "banner": { + "format": [ + { + "wratio": 30 + } + ] } - } + } + ], + "app": { + "id": "app_001" + } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Request imp[0].banner.format[0] must define non-zero \"wmin\", \"wratio\", and \"hratio\" properties.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/format-two-widths.json b/endpoints/openrtb2/sample-requests/invalid-whole/format-two-widths.json index cbe7b4af663..387299b2115 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/format-two-widths.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/format-two-widths.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" objects in the request.\n", - "requestPayload": { + "description": "Banner specifies both w and wratio values, which is invalid", + "mockBidRequest": { "id":"req-id", "imp": [ { @@ -18,5 +18,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" objects in the request.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-array.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-array.json index f3e3914db3b..9de06382bc8 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-array.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-array.json @@ -1,7 +1,9 @@ { - "message": "Invalid request: request.imp must contain at least one element.\n", - "requestPayload": { + "description": "Bid request comes with an empty Imp array", + "mockBidRequest": { "id": "req-id", "imp": [] - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp must contain at least one element.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-object.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-object.json index f2fc508910d..d66d7ed0fae 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-object.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-empty-object.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0] missing required field: \"id\"\n", - "requestPayload": { + "description": "Bid request's sole imp element is empty", + "mockBidRequest": { "id": "req-id", "imp": [ { } @@ -8,5 +8,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0] missing required field: \"id\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-empty.json index 4d199e48b60..6c60ed5def2 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].ext must contain at least one bidder with valid parameters\n", - "requestPayload": { + "description": "Bid request's sole imp element has empty extension", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -8,7 +8,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "ext": {} } @@ -16,5 +16,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].ext must contain at least one bidder\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json index c65e9b3ba59..d9ed59c8c6e 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].ext.appnexus failed validation.\n(root): Invalid type. Expected: object, given: string\n", - "requestPayload": { + "description": "Bid request's sole imp element has invalid ext value", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -8,7 +8,7 @@ "audio": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "appnexus": "invalidParams" @@ -18,5 +18,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].ext.appnexus failed validation.\n(root): Invalid type. Expected: object, given: string\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-unknown-bidder.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-unknown-bidder.json index 436e62f7174..9f5a45c861d 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-unknown-bidder.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-unknown-bidder.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].ext contains unknown bidder: noBidderShouldEverHaveThisName. Did you forget an alias in request.ext.prebid.aliases?\n", - "requestPayload": { + "description": "Unknown biddername in bid request's sole imp element's ext", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -8,7 +8,7 @@ "audio": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "noBidderShouldEverHaveThisName": { @@ -20,5 +20,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].ext contains unknown bidder: noBidderShouldEverHaveThisName. Did you forget an alias in request.ext.prebid.aliases?\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json index 53517c268b6..90e605362fa 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].id and request.imp[1].id are both \"some-impression-id\". Imp IDs must be unique.\n", - "requestPayload": { + "description": "Identical id's in more than one imp element", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "prebid.org" @@ -40,5 +40,7 @@ } ], "tmax": 500 - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].id and request.imp[1].id are both \"some-impression-id\". Imp IDs must be unique.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-ext.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-ext.json index 3249f077c2c..ce2aa197d3e 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-ext.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-ext.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].ext is required\n", - "requestPayload": { + "description": "Bid request's sole imp element has no ext field", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -8,12 +8,14 @@ "video": { "mimes": [ "video/mp4" - ] + ] } } ], "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].ext is required\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-type.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-type.json index c7b005ca5d3..713e009b51d 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-type.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-no-type.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"\n", - "requestPayload": { + "description": "Bid request's sole imp element comes with no Banner, Video, Audio, nor Native field", + "mockBidRequest": { "id":"req-id", "imp": [ { @@ -10,5 +10,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/integer.json b/endpoints/openrtb2/sample-requests/invalid-whole/integer.json deleted file mode 100644 index 5eefb89b2a7..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/integer.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Invalid request: json: cannot unmarshal number into Go value of type openrtb.BidRequest\n", - "requestPayload": 5 -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json b/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json index 6854ea9a470..302372b5e5d 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json @@ -1,51 +1,53 @@ { - "message": "Invalid request: request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100\n", - "requestPayload": { - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "id": "some-imp-id", - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "instl": 1, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "device": { - "h": 640, - "w": 320, - "ext": { - "prebid": { - "interstitial": { - "minwidthperc": 120, - "minheightperc": 60 - } - } + "description": "Bid request's device field comes with a minwidthperc value greater than 100", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "id": "some-imp-id", + "format": [ + { + "w": 300, + "h": 600 } + ] }, + "instl": 1, "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "device": { + "h": 640, + "w": 320, + "ext": { "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} - } + "interstitial": { + "minwidthperc": 120, + "minheightperc": 60 + } } + } + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} } + } } -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100\n" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json index a69f287dfab..8753d50fb99 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json @@ -1,43 +1,45 @@ { - "message": "Invalid request: Unable to set interstitial size list for Imp id=my-imp-id (No valid sizes between 0x0 and 0x0)\n", - "requestPayload": { - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "id": "some-imp-id" - }, - "instl": 1, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "device": { - "ext": { - "prebid": { - "interstitial": { - "minwidthperc": 60, - "minheightperc": 60 - } - } - } + "description": "Bid request banner field comes with no data to set interstitial size list", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "id": "some-imp-id" }, + "instl": 1, "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "device": { + "ext": { "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} - } + "interstitial": { + "minwidthperc": 60, + "minheightperc": 60 + } } + } + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} } + } } -} \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: Unable to set interstitial size list for Imp id=my-imp-id (No valid sizes between 0x0 and 0x0)\n" +} diff --git a/endpoints/openrtb2/sample-requests/aliased/site.json b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json similarity index 53% rename from endpoints/openrtb2/sample-requests/aliased/site.json rename to endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json index cf7e9a77533..8385f924a56 100644 --- a/endpoints/openrtb2/sample-requests/aliased/site.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json @@ -1,7 +1,16 @@ { + "description": "Request with wrong source value", + "mockBidRequest": { "id": "some-request-id", - "site": { - "page": "test.somepage.com" + "app": { + "ext": { + "prebid": { + "source": { + "foo": "prebid-mobile" + }, + "version": "1.0" + } + } }, "imp": [ { @@ -24,27 +33,11 @@ "ext": { "appnexus": { "placementId": 12883451 - }, - "test1": { - "placementId": 12883451 - }, - "test2": { - "accountId": 1001, - "siteId": 113932, - "zoneId": 535510 - } - } - } - ], - "ext": { - "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} + } } } - } - } - \ No newline at end of file + ] + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: json: cannot unmarshal object into Go struct field ExtAppPrebid.prebid.source of type string" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/malformed-bid-request.json b/endpoints/openrtb2/sample-requests/invalid-whole/malformed-bid-request.json new file mode 100644 index 00000000000..c6fd52304a7 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/invalid-whole/malformed-bid-request.json @@ -0,0 +1,6 @@ +{ + "description": "Malformed bid request throws an error", + "mockBidRequest": "malformed", + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: invalid character 'm' looking for beginning of value\n" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/metric-empty-object.json b/endpoints/openrtb2/sample-requests/invalid-whole/metric-empty-object.json index b8cc1f7983a..022326a5b02 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/metric-empty-object.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/metric-empty-object.json @@ -1,15 +1,17 @@ { - "message": "Invalid request: request.imp[0].metric is not yet supported by prebid-server. Support may be added in the future\n", - "requestPayload": { - "id":"req-id", - "imp":[ - { - "id":"imp-id", - "metric": [{}] + "description": "Bid request's imp comes with elements in unsupported metric array", + "mockBidRequest": { + "id":"req-id", + "imp":[ + { + "id":"imp-id", + "metric": [{}] + } + ], + "app": { + "id": "app_001" } - ], - "app": { - "id": "app_001" - } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].metric is not yet supported by prebid-server. Support may be added in the future\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/native-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/native-empty.json index fa2bdded0e7..185ad8fd8a5 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/native-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/native-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].native missing required property \"request\"\n", - "requestPayload": { + "description": "Bid request's sole imp element has empty Native field and does not define a Banner, Video nor Audio field", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -11,5 +11,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].native missing required property \"request\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json index c56dae324fc..d21ca4a94ae 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.site or request.app must be defined, but not both.\n", - "requestPayload": { + "description": "Request does not come with site field nor app field", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -8,7 +8,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "appnexus": { @@ -17,5 +17,7 @@ } } ] - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.site or request.app must be defined, but not both.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/null.json b/endpoints/openrtb2/sample-requests/invalid-whole/null.json index eb221dddfeb..ac94de741c8 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/null.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/null.json @@ -1,4 +1,6 @@ { - "message": "Invalid request: request missing required field: \"id\"\n", - "requestPayload": null + "description": "Bid request is null", + "mockBidRequest": null, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request missing required field: \"id\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/only-request-id.json b/endpoints/openrtb2/sample-requests/invalid-whole/only-request-id.json index 9148e1a4d5b..5938cfc0243 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/only-request-id.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/only-request-id.json @@ -1,6 +1,8 @@ { - "message": "Invalid request: request.imp must contain at least one element.\n", - "requestPayload": { + "description": "Bid request is is missing imp array", + "mockBidRequest": { "id": "req-id" - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp must contain at least one element.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json index dff3023c702..1847dc72283 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json @@ -1,11 +1,11 @@ { - "message": "Invalid request: request.regs.ext.gdpr must be either 0 or 1.\n", - "requestPayload": { + "description": "Bid request defines an invalid GDPR value", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", "publisher": { - "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" + "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" } }, "source": { @@ -23,7 +23,7 @@ "banner": { "format": [ { - "w": 300, + "w": 300, "h": 250 }, { @@ -36,11 +36,13 @@ ], "regs": { "ext": { - "gdpr": 2 + "gdpr": 2 } }, "user": { "ext": {} } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.regs.ext.gdpr must be either 0 or 1.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json index ce887889034..afdabdab7cf 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go struct field ExtRegs.gdpr of type int8\n", - "requestPayload": { + "description": "Invalid GDPR value in regs field", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -42,5 +42,7 @@ "user": { "ext": {} } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go struct field ExtRegs.gdpr of type int8\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json index a403103d6fb..a8e94008cf1 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json @@ -1,45 +1,46 @@ { - "message": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go value of type openrtb_ext.ExtRegs\n", - "requestPayload": { - "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", - "site": { - "page": "prebid.org", - "publisher": { + "description": "Malformed ext in regs field", + "mockBidRequest": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org", + "publisher": { "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" - } - }, - "source": { - "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" - }, - "tmax": 1000, - "imp": [ - { - "id": "/19968336/header-bid-tag-0", - "ext": { - "appnexus": { - "placementId": 12883451 - } - }, - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 - } - ] + } + }, + "source": { + "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" + }, + "tmax": 1000, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "ext": { + "appnexus": { + "placementId": 12883451 } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] } - ], - "regs": { - "ext": "malformed" - }, - "user": { - "ext": {} } + ], + "regs": { + "ext": "malformed" + }, + "user": { + "ext": {} } - } - \ No newline at end of file + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go value of type openrtb_ext.ExtRegs\n" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json index 4b643705640..5bc3054c356 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.site or request.app must be defined, but not both.\n", - "requestPayload": { + "description": "Bid request comes with both site and app fields, it should only come with one or the other", + "mockBidRequest": { "id": "req-id", "site": { "page": "test.mysite.com" @@ -12,7 +12,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "appnexus": { @@ -21,5 +21,7 @@ } } ] - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.site or request.app must be defined, but not both.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json index 3d53314dbb7..80f88abbf04 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.site should include at least one of request.site.id or request.site.page.\n", - "requestPayload": { + "description": "Bid request's site field is missing an id", + "mockBidRequest": { "id": "req-id", "site": {}, "imp": [ @@ -9,7 +9,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "appnexus": { @@ -18,5 +18,7 @@ } } ] - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.site should include at least one of request.site.id or request.site.page.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json index bebe4625578..6575bb16fe9 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.site.ext.amp must be either 1, 0, or undefined\n", - "requestPayload": { + "description": "Request's amp value in site's ext is invalid", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com", @@ -43,5 +43,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.site.ext.amp must be either 1, 0, or undefined\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json b/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json index 5d510d21dbd..86e4b9cac5f 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: ext.prebid.storedrequest.id must be a string\n", - "requestPayload": { + "description": "Bid request with an invalid value for ext.prebid.storedrequest.id field", + "mockBidRequest": { "id": "some-request-id", "site": { "page": "test.somepage.com" @@ -11,7 +11,7 @@ "video": { "mimes": [ "video/mp4" - ] + ] }, "ext": { "appnexus": { @@ -27,5 +27,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: ext.prebid.storedrequest.id must be a string\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/tmax-negative.json b/endpoints/openrtb2/sample-requests/invalid-whole/tmax-negative.json index e97a20816f2..8fa52f24ca9 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/tmax-negative.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/tmax-negative.json @@ -1,7 +1,9 @@ { - "message": "Invalid request: request.tmax must be nonnegative. Got -2\n", - "requestPayload": { + "description": "Bid request with negative tmax value. Expect error", + "mockBidRequest": { "id": "req-id", "tmax": -2 - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.tmax must be nonnegative. Got -2\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json b/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json index 3914ae7ae49..a907d4d9257 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json @@ -1,38 +1,38 @@ { - "description": "Copy of the prebid test ad, with the addition of an unknown bidder", - - "message": "Invalid request: request.imp[0].ext contains unknown bidder: unknownbidder. Did you forget an alias in request.ext.prebid.aliases?\n", - "requestPayload": { - "id": "some-request-id", - "site": { - "page": "prebid.org" - }, - "imp": [ - { - "id": "some-impression-id", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 600 - } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 + "description": "Copy of the prebid test ad, with the addition of an unknown bidder", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 }, - "unknownbidder": { - "param1": "foobar", - "param2": 42 + { + "w": 300, + "h": 600 } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + }, + "unknownbidder": { + "param1": "foobar", + "param2": 42 } } - ], - "tmax": 500 - } - } \ No newline at end of file + } + ], + "tmax": 500 + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request" +} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json index 5bc0ed33eab..b61be105df0 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go struct field ExtUser.consent of type string\n", - "requestPayload": { + "description": "Bid request comes with an integer value for user.ext.consent instead of a JSON object", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -22,14 +22,14 @@ }, "banner": { "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 - } + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } ] } } @@ -44,5 +44,7 @@ "consent": 1 } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go struct field ExtUser.consent of type string\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json index ebbb4e2701c..d1cd1eb512e 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.eids[0].uids must contain at least one element or be undefined\n", - "requestPayload": { + "description": "Bid request with empty uids array in user.ext.eids array element", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -49,5 +49,7 @@ ] } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.eids[0].uids must contain at least one element or be undefined\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json index 3d73d73117e..98297739105 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.eids must contain at least one element or be undefined\n", - "requestPayload": { + "description": "Bid request with empty eids array in user.ext", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -44,5 +44,7 @@ "eids": [] } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.eids must contain at least one element or be undefined\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json index bbd0dadfd70..09ad4eeb68a 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.eids[0] must contain either \"id\" or \"uids\" field\n", - "requestPayload": { + "description": "Bid request with user.ext.eids array element array element that does not contain an id nor uids", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -48,5 +48,7 @@ ] } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.eids[0] must contain either \"id\" or \"uids\" field\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json index 5efff0626ef..06684b5e62e 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.eids[0] missing required field: \"source\"\n", - "requestPayload": { + "description": "Bid request with user.ext.eids array element array element that does not contain source field", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -46,5 +46,7 @@ ] } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.eids[0] missing required field: \"source\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json index e508b113aff..5a6548c3eb2 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.eids must contain unique sources\n", - "requestPayload": { + "description": "Bid request where more than one request.user.ext.eids array elements share the same source field value", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -53,5 +53,7 @@ ] } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.eids must contain unique sources\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json index 3a9659b7327..faa09f93ad1 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.eids[0].uids[0] missing required field: \"id\"\n", - "requestPayload": { + "description": "Bid request where a request.user.ext.eids.uids array element is missing its id field", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -51,5 +51,7 @@ ] } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.eids[0].uids[0] missing required field: \"id\"\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json index 44bee775844..7ec84992778 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.\n", - "requestPayload": { + "description": "Bid request with an empty request.user.ext.prebid.buyeruids object", + "mockBidRequest": { "id": "request-without-user-ext-obj", "site": { "page": "test.somepage.com" @@ -30,5 +30,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json index 78773066744..715cc667274 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.unknown is neither a known bidder name nor an alias in request.ext.prebid.aliases.\n", - "requestPayload": { + "description": "Bid request with an unknown bidder name or alias inside the request.user.ext.unknown object", + "mockBidRequest": { "id": "request-without-user-ext-obj", "site": { "page": "test.somepage.com" @@ -32,5 +32,7 @@ } } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.unknown is neither a known bidder name nor an alias in request.ext.prebid.aliases.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json index f2e497514b7..6d811cf7030 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.\n", - "requestPayload": { + "description": "Bid request with an empty request.user.ext.prebid object", + "mockBidRequest": { "id": "request-without-user-ext-obj", "site": { "page": "test.somepage.com" @@ -28,5 +28,7 @@ "prebid": {} } } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-badtype.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-badtype.json deleted file mode 100644 index ce887889034..00000000000 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-badtype.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "message": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go struct field ExtRegs.gdpr of type int8\n", - "requestPayload": { - "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", - "site": { - "page": "prebid.org", - "publisher": { - "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" - } - }, - "source": { - "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" - }, - "tmax": 1000, - "imp": [ - { - "id": "/19968336/header-bid-tag-0", - "ext": { - "appnexus": { - "placementId": 12883451 - } - }, - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 - } - ] - } - } - ], - "regs": { - "ext": { - "gdpr": "foo" - } - }, - "user": { - "ext": {} - } - } -} diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json similarity index 70% rename from endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-invalid.json rename to endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json index 0729a22db80..08eed44b2b0 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-invalid.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.regs.ext.gdpr must be either 0 or 1.\n", - "requestPayload": { + "description": "Invalid GDPR consent string in user field", + "mockBidRequest": { "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", "site": { "page": "prebid.org", @@ -34,13 +34,12 @@ } } ], - "regs": { + "user": { "ext": { - "gdpr": 2 + "consent": 2 } - }, - "user": { - "ext": {} } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go struct field ExtUser.consent of type string" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/video-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/video-empty.json index 30fd1f13245..ec203d0f898 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/video-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/video-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].video.mimes must contain at least one supported MIME type\n", - "requestPayload": { + "description": "Bid request's sole imp element has empty video field and does not define a Banner, Video nor Audio field", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -11,5 +11,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].video.mimes must contain at least one supported MIME type\n" } diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/video-mimes-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/video-mimes-empty.json index 5eb5c36f514..d2152dd29ed 100644 --- a/endpoints/openrtb2/sample-requests/invalid-whole/video-mimes-empty.json +++ b/endpoints/openrtb2/sample-requests/invalid-whole/video-mimes-empty.json @@ -1,6 +1,6 @@ { - "message": "Invalid request: request.imp[0].video.mimes must contain at least one supported MIME type\n", - "requestPayload": { + "description": "Bid request with empty mimes array in a video imp element", + "mockBidRequest": { "id": "req-id", "imp": [ { @@ -13,5 +13,7 @@ "app": { "id": "app_001" } - } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: request.imp[0].video.mimes must contain at least one supported MIME type\n" } diff --git a/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json index b20b461646c..15af8551da6 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json +++ b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-hmin.json @@ -1,11 +1,41 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ + "description": "Well formed native request that defines a 'wmin' on its 'img' field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request":"{\"context\":1,\"plcmttype\":1,\"assets\":[{\"img\":{\"wmin\":30}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ { - "img": { - "wmin": 30 + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 } + ], + "seat": "appnexus-bids" } - ] -} \ No newline at end of file + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json index f34dca050a6..5d986bcf755 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json +++ b/endpoints/openrtb2/sample-requests/valid-native/asset-img-no-wmin.json @@ -1,11 +1,41 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ + "description": "Well formed native request that defines a 'hmin' on its 'img' field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"img\":{\"hmin\":30}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ { - "img": { - "hmin": 30 + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 } + ], + "seat": "appnexus-bids" } - ] -} \ No newline at end of file + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json b/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json index 7c55888ba29..1e55cdda63f 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json +++ b/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json @@ -1,12 +1,41 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ + "description": "Well formed native request that comes with 'id' field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"id\":1,\"img\":{\"wmin\":30}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ { - "id": 1, - "img": { - "wmin": 30 + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 } + ], + "seat": "appnexus-bids" } - ] + ] + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-native/asset-with-no-id.json b/endpoints/openrtb2/sample-requests/valid-native/asset-with-no-id.json index a9bc57ea274..36a1745cb19 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/asset-with-no-id.json +++ b/endpoints/openrtb2/sample-requests/valid-native/asset-with-no-id.json @@ -1,11 +1,41 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "img": { - "wmin": 30 - } + "description": "Well formed native request that comes with no 'id' field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"img\":{\"wmin\":30}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-native/assets-with-unique-ids.json b/endpoints/openrtb2/sample-requests/valid-native/assets-with-unique-ids.json index 10692b9aaf2..98cdeedadbe 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/assets-with-unique-ids.json +++ b/endpoints/openrtb2/sample-requests/valid-native/assets-with-unique-ids.json @@ -1,18 +1,41 @@ { - "context": 1, - "plcmttype": 1, - "assets": [ - { - "id": 1, - "img": { - "wmin": 30 - } + "description": "Multi-asset native request with different ids", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"id\":1,\"img\":{\"wmin\":30}},{\"id\":2,\"title\":{\"len\":20}}]}" }, - { - "id": 2, - "title": { - "len": 20 - } + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json b/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json index f5b0b35979e..1ad97c8ff8f 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json +++ b/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json @@ -1,11 +1,41 @@ { - "plcmttype": 2, - "assets": [ - { - "img": { - "hmin": 30, - "wmin": 20 - } + "description": "Well formed native request that comes with no 'context' field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"plcmttype\":2,\"assets\":[{\"img\":{\"hmin\":30,\"wmin\":20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json b/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json index 20dcdfb2aaa..88af803684d 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json +++ b/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json @@ -1,11 +1,41 @@ { - "context": 1, - "assets": [ - { - "img": { - "hmin": 30, - "wmin": 20 - } + "description": "Well formed native request that comes with no 'plcmttype' field", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"assets\":[{\"img\":{\"hmin\":30,\"wmin\":20}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } + } ] -} \ No newline at end of file + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-native/sample-v1.1.json b/endpoints/openrtb2/sample-requests/valid-native/sample-v1.1.json deleted file mode 100644 index c0011724adb..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-native/sample-v1.1.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "context": 1, - "contextsubtype": 10, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } - }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } - } - ] -} diff --git a/endpoints/openrtb2/sample-requests/valid-native/sample-v1.2.json b/endpoints/openrtb2/sample-requests/valid-native/sample-v1.2.json deleted file mode 100644 index 10e4e7fdf61..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-native/sample-v1.2.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "context": 1, - "plcmttype": 1, - "assets": [ - { - "title": { - "len": 90 - } - }, - { - "img": { - "hmin": 30, - "wmin": 20 - } - }, - { - "video": { - "mimes": ["video/mp4"], - "minduration": 5, - "maxduration": 10, - "protocols": [1] - } - }, - { - "data": { - "type": 2 - } - } - ], - "eventtrackers": [{ - "event": 1, - "methods": [1] - }] -} diff --git a/endpoints/openrtb2/sample-requests/valid-native/video-asset-event-tracker.json b/endpoints/openrtb2/sample-requests/valid-native/video-asset-event-tracker.json new file mode 100644 index 00000000000..ab192e14881 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-native/video-asset-event-tracker.json @@ -0,0 +1,41 @@ +{ + "description": "Well formed native request with video asset", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}],\"eventtrackers\":[{\"event\":1,\"methods\":[1]}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-native/with-video-asset.json b/endpoints/openrtb2/sample-requests/valid-native/with-video-asset.json new file mode 100644 index 00000000000..0ec3c993251 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-native/with-video-asset.json @@ -0,0 +1,41 @@ +{ + "description": "Well formed native request with video asset", + "mockBidRequest": { + "id": "req-id", + "site": { + "page": "some.page.com" + }, + "tmax": 500, + "imp": [ + { + "id": "some-imp", + "native": { + "request": "{\"context\":1,\"contextsubtype\":10,\"plcmttype\":1,\"assets\":[{\"title\":{\"len\":90}},{\"img\":{\"hmin\":30,\"wmin\":20}},{\"video\":{\"mimes\":[\"video/mp4\"],\"minduration\":5,\"maxduration\":10,\"protocols\":[1]}},{\"data\":{\"type\":2}}]}" + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedBidResponse": { + "id": "req-id", + "bidid": "test bid id", + "nbr": 0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json index 3e2beedefac..f875fa880bc 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json @@ -1,7 +1,6 @@ { "description": "This demonstrates all of the OpenRTB extensions supported by Prebid Server. Very few requests will need all of these at once.", - - "requestPayload": { + "mockBidRequest": { "id": "some-request-id", "site": { "page": "prebid.org" @@ -80,5 +79,43 @@ } } } - } + }, + "expectedBidResponse": { + "id":"some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + }, + { + "bid": [ + { + "id": "districtm-bid", + "impid": "", + "price": 0 + } + ], + "seat": "districtm-bids" + }, + { + "bid": [ + { + "id": "rubicon-bid", + "impid": "", + "price": 0 + } + ], + "seat": "rubicon-bids" + } + ], + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json index fc4794328a4..2c6a34f569e 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json @@ -1,7 +1,6 @@ { "description": "This uses Appnexus to fetch the prebid sample ad, as seen on prebid.org.", - - "requestPayload": { + "mockBidRequest": { "id": "some-request-id", "site": { "page": "prebid.org" @@ -29,5 +28,23 @@ } ], "tmax": 500 - } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ], + "bidid": "test bid id", + "nbr": 0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json index 82125592e46..141b643a520 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json @@ -1,40 +1,49 @@ { - "id": "request-without-user-ext-obj", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, + "description": "Well formed amp request that comes with user field and buyeruids values", + "mockBidRequest": { + "id": "request-without-user-ext-obj", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "user": { + "ext": { + "prebid": { + "buyeruids": { + "unknown": "123" + } + } + } + }, "ext": { - "appnexus": { - "placementId": 12883451 + "prebid": { + "aliases": { + "unknown": "appnexus" + } + } } - } - } - ], - "user": { - "ext": { - "prebid": { - "buyeruids": { - "unknown": "123" - } - } - } - }, - "ext": { - "prebid": { - "aliases": { - "unknown": "appnexus" - } - } - } + }, + "expectedBidResponse": { + "id":"request-without-user-ext-obj", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json index f6137e4a019..be7c2269331 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json @@ -1,28 +1,37 @@ { - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "unknown": { - "placementId": 12883451 + "description": "Well formed amp request with aliases that should run properly", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "unknown": { + "placementId": 12883451 + } } } - } - ], - "ext": { - "prebid": { - "aliases": { - "unknown": "appnexus" + ], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + } } } - } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/app.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/app.json deleted file mode 100644 index 66e05d7636b..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/app.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "id": "some-request-id", - "app": { - "ext": { - "prebid": { - "source": "prebid-mobile", - "version": "1.0.0" - } - } - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" - } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ] -} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/bid-adjustments.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/bid-adjustments.json deleted file mode 100644 index 0cf52a8915f..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/bid-adjustments.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "unknown": { - "placementId": 12883451 - } - } - } - ], - "ext": { - "prebid": { - "bidadjustmentfactors": { - "appnexus": 2.0, - "unknown": 1.5 - }, - "aliases": { - "unknown": "appnexus" - } - } - } -} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json deleted file mode 100644 index a4c93b3d3cb..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "ext": { - "prebid": { - "cache": { - "bids": {} - }, - "targeting": {} - } - } -} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json deleted file mode 100644 index fe9445358ba..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "ext": { - "prebid": { - "cache": { - "vastxml": {} - } - } - } -} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/ccpa-invalid.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/ccpa-invalid.json deleted file mode 100644 index f3b677635c0..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/ccpa-invalid.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", - "site": { - "page": "prebid.org", - "publisher": { - "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" - } - }, - "source": { - "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" - }, - "tmax": 1000, - "imp": [ - { - "id": "/19968336/header-bid-tag-0", - "ext": { - "appnexus": { - "placementId": 12883451 - } - }, - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 - } - ] - } - } - ], - "regs": { - "ext": { - "us_privacy": "invalid by length. allowed since it only produces a warning." - } - } - } - \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json index ca8e090760d..5cd070745ab 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json @@ -1,41 +1,50 @@ { - "id": "request-with-valid-digitrust-obj", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" + "description": "Well formed amp request with digitrust extension that should run properly", + "mockBidRequest": { + "id": "request-with-valid-digitrust-obj", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 } } - } - ], - "user": { - "yob": 1989, - "ext": { - "digitrust": { - "id": "sample-digitrust-id", - "keyv": 1, - "pref": 0 + ], + "user": { + "yob": 1989, + "ext": { + "digitrust": { + "id": "sample-digitrust-id", + "keyv": 1, + "pref": 0 + } } } - } + }, + "expectedBidResponse": { + "id":"request-with-valid-digitrust-obj", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json index 5a63c6d11ce..6922dfb2a5c 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json @@ -1,43 +1,52 @@ { - "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", - "site": { - "page": "prebid.org", - "publisher": { - "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" - } - }, - "source": { - "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" - }, - "tmax": 1000, - "imp": [ - { - "id": "/19968336/header-bid-tag-0", - "ext": { - "appnexus": { - "placementId": 12883451 - } - }, - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 + "description": "Well formed amp request with GDPR value but missing consent string", + "mockBidRequest": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org", + "publisher": { + "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" + } + }, + "source": { + "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" + }, + "tmax": 1000, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "ext": { + "appnexus": { + "placementId": 12883451 } - ] + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + } } + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": {} } - ], - "regs": { - "ext": { - "gdpr": 1 - } }, - "user": { - "ext": {} - } + "expectedBidResponse": { + "id":"b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json index ef9f10d0cd0..1e3a2d41f2c 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json @@ -1,45 +1,54 @@ { - "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", - "site": { - "page": "prebid.org", - "publisher": { - "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" - } - }, - "source": { - "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" - }, - "tmax": 1000, - "imp": [ - { - "id": "/19968336/header-bid-tag-0", - "ext": { - "appnexus": { - "placementId": 12883451 - } - }, - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 300, - "h": 300 + "description": "Well formed amp request with GDPR value and consent string", + "mockBidRequest": { + "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "site": { + "page": "prebid.org", + "publisher": { + "id": "a3de7af2-a86a-4043-a77b-c7e86744155e" + } + }, + "source": { + "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5" + }, + "tmax": 1000, + "imp": [ + { + "id": "/19968336/header-bid-tag-0", + "ext": { + "appnexus": { + "placementId": 12883451 } - ] + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + } + } + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "some-consent-string" } - } - ], - "regs": { - "ext": { - "gdpr": 1 } }, - "user": { - "ext": { - "consent": "some-consent-string" - } - } + "expectedBidResponse": { + "id":"b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-device-only.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-device-only.json deleted file mode 100644 index 64146eaebe8..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-device-only.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": {}, - "instl": 1, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "device": { - "h": 640, - "w": 320, - "ext": { - "prebid": { - "interstitial": { - "minwidthperc": 60, - "minheightperc": 60 - } - } - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} - } - } - } -} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-no-extension.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-no-extension.json deleted file mode 100644 index 15cd832053f..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-no-extension.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "instl": 1, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "ext": { - "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} - } - } - } -} \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial.json deleted file mode 100644 index 64fc2fe2653..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "instl": 1, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ], - "device":{ - "h": 640, - "w": 320, - "ext": { - "prebid": { - "interstitial": { - "minwidthperc": 60, - "minheightperc": 60 - } - } - } - }, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} - } - } - } -} - \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json index 30c0afc800a..5bea908b41f 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json @@ -1,44 +1,53 @@ { - "id": "some-request-id", - "site": { - "page": "test.somepage.com", - "ext": { - "amp": 1 - } - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" - } - ] - }, + "description": "Request that comes with a valid amp value in its site.ext field", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com", "ext": { - "appnexus": { - "placementId": 12883451 + "amp": 1 + } + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } } } - } - ], - "ext": { - "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} + ], + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } } } - } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json new file mode 100644 index 00000000000..fb9d3bff0f5 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-dnt.json @@ -0,0 +1,53 @@ +{ + "description": "Request that comes with a valid device and dnt fields", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "device": { + "dnt": 1 + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 + } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json new file mode 100644 index 00000000000..8ef362458b6 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json @@ -0,0 +1,47 @@ +{ + "description": "Well formed request with valid IPV4 in its device field", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 600 + }] + }, + "pmp": { + "deals": [{ + "id": "some-deal-id" + }] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + }], + "device": { + "ip": "8.8.8.8" + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json new file mode 100644 index 00000000000..90610a3b5a0 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json @@ -0,0 +1,47 @@ +{ + "description": "Well formed request with valid IPV6 in its device field", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 600 + }] + }, + "pmp": { + "deals": [{ + "id": "some-deal-id" + }] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + }], + "device": { + "ipv6": "8888::" + }, + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json index 7a25249c763..60b8d7ecd4f 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json @@ -1,41 +1,50 @@ { - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" + "description": "Well formed amp request with valid Site field", + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 } } - } - ], - "ext": { - "prebid": { - "targeting": { - "pricegranularity": "low" - }, - "cache": { - "bids": {} + ], + "ext": { + "prebid": { + "targeting": { + "pricegranularity": "low" + }, + "cache": { + "bids": {} + } } } - } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/timeout.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/timeout.json deleted file mode 100644 index b3dbe1f5d4b..00000000000 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/timeout.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "id": "some-request-id", - "app": {}, - "tmax": 500, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" - } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 - } - } - } - ] -} diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json index 243b0739b7b..686489cf1ff 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json @@ -1,34 +1,43 @@ { - "id": "request-without-user-ext-obj", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "banner": { - "format": [ - { - "w": 300, - "h": 600 - } - ] - }, - "pmp": { - "deals": [ - { - "id": "some-deal-id" + "description": "Well formed amp request with valid User field", + "mockBidRequest": { + "id": "request-without-user-ext-obj", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "pmp": { + "deals": [ + { + "id": "some-deal-id" + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 } - ] - }, - "ext": { - "appnexus": { - "placementId": 12883451 } } + ], + "user": { + "yob": 1989 } - ], - "user": { - "yob": 1989 - } + }, + "expectedBidResponse": { + "id":"request-without-user-ext-obj", + "bidid":"test bid id", + "nbr":0 + }, + "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json new file mode 100644 index 00000000000..4850fe91652 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json @@ -0,0 +1,86 @@ +{ + "description": "Video endpoint valid request with AppendBidderNames.", + "requestPayload": { + "appendbiddernames": true, + "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" + } + } + } + }, + "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/video_auction.go b/endpoints/openrtb2/video_auction.go index 2629eb24454..2e806bffc07 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -15,11 +15,13 @@ import ( "time" "github.com/PubMatic-OpenWrap/prebid-server/errortypes" + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" "github.com/buger/jsonparser" jsonpatch "github.com/evanphx/json-patch" "github.com/gofrs/uuid" "github.com/PubMatic-OpenWrap/openrtb" + accountService "github.com/PubMatic-OpenWrap/prebid-server/account" "github.com/PubMatic-OpenWrap/prebid-server/analytics" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/exchange" @@ -34,16 +36,37 @@ 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, cache prebid_cache_client.Client) (httprouter.Handle, error) { +func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidator, requestsById stored_requests.Fetcher, videoFetcher stored_requests.Fetcher, accounts stored_requests.AccountFetcher, 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 { + if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { return nil, errors.New("NewVideoEndpoint requires non-nil arguments.") } + defRequest := defReqJSON != nil && len(defReqJSON) > 0 + ipValidator := iputil.PublicNetworkIPValidator{ + IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, + IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, + } + videoEndpointRegexp := regexp.MustCompile(`[<>]`) - return httprouter.Handle((&endpointDeps{ex, validator, requestsById, videoFetcher, categories, cfg, met, pbsAnalytics, disabledBidders, defRequest, defReqJSON, bidderMap, cache, videoEndpointRegexp}).VideoAuctionEndpoint), nil + return httprouter.Handle((&endpointDeps{ + ex, + validator, + requestsById, + videoFetcher, + accounts, + cfg, + met, + pbsAnalytics, + disabledBidders, + defRequest, + defReqJSON, + bidderMap, + cache, + videoEndpointRegexp, + ipValidator}).VideoAuctionEndpoint), nil } /* @@ -100,7 +123,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re defer func() { if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil { - err := putDebugLogError(deps.cache, &debugLog, start) + err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors) if err != nil { vo.Errors = append(vo.Errors, err) } @@ -220,7 +243,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re usersyncs := usersync.ParsePBSCookieFromRequest(r, &(deps.cfg.HostCookie)) if bidReq.App != nil { labels.Source = pbsmetrics.DemandApp - labels.PubID = effectivePubID(bidReq.App.Publisher) + labels.PubID = getAccountID(bidReq.App.Publisher) } else { // both bidReq.App == nil and bidReq.Site != nil are true labels.Source = pbsmetrics.DemandWeb if usersyncs.LiveSyncCount() == 0 { @@ -228,16 +251,25 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } else { labels.CookieFlag = pbsmetrics.CookieFlagYes } - labels.PubID = effectivePubID(bidReq.Site.Publisher) + labels.PubID = getAccountID(bidReq.Site.Publisher) } - if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil { - errL := []error{err} - handleError(&labels, w, errL, &vo, &debugLog) + // Look up account now that we have resolved the pubID value + account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID) + if len(acctIDErrs) > 0 { + handleError(&labels, w, acctIDErrs, &vo, &debugLog) return } - //execute auction logic - response, err := deps.ex.HoldAuction(ctx, bidReq, usersyncs, labels, &deps.categories, &debugLog) + + auctionRequest := exchange.AuctionRequest{ + BidRequest: bidReq, + Account: *account, + UserSyncs: usersyncs, + RequestType: labels.RType, + LegacyLabels: labels, + } + + response, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog) vo.Request = bidReq vo.Response = response if err != nil { @@ -257,6 +289,21 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re bidResp.Ext = response.Ext } + if len(bidResp.AdPods) == 0 && debugLog.Enabled { + err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors) + if err != nil { + vo.Errors = append(vo.Errors, err) + } else { + bidResp.AdPods = append(bidResp.AdPods, &openrtb_ext.AdPod{ + Targeting: []openrtb_ext.VideoTargeting{ + { + HbCacheID: debugLog.CacheKey, + }, + }, + }) + } + } + vo.VideoResponse = bidResp resp, err := json.Marshal(bidResp) @@ -272,34 +319,6 @@ 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:]...) @@ -448,7 +467,7 @@ func buildVideoResponse(bidresponse *openrtb.BidResponse, podErrors []PodError) if err := json.Unmarshal(bid.Ext, &tempRespBidExt); err != nil { return nil, err } - if tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbVastCacheKey)] == "" { + if tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)] == "" { continue } @@ -457,9 +476,9 @@ func buildVideoResponse(bidresponse *openrtb.BidResponse, podErrors []PodError) podId, _ := strconv.ParseInt(podNum, 0, 64) videoTargeting := openrtb_ext.VideoTargeting{ - HbPb: tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbpbConstantKey)], - HbPbCatDur: tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbCategoryDurationKey)], - HbCacheID: tempRespBidExt.Prebid.Targeting[string(openrtb_ext.HbVastCacheKey)], + HbPb: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbpbConstantKey, seatBid.Seat)], + HbPbCatDur: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbCategoryDurationKey, seatBid.Seat)], + HbCacheID: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)], } adPod := findAdPod(podId, adPods) @@ -497,6 +516,14 @@ func buildVideoResponse(bidresponse *openrtb.BidResponse, podErrors []PodError) return &openrtb_ext.BidResponseVideo{AdPods: adPods}, nil } +func formatTargetingKey(key openrtb_ext.TargetingKey, bidderName string) string { + fullKey := fmt.Sprintf("%s_%s", string(key), bidderName) + if len(fullKey) > exchange.MaxKeyLength { + return string(fullKey[0:exchange.MaxKeyLength]) + } + return fullKey +} + func findAdPod(podInd int64, pods []*openrtb_ext.AdPod) *openrtb_ext.AdPod { for _, pod := range pods { if pod.PodId == podInd { @@ -601,9 +628,10 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro targeting := openrtb_ext.ExtRequestTargeting{ PriceGranularity: priceGranularity, - IncludeWinners: true, IncludeBrandCategory: inclBrandCat, DurationRangeSec: durationRangeSec, + IncludeBidderKeys: true, + AppendBidderNames: videoRequest.AppendBidderNames, } vastXml := openrtb_ext.ExtRequestPrebidCacheVAST{} diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index c21b4324ba0..0e85d07c675 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -20,7 +20,6 @@ import ( "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" "github.com/stretchr/testify/assert" @@ -81,6 +80,10 @@ func TestVideoEndpointImpressionsDuration(t *testing.T) { t.Fatalf("The request never made it into the Exchange.") } + var extData openrtb_ext.ExtRequest + json.Unmarshal(ex.lastRequest.Ext, &extData) + assert.True(t, extData.Prebid.Targeting.IncludeBidderKeys, "Request ext incorrect: IncludeBidderKeys should be true ") + assert.Len(t, ex.lastRequest.Imp, 22, "Incorrect number of impressions in request") assert.Equal(t, ex.lastRequest.Imp[0].ID, "1_0", "Incorrect impression id in request") assert.Equal(t, ex.lastRequest.Imp[0].Video.MaxDuration, int64(15), "Incorrect impression max duration in request") @@ -280,6 +283,42 @@ func TestVideoEndpointDebugError(t *testing.T) { assert.Equal(t, recorder.Code, 500, "Should catch error in request") } +func TestVideoEndpointDebugNoAdPods(t *testing.T) { + ex := &mockExchangeVideoNoBids{ + 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 := mockDepsNoBids(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, resp.AdPods, 1, "Debug AdPod should be added to response") + assert.Empty(t, resp.AdPods[0].Errors, "AdPod Errors should be empty") + assert.Empty(t, resp.AdPods[0].Targeting[0].HbPb, "Hb_pb should be empty") + assert.Empty(t, resp.AdPods[0].Targeting[0].HbPbCatDur, "Hb_pb_cat_dur should be empty") + assert.NotEmpty(t, resp.AdPods[0].Targeting[0].HbCacheID, "Hb_cache_id should not be empty") + assert.Equal(t, int64(0), resp.AdPods[0].PodId, "Pod ID should be 0") +} + func TestVideoEndpointNoPods(t *testing.T) { ex := &mockExchangeVideo{} reqData, err := ioutil.ReadFile("sample-requests/video/video_invalid_sample.json") @@ -643,9 +682,9 @@ func TestVideoBuildVideoResponseMissedCacheForOneBid(t *testing.T) { bid2 := openrtb.Bid{} bid3 := openrtb.Bid{} - extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_123_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) - extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_456_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) - extBid3 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_406_30s","hb_size":"1x1"}}}`) + extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_123_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_456_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid3 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_406_30s","hb_size":"1x1"}}}`) bid1.Ext = extBid1 bids = append(bids, bid1) @@ -657,6 +696,7 @@ func TestVideoBuildVideoResponseMissedCacheForOneBid(t *testing.T) { bids = append(bids, bid3) seatBid.Bid = bids + seatBid.Seat = "appnexus" seatBids = append(seatBids, seatBid) openRtbBidResp.SeatBid = seatBids @@ -713,8 +753,8 @@ func TestVideoBuildVideoResponsePodErrors(t *testing.T) { bid1 := openrtb.Bid{} bid2 := openrtb.Bid{} - extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_123_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) - extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder":"appnexus","hb_pb":"17.00","hb_pb_cat_dur":"17.00_456_30s","hb_size":"1x1","hb_uuid":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid1 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_123_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) + extBid2 := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"17.00","hb_pb_cat_dur_appnex":"17.00_456_30s","hb_size":"1x1","hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"}}}`) bid1.Ext = extBid1 bids = append(bids, bid1) @@ -723,6 +763,7 @@ func TestVideoBuildVideoResponsePodErrors(t *testing.T) { bids = append(bids, bid2) seatBid.Bid = bids + seatBid.Seat = "appnexus" seatBids = append(seatBids, seatBid) openRtbBidResp.SeatBid = seatBids @@ -1107,10 +1148,62 @@ func TestCCPA(t *testing.T) { } } +func TestVideoEndpointAppendBidderNames(t *testing.T) { + ex := &mockExchangeAppendBidderNames{} + reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_appendbiddernames.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", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps := mockDepsAppendBidderNames(t, ex) + deps.VideoAuctionEndpoint(recorder, req, nil) + + if ex.lastRequest == nil { + t.Fatalf("The request never made it into the Exchange.") + } + + var extData openrtb_ext.ExtRequest + json.Unmarshal(ex.lastRequest.Ext, &extData) + assert.True(t, extData.Prebid.Targeting.AppendBidderNames, "Request ext incorrect: AppendBidderNames should be true ") + + 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_appnexus", "Incorrect number of Ad Pods in response") + +} + +func TestFormatTargetingKey(t *testing.T) { + res := formatTargetingKey(openrtb_ext.HbCategoryDurationKey, "appnexus") + assert.Equal(t, "hb_pb_cat_dur_appnex", res, "Tergeting key constructed incorrectly") +} + +func TestFormatTargetingKeyLongKey(t *testing.T) { + res := formatTargetingKey(openrtb_ext.HbpbConstantKey, "20.00") + assert.Equal(t, "hb_pb_20.00", res, "Tergeting key constructed incorrectly") +} + func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) mockModule := &mockAnalyticsModule{} - edep := &endpointDeps{ + deps := &endpointDeps{ ex, newParamsValidator(t), &mockVideoStoredReqFetcher{}, @@ -1125,9 +1218,10 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *p openrtb_ext.BidderMap, nil, nil, + hardcodedResponseIPValidator{response: true}, } - return edep, theMetrics, mockModule + return deps, theMetrics, mockModule } type mockAnalyticsModule struct { @@ -1149,7 +1243,55 @@ func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) { retu func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { return } +func (m *mockAnalyticsModule) LogNotificationEventObject(ne *analytics.NotificationEvent) { return } + func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { + theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + deps := &endpointDeps{ + ex, + newParamsValidator(t), + &mockVideoStoredReqFetcher{}, + &mockVideoStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + theMetrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + ex.cache, + regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, + } + + return deps +} + +func mockDepsAppendBidderNames(t *testing.T, ex *mockExchangeAppendBidderNames) *endpointDeps { + theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) + deps := &endpointDeps{ + ex, + newParamsValidator(t), + &mockVideoStoredReqFetcher{}, + &mockVideoStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{MaxRequestSize: maxSize}, + theMetrics, + analyticsConf.NewPBSAnalytics(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BidderMap, + ex.cache, + regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, + } + + return deps +} + +func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps { theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) edep := &endpointDeps{ ex, @@ -1166,6 +1308,7 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps { openrtb_ext.BidderMap, ex.cache, regexp.MustCompile(`[<>]`), + hardcodedResponseIPValidator{response: true}, } return edep @@ -1182,8 +1325,8 @@ func (m *mockCacheClient) PutJson(ctx context.Context, values []prebid_cache_cli return []string{}, []error{} } -func (m *mockCacheClient) GetExtCacheData() (string, string) { - return "", "" +func (m *mockCacheClient) GetExtCacheData() (scheme string, host string, path string) { + return "", "", "" } type mockVideoStoredReqFetcher struct { @@ -1198,14 +1341,15 @@ type mockExchangeVideo struct { cache *mockCacheClient } -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 +func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + m.lastRequest = r.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"]}}}}}`) + ext := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"20.00","hb_pb_cat_dur_appnex":"20.00_395_30s","hb_size":"1x1", "hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"},"type":"video","dealpriority":0,"dealtiersatisfied":false},"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{{ + Seat: "appnexus", Bid: []openrtb.Bid{ {ID: "01", ImpID: "1_0", Ext: ext}, {ID: "02", ImpID: "1_1", Ext: ext}, @@ -1228,6 +1372,54 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb }, nil } +type mockExchangeAppendBidderNames struct { + lastRequest *openrtb.BidRequest + cache *mockCacheClient +} + +func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + m.lastRequest = r.BidRequest + if debugLog != nil && debugLog.Enabled { + m.cache.called = true + } + ext := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"20.00","hb_pb_cat_dur_appnex":"20.00_395_30s_appnexus","hb_size":"1x1", "hb_uuid_appnexus":"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{{ + Seat: "appnexus", + Bid: []openrtb.Bid{ + {ID: "01", ImpID: "1_0", Ext: ext}, + {ID: "02", ImpID: "1_1", Ext: ext}, + {ID: "03", ImpID: "1_2", Ext: ext}, + {ID: "04", ImpID: "1_3", Ext: ext}, + {ID: "05", ImpID: "2_0", Ext: ext}, + {ID: "06", ImpID: "2_1", Ext: ext}, + {ID: "07", ImpID: "2_2", Ext: ext}, + {ID: "08", ImpID: "3_0", Ext: ext}, + {ID: "09", ImpID: "3_1", Ext: ext}, + {ID: "10", ImpID: "3_2", Ext: ext}, + {ID: "11", ImpID: "3_3", Ext: ext}, + {ID: "12", ImpID: "3_5", Ext: ext}, + {ID: "13", ImpID: "4_0", Ext: ext}, + {ID: "14", ImpID: "5_0", Ext: ext}, + {ID: "15", ImpID: "5_1", Ext: ext}, + {ID: "16", ImpID: "5_2", Ext: ext}, + }, + }}, + }, nil +} + +type mockExchangeVideoNoBids struct { + lastRequest *openrtb.BidRequest + cache *mockCacheClient +} + +func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) { + m.lastRequest = r.BidRequest + return &openrtb.BidResponse{ + SeatBid: []openrtb.SeatBid{{}}, + }, nil +} + var testVideoStoredImpData = map[string]json.RawMessage{ "fba10607-0c12-43d1-ad07-b8a513bc75d6": json.RawMessage(`{"ext": {"appnexus": {"placementId": 14997137}}}`), "8b452b41-2681-4a20-9086-6f16ffad7773": json.RawMessage(`{"ext": {"appnexus": {"placementId": 15016213}}}`), diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 7b056d85f4b..d731b2dff17 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -443,8 +443,8 @@ 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, bool, error) { - return g.allowPI, g.allowPI, nil +func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return g.allowPI, g.allowPI, g.allowPI, nil } func (g *mockPermsSetUID) AMPException() bool { diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go index c01dc64da52..b8855583a2a 100755 --- a/exchange/adapter_map.go +++ b/exchange/adapter_map.go @@ -9,26 +9,33 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters" ttx "github.com/PubMatic-OpenWrap/prebid-server/adapters/33across" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/acuityads" "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/adman" "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/adprime" "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/amx" "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/between" "github.com/PubMatic-OpenWrap/prebid-server/adapters/brightroll" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/colossus" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/connectad" "github.com/PubMatic-OpenWrap/prebid-server/adapters/consumable" "github.com/PubMatic-OpenWrap/prebid-server/adapters/conversant" "github.com/PubMatic-OpenWrap/prebid-server/adapters/cpmstar" @@ -42,17 +49,22 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters/grid" "github.com/PubMatic-OpenWrap/prebid-server/adapters/gumgum" "github.com/PubMatic-OpenWrap/prebid-server/adapters/improvedigital" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/inmobi" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/invibes" "github.com/PubMatic-OpenWrap/prebid-server/adapters/ix" "github.com/PubMatic-OpenWrap/prebid-server/adapters/kidoz" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/krushmedia" "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/logicad" "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/nobid" "github.com/PubMatic-OpenWrap/prebid-server/adapters/openx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/orbidder" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pubmatic" @@ -62,7 +74,11 @@ 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/silvermob" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/smaato" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/smartadserver" "github.com/PubMatic-OpenWrap/prebid-server/adapters/smartrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/smartyads" "github.com/PubMatic-OpenWrap/prebid-server/adapters/somoaudience" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sonobi" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sovrn" @@ -93,26 +109,33 @@ import ( 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.BidderAcuityAds: acuityads.NewAcuityAdsBidder(cfg.Adapters[string(openrtb_ext.BidderAcuityAds)].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.BidderAdman: adman.NewAdmanBidder(cfg.Adapters[string(openrtb_ext.BidderAdman)].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.BidderAdprime: adprime.NewAdprimeBidder(cfg.Adapters[string(openrtb_ext.BidderAdprime)].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.BidderAMX: amx.NewAMXBidder(cfg.Adapters[string(openrtb_ext.BidderAMX)].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), 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.BidderBrightroll: brightroll.NewBrightrollBidder(cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderBrightroll)].ExtraAdapterInfo), + openrtb_ext.BidderColossus: colossus.NewColossusBidder(cfg.Adapters[string(openrtb_ext.BidderColossus)].Endpoint), + openrtb_ext.BidderConnectAd: connectad.NewConnectAdBidder(cfg.Adapters[string(openrtb_ext.BidderConnectAd)].Endpoint), openrtb_ext.BidderConsumable: consumable.NewConsumableBidder(cfg.Adapters[string(openrtb_ext.BidderConsumable)].Endpoint), + openrtb_ext.BidderConversant: conversant.NewConversantBidder(cfg.Adapters[string(openrtb_ext.BidderConversant)].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), @@ -120,7 +143,6 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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), @@ -128,15 +150,20 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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.BidderInMobi: inmobi.NewInMobiAdapter(cfg.Adapters[string(openrtb_ext.BidderInMobi)].Endpoint), + openrtb_ext.BidderInvibes: invibes.NewInvibesBidder(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderInvibes))].Endpoint), openrtb_ext.BidderKidoz: kidoz.NewKidozBidder(cfg.Adapters[string(openrtb_ext.BidderKidoz)].Endpoint), + openrtb_ext.BidderKrushmedia: krushmedia.NewKrushmediaBidder(cfg.Adapters[string(openrtb_ext.BidderKrushmedia)].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.BidderLogicad: logicad.NewLogicadBidder(cfg.Adapters[string(openrtb_ext.BidderLogicad)].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.BidderNoBid: nobid.NewNoBidBidder(cfg.Adapters[string(openrtb_ext.BidderNoBid)].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), @@ -151,7 +178,11 @@ 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.BidderSilverMob: silvermob.NewSilverMobBidder(cfg.Adapters[string(openrtb_ext.BidderSilverMob)].Endpoint), + openrtb_ext.BidderSmaato: smaato.NewSmaatoBidder(cfg.Adapters[string(openrtb_ext.BidderSmaato)].Endpoint), + openrtb_ext.BidderSmartadserver: smartadserver.NewSmartadserverBidder(cfg.Adapters[string(openrtb_ext.BidderSmartadserver)].Endpoint), openrtb_ext.BidderSmartRTB: smartrtb.NewSmartRTBBidder(cfg.Adapters[string(openrtb_ext.BidderSmartRTB)].Endpoint), + openrtb_ext.BidderSmartyAds: smartyads.NewSmartyAdsBidder(cfg.Adapters[string(openrtb_ext.BidderSmartyAds)].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), @@ -172,11 +203,10 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter 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), + openrtb_ext.BidderBetween: between.NewBetweenBidder(cfg.Adapters[string(openrtb_ext.BidderBetween)].Endpoint), } legacyBidders := map[openrtb_ext.BidderName]adapters.Adapter{ - // TODO #267: Upgrade the Conversant adapter - openrtb_ext.BidderConversant: conversant.NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderConversant)].Endpoint), // TODO #212: Upgrade the Index adapter openrtb_ext.BidderIx: ix.NewIxAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderIx))].Endpoint), // TODO #213: Upgrade the Lifestreet adapter @@ -198,7 +228,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, cfg, me) + allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client, cfg, me, name) } } diff --git a/exchange/auction.go b/exchange/auction.go index 1ead3c616c6..646c13bbcb7 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -8,13 +8,13 @@ import ( "fmt" "regexp" "strings" + "time" "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/prebid_cache_client" uuid "github.com/gofrs/uuid" - "github.com/golang/glog" ) type DebugLog struct { @@ -47,7 +47,53 @@ func (d *DebugLog) BuildCacheString() { 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 { +func (d *DebugLog) PutDebugLogError(cache prebid_cache_client.Client, timeout int, errors []error) error { + if len(d.Data.Response) == 0 && len(errors) == 0 { + d.Data.Response = "No response or errors created" + } + + if len(errors) > 0 { + errStrings := []string{} + for _, err := range errors { + errStrings = append(errStrings, err.Error()) + } + d.Data.Response = fmt.Sprintf("%s\nErrors:\n%s", d.Data.Response, strings.Join(errStrings, "\n")) + } + + d.BuildCacheString() + + if len(d.CacheKey) == 0 { + rawUUID, err := uuid.NewV4() + if err != nil { + return err + } + d.CacheKey = rawUUID.String() + } + + data, err := json.Marshal(d.CacheString) + if err != nil { + return err + } + + toCache := []prebid_cache_client.Cacheable{ + { + Type: d.CacheType, + Data: data, + TTLSeconds: d.TTL, + Key: "log_" + d.CacheKey, + }, + } + + if cache != nil { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(timeout)*time.Millisecond)) + defer cancel() + cache.PutJson(ctx, toCache) + } + + return nil +} + +func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int, preferDeals bool) *auction { winningBids := make(map[string]*pbsOrtbBid, numImps) winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName]*pbsOrtbBid, numImps) @@ -56,7 +102,7 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int for _, bid := range seatBid.bids { cpm := bid.bid.Price wbid, ok := winningBids[bid.bid.ImpID] - if !ok || cpm > wbid.bid.Price { + if !ok || isNewWinningBid(bid.bid, wbid.bid, preferDeals) { winningBids[bid.bid.ImpID] = bid } if bidMap, ok := winningBidsByBidder[bid.bid.ImpID]; ok { @@ -78,15 +124,24 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int } } +// isNewWinningBid calculates if the new bid (nbid) will win against the current winning bid (wbid) given preferDeals. +func isNewWinningBid(bid, wbid *openrtb.Bid, preferDeals bool) bool { + if preferDeals { + if len(wbid.DealID) > 0 && len(bid.DealID) == 0 { + return false + } + if len(wbid.DealID) == 0 && len(bid.DealID) > 0 { + return true + } + } + return bid.Price > wbid.Price +} + func (a *auction) setRoundedPrices(priceGranularity openrtb_ext.PriceGranularity) { roundedPrices := make(map[*pbsOrtbBid]string, 5*len(a.winningBids)) for _, topBidsPerImp := range a.winningBidsByBidder { for _, topBidPerBidder := range topBidsPerImp { - roundedPrice, err := GetCpmStringValue(topBidPerBidder.bid.Price, priceGranularity) - if err != nil { - glog.Errorf(`Error rounding price according to granularity. This shouldn't happen unless /openrtb2 input validation is buggy. Granularity was "%v".`, priceGranularity) - } - roundedPrices[topBidPerBidder] = roundedPrice + roundedPrices[topBidPerBidder] = GetPriceBucket(topBidPerBidder.bid.Price, priceGranularity) } } a.roundedPrices = roundedPrices @@ -130,7 +185,7 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, var customCacheKey string var catDur string useCustomCacheKey := false - if competitiveExclusion && isOverallWinner { + if competitiveExclusion && isOverallWinner || includeBidderKeys { // set custom cache key for winning bid when competitive exclusion applies catDur = bidCategory[topBidPerBidder.bid.ID] if len(catDur) > 0 { @@ -179,9 +234,9 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, } } - if debugLog != nil && debugLog.Enabled { - debugLog.BuildCacheString() + if len(toCache) > 0 && debugLog != nil && debugLog.Enabled { debugLog.CacheKey = hbCacheID + debugLog.BuildCacheString() if jsonBytes, err := json.Marshal(debugLog.CacheString); err == nil { toCache = append(toCache, prebid_cache_client.Cacheable{ Type: debugLog.CacheType, diff --git a/exchange/auction_test.go b/exchange/auction_test.go index 36e06a7d70a..6027a8f4bfd 100644 --- a/exchange/auction_test.go +++ b/exchange/auction_test.go @@ -280,6 +280,225 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) { } } +func TestNewAuction(t *testing.T) { + bid1p077 := pbsOrtbBid{ + bid: &openrtb.Bid{ + ImpID: "imp1", + Price: 0.77, + }, + } + bid1p123 := pbsOrtbBid{ + bid: &openrtb.Bid{ + ImpID: "imp1", + Price: 1.23, + }, + } + bid1p230 := pbsOrtbBid{ + bid: &openrtb.Bid{ + ImpID: "imp1", + Price: 2.30, + }, + } + bid1p088d := pbsOrtbBid{ + bid: &openrtb.Bid{ + ImpID: "imp1", + Price: 0.88, + DealID: "SpecialDeal", + }, + } + bid1p166d := pbsOrtbBid{ + bid: &openrtb.Bid{ + ImpID: "imp1", + Price: 1.66, + DealID: "BigDeal", + }, + } + bid2p123 := pbsOrtbBid{ + bid: &openrtb.Bid{ + ImpID: "imp2", + Price: 1.23, + }, + } + bid2p144 := pbsOrtbBid{ + bid: &openrtb.Bid{ + ImpID: "imp2", + Price: 1.44, + }, + } + tests := []struct { + description string + seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid + numImps int + preferDeals bool + expectedAuction auction + }{ + { + description: "Basic auction test", + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "appnexus": { + bids: []*pbsOrtbBid{&bid1p123}, + }, + "rubicon": { + bids: []*pbsOrtbBid{&bid1p230}, + }, + }, + numImps: 1, + preferDeals: false, + expectedAuction: auction{ + winningBids: map[string]*pbsOrtbBid{ + "imp1": &bid1p230, + }, + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp1": { + "appnexus": &bid1p123, + "rubicon": &bid1p230, + }, + }, + }, + }, + { + description: "Multi-imp auction", + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "appnexus": { + bids: []*pbsOrtbBid{&bid1p230, &bid2p123}, + }, + "rubicon": { + bids: []*pbsOrtbBid{&bid1p077, &bid2p144}, + }, + "openx": { + bids: []*pbsOrtbBid{&bid1p123}, + }, + }, + numImps: 2, + preferDeals: false, + expectedAuction: auction{ + winningBids: map[string]*pbsOrtbBid{ + "imp1": &bid1p230, + "imp2": &bid2p144, + }, + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp1": { + "appnexus": &bid1p230, + "rubicon": &bid1p077, + "openx": &bid1p123, + }, + "imp2": { + "appnexus": &bid2p123, + "rubicon": &bid2p144, + }, + }, + }, + }, + { + description: "Basic auction with deals, no preference", + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "appnexus": { + bids: []*pbsOrtbBid{&bid1p123}, + }, + "rubicon": { + bids: []*pbsOrtbBid{&bid1p088d}, + }, + }, + numImps: 1, + preferDeals: false, + expectedAuction: auction{ + winningBids: map[string]*pbsOrtbBid{ + "imp1": &bid1p123, + }, + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp1": { + "appnexus": &bid1p123, + "rubicon": &bid1p088d, + }, + }, + }, + }, + { + description: "Basic auction with deals, prefer deals", + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "appnexus": { + bids: []*pbsOrtbBid{&bid1p123}, + }, + "rubicon": { + bids: []*pbsOrtbBid{&bid1p088d}, + }, + }, + numImps: 1, + preferDeals: true, + expectedAuction: auction{ + winningBids: map[string]*pbsOrtbBid{ + "imp1": &bid1p088d, + }, + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp1": { + "appnexus": &bid1p123, + "rubicon": &bid1p088d, + }, + }, + }, + }, + { + description: "Auction with 2 deals", + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "appnexus": { + bids: []*pbsOrtbBid{&bid1p166d}, + }, + "rubicon": { + bids: []*pbsOrtbBid{&bid1p088d}, + }, + }, + numImps: 1, + preferDeals: true, + expectedAuction: auction{ + winningBids: map[string]*pbsOrtbBid{ + "imp1": &bid1p166d, + }, + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp1": { + "appnexus": &bid1p166d, + "rubicon": &bid1p088d, + }, + }, + }, + }, + { + description: "Auction with 3 bids and 2 deals", + seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{ + "appnexus": { + bids: []*pbsOrtbBid{&bid1p166d}, + }, + "rubicon": { + bids: []*pbsOrtbBid{&bid1p088d}, + }, + "openx": { + bids: []*pbsOrtbBid{&bid1p230}, + }, + }, + numImps: 1, + preferDeals: true, + expectedAuction: auction{ + winningBids: map[string]*pbsOrtbBid{ + "imp1": &bid1p166d, + }, + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "imp1": { + "appnexus": &bid1p166d, + "rubicon": &bid1p088d, + "openx": &bid1p230, + }, + }, + }, + }, + } + + for _, test := range tests { + auc := newAuction(test.seatBids, test.numImps, test.preferDeals) + + assert.Equal(t, test.expectedAuction, *auc, test.description) + } + +} + type cacheSpec struct { BidRequest openrtb.BidRequest `json:"bidRequest"` PbsBids []pbsBid `json:"pbsBids"` @@ -298,22 +517,27 @@ type pbsBid struct { Bidder openrtb_ext.BidderName `json:"bidder"` } -type mockCache struct { - items []prebid_cache_client.Cacheable -} - type cacheComparator struct { freq int expectedKeys []string actualKeys []string } -func (c *mockCache) GetExtCacheData() (string, string) { - return "", "" +type mockCache struct { + scheme string + host string + path string + items []prebid_cache_client.Cacheable +} + +func (c *mockCache) GetExtCacheData() (scheme string, host string, path string) { + return c.scheme, c.host, c.path } + func (c *mockCache) GetPutUrl() string { return "" } + func (c *mockCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) { c.items = values return []string{"", "", "", "", ""}, nil diff --git a/exchange/bidder.go b/exchange/bidder.go index 7e28214890a..e5939f018ed 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/http/httptrace" "time" "github.com/PubMatic-OpenWrap/prebid-server/config/util" @@ -47,7 +48,7 @@ type adaptedBidder interface { // // Any errors will be user-facing in the API. // Error messages should help publishers understand what might account for "bad" bids. - requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo, debug bool) (*pbsOrtbSeatBid, []error) + requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) } // pbsOrtbBid is a Bid returned by an adaptedBidder. @@ -56,7 +57,8 @@ 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. +// pbsOrtbBid.dealPriority is optionally provided by adapters and used internally by the exchange to support deal targeted campaigns. +// pbsOrtbBid.dealTierSatisfied is set to true by exchange.updateHbPbCatDur if deal tier satisfied otherwise it will be set to false type pbsOrtbBid struct { bid *openrtb.Bid bidType openrtb_ext.BidType @@ -88,23 +90,33 @@ 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, cfg *config.Configuration, me pbsmetrics.MetricsEngine) adaptedBidder { +func adaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me pbsmetrics.MetricsEngine, name openrtb_ext.BidderName) adaptedBidder { return &bidderAdapter{ - Bidder: bidder, - Client: client, - DebugConfig: cfg.Debug, - me: me, + Bidder: bidder, + BidderName: name, + Client: client, + me: me, + config: bidderAdapterConfig{ + Debug: cfg.Debug, + DisableConnMetrics: cfg.Metrics.Disabled.AdapterConnectionMetrics, + }, } } type bidderAdapter struct { - Bidder adapters.Bidder - Client *http.Client - DebugConfig config.Debug - me pbsmetrics.MetricsEngine + Bidder adapters.Bidder + BidderName openrtb_ext.BidderName + Client *http.Client + me pbsmetrics.MetricsEngine + config bidderAdapterConfig +} + +type bidderAdapterConfig struct { + Debug config.Debug + DisableConnMetrics bool } -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) { +func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) { reqData, errs := bidder.Bidder.MakeRequests(request, reqInfo) if len(reqData) == 0 { @@ -140,7 +152,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.Bi for i := 0; i < len(reqData); i++ { httpInfo := <-responseChannel // If this is a test bid, capture debugging info from the requests. - if debug { + if debugInfo := ctx.Value(DebugContextKey); debugInfo != nil && debugInfo.(bool) { seatBid.httpCalls = append(seatBid.httpCalls, makeExt(httpInfo)) } @@ -314,6 +326,10 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall { // doRequest makes a request, handles the response, and returns the data needed by the // Bidder interface. func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData) *httpCallInfo { + return bidder.doRequestImpl(ctx, req, glog.Warningf) +} + +func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg) *httpCallInfo { httpReq, err := http.NewRequest(req.Method, req.Uri, bytes.NewBuffer(req.Body)) if err != nil { return &httpCallInfo{ @@ -323,16 +339,27 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques } httpReq.Header = req.Headers + // If adapter connection metrics are not disabled, add the client trace + // to get complete connection info into our metrics + if !bidder.config.DisableConnMetrics { + ctx = bidder.addClientTrace(ctx) + } httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq) if err != nil { if err == context.DeadlineExceeded { err = &errortypes.Timeout{Message: err.Error()} - if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); ok { + var corebidder adapters.Bidder = bidder.Bidder + // The bidder adapter normally stores an info-aware bidder (a bidder wrapper) + // rather than the actual bidder. So we need to unpack that first. + if b, ok := corebidder.(*adapters.InfoAwareBidder); ok { + corebidder = b.Bidder + } + if tb, ok := corebidder.(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) + go bidder.doTimeoutNotification(tb, req, logger) } } @@ -368,7 +395,7 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques } } -func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData) { +func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData, logger util.LogMsg) { ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() toReq, errL := timeoutBidder.MakeTimeoutNotification(req) @@ -379,7 +406,7 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou 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) { + if bidder.config.Debug.TimeoutNotification.Log && !(bidder.config.Debug.TimeoutNotification.FailOnly && success) { var msg string if err == nil { msg = fmt.Sprintf("TimeoutNotification: status:(%d) body:%s", httpResp.StatusCode, string(toReq.Body)) @@ -387,16 +414,16 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou 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) + util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate) } } else { bidder.me.RecordTimeoutNotice(false) - if bidder.DebugConfig.TimeoutNotification.Log { + if bidder.config.Debug.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) + util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate) } } - } else if bidder.DebugConfig.TimeoutNotification.Log { + } else if bidder.config.Debug.TimeoutNotification.Log { reqJSON, err := json.Marshal(req) var msg string if err == nil { @@ -404,7 +431,7 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou } 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) + util.LogRandomSample(msg, logger, bidder.config.Debug.TimeoutNotification.SamplingRate) } } @@ -414,3 +441,34 @@ type httpCallInfo struct { response *adapters.ResponseData err error } + +// This function adds an httptrace.ClientTrace object to the context so, if connection with the bidder +// endpoint is established, we can keep track of whether the connection was newly created, reused, and +// the time from the connection request, to the connection creation. +func (bidder *bidderAdapter) addClientTrace(ctx context.Context) context.Context { + var connStart, dnsStart time.Time + + trace := &httptrace.ClientTrace{ + // GetConn is called before a connection is created or retrieved from an idle pool + GetConn: func(hostPort string) { + connStart = time.Now() + }, + // GotConn is called after a successful connection is obtained + GotConn: func(info httptrace.GotConnInfo) { + connWaitTime := time.Now().Sub(connStart) + + bidder.me.RecordAdapterConnections(bidder.BidderName, info.Reused, connWaitTime) + }, + // DNSStart is called when a DNS lookup begins. + DNSStart: func(info httptrace.DNSStartInfo) { + dnsStart = time.Now() + }, + // DNSDone is called when a DNS lookup ends. + DNSDone: func(info httptrace.DNSDoneInfo) { + dnsLookupTime := time.Now().Sub(dnsStart) + + bidder.me.RecordDNSTime(dnsLookupTime) + }, + } + return httptrace.WithClientTrace(ctx, trace) +} diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index ebf9eccbf9d..9e27bc41477 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -1,12 +1,16 @@ package exchange import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "io/ioutil" "net/http" "net/http/httptest" + "net/http/httptrace" + "strings" "testing" "time" @@ -15,8 +19,12 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/currencies" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "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/golang/glog" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" nativeRequests "github.com/PubMatic-OpenWrap/openrtb/native/request" nativeResponse "github.com/PubMatic-OpenWrap/openrtb/native/response" @@ -66,9 +74,9 @@ func TestSingleBidder(t *testing.T) { }, bidResponse: mockBidderResponse, } - 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) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) // Make sure the goodSingleBidder was called with the expected arguments. if bidderImpl.httpResponse == nil { @@ -154,9 +162,9 @@ func TestMultiBidder(t *testing.T) { }}, bidResponse: mockBidderResponse, } - 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) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if seatBid == nil { t.Fatalf("SeatBid should exist, because bids exist.") @@ -192,8 +200,10 @@ func TestBidderTimeout(t *testing.T) { defer server.Close() bidder := &bidderAdapter{ - Bidder: &mixedMultiBidder{}, - Client: server.Client(), + Bidder: &mixedMultiBidder{}, + BidderName: openrtb_ext.BidderAppnexus, + Client: server.Client(), + me: &metricsConf.DummyMetricsEngine{}, } callInfo := bidder.doRequest(ctx, &adapters.RequestData{ @@ -233,8 +243,10 @@ func TestConnectionClose(t *testing.T) { server = httptest.NewServer(handler) bidder := &bidderAdapter{ - Bidder: &mixedMultiBidder{}, - Client: server.Client(), + Bidder: &mixedMultiBidder{}, + Client: server.Client(), + BidderName: openrtb_ext.BidderAppnexus, + me: &metricsConf.DummyMetricsEngine{}, } callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{ @@ -512,12 +524,15 @@ func TestMultiCurrencies(t *testing.T) { ) // Execute: - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, - time.Duration(10)*time.Second, + time.Duration(24)*time.Hour, ) + time.Sleep(time.Duration(500) * time.Millisecond) + currencyConverter.Run() + seatBid, errs := bidder.requestBid( context.Background(), &openrtb.BidRequest{}, @@ -525,7 +540,6 @@ func TestMultiCurrencies(t *testing.T) { 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, - false, ) // Verify: @@ -661,8 +675,8 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { } // Execute: - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) - currencyConverter := currencies.NewRateConverterDefault() + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) seatBid, errs := bidder.requestBid( context.Background(), &openrtb.BidRequest{}, @@ -670,7 +684,6 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, - false, ) // Verify: @@ -828,11 +841,11 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { } // Execute: - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) currencyConverter := currencies.NewRateConverter( &http.Client{}, mockedHTTPServer.URL, - time.Duration(10)*time.Second, + time.Duration(24)*time.Hour, ) seatBid, errs := bidder.requestBid( context.Background(), @@ -843,7 +856,6 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, - false, ) // Verify: @@ -927,55 +939,6 @@ func TestSuccessfulResponseLogging(t *testing.T) { } } -// TestServerCallDebugging makes sure that we log the server calls made by the Bidder on test bids. -func TestServerCallDebugging(t *testing.T) { - respBody := "{\"bid\":false}" - respStatus := 200 - server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) - defer server.Close() - - reqBody := "{\"key\":\"val\"}" - reqUrl := server.URL - bidderImpl := &goodSingleBidder{ - httpRequest: &adapters.RequestData{ - Method: "POST", - Uri: reqUrl, - Body: []byte(reqBody), - Headers: http.Header{}, - }, - } - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) - currencyConverter := currencies.NewRateConverterDefault() - - bids, _ := bidder.requestBid( - context.Background(), - &openrtb.BidRequest{ - Test: 1, - }, - "test", - 1.0, - currencyConverter.Rates(), - &adapters.ExtraRequestInfo{}, - true, - ) - - if len(bids.httpCalls) != 1 { - t.Errorf("We should log the server call if this is a test bid. Got %d", len(bids.httpCalls)) - } - if bids.httpCalls[0].Uri != reqUrl { - t.Errorf("Wrong httpcalls URI. Expected %s, got %s", reqUrl, bids.httpCalls[0].Uri) - } - if bids.httpCalls[0].RequestBody != reqBody { - t.Errorf("Wrong httpcalls RequestBody. Expected %s, got %s", reqBody, bids.httpCalls[0].RequestBody) - } - if bids.httpCalls[0].ResponseBody != respBody { - t.Errorf("Wrong httpcalls ResponseBody. Expected %s, got %s", respBody, bids.httpCalls[0].ResponseBody) - } - if bids.httpCalls[0].Status != respStatus { - t.Errorf("Wrong httpcalls Status. Expected %d, got %d", respStatus, bids.httpCalls[0].Status) - } -} - func TestMobileNativeTypes(t *testing.T) { respBody := "{\"bid\":false}" respStatus := 200 @@ -1057,8 +1020,8 @@ func TestMobileNativeTypes(t *testing.T) { }, bidResponse: tc.mockBidderResponse, } - bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) - currencyConverter := currencies.NewRateConverterDefault() + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) seatBids, _ := bidder.requestBid( context.Background(), @@ -1067,7 +1030,6 @@ func TestMobileNativeTypes(t *testing.T) { 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, - false, ) var actualValue string @@ -1079,9 +1041,9 @@ func TestMobileNativeTypes(t *testing.T) { } func TestErrorReporting(t *testing.T) { - 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) + bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + bids, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if bids != nil { t.Errorf("There should be no seatbid if no http requests are returned.") } @@ -1234,14 +1196,91 @@ func TestSetAssetTypes(t *testing.T) { } } +func TestCallRecordAdapterConnections(t *testing.T) { + // Setup mock server + respStatus := 200 + respBody := "{\"bid\":false}" + server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + defer server.Close() + + // declare requestBid parameters + bidAdjustment := 2.0 + + bidderImpl := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{}, + } + + // setup a mock metrics engine and its expectation + metrics := &pbsmetrics.MetricsEngineMock{} + expectedAdapterName := openrtb_ext.BidderAppnexus + compareConnWaitTime := func(dur time.Duration) bool { return dur.Nanoseconds() > 0 } + + metrics.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once() + + // Run requestBid using an http.Client with a mock handler + bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, metrics, openrtb_ext.BidderAppnexus) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + _, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) + + // Assert no errors + assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs) + + // Assert RecordAdapterConnections() was called with the parameters we expected + metrics.AssertExpectations(t) +} + +type DNSDoneTripper struct{} + +func (DNSDoneTripper) RoundTrip(req *http.Request) (*http.Response, error) { + //Access the httptrace.ClientTrace + trace := httptrace.ContextClientTrace(req.Context()) + + //Force DNSDone call defined in exchange/bidder.go + trace.DNSDone(httptrace.DNSDoneInfo{}) + + resp := &http.Response{ + StatusCode: 200, + Header: map[string][]string{"Location": {"http://www.example.com/"}}, + Body: ioutil.NopCloser(strings.NewReader("postBody")), + } + return resp, nil +} + +func TestCallRecordRecordDNSTime(t *testing.T) { + // setup a mock metrics engine and its expectation + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return() + + // Instantiate the bidder that will send the request. We'll make sure to use an + // http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{}) + // gets called + bidder := &bidderAdapter{ + Bidder: &mixedMultiBidder{}, + Client: &http.Client{Transport: DNSDoneTripper{}}, + me: metricsMock, + } + + // Run test + bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}) + + // Tried one or another, none seem to work without panicking + metricsMock.AssertExpectations(t) +} + 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{ + bidderImpl := ¬ifyingBidder{ + notifyRequest: adapters.RequestData{ Method: "GET", Uri: server.URL + "/notify/me", Body: nil, @@ -1249,47 +1288,93 @@ func TestTimeoutNotificationOff(t *testing.T) { }, } bidder := &bidderAdapter{ - Bidder: bidderImpl, - Client: server.Client(), - DebugConfig: config.Debug{}, - me: &metricsConfig.DummyMetricsEngine{}, + Bidder: bidderImpl, + Client: server.Client(), + config: bidderAdapterConfig{Debug: config.Debug{}}, + me: &metricsConf.DummyMetricsEngine{}, } if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok { t.Error("Failed to cast bidder to a TimeoutBidder") } else { - bidder.doTimeoutNotification(tb, &adapters.RequestData{}) + bidder.doTimeoutNotification(tb, &adapters.RequestData{}, glog.Warningf) } } func TestTimeoutNotificationOn(t *testing.T) { - respBody := "{\"bid\":false}" - respStatus := 200 - server := httptest.NewServer(mockHandler(respStatus, "getBody", respBody)) + // Expire context immediately to force timeout handler. + ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now()) + cancelFunc() + + // Notification logic is hardcoded for 200ms. We need to wait for a little longer than that. + server := httptest.NewServer(mockSlowHandler(205*time.Millisecond, 200, `{"bid":false}`)) defer server.Close() - bidderImpl := ¬ifingBidder{ - notiRequest: adapters.RequestData{ + bidder := ¬ifyingBidder{ + notifyRequest: adapters.RequestData{ Method: "GET", Uri: server.URL + "/notify/me", Body: nil, Headers: http.Header{}, }, } - bidder := &bidderAdapter{ - Bidder: bidderImpl, + + // Wrap with BidderInfo to mimic exchange.go flow. + bidderWrappedWithInfo := wrapWithBidderInfo(bidder) + + bidderAdapter := &bidderAdapter{ + Bidder: bidderWrappedWithInfo, Client: server.Client(), - DebugConfig: config.Debug{ - TimeoutNotification: config.TimeoutNotification{ - Log: true, + config: bidderAdapterConfig{ + Debug: config.Debug{ + TimeoutNotification: config.TimeoutNotification{ + Log: true, + SamplingRate: 1.0, + }, }, }, - me: &metricsConfig.DummyMetricsEngine{}, + me: &metricsConf.DummyMetricsEngine{}, } - if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); !ok { - t.Error("Failed to cast bidder to a TimeoutBidder") - } else { - bidder.doTimeoutNotification(tb, &adapters.RequestData{}) + + // Unwrap To Mimic exchange.go Casting Code + var coreBidder adapters.Bidder = bidderAdapter.Bidder + if b, ok := coreBidder.(*adapters.InfoAwareBidder); ok { + coreBidder = b.Bidder + } + if _, ok := coreBidder.(adapters.TimeoutBidder); !ok { + t.Fatal("Failed to cast bidder to a TimeoutBidder") + } + + bidRequest := adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte(`{"id":"this-id","app":{"publisher":{"id":"pub-id"}}}`), } + + var loggerBuffer bytes.Buffer + logger := func(msg string, args ...interface{}) { + loggerBuffer.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...)) + } + + bidderAdapter.doRequestImpl(ctx, &bidRequest, logger) + + // Wait a little longer than the 205ms mock server sleep. + time.Sleep(210 * time.Millisecond) + + logExpected := "TimeoutNotification: error:(context deadline exceeded) body:\n" + logActual := loggerBuffer.String() + assert.EqualValues(t, logExpected, logActual) +} + +func wrapWithBidderInfo(bidder adapters.Bidder) adapters.Bidder { + bidderInfo := adapters.BidderInfo{ + Status: adapters.StatusActive, + Capabilities: &adapters.CapabilitiesInfo{ + App: &adapters.PlatformInfo{ + MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}, + }, + }, + } + return adapters.EnforceBidderInfo(bidder, bidderInfo) } type goodSingleBidder struct { @@ -1366,18 +1451,19 @@ func (bidder *bidRejector) MakeBids(internalRequest *openrtb.BidRequest, externa return nil, []error{errors.New("Can't make a response.")} } -type notifingBidder struct { - notiRequest adapters.RequestData +type notifyingBidder struct { + requests []*adapters.RequestData + notifyRequest adapters.RequestData } -func (bidder *notifingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - return nil, nil +func (bidder *notifyingBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + return bidder.requests, nil } -func (bidder *notifingBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { +func (bidder *notifyingBidder) 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 +func (bidder *notifyingBidder) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { + return &bidder.notifyRequest, nil } diff --git a/exchange/bidder_validate_bids.go b/exchange/bidder_validate_bids.go index 723515800ba..9981a83f54c 100644 --- a/exchange/bidder_validate_bids.go +++ b/exchange/bidder_validate_bids.go @@ -7,11 +7,9 @@ import ( "strings" "github.com/PubMatic-OpenWrap/openrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/currencies" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" - - "github.com/PubMatic-OpenWrap/prebid-server/adapters" - "golang.org/x/text/currency" ) @@ -30,8 +28,8 @@ type validatedBidder struct { bidder adaptedBidder } -func (v *validatedBidder) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo, debug bool) (*pbsOrtbSeatBid, []error) { - seatBid, errs := v.bidder.requestBid(ctx, request, name, bidAdjustment, conversions, reqInfo, debug) +func (v *validatedBidder) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) { + seatBid, errs := v.bidder.requestBid(ctx, request, name, bidAdjustment, conversions, reqInfo) if validationErrors := removeInvalidBids(request, seatBid); len(validationErrors) > 0 { errs = append(errs, validationErrors...) } diff --git a/exchange/bidder_validate_bids_test.go b/exchange/bidder_validate_bids_test.go index 332a67d8c62..99f687ff6bc 100644 --- a/exchange/bidder_validate_bids_test.go +++ b/exchange/bidder_validate_bids_test.go @@ -42,7 +42,7 @@ func TestAllValidBids(t *testing.T) { }, }, }) - seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}, false) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}) assert.Len(t, seatBid.bids, 3) assert.Len(t, errs, 0) } @@ -83,7 +83,7 @@ func TestAllBadBids(t *testing.T) { }, }, }) - seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}, false) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}) assert.Len(t, seatBid.bids, 0) assert.Len(t, errs, 5) } @@ -126,7 +126,7 @@ func TestMixedBids(t *testing.T) { }, }, }) - seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}, false) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}) assert.Len(t, seatBid.bids, 2) assert.Len(t, errs, 3) } @@ -246,7 +246,7 @@ func TestCurrencyBids(t *testing.T) { Cur: tc.brqCur, } - seatBid, errs := bidder.requestBid(context.Background(), request, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}, false) + seatBid, errs := bidder.requestBid(context.Background(), request, openrtb_ext.BidderAppnexus, 1.0, currencies.NewConstantRates(), &adapters.ExtraRequestInfo{}) assert.Len(t, seatBid.bids, expectedValidBids) assert.Len(t, errs, expectedErrs) } @@ -257,6 +257,6 @@ type mockAdaptedBidder struct { errorResponse []error } -func (b *mockAdaptedBidder) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo, debug bool) (*pbsOrtbSeatBid, []error) { +func (b *mockAdaptedBidder) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) { return b.bidResponse, b.errorResponse } diff --git a/exchange/cachetest/customcachekey.json b/exchange/cachetest/customcachekey.json index bb2a37ca356..9a9008fe5d7 100644 --- a/exchange/cachetest/customcachekey.json +++ b/exchange/cachetest/customcachekey.json @@ -26,14 +26,14 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"appbid001\",\"impid\": \"oneImp\",\"price\": 7.64,\"cat\": [\"11_sports_22\"]}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"appbid001\",\"impid\": \"oneImp\",\"price\": 7.64,\"cat\": [\"11_sports_22\"]}" }, { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"pubbid001\", \"impid\": \"oneImp\", \"price\": 5.64, \"cat\": [\"33_news_44\"]}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"pubbid001\", \"impid\": \"oneImp\", \"price\": 5.64, \"cat\": [\"33_news_44\"]}" } ], "defaultTTLs": { diff --git a/exchange/cachetest/customcachekey_no_bidders.json b/exchange/cachetest/customcachekey_no_bidders.json index b8521582a47..9824ed55d15 100644 --- a/exchange/cachetest/customcachekey_no_bidders.json +++ b/exchange/cachetest/customcachekey_no_bidders.json @@ -26,9 +26,9 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}" } ], "defaultTTLs": { diff --git a/exchange/cachetest/customcachekey_no_winners.json b/exchange/cachetest/customcachekey_no_winners.json index f6204db37f5..75ef628e175 100644 --- a/exchange/cachetest/customcachekey_no_winners.json +++ b/exchange/cachetest/customcachekey_no_winners.json @@ -26,14 +26,14 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}" }, { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\":\"pubbid001\",\"impid\":\"oneImp\",\"price\":5.64,\"cat\":[\"33_news_44\"]}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\":\"pubbid001\",\"impid\":\"oneImp\",\"price\":5.64,\"cat\":[\"33_news_44\"]}" } ], "defaultTTLs": { diff --git a/exchange/cachetest/debuglog_disabled.json b/exchange/cachetest/debuglog_disabled.json index 88d6332cb09..a7efeff0db5 100644 --- a/exchange/cachetest/debuglog_disabled.json +++ b/exchange/cachetest/debuglog_disabled.json @@ -36,13 +36,13 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" }, { - "Type": "json", - "TTLSeconds": 3660, - "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}" + "type": "json", + "ttlseconds": 3660, + "value": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}" } ], "defaultTTLs": { diff --git a/exchange/cachetest/debuglog_enabled.json b/exchange/cachetest/debuglog_enabled.json index 670b694f7a7..e6c85c57055 100644 --- a/exchange/cachetest/debuglog_enabled.json +++ b/exchange/cachetest/debuglog_enabled.json @@ -36,17 +36,17 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" }, { - "Type": "json", - "TTLSeconds": 3660, - "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}" + "type": "json", + "ttlseconds": 3660, + "value": "{\"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" + "type": "xml", + "ttlseconds": 3600, + "value": "\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": { diff --git a/exchange/cachetest/debuglog_enabled_no_winners_nor_bids.json b/exchange/cachetest/debuglog_enabled_no_winners_nor_bids.json new file mode 100644 index 00000000000..637b33e171b --- /dev/null +++ b/exchange/cachetest/debuglog_enabled_no_winners_nor_bids.json @@ -0,0 +1,54 @@ +{ + "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": [], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners": false, + "targetDataIncludeBidderKeys": false, + "targetDataIncludeCacheBids": true, + "targetDataIncludeCacheVast": false +} \ No newline at end of file diff --git a/exchange/cachetest/defaultbanner.json b/exchange/cachetest/defaultbanner.json index ca44589cb1b..8bc59f632fe 100644 --- a/exchange/cachetest/defaultbanner.json +++ b/exchange/cachetest/defaultbanner.json @@ -24,13 +24,13 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600}" + "type": "json", + "ttlseconds": 660, + "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600}" }, { - "Type": "json", - "TTLSeconds": 360, - "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }" + "type": "json", + "ttlseconds": 360, + "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }" } ], "defaultTTLs": { diff --git a/exchange/cachetest/defaultbanner_no_bidders.json b/exchange/cachetest/defaultbanner_no_bidders.json index d517182168d..6dc534b5c6f 100644 --- a/exchange/cachetest/defaultbanner_no_bidders.json +++ b/exchange/cachetest/defaultbanner_no_bidders.json @@ -24,9 +24,9 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" + "type": "json", + "ttlseconds": 660, + "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" } ], "defaultTTLs": { diff --git a/exchange/cachetest/defaultbanner_no_winners.json b/exchange/cachetest/defaultbanner_no_winners.json index fe43462b241..154e1faa600 100644 --- a/exchange/cachetest/defaultbanner_no_winners.json +++ b/exchange/cachetest/defaultbanner_no_winners.json @@ -24,13 +24,13 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" + "type": "json", + "ttlseconds": 660, + "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" }, { - "Type": "json", - "TTLSeconds": 360, - "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }" + "type": "json", + "ttlseconds": 360, + "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }" } ], "defaultTTLs": { diff --git a/exchange/cachetest/defaultvideo.json b/exchange/cachetest/defaultvideo.json index 4d38585ddf1..8d7fcfed836 100644 --- a/exchange/cachetest/defaultvideo.json +++ b/exchange/cachetest/defaultvideo.json @@ -26,13 +26,13 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" }, { - "Type": "json", - "TTLSeconds": 3660, - "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}" + "type": "json", + "ttlseconds": 3660, + "value": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}" } ], "defaultTTLs": { diff --git a/exchange/cachetest/defaultvideo_no_bidders.json b/exchange/cachetest/defaultvideo_no_bidders.json index 0a9e9f1de61..ab8f13ff5d5 100644 --- a/exchange/cachetest/defaultvideo_no_bidders.json +++ b/exchange/cachetest/defaultvideo_no_bidders.json @@ -26,9 +26,9 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}" } ], "defaultTTLs": { diff --git a/exchange/cachetest/defaultvideo_no_winners.json b/exchange/cachetest/defaultvideo_no_winners.json index 98a12a5ad2b..21ad558ce4c 100644 --- a/exchange/cachetest/defaultvideo_no_winners.json +++ b/exchange/cachetest/defaultvideo_no_winners.json @@ -28,13 +28,13 @@ }], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 660, - "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"nurl\": \"http://domain.com/win-notify/1\"}" + "type": "json", + "ttlseconds": 660, + "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"nurl\": \"http://domain.com/win-notify/1\"}" }, { - "Type": "json", - "TTLSeconds": 3660, - "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64, \"nurl\": \"http://domain.com/win-notify/1\"}" + "type": "json", + "ttlseconds": 3660, + "value": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64, \"nurl\": \"http://domain.com/win-notify/1\"}" } ], "defaultTTLs": { diff --git a/exchange/cachetest/multibid.json b/exchange/cachetest/multibid.json index f2405466235..09095bd51f2 100644 --- a/exchange/cachetest/multibid.json +++ b/exchange/cachetest/multibid.json @@ -54,25 +54,25 @@ ], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 360, - "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" + "type": "json", + "ttlseconds": 360, + "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" }, { - "Type": "json", - "TTLSeconds": 260, - "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\": 200 }" + "type": "json", + "ttlseconds": 260, + "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\": 200 }" }, { - "Type": "json", - "TTLSeconds": 360, - "Data": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }" + "type": "json", + "ttlseconds": 360, + "value": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }" }, { - "Type": "json", - "TTLSeconds": 0, - "Data": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }" + "type": "json", + "ttlseconds": 0, + "value": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }" }, { - "Type": "json", - "TTLSeconds": 960, - "Data": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\": 900 }" + "type": "json", + "ttlseconds": 960, + "value": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\": 900 }" } ], "targetDataIncludeWinners":true, diff --git a/exchange/cachetest/multibid_no_bidders.json b/exchange/cachetest/multibid_no_bidders.json index 1ec47579daf..446ae9ca189 100644 --- a/exchange/cachetest/multibid_no_bidders.json +++ b/exchange/cachetest/multibid_no_bidders.json @@ -54,13 +54,13 @@ ], "expectedCacheables": [ { - "Type": "json", - "Data": "{\"id\": \"bidOne\",\"impid\": \"oneImp\",\"price\": 7.64,\"exp\": 600}", - "TTLSeconds": 360 + "type": "json", + "value": "{\"id\": \"bidOne\",\"impid\": \"oneImp\",\"price\": 7.64,\"exp\": 600}", + "ttlseconds": 360 }, { - "Type": "json", - "Data": "{\"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\": 900}", - "TTLSeconds": 960 + "type": "json", + "value": "{\"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\": 900}", + "ttlseconds": 960 } ], "targetDataIncludeWinners":true, diff --git a/exchange/cachetest/multibid_no_winners.json b/exchange/cachetest/multibid_no_winners.json index 2221a54ca3c..2f260076d18 100644 --- a/exchange/cachetest/multibid_no_winners.json +++ b/exchange/cachetest/multibid_no_winners.json @@ -54,25 +54,25 @@ ], "expectedCacheables": [ { - "Type": "json", - "TTLSeconds": 360, - "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" + "type": "json", + "ttlseconds": 360, + "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }" }, { - "Type": "json", - "TTLSeconds": 260, - "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\": 200 }" + "type": "json", + "ttlseconds": 260, + "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\": 200 }" }, { - "Type": "json", - "TTLSeconds": 360, - "Data": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }" + "type": "json", + "ttlseconds": 360, + "value": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }" }, { - "Type": "json", - "TTLSeconds": 0, - "Data": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }" + "type": "json", + "ttlseconds": 0, + "value": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }" }, { - "Type": "json", - "TTLSeconds": 960, - "Data": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\": 900 }" + "type": "json", + "ttlseconds": 960, + "value": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\": 900 }" } ], "targetDataIncludeWinners":false, diff --git a/exchange/customcachekeytest/customcachekey.json b/exchange/customcachekeytest/customcachekey.json index e578f980943..9b903575edc 100644 --- a/exchange/customcachekeytest/customcachekey.json +++ b/exchange/customcachekeytest/customcachekey.json @@ -28,14 +28,14 @@ }], "expectedCacheables": [ { - "Type": "xml", - "TTLSeconds": 660, - "Key": "11_sports_22_", - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 660, + "key": "11_sports_22_", + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" }, { - "Type": "xml", - "TTLSeconds": 660, - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 660, + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" } ], "defaultTTLs": { diff --git a/exchange/customcachekeytest/customcachekey_no_bidders.json b/exchange/customcachekeytest/customcachekey_no_bidders.json index 0f09c8dbb9d..589d1e6b4f1 100644 --- a/exchange/customcachekeytest/customcachekey_no_bidders.json +++ b/exchange/customcachekeytest/customcachekey_no_bidders.json @@ -27,10 +27,10 @@ }], "expectedCacheables": [ { - "Type": "xml", - "TTLSeconds": 660, - "Key": "11_sports_22_", - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 660, + "key": "11_sports_22_", + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" } ], "defaultTTLs": { diff --git a/exchange/customcachekeytest/customcachekey_no_winners.json b/exchange/customcachekeytest/customcachekey_no_winners.json index f21c8bda6a1..3eaf6cfb46a 100644 --- a/exchange/customcachekeytest/customcachekey_no_winners.json +++ b/exchange/customcachekeytest/customcachekey_no_winners.json @@ -27,14 +27,14 @@ }], "expectedCacheables": [ { - "Type": "xml", - "TTLSeconds": 660, - "Key": "11_sports_22_", - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 660, + "key": "11_sports_22_", + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" }, { - "Type": "xml", - "TTLSeconds": 660, - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 660, + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" } ], "defaultTTLs": { diff --git a/exchange/exchange.go b/exchange/exchange.go index a898e608355..8fad1d748ad 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -7,15 +7,16 @@ import ( "errors" "fmt" "math/rand" - - // "math/rand" "net/http" + "net/url" "runtime/debug" "sort" + "strconv" "strings" "time" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" + uuid "github.com/gofrs/uuid" "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters" @@ -29,16 +30,25 @@ import ( "github.com/golang/glog" ) +type ContextKey string + +const DebugContextKey = ContextKey("debugInfo") + +type extCacheInstructions struct { + cacheBids, cacheVAST, returnCreative bool +} + // 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, debugLog *DebugLog) (*openrtb.BidResponse, error) + HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb.BidResponse, error) } // IdFetcher can find the user's ID for a specific Bidder. type IdFetcher interface { // GetId returns the ID for the bidder. The boolean will be true if the ID exists, and false otherwise. GetId(bidder openrtb_ext.BidderName) (string, bool) + LiveSyncCount() int } type exchange struct { @@ -49,8 +59,8 @@ type exchange struct { gDPR gdpr.Permissions currencyConverter *currencies.RateConverter UsersyncIfAmbiguous bool - defaultTTLs config.DefaultTTLs privacyConfig config.Privacy + categoriesFetcher stored_requests.CategoryFetcher } // Container to pass out response ext data from the GetAllBids goroutines back into the main thread @@ -68,7 +78,7 @@ type bidResponseWrapper struct { bidder openrtb_ext.BidderName } -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 { +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, categoriesFetcher stored_requests.CategoryFetcher) Exchange { e := new(exchange) e.adapterMap = newAdapterMap(client, cfg, infos, metricsEngine) @@ -78,101 +88,79 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con e.gDPR = gDPR e.currencyConverter = currencyConverter e.UsersyncIfAmbiguous = cfg.GDPR.UsersyncIfAmbiguous - e.defaultTTLs = cfg.CacheURL.DefaultTTLs e.privacyConfig = config.Privacy{ CCPA: cfg.CCPA, GDPR: cfg.GDPR, LMT: cfg.LMT, } + e.categoriesFetcher = categoriesFetcher return e } -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 - err := json.Unmarshal(bidRequest.Ext, &requestExt) - if err != nil { - return nil, fmt.Errorf("Error decoding Request.ext : %s", err.Error()) - } +type AuctionRequest struct { + BidRequest *openrtb.BidRequest + Account config.Account + UserSyncs IdFetcher + RequestType pbsmetrics.RequestType - if requestExt.Prebid.Debug == 1 { - debug = true - } - } + // LegacyLabels is included here for temporary compatability with cleanOpenRTBRequests + // in HoldAuction until we get to factoring it away. Do not use for anything new. + LegacyLabels pbsmetrics.Labels +} - // Snapshot of resolved bid request for debug if test request - resolvedRequest, err := buildResolvedRequest(bidRequest, debug) +func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb.BidResponse, error) { + var err error + requestExt, err := extractBidRequestExt(r.BidRequest) if err != nil { - glog.Errorf("Error marshalling bid request for debug: %v", err) + return nil, err } - for _, impInRequest := range bidRequest.Imp { - var impLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{ - BannerImps: impInRequest.Banner != nil, - VideoImps: impInRequest.Video != nil, - AudioImps: impInRequest.Audio != nil, - NativeImps: impInRequest.Native != nil, - } - e.me.RecordImps(impLabels) + cacheInstructions := getExtCacheInstructions(requestExt) + targData := getExtTargetData(requestExt, &cacheInstructions) + if targData != nil { + _, targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() + } + + debugInfo := getDebugInfo(r.BidRequest, requestExt) + if debugInfo { + ctx = e.makeDebugContext(ctx, debugInfo) } + bidAdjustmentFactors := getExtBidAdjustmentFactors(requestExt) + + recordImpMetrics(r.BidRequest, e.me) + + // Make our best guess if GDPR applies + usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(r.BidRequest) + // 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.privacyConfig) + cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, r.BidRequest, requestExt, r.UserSyncs, blabels, r.LegacyLabels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig, &r.Account) + + e.me.RecordRequestPrivacy(privacyLabels) // List of bidders we have requests for. liveAdapters := listBiddersWithRequests(cleanRequests) - // Process the request to check for targeting parameters. - var targData *targetData - shouldCacheBids := false - shouldCacheVAST := false - var bidAdjustmentFactors map[string]float64 - var requestExt openrtb_ext.ExtRequest - if len(bidRequest.Ext) > 0 { - err := json.Unmarshal(bidRequest.Ext, &requestExt) - if err != nil { - return nil, fmt.Errorf("Error decoding Request.ext : %s", err.Error()) - } - bidAdjustmentFactors = requestExt.Prebid.BidAdjustmentFactors - if requestExt.Prebid.Cache != nil { - shouldCacheBids = requestExt.Prebid.Cache.Bids != nil - shouldCacheVAST = requestExt.Prebid.Cache.VastXML != nil - } - - if requestExt.Prebid.Targeting != nil { - targData = &targetData{ - priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, - includeWinners: requestExt.Prebid.Targeting.IncludeWinners, - includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys, - includeCacheBids: shouldCacheBids, - includeCacheVast: shouldCacheVAST, - } - targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() - } - } - // If we need to cache bids, then it will take some time to call prebid cache. // We should reduce the amount of time the bidders have, to compensate. - auctionCtx, cancel := e.makeAuctionContext(ctx, shouldCacheBids) //Why no context for `shouldCacheVast`? + auctionCtx, cancel := e.makeAuctionContext(ctx, cacheInstructions.cacheBids) defer cancel() // Get currency rates conversions for the auction conversions := e.currencyConverter.Rates() - adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions, debug) + adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions) - var auc *auction = nil - var bidResponseExt *openrtb_ext.ExtBidResponse = nil + var auc *auction + var cacheErrs []error 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 var rejections []string - bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, bidRequest, requestExt, adapterBids, *categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, r.BidRequest, requestExt, adapterBids, e.categoriesFetcher, targData) if err != nil { return nil, fmt.Errorf("Error in category mapping : %s", err.Error()) } @@ -181,56 +169,86 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque } } - auc = newAuction(adapterBids, len(bidRequest.Imp)) - if targData != nil { + // A non-nil auction is only needed if targeting is active. (It is used below this block to extract cache keys) + auc = newAuction(adapterBids, len(r.BidRequest.Imp), targData.preferDeals) auc.setRoundedPrices(targData.priceGranularity) if requestExt.Prebid.SupportDeals { - dealErrs := applyDealSupport(bidRequest, auc, bidCategory) + dealErrs := applyDealSupport(r.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) + cacheErrs := auc.doCache(ctx, e.cache, targData, r.BidRequest, 60, &r.Account.CacheTTL, bidCategory, debugLog) if len(cacheErrs) > 0 { errs = append(errs, cacheErrs...) } - targData.setTargeting(auc, bidRequest.App != nil, bidCategory) + targData.setTargeting(auc, r.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...) - } } + } + bidResponseExt := e.makeExtBidResponse(adapterBids, adapterExtra, r.BidRequest, debugInfo, errs) + + // Ensure caching errors are added in case auc.doCache was called and errors were returned + if len(cacheErrs) > 0 { + bidderCacheErrs := errsToBidderErrors(cacheErrs) + bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = append(bidResponseExt.Errors[openrtb_ext.PrebidExtKey], bidderCacheErrs...) + } + + if debugLog != nil && debugLog.Enabled { + 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, err) + } + if !anyBidsReturned { + if rawUUID, err := uuid.NewV4(); err == nil { + debugLog.CacheKey = rawUUID.String() + } else { + errs = append(errs, err) + } + } } // Build the response - return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, bidResponseExt, debug, errs) + return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, errs) } -type DealTierInfo struct { - Prefix string `json:"prefix"` - MinDealTier int `json:"minDealTier"` -} +func (e *exchange) parseUsersyncIfAmbiguous(bidRequest *openrtb.BidRequest) bool { + usersyncIfAmbiguous := e.UsersyncIfAmbiguous + var geo *openrtb.Geo = nil + + if bidRequest.User != nil && bidRequest.User.Geo != nil { + geo = bidRequest.User.Geo + } else if bidRequest.Device != nil && bidRequest.Device.Geo != nil { + geo = bidRequest.Device.Geo + } + if geo != nil { + // If we have a country set, and it is on the list, we assume GDPR applies if not set on the request. + // Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long). + if _, found := e.privacyConfig.GDPR.EEACountriesMap[strings.ToUpper(geo.Country)]; found { + usersyncIfAmbiguous = false + } else if len(geo.Country) == 3 { + // The country field is formatted properly as a three character country code + usersyncIfAmbiguous = true + } + } -type DealTier struct { - Info *DealTierInfo `json:"dealTier,omitempty"` + return usersyncIfAmbiguous } -type BidderDealTier struct { - DealInfo map[string]*DealTier +func recordImpMetrics(bidRequest *openrtb.BidRequest, metricsEngine pbsmetrics.MetricsEngine) { + for _, impInRequest := range bidRequest.Imp { + var impLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{ + BannerImps: impInRequest.Banner != nil, + VideoImps: impInRequest.Video != nil, + AudioImps: impInRequest.Audio != nil, + NativeImps: impInRequest.Native != nil, + } + metricsEngine.RecordImps(impLabels) + } } // applyDealSupport updates targeting keys with deal prefixes if minimum deal tier exceeded @@ -239,15 +257,13 @@ func applyDealSupport(bidRequest *openrtb.BidRequest, auc *auction, bidCategory impDealMap := getDealTiers(bidRequest) for impID, topBidsPerImp := range auc.winningBidsByBidder { - impDeal := impDealMap[impID].DealInfo + impDeal := impDealMap[impID] for bidder, topBidPerBidder := range topBidsPerImp { - bidderString := bidder.String() - if topBidPerBidder.dealPriority > 0 { - if validateAndNormalizeDealTier(impDeal[bidderString]) { - updateHbPbCatDur(topBidPerBidder, impDeal[bidderString].Info, bidCategory) + if validateDealTier(impDeal[bidder]) { + updateHbPbCatDur(topBidPerBidder, impDeal[bidder], bidCategory) } else { - errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", bidderString, impID)) + errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", string(bidder), impID)) } } } @@ -257,36 +273,29 @@ func applyDealSupport(bidRequest *openrtb.BidRequest, auc *auction, bidCategory } // getDealTiers creates map of impression to bidder deal tier configuration -func getDealTiers(bidRequest *openrtb.BidRequest) map[string]*BidderDealTier { - impDealMap := make(map[string]*BidderDealTier) +func getDealTiers(bidRequest *openrtb.BidRequest) map[string]openrtb_ext.DealTierBidderMap { + impDealMap := make(map[string]openrtb_ext.DealTierBidderMap) for _, imp := range bidRequest.Imp { - var bidderDealTier BidderDealTier - err := json.Unmarshal(imp.Ext, &bidderDealTier.DealInfo) + dealTierBidderMap, err := openrtb_ext.ReadDealTiersFromImp(imp) if err != nil { continue } - - impDealMap[imp.ID] = &bidderDealTier + impDealMap[imp.ID] = dealTierBidderMap } 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 validateDealTier(dealTier openrtb_ext.DealTier) bool { + return len(dealTier.Prefix) > 0 && dealTier.MinDealTier > 0 } -func updateHbPbCatDur(bid *pbsOrtbBid, dealTierInfo *DealTierInfo, bidCategory map[string]string) { - if bid.dealPriority >= dealTierInfo.MinDealTier { +func updateHbPbCatDur(bid *pbsOrtbBid, dealTier openrtb_ext.DealTier, bidCategory map[string]string) { + if bid.dealPriority >= dealTier.MinDealTier { + prefixTier := fmt.Sprintf("%s%d_", dealTier.Prefix, bid.dealPriority) bid.dealTierSatisfied = true - 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 @@ -297,6 +306,11 @@ func updateHbPbCatDur(bid *pbsOrtbBid, dealTierInfo *DealTierInfo, bidCategory m } } +func (e *exchange) makeDebugContext(ctx context.Context, debugInfo bool) (debugCtx context.Context) { + debugCtx = context.WithValue(ctx, DebugContextKey, debugInfo) + return +} + func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auctionCtx context.Context, cancel context.CancelFunc) { auctionCtx = ctx cancel = func() {} @@ -309,7 +323,7 @@ func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auc } // This piece sends all the requests to the bidder adapters and gathers the results. -func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, bidAdjustments map[string]float64, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, conversions currencies.Conversions, debug bool) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, map[openrtb_ext.BidderName]*seatResponseExtra, bool) { +func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, bidAdjustments map[string]float64, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, conversions currencies.Conversions) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, map[openrtb_ext.BidderName]*seatResponseExtra, bool) { // Set up pointers to the bid results adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid, len(cleanRequests)) adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(cleanRequests)) @@ -340,7 +354,7 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext } var reqInfo adapters.ExtraRequestInfo reqInfo.PbsEntryPoint = bidlabels.RType - bids, err := e.adapterMap[coreBidder].requestBid(ctx, request, aName, adjustmentFactor, conversions, &reqInfo, debug) + bids, err := e.adapterMap[coreBidder].requestBid(ctx, request, aName, adjustmentFactor, conversions, &reqInfo) // Add in time reporting elapsed := time.Since(start) @@ -389,6 +403,7 @@ func (e *exchange) getAllBids(ctx context.Context, cleanRequests map[openrtb_ext bidsFound = true bidIDsCollision = recordAdaptorDuplicateBidIDs(e.me, adapterBids) } + } if bidIDsCollision { // record this request count this request if bid collision is detected @@ -466,8 +481,9 @@ func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError { } // 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, bidResponseExt *openrtb_ext.ExtBidResponse, 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, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, errList []error) (*openrtb.BidResponse, error) { bidResponse := new(openrtb.BidResponse) + var err error bidResponse.ID = bidRequest.ID if len(liveAdapters) == 0 { @@ -481,7 +497,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ for _, a := range liveAdapters { //while processing every single bib, do we need to handle categories here? if adapterBids[a] != nil && len(adapterBids[a].bids) > 0 { - sb := e.makeSeatBid(adapterBids[a], a, adapterExtra, auc) + sb := e.makeSeatBid(adapterBids[a], a, adapterExtra, auc, returnCreative) seatBids = append(seatBids, *sb) bidResponse.Cur = adapterBids[a].currency } @@ -489,33 +505,42 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ bidResponse.SeatBid = seatBids - if bidResponseExt == nil { - bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, resolvedRequest, debug, errList) - } + bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt) + + return bidResponse, err +} + +func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, error) { buffer := &bytes.Buffer{} enc := json.NewEncoder(buffer) + enc.SetEscapeHTML(false) err := enc.Encode(bidResponseExt) - bidResponse.Ext = buffer.Bytes() - return bidResponse, err + return buffer.Bytes(), err } -func applyCategoryMapping(ctx context.Context, bidRequest *openrtb.BidRequest, 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) { +func applyCategoryMapping(ctx context.Context, bidRequest *openrtb.BidRequest, 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 { bidderName openrtb_ext.BidderName bidIndex int bidID string + bidPrice string } dedupe := make(map[string]bidDedupe) + impMap := make(map[string]*openrtb.Imp) + + // applyCategoryMapping doesn't get called unless + // requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil brandCatExt := requestExt.Prebid.Targeting.IncludeBrandCategory //If ext.prebid.targeting.includebrandcategory is present in ext then competitive exclusion feature is on. var includeBrandCategory = brandCatExt != nil //if not present - category will no be appended + appendBidderNames := requestExt.Prebid.Targeting.AppendBidderNames var primaryAdServer string var publisher string @@ -586,7 +611,7 @@ func applyCategoryMapping(ctx context.Context, bidRequest *openrtb.BidRequest, r // TODO: consider should we remove bids with zero duration here? - pb, _ = GetCpmStringValue(bid.bid.Price, targData.priceGranularity) + pb = GetPriceBucket(bid.bid.Price, targData.priceGranularity) newDur := duration if len(requestExt.Prebid.Targeting.DurationRangeSec) > 0 { @@ -613,16 +638,39 @@ func applyCategoryMapping(ctx context.Context, bidRequest *openrtb.BidRequest, r } var categoryDuration string + var dupeKey string if brandCatExt.WithCategory { categoryDuration = fmt.Sprintf("%s_%s_%ds", pb, category, newDur) + dupeKey = category } else { categoryDuration = fmt.Sprintf("%s_%ds", pb, newDur) + dupeKey = categoryDuration + } + + if appendBidderNames { + categoryDuration = fmt.Sprintf("%s_%s", categoryDuration, bidderName.String()) } if false == brandCatExt.SkipDedup { - if dupe, ok := dedupe[categoryDuration]; ok { - // 50% chance for either bid with duplicate categoryDuration values to be kept - if rand.Intn(100) < 50 { + if dupe, ok := dedupe[dupeKey]; ok { + + dupeBidPrice, err := strconv.ParseFloat(dupe.bidPrice, 64) + if err != nil { + dupeBidPrice = 0 + } + currBidPrice, err := strconv.ParseFloat(pb, 64) + if err != nil { + currBidPrice = 0 + } + if dupeBidPrice == currBidPrice { + if rand.Intn(100) < 50 { + dupeBidPrice = -1 + } else { + currBidPrice = -1 + } + } + + if dupeBidPrice < currBidPrice { if dupe.bidderName == bidderName { // An older bid from the current bidder bidsToRemove = append(bidsToRemove, dupe.bidIndex) @@ -631,7 +679,7 @@ func applyCategoryMapping(ctx context.Context, bidRequest *openrtb.BidRequest, r // 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) + seatBidsToRemove = append(seatBidsToRemove, dupe.bidderName) rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") } else { oldSeatBid.bids = append(oldSeatBid.bids[:dupe.bidIndex], oldSeatBid.bids[dupe.bidIndex+1:]...) @@ -645,9 +693,8 @@ func applyCategoryMapping(ctx context.Context, bidRequest *openrtb.BidRequest, r continue } } - dedupe[categoryDuration] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID} + dedupe[dupeKey] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID, bidPrice: pb} } - res[bidID] = categoryDuration } @@ -691,24 +738,22 @@ func getPrimaryAdServer(adServerId int) (string, error) { } // Extract all the data from the SeatBids and build the ExtBidResponse -func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, req *openrtb.BidRequest, resolvedRequest json.RawMessage, debug bool, errList []error) *openrtb_ext.ExtBidResponse { +func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, req *openrtb.BidRequest, debugInfo bool, errList []error) *openrtb_ext.ExtBidResponse { bidResponseExt := &openrtb_ext.ExtBidResponse{ Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderError, len(adapterBids)), ResponseTimeMillis: make(map[openrtb_ext.BidderName]int, len(adapterBids)), RequestTimeoutMillis: req.TMax, } - if debug { + if debugInfo { bidResponseExt.Debug = &openrtb_ext.ExtResponseDebug{ - HttpCalls: make(map[openrtb_ext.BidderName][]*openrtb_ext.ExtHttpCall), - } - if err := json.Unmarshal(resolvedRequest, &bidResponseExt.Debug.ResolvedRequest); err != nil { - glog.Errorf("Error unmarshalling bid request snapshot: %v", err) + HttpCalls: make(map[openrtb_ext.BidderName][]*openrtb_ext.ExtHttpCall), + ResolvedRequest: req, } } for bidderName, responseExtra := range adapterExtra { - if debug { + if debugInfo { bidResponseExt.Debug.HttpCalls[bidderName] = responseExtra.HttpCalls } // Only make an entry for bidder errors if the bidder reported any. @@ -727,7 +772,7 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb // Return an openrtb seatBid for a bidder // BuildBidResponse is responsible for ensuring nil bid seatbids are not included -func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction) *openrtb.SeatBid { +func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool) *openrtb.SeatBid { seatBid := new(openrtb.SeatBid) seatBid.Seat = adapter.String() // Prebid cannot support roadblocking @@ -750,7 +795,7 @@ func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.B } var errList []error - seatBid.Bid, errList = e.makeBid(adapterBid.bids, adapter, auc) + seatBid.Bid, errList = e.makeBid(adapterBid.bids, auc, returnCreative) if len(errList) > 0 { adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...) } @@ -759,7 +804,7 @@ func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.B } // Create the Bid array inside of SeatBid -func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName, auc *auction) ([]openrtb.Bid, []error) { +func (e *exchange) makeBid(Bids []*pbsOrtbBid, auc *auction, returnCreative bool) ([]openrtb.Bid, []error) { bids := make([]openrtb.Bid, 0, len(Bids)) errList := make([]error, 0, 1) for _, thisBid := range Bids { @@ -784,6 +829,9 @@ func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName, a } else { bids = append(bids, *thisBid.bid) bids[len(bids)-1].Ext = ext + if !returnCreative { + bids[len(bids)-1].AdM = "" + } } } return bids, errList @@ -791,32 +839,50 @@ func (e *exchange) makeBid(Bids []*pbsOrtbBid, adapter openrtb_ext.BidderName, a // If bid got cached inside `(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)`, // a UUID should be found inside `a.cacheIds` or `a.vastCacheIds`. This function returns the UUID along with the internal cache URL -func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auc *auction) (openrtb_ext.ExtBidPrebidCacheBids, bool) { - var cacheInfo openrtb_ext.ExtBidPrebidCacheBids - var cacheUUID string - var found bool = false - - if auc != nil { - var extCacheHost, extCachePath string - if cacheUUID, found = auc.cacheIds[bid.bid]; found { - cacheInfo.CacheId = cacheUUID - extCacheHost, extCachePath = e.cache.GetExtCacheData() - cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID - } else if cacheUUID, found = auc.vastCacheIds[bid.bid]; found { - cacheInfo.CacheId = cacheUUID - extCacheHost, extCachePath = e.cache.GetExtCacheData() - cacheInfo.Url = extCacheHost + extCachePath + "?uuid=" + cacheUUID +func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auction *auction) (cacheInfo openrtb_ext.ExtBidPrebidCacheBids, found bool) { + uuid, found := findCacheID(bid, auction) + + if found { + cacheInfo.CacheId = uuid + cacheInfo.Url = buildCacheURL(e.cache, uuid) + } + + return +} + +func findCacheID(bid *pbsOrtbBid, auction *auction) (string, bool) { + if bid != nil && bid.bid != nil && auction != nil { + if id, found := auction.cacheIds[bid.bid]; found { + return id, true + } + + if id, found := auction.vastCacheIds[bid.bid]; found { + return id, true } } - return cacheInfo, found + + return "", false } -// Returns a snapshot of resolved bid request for debug if test field is set in the incomming request -func buildResolvedRequest(bidRequest *openrtb.BidRequest, debug bool) (json.RawMessage, error) { - if debug { - return json.Marshal(bidRequest) +func buildCacheURL(cache prebid_cache_client.Client, uuid string) string { + scheme, host, path := cache.GetExtCacheData() + + if host == "" || path == "" { + return "" } - return nil, nil + + query := url.Values{"uuid": []string{uuid}} + cacheURL := url.URL{ + Scheme: scheme, + Host: host, + Path: path, + RawQuery: query.Encode(), + } + cacheURL.Query() + + // URLs without a scheme will begin with //, in which case we + // want to trim it off to keep compatbile with current behavior. + return strings.TrimPrefix(cacheURL.String(), "//") } func listBiddersWithRequests(cleanRequests map[openrtb_ext.BidderName]*openrtb.BidRequest) []openrtb_ext.BidderName { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index aa48b9b71cc..6f9e9cdfa0b 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -16,18 +16,18 @@ import ( "time" "github.com/PubMatic-OpenWrap/prebid-server/adapters" - "github.com/PubMatic-OpenWrap/prebid-server/currencies" - "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/file_fetcher" - - "github.com/PubMatic-OpenWrap/openrtb" "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/openrtb_ext" "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" pbc "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/file_fetcher" + + "github.com/PubMatic-OpenWrap/openrtb" "github.com/buger/jsonparser" "github.com/rcrowley/go-metrics" "github.com/stretchr/testify/assert" @@ -48,9 +48,13 @@ func TestNewExchange(t *testing.T) { ExpectedTimeMillis: 20, }, Adapters: blankAdapterConfig(openrtb_ext.BidderList()), + GDPR: config.GDPR{ + EEACountries: []string{"FIN", "FRA", "GUF"}, + }, } - e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), knownAdapters, config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), knownAdapters, config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter, nilCategoryFetcher{}).(*exchange) for _, bidderName := range knownAdapters { if _, ok := e.adapterMap[bidderName]; !ok { t.Errorf("NewExchange produced an Exchange without bidder %s", bidderName) @@ -87,7 +91,8 @@ func TestCharacterEscape(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() - e := NewExchange(server.Client(), nil, 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) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter, nilCategoryFetcher{}).(*exchange) /* 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */ //liveAdapters []openrtb_ext.BidderName, @@ -113,9 +118,6 @@ func TestCharacterEscape(t *testing.T) { Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`), } - //resolvedRequest json.RawMessage - resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`) - //adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, 1) adapterExtra["appnexus"] = &seatResponseExtra{ @@ -127,7 +129,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, nil, false, errList) + bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, errList) /* 5) Assert we have no errors and one '&' character as we are supposed to */ if err != nil { @@ -141,9 +143,342 @@ func TestCharacterEscape(t *testing.T) { } } -func TestGetBidCacheInfo(t *testing.T) { +// TestDebugBehaviour asserts the HttpCalls object is included inside the json "debug" field of the bidResponse extension when the +// openrtb.BidRequest "Test" value is set to 1 or the openrtb.BidRequest.Ext.Debug boolean field is set to true +func TestDebugBehaviour(t *testing.T) { + + // Define test cases + type inTest struct { + test int8 + debug bool + } + type outTest struct { + debugInfoIncluded bool + } + type aTest struct { + desc string + in inTest + out outTest + } + testCases := []aTest{ + { + desc: "test flag equals zero, ext debug flag false, no debug info expected", + in: inTest{test: 0, debug: false}, + out: outTest{debugInfoIncluded: false}, + }, + { + desc: "test flag equals zero, ext debug flag true, debug info expected", + in: inTest{test: 0, debug: true}, + out: outTest{debugInfoIncluded: true}, + }, + { + desc: "test flag equals 1, ext debug flag false, debug info expected", + in: inTest{test: 1, debug: false}, + out: outTest{debugInfoIncluded: true}, + }, + { + desc: "test flag equals 1, ext debug flag true, debug info expected", + in: inTest{test: 1, debug: true}, + out: outTest{debugInfoIncluded: true}, + }, + { + desc: "test flag not equal to 0 nor 1, ext debug flag false, no debug info expected", + in: inTest{test: 2, debug: false}, + out: outTest{debugInfoIncluded: false}, + }, + { + desc: "test flag not equal to 0 nor 1, ext debug flag true, debug info expected", + in: inTest{test: -1, debug: true}, + out: outTest{debugInfoIncluded: true}, + }, + } + + // Set up test + noBidServer := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(204) + } + server := httptest.NewServer(http.HandlerFunc(noBidServer)) + defer server.Close() + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + bidRequest := &openrtb.BidRequest{ + ID: "some-request-id", + Imp: []openrtb.Imp{{ + ID: "some-impression-id", + Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + }}, + Site: &openrtb.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + Device: &openrtb.Device{UA: "curl/7.54.0", IP: "::1"}, + AT: 1, + TMax: 500, + } + + bidderImpl := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{}, + } + + e := new(exchange) + e.adapterMap = map[openrtb_ext.BidderName]adaptedBidder{ + openrtb_ext.BidderAppnexus: adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus), + } + e.cache = &wellBehavedCache{} + e.me = &metricsConf.DummyMetricsEngine{} + e.gDPR = gdpr.AlwaysAllow{} + e.currencyConverter = currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e.categoriesFetcher = categoriesFetcher + + // Run tests + for _, test := range testCases { + bidRequest.Test = test.in.test + + if test.in.debug { + bidRequest.Ext = json.RawMessage(`{"prebid":{"debug":true}}`) + } else { + bidRequest.Ext = nil + } + + auctionRequest := AuctionRequest{ + BidRequest: bidRequest, + Account: config.Account{}, + UserSyncs: &emptyUsersync{}, + } + + // Run test + outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, nil) + + // Assert no HoldAuction error + assert.NoErrorf(t, err, "%s. ex.HoldAuction returned an error: %v \n", test.desc, err) + assert.NotNilf(t, outBidResponse.Ext, "%s. outBidResponse.Ext should not be nil \n", test.desc) + + actualExt := &openrtb_ext.ExtBidResponse{} + err = json.Unmarshal(outBidResponse.Ext, actualExt) + assert.NoErrorf(t, err, "%s. \"ext\" JSON field could not be unmarshaled. err: \"%v\" \n outBidResponse.Ext: \"%s\" \n", test.desc, err, outBidResponse.Ext) + + if test.out.debugInfoIncluded { + assert.NotNilf(t, actualExt, "%s. ext.debug field is expected to be included in this outBidResponse.Ext and not be nil. outBidResponse.Ext.Debug = %v \n", test.desc, actualExt.Debug) + + // Assert "Debug fields + assert.Greater(t, len(actualExt.Debug.HttpCalls), 0, "%s. ext.debug.httpcalls array should not be empty\n", test.desc) + assert.Equal(t, server.URL, actualExt.Debug.HttpCalls["appnexus"][0].Uri, "%s. ext.debug.httpcalls array should not be empty\n", test.desc) + assert.NotNilf(t, actualExt.Debug.ResolvedRequest, "%s. ext.debug.resolvedrequest field is expected to be included in this outBidResponse.Ext and not be nil. outBidResponse.Ext.Debug = %v \n", test.desc, actualExt.Debug) + + // If not nil, assert bid extension + if test.in.debug { + diffJson(t, test.desc, bidRequest.Ext, actualExt.Debug.ResolvedRequest.Ext) + } + } + } +} + +func TestReturnCreativeEndToEnd(t *testing.T) { + sampleAd := "" + + // Define test cases + type aTest struct { + desc string + inExt json.RawMessage + outAdM string + } + testGroups := []struct { + groupDesc string + testCases []aTest + expectError bool + }{ + { + groupDesc: "Invalid or malformed bidRequest Ext, expect error in these scenarios", + testCases: []aTest{ + { + desc: "Malformed ext in bidRequest", + inExt: json.RawMessage(`malformed`), + }, + { + desc: "empty cache field", + inExt: json.RawMessage(`{"prebid":{"cache":{}}}`), + }, + }, + expectError: true, + }, + { + groupDesc: "Valid bidRequest Ext but no returnCreative value specified, default to returning creative", + testCases: []aTest{ + { + "Nil ext in bidRequest", + nil, + sampleAd, + }, + { + "empty ext", + json.RawMessage(``), + sampleAd, + }, + { + "bids doesn't come with returnCreative value", + json.RawMessage(`{"prebid":{"cache":{"bids":{}}}}`), + sampleAd, + }, + { + "vast doesn't come with returnCreative value", + json.RawMessage(`{"prebid":{"cache":{"vastXml":{}}}}`), + sampleAd, + }, + }, + }, + { + groupDesc: "Bids field comes with returnCreative value", + testCases: []aTest{ + { + "Bids returnCreative set to true, return ad markup in response", + json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true}}}}`), + sampleAd, + }, + { + "Bids returnCreative set to false, don't return ad markup in response", + json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":false}}}}`), + "", + }, + }, + }, + { + groupDesc: "Vast field comes with returnCreative value", + testCases: []aTest{ + { + "Vast returnCreative set to true, return ad markup in response", + json.RawMessage(`{"prebid":{"cache":{"vastXml":{"returnCreative":true}}}}`), + sampleAd, + }, + { + "Vast returnCreative set to false, don't return ad markup in response", + json.RawMessage(`{"prebid":{"cache":{"vastXml":{"returnCreative":false}}}}`), + "", + }, + }, + }, + { + groupDesc: "Both Bids and Vast come with their own returnCreative value", + testCases: []aTest{ + { + "Both false, expect empty AdM", + json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":false},"vastXml":{"returnCreative":false}}}}`), + "", + }, + { + "Bids returnCreative is true, expect valid AdM", + json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true},"vastXml":{"returnCreative":false}}}}`), + sampleAd, + }, + { + "Vast returnCreative is true, expect valid AdM", + json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":false},"vastXml":{"returnCreative":true}}}}`), + sampleAd, + }, + { + "Both field's returnCreative set to true, expect valid AdM", + json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true},"vastXml":{"returnCreative":true}}}}`), + sampleAd, + }, + }, + }, + } + + // Init an exchange to run an auction from + noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } + server := httptest.NewServer(http.HandlerFunc(noBidServer)) + defer server.Close() + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + bidderImpl := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb.Bid{AdM: sampleAd}, + }, + }, + }, + } + + e := new(exchange) + e.adapterMap = map[openrtb_ext.BidderName]adaptedBidder{ + openrtb_ext.BidderAppnexus: adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus), + } + e.cache = &wellBehavedCache{} + e.me = &metricsConf.DummyMetricsEngine{} + e.gDPR = gdpr.AlwaysAllow{} + e.currencyConverter = currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e.categoriesFetcher = categoriesFetcher + + // Define mock incoming bid requeset + mockBidRequest := &openrtb.BidRequest{ + ID: "some-request-id", + Imp: []openrtb.Imp{{ + ID: "some-impression-id", + Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + }}, + Site: &openrtb.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + } + + // Run tests + for _, testGroup := range testGroups { + for _, test := range testGroup.testCases { + mockBidRequest.Ext = test.inExt + + auctionRequest := AuctionRequest{ + BidRequest: mockBidRequest, + Account: config.Account{}, + UserSyncs: &emptyUsersync{}, + } + + // Run test + outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, nil) + + // Assert return error, if any + if testGroup.expectError { + assert.Errorf(t, err, "HoldAuction expected to throw error for: %s - %s. \n", testGroup.groupDesc, test.desc) + continue + } else { + assert.NoErrorf(t, err, "%s: %s. HoldAuction error: %v \n", testGroup.groupDesc, test.desc, err) + } + + // Assert returned bid + if !assert.NotNil(t, outBidResponse, "%s: %s. outBidResponse is nil \n", testGroup.groupDesc, test.desc) { + return + } + if !assert.NotEmpty(t, outBidResponse.SeatBid, "%s: %s. outBidResponse.SeatBid is empty \n", testGroup.groupDesc, test.desc) { + return + } + if !assert.NotEmpty(t, outBidResponse.SeatBid[0].Bid, "%s: %s. outBidResponse.SeatBid[0].Bid is empty \n", testGroup.groupDesc, test.desc) { + return + } + assert.Equal(t, test.outAdM, outBidResponse.SeatBid[0].Bid[0].AdM, "Ad markup string doesn't match in: %s - %s \n", testGroup.groupDesc, test.desc) + } + } +} + +func TestGetBidCacheInfoEndToEnd(t *testing.T) { testUUID := "CACHE_UUID_1234" - testExternalCacheHost := "https://www.externalprebidcache.net" + testExternalCacheScheme := "https" + testExternalCacheHost := "www.externalprebidcache.net" testExternalCachePath := "endpoints/cache" /* 1) An adapter */ @@ -159,8 +494,9 @@ func TestGetBidCacheInfo(t *testing.T) { Host: "www.internalprebidcache.net", }, ExtCacheURL: config.ExternalCache{ - Host: testExternalCacheHost, - Path: testExternalCachePath, + Scheme: testExternalCacheScheme, + Host: testExternalCacheHost, + Path: testExternalCachePath, }, } adapterList := make([]openrtb_ext.BidderName, 0, 2) @@ -171,7 +507,8 @@ func TestGetBidCacheInfo(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() - 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) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + 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{}, currencyConverter, nilCategoryFetcher{}).(*exchange) /* 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */ liveAdapters := []openrtb_ext.BidderName{bidderName} @@ -231,9 +568,6 @@ func TestGetBidCacheInfo(t *testing.T) { }, } - //resolvedRequest json.RawMessage - resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`) - //adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ bidderName: { @@ -279,7 +613,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, nil, false, errList) + bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, errList) /* 5) Assert we have no errors and the bid response we expected*/ assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error") @@ -290,7 +624,7 @@ func TestGetBidCacheInfo(t *testing.T) { Seat: string(bidderName), Bid: []openrtb.Bid{ { - Ext: json.RawMessage(`{ "prebid": { "cache": { "bids": { "cacheId": "` + testUUID + `", "url": "` + testExternalCacheHost + `/` + testExternalCachePath + `?uuid=` + testUUID + `" }, "key": "", "url": "" }`), + Ext: json.RawMessage(`{ "prebid": { "cache": { "bids": { "cacheId": "` + testUUID + `", "url": "` + testExternalCacheScheme + `://` + testExternalCacheHost + `/` + testExternalCachePath + `?uuid=` + testUUID + `" }, "key": "", "url": "" }`), }, }, }, @@ -305,7 +639,7 @@ func TestGetBidCacheInfo(t *testing.T) { assert.Equal(t, expCacheUUID, cacheUUID, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheUUID) - // compare cache UUID + // compare cache URL expCacheURL, err := jsonparser.GetString(expectedBidResponse.SeatBid[0].Bid[0].Ext, "prebid", "cache", "bids", "url") assert.NoErrorf(t, err, "[TestGetBidCacheInfo] Error found while trying to json parse the url field from expected build response. Message: %v \n", err) @@ -315,6 +649,199 @@ func TestGetBidCacheInfo(t *testing.T) { assert.Equal(t, expCacheURL, cacheURL, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheURL) } +func TestBidReturnsCreative(t *testing.T) { + sampleAd := "" + sampleOpenrtbBid := &openrtb.Bid{ID: "some-bid-id", AdM: sampleAd} + + // Define test cases + testCases := []struct { + description string + inReturnCreative bool + expectedCreativeMarkup string + }{ + { + "returnCreative set to true, expect a full creative markup string in returned bid", + true, + sampleAd, + }, + { + "returnCreative set to false, expect empty creative markup string in returned bid", + false, + "", + }, + } + + // Test set up + sampleBids := []*pbsOrtbBid{ + { + bid: sampleOpenrtbBid, + bidType: openrtb_ext.BidTypeBanner, + bidTargets: map[string]string{}, + }, + } + sampleAuction := &auction{cacheIds: map[*openrtb.Bid]string{sampleOpenrtbBid: "CACHE_UUID_1234"}} + + noBidHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } + server := httptest.NewServer(http.HandlerFunc(noBidHandler)) + defer server.Close() + + bidderImpl := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: server.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + bidResponse: &adapters.BidderResponse{}, + } + e := new(exchange) + e.adapterMap = map[openrtb_ext.BidderName]adaptedBidder{ + openrtb_ext.BidderAppnexus: adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus), + } + e.cache = &wellBehavedCache{} + e.me = &metricsConf.DummyMetricsEngine{} + e.gDPR = gdpr.AlwaysAllow{} + e.currencyConverter = currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + + //Run tests + for _, test := range testCases { + resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative) + + assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description) + assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description) + + var bidExt openrtb_ext.ExtBid + json.Unmarshal(resultingBids[0].Ext, &bidExt) + assert.Equal(t, 0, bidExt.Prebid.DealPriority, "%s. Test should have DealPriority set to 0", test.description) + assert.Equal(t, false, bidExt.Prebid.DealTierSatisfied, "%s. Test should have DealTierSatisfied set to false", test.description) + } +} + +func TestGetBidCacheInfo(t *testing.T) { + bid := &openrtb.Bid{ID: "42"} + testCases := []struct { + description string + scheme string + host string + path string + bid *pbsOrtbBid + auction *auction + expectedFound bool + expectedCacheID string + expectedCacheURL string + }{ + { + description: "JSON Cache ID", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "https://prebid.org/cache?uuid=anyID", + }, + { + description: "VAST Cache ID", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{vastCacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "https://prebid.org/cache?uuid=anyID", + }, + { + description: "Cache ID Not Found", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{}, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + { + description: "Scheme Not Provided", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "prebid.org/cache?uuid=anyID", + }, + { + description: "Host And Path Not Provided - Without Scheme", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "", + }, + { + description: "Host And Path Not Provided - With Scheme", + scheme: "https", + bid: &pbsOrtbBid{bid: bid}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: true, + expectedCacheID: "anyID", + expectedCacheURL: "", + }, + { + description: "Nil Bid", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: nil, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + { + description: "Nil Embedded Bid", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: nil}, + auction: &auction{cacheIds: map[*openrtb.Bid]string{bid: "anyID"}}, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + { + description: "Nil Auction", + scheme: "https", + host: "prebid.org", + path: "cache", + bid: &pbsOrtbBid{bid: bid}, + auction: nil, + expectedFound: false, + expectedCacheID: "", + expectedCacheURL: "", + }, + } + + for _, test := range testCases { + exchange := &exchange{ + cache: &mockCache{ + scheme: test.scheme, + host: test.host, + path: test.path, + }, + } + + cacheInfo, found := exchange.getBidCacheInfo(test.bid, test.auction) + + assert.Equal(t, test.expectedFound, found, test.description+":found") + assert.Equal(t, test.expectedCacheID, cacheInfo.CacheId, test.description+":id") + assert.Equal(t, test.expectedCacheURL, cacheInfo.Url, test.description+":url") + } +} + func TestBidResponseCurrency(t *testing.T) { // Init objects cfg := &config.Configuration{Adapters: make(map[string]config.Adapter, 1)} @@ -324,7 +851,8 @@ func TestBidResponseCurrency(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() - e := NewExchange(server.Client(), nil, 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) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter, nilCategoryFetcher{}).(*exchange) liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" @@ -343,8 +871,6 @@ func TestBidResponseCurrency(t *testing.T) { Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`), } - resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`) - adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ "appnexus": {ResponseTimeMillis: 5}, } @@ -448,9 +974,13 @@ func TestBidResponseCurrency(t *testing.T) { }, } + bidResponseExt := &openrtb_ext.ExtBidResponse{ + ResponseTimeMillis: map[openrtb_ext.BidderName]int{openrtb_ext.BidderName("appnexus"): 5}, + RequestTimeoutMillis: 500, + } // Run tests for i := range testCases { - actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, nil, false, errList) + actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, 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)) } @@ -493,8 +1023,16 @@ func TestRaceIntegration(t *testing.T) { 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, nil) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + + auctionRequest := AuctionRequest{ + BidRequest: newRaceCheckingRequest(t), + Account: config.Account{}, + UserSyncs: &emptyUsersync{}, + } + + ex := NewExchange(server.Client(), &wellBehavedCache{}, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter, categoriesFetcher) + _, err := ex.HoldAuction(context.Background(), auctionRequest, nil) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) } @@ -577,7 +1115,8 @@ func TestPanicRecovery(t *testing.T) { } theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}) - e := NewExchange(&http.Client{}, nil, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(&http.Client{}, nil, cfg, theMetrics, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter, nilCategoryFetcher{}).(*exchange) chBids := make(chan *bidResponseWrapper, 1) panicker := func(aName openrtb_ext.BidderName, coreBidder openrtb_ext.BidderName, request *openrtb.BidRequest, bidlabels *pbsmetrics.AdapterLabels, conversions currencies.Conversions) { panic("panic!") @@ -642,7 +1181,12 @@ func TestPanicRecoveryHighLevel(t *testing.T) { Endpoint: server.URL, } } - e := NewExchange(server.Client(), &mockCache{}, 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) + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + e := NewExchange(server.Client(), &mockCache{}, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencyConverter, categoriesFetcher).(*exchange) e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{} e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{} @@ -675,11 +1219,13 @@ func TestPanicRecoveryHighLevel(t *testing.T) { }}, } - categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") - if error != nil { - t.Errorf("Failed to create a category Fetcher: %v", error) + auctionRequest := AuctionRequest{ + BidRequest: request, + Account: config.Account{}, + UserSyncs: &emptyUsersync{}, } - _, err := e.HoldAuction(context.Background(), request, &emptyUsersync{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) + + _, err := e.HoldAuction(context.Background(), auctionRequest, nil) if err != nil { t.Errorf("HoldAuction returned unexpected error: %v", err) } @@ -703,6 +1249,38 @@ func TestTimeoutComputation(t *testing.T) { } } +func TestSetDebugContextKey(t *testing.T) { + // Test cases + testCases := []struct { + desc string + inDebugInfo bool + expectedDebugInfo bool + }{ + { + desc: "debugInfo flag on, we expect to find DebugContextKey key in context", + inDebugInfo: true, + expectedDebugInfo: true, + }, + { + desc: "debugInfo flag off, we don't expect to find DebugContextKey key in context", + inDebugInfo: false, + expectedDebugInfo: false, + }, + } + + // Setup test + ex := exchange{} + + // Run tests + for _, test := range testCases { + auctionCtx := ex.makeDebugContext(context.Background(), test.inDebugInfo) + + debugInfo := auctionCtx.Value(DebugContextKey) + assert.NotNil(t, debugInfo, "%s. Flag set, `debugInfo` shouldn't be nil") + assert.Equal(t, test.expectedDebugInfo, debugInfo.(bool), "Desc: %s. Incorrect value mapped to DebugContextKey(`debugInfo`) in the context\n", test.desc) + } +} + // TestExchangeJSON executes tests for all the *.json files in exchangetest. func TestExchangeJSON(t *testing.T) { if specFiles, err := ioutil.ReadDir("./exchangetest"); err == nil { @@ -740,6 +1318,12 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { t.Fatalf("%s: Failed to parse aliases", filename) } + var s struct{} + eeac := make(map[string]struct{}) + for _, c := range []string{"FIN", "FRA", "GUF"} { + eeac[c] = s + } + privacyConfig := config.Privacy{ CCPA: config.CCPA{ Enforce: spec.EnforceCCPA, @@ -747,20 +1331,28 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { LMT: config.LMT{ Enforce: spec.EnforceLMT, }, + GDPR: config.GDPR{ + Enabled: spec.GDPREnabled, + UsersyncIfAmbiguous: !spec.AssumeGDPRApplies, + EEACountriesMap: eeac, + }, } 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) - } 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) + + auctionRequest := AuctionRequest{ + BidRequest: &spec.IncomingRequest.OrtbRequest, + Account: config.Account{}, + UserSyncs: mockIdFetcher(spec.IncomingRequest.Usersyncs), + } + + bid, err := ex.HoldAuction(context.Background(), auctionRequest, debugLog) responseTimes := extractResponseTimes(t, filename, bid) for _, bidderName := range biddersInAuction { if _, ok := responseTimes[bidderName]; !ok { @@ -864,15 +1456,21 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] } } + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Fatalf("Failed to create a category Fetcher: %v", error) + } + return &exchange{ adapterMap: adapters, me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.BidderList()), cache: &wellBehavedCache{}, cacheTime: 0, - gDPR: gdpr.AlwaysAllow{}, - currencyConverter: currencies.NewRateConverterDefault(), - UsersyncIfAmbiguous: false, + gDPR: gdpr.AlwaysFail{}, + currencyConverter: currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), + UsersyncIfAmbiguous: privacyConfig.GDPR.UsersyncIfAmbiguous, privacyConfig: privacyConfig, + categoriesFetcher: categoriesFetcher, } } @@ -976,7 +1574,7 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &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") @@ -1032,7 +1630,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &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") @@ -1085,7 +1683,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &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") @@ -1168,7 +1766,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &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") @@ -1202,19 +1800,22 @@ func TestCategoryDedupe(t *testing.T) { cats4 := []string{"IAB1-2000"} bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 15.0000, Cat: cats2, W: 1, H: 1} - bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.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} + bid5 := openrtb.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: cats1, W: 1, H: 1} bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, 0, false} bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_5 := pbsOrtbBid{&bid5, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} selectedBids := make(map[string]int) expectedCategories := map[string]string{ "bid_id1": "10.00_Electronics_30s", "bid_id2": "14.00_Sports_50s", - "bid_id3": "10.00_Electronics_30s", + "bid_id3": "20.00_Electronics_30s", + "bid_id5": "20.00_Electronics_30s", } numIterations := 10 @@ -1228,6 +1829,7 @@ func TestCategoryDedupe(t *testing.T) { &bid1_2, &bid1_3, &bid1_4, + &bid1_5, } seatBid := pbsOrtbSeatBid{innerBids, "USD", nil, nil} @@ -1235,10 +1837,10 @@ func TestCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, requestExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &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.Equal(t, 3, 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") @@ -1251,8 +1853,201 @@ func TestCategoryDedupe(t *testing.T) { } assert.Equal(t, numIterations, selectedBids["bid_id2"], "Bid 2 did not make it through every time") - assert.NotEqual(t, numIterations, selectedBids["bid_id1"], "Bid 1 made it through every time") - assert.NotEqual(t, numIterations, selectedBids["bid_id3"], "Bid 3 made it through every time") + assert.Equal(t, 0, selectedBids["bid_id1"], "Bid 1 should be rejected on every iteration due to lower price") + assert.NotEqual(t, 0, selectedBids["bid_id3"], "Bid 3 should be accepted at least once") + assert.NotEqual(t, 0, selectedBids["bid_id5"], "Bid 5 should be accepted at least once") +} + +func TestNoCategoryDedupe(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + bidRequest := &openrtb.BidRequest{} + requestExt := newExtRequestNoBrandCat() + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-3"} + cats2 := []string{"IAB1-4"} + cats4 := []string{"IAB1-2000"} + bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 14.0000, Cat: cats1, W: 1, H: 1} + bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 14.0000, Cat: cats2, W: 1, H: 1} + bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 20.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} + bid5 := openrtb.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_5 := pbsOrtbBid{&bid5, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + + selectedBids := make(map[string]int) + expectedCategories := map[string]string{ + "bid_id1": "14.00_30s", + "bid_id2": "14.00_30s", + "bid_id3": "20.00_30s", + "bid_id4": "20.00_30s", + "bid_id5": "10.00_30s", + } + + numIterations := 10 + + // Run the function many times, this should be enough for the 50% chance of which bid to remove to remove bid1 sometimes + // and bid3 others. It's conceivably possible (but highly unlikely) that the same bid get chosen every single time, but + // if you notice false fails from this test increase numIterations to make it even less likely to happen. + for i := 0; i < numIterations; i++ { + innerBids := []*pbsOrtbBid{ + &bid1_1, + &bid1_2, + &bid1_3, + &bid1_4, + &bid1_5, + } + + seatBid := pbsOrtbSeatBid{innerBids, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("appnexus") + + adapterBids[bidderName1] = &seatBid + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &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|2)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected") + assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_id(3|4)\] reason: Bid was deduplicated`), rejections[1], "Rejection message did not match expected") + assert.Equal(t, 3, len(adapterBids[bidderName1].bids), "Bidders number doesn't match") + assert.Equal(t, 3, len(bidCategory), "Bidders category mapping doesn't match") + + for bidId, bidCat := range bidCategory { + assert.Equal(t, expectedCategories[bidId], bidCat, "Category mapping doesn't match") + selectedBids[bidId]++ + } + } + assert.Equal(t, numIterations, selectedBids["bid_id5"], "Bid 5 did not make it through every time") + assert.NotEqual(t, 0, selectedBids["bid_id1"], "Bid 1 should be selected at least once") + assert.NotEqual(t, 0, selectedBids["bid_id2"], "Bid 2 should be selected at least once") + assert.NotEqual(t, 0, selectedBids["bid_id1"], "Bid 3 should be selected at least once") + assert.NotEqual(t, 0, selectedBids["bid_id4"], "Bid 4 should be selected at least once") + +} + +func TestCategoryMappingBidderName(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + bidRequest := &openrtb.BidRequest{} + requestExt := newExtRequest() + requestExt.Prebid.Targeting.AppendBidderNames = true + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-1"} + cats2 := []string{"IAB1-2"} + bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + + innerBids1 := []*pbsOrtbBid{ + &bid1_1, + } + innerBids2 := []*pbsOrtbBid{ + &bid1_2, + } + + seatBid1 := pbsOrtbSeatBid{innerBids1, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("bidder1") + + seatBid2 := pbsOrtbSeatBid{innerBids2, "USD", nil, nil} + bidderName2 := openrtb_ext.BidderName("bidder2") + + adapterBids[bidderName1] = &seatBid1 + adapterBids[bidderName2] = &seatBid2 + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &requestExt, adapterBids, categoriesFetcher, targData) + + assert.NoError(t, err, "Category mapping error should be empty") + assert.Empty(t, rejections, "There should be 0 bid rejection messages") + assert.Equal(t, "10.00_VideoGames_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") + assert.Equal(t, "10.00_HomeDecor_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") + assert.Len(t, adapterBids[bidderName1].bids, 1, "Bidders number doesn't match") + assert.Len(t, adapterBids[bidderName2].bids, 1, "Bidders number doesn't match") + assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") +} + +func TestCategoryMappingBidderNameNoCategories(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + bidRequest := &openrtb.BidRequest{} + requestExt := newExtRequestNoBrandCat() + requestExt.Prebid.Targeting.AppendBidderNames = true + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30} + + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + cats1 := []string{"IAB1-1"} + cats2 := []string{"IAB1-2"} + bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1} + + bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + + innerBids1 := []*pbsOrtbBid{ + &bid1_1, + } + innerBids2 := []*pbsOrtbBid{ + &bid1_2, + } + + seatBid1 := pbsOrtbSeatBid{innerBids1, "USD", nil, nil} + bidderName1 := openrtb_ext.BidderName("bidder1") + + seatBid2 := pbsOrtbSeatBid{innerBids2, "USD", nil, nil} + bidderName2 := openrtb_ext.BidderName("bidder2") + + adapterBids[bidderName1] = &seatBid1 + adapterBids[bidderName2] = &seatBid2 + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &requestExt, adapterBids, categoriesFetcher, targData) + + assert.NoError(t, err, "Category mapping error should be empty") + assert.Empty(t, rejections, "There should be 0 bid rejection messages") + assert.Equal(t, "10.00_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match") + assert.Equal(t, "12.00_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match") + assert.Len(t, adapterBids[bidderName1].bids, 1, "Bidders number doesn't match") + assert.Len(t, adapterBids[bidderName2].bids, 1, "Bidders number doesn't match") + assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match") } func TestBidRejectionErrors(t *testing.T) { @@ -1347,7 +2142,7 @@ func TestBidRejectionErrors(t *testing.T) { adapterBids[bidderName] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, test.reqExt, adapterBids, categoriesFetcher, targData) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &test.reqExt, adapterBids, categoriesFetcher, targData) if len(test.expectedCatDur) > 0 { // Bid deduplication case @@ -1364,6 +2159,79 @@ func TestBidRejectionErrors(t *testing.T) { } } +func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + bidRequest := &openrtb.BidRequest{} + requestExt := newExtRequestTranslateCategories(nil) + + targData := &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: true, + } + + requestExt.Prebid.Targeting.DurationRangeSec = []int{30} + requestExt.Prebid.Targeting.IncludeBrandCategory.WithCategory = false + + cats1 := []string{"IAB1-3"} + cats2 := []string{"IAB1-4"} + + bidApn1 := openrtb.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1} + bidApn2 := openrtb.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1} + + bid1_Apn1 := pbsOrtbBid{&bidApn1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + bid1_Apn2 := pbsOrtbBid{&bidApn2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0, false} + + innerBidsApn1 := []*pbsOrtbBid{ + &bid1_Apn1, + } + + innerBidsApn2 := []*pbsOrtbBid{ + &bid1_Apn2, + } + + for i := 1; i < 10; i++ { + adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid) + + seatBidApn1 := pbsOrtbSeatBid{innerBidsApn1, "USD", nil, nil} + bidderNameApn1 := openrtb_ext.BidderName("appnexus1") + + seatBidApn2 := pbsOrtbSeatBid{innerBidsApn2, "USD", nil, nil} + bidderNameApn2 := openrtb_ext.BidderName("appnexus2") + + adapterBids[bidderNameApn1] = &seatBidApn1 + adapterBids[bidderNameApn2] = &seatBidApn2 + + bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, bidRequest, &requestExt, adapterBids, categoriesFetcher, targData) + + assert.NoError(t, err, "Category mapping error should be empty") + assert.Len(t, rejections, 1, "There should be 1 bid rejection message") + assert.Regexpf(t, regexp.MustCompile(`bid rejected \[bid ID: bid_idApn(1|2)\] reason: Bid was deduplicated`), rejections[0], "Rejection message did not match expected") + assert.Len(t, bidCategory, 1, "Bidders category mapping should have only one element") + + var resultBid string + for bidId := range bidCategory { + resultBid = bidId + } + + if resultBid == "bid_idApn1" { + assert.Nil(t, seatBidApn2.bids, "Appnexus_2 seat bid should not have any bids back") + assert.Len(t, seatBidApn1.bids, 1, "Appnexus_1 seat bid should have only one back") + + } else { + assert.Nil(t, seatBidApn1.bids, "Appnexus_1 seat bid should not have any bids back") + assert.Len(t, seatBidApn2.bids, 1, "Appnexus_2 seat bid should have only one back") + + } + + } + +} + func TestUpdateRejections(t *testing.T) { rejections := []string{} @@ -1377,12 +2245,13 @@ func TestUpdateRejections(t *testing.T) { func TestApplyDealSupport(t *testing.T) { testCases := []struct { - description string - dealPriority int - impExt json.RawMessage - targ map[string]string - expectedHbPbCatDur string - expectedDealErr string + description string + dealPriority int + impExt json.RawMessage + targ map[string]string + expectedHbPbCatDur string + expectedDealErr string + expectedDealTierSatisfied bool }{ { description: "hb_pb_cat_dur should be modified", @@ -1391,8 +2260,9 @@ func TestApplyDealSupport(t *testing.T) { targ: map[string]string{ "hb_pb_cat_dur": "12.00_movies_30s", }, - expectedHbPbCatDur: "tier5_movies_30s", - expectedDealErr: "", + expectedHbPbCatDur: "tier5_movies_30s", + expectedDealErr: "", + expectedDealTierSatisfied: true, }, { description: "hb_pb_cat_dur should not be modified due to priority not exceeding min", @@ -1401,8 +2271,9 @@ func TestApplyDealSupport(t *testing.T) { targ: map[string]string{ "hb_pb_cat_dur": "12.00_medicine_30s", }, - expectedHbPbCatDur: "12.00_medicine_30s", - expectedDealErr: "", + expectedHbPbCatDur: "12.00_medicine_30s", + expectedDealErr: "", + expectedDealTierSatisfied: false, }, { description: "hb_pb_cat_dur should not be modified due to invalid config", @@ -1411,8 +2282,9 @@ func TestApplyDealSupport(t *testing.T) { 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'", + expectedHbPbCatDur: "12.00_games_30s", + expectedDealErr: "dealTier configuration invalid for bidder 'appnexus', imp ID 'imp_id1'", + expectedDealTierSatisfied: false, }, { description: "hb_pb_cat_dur should not be modified due to deal priority of 0", @@ -1421,8 +2293,9 @@ func TestApplyDealSupport(t *testing.T) { targ: map[string]string{ "hb_pb_cat_dur": "12.00_auto_30s", }, - expectedHbPbCatDur: "12.00_auto_30s", - expectedDealErr: "", + expectedHbPbCatDur: "12.00_auto_30s", + expectedDealErr: "", + expectedDealTierSatisfied: false, }, } @@ -1454,6 +2327,7 @@ func TestApplyDealSupport(t *testing.T) { dealErrs := applyDealSupport(bidRequest, auc, bidCategory) assert.Equal(t, test.expectedHbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][bidderName].bid.ID], test.description) + assert.Equal(t, test.expectedDealTierSatisfied, auc.winningBidsByBidder["imp_id1"][bidderName].dealTierSatisfied, "expectedDealTierSatisfied=%v when %v", test.expectedDealTierSatisfied, test.description) if len(test.expectedDealErr) > 0 { assert.Containsf(t, dealErrs, errors.New(test.expectedDealErr), "Expected error message not found in deal errors") } @@ -1462,129 +2336,102 @@ func TestApplyDealSupport(t *testing.T) { func TestGetDealTiers(t *testing.T) { testCases := []struct { - impExt json.RawMessage - bidderResult map[string]bool // true indicates bidder had valid config, false indicates invalid + description string + request openrtb.BidRequest + expected map[string]openrtb_ext.DealTierBidderMap }{ { - impExt: json.RawMessage(`{"validbase": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), - bidderResult: map[string]bool{ - "validbase": true, + description: "None", + request: openrtb.BidRequest{ + Imp: []openrtb.Imp{}, }, + expected: map[string]openrtb_ext.DealTierBidderMap{}, }, { - 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, + description: "One", + request: openrtb.BidRequest{ + Imp: []openrtb.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}}}`)}, + }, + }, + expected: map[string]openrtb_ext.DealTierBidderMap{ + "imp1": {openrtb_ext.BidderAppnexus: {Prefix: "tier", MinDealTier: 5}}, }, }, { - impExt: json.RawMessage(`{"nodealtier": {"placementId": 10433394}}`), - bidderResult: map[string]bool{ - "nodealtier": false, + description: "Many", + request: openrtb.BidRequest{ + Imp: []openrtb.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}`)}, + {ID: "imp2", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 8, "prefix": "tier2"}}}`)}, + }, + }, + expected: map[string]openrtb_ext.DealTierBidderMap{ + "imp1": {openrtb_ext.BidderAppnexus: {Prefix: "tier1", MinDealTier: 5}}, + "imp2": {openrtb_ext.BidderAppnexus: {Prefix: "tier2", MinDealTier: 8}}, }, }, { - impExt: json.RawMessage(`{"validbase": {"placementId": 10433394}, "onedealTier2": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), - bidderResult: map[string]bool{ - "onedealTier2": true, - "validbase": false, + description: "Many - Skips Malformed", + request: openrtb.BidRequest{ + Imp: []openrtb.Imp{ + {ID: "imp1", Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier1"}}}`)}, + {ID: "imp2", Ext: json.RawMessage(`{"appnexus": {"dealTier": "wrong type"}}`)}, + }, + }, + expected: map[string]openrtb_ext.DealTierBidderMap{ + "imp1": {openrtb_ext.BidderAppnexus: {Prefix: "tier1", MinDealTier: 5}}, }, }, } - 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") - } - } + result := getDealTiers(&test.request) + assert.Equal(t, test.expected, result, test.description) } } -func TestValidateAndNormalizeDealTier(t *testing.T) { +func TestValidateDealTier(t *testing.T) { testCases := []struct { description string - params json.RawMessage + dealTier openrtb_ext.DealTier expectedResult bool }{ { - description: "BidderDealTier should be valid", - params: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`), + description: "Valid", + dealTier: openrtb_ext.DealTier{Prefix: "prefix", MinDealTier: 5}, 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}}`), + description: "Invalid - Empty", + dealTier: openrtb_ext.DealTier{}, expectedResult: false, }, { - description: "BidderDealTier should be invalid due to missing dealTier", - params: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), + description: "Invalid - Empty Prefix", + dealTier: openrtb_ext.DealTier{MinDealTier: 5}, expectedResult: false, }, { - description: "BidderDealTier should be invalid due to prefix containing all whitespace", - params: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": " "}, "placementId": 10433394}}`), + description: "Invalid - Empty Deal Tier", + dealTier: openrtb_ext.DealTier{Prefix: "prefix"}, 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) + assert.Equal(t, test.expectedResult, validateDealTier(test.dealTier), test.description) } } func TestUpdateHbPbCatDur(t *testing.T) { testCases := []struct { - description string - targ map[string]string - dealTier *DealTierInfo - dealPriority int - expectedHbPbCatDur string + description string + targ map[string]string + dealTier openrtb_ext.DealTier + dealPriority int + expectedHbPbCatDur string + expectedDealTierSatisfied bool }{ { description: "hb_pb_cat_dur should be updated with prefix and tier", @@ -1592,12 +2439,13 @@ func TestUpdateHbPbCatDur(t *testing.T) { "hb_pb": "12.00", "hb_pb_cat_dur": "12.00_movies_30s", }, - dealTier: &DealTierInfo{ + dealTier: openrtb_ext.DealTier{ Prefix: "tier", MinDealTier: 5, }, - dealPriority: 5, - expectedHbPbCatDur: "tier5_movies_30s", + dealPriority: 5, + expectedHbPbCatDur: "tier5_movies_30s", + expectedDealTierSatisfied: true, }, { description: "hb_pb_cat_dur should not be updated due to bid priority", @@ -1605,12 +2453,13 @@ func TestUpdateHbPbCatDur(t *testing.T) { "hb_pb": "12.00", "hb_pb_cat_dur": "12.00_auto_30s", }, - dealTier: &DealTierInfo{ + dealTier: openrtb_ext.DealTier{ Prefix: "tier", MinDealTier: 10, }, - dealPriority: 6, - expectedHbPbCatDur: "12.00_auto_30s", + dealPriority: 6, + expectedHbPbCatDur: "12.00_auto_30s", + expectedDealTierSatisfied: false, }, { description: "hb_pb_cat_dur should be updated with prefix and tier", @@ -1618,12 +2467,13 @@ func TestUpdateHbPbCatDur(t *testing.T) { "hb_pb": "12.00", "hb_pb_cat_dur": "12.00_medicine_30s", }, - dealTier: &DealTierInfo{ + dealTier: openrtb_ext.DealTier{ Prefix: "tier", MinDealTier: 1, }, - dealPriority: 7, - expectedHbPbCatDur: "tier7_medicine_30s", + dealPriority: 7, + expectedHbPbCatDur: "tier7_medicine_30s", + expectedDealTierSatisfied: true, }, } @@ -1636,6 +2486,7 @@ func TestUpdateHbPbCatDur(t *testing.T) { updateHbPbCatDur(&bid, test.dealTier, bidCategory) assert.Equal(t, test.expectedHbPbCatDur, bidCategory[bid.bid.ID], test.description) + assert.Equal(t, test.expectedDealTierSatisfied, bid.dealTierSatisfied, test.description) } } @@ -1684,12 +2535,14 @@ func TestRecordAdaptorDuplicateBidIDs(t *testing.T) { } 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"` + GDPREnabled bool `json:"gdpr_enabled"` + IncomingRequest exchangeRequest `json:"incomingRequest"` + OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"` + Response exchangeResponse `json:"response,omitempty"` + EnforceCCPA bool `json:"enforceCcpa"` + EnforceLMT bool `json:"enforceLmt"` + AssumeGDPRApplies bool `json:"assume_gdpr_applies"` + DebugLog *DebugLog `json:"debuglog,omitempty"` } type exchangeRequest struct { @@ -1740,6 +2593,10 @@ func (f mockIdFetcher) GetId(bidder openrtb_ext.BidderName) (id string, ok bool) return } +func (f mockIdFetcher) LiveSyncCount() int { + return len(f) +} + type validatingBidder struct { t *testing.T fileName string @@ -1750,7 +2607,7 @@ type validatingBidder struct { mockResponses map[string]bidderResponse } -func (b *validatingBidder) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo, debug bool) (seatBid *pbsOrtbSeatBid, errs []error) { +func (b *validatingBidder) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (seatBid *pbsOrtbSeatBid, errs []error) { if expectedRequest, ok := b.expectations[string(name)]; ok { if expectedRequest != nil { if expectedRequest.BidAdjustment != bidAdjustment { @@ -1881,13 +2738,22 @@ func mockHandler(statusCode int, getBody string, postBody string) http.Handler { }) } +func mockSlowHandler(delay time.Duration, statusCode int, body string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(delay) + + w.WriteHeader(statusCode) + w.Write([]byte(body)) + }) +} + type wellBehavedCache struct{} -func (c *wellBehavedCache) GetExtCacheData() (string, string) { - return "www.pbcserver.com", "/pbcache/endpoint" +func (c *wellBehavedCache) GetExtCacheData() (scheme string, host string, path string) { + return "https", "www.pbcserver.com", "/pbcache/endpoint" } -func (c *wellBehavedCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) { +func (c *wellBehavedCache) PutJson(ctx context.Context, values []pbc.Cacheable) ([]string, []error) { ids := make([]string, len(values)) for i := 0; i < len(values); i++ { ids[i] = strconv.Itoa(i) @@ -1901,6 +2767,10 @@ func (e *emptyUsersync) GetId(bidder openrtb_ext.BidderName) (string, bool) { return "", false } +func (e *emptyUsersync) LiveSyncCount() int { + return 0 +} + type mockUsersync struct { syncs map[string]string } @@ -1910,8 +2780,18 @@ func (e *mockUsersync) GetId(bidder openrtb_ext.BidderName) (id string, exists b return } +func (e *mockUsersync) LiveSyncCount() int { + return len(e.syncs) +} + type panicingAdapter struct{} -func (panicingAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo, debug bool) (posb *pbsOrtbSeatBid, errs []error) { +func (panicingAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (posb *pbsOrtbSeatBid, errs []error) { panic("Panic! Panic! The world is ending!") } + +type nilCategoryFetcher struct{} + +func (nilCategoryFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { + return "", nil +} diff --git a/exchange/exchangetest/append-bidder-names.json b/exchange/exchangetest/append-bidder-names.json new file mode 100644 index 00000000000..1247b9f0261 --- /dev/null +++ b/exchange/exchangetest/append-bidder-names.json @@ -0,0 +1,222 @@ +{ + "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": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "appendbiddernames": 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_appnexus", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", + "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_appnexus", + "hb_pb_cat_dur_appnex": "0.50_HomeDecor_0s_appnexus", + "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": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "appendbiddernames": true + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/ccpa-nosale-any-bidder.json b/exchange/exchangetest/ccpa-nosale-any-bidder.json new file mode 100644 index 00000000000..f7abd91f512 --- /dev/null +++ b/exchange/exchangetest/ccpa-nosale-any-bidder.json @@ -0,0 +1,75 @@ +{ + "enforceCcpa": 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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["*"] + } + }, + "user": { + "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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["*"] + } + }, + "user": { + "buyeruid": "some-buyer-id" + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/ccpa-nosale-specific-bidder.json b/exchange/exchangetest/ccpa-nosale-specific-bidder.json new file mode 100644 index 00000000000..b89e29aea01 --- /dev/null +++ b/exchange/exchangetest/ccpa-nosale-specific-bidder.json @@ -0,0 +1,75 @@ +{ + "enforceCcpa": 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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["appnexus"] + } + }, + "user": { + "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 + } + } + }], + "regs": { + "ext": { + "us_privacy": "1-Y-" + } + }, + "ext": { + "prebid": { + "nosale": ["appnexus"] + } + }, + "user": { + "buyeruid": "some-buyer-id" + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/debuglog_disabled.json b/exchange/exchangetest/debuglog_disabled.json index 9902dea4bbc..0497e7e2b07 100644 --- a/exchange/exchangetest/debuglog_disabled.json +++ b/exchange/exchangetest/debuglog_disabled.json @@ -46,7 +46,6 @@ "test": 1, "ext": { "prebid": { - "debug" :1, "targeting": { "includebrandcategory": { "primaryadserver": 1, @@ -215,7 +214,6 @@ "test": 1, "ext": { "prebid": { - "debug": 1, "targeting": { "includebrandcategory": { "primaryadserver": 1, @@ -229,4 +227,4 @@ } } } -} \ No newline at end of file +} diff --git a/exchange/exchangetest/debuglog_enabled.json b/exchange/exchangetest/debuglog_enabled.json index 3b307b67e55..885b8b544b3 100644 --- a/exchange/exchangetest/debuglog_enabled.json +++ b/exchange/exchangetest/debuglog_enabled.json @@ -46,7 +46,6 @@ "test": 1, "ext": { "prebid": { - "debug":1, "targeting": { "includebrandcategory": { "primaryadserver": 1, @@ -215,7 +214,6 @@ "test": 1, "ext": { "prebid": { - "debug":1, "targeting": { "includebrandcategory": { "primaryadserver": 1, @@ -229,4 +227,4 @@ } } } -} \ No newline at end of file +} diff --git a/exchange/exchangetest/debuglog_enabled_no_bids.json b/exchange/exchangetest/debuglog_enabled_no_bids.json new file mode 100644 index 00000000000..4823acf8f16 --- /dev/null +++ b/exchange/exchangetest/debuglog_enabled_no_bids.json @@ -0,0 +1,72 @@ +{ + "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 + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + } + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBid": {} + } + } + }, + "response": { + "bids": {} + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json new file mode 100644 index 00000000000..8004c3c2646 --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json @@ -0,0 +1,173 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }, { + "seat": "rubicon", + "bid": [{ + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json new file mode 100644 index 00000000000..d62afccf426 --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json @@ -0,0 +1,179 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "rubicon": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + } + } + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "prebid": {}, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + }, + "rubicon": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "accountId": 1, + "siteId": 2, + "zoneId": 3 + }, + "prebid": {}, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }, { + "seat": "rubicon", + "bid": [{ + "id": "rubi-bid", + "impid": "some-imp-id", + "price": 0.4, + "w": 200, + "h": 500, + "crid": "creative-2", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json new file mode 100644 index 00000000000..6f0bab9529c --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json @@ -0,0 +1,103 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "appnexus": { + "placementId": 1 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json new file mode 100644 index 00000000000..1610b9ea47e --- /dev/null +++ b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json @@ -0,0 +1,108 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + }, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "some-imp-id", + "banner": { + "format": [{ + "w": 600, + "h": 500 + }, { + "w": 300, + "h": 600 + }] + }, + "ext": { + "bidder": { + "placementId": 1 + }, + "prebid": {}, + "context": { + "data": { + "keywords": "prebid server example" + } + } + } + }] + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "pbsSeatBid": { + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1" + }, + "bidType": "banner" + }] + } + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "some-imp-id", + "price": 0.3, + "w": 200, + "h": 500, + "crid": "creative-1", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-off-device.json b/exchange/exchangetest/gdpr-geo-eu-off-device.json new file mode 100644 index 00000000000..f704cdd5c8e --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-off-device.json @@ -0,0 +1,65 @@ +{ + "assume_gdpr_applies": false, + "gdpr_enabled": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id" + }, + "device": { + "geo": { + "country": "FRA" + } + } +} + }, + "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 + } + } + }], + "user": { + }, + "device": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-off.json b/exchange/exchangetest/gdpr-geo-eu-off.json new file mode 100644 index 00000000000..24357eb7eec --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-off.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": false, + "gdpr_enabled": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "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 + } + } + }], + "user": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-on-featureflag-off.json b/exchange/exchangetest/gdpr-geo-eu-on-featureflag-off.json new file mode 100644 index 00000000000..6c6ca3edc62 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-on-featureflag-off.json @@ -0,0 +1,62 @@ +{ + "assume_gdpr_applies": true, + "gdpr_enabled": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-eu-on.json b/exchange/exchangetest/gdpr-geo-eu-on.json new file mode 100644 index 00000000000..eb42a17c936 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-eu-on.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": true, + "gdpr_enabled": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "FRA" + } + } + } + }, + "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 + } + } + }], + "user": { + "geo": { + "country": "FRA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-usa-off.json b/exchange/exchangetest/gdpr-geo-usa-off.json new file mode 100644 index 00000000000..d56c9318a56 --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-usa-off.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + } + }, + "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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + }, + "bidAdjustment": 1.0 + }, + "mockResponse": { + "errors": ["appnexus-error"] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/gdpr-geo-usa-on.json b/exchange/exchangetest/gdpr-geo-usa-on.json new file mode 100644 index 00000000000..f922be9ea4e --- /dev/null +++ b/exchange/exchangetest/gdpr-geo-usa-on.json @@ -0,0 +1,61 @@ +{ + "assume_gdpr_applies": 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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + } + }, + "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 + } + } + }], + "user": { + "buyeruid": "some-buyer-id", + "geo": { + "country": "USA" + } + } + }, + "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 index db16dbe6013..ec174f75b36 100644 --- a/exchange/exchangetest/request-multi-bidders-debug-info.json +++ b/exchange/exchangetest/request-multi-bidders-debug-info.json @@ -42,7 +42,6 @@ ], "ext": { "prebid": { - "debug":1, "targeting": { "durationRangeSec": [ 15, @@ -204,9 +203,7 @@ }, "test": 1, "ext": { - "prebid": { - "debug": 1, "targeting": { "durationRangeSec": [ 15, diff --git a/exchange/exchangetest/targeting-cache-vast.json b/exchange/exchangetest/targeting-cache-vast.json index f348dd1b29d..53a48c4ec69 100644 --- a/exchange/exchangetest/targeting-cache-vast.json +++ b/exchange/exchangetest/targeting-cache-vast.json @@ -67,7 +67,7 @@ "cache": { "bids": { "cacheId": "0", - "url": "www.pbcserver.com/pbcache/endpoint?uuid=0" + "url": "https://www.pbcserver.com/pbcache/endpoint?uuid=0" }, "key": "", "url": "" diff --git a/exchange/exchangetest/targeting-cache-zero.json b/exchange/exchangetest/targeting-cache-zero.json index 5130153026a..0048ea10917 100644 --- a/exchange/exchangetest/targeting-cache-zero.json +++ b/exchange/exchangetest/targeting-cache-zero.json @@ -70,7 +70,7 @@ "cache": { "bids": { "cacheId": "0", - "url": "www.pbcserver.com/pbcache/endpoint?uuid=0" + "url": "https://www.pbcserver.com/pbcache/endpoint?uuid=0" }, "key": "", "url": "" diff --git a/exchange/impcustomcachekeytest/multiImpVast.json b/exchange/impcustomcachekeytest/multiImpVast.json index bf0b310b04c..db10697431a 100644 --- a/exchange/impcustomcachekeytest/multiImpVast.json +++ b/exchange/impcustomcachekeytest/multiImpVast.json @@ -64,29 +64,29 @@ "bidder": "rubicon" }], "expectedCacheables": [{ - "Type": "xml", - "TTLSeconds": 360, - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidoneproducts.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 360, + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidoneproducts.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" }, { - "Type": "xml", - "TTLSeconds": 260, - "Key": "34_news_44", - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 260, + "key": "34_news_44", + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" }, { - "Type": "xml", - "TTLSeconds": 360, - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 360, + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" }, { - "Type": "xml", - "TTLSeconds": 3660, - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidseveryday.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 3660, + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidseveryday.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" }, { - "Type": "xml", - "TTLSeconds": 960, - "Key": "13_sports_22", - "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherbidder.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" + "type": "xml", + "ttlseconds": 960, + "key": "13_sports_22", + "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherbidder.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" } ], "defaultTTLs": { diff --git a/exchange/impcustomcachekeytest/multiImpVideo.json b/exchange/impcustomcachekeytest/multiImpVideo.json new file mode 100644 index 00000000000..fc34388875c --- /dev/null +++ b/exchange/impcustomcachekeytest/multiImpVideo.json @@ -0,0 +1,101 @@ +{ + "bidRequest": { + "imp": [{ + "id": "1_0" + }, + { + "id": "1_1" + } + ] + }, + "pbsBids": [{ + "bid": { + "id": "apn_1_0", + "impid": "1_0", + "price": 12.00, + "nurl": "http://apn_1_0.com", + "cat": ["12.00_sports_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_0", + "impid": "1_0", + "price": 20.00, + "nurl": "http://spotx_1_0.com", + "cat": ["20_news_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "apn_1_1", + "impid": "1_1", + "price": 18.00, + "nurl": "http://apn_1_1.com", + "cat": ["18_furniture_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_1", + "impid": "1_1", + "price": 17.00, + "nurl": "http://spotx_1_1.com", + "cat": ["17_auto_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "rubicon_1_1", + "impid": "1_1", + "price": 17.50, + "nurl": "http://rubicon_1_1.com", + "cat": ["17_music_30s"] + }, + "bidType": "video", + "bidder": "rubicon" + }], + "expectedCacheables": [{ + "type": "xml", + "ttlseconds": 3660, + "value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "key": "12.00_sports_30s" + }, { + "type": "xml", + "ttlseconds": 3660, + "value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "key": "20_news_30s" + }, { + "type": "xml", + "ttlseconds": 3660, + "value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "key": "18_furniture_30s" + }, + { + "type": "xml", + "ttlseconds": 3660, + "value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "key": "17_auto_30s" + }, + { + "type": "xml", + "ttlseconds": 3660, + "value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://rubicon_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "key": "17_music_30s" + } + ], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners": false, + "targetDataIncludeBidderKeys": true, + "targetDataIncludeCacheBids": false, + "targetDataIncludeCacheVast": true +} diff --git a/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json b/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json new file mode 100644 index 00000000000..4dd15344729 --- /dev/null +++ b/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json @@ -0,0 +1,86 @@ +{ + "bidRequest": { + "imp": [{ + "id": "1_0" + }, + { + "id": "1_1" + } + ] + }, + "pbsBids": [{ + "bid": { + "id": "apn_1_0", + "impid": "1_0", + "price": 12.00, + "nurl": "http://apn_1_0.com", + "cat": ["12.00_sports_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_0", + "impid": "1_0", + "price": 20.00, + "nurl": "http://spotx_1_0.com", + "cat": ["20_news_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "apn_1_1", + "impid": "1_1", + "price": 18.00, + "nurl": "http://apn_1_1.com", + "cat": ["18_furniture_30s"] + }, + "bidType": "video", + "bidder": "appnexus" + }, { + "bid": { + "id": "spotx_1_1", + "impid": "1_1", + "price": 17.00, + "nurl": "http://spotx_1_1.com", + "cat": ["17_auto_30s"] + }, + "bidType": "video", + "bidder": "spotx" + }, { + "bid": { + "id": "rubicon_1_1", + "impid": "1_1", + "price": 17.50, + "nurl": "http://rubicon_1_1.com", + "cat": ["17_music_30s"] + }, + "bidType": "video", + "bidder": "rubicon" + }], + "expectedCacheables": [ + { + "type": "xml", + "ttlseconds": 3660, + "value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "key": "20_news_30s" + }, + { + "type": "xml", + "ttlseconds": 3660, + "value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e", + "key": "18_furniture_30s" + } + ], + "defaultTTLs": { + "banner": 300, + "video": 3600, + "audio": 1800, + "native": 300 + }, + "targetDataIncludeWinners": true, + "targetDataIncludeBidderKeys": false, + "targetDataIncludeCacheBids": false, + "targetDataIncludeCacheVast": true +} diff --git a/exchange/legacy.go b/exchange/legacy.go index 619cafbd3b9..43aa3d73b9b 100644 --- a/exchange/legacy.go +++ b/exchange/legacy.go @@ -34,7 +34,7 @@ type adaptedAdapter struct { // // This is not ideal. OpenRTB provides a superset of the legacy data structures. // For requests which use those features, the best we can do is respond with "no bid". -func (bidder *adaptedAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo, debug bool) (*pbsOrtbSeatBid, []error) { +func (bidder *adaptedAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) { legacyRequest, legacyBidder, errs := bidder.toLegacyAdapterInputs(request, name) if legacyRequest == nil || legacyBidder == nil { return nil, errs @@ -98,7 +98,7 @@ func (bidder *adaptedAdapter) toLegacyRequest(req *openrtb.BidRequest) (*pbs.PBS } } - if requestExt.Prebid.Debug == 1 { + if requestExt.Prebid.Debug { isDebug = true } diff --git a/exchange/legacy_test.go b/exchange/legacy_test.go index 3c2d1c06ee0..62553ff8a2e 100644 --- a/exchange/legacy_test.go +++ b/exchange/legacy_test.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "errors" + "net/http" "reflect" "testing" + "time" "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters" @@ -58,8 +60,8 @@ func TestSiteVideo(t *testing.T) { mockAdapter := mockLegacyAdapter{} exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() - _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) > 0 { t.Errorf("Unexpected error requesting bids: %v", errs) } @@ -92,8 +94,8 @@ func TestAppBanner(t *testing.T) { mockAdapter := mockLegacyAdapter{} exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() - _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) > 0 { t.Errorf("Unexpected error requesting bids: %v", errs) } @@ -138,8 +140,8 @@ func TestBidTransforms(t *testing.T) { } exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() - seatBid, errs := exchangeBidder.requestBid(context.Background(), newAppOrtbRequest(), openrtb_ext.BidderRubicon, bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + seatBid, errs := exchangeBidder.requestBid(context.Background(), newAppOrtbRequest(), openrtb_ext.BidderRubicon, bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) != 1 { t.Fatalf("Bad error count. Expected 1, got %d", len(errs)) } @@ -287,8 +289,8 @@ func TestErrorResponse(t *testing.T) { } exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() - _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + _, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderRubicon, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) != 1 { t.Fatalf("Bad error count. Expected 1, got %d", len(errs)) } @@ -326,8 +328,8 @@ func TestWithTargeting(t *testing.T) { }}, } exchangeBidder := adaptLegacyAdapter(&mockAdapter) - currencyConverter := currencies.NewRateConverterDefault() - bid, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderFacebook, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, false) + currencyConverter := currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)) + bid, errs := exchangeBidder.requestBid(context.Background(), ortbRequest, openrtb_ext.BidderFacebook, 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}) if len(errs) != 0 { t.Fatalf("This should not produce errors. Got %v", errs) } diff --git a/exchange/price_granularity.go b/exchange/price_granularity.go index ad31f0ae344..ffcce061465 100644 --- a/exchange/price_granularity.go +++ b/exchange/price_granularity.go @@ -7,33 +7,32 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" ) -// DEFAULT_PRECISION should be taken care of in openrtb_ext/request.go, but throwing an additional safety check here. - -// GetCpmStringValue is the externally facing function for computing CPM buckets -func GetCpmStringValue(cpm float64, config openrtb_ext.PriceGranularity) (string, error) { +// GetPriceBucket is the externally facing function for computing CPM buckets +func GetPriceBucket(cpm float64, config openrtb_ext.PriceGranularity) string { cpmStr := "" bucketMax := 0.0 increment := 0.0 precision := config.Precision - // calculate max of highest bucket + for i := 0; i < len(config.Ranges); i++ { if config.Ranges[i].Max > bucketMax { bucketMax = config.Ranges[i].Max } - } // calculate which bucket cpm is in - if cpm > bucketMax { - // If we are over max, just return that - return strconv.FormatFloat(bucketMax, 'f', precision, 64), nil - } - for i := 0; i < len(config.Ranges); i++ { + // find what range cpm is in if cpm >= config.Ranges[i].Min && cpm <= config.Ranges[i].Max { increment = config.Ranges[i].Increment } } - if increment > 0 { + + if cpm > bucketMax { + // We are over max, just return that + cpmStr = strconv.FormatFloat(bucketMax, 'f', precision, 64) + } else if increment > 0 { + // If increment exists, get cpm string value cpmStr = getCpmTarget(cpm, increment, precision) } - return cpmStr, nil + + return cpmStr } func getCpmTarget(cpm float64, increment float64, precision int) string { diff --git a/exchange/price_granularity_test.go b/exchange/price_granularity_test.go index 9c3aa1411d9..6dccc677b7b 100644 --- a/exchange/price_granularity_test.go +++ b/exchange/price_granularity_test.go @@ -1,9 +1,11 @@ package exchange import ( + "math" "testing" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" ) func TestGetPriceBucketString(t *testing.T) { @@ -28,32 +30,105 @@ func TestGetPriceBucketString(t *testing.T) { }, } - price := 1.87 - getOnePriceBucket(t, "low", low, price, "1.50") - getOnePriceBucket(t, "medium", medium, price, "1.80") - getOnePriceBucket(t, "high", high, price, "1.87") - getOnePriceBucket(t, "auto", auto, price, "1.85") - getOnePriceBucket(t, "dense", dense, price, "1.87") - getOnePriceBucket(t, "custom1", custom1, price, "1.86") - - // test a cpm above the max in low price bucket - price = 5.72 - getOnePriceBucket(t, "low", low, price, "5.00") - getOnePriceBucket(t, "medium", medium, price, "5.70") - getOnePriceBucket(t, "high", high, price, "5.72") - getOnePriceBucket(t, "auto", auto, price, "5.70") - getOnePriceBucket(t, "dense", dense, price, "5.70") - getOnePriceBucket(t, "custom1", custom1, price, "5.70") + // Define test cases + type aTest struct { + granularityId string + granularity openrtb_ext.PriceGranularity + expectedPriceBucket string + } + testGroups := []struct { + groupDesc string + cpm float64 + testCases []aTest + }{ + { + groupDesc: "cpm below the max in every price bucket", + cpm: 1.87, + testCases: []aTest{ + {"low", low, "1.50"}, + {"medium", medium, "1.80"}, + {"high", high, "1.87"}, + {"auto", auto, "1.85"}, + {"dense", dense, "1.87"}, + {"custom1", custom1, "1.86"}, + }, + }, + { + groupDesc: "cpm above the max in low price bucket", + cpm: 5.72, + testCases: []aTest{ + {"low", low, "5.00"}, + {"medium", medium, "5.70"}, + {"high", high, "5.72"}, + {"auto", auto, "5.70"}, + {"dense", dense, "5.70"}, + {"custom1", custom1, "5.70"}, + }, + }, + { + groupDesc: "Precision value corner cases", + cpm: 1.876, + testCases: []aTest{ + { + "Negative precision defaults to number of digits already in CPM float", + openrtb_ext.PriceGranularity{Precision: -1, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}, + "1.85", + }, + { + "Precision value equals zero, we expect to round up to the nearest integer", + openrtb_ext.PriceGranularity{Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}, + "2", + }, + { + "Largest precision value PBS supports 15", + openrtb_ext.PriceGranularity{Precision: 15, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}}, + "1.850000000000000", + }, + }, + }, + { + groupDesc: "Increment value corner cases", + cpm: 1.876, + testCases: []aTest{ + { + "Negative increment, return empty string", + openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: -0.05}}}, + "", + }, + { + "Zero increment, return empty string", + openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5}}}, + "", + }, + { + "Increment value is greater than CPM itself, return zero float value", + openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 1.877}}}, + "0.00", + }, + }, + }, + { + groupDesc: "Negative Cpm, return empty string since it does not belong into any range", + cpm: -1.876, + testCases: []aTest{{"low", low, ""}}, + }, + { + groupDesc: "Zero value Cpm, return the same, only in string format", + cpm: 0, + testCases: []aTest{{"low", low, "0.00"}}, + }, + { + groupDesc: "Large Cpm, return bucket Max", + cpm: math.MaxFloat64, + testCases: []aTest{{"low", low, "5.00"}}, + }, + } -} + for _, testGroup := range testGroups { + for _, test := range testGroup.testCases { + priceBucket := GetPriceBucket(testGroup.cpm, test.granularity) -func getOnePriceBucket(t *testing.T, name string, granularity openrtb_ext.PriceGranularity, price float64, expected string) { - t.Helper() - priceBucket, err := GetCpmStringValue(price, granularity) - if err != nil { - t.Errorf("Granularity: %s :: GetPriceBucketString: %s", name, err.Error()) - } - if priceBucket != expected { - t.Errorf("Granularity: %s :: Expected %s, got %s from %f", name, expected, priceBucket, price) + assert.Equal(t, test.expectedPriceBucket, priceBucket, "Group: %s Granularity: %s :: Expected %s, got %s from %f", testGroup.groupDesc, test.granularityId, test.expectedPriceBucket, priceBucket, testGroup.cpm) + } } } diff --git a/exchange/targeting.go b/exchange/targeting.go index 1bb6a7641ad..31db7114f67 100644 --- a/exchange/targeting.go +++ b/exchange/targeting.go @@ -7,7 +7,7 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" ) -const maxKeyLength = 20 +const MaxKeyLength = 20 // targetData tracks information about the winning Bid in each Imp. // @@ -22,6 +22,8 @@ type targetData struct { includeBidderKeys bool includeCacheBids bool includeCacheVast bool + includeFormat bool + preferDeals bool // cacheHost and cachePath exist to supply cache host and path as targeting parameters cacheHost string cachePath string @@ -53,6 +55,9 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi if vastID, ok := auc.vastCacheIds[topBidPerBidder.bid]; ok { targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner) } + if targData.includeFormat { + targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBidPerBidder.bidType), bidderName, isOverallWinner) + } if targData.cacheHost != "" { targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, bidderName, isOverallWinner) @@ -79,7 +84,7 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool) { if targData.includeBidderKeys { - keys[key.BidderKey(bidderName, maxKeyLength)] = value + keys[key.BidderKey(bidderName, MaxKeyLength)] = value } if targData.includeWinners && overallWinner { keys[string(key)] = value diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index 59b92332100..c152f8ea3d6 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -13,7 +13,6 @@ import ( "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" @@ -50,13 +49,13 @@ func TestTargetingCache(t *testing.T) { // Make sure that the cache keys exist on the bids where they're expected to assertKeyExists(t, bids["winning-bid"], string(openrtb_ext.HbCacheKey), true) - assertKeyExists(t, bids["winning-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, maxKeyLength), true) + assertKeyExists(t, bids["winning-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, MaxKeyLength), true) assertKeyExists(t, bids["contending-bid"], string(openrtb_ext.HbCacheKey), false) - assertKeyExists(t, bids["contending-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderRubicon, maxKeyLength), true) + assertKeyExists(t, bids["contending-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderRubicon, MaxKeyLength), true) assertKeyExists(t, bids["losing-bid"], string(openrtb_ext.HbCacheKey), false) - assertKeyExists(t, bids["losing-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, maxKeyLength), false) + assertKeyExists(t, bids["losing-bid"], openrtb_ext.HbCacheKey.BidderKey(openrtb_ext.BidderAppnexus, MaxKeyLength), false) //assert hb_cache_host was included assert.Contains(t, string(bids["winning-bid"].Ext), string(openrtb_ext.HbConstantCacheHostKey)) @@ -82,14 +81,20 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op server := httptest.NewServer(http.HandlerFunc(mockServer)) defer server.Close() + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + ex := &exchange{ adapterMap: buildAdapterMap(mockBids, server.URL, server.Client()), me: &metricsConf.DummyMetricsEngine{}, cache: &wellBehavedCache{}, cacheTime: time.Duration(0), gDPR: gdpr.AlwaysAllow{}, - currencyConverter: currencies.NewRateConverterDefault(), + currencyConverter: currencies.NewRateConverter(&http.Client{}, "", time.Duration(0)), UsersyncIfAmbiguous: false, + categoriesFetcher: categoriesFetcher, } imps := buildImps(t, mockBids) @@ -104,11 +109,13 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op req.Site = &openrtb.Site{} } - categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") - if error != nil { - t.Errorf("Failed to create a category Fetcher: %v", error) + auctionRequest := AuctionRequest{ + BidRequest: req, + Account: config.Account{}, + UserSyncs: &emptyUsersync{}, } - bidResp, err := ex.HoldAuction(context.Background(), req, &mockFetcher{}, pbsmetrics.Labels{}, &categoriesFetcher, nil) + + bidResp, err := ex.HoldAuction(context.Background(), auctionRequest, nil) if err != nil { t.Fatalf("Unexpected errors running auction: %v", err) @@ -134,7 +141,7 @@ func buildAdapterMap(bids map[openrtb_ext.BidderName][]*openrtb.Bid, mockServerU adapterMap[bidder] = adaptBidder(&mockTargetingBidder{ mockServerURL: mockServerURL, bids: bids, - }, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}) + }, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus) } return adapterMap } @@ -238,12 +245,215 @@ func (m *mockTargetingBidder) MakeBids(internalRequest *openrtb.BidRequest, exte return bidResponse, nil } -type mockFetcher struct{} +func mockServer(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("{}")) +} -func (f *mockFetcher) GetId(bidder openrtb_ext.BidderName) (string, bool) { - return "", false +type TargetingTestData struct { + Description string + TargetData targetData + Auction auction + IsApp bool + CategoryMapping map[string]string + ExpectedBidTargetsByBidder map[string]map[openrtb_ext.BidderName]map[string]string } -func mockServer(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("{}")) +var bid123 *openrtb.Bid = &openrtb.Bid{ + Price: 1.23, +} + +var bid111 *openrtb.Bid = &openrtb.Bid{ + Price: 1.11, + DealID: "mydeal", +} +var bid084 *openrtb.Bid = &openrtb.Bid{ + Price: 0.84, +} + +var TargetingTests []TargetingTestData = []TargetingTestData{ + { + Description: "Targeting winners only (most basic targeting example)", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeWinners: true, + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid084, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder": "appnexus", + "hb_pb": "1.20", + }, + openrtb_ext.BidderRubicon: {}, + }, + }, + }, + { + Description: "Targeting on bidders only", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeBidderKeys: true, + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid084, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder_appnexus": "appnexus", + "hb_pb_appnexus": "1.20", + }, + openrtb_ext.BidderRubicon: { + "hb_bidder_rubicon": "rubicon", + "hb_pb_rubicon": "0.80", + }, + }, + }, + }, + { + Description: "Full basic targeting with hd_format", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeWinners: true, + includeBidderKeys: true, + includeFormat: true, + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid084, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_pb": "1.20", + "hb_pb_appnexus": "1.20", + "hb_format": "banner", + "hb_format_appnexus": "banner", + }, + openrtb_ext.BidderRubicon: { + "hb_bidder_rubicon": "rubicon", + "hb_pb_rubicon": "0.80", + "hb_format_rubicon": "banner", + }, + }, + }, + }, + { + Description: "Cache and deal targeting test", + TargetData: targetData{ + priceGranularity: openrtb_ext.PriceGranularityFromString("med"), + includeBidderKeys: true, + cacheHost: "cache.prebid.com", + cachePath: "cache", + }, + Auction: auction{ + winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + bid: bid123, + bidType: openrtb_ext.BidTypeBanner, + }, + openrtb_ext.BidderRubicon: { + bid: bid111, + bidType: openrtb_ext.BidTypeBanner, + }, + }, + }, + cacheIds: map[*openrtb.Bid]string{ + bid123: "55555", + bid111: "cacheme", + }, + }, + ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: { + "hb_bidder_appnexus": "appnexus", + "hb_pb_appnexus": "1.20", + "hb_cache_id_appnexus": "55555", + "hb_cache_host_appnex": "cache.prebid.com", + "hb_cache_path_appnex": "cache", + }, + openrtb_ext.BidderRubicon: { + "hb_bidder_rubicon": "rubicon", + "hb_pb_rubicon": "1.10", + "hb_cache_id_rubicon": "cacheme", + "hb_deal_rubicon": "mydeal", + "hb_cache_host_rubico": "cache.prebid.com", + "hb_cache_path_rubico": "cache", + }, + }, + }, + }, +} + +func TestSetTargeting(t *testing.T) { + for _, test := range TargetingTests { + auc := &test.Auction + // Set rounded prices from the auction data + auc.setRoundedPrices(test.TargetData.priceGranularity) + winningBids := make(map[string]*pbsOrtbBid) + // Set winning bids from the auction data + for imp, bidsByBidder := range auc.winningBidsByBidder { + for _, bid := range bidsByBidder { + if winningBid, ok := winningBids[imp]; ok { + if winningBid.bid.Price < bid.bid.Price { + winningBids[imp] = bid + } + } else { + winningBids[imp] = bid + } + } + } + auc.winningBids = winningBids + targData := test.TargetData + targData.setTargeting(auc, test.IsApp, test.CategoryMapping) + for imp, targetsByBidder := range test.ExpectedBidTargetsByBidder { + for bidder, expected := range targetsByBidder { + assert.Equal(t, + expected, + auc.winningBidsByBidder[imp][bidder].bidTargets, + "Test: %s\nTargeting failed for bidder %s on imp %s.", + test.Description, + string(bidder), + imp) + } + } + } + } diff --git a/exchange/utils.go b/exchange/utils.go index 969471b04c3..78baa00ad9f 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -6,6 +6,8 @@ import ( "fmt" "math/rand" + "github.com/prebid/go-gdpr/vendorconsent" + "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/gdpr" @@ -17,6 +19,34 @@ import ( "github.com/buger/jsonparser" ) +var integrationTypeMap = map[pbsmetrics.RequestType]config.IntegrationType{ + pbsmetrics.ReqTypeAMP: config.IntegrationTypeAMP, + pbsmetrics.ReqTypeORTB2App: config.IntegrationTypeApp, + pbsmetrics.ReqTypeVideo: config.IntegrationTypeVideo, + pbsmetrics.ReqTypeORTB2Web: config.IntegrationTypeWeb, +} + +const unknownBidder string = "" + +func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) { + bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) + + if req != nil { + for _, schainWrapper := range req.Prebid.SChains { + for _, bidder := range schainWrapper.Bidders { + if _, present := bidderToSChains[bidder]; present { + return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+ + "it must contain no more than one per bidder.", bidder) + } else { + bidderToSChains[bidder] = &schainWrapper.SChain + } + } + } + } + + return bidderToSChains, nil +} + // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is: // // 1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder. @@ -24,12 +54,14 @@ import ( // 3. BidRequest.User.BuyerUID will be set to that Bidder's ID. func cleanOpenRTBRequests(ctx context.Context, orig *openrtb.BidRequest, + requestExt *openrtb_ext.ExtRequest, usersyncs IdFetcher, blables map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels, gDPR gdpr.Permissions, usersyncIfAmbiguous bool, - privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) { + privacyConfig config.Privacy, + account *config.Account) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, privacyLabels pbsmetrics.PrivacyLabels, errs []error) { impsByBidder, errs := splitImps(orig.Imp) if len(errs) > 0 { @@ -41,42 +73,62 @@ func cleanOpenRTBRequests(ctx context.Context, return } - requestsByBidder, errs = splitBidRequest(orig, impsByBidder, aliases, usersyncs, blables, labels) + requestsByBidder, errs = splitBidRequest(orig, requestExt, impsByBidder, aliases, usersyncs, blables, labels) + + if len(requestsByBidder) == 0 { + return + } gdpr := extractGDPR(orig, usersyncIfAmbiguous) consent := extractConsent(orig) ampGDPRException := (labels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException() - var ccpaPolicy ccpa.Policy - if privacyConfig.CCPA.Enforce { - ccpaPolicy, _ = ccpa.ReadPolicy(orig) + ccpaEnforcer, err := extractCCPA(orig, privacyConfig, account, aliases, integrationTypeMap[labels.RType]) + if err != nil { + errs = append(errs, err) + return } - var lmtPolicy lmt.Policy - if privacyConfig.LMT.Enforce { - lmtPolicy = lmt.ReadPolicy(orig) - } + lmtEnforcer := extractLMT(orig, privacyConfig) // request level privacy policies privacyEnforcement := privacy.Enforcement{ - CCPA: ccpaPolicy.ShouldEnforce(), COPPA: orig.Regs != nil && orig.Regs.COPPA == 1, - LMT: lmtPolicy.ShouldEnforce(), + LMT: lmtEnforcer.ShouldEnforce(unknownBidder), + } + + privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce() + privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder) + privacyLabels.COPPAEnforced = privacyEnforcement.COPPA + privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder) + + gdprEnabled := gdprEnabled(account, privacyConfig, integrationTypeMap[labels.RType]) + + if gdpr == 1 && gdprEnabled { + privacyLabels.GDPREnforced = true + parsedConsent, err := vendorconsent.ParseString(consent) + if err == nil { + version := int(parsedConsent.Version()) + privacyLabels.GDPRTCFVersion = pbsmetrics.TCFVersionToValue(version) + } } // bidder level privacy policies for bidder, bidReq := range requestsByBidder { + // CCPA + privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidder.String()) - if gdpr == 1 { + // GDPR + if gdpr == 1 && gdprEnabled { coreBidder := resolveBidder(bidder.String(), aliases) var publisherID = labels.PubID - ok, geo, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) - privacyEnforcement.GDPR = !ok && err == nil + _, geo, id, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent) privacyEnforcement.GDPRGeo = !geo && err == nil + privacyEnforcement.GDPRID = !id && err == nil } else { - privacyEnforcement.GDPR = false privacyEnforcement.GDPRGeo = false + privacyEnforcement.GDPRID = false } privacyEnforcement.Apply(bidReq, ampGDPRException) @@ -85,6 +137,46 @@ func cleanOpenRTBRequests(ctx context.Context, return } +func gdprEnabled(account *config.Account, privacyConfig config.Privacy, integrationType config.IntegrationType) bool { + if accountEnabled := account.GDPR.EnabledForIntegrationType(integrationType); accountEnabled != nil { + return *accountEnabled + } + return privacyConfig.GDPR.Enabled +} + +func ccpaEnabled(account *config.Account, privacyConfig config.Privacy, requestType config.IntegrationType) bool { + if accountEnabled := account.CCPA.EnabledForIntegrationType(requestType); accountEnabled != nil { + return *accountEnabled + } + return privacyConfig.CCPA.Enforce +} + +func extractCCPA(orig *openrtb.BidRequest, privacyConfig config.Privacy, account *config.Account, aliases map[string]string, requestType config.IntegrationType) (privacy.PolicyEnforcer, error) { + ccpaPolicy, err := ccpa.ReadFromRequest(orig) + if err != nil { + return privacy.NilPolicyEnforcer{}, err + } + + validBidders := GetValidBidders(aliases) + ccpaParsedPolicy, err := ccpaPolicy.Parse(validBidders) + if err != nil { + return privacy.NilPolicyEnforcer{}, err + } + + ccpaEnforcer := privacy.EnabledPolicyEnforcer{ + Enabled: ccpaEnabled(account, privacyConfig, requestType), + PolicyEnforcer: ccpaParsedPolicy, + } + return ccpaEnforcer, nil +} + +func extractLMT(orig *openrtb.BidRequest, privacyConfig config.Privacy) privacy.PolicyEnforcer { + return privacy.EnabledPolicyEnforcer{ + Enabled: privacyConfig.LMT.Enforce, + PolicyEnforcer: lmt.ReadFromRequest(orig), + } +} + func getBidderExts(reqExt *openrtb_ext.ExtRequest) (map[string]map[string]interface{}, error) { if reqExt == nil { return nil, nil @@ -108,7 +200,14 @@ func getBidderExts(reqExt *openrtb_ext.ExtRequest) (map[string]map[string]interf return bidderParams, nil } -func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.Imp, aliases map[string]string, usersyncs IdFetcher, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) { +func splitBidRequest(req *openrtb.BidRequest, + requestExt *openrtb_ext.ExtRequest, + impsByBidder map[string][]openrtb.Imp, + aliases map[string]string, + usersyncs IdFetcher, + blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, + labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) { + requestsByBidder := make(map[openrtb_ext.BidderName]*openrtb.BidRequest, len(impsByBidder)) explicitBuyerUIDs, err := extractBuyerUIDs(req.User) if err != nil { @@ -116,17 +215,23 @@ func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb. } var bidderExt map[string]map[string]interface{} - var reqExt openrtb_ext.ExtRequest - if req.Ext != nil { - err = json.Unmarshal(req.Ext, &reqExt) + if requestExt != nil { + bidderExt, err = getBidderExts(requestExt) if err != nil { return nil, []error{err} } + } - bidderExt, err = getBidderExts(&reqExt) - if err != nil { - return nil, []error{err} - } + var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain + + sChainsByBidder, err = BidderToPrebidSChains(requestExt) + if err != nil { + return nil, []error{err} + } + + reqExt, err := getExtJson(req, requestExt) + if err != nil { + return nil, []error{err} } for bidder, imps := range impsByBidder { @@ -147,18 +252,23 @@ func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb. } else { blabels[coreBidder].CookieFlag = pbsmetrics.CookieFlagYes } + reqCopy.Imp = imps + prepareSource(&reqCopy, bidder, sChainsByBidder) + if len(bidderExt) != 0 { bidderName := openrtb_ext.BidderName(bidder) if bidderParams, ok := bidderExt[string(bidderName)]; ok { - reqExt.Prebid.BidderParams = bidderParams + requestExt.Prebid.BidderParams = bidderParams } else { - reqExt.Prebid.BidderParams = nil + requestExt.Prebid.BidderParams = nil } - if reqCopy.Ext, err = json.Marshal(&reqExt); err != nil { + if reqCopy.Ext, err = getExtJson(req, requestExt); err != nil { return nil, []error{err} } + } else { + reqCopy.Ext = reqExt } requestsByBidder[openrtb_ext.BidderName(bidder)] = &reqCopy @@ -166,6 +276,47 @@ func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb. return requestsByBidder, nil } +func getExtJson(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) (json.RawMessage, error) { + if len(req.Ext) == 0 || unpackedExt == nil { + return json.RawMessage(``), nil + } + + extCopy := *unpackedExt + extCopy.Prebid.SChains = nil + return json.Marshal(extCopy) +} + +func prepareSource(req *openrtb.BidRequest, bidder string, sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) { + const sChainWildCard = "*" + var selectedSChain *openrtb_ext.ExtRequestPrebidSChainSChain + + wildCardSChain := sChainsByBidder[sChainWildCard] + bidderSChain := sChainsByBidder[bidder] + + // source should not be modified + if bidderSChain == nil && wildCardSChain == nil { + return + } + + if bidderSChain != nil { + selectedSChain = bidderSChain + } else { + selectedSChain = wildCardSChain + } + + // set source + if req.Source == nil { + req.Source = &openrtb.Source{} + } + schain := openrtb_ext.ExtRequestPrebidSChain{ + SChain: *selectedSChain, + } + sourceExt, err := json.Marshal(schain) + if err == nil { + req.Source.Ext = sourceExt + } +} + // extractBuyerUIDs parses the values from user.ext.prebid.buyeruids, and then deletes those values from the ext. // This prevents a Bidder from using these values to figure out who else is involved in the Auction. func extractBuyerUIDs(user *openrtb.User) (map[string]string, error) { @@ -221,13 +372,18 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) { imp := imps[i] impExt := impExts[i] + var firstPartyDataContext json.RawMessage + if context, exists := impExt[openrtb_ext.FirstPartyDataContextExtKey]; exists { + firstPartyDataContext = context + } + rawPrebidExt, ok := impExt[openrtb_ext.PrebidExtKey] if ok { var prebidExt openrtb_ext.ExtImpPrebid if err := json.Unmarshal(rawPrebidExt, &prebidExt); err == nil && prebidExt.Bidder != nil { - if errs := sanitizedImpCopy(&imp, prebidExt.Bidder, rawPrebidExt, &splitImps); errs != nil { + if errs := sanitizedImpCopy(&imp, prebidExt.Bidder, rawPrebidExt, firstPartyDataContext, &splitImps); errs != nil { errList = append(errList, errs...) } @@ -235,7 +391,7 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) { } } - if errs := sanitizedImpCopy(&imp, impExt, rawPrebidExt, &splitImps); errs != nil { + if errs := sanitizedImpCopy(&imp, impExt, rawPrebidExt, firstPartyDataContext, &splitImps); errs != nil { errList = append(errList, errs...) } } @@ -243,35 +399,38 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) { return splitImps, nil } -// sanitizedImpCopy returns a copy of imp with its ext filtered so that only "prebid" and bidder params exist. +// sanitizedImpCopy returns a copy of imp with its ext filtered so that only "prebid", "context", and bidder params exist. // It will not mutate the input imp. // This function will write the new imps to the output map passed in func sanitizedImpCopy(imp *openrtb.Imp, bidderExts map[string]json.RawMessage, rawPrebidExt json.RawMessage, + firstPartyDataContext json.RawMessage, out *map[string][]openrtb.Imp) []error { var prebidExt map[string]json.RawMessage var errs []error - // We don't want to include other demand partners' bidder params - // in the sanitized imp if err := json.Unmarshal(rawPrebidExt, &prebidExt); err == nil { - delete(prebidExt, "bidder") - - var err error - if rawPrebidExt, err = json.Marshal(prebidExt); err != nil { - errs = append(errs, err) + // Remove the entire bidder field. We will already have the content we need in bidderExts. We + // don't want to include other demand partners' bidder params in the sanitized imp. + if _, hasBidderField := prebidExt["bidder"]; hasBidderField { + delete(prebidExt, "bidder") + + var err error + if rawPrebidExt, err = json.Marshal(prebidExt); err != nil { + errs = append(errs, err) + } } } for bidder, ext := range bidderExts { - if bidder == openrtb_ext.PrebidExtKey { + if bidder == openrtb_ext.PrebidExtKey || bidder == openrtb_ext.FirstPartyDataContextExtKey { continue } impCopy := *imp - newExt := make(map[string]json.RawMessage, 2) + newExt := make(map[string]json.RawMessage, 3) newExt["bidder"] = ext @@ -279,6 +438,10 @@ func sanitizedImpCopy(imp *openrtb.Imp, newExt[openrtb_ext.PrebidExtKey] = rawPrebidExt } + if len(firstPartyDataContext) > 0 { + newExt[openrtb_ext.FirstPartyDataContextExtKey] = firstPartyDataContext + } + rawExt, err := json.Marshal(newExt) if err != nil { errs = append(errs, err) @@ -340,7 +503,7 @@ func resolveBidder(bidder string, aliases map[string]string) openrtb_ext.BidderN } // parseImpExts does a partial-unmarshal of the imp[].Ext field. -// The keys in the returned map are expected to be "prebid", core BidderNames, or Aliases for this request. +// The keys in the returned map are expected to be "prebid", "context", core BidderNames, or Aliases for this request. func parseImpExts(imps []openrtb.Imp) ([]map[string]json.RawMessage, error) { exts := make([]map[string]json.RawMessage, len(imps)) // Loop over every impression in the request @@ -367,6 +530,20 @@ func parseAliases(orig *openrtb.BidRequest) (map[string]string, []error) { return aliases, nil } +func GetValidBidders(aliases map[string]string) map[string]struct{} { + validBidders := make(map[string]struct{}) + + for _, v := range openrtb_ext.BidderMap { + validBidders[v.String()] = struct{}{} + } + + for k := range aliases { + validBidders[k] = struct{}{} + } + + return validBidders +} + // Quick little randomizer for a list of strings. Stuffing it in utils to keep other files clean func randomizeList(list []openrtb_ext.BidderName) { l := len(list) @@ -377,3 +554,78 @@ func randomizeList(list []openrtb_ext.BidderName) { list[i], list[j] = list[j], list[i] } } + +func extractBidRequestExt(bidRequest *openrtb.BidRequest) (*openrtb_ext.ExtRequest, error) { + requestExt := &openrtb_ext.ExtRequest{} + + if bidRequest == nil { + return requestExt, fmt.Errorf("Error bidRequest should not be nil") + } + + if len(bidRequest.Ext) > 0 { + err := json.Unmarshal(bidRequest.Ext, &requestExt) + if err != nil { + return requestExt, fmt.Errorf("Error decoding Request.ext : %s", err.Error()) + } + } + return requestExt, nil +} + +func getExtCacheInstructions(requestExt *openrtb_ext.ExtRequest) extCacheInstructions { + //returnCreative defaults to true + cacheInstructions := extCacheInstructions{returnCreative: true} + foundBidsRC := false + foundVastRC := false + + if requestExt != nil && requestExt.Prebid.Cache != nil { + if requestExt.Prebid.Cache.Bids != nil { + cacheInstructions.cacheBids = true + if requestExt.Prebid.Cache.Bids.ReturnCreative != nil { + cacheInstructions.returnCreative = *requestExt.Prebid.Cache.Bids.ReturnCreative + foundBidsRC = true + } + } + if requestExt.Prebid.Cache.VastXML != nil { + cacheInstructions.cacheVAST = true + if requestExt.Prebid.Cache.VastXML.ReturnCreative != nil { + cacheInstructions.returnCreative = *requestExt.Prebid.Cache.VastXML.ReturnCreative + foundVastRC = true + } + } + } + + if foundBidsRC && foundVastRC { + cacheInstructions.returnCreative = *requestExt.Prebid.Cache.Bids.ReturnCreative || *requestExt.Prebid.Cache.VastXML.ReturnCreative + } + + return cacheInstructions +} + +func getExtTargetData(requestExt *openrtb_ext.ExtRequest, cacheInstructions *extCacheInstructions) *targetData { + var targData *targetData + + if requestExt != nil && requestExt.Prebid.Targeting != nil { + targData = &targetData{ + priceGranularity: requestExt.Prebid.Targeting.PriceGranularity, + includeWinners: requestExt.Prebid.Targeting.IncludeWinners, + includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys, + includeCacheBids: cacheInstructions.cacheBids, + includeCacheVast: cacheInstructions.cacheVAST, + includeFormat: requestExt.Prebid.Targeting.IncludeFormat, + preferDeals: requestExt.Prebid.Targeting.PreferDeals, + } + } + return targData +} + +func getDebugInfo(bidRequest *openrtb.BidRequest, requestExt *openrtb_ext.ExtRequest) bool { + return (bidRequest != nil && bidRequest.Test == 1) || (requestExt != nil && requestExt.Prebid.Debug) +} + +func getExtBidAdjustmentFactors(requestExt *openrtb_ext.ExtRequest) map[string]float64 { + var bidAdjustmentFactors map[string]float64 + if requestExt != nil { + bidAdjustmentFactors = requestExt.Prebid.BidAdjustmentFactors + } + return bidAdjustmentFactors +} diff --git a/exchange/utils_test.go b/exchange/utils_test.go index bd1be73ff3b..202b479fecd 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -3,10 +3,13 @@ package exchange import ( "context" "encoding/json" + "errors" + "fmt" "testing" "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" "github.com/stretchr/testify/assert" @@ -15,7 +18,9 @@ import ( // permissionsMock mocks the Permissions interface for tests // // It only allows appnexus for GDPR consent -type permissionsMock struct{} +type permissionsMock struct { + personalInfoAllowed bool +} func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { return true, nil @@ -25,11 +30,8 @@ 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, bool, error) { - if bidder == "appnexus" { - return true, true, nil - } - return false, false, nil +func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil } func (p *permissionsMock) AMPException() bool { @@ -80,7 +82,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { } for _, test := range testCases { - reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{}) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -91,40 +93,154 @@ func TestCleanOpenRTBRequests(t *testing.T) { } func TestCleanOpenRTBRequestsCCPA(t *testing.T) { + trueValue, falseValue := true, false + testCases := []struct { - description string - enforceCCPA bool - expectDataScrub bool + description string + reqExt json.RawMessage + ccpaConsent string + ccpaHostEnabled bool + ccpaAccountEnabled *bool + expectDataScrub bool + expectPrivacyLabels pbsmetrics.PrivacyLabels }{ { - description: "Feature Flag Enabled", - enforceCCPA: true, - expectDataScrub: true, + description: "Feature Flags Enabled - Opt Out", + ccpaConsent: "1-Y-", + ccpaHostEnabled: true, + ccpaAccountEnabled: &trueValue, + expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, }, { - description: "Feature Flag Disabled", - enforceCCPA: false, - expectDataScrub: false, + description: "Feature Flags Enabled - Opt In", + ccpaConsent: "1-N-", + ccpaHostEnabled: true, + ccpaAccountEnabled: &trueValue, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: false, + }, + }, + { + description: "Feature Flags Enabled - No Sale Star - Doesn't Scrub", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*"]}}`), + ccpaConsent: "1-Y-", + ccpaHostEnabled: true, + ccpaAccountEnabled: &trueValue, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: false, + }, + }, + { + description: "Feature Flags Enabled - No Sale Specific Bidder - Doesn't Scrub", + reqExt: json.RawMessage(`{"prebid":{"nosale":["appnexus"]}}`), + ccpaConsent: "1-Y-", + ccpaHostEnabled: true, + ccpaAccountEnabled: &trueValue, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, + }, + { + description: "Feature Flags Enabled - No Sale Different Bidder - Scrubs", + reqExt: json.RawMessage(`{"prebid":{"nosale":["rubicon"]}}`), + ccpaConsent: "1-Y-", + ccpaHostEnabled: true, + ccpaAccountEnabled: &trueValue, + expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, + }, + { + description: "Feature flags Account CCPA enabled, host CCPA disregarded - Opt Out", + ccpaConsent: "1-Y-", + ccpaHostEnabled: false, + ccpaAccountEnabled: &trueValue, + expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, + }, + { + description: "Feature flags Account CCPA disabled, host CCPA disregarded", + ccpaConsent: "1-Y-", + ccpaHostEnabled: true, + ccpaAccountEnabled: &falseValue, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: false, + }, + }, + { + description: "Feature flags Account CCPA not specified, host CCPA enabled - Opt Out", + ccpaConsent: "1-Y-", + ccpaHostEnabled: true, + ccpaAccountEnabled: nil, + expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: true, + }, + }, + { + description: "Feature flags Account CCPA not specified, host CCPA disabled", + ccpaConsent: "1-Y-", + ccpaHostEnabled: false, + ccpaAccountEnabled: nil, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + CCPAProvided: true, + CCPAEnforced: false, + }, }, } for _, test := range testCases { req := newBidRequest(t) + req.Ext = test.reqExt req.Regs = &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`), + Ext: json.RawMessage(`{"us_privacy":"` + test.ccpaConsent + `"}`), } privacyConfig := config.Privacy{ CCPA: config.CCPA{ - Enforce: test.enforceCCPA, + Enforce: test.ccpaHostEnabled, }, } - results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + accountConfig := config.Account{ + CCPA: config.AccountCCPA{ + Enabled: test.ccpaAccountEnabled, + }, + } + + results, _, privacyLabels, errs := cleanOpenRTBRequests( + context.Background(), + req, + nil, + &emptyUsersync{}, + map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, + pbsmetrics.Labels{}, + &permissionsMock{personalInfoAllowed: true}, + true, + privacyConfig, + &accountConfig) 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") @@ -132,6 +248,696 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5") } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") + } +} + +func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { + testCases := []struct { + description string + reqExt json.RawMessage + reqRegsExt json.RawMessage + expectError error + }{ + { + description: "Invalid Consent", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*"]}}`), + reqRegsExt: json.RawMessage(`{"us_privacy":"malformed"}`), + expectError: &errortypes.InvalidPrivacyConsent{"request.regs.ext.us_privacy must contain 4 characters"}, + }, + { + description: "Invalid No Sale Bidders", + reqExt: json.RawMessage(`{"prebid":{"nosale":["*", "another"]}}`), + reqRegsExt: json.RawMessage(`{"us_privacy":"1NYN"}`), + expectError: errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided"), + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.Ext = test.reqExt + req.Regs = &openrtb.Regs{Ext: test.reqRegsExt} + + var reqExtStruct openrtb_ext.ExtRequest + err := json.Unmarshal(req.Ext, &reqExtStruct) + assert.NoError(t, err, test.description+":marshal_ext") + + privacyConfig := config.Privacy{ + CCPA: config.CCPA{ + Enforce: true, + }, + } + _, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &reqExtStruct, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{}) + + assert.ElementsMatch(t, []error{test.expectError}, errs, test.description) + } +} + +func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { + testCases := []struct { + description string + coppa int8 + expectDataScrub bool + expectPrivacyLabels pbsmetrics.PrivacyLabels + }{ + { + description: "Enabled", + coppa: 1, + expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + COPPAEnforced: true, + }, + }, + { + description: "Disabled", + coppa: 0, + expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + COPPAEnforced: false, + }, + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.Regs = &openrtb.Regs{COPPA: test.coppa} + + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, config.Privacy{}, &config.Account{}) + result := results["appnexus"] + + assert.Nil(t, errs) + if test.expectDataScrub { + assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") + assert.Equal(t, result.User.Yob, int64(0), test.description+":User.Yob") + } else { + assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") + assert.NotEqual(t, result.User.Yob, int64(0), test.description+":User.Yob") + } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") + } +} + +func TestCleanOpenRTBRequestsSChain(t *testing.T) { + testCases := []struct { + description string + inExt json.RawMessage + inSourceExt json.RawMessage + outSourceExt json.RawMessage + outRequestExt json.RawMessage + hasError bool + }{ + { + description: "Empty root ext and source ext, nil unmarshaled ext", + inExt: nil, + inSourceExt: json.RawMessage(``), + outSourceExt: json.RawMessage(``), + outRequestExt: json.RawMessage(``), + hasError: false, + }, + { + description: "Empty root ext, source ext, and unmarshaled ext", + inExt: json.RawMessage(``), + inSourceExt: json.RawMessage(``), + outSourceExt: json.RawMessage(``), + outRequestExt: json.RawMessage(``), + hasError: false, + }, + { + description: "No schains in root ext and empty source ext. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[]}}`), + outSourceExt: json.RawMessage(``), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use schain for bidder in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use wildcard schain in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{}}`), + hasError: false, + }, + { + description: "Use schain for bidder in ext.prebid.schains instead of wildcard. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(``), + inExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`), + outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`), + outRequestExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`), + hasError: false, + }, + { + description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains. Unmarshaled ext is equivalent to root ext", + inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`), + inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), + outSourceExt: nil, + outRequestExt: nil, + hasError: true, + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.Source.Ext = test.inSourceExt + + var extRequest *openrtb_ext.ExtRequest + if test.inExt != nil { + req.Ext = test.inExt + unmarshaledExt, err := extractBidRequestExt(req) + assert.NoErrorf(t, err, test.description+":Error unmarshaling inExt") + extRequest = unmarshaledExt + } + + results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, extRequest, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{}, &config.Account{}) + result := results["appnexus"] + + if test.hasError == true { + assert.NotNil(t, errs) + assert.Nil(t, result) + } else { + assert.Nil(t, errs) + assert.Equal(t, test.outSourceExt, result.Source.Ext, test.description+":Source.Ext") + assert.Equal(t, test.outRequestExt, result.Ext, test.description+":Ext") + } + } +} + +func TestExtractBidRequestExt(t *testing.T) { + var boolFalse, boolTrue *bool = new(bool), new(bool) + *boolFalse = false + *boolTrue = true + + testCases := []struct { + desc string + inBidRequest *openrtb.BidRequest + outRequestExt *openrtb_ext.ExtRequest + outError error + }{ + { + desc: "Valid vastxml.returnCreative set to false", + inBidRequest: &openrtb.BidRequest{Ext: json.RawMessage(`{"prebid":{"debug":true,"cache":{"vastxml":{"returnCreative":false}}}}`)}, + outRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Debug: true, + Cache: &openrtb_ext.ExtRequestPrebidCache{ + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ + ReturnCreative: boolFalse, + }, + }, + }, + }, + outError: nil, + }, + { + desc: "Valid vastxml.returnCreative set to true", + inBidRequest: &openrtb.BidRequest{Ext: json.RawMessage(`{"prebid":{"debug":true,"cache":{"vastxml":{"returnCreative":true}}}}`)}, + outRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Debug: true, + Cache: &openrtb_ext.ExtRequestPrebidCache{ + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ + ReturnCreative: boolTrue, + }, + }, + }, + }, + outError: nil, + }, + { + desc: "bidRequest nil, we expect an error", + inBidRequest: nil, + outRequestExt: &openrtb_ext.ExtRequest{}, + outError: fmt.Errorf("Error bidRequest should not be nil"), + }, + { + desc: "Non-nil bidRequest with empty Ext, we expect a blank requestExt", + inBidRequest: &openrtb.BidRequest{}, + outRequestExt: &openrtb_ext.ExtRequest{}, + outError: nil, + }, + { + desc: "Non-nil bidRequest with non-empty, invalid Ext, we expect unmarshaling error", + inBidRequest: &openrtb.BidRequest{Ext: json.RawMessage(`invalid`)}, + outRequestExt: &openrtb_ext.ExtRequest{}, + outError: fmt.Errorf("Error decoding Request.ext : invalid character 'i' looking for beginning of value"), + }, + } + for _, test := range testCases { + actualRequestExt, actualErr := extractBidRequestExt(test.inBidRequest) + + // Given that assert.Equal asserts pointer variable equality based on the equality of the referenced values (as opposed to + // the memory addresses) the call below asserts whether or not *test.outRequestExt.Prebid.Cache.VastXML.ReturnCreative boolean + // value is equal to that of *actualRequestExt.Prebid.Cache.VastXML.ReturnCreative + assert.Equal(t, test.outRequestExt, actualRequestExt, "%s. Unexpected RequestExt value. \n", test.desc) + assert.Equal(t, test.outError, actualErr, "%s. Unexpected error value. \n", test.desc) + } +} + +func TestGetExtCacheInstructions(t *testing.T) { + var boolFalse, boolTrue *bool = new(bool), new(bool) + *boolFalse = false + *boolTrue = true + + testCases := []struct { + desc string + inRequestExt *openrtb_ext.ExtRequest + outCacheInstructions extCacheInstructions + }{ + { + desc: "Nil inRequestExt, all cache flags false except for returnCreative that defaults to true", + inRequestExt: nil, + outCacheInstructions: extCacheInstructions{ + cacheBids: false, + cacheVAST: false, + returnCreative: true, + }, + }, + { + desc: "Non-nil inRequestExt, nil Cache field, all cache flags false except for returnCreative that defaults to true", + inRequestExt: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Cache: nil}}, + outCacheInstructions: extCacheInstructions{ + cacheBids: false, + cacheVAST: false, + returnCreative: true, + }, + }, + { + desc: "Non-nil Cache field, both ExtRequestPrebidCacheBids and ExtRequestPrebidCacheVAST nil returnCreative that defaults to true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: nil, + VastXML: nil, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: false, + cacheVAST: false, + returnCreative: true, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheVAST with unspecified ReturnCreative field, cacheVAST = true and returnCreative defaults to true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: nil, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: false, + cacheVAST: true, + returnCreative: true, // default value + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheVAST where ReturnCreative is set to false, cacheVAST = true and returnCreative = false", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: nil, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ReturnCreative: boolFalse}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: false, + cacheVAST: true, + returnCreative: false, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheVAST where ReturnCreative is set to true, cacheVAST = true and returnCreative = true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: nil, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ReturnCreative: boolTrue}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: false, + cacheVAST: true, + returnCreative: true, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheBids with unspecified ReturnCreative field, cacheBids = true and returnCreative defaults to true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, + VastXML: nil, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: false, + returnCreative: true, // default value + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheBids where ReturnCreative is set to false, cacheBids = true and returnCreative = false", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{ReturnCreative: boolFalse}, + VastXML: nil, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: false, + returnCreative: false, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheBids where ReturnCreative is set to true, cacheBids = true and returnCreative = true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{ReturnCreative: boolTrue}, + VastXML: nil, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: false, + returnCreative: true, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheBids and ExtRequest.Cache.ExtRequestPrebidCacheVAST, neither specify a ReturnCreative field value, all extCacheInstructions fields set to true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + returnCreative: true, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheBids and ExtRequest.Cache.ExtRequestPrebidCacheVAST sets ReturnCreative to true, all extCacheInstructions fields set to true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ReturnCreative: boolTrue}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + returnCreative: true, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheBids and ExtRequest.Cache.ExtRequestPrebidCacheVAST sets ReturnCreative to false, returnCreative = false", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ReturnCreative: boolFalse}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + returnCreative: false, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheVAST and ExtRequest.Cache.ExtRequestPrebidCacheBids sets ReturnCreative to true, all extCacheInstructions fields set to true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{ReturnCreative: boolTrue}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + returnCreative: true, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheVAST and ExtRequest.Cache.ExtRequestPrebidCacheBids sets ReturnCreative to false, returnCreative = false", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{ReturnCreative: boolFalse}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + returnCreative: false, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheVAST and ExtRequest.Cache.ExtRequestPrebidCacheBids set different ReturnCreative values, returnCreative = true because one of them is true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{ReturnCreative: boolFalse}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ReturnCreative: boolTrue}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + returnCreative: true, + }, + }, + { + desc: "Non-nil ExtRequest.Cache.ExtRequestPrebidCacheVAST and ExtRequest.Cache.ExtRequestPrebidCacheBids set different ReturnCreative values, returnCreative = true because one of them is true", + inRequestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Cache: &openrtb_ext.ExtRequestPrebidCache{ + Bids: &openrtb_ext.ExtRequestPrebidCacheBids{ReturnCreative: boolTrue}, + VastXML: &openrtb_ext.ExtRequestPrebidCacheVAST{ReturnCreative: boolFalse}, + }, + }, + }, + outCacheInstructions: extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + returnCreative: true, + }, + }, + } + + for _, test := range testCases { + cacheInstructions := getExtCacheInstructions(test.inRequestExt) + + assert.Equal(t, test.outCacheInstructions.cacheBids, cacheInstructions.cacheBids, "%s. Unexpected shouldCacheBids value. \n", test.desc) + assert.Equal(t, test.outCacheInstructions.cacheVAST, cacheInstructions.cacheVAST, "%s. Unexpected shouldCacheVAST value. \n", test.desc) + assert.Equal(t, test.outCacheInstructions.returnCreative, cacheInstructions.returnCreative, "%s. Unexpected returnCreative value. \n", test.desc) + } +} + +func TestGetExtTargetData(t *testing.T) { + type inTest struct { + requestExt *openrtb_ext.ExtRequest + cacheInstructions *extCacheInstructions + } + type outTest struct { + targetData *targetData + nilTargetData bool + } + testCases := []struct { + desc string + in inTest + out outTest + }{ + { + "nil requestExt, nil outTargetData", + inTest{ + requestExt: nil, + cacheInstructions: &extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + }, + }, + outTest{targetData: nil, nilTargetData: true}, + }, + { + "Valid requestExt, nil Targeting field, nil outTargetData", + inTest{ + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Targeting: nil, + }, + }, + cacheInstructions: &extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + }, + }, + outTest{targetData: nil, nilTargetData: true}, + }, + { + "Valid targeting data in requestExt, valid outTargetData", + inTest{ + requestExt: &openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + Targeting: &openrtb_ext.ExtRequestTargeting{ + PriceGranularity: openrtb_ext.PriceGranularity{ + Precision: 2, + Ranges: []openrtb_ext.GranularityRange{{Min: 0.00, Max: 5.00, Increment: 1.00}}, + }, + IncludeWinners: true, + IncludeBidderKeys: true, + }, + }, + }, + cacheInstructions: &extCacheInstructions{ + cacheBids: true, + cacheVAST: true, + }, + }, + outTest{ + targetData: &targetData{ + priceGranularity: openrtb_ext.PriceGranularity{ + Precision: 2, + Ranges: []openrtb_ext.GranularityRange{{Min: 0.00, Max: 5.00, Increment: 1.00}}, + }, + includeWinners: true, + includeBidderKeys: true, + includeCacheBids: true, + includeCacheVast: true, + }, + nilTargetData: false, + }, + }, + } + for _, test := range testCases { + actualTargetData := getExtTargetData(test.in.requestExt, test.in.cacheInstructions) + + if test.out.nilTargetData { + assert.Nil(t, actualTargetData, "%s. Targeting data should be nil. \n", test.desc) + } else { + assert.NotNil(t, actualTargetData, "%s. Targeting data should NOT be nil. \n", test.desc) + assert.Equal(t, *test.out.targetData, *actualTargetData, "%s. Unexpected targeting data value. \n", test.desc) + } + } +} + +func TestGetDebugInfo(t *testing.T) { + type inTest struct { + bidRequest *openrtb.BidRequest + requestExt *openrtb_ext.ExtRequest + } + testCases := []struct { + desc string + in inTest + out bool + }{ + { + desc: "Nil bid request, nil requestExt", + in: inTest{nil, nil}, + out: false, + }, + { + desc: "bid request test == 0, nil requestExt", + in: inTest{&openrtb.BidRequest{Test: 0}, nil}, + out: false, + }, + { + desc: "Nil bid request, requestExt debug flag false", + in: inTest{nil, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: false}}}, + out: false, + }, + { + desc: "bid request test == 0, requestExt debug flag false", + in: inTest{&openrtb.BidRequest{Test: 0}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: false}}}, + out: false, + }, + { + desc: "bid request test == 1, requestExt debug flag false", + in: inTest{&openrtb.BidRequest{Test: 1}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: false}}}, + out: true, + }, + { + desc: "bid request test == 0, requestExt debug flag true", + in: inTest{&openrtb.BidRequest{Test: 0}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: true}}}, + out: true, + }, + { + desc: "bid request test == 1, requestExt debug flag true", + in: inTest{&openrtb.BidRequest{Test: 1}, &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Debug: true}}}, + out: true, + }, + } + for _, test := range testCases { + actualDebugInfo := getDebugInfo(test.in.bidRequest, test.in.requestExt) + + assert.Equal(t, test.out, actualDebugInfo, "%s. Unexpected debug value. \n", test.desc) + } +} + +func TestGetExtBidAdjustmentFactors(t *testing.T) { + testCases := []struct { + desc string + inRequestExt *openrtb_ext.ExtRequest + outBidAdjustmentFactors map[string]float64 + }{ + { + desc: "Nil request ext", + inRequestExt: nil, + outBidAdjustmentFactors: nil, + }, + { + desc: "Non-nil request ext, nil BidAdjustmentFactors field", + inRequestExt: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{BidAdjustmentFactors: nil}}, + outBidAdjustmentFactors: nil, + }, + { + desc: "Non-nil request ext, valid BidAdjustmentFactors field", + inRequestExt: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{BidAdjustmentFactors: map[string]float64{"bid-factor": 1.0}}}, + outBidAdjustmentFactors: map[string]float64{"bid-factor": 1.0}, + }, + } + for _, test := range testCases { + actualBidAdjustmentFactors := getExtBidAdjustmentFactors(test.inRequestExt) + + assert.Equal(t, test.outBidAdjustmentFactors, actualBidAdjustmentFactors, "%s. Unexpected BidAdjustmentFactors value. \n", test.desc) } } @@ -141,34 +947,47 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { disabled int8 = 0 ) testCases := []struct { - description string - lmt *int8 - enforceLMT bool - expectDataScrub bool + description string + lmt *int8 + enforceLMT bool + expectDataScrub bool + expectPrivacyLabels pbsmetrics.PrivacyLabels }{ { description: "Feature Flag Enabled - OpenTRB Enabled", lmt: &enabled, enforceLMT: true, expectDataScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: true, + }, }, { description: "Feature Flag Disabled - OpenTRB Enabled", lmt: &enabled, enforceLMT: false, expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: false, + }, }, { description: "Feature Flag Enabled - OpenTRB Disabled", lmt: &disabled, enforceLMT: true, expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: false, + }, }, { description: "Feature Flag Disabled - OpenTRB Disabled", lmt: &disabled, enforceLMT: false, expectDataScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + LMTEnforced: false, + }, }, } @@ -182,11 +1001,10 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { }, } - results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig) + results, _, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), req, nil, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig, &config.Account{}) 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") @@ -194,6 +1012,164 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID") assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5") } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") + } +} + +func TestCleanOpenRTBRequestsGDPR(t *testing.T) { + trueValue, falseValue := true, false + + testCases := []struct { + description string + gdprAccountEnabled *bool + gdprHostEnabled bool + gdpr string + gdprConsent string + gdprScrub bool + expectPrivacyLabels pbsmetrics.PrivacyLabels + }{ + { + description: "Enforce - TCF Invalid", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "malformed", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: "", + }, + }, + { + description: "Enforce - TCF 1", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }, + }, + { + description: "Enforce - TCF 2", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV2, + }, + }, + { + description: "Not Enforce - TCF 1", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: true, + gdpr: "0", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + }, + { + description: "Enforce - TCF 1; account GDPR enabled, host GDPR setting disregarded", + gdprAccountEnabled: &trueValue, + gdprHostEnabled: false, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }, + }, + { + description: "Not Enforce - TCF 1; account GDPR disabled, host GDPR setting disregarded", + gdprAccountEnabled: &falseValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + }, + { + description: "Enforce - TCF 1; account GDPR not specified, host GDPR enabled", + gdprAccountEnabled: nil, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }, + }, + { + description: "Not Enforce - TCF 1; account GDPR not specified, host GDPR disabled", + gdprAccountEnabled: nil, + gdprHostEnabled: false, + gdpr: "1", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, + expectPrivacyLabels: pbsmetrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + }, + } + + for _, test := range testCases { + req := newBidRequest(t) + req.User.Ext = json.RawMessage(`{"consent":"` + test.gdprConsent + `"}`) + req.Regs = &openrtb.Regs{ + Ext: json.RawMessage(`{"gdpr":` + test.gdpr + `}`), + } + + privacyConfig := config.Privacy{ + GDPR: config.GDPR{ + Enabled: test.gdprHostEnabled, + TCF2: config.TCF2{ + Enabled: true, + }, + }, + } + + accountConfig := config.Account{ + GDPR: config.AccountGDPR{ + Enabled: test.gdprAccountEnabled, + }, + } + + results, _, privacyLabels, errs := cleanOpenRTBRequests( + context.Background(), + req, + nil, + &emptyUsersync{}, + map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, + pbsmetrics.Labels{}, + &permissionsMock{personalInfoAllowed: !test.gdprScrub}, + true, + privacyConfig, + &accountConfig) + result := results["appnexus"] + + assert.Nil(t, errs) + if test.gdprScrub { + 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") + } + assert.Equal(t, test.expectPrivacyLabels, privacyLabels, test.description+":PrivacyLabels") } } @@ -266,6 +1242,7 @@ func newBidRequest(t *testing.T) *openrtb.BidRequest { User: &openrtb.User{ ID: "our-id", BuyerUID: "their-id", + Yob: 1982, Ext: json.RawMessage(`{"digitrust":{"id":"digi-id","keyv":1,"pref":1}}`), }, Imp: []openrtb.Imp{{ @@ -302,5 +1279,99 @@ func TestRandomizeList(t *testing.T) { if len(adapters) != 1 { t.Errorf("RandomizeList, expected a list of 1, found %d", len(adapters)) } +} + +func TestBidderToPrebidChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{ + { + Bidders: []string{"Bidder1", "Bidder2"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{ + Complete: 1, + Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{ + { + ASI: "asi1", + SID: "sid1", + Name: "name1", + RID: "rid1", + Domain: "domain1", + HP: 1, + }, + { + ASI: "asi2", + SID: "sid2", + Name: "name2", + RID: "rid2", + Domain: "domain2", + HP: 2, + }, + }, + Ver: "version1", + }, + }, + { + Bidders: []string{"Bidder3", "Bidder4"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + }, + }, + } + + output, err := BidderToPrebidSChains(&input) + + assert.Nil(t, err) + assert.Equal(t, len(output), 4) + assert.Same(t, output["Bidder1"], &input.Prebid.SChains[0].SChain) + assert.Same(t, output["Bidder2"], &input.Prebid.SChains[0].SChain) + assert.Same(t, output["Bidder3"], &input.Prebid.SChains[1].SChain) + assert.Same(t, output["Bidder4"], &input.Prebid.SChains[1].SChain) +} + +func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{ + { + Bidders: []string{"Bidder1"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + { + Bidders: []string{"Bidder1", "Bidder2"}, + SChain: openrtb_ext.ExtRequestPrebidSChainSChain{}, + }, + }, + }, + } + + output, err := BidderToPrebidSChains(&input) + + assert.NotNil(t, err) + assert.Nil(t, output) +} + +func TestBidderToPrebidChainsNilSChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: nil, + }, + } + + output, err := BidderToPrebidSChains(&input) + + assert.Nil(t, err) + assert.Equal(t, len(output), 0) +} + +func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) { + input := openrtb_ext.ExtRequest{ + Prebid: openrtb_ext.ExtRequestPrebid{ + SChains: []*openrtb_ext.ExtRequestPrebidSChain{}, + }, + } + + output, err := BidderToPrebidSChains(&input) + assert.Nil(t, err) + assert.Equal(t, len(output), 0) } diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index b4cb336986a..eab8e4bd8d1 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -23,15 +23,16 @@ type Permissions interface { // Determines whether or not to send PI information to a bidder, or mask it out. // // 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) + PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) // Exposes the AMP execption flag AMPException() bool } +// Versions of the GDPR TCF technical specification. const ( - tCF1 uint8 = 1 - tCF2 uint8 = 2 + tcf1SpecVersion uint8 = 1 + tcf2SpecVersion uint8 = 2 ) // NewPermissions gets an instance of the Permissions for use elsewhere in the project. @@ -45,8 +46,8 @@ func NewPermissions(ctx context.Context, cfg config.GDPR, vendorIDs map[openrtb_ 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)}, + tcf1SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tcf1SpecVersion), + tcf2SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tcf2SpecVersion)}, } } diff --git a/gdpr/impl.go b/gdpr/impl.go index 7fa6fde588f..8fe8908ab40 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -42,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, bool, error) { +func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { _, ok := p.cfg.NonStandardPublisherMap[PublisherID] if ok { - return true, true, nil + return true, true, true, nil } id, ok := p.vendorIDs[bidder] @@ -54,10 +54,10 @@ func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrt } if consent == "" { - return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } - return false, false, nil + return false, false, false, nil } func (p *permissionsImpl) AMPException() bool { @@ -98,19 +98,19 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen return false, nil } -func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, error) { +func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, bool, error) { // If we're not given a consent string, respect the preferences in the app config. if consent == "" { - return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil + return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil } parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent) if err != nil { - return false, false, err + return false, false, false, err } if vendor == nil { - return false, false, nil + return false, false, false, nil } if parsedConsent.Version() == 2 { @@ -118,21 +118,22 @@ func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent 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 + return true, 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 true, true, true, nil } } - return false, false, nil + return false, false, false, nil } -func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16) (allowPI bool, allowGeo bool, err error) { +func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16) (allowPI bool, allowGeo bool, allowID bool, err error) { consent, ok := parsedConsent.(tcf2.ConsentMetadata) err = nil allowPI = false allowGeo = false + allowID = false if !ok { err = fmt.Errorf("Unable to access TCF2 parsed consent") return @@ -142,6 +143,12 @@ func (p *permissionsImpl) allowPITCF2(parsedConsent api.VendorConsents, vendor a } else { allowGeo = true } + for i := 2; i <= 10; i++ { + if p.checkPurpose(consent, vendor, vendorID, tcf1constants.Purpose(i)) { + allowID = true + break + } + } // Set to true so any purpose check can flip it to false allowPI = true if p.cfg.TCF2.Purpose1.Enabled { @@ -214,10 +221,29 @@ 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, bool, error) { - return true, true, nil +func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return true, true, true, nil } func (a AlwaysAllow) AMPException() bool { return false } + +// Exporting to allow for easy test setups +type AlwaysFail struct{} + +func (a AlwaysFail) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { + return false, nil +} + +func (a AlwaysFail) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName, consent string) (bool, error) { + return false, nil +} + +func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { + return false, false, false, nil +} + +func (a AlwaysFail) AMPException() bool { + return false +} diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 0635ee4e512..b65e9cf4824 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -23,8 +23,8 @@ func TestNoConsentButAllowByDefault(t *testing.T) { }, vendorIDs: nil, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: failedListFetcher, - tCF2: failedListFetcher, + tcf1SpecVersion: failedListFetcher, + tcf2SpecVersion: failedListFetcher, }, } allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "") @@ -43,8 +43,8 @@ func TestNoConsentAndRejectByDefault(t *testing.T) { }, vendorIDs: nil, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: failedListFetcher, - tCF2: failedListFetcher, + tcf1SpecVersion: failedListFetcher, + tcf2SpecVersion: failedListFetcher, }, } allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "") @@ -56,12 +56,11 @@ func TestNoConsentAndRejectByDefault(t *testing.T) { } func TestAllowedSyncs(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, - }, - 3: { - purposes: []int{1}, + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, + {ID: 3, Purposes: []int{1}}, }, }) perms := permissionsImpl{ @@ -73,10 +72,10 @@ func TestAllowedSyncs(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), }, @@ -92,12 +91,11 @@ func TestAllowedSyncs(t *testing.T) { } func TestProhibitedPurposes(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, // cookie reads/writes - }, - 3: { - purposes: []int{3}, // ad personalization + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, // cookie reads/writes + {ID: 3, Purposes: []int{3}}, // ad personalization }, }) perms := permissionsImpl{ @@ -109,10 +107,10 @@ func TestProhibitedPurposes(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), }, @@ -128,12 +126,11 @@ func TestProhibitedPurposes(t *testing.T) { } func TestProhibitedVendors(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, // cookie reads/writes - }, - 3: { - purposes: []int{3}, // ad personalization + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, // cookie reads/writes + {ID: 3, Purposes: []int{3}}, // ad personalization }, }) perms := permissionsImpl{ @@ -145,10 +142,10 @@ func TestProhibitedVendors(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), }, @@ -169,8 +166,8 @@ func TestMalformedConsent(t *testing.T) { HostVendorID: 2, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(nil), - tCF2: listFetcher(nil), + tcf1SpecVersion: listFetcher(nil), + tcf2SpecVersion: listFetcher(nil), }, } @@ -180,12 +177,11 @@ func TestMalformedConsent(t *testing.T) { } func TestAllowPersonalInfo(t *testing.T) { - vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{ - 2: { - purposes: []int{1}, // cookie reads/writes - }, - 3: { - purposes: []int{1, 3}, // ad personalization + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{ + {ID: 2, Purposes: []int{1}}, // cookie reads/writes + {ID: 3, Purposes: []int{1, 3}}, // ad personalization }, }) perms := permissionsImpl{ @@ -197,49 +193,64 @@ func TestAllowPersonalInfo(t *testing.T) { openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 1: parseVendorListData(t, vendorListData), }), - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf2SpecVersion: 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") + perms.cfg.NonStandardPublisherMap = map[string]struct{}{"appNexusAppID": {}} + 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}}, +func buildTCF2VendorList34() tcf2VendorList { + return tcf2VendorList{ + VendorListVersion: 2, + Vendors: map[string]*tcf2Vendor{ + "2": { + ID: 2, + Purposes: []int{1}, + }, + "6": { + ID: 6, + Purposes: []int{1, 2, 4}, + LegIntPurposes: []int{7}, + SpecialPurposes: []int{1}, + FlexiblePurposes: []int{1, 2, 4, 7}, + }, + "8": { + ID: 8, + Purposes: []int{1, 7}, + LegIntPurposes: []int{2, 4}, + }, + "10": { + ID: 10, + Purposes: []int{2, 4, 7}, + SpecialPurposes: []int{1}, + }, + "32": { + ID: 32, + Purposes: []int{1, 2, 4, 7}, + }, + }, + } } + var tcf2Config = config.GDPR{ HostVendorID: 2, TCF2: config.TCF2{ @@ -257,10 +268,11 @@ type tcf2TestDef struct { consent string allowPI bool allowGeo bool + allowID bool } func TestAllowPersonalInfoTCF2(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -269,14 +281,14 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, } - // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes and vendors 2, 6, 8 // PI needs all purposes to succeed testDefs := []tcf2TestDef{ { @@ -285,6 +297,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -292,6 +305,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", allowPI: true, allowGeo: true, + allowID: true, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -299,19 +313,21 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", allowPI: true, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowGeo failure on %s", td.description) } } func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -320,23 +336,23 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: 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") + perms.cfg.NonStandardPublisherMap = map[string]struct{}{"appNexusAppID": {}} + allowPI, allowGeo, allowID, 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") - + assert.EqualValuesf(t, true, allowID, "AllowID failure") } func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -345,8 +361,8 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 15: parseVendorListDataV2(t, vendorListData), }), }, @@ -361,6 +377,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -368,6 +385,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -375,19 +393,21 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { consent: "COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA", allowPI: false, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowPI failure on %s", td.description) } } func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -396,8 +416,8 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -413,6 +433,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -420,6 +441,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: true, allowGeo: true, + allowID: true, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -427,19 +449,21 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: true, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowID failure on %s", td.description) } } func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -448,8 +472,8 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -458,6 +482,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { perms.cfg.TCF2.PurposeOneTreatment.AccessAllowed = false // COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA Purpose one flag set + // Purpose one treatment will fail PI, but allow passing the IDs. testDefs := []tcf2TestDef{ { description: "Appnexus vendor test, insufficient purposes claimed", @@ -465,6 +490,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: false, + allowID: false, }, { description: "Pubmatic vendor test, flex purposes claimed", @@ -472,6 +498,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: true, + allowID: true, }, { description: "Rubicon vendor test, Specific purposes/LIs claimed, no geo claimed", @@ -479,19 +506,21 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { consent: "COzqiL3OzqiL3NIAAAENAiCMAP_AAH_AAIAAAQEX2S5MAICL7JcmAAA", allowPI: false, allowGeo: false, + allowID: true, }, } for _, td := range testDefs { - allowPI, allowGeo, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, 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) + assert.EqualValuesf(t, td.allowID, allowID, "AllowID failure on %s", td.description) } } func TestAllowSyncTCF2(t *testing.T) { - vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -500,8 +529,8 @@ func TestAllowSyncTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), }, @@ -518,9 +547,9 @@ func TestAllowSyncTCF2(t *testing.T) { } func TestProhibitedPurposeSyncTCF2(t *testing.T) { - basicPurposes := tcf2BasicPurposes - basicPurposes[8] = &purposes{purposes: []int{7}} - vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + tcf2VendorList34 := buildTCF2VendorList34() + tcf2VendorList34.Vendors["8"].Purposes = []int{7} + vendorListData := tcf2MarshalVendorList(tcf2VendorList34) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -529,15 +558,15 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) { openrtb_ext.BidderRubicon: 8, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: 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 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for 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") @@ -548,9 +577,7 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) { } func TestProhibitedVendorSyncTCF2(t *testing.T) { - basicPurposes := tcf2BasicPurposes - basicPurposes[10] = &purposes{purposes: []int{1}} - vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses) + vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) perms := permissionsImpl{ cfg: tcf2Config, vendorIDs: map[openrtb_ext.BidderName]uint16{ @@ -560,20 +587,21 @@ func TestProhibitedVendorSyncTCF2(t *testing.T) { openrtb_ext.BidderOpenx: 10, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tCF1: nil, - tCF2: listFetcher(map[uint16]vendorlist.VendorList{ + tcf1SpecVersion: nil, + tcf2SpecVersion: 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 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for 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") + // Permission disallowed due to consent string not including vendor 10. + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderOpenx, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") assert.NoErrorf(t, err, "Error processing BidderSyncAllowed") assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure") } diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index 5cbcbfac784..42480041bc1 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -26,52 +26,83 @@ type saveVendors func(uint16, api.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, 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() +func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16, uint8) string, tcfSpecVersion uint8) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { + var fallback api.VendorList + if tcfSpecVersion == tcf1SpecVersion && len(cfg.TCF1.FallbackGVLPath) > 0 { + fallback = loadFallbackGVL(cfg.TCF1.FallbackGVLPath) + } - withTimeout, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) - defer cancel() - populateCache(withTimeout, client, urlMaker, save, TCFVer) + // If we are not going to try fetching the GVL dynamically, we have a simple fetcher. + if !cfg.TCF1.FetchGVL && tcfSpecVersion == tcf1SpecVersion { + if fallback != nil { + return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { + return fallback, nil + } + } + return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { + return nil, makeVendorListNotFoundError(vendorListVersion) + } + } + + cacheSave, cacheLoad := newVendorListCache(fallback) - saveOneSometimes := newOccasionalSaver(cfg.Timeouts.ActiveTimeout(), TCFVer) + preloadContext, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) + defer cancel() + preloadCache(preloadContext, client, urlMaker, cacheSave, tcfSpecVersion) - return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { - list := load(id) - if list != nil { + saveOneRateLimited := newOccasionalSaver(cfg.Timeouts.ActiveTimeout(), tcfSpecVersion) + return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { + // Attempt To Load From Cache + if list := cacheLoad(vendorListVersion); list != nil { return list, nil } - saveOneSometimes(ctx, client, urlMaker(id, TCFVer), save) - list = load(id) - if list != nil { + + // Attempt To Download + // - May not add to cache immediately. + saveOneRateLimited(ctx, client, urlMaker(vendorListVersion, tcfSpecVersion), cacheSave) + + // Attempt To Load From Cache Again + // - May have been added by the call to saveOneRateLimited. + if list := cacheLoad(vendorListVersion); list != nil { return list, nil } - return nil, fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", id) + + // Attempt To Use Hardcoded Fallback + if fallback != nil { + return fallback, nil + } + + // Give Up + return nil, makeVendorListNotFoundError(vendorListVersion) } } -// populateCache saves all the known versions of the vendor list for future use. -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) +func makeVendorListNotFoundError(vendorListVersion uint16) error { + return fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", vendorListVersion) +} + +// preloadCache saves all the known versions of the vendor list for future use. +func preloadCache(ctx context.Context, client *http.Client, urlMaker func(uint16, uint8) string, saver saveVendors, tcfSpecVersion uint8) { + latestVersion := saveOne(ctx, client, urlMaker(0, tcfSpecVersion), saver, tcfSpecVersion) for i := uint16(1); i < latestVersion; i++ { - saveOne(ctx, client, urlMaker(i, TCFVer), saver, TCFVer) + saveOne(ctx, client, urlMaker(i, tcfSpecVersion), saver, tcfSpecVersion) } } // 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, TCFVer uint8) string { - if TCFVer == 2 { - if version == 0 { +func vendorListURLMaker(vendorListVersion uint16, tcfSpecVersion uint8) string { + if tcfSpecVersion == tcf2SpecVersion { + if vendorListVersion == 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" + return "https://vendorlist.consensu.org/v2/archives/vendor-list-v" + strconv.Itoa(int(vendorListVersion)) + ".json" } - if version == 0 { + if vendorListVersion == 0 { return "https://vendorlist.consensu.org/vendorlist.json" } - return "https://vendorlist.consensu.org/v-" + strconv.Itoa(int(version)) + "/vendorlist.json" + return "https://vendorlist.consensu.org/v-" + strconv.Itoa(int(vendorListVersion)) + "/vendorlist.json" } // newOccasionalSaver returns a wrapped version of saveOne() which only activates every few minutes. @@ -79,22 +110,24 @@ func vendorListURLMaker(version uint16, TCFVer uint8) 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, TCFVer uint8) func(ctx context.Context, client *http.Client, url string, saver saveVendors) { +func newOccasionalSaver(timeout time.Duration, tcfSpecVersion uint8) func(ctx context.Context, client *http.Client, url string, saver saveVendors) { lastSaved := &atomic.Value{} lastSaved.Store(time.Time{}) return func(ctx context.Context, client *http.Client, url string, saver saveVendors) { now := time.Now() - if now.Sub(lastSaved.Load().(time.Time)).Minutes() > 10 { + timeSinceLastSave := now.Sub(lastSaved.Load().(time.Time)) + + if timeSinceLastSave.Minutes() > 10 { withTimeout, cancel := context.WithTimeout(ctx, timeout) defer cancel() - saveOne(withTimeout, client, url, saver, TCFVer) + saveOne(withTimeout, client, url, saver, tcfSpecVersion) lastSaved.Store(now) } } } -func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors, cTFVer uint8) uint16 { +func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors, tcfSpecVersion 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) @@ -118,7 +151,7 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen return 0 } var newList api.VendorList - if cTFVer == 2 { + if tcfSpecVersion == tcf2SpecVersion { newList, err = vendorlist2.ParseEagerly(respBody) } else { newList, err = vendorlist.ParseEagerly(respBody) @@ -132,14 +165,15 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen return newList.Version() } -func newVendorListCache() (save func(id uint16, list api.VendorList), load func(id uint16) api.VendorList) { +func newVendorListCache(fallbackVL api.VendorList) (save func(vendorListVersion uint16, list api.VendorList), load func(vendorListVersion uint16) api.VendorList) { cache := &sync.Map{} - save = func(id uint16, list api.VendorList) { - cache.Store(id, list) + save = func(vendorListVersion uint16, list api.VendorList) { + cache.Store(vendorListVersion, list) } - load = func(id uint16) api.VendorList { - list, ok := cache.Load(id) + + load = func(vendorListVersion uint16) api.VendorList { + list, ok := cache.Load(vendorListVersion) if ok { return list.(vendorlist.VendorList) } @@ -147,3 +181,16 @@ func newVendorListCache() (save func(id uint16, list api.VendorList), load func( } return } + +func loadFallbackGVL(fallbackGVLPath string) vendorlist.VendorList { + fallbackContents, err := ioutil.ReadFile(fallbackGVLPath) + if err != nil { + glog.Fatalf("Error reading from file %s: %v", fallbackGVLPath, err) + } + + fallback, err := vendorlist.ParseEagerly(fallbackContents) + if err != nil { + glog.Fatalf("Error processing default GVL from %s: %v", fallbackGVLPath, err) + } + return fallback +} diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index 32d7ef351b3..6329d8fb69c 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -7,136 +7,697 @@ import ( "net/http/httptest" "strconv" "testing" - "time" + + "github.com/stretchr/testify/assert" "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/prebid/go-gdpr/consentconstants" ) -func TestVendorFetch(t *testing.T) { - vendorListOne := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, +func TestTCF1FetcherInitialLoad(t *testing.T) { + // Loads two vendor lists during initialization by setting the latest vendor list version to 2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 2, + vendorLists: map[int]string{ + 1: tcf1VendorList1, + 2: tcf1VendorList2, + }, + }))) + defer server.Close() + + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, }, - }) - vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2, 3}, + { + description: "No Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } + + for _, test := range testCases { + runTest(t, test, tcf1SpecVersion, server) + } +} + +func TestTCF2FetcherInitialLoad(t *testing.T) { + // Loads two vendor lists during initialization by setting the latest vendor list version to 2. + // Ensures TCF1 fetch settings have no effect on TCF2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 2, + vendorLists: map[int]string{ + 1: tcf2VendorList1, + 2: tcf2VendorList2, }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{ - 1: vendorListOne, - 2: vendorListTwo, }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - list, err := fetcher(context.Background(), 1) - assertNilErr(t, err) - vendor := list.Vendor(32) - assertBoolsEqual(t, true, vendor.Purpose(1)) - assertBoolsEqual(t, false, vendor.Purpose(3)) - assertBoolsEqual(t, false, vendor.Purpose(4)) - - list, err = fetcher(context.Background(), 2) - assertNilErr(t, err) - vendor = list.Vendor(32) - assertBoolsEqual(t, true, vendor.Purpose(1)) - assertBoolsEqual(t, true, vendor.Purpose(3)) -} - -func TestLazyFetch(t *testing.T) { - firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, - }, - }) - secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{ - 3: { - purposes: []int{1}, - }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: firstVendorList, - 2: secondVendorList, + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - No Fallback - Vendor List 1", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 1, + }, + expected: vendorList1Expected, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + } + + for _, test := range testCases { + runTest(t, test, tcf2SpecVersion, server) + } +} + +func TestTCF1FetcherDynamicLoadListExists(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor lists will be dynamically loaded. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf1VendorList1, + 2: tcf1VendorList2, + }, }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - list, err := fetcher(context.Background(), 2) - assertNilErr(t, err) + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } - vendor := list.Vendor(3) - assertBoolsEqual(t, true, vendor.Purpose(1)) - assertBoolsEqual(t, false, vendor.Purpose(2)) + for _, test := range testCases { + runTest(t, test, tcf1SpecVersion, server) + } } -func TestInitialTimeout(t *testing.T) { - list := mockVendorListData(t, 1, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, +func TestTCF2FetcherDynamicLoadListExists(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor lists will be dynamically loaded. + // Ensures TCF1 fetch settings have no effect on TCF2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf2VendorList1, + 2: tcf2VendorList2, }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: list, }))) defer server.Close() - ctx, cancel := context.WithDeadline(context.Background(), time.Time{}) - defer cancel() - 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) + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + { + description: "No Fetch - No Fallback - Vendor List 2", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: vendorList2Expected, + }, + } + + for _, test := range testCases { + runTest(t, test, tcf2SpecVersion, server) + } } -func TestFetchThrottling(t *testing.T) { - vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, +func TestTCF1FetcherDynamicLoadListDoesntExist(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor list load attempts will be done dynamically. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf1VendorList1, + }, + }))) + defer server.Close() + + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, + }, + { + description: "No Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: vendorListFallbackExpected, }, - }) - vendorListThree := mockVendorListData(t, 3, map[uint16]*purposes{ - 32: { - purposes: []int{1, 2}, + { + description: "No Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } + + for _, test := range testCases { + runTest(t, test, 1, server) + } +} + +func TestTCF2FetcherDynamicLoadListDoesntExist(t *testing.T) { + // Loads the first vendor list during initialization by setting the latest vendor list version to 1. + // All other vendor list load attempts will be done dynamically. + // Ensures TCF1 fetch settings have no effect on TCF2. + + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf2VendorList1, }, - }) - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{ - 1: "{}", - 2: vendorListTwo, - 3: vendorListThree, }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - _, err := fetcher(context.Background(), 2) - assertNilErr(t, err) - _, err = fetcher(context.Background(), 3) - assertErr(t, err, false) + testCases := []test{ + { + description: "Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: true, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "No Fetch - Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: true, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + { + description: "No Fetch - No Fallback - Vendor Doesn't Exist", + setup: testSetup{ + enableTCF1Fetch: false, + enableTCF1Fallback: false, + vendorListVersion: 2, + }, + expected: testExpected{ + errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes", + }, + }, + } + + for _, test := range testCases { + runTest(t, test, tcf2SpecVersion, server) + } +} + +func TestTCF1FetcherThrottling(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{1}}}, + }), + 2: tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 2, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{1, 2}}}, + }), + 3: tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 3, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{1, 2, 3}}}, + }), + }, + }))) + defer server.Close() + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion) + + // Dynamically Load List 2 Successfully + _, errList1 := fetcher(context.Background(), 2) + assert.NoError(t, errList1) + + // Fail To Load List 3 Due To Rate Limiting + // - The request is rate limited after dynamically list 2. + _, errList2 := fetcher(context.Background(), 3) + assert.EqualError(t, errList2, "gdpr vendor list version 3 does not exist, or has not been loaded yet. Try again in a few minutes") } -func TestMalformedVendorlistFetch(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"}))) +func TestTCF2FetcherThrottling(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 1, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1}}}, + }), + 2: tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 2, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2}}}, + }), + 3: tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 3, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}}, + }), + }, + }))) + defer server.Close() + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion) + + // Dynamically Load List 2 Successfully + _, errList1 := fetcher(context.Background(), 2) + assert.NoError(t, errList1) + + // Fail To Load List 3 Due To Rate Limiting + // - The request is rate limited after dynamically list 2. + _, errList2 := fetcher(context.Background(), 3) + assert.EqualError(t, errList2, "gdpr vendor list version 3 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF1MalformedVendorlist(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: "malformed", + }, + }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion) _, err := fetcher(context.Background(), 1) - assertErr(t, err, false) + + // Fetching should fail since vendor list could not be unmarshalled. + assert.Error(t, err) } -func TestMissingVendorlistFetch(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"}))) +func TestTCF2MalformedVendorlist(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ + vendorListLatestVersion: 1, + vendorLists: map[int]string{ + 1: "malformed", + }, + }))) defer server.Close() - fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1) - _, err := fetcher(context.Background(), 2) - assertErr(t, err, false) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion) + _, err := fetcher(context.Background(), 1) + + // Fetching should fail since vendor list could not be unmarshalled. + assert.Error(t, err) +} + +func TestTCF1ServerUrlInvalid(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + invalidURLGenerator := func(uint16, uint8) string { return " http://invalid-url-has-leading-whitespace" } + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator, tcf1SpecVersion) + _, err := fetcher(context.Background(), 1) + + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF2ServerUrlInvalid(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + invalidURLGenerator := func(uint16, uint8) string { return " http://invalid-url-has-leading-whitespace" } + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator, tcf2SpecVersion) + _, err := fetcher(context.Background(), 1) + + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF1ServerUnavailable(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion) + _, err := fetcher(context.Background(), 1) + + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestTCF2ServerUnavailable(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Close() + + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion) + _, err := fetcher(context.Background(), 1) + + assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes") +} + +func TestVendorListURLMaker(t *testing.T) { + testCases := []struct { + description string + tcfSpecVersion uint8 + vendorListVersion uint16 + expectedURL string + }{ + { + description: "TCF1 - Latest", + tcfSpecVersion: 1, + vendorListVersion: 0, // Forces latest version. + expectedURL: "https://vendorlist.consensu.org/vendorlist.json", + }, + { + description: "TCF1 - Specific", + tcfSpecVersion: 1, + vendorListVersion: 42, + expectedURL: "https://vendorlist.consensu.org/v-42/vendorlist.json", + }, + { + description: "TCF2 - Latest", + tcfSpecVersion: 2, + vendorListVersion: 0, // Forces latest version. + expectedURL: "https://vendorlist.consensu.org/v2/vendor-list.json", + }, + { + description: "TCF2 - Specific", + tcfSpecVersion: 2, + vendorListVersion: 42, + expectedURL: "https://vendorlist.consensu.org/v2/archives/vendor-list-v42.json", + }, + } + + for _, test := range testCases { + result := vendorListURLMaker(test.vendorListVersion, test.tcfSpecVersion) + assert.Equal(t, test.expectedURL, result) + } +} + +var tcf1VendorList1 = tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 1, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{2}}}, +}) + +var tcf2VendorList1 = tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 1, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{2}}}, +}) + +var vendorList1Expected = testExpected{ + vendorListVersion: 1, + vendorID: 12, + vendorPurposes: map[int]bool{1: false, 2: true, 3: false}, +} + +var tcf1VendorList2 = tcf1MarshalVendorList(tcf1VendorList{ + VendorListVersion: 2, + Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{2, 3}}}, +}) + +var tcf2VendorList2 = tcf2MarshalVendorList(tcf2VendorList{ + VendorListVersion: 2, + Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{2, 3}}}, +}) + +var vendorList2Expected = testExpected{ + vendorListVersion: 2, + vendorID: 12, + vendorPurposes: map[int]bool{1: false, 2: true, 3: true}, } -func TestVendorListMaker(t *testing.T) { - 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)) +var vendorListFallbackExpected = testExpected{ + vendorListVersion: 215, // Values from hardcoded fallback file. + vendorID: 12, + vendorPurposes: map[int]bool{1: true, 2: false, 3: true}, +} + +type tcf1VendorList struct { + VendorListVersion uint16 `json:"vendorListVersion"` + Vendors []tcf1Vendor `json:"vendors"` +} + +type tcf1Vendor struct { + ID uint16 `json:"id"` + Purposes []int `json:"purposeIds"` +} + +func tcf1MarshalVendorList(vendorList tcf1VendorList) string { + json, _ := json.Marshal(vendorList) + return string(json) +} + +type tcf2VendorList struct { + VendorListVersion uint16 `json:"vendorListVersion"` + Vendors map[string]*tcf2Vendor `json:"vendors"` +} + +type tcf2Vendor struct { + ID uint16 `json:"id"` + Purposes []int `json:"purposes"` + LegIntPurposes []int `json:"legIntPurposes"` + FlexiblePurposes []int `json:"flexiblePurposes"` + SpecialPurposes []int `json:"specialPurposes"` +} + +func tcf2MarshalVendorList(vendorList tcf2VendorList) string { + json, _ := json.Marshal(vendorList) + return string(json) +} + +type serverSettings struct { + vendorListLatestVersion int + vendorLists map[int]string } // mockServer returns a handler which returns the given response for each global vendor list version. @@ -150,129 +711,74 @@ func TestVendorListMaker(t *testing.T) { // // If the "version" query param points to a version which doesn't exist, it returns a 403. // Don't ask why... that's just what the official page is doing. See https://vendorlist.consensu.org/v-9999/vendorlist.json -func mockServer(latestVersion int, responses map[int]string) func(http.ResponseWriter, *http.Request) { +func mockServer(settings serverSettings) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, req *http.Request) { - version := req.URL.Query().Get("version") - versionInt, err := strconv.Atoi(version) + vendorListVersion := req.URL.Query().Get("version") + vendorListVersionInt, err := strconv.Atoi(vendorListVersion) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Request had invalid version: " + version)) + w.Write([]byte("Request had invalid version: " + vendorListVersion)) return } - if versionInt == 0 { - versionInt = latestVersion + if vendorListVersionInt == 0 { + vendorListVersionInt = settings.vendorListLatestVersion } - response, ok := responses[versionInt] + response, ok := settings.vendorLists[vendorListVersionInt] if !ok { w.WriteHeader(http.StatusForbidden) - w.Write([]byte("Version not found: " + version)) + w.Write([]byte("Version not found: " + vendorListVersion)) return } w.Write([]byte(response)) } } -func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purposes) string { - type vendorContract struct { - ID uint16 `json:"id"` - Purposes []int `json:"purposeIds"` - } - - type vendorListContract struct { - Version uint16 `json:"vendorListVersion"` - Vendors []vendorContract `json:"vendors"` - } - - buildVendors := func(input map[uint16]*purposes) []vendorContract { - vendors := make([]vendorContract, 0, len(input)) - for id, purpose := range input { - vendors = append(vendors, vendorContract{ - ID: id, - Purposes: purpose.purposes, - }) - } - return vendors - } - - obj := vendorListContract{ - Version: version, - Vendors: buildVendors(vendors), - } - data, err := json.Marshal(obj) - assertNilErr(t, err) - return string(data) +type test struct { + description string + setup testSetup + expected testExpected } -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 - } +type testSetup struct { + enableTCF1Fetch bool + enableTCF1Fallback bool + vendorListVersion uint16 +} - 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 - } +type testExpected struct { + errorMessage string + vendorListVersion uint16 + vendorID uint16 + vendorPurposes map[int]bool +} - 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 +func runTest(t *testing.T, test test, tcfSpecVersion uint8, server *httptest.Server) { + config := testConfig() + config.TCF1.FetchGVL = test.setup.enableTCF1Fetch + if test.setup.enableTCF1Fallback { + config.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" } - for id, purpose := range specialPurposes { - sid := strconv.Itoa(int(id)) - vendor, ok := vendors[sid] - if !ok { - vendor = vendorContract{ID: id} + fetcher := newVendorListFetcher(context.Background(), config, server.Client(), testURLMaker(server), tcfSpecVersion) + vendorList, err := fetcher(context.Background(), test.setup.vendorListVersion) + + if test.expected.errorMessage != "" { + assert.EqualError(t, err, test.expected.errorMessage, test.description+":error") + } else { + assert.NoError(t, err, test.description+":vendorlist") + assert.Equal(t, test.expected.vendorListVersion, vendorList.Version(), test.description+":vendorlistid") + vendor := vendorList.Vendor(test.expected.vendorID) + for id, expected := range test.expected.vendorPurposes { + result := vendor.Purpose(consentconstants.Purpose(id)) + assert.Equalf(t, expected, result, "%s:vendor-%d:purpose-%d", test.description, vendorList.Version(), 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, TCFVer uint8) string { - return url + "?version=" + strconv.Itoa(int(version)) + return func(vendorListVersion uint16, tcfSpecVersion uint8) string { + return url + "?version=" + strconv.Itoa(int(vendorListVersion)) } } @@ -282,9 +788,8 @@ func testConfig() config.GDPR { InitVendorlistFetch: 60 * 1000, ActiveVendorlistFetch: 1000 * 5, }, + TCF1: config.TCF1{ + FetchGVL: true, + }, } } - -type purposes struct { - purposes []int -} diff --git a/go.mod b/go.mod index 949125b8594..41bd4a9384f 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/cespare/xxhash v1.0.0 // indirect github.com/chasex/glog v0.0.0-20160217080310-c62392af379c github.com/coocood/freecache v1.0.1 + github.com/docker/go-units v0.4.0 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd github.com/gofrs/uuid v3.2.0+incompatible @@ -33,7 +34,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.8.2 + github.com/prebid/go-gdpr v0.8.3 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 diff --git a/go.sum b/go.sum index 98713ba6857..37a7df16073 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,7 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLM github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/buger/jsonparser v0.0.0-20180318095312-2cac668e8456/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44 h1:y853v6rXx+zefEcjET3JuKAqvhj+FKflQijjeaSv2iA= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cespare/xxhash v1.0.0 h1:naDmySfoNg0nKS62/ujM6e71ZgM2AoVdaqGwMG0w18A= @@ -27,6 +28,8 @@ github.com/coocood/freecache v1.0.1/go.mod h1:ePwxCDzOYvARfHdr1pByNct1at3CoKnsip github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd h1:biTJQdqouE5by89AAffXG8++TY+9Fsdrg5rinbt3tHk= @@ -72,8 +75,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.8.2 h1:mN2jKYZZpJkCYFQB/nDTJoPpuGYblOYP2UUzOzRggII= -github.com/prebid/go-gdpr v0.8.2/go.mod h1:FPY0uxSrl9/Mz237LnPo3ge4aCG0wQ9FWf2b4WhwNn0= +github.com/prebid/go-gdpr v0.8.3 h1:rjCZNV0AdKygiGHpVhNB42usjEpTN3qidXUPB1yarb0= +github.com/prebid/go-gdpr v0.8.3/go.mod h1:TGzgqQDGKOVUkbqmY25K4uvcwMywSddXEaY4zUFiVBQ= 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= diff --git a/macros/macros.go b/macros/macros.go index a9f77ea95fa..5d6bd7af65e 100644 --- a/macros/macros.go +++ b/macros/macros.go @@ -12,6 +12,7 @@ type EndpointTemplateParams struct { ZoneID string SourceId string AccountID string + AdUnit string } // UserSyncTemplateParams specifies params for an user sync URL template diff --git a/main.go b/main.go index 802714590e0..7312fc99c98 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( pbc "github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client" "github.com/PubMatic-OpenWrap/prebid-server/router" "github.com/PubMatic-OpenWrap/prebid-server/usersync" - "github.com/julienschmidt/httprouter" + "github.com/PubMatic-OpenWrap/prebid-server/util/task" "github.com/golang/glog" "github.com/spf13/viper" @@ -75,7 +75,11 @@ func loadConfig(configFileName string) (*config.Configuration, error) { func serve(revision string, cfg *config.Configuration) error { fetchingInterval := time.Duration(cfg.CurrencyConverter.FetchIntervalSeconds) * time.Second - currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL, fetchingInterval) + staleRatesThreshold := time.Duration(cfg.CurrencyConverter.StaleRatesSeconds) * time.Second + currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL, staleRatesThreshold) + + currencyConverterTickerTask := task.NewTickerTask(fetchingInterval, currencyConverter) + currencyConverterTickerTask.Start() _, err := router.New(cfg, currencyConverter) if err != nil { @@ -85,9 +89,9 @@ func serve(revision string, cfg *config.Configuration) error { pbc.InitPrebidCache(cfg.CacheURL.GetBaseURL()) pbc.InitPrebidCacheURL(cfg.ExternalURL) - // Add cors support //corsRouter := router.SupportCORS(r) - //server.Listen(cfg, router.NoCache{Handler: corsRouter}, router.Admin(revision, currencyConverter), r.MetricsEngine) + //server.Listen(cfg, router.NoCache{Handler: corsRouter}, router.Admin(revision, currencyConverter, fetchingInterval), r.MetricsEngine) + //r.Shutdown() return nil } @@ -112,7 +116,7 @@ func SetUIDS(w http.ResponseWriter, r *http.Request) { router.SetUIDSWrapper(w, r) } -func CookieSync(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func CookieSync(w http.ResponseWriter, r *http.Request) { router.CookieSync(w, r) } diff --git a/main_test.go b/main_test.go index 7888d85062f..f3b6748ba48 100644 --- a/main_test.go +++ b/main_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/stretchr/testify/assert" "github.com/spf13/viper" ) @@ -56,10 +57,11 @@ func TestViperEnv(t *testing.T) { ttl := forceEnv(t, "PBS_HOST_COOKIE_TTL_DAYS", "60") defer ttl() - // Basic config set - compareStrings(t, "Viper error: port expected to be %s, found %s", "7777", v.Get("port").(string)) - // Nested config set - compareStrings(t, "Viper error: adapters.pubmatic.endpoint expected to be %s, found %s", "not_an_endpoint", v.Get("adapters.pubmatic.endpoint").(string)) - // Config set with underscores - compareStrings(t, "Viper error: host_cookie.ttl_days expected to be %s, found %s", "60", v.Get("host_cookie.ttl_days").(string)) + ipv4Networks := forceEnv(t, "PBS_REQUEST_VALIDATION_IPV4_PRIVATE_NETWORKS", "1.1.1.1/24 2.2.2.2/24") + defer ipv4Networks() + + assert.Equal(t, 7777, v.Get("port"), "Basic Config") + assert.Equal(t, "not_an_endpoint", v.Get("adapters.pubmatic.endpoint"), "Nested Config") + assert.Equal(t, 60, v.Get("host_cookie.ttl_days"), "Config With Underscores") + assert.ElementsMatch(t, []string{"1.1.1.1/24", "2.2.2.2/24"}, v.Get("request_validation.ipv4_private_networks"), "Arrays") } diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index c1ba5bdbadb..75d83c9d7dd 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -13,6 +13,7 @@ type ExtBid struct { // ExtBidPrebid defines the contract for bidresponse.seatbid.bid[i].ext.prebid // DealPriority represents priority of deal bid. If its non deal bid then value will be 0 +// DealTierSatisfied true represents corresponding bid has satisfied the deal tier type ExtBidPrebid struct { Cache *ExtBidPrebidCache `json:"cache,omitempty"` Targeting map[string]string `json:"targeting,omitempty"` @@ -102,6 +103,9 @@ const ( HbSizeConstantKey TargetingKey = "hb_size" HbDealIDConstantKey TargetingKey = "hb_deal" + // HbFormatKey is the format of the bid. For example, "video", "banner" + HbFormatKey TargetingKey = "hb_format" + // HbCacheKey and HbVastCacheKey store UUIDs which can be used to fetch things from prebid cache. // Callers should *never* assume that either of these exist, since the call to the cache may always fail. // diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go index cbaa47d4f49..13ec8eb4538 100644 --- a/openrtb_ext/bid_request_video.go +++ b/openrtb_ext/bid_request_video.go @@ -144,6 +144,13 @@ type BidRequestVideo struct { // Description: // Indicates that the response should update key to include prefix and tier SupportDeals bool `json:"supportdeals,omitempty"` + + // Attribute: + // appendbiddernames + // Type: + // boolean, optional + // Flag indicating if the bidder name will be added to the hb_pb_cat_dur. Default is false. + AppendBidderNames bool `json:"appendbiddernames,omitempty"` } type PodConfig struct { diff --git a/openrtb_ext/bid_response_video.go b/openrtb_ext/bid_response_video.go index 4c123498ec8..22661547ca7 100644 --- a/openrtb_ext/bid_response_video.go +++ b/openrtb_ext/bid_response_video.go @@ -14,7 +14,7 @@ type AdPod struct { } type VideoTargeting struct { - HbPb string `json:"hb_pb"` - HbPbCatDur string `json:"hb_pb_cat_dur"` - HbCacheID string `json:"hb_cache_id"` + HbPb string `json:"hb_pb,omitempty"` + HbPbCatDur string `json:"hb_pb_cat_dur,omitempty"` + HbCacheID string `json:"hb_cache_id,omitempty"` } diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 424e4c37103..d5c953c57fa 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -20,30 +20,40 @@ type BidderName string // BidderNameGeneral is reserved for non-bidder specific messages when using a map keyed on the bidder name. const BidderNameGeneral = BidderName("general") +// BidderNameContext is reserved for first party data. +const BidderNameContext = BidderName("context") + // 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" + BidderAcuityAds BidderName = "acuityads" BidderAdform BidderName = "adform" BidderAdgeneration BidderName = "adgeneration" BidderAdhese BidderName = "adhese" BidderAdkernel BidderName = "adkernel" BidderAdkernelAdn BidderName = "adkernelAdn" BidderAdpone BidderName = "adpone" + BidderAdman BidderName = "adman" BidderAdmixer BidderName = "admixer" BidderAdOcean BidderName = "adocean" + BidderAdprime BidderName = "adprime" BidderAdtarget BidderName = "adtarget" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderAJA BidderName = "aja" + BidderAMX BidderName = "amx" BidderApplogy BidderName = "applogy" BidderAppnexus BidderName = "appnexus" BidderAdoppler BidderName = "adoppler" BidderAvocet BidderName = "avocet" BidderBeachfront BidderName = "beachfront" BidderBeintoo BidderName = "beintoo" + BidderBetween BidderName = "between" BidderBrightroll BidderName = "brightroll" + BidderColossus BidderName = "colossus" + BidderConnectAd BidderName = "connectad" BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" BidderCpmstar BidderName = "cpmstar" @@ -58,17 +68,22 @@ const ( BidderGrid BidderName = "grid" BidderGumGum BidderName = "gumgum" BidderImprovedigital BidderName = "improvedigital" + BidderInMobi BidderName = "inmobi" + BidderInvibes BidderName = "invibes" BidderIx BidderName = "ix" BidderKidoz BidderName = "kidoz" + BidderKrushmedia BidderName = "krushmedia" BidderKubient BidderName = "kubient" BidderLifestreet BidderName = "lifestreet" BidderLockerDome BidderName = "lockerdome" + BidderLogicad BidderName = "logicad" BidderLunaMedia BidderName = "lunamedia" BidderMarsmedia BidderName = "marsmedia" BidderMgid BidderName = "mgid" BidderMobileFuse BidderName = "mobilefuse" BidderNanoInteractive BidderName = "nanointeractive" BidderNinthDecimal BidderName = "ninthdecimal" + BidderNoBid BidderName = "nobid" BidderOpenx BidderName = "openx" BidderOrbidder BidderName = "orbidder" BidderPubmatic BidderName = "pubmatic" @@ -78,7 +93,11 @@ const ( BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSharethrough BidderName = "sharethrough" + BidderSilverMob BidderName = "silvermob" + BidderSmaato BidderName = "smaato" + BidderSmartadserver BidderName = "smartadserver" BidderSmartRTB BidderName = "smartrtb" + BidderSmartyAds BidderName = "smartyads" BidderSomoaudience BidderName = "somoaudience" BidderSonobi BidderName = "sonobi" BidderSovrn BidderName = "sovrn" @@ -105,25 +124,32 @@ const ( // The bidder name 'general' is not allowed since it has special meaning in message maps. var BidderMap = map[string]BidderName{ "33across": Bidder33Across, + "acuityads": BidderAcuityAds, "adform": BidderAdform, "adgeneration": BidderAdgeneration, "adhese": BidderAdhese, "adkernel": BidderAdkernel, "adkernelAdn": BidderAdkernelAdn, + "adman": BidderAdman, "admixer": BidderAdmixer, "adocean": BidderAdOcean, + "adprime": BidderAdprime, "adpone": BidderAdpone, "adtarget": BidderAdtarget, "adtelligent": BidderAdtelligent, "advangelists": BidderAdvangelists, "aja": BidderAJA, + "amx": BidderAMX, "applogy": BidderApplogy, "appnexus": BidderAppnexus, "adoppler": BidderAdoppler, "avocet": BidderAvocet, "beachfront": BidderBeachfront, "beintoo": BidderBeintoo, + "between": BidderBetween, "brightroll": BidderBrightroll, + "colossus": BidderColossus, + "connectad": BidderConnectAd, "consumable": BidderConsumable, "conversant": BidderConversant, "cpmstar": BidderCpmstar, @@ -138,17 +164,22 @@ var BidderMap = map[string]BidderName{ "grid": BidderGrid, "gumgum": BidderGumGum, "improvedigital": BidderImprovedigital, + "inmobi": BidderInMobi, + "invibes": BidderInvibes, "ix": BidderIx, "kidoz": BidderKidoz, + "krushmedia": BidderKrushmedia, "kubient": BidderKubient, "lifestreet": BidderLifestreet, "lockerdome": BidderLockerDome, + "logicad": BidderLogicad, "lunamedia": BidderLunaMedia, "marsmedia": BidderMarsmedia, "mgid": BidderMgid, "mobilefuse": BidderMobileFuse, "nanointeractive": BidderNanoInteractive, "ninthdecimal": BidderNinthDecimal, + "nobid": BidderNoBid, "openx": BidderOpenx, "orbidder": BidderOrbidder, "pubmatic": BidderPubmatic, @@ -158,7 +189,11 @@ var BidderMap = map[string]BidderName{ "rtbhouse": BidderRTBHouse, "rubicon": BidderRubicon, "sharethrough": BidderSharethrough, + "silvermob": BidderSilverMob, + "smaato": BidderSmaato, + "smartadserver": BidderSmartadserver, "smartrtb": BidderSmartRTB, + "smartyads": BidderSmartyAds, "somoaudience": BidderSomoaudience, "sonobi": BidderSonobi, "sovrn": BidderSovrn, diff --git a/openrtb_ext/bidders_test.go b/openrtb_ext/bidders_test.go index d49b23237ed..7b6a03b4de1 100644 --- a/openrtb_ext/bidders_test.go +++ b/openrtb_ext/bidders_test.go @@ -61,3 +61,64 @@ func TestBidderListDoesNotDefineGeneral(t *testing.T) { bidders := BidderList() assert.NotContains(t, bidders, BidderNameGeneral) } + +func TestBidderListDoesNotDefineContext(t *testing.T) { + bidders := BidderList() + assert.NotContains(t, bidders, BidderNameContext) +} + +// TestBidderUniquenessGatekeeping acts as a gatekeeper of bidder name uniqueness. If this test fails +// when you're building a new adapter, please consider choosing a different bidder name to maintain the +// current uniqueness threshold, or else start a discussion in the PR. +func TestBidderUniquenessGatekeeping(t *testing.T) { + // Get List Of Bidders + // - Exclude duplicates of adapters for the same bidder, as it's unlikely a publisher will use both. + var bidders []string + for _, bidder := range BidderMap { + if bidder != BidderTripleliftNative && bidder != BidderAdkernelAdn && bidder != BidderSmartadserver { + bidders = append(bidders, string(bidder)) + } + } + + currentThreshold := 6 + measuredThreshold := minUniquePrefixLength(bidders) + + assert.NotZero(t, measuredThreshold, "BidderMap contains duplicate bidder name values.") + assert.LessOrEqual(t, measuredThreshold, currentThreshold) +} + +// minUniquePrefixLength measures the minimun amount of characters needed to uniquely identify +// one of the strings, or returns 0 if there are duplicates. +func minUniquePrefixLength(b []string) int { + targetingKeyMaxLength := 20 + for prefixLength := 1; prefixLength <= targetingKeyMaxLength; prefixLength++ { + if uniqueForPrefixLength(b, prefixLength) { + return prefixLength + } + } + return 0 +} + +func uniqueForPrefixLength(b []string, prefixLength int) bool { + m := make(map[string]struct{}) + + if prefixLength <= 0 { + return false + } + + for i, n := range b { + ns := string(n) + + if len(ns) > prefixLength { + ns = ns[0:prefixLength] + } + + m[ns] = struct{}{} + + if len(m) != i+1 { + return false + } + } + + return true +} diff --git a/openrtb_ext/deal_tier.go b/openrtb_ext/deal_tier.go new file mode 100644 index 00000000000..e882235d01e --- /dev/null +++ b/openrtb_ext/deal_tier.go @@ -0,0 +1,61 @@ +package openrtb_ext + +import ( + "encoding/json" + + "github.com/PubMatic-OpenWrap/openrtb" +) + +// DealTier defines the configuration of a deal tier. +type DealTier struct { + // Prefix specifies the beginning of the hb_pb_cat_dur targeting key value. Must be non-empty. + Prefix string `json:"prefix"` + + // MinDealTier specifies the minimum deal priority value (inclusive) that must be met for the targeting + // key value to be modified. Must be greater than 0. + MinDealTier int `json:"minDealTier"` +} + +// DealTierBidderMap defines a correlation between bidders and deal tiers. +type DealTierBidderMap map[BidderName]DealTier + +// ReadDealTiersFromImp returns a map of bidder deal tiers read from the impression of an original request (not split / cleaned). +func ReadDealTiersFromImp(imp openrtb.Imp) (DealTierBidderMap, error) { + dealTiers := make(DealTierBidderMap) + + if len(imp.Ext) == 0 { + return dealTiers, nil + } + + // imp.ext.{bidder} + var impExt map[string]struct { + DealTier *DealTier `json:"dealTier"` + } + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + return nil, err + } + for bidder, param := range impExt { + if param.DealTier != nil { + dealTiers[BidderName(bidder)] = *param.DealTier + } + } + + // imp.ext.prebid.{bidder} + var impPrebidExt struct { + Prebid struct { + Bidders map[string]struct { + DealTier *DealTier `json:"dealTier"` + } `json:"bidder"` + } `json:"prebid"` + } + if err := json.Unmarshal(imp.Ext, &impPrebidExt); err != nil { + return nil, err + } + for bidder, param := range impPrebidExt.Prebid.Bidders { + if param.DealTier != nil { + dealTiers[BidderName(bidder)] = *param.DealTier + } + } + + return dealTiers, nil +} diff --git a/openrtb_ext/deal_tier_test.go b/openrtb_ext/deal_tier_test.go new file mode 100644 index 00000000000..717e0703466 --- /dev/null +++ b/openrtb_ext/deal_tier_test.go @@ -0,0 +1,98 @@ +package openrtb_ext + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestReadDealTiersFromImp(t *testing.T) { + testCases := []struct { + description string + impExt json.RawMessage + expectedResult DealTierBidderMap + expectedError string + }{ + { + description: "Nil", + impExt: nil, + expectedResult: DealTierBidderMap{}, + }, + { + description: "None", + impExt: json.RawMessage(``), + expectedResult: DealTierBidderMap{}, + }, + { + description: "Empty Object", + impExt: json.RawMessage(`{}`), + expectedResult: DealTierBidderMap{}, + }, + { + description: "imp.ext - with other params", + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}`), + expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}}, + }, + { + description: "imp.ext - multiple", + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "appnexusPrefix"}, "placementId": 12345}, "rubicon": {"dealTier": {"minDealTier": 8, "prefix": "rubiconPrefix"}, "placementId": 12345}}`), + expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "appnexusPrefix", MinDealTier: 5}, BidderRubicon: {Prefix: "rubiconPrefix", MinDealTier: 8}}, + }, + { + description: "imp.ext - no deal tier", + impExt: json.RawMessage(`{"appnexus": {"placementId": 12345}}`), + expectedResult: DealTierBidderMap{}, + }, + { + description: "imp.ext - error", + impExt: json.RawMessage(`{"appnexus": {"dealTier": "wrong type", "placementId": 12345}}`), + expectedError: "json: cannot unmarshal string into Go struct field .dealTier of type openrtb_ext.DealTier", + }, + { + description: "imp.ext.prebid", + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "anyPrefix"}, "placementId": 12345}}}}`), + expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "anyPrefix", MinDealTier: 5}}, + }, + { + description: "imp.ext.prebid- multiple", + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "appnexusPrefix"}, "placementId": 12345}, "rubicon": {"dealTier": {"minDealTier": 8, "prefix": "rubiconPrefix"}, "placementId": 12345}}}}`), + expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "appnexusPrefix", MinDealTier: 5}, BidderRubicon: {Prefix: "rubiconPrefix", MinDealTier: 8}}, + }, + { + description: "imp.ext.prebid - no deal tier", + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"placementId": 12345}}}}`), + expectedResult: DealTierBidderMap{}, + }, + { + description: "imp.ext.prebid - error", + impExt: json.RawMessage(`{"prebid": {"bidder": {"appnexus": {"dealTier": "wrong type", "placementId": 12345}}}}`), + expectedError: "json: cannot unmarshal string into Go struct field .prebid.bidder.dealTier of type openrtb_ext.DealTier", + }, + { + description: "imp.ext.prebid wins over imp.ext", + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "impExt"}, "placementId": 12345}, "prebid": {"bidder": {"appnexus": {"dealTier": {"minDealTier": 8, "prefix": "impExtPrebid"}, "placementId": 12345}}}}`), + expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "impExtPrebid", MinDealTier: 8}}, + }, + { + description: "imp.ext.prebid coexists with imp.ext", + impExt: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "impExt"}, "placementId": 12345}, "prebid": {"bidder": {"rubicon": {"dealTier": {"minDealTier": 8, "prefix": "impExtPrebid"}, "placementId": 12345}}}}`), + expectedResult: DealTierBidderMap{BidderAppnexus: {Prefix: "impExt", MinDealTier: 5}, BidderRubicon: {Prefix: "impExtPrebid", MinDealTier: 8}}, + }, + } + + for _, test := range testCases { + imp := openrtb.Imp{Ext: test.impExt} + + result, err := ReadDealTiersFromImp(imp) + + assert.Equal(t, test.expectedResult, result, test.description+":result") + + if len(test.expectedError) == 0 { + assert.NoError(t, err, test.description+":error") + } else { + assert.EqualError(t, err, test.expectedError, test.description+":error") + } + } +} diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go index 0d5e1f655cb..f83fa63df84 100644 --- a/openrtb_ext/imp.go +++ b/openrtb_ext/imp.go @@ -4,30 +4,16 @@ import ( "encoding/json" ) -// ExtImp defines the contract for bidrequest.imp[i].ext -type ExtImp struct { - Prebid *ExtImpPrebid `json:"prebid,omitempty"` - Appnexus *ExtImpAppnexus `json:"appnexus"` - Consumable *ExtImpConsumable `json:"consumable"` - Rubicon *ExtImpRubicon `json:"rubicon"` - Adform *ExtImpAdform `json:"adform"` - Rhythmone *ExtImpRhythmone `json:"rhythmone"` - Unruly *ExtImpUnruly `json:"unruly"` - EmxDigital *ExtImpEmxDigital `json:"emx_digital"` -} - // ExtImpPrebid defines the contract for bidrequest.imp[i].ext.prebid type ExtImpPrebid struct { - StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` + // StoredRequest specifies which stored impression to use, if any. + StoredRequest *ExtStoredRequest `json:"storedrequest"` - // Rewarded inventory signal, can be 0 or 1 - IsRewardedInventory int8 `json:"is_rewarded_inventory,omitempty"` + // IsRewardedInventory is a signal intended for video impressions. Must 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 - // https://github.com/PubMatic-OpenWrap/prebid-server/pull/846#issuecomment-476352224 - Bidder map[string]json.RawMessage `json:"bidder,omitempty"` + // Bidder is the preferred approach for providing paramters to be interepreted by the bidder's adapter. + Bidder map[string]json.RawMessage `json:"bidder"` SKAdnetwork json.RawMessage `json:"skadn,omitempty"` } diff --git a/openrtb_ext/imp_acuityads.go b/openrtb_ext/imp_acuityads.go new file mode 100644 index 00000000000..f0275e39f89 --- /dev/null +++ b/openrtb_ext/imp_acuityads.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtAcuityAds struct { + Host string `json:"host"` + AccountID string `json:"accountid"` +} diff --git a/openrtb_ext/imp_adform.go b/openrtb_ext/imp_adform.go index 3e7c1a7261e..3206ece7c9b 100644 --- a/openrtb_ext/imp_adform.go +++ b/openrtb_ext/imp_adform.go @@ -1,8 +1,11 @@ package openrtb_ext type ExtImpAdform struct { - MasterTagId string `json:"mid"` - PriceType string `json:"priceType,omitempty"` - KeyValues string `json:"mkv,omitempty"` - KeyWords string `json:"mkw,omitempty"` + MasterTagId string `json:"mid"` + PriceType string `json:"priceType,omitempty"` + KeyValues string `json:"mkv,omitempty"` + KeyWords string `json:"mkw,omitempty"` + CDims string `json:"cdims,omitempty"` + MinPrice float64 `json:"minp,omitempty"` + Url string `json:"url,omitempty"` } diff --git a/openrtb_ext/imp_adman.go b/openrtb_ext/imp_adman.go new file mode 100644 index 00000000000..bc79415452c --- /dev/null +++ b/openrtb_ext/imp_adman.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpAdman defines adman specifiec param +type ExtImpAdman struct { + TagID string `json:"TagID"` +} diff --git a/openrtb_ext/imp_adoppler.go b/openrtb_ext/imp_adoppler.go index 4b3ba97ce05..9d4d5e5ca01 100644 --- a/openrtb_ext/imp_adoppler.go +++ b/openrtb_ext/imp_adoppler.go @@ -1,5 +1,6 @@ package openrtb_ext type ExtImpAdoppler struct { + Client string `json:"client"` AdUnit string `json:"adunit"` } diff --git a/openrtb_ext/imp_adprime.go b/openrtb_ext/imp_adprime.go new file mode 100644 index 00000000000..a089b818b56 --- /dev/null +++ b/openrtb_ext/imp_adprime.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpAdprime defines adprime specifiec param +type ExtImpAdprime struct { + TagID string `json:"TagID"` +} diff --git a/openrtb_ext/imp_amx.go b/openrtb_ext/imp_amx.go new file mode 100644 index 00000000000..d4439d05f60 --- /dev/null +++ b/openrtb_ext/imp_amx.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpAMX is the imp.ext format for the AMX bidder +type ExtImpAMX struct { + TagID string `json:"tagId,omitempty"` + AdUnitID string `json:"adUnitId,omitempty"` +} diff --git a/openrtb_ext/imp_between.go b/openrtb_ext/imp_between.go new file mode 100644 index 00000000000..788ce215b9a --- /dev/null +++ b/openrtb_ext/imp_between.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpBetween struct { + Host string `json:"host"` +} diff --git a/openrtb_ext/imp_colossus.go b/openrtb_ext/imp_colossus.go new file mode 100644 index 00000000000..8969000558d --- /dev/null +++ b/openrtb_ext/imp_colossus.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpColossus defines colossus specifiec param +type ExtImpColossus struct { + TagID string `json:"TagID"` +} diff --git a/openrtb_ext/imp_connectad.go b/openrtb_ext/imp_connectad.go new file mode 100644 index 00000000000..c4c7ab696f2 --- /dev/null +++ b/openrtb_ext/imp_connectad.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ExtImpConnectAd struct { + NetworkID int `json:"networkId"` + SiteID int `json:"siteId"` + Bidfloor float64 `json:"bidfloor,omitempty"` +} diff --git a/openrtb_ext/imp_conversant.go b/openrtb_ext/imp_conversant.go new file mode 100644 index 00000000000..8587e111153 --- /dev/null +++ b/openrtb_ext/imp_conversant.go @@ -0,0 +1,13 @@ +package openrtb_ext + +type ExtImpConversant struct { + SiteID string `json:"site_id"` + Secure *int8 `json:"secure"` + TagID string `json:"tag_id"` + Position *int8 `json:"position"` + BidFloor float64 `json:"bidfloor"` + MIMEs []string `json:"mimes"` + API []int8 `json:"api"` + Protocols []int8 `json:"protocols"` + MaxDuration *int64 `json:"maxduration"` +} diff --git a/openrtb_ext/imp_inmobi.go b/openrtb_ext/imp_inmobi.go new file mode 100644 index 00000000000..d74e3cac8b0 --- /dev/null +++ b/openrtb_ext/imp_inmobi.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpInMobi struct { + Plc string `json:"plc"` +} diff --git a/openrtb_ext/imp_invibes.go b/openrtb_ext/imp_invibes.go new file mode 100644 index 00000000000..37ed31ced63 --- /dev/null +++ b/openrtb_ext/imp_invibes.go @@ -0,0 +1,12 @@ +package openrtb_ext + +type ExtImpInvibes struct { + PlacementID string `json:"placementId,omitempty"` + DomainID int `json:"domainId"` + Debug ExtImpInvibesDebug `json:"debug,omitempty"` +} + +type ExtImpInvibesDebug struct { + TestBvid string `json:"testBvid,omitempty"` + TestLog bool `json:"testLog,omitempty"` +} diff --git a/openrtb_ext/imp_krushmedia.go b/openrtb_ext/imp_krushmedia.go new file mode 100644 index 00000000000..a175c227fda --- /dev/null +++ b/openrtb_ext/imp_krushmedia.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtKrushmedia defines imp[0].ext object structure +type ExtKrushmedia struct { + AccountID string `json:"key"` +} diff --git a/openrtb_ext/imp_kubient.go b/openrtb_ext/imp_kubient.go new file mode 100644 index 00000000000..fafd2a0eb8f --- /dev/null +++ b/openrtb_ext/imp_kubient.go @@ -0,0 +1,6 @@ +package openrtb_ext + +// ExtImpKubient defines the contract for bidrequest.imp[i].ext.kubient +type ExtImpKubient struct { + ZoneID string `json:"zoneid"` +} diff --git a/openrtb_ext/imp_logicad.go b/openrtb_ext/imp_logicad.go new file mode 100644 index 00000000000..e4e3c3b091c --- /dev/null +++ b/openrtb_ext/imp_logicad.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpLogicad struct { + Tid string `json:"tid"` +} diff --git a/openrtb_ext/imp_nobid.go b/openrtb_ext/imp_nobid.go new file mode 100644 index 00000000000..8af16952c39 --- /dev/null +++ b/openrtb_ext/imp_nobid.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpNoBid struct { + SiteID string `json:"siteId"` + PlacementID string `json:"placementId"` +} diff --git a/openrtb_ext/imp_openx.go b/openrtb_ext/imp_openx.go index e63595b0912..2625cb3802d 100644 --- a/openrtb_ext/imp_openx.go +++ b/openrtb_ext/imp_openx.go @@ -3,6 +3,7 @@ package openrtb_ext // ExtImpOpenx defines the contract for bidrequest.imp[i].ext.openx type ExtImpOpenx struct { Unit string `json:"unit"` + Platform string `json:"platform"` DelDomain string `json:"delDomain"` CustomFloor float64 `json:"customFloor"` CustomParams map[string]interface{} `json:"customParams"` diff --git a/openrtb_ext/imp_silvermob.go b/openrtb_ext/imp_silvermob.go new file mode 100644 index 00000000000..9b2465534ca --- /dev/null +++ b/openrtb_ext/imp_silvermob.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtSilverMob defines the contract for bidrequest.imp[i].ext.silvermob +type ExtSilverMob struct { + ZoneID string `json:"zoneid"` + Host string `json:"host"` +} diff --git a/openrtb_ext/imp_smaato.go b/openrtb_ext/imp_smaato.go new file mode 100644 index 00000000000..10de97fb017 --- /dev/null +++ b/openrtb_ext/imp_smaato.go @@ -0,0 +1,9 @@ +package openrtb_ext + +// ExtImpSmaato defines the contract for bidrequest.imp[i].ext.smaato +// PublisherId and AdSpaceId are mandatory parameters, others are optional parameters +// AdSpaceId is identifier for specific ad placement or ad tag +type ExtImpSmaato struct { + PublisherID string `json:"publisherId"` + AdSpaceID string `json:"adspaceId"` +} diff --git a/openrtb_ext/imp_smartadserver.go b/openrtb_ext/imp_smartadserver.go new file mode 100644 index 00000000000..d542e0ffd27 --- /dev/null +++ b/openrtb_ext/imp_smartadserver.go @@ -0,0 +1,9 @@ +package openrtb_ext + +// ExtImpSmartadserver defines the contract for bidrequest.imp[i].ext.smartadserver +type ExtImpSmartadserver struct { + SiteID int `json:"siteId"` + PageID int `json:"pageId"` + FormatID int `json:"formatId"` + NetworkID int `json:"networkId"` +} diff --git a/openrtb_ext/imp_smartyads.go b/openrtb_ext/imp_smartyads.go new file mode 100644 index 00000000000..54911373e61 --- /dev/null +++ b/openrtb_ext/imp_smartyads.go @@ -0,0 +1,8 @@ +package openrtb_ext + +// ExtSmartyAds defines the contract for bidrequest.imp[i].ext.smartyads +type ExtSmartyAds struct { + AccountID string `json:"accountid"` + SourceID string `json:"sourceid"` + Host string `json:"host"` +} diff --git a/openrtb_ext/imp_telaria.go b/openrtb_ext/imp_telaria.go index 8ea371a8ad0..19a025c0b15 100644 --- a/openrtb_ext/imp_telaria.go +++ b/openrtb_ext/imp_telaria.go @@ -1,6 +1,9 @@ package openrtb_ext +import "encoding/json" + type ExtImpTelaria struct { - AdCode string `json:"adCode,omitempty"` - SeatCode string `json:"seatCode"` + AdCode string `json:"adCode,omitempty"` + SeatCode string `json:"seatCode"` + Extra json.RawMessage `json:"extra,omitempty"` } diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index ca7c0b40c17..e3684dffa5f 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -5,6 +5,11 @@ import ( "errors" ) +// FirstPartyDataContextExtKey defines the field name within bidrequest.ext reserved +// for first party data support. +const FirstPartyDataContextExtKey string = "context" +const MaxDecimalFigures int = 15 + // ExtRequest defines the contract for bidrequest.ext type ExtRequest struct { Prebid ExtRequestPrebid `json:"prebid"` @@ -12,14 +17,50 @@ type ExtRequest struct { // ExtRequestPrebid defines the contract for bidrequest.ext.prebid type ExtRequestPrebid struct { - Aliases map[string]string `json:"aliases,omitempty"` - BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"` - 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"` + Aliases map[string]string `json:"aliases,omitempty"` + BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"` + Cache *ExtRequestPrebidCache `json:"cache,omitempty"` + SChains []*ExtRequestPrebidSChain `json:"schains,omitempty"` + StoredRequest *ExtStoredRequest `json:"storedrequest,omitempty"` + Targeting *ExtRequestTargeting `json:"targeting,omitempty"` + SupportDeals bool `json:"supportdeals,omitempty"` + Debug bool `json:"debug,omitempty"` + BidderParams interface{} `json:"bidderparams,omitempty"` + + // NoSale specifies bidders with whom the publisher has a legal relationship where the + // passing of personally identifiable information doesn't constitute a sale per CCPA law. + // The array may contain a single sstar ('*') entry to represent all bidders. + NoSale []string `json:"nosale,omitempty"` +} + +// ExtRequestPrebid defines the contract for bidrequest.ext.prebid.schains +type ExtRequestPrebidSChain struct { + Bidders []string `json:"bidders,omitempty"` + SChain ExtRequestPrebidSChainSChain `json:"schain"` +} + +// ExtRequestPrebidSChainSChain defines the contract for bidrequest.ext.prebid.schains[i].schain +type ExtRequestPrebidSChainSChain struct { + Complete int `json:"complete"` + Nodes []*ExtRequestPrebidSChainSChainNode `json:"nodes"` + Ver string `json:"ver"` + Ext json.RawMessage `json:"ext,omitempty"` +} + +// ExtRequestPrebidSChainSChainNode defines the contract for bidrequest.ext.prebid.schains[i].schain[i].nodes +type ExtRequestPrebidSChainSChainNode struct { + ASI string `json:"asi"` + SID string `json:"sid"` + RID string `json:"rid,omitempty"` + Name string `json:"name,omitempty"` + Domain string `json:"domain,omitempty"` + HP int `json:"hp"` + Ext json.RawMessage `json:"ext,omitempty"` +} + +// SourceExt defines the contract for bidrequest.source.ext +type SourceExt struct { + SChain ExtRequestPrebidSChainSChain `json:"schain"` } // ExtRequestPrebidCache defines the contract for bidrequest.ext.prebid.cache @@ -37,7 +78,7 @@ func (ert *ExtRequestPrebidCache) UnmarshalJSON(b []byte) error { } if proxy.Bids == nil && proxy.VastXML == nil { - return errors.New(`request.ext.prebid.cache requires one of the "bids" or "vastml" properties`) + return errors.New(`request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`) } *ert = ExtRequestPrebidCache(proxy) @@ -45,10 +86,14 @@ func (ert *ExtRequestPrebidCache) UnmarshalJSON(b []byte) error { } // ExtRequestPrebidCacheBids defines the contract for bidrequest.ext.prebid.cache.bids -type ExtRequestPrebidCacheBids struct{} +type ExtRequestPrebidCacheBids struct { + ReturnCreative *bool `json:"returnCreative"` +} // ExtRequestPrebidCacheVAST defines the contract for bidrequest.ext.prebid.cache.vastxml -type ExtRequestPrebidCacheVAST struct{} +type ExtRequestPrebidCacheVAST struct { + ReturnCreative *bool `json:"returnCreative"` +} // ExtRequestTargeting defines the contract for bidrequest.ext.prebid.targeting type ExtRequestTargeting struct { @@ -56,7 +101,10 @@ type ExtRequestTargeting struct { IncludeWinners bool `json:"includewinners"` IncludeBidderKeys bool `json:"includebidderkeys"` IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"` + IncludeFormat bool `json:"includeformat"` DurationRangeSec []int `json:"durationrangesec"` + PreferDeals bool `json:"preferdeals"` + AppendBidderNames bool `json:"appendbiddernames,omitempty"` } type ExtIncludeBrandCategory struct { @@ -135,6 +183,9 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error { if pgraw.Precision < 0 { return errors.New("Price granularity error: precision must be non-negative") } + if pgraw.Precision > MaxDecimalFigures { + return errors.New("Price granularity error: precision of more than 15 significant figures is not supported") + } if len(pgraw.Ranges) > 0 { var prevMax float64 = 0 for i, gr := range pgraw.Ranges { @@ -146,9 +197,6 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error { } // Enforce that we don't read "min" from the request pgraw.Ranges[i].Min = prevMax - if pgraw.Ranges[i].Min < prevMax { - return errors.New("Price granularity error: overlapping granularity ranges") - } prevMax = gr.Max } *pg = PriceGranularity(pgraw) diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go index e4046a622db..98a2e1645a0 100644 --- a/openrtb_ext/request_test.go +++ b/openrtb_ext/request_test.go @@ -190,21 +190,43 @@ var validGranularityTests []granularityTestData = []granularityTestData{ } func TestGranularityUnmarshalBad(t *testing.T) { - tests := [][]byte{ - []byte(`[]`), - []byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`), - []byte(`{"ranges":[{"max":20, "increment": -1}]}`), - []byte(`{"ranges":[{"max":"20", "increment": "0.1"}]}`), - []byte(`{"ranges":[{"max":20, "increment":0.1}. {"max":10, "increment":0.02}]}`), - []byte(`{"ranges":[{"max":20, "min":10, "increment": 0.1}, {"max":10, "min":0, "increment":0.05}]}`), - []byte(`{"ranges":[{"max":1.0, "increment": 0.07}, {"max" 1.0, "increment": 0.03}]}`), + testCases := []struct { + description string + jsonPriceGranularity []byte + }{ + { + "Malformed", + []byte(`[]`), + }, + { + "Negative precision", + []byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`), + }, + { + "Precision greater than MaxDecimalFigures supported", + []byte(`{"precision": 16, "ranges": [{"max":20, "increment":0.5}]}`), + }, + { + "Negative increment", + []byte(`{"ranges":[{"max":20, "increment": -1}]}`), + }, + { + "Range with non float64 max value", + []byte(`{"ranges":[{"max":"20", "increment": "0.1"}]}`), + }, + { + "Ranges in decreasing order", + []byte(`{"ranges":[{"max":20, "increment":0.1}. {"max":10, "increment":0.02}]}`), + }, + { + "Max equal to previous max", + []byte(`{"ranges":[{"max":1.0, "increment": 0.07}, {"max" 1.0, "increment": 0.03}]}`), + }, } - var resolved PriceGranularity - for _, b := range tests { - resolved = PriceGranularity{} - err := json.Unmarshal(b, &resolved) - if err == nil { - t.Errorf("Invalid granularity unmarshalled without error.\nJSON was: %s\n Resolved to: %v", string(b), resolved) - } + + for _, test := range testCases { + resolved := PriceGranularity{} + err := json.Unmarshal(test.jsonPriceGranularity, &resolved) + assert.Errorf(t, err, "Invalid granularity unmarshalled without error.\nJSON was: %s\n Resolved to: %v. Test: %s", string(test.jsonPriceGranularity), resolved, test.description) } } diff --git a/openrtb_ext/user.go b/openrtb_ext/user.go index a7c8505d226..b83f82330db 100644 --- a/openrtb_ext/user.go +++ b/openrtb_ext/user.go @@ -33,7 +33,7 @@ type ExtUserDigiTrust struct { // ExtUserEid defines the contract for bidrequest.user.ext.eids // Responsible for the Universal User ID support: establishing pseudonymous IDs for users. -// See https://github.com/PubMatic-OpenWrap/Prebid.js/issues/3900 for details. +// See https://github.com/prebid/Prebid.js/issues/3900 for details. type ExtUserEid struct { Source string `json:"source"` ID string `json:"id,omitempty"` @@ -44,6 +44,6 @@ type ExtUserEid struct { // ExtUserEidUid defines the contract for bidrequest.user.ext.eids[i].uids[j] type ExtUserEidUid struct { ID string `json:"id"` - AType int `json:"atype,omitempty"` + Atype int `json:"atype,omitempty"` Ext json.RawMessage `json:"ext,omitempty"` } diff --git a/pbs/pbsrequest.go b/pbs/pbsrequest.go index bb8db11ba90..bd07a6c558b 100644 --- a/pbs/pbsrequest.go +++ b/pbs/pbsrequest.go @@ -12,9 +12,10 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/cache" "github.com/PubMatic-OpenWrap/prebid-server/config" - "github.com/PubMatic-OpenWrap/prebid-server/prebid" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/usersync" + "github.com/PubMatic-OpenWrap/prebid-server/util/httputil" + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" "github.com/PubMatic-OpenWrap/openrtb" "github.com/blang/semver" @@ -216,6 +217,8 @@ func ParseMediaTypes(types []string) []MediaType { return mtypes } +var ipv4Validator iputil.IPValidator = iputil.VersionIPValidator{iputil.IPv4} + func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.Cache, hostCookieConfig *config.HostCookie) (*PBSRequest, error) { defer r.Body.Close() @@ -235,7 +238,9 @@ func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.C if pbsReq.Device == nil { pbsReq.Device = &openrtb.Device{} } - pbsReq.Device.IP = prebid.GetIP(r) + if ip, _ := httputil.FindIP(r, ipv4Validator); ip != nil { + pbsReq.Device.IP = ip.String() + } if pbsReq.SDK == nil { pbsReq.SDK = &SDK{} @@ -291,7 +296,7 @@ func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.C pbsReq.IsDebug = true } - if prebid.IsSecure(r) { + if httputil.IsSecure(r) { pbsReq.Secure = 1 } diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index e275910fa6e..10eda83a856 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -37,7 +37,7 @@ func NewMetricsEngine(cfg *config.Configuration, adapterList []openrtb_ext.Bidde } if cfg.Metrics.Prometheus.Port != 0 { // Set up the Prometheus metrics. - returnEngine.PrometheusMetrics = prometheusmetrics.NewMetrics(cfg.Metrics.Prometheus) + returnEngine.PrometheusMetrics = prometheusmetrics.NewMetrics(cfg.Metrics.Prometheus, cfg.Metrics.Disabled) engineList = append(engineList, returnEngine.PrometheusMetrics) } @@ -104,6 +104,20 @@ func (me *MultiMetricsEngine) RecordRequestTime(labels pbsmetrics.Labels, length } } +// RecordStoredDataFetchTime across all engines +func (me *MultiMetricsEngine) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) { + for _, thisME := range *me { + thisME.RecordStoredDataFetchTime(labels, length) + } +} + +// RecordStoredDataError across all engines +func (me *MultiMetricsEngine) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) { + for _, thisME := range *me { + thisME.RecordStoredDataError(labels) + } +} + // RecordAdapterPanic across all engines func (me *MultiMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) { for _, thisME := range *me { @@ -118,6 +132,21 @@ func (me *MultiMetricsEngine) RecordAdapterRequest(labels pbsmetrics.AdapterLabe } } +// Keeps track of created and reused connections to adapter bidders and the time from the +// connection request, to the connection creation, or reuse from the pool across all engines +func (me *MultiMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { + for _, thisME := range *me { + thisME.RecordAdapterConnections(bidderName, connWasReused, connWaitTime) + } +} + +// Times the DNS resolution process +func (me *MultiMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { + for _, thisME := range *me { + thisME.RecordDNSTime(dnsLookupTime) + } +} + // RecordAdapterBidReceived across all engines func (me *MultiMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { for _, thisME := range *me { @@ -160,6 +189,13 @@ func (me *MultiMetricsEngine) RecordStoredImpCacheResult(cacheResult pbsmetrics. } } +// RecordAccountCacheResult across all engines +func (me *MultiMetricsEngine) RecordAccountCacheResult(cacheResult pbsmetrics.CacheResult, inc int) { + for _, thisME := range *me { + thisME.RecordAccountCacheResult(cacheResult, inc) + } +} + // RecordAdapterCookieSync across all engines func (me *MultiMetricsEngine) RecordAdapterCookieSync(adapter openrtb_ext.BidderName, gdprBlocked bool) { for _, thisME := range *me { @@ -195,6 +231,13 @@ func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) { } } +// RecordRequestPrivacy across all engines +func (me *MultiMetricsEngine) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) { + for _, thisME := range *me { + thisME.RecordRequestPrivacy(privacy) + } +} + // RecordAdapterDuplicateBidID across all engines func (me *MultiMetricsEngine) RecordAdapterDuplicateBidID(adaptor string, collisions int) { for _, thisME := range *me { @@ -264,6 +307,14 @@ func (me *DummyMetricsEngine) RecordLegacyImps(labels pbsmetrics.Labels, numImps func (me *DummyMetricsEngine) RecordRequestTime(labels pbsmetrics.Labels, length time.Duration) { } +// RecordStoredDataFetchTime as a noop +func (me *DummyMetricsEngine) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) { +} + +// RecordStoredDataError as a noop +func (me *DummyMetricsEngine) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) { +} + // RecordAdapterPanic as a noop func (me *DummyMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) { } @@ -272,6 +323,14 @@ func (me *DummyMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels func (me *DummyMetricsEngine) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) { } +// RecordAdapterConnections as a noop +func (me *DummyMetricsEngine) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { +} + +// RecordDNSTime as a noop +func (me *DummyMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) { +} + // RecordAdapterBidReceived as a noop func (me *DummyMetricsEngine) RecordAdapterBidReceived(labels pbsmetrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { } @@ -304,6 +363,10 @@ func (me *DummyMetricsEngine) RecordStoredReqCacheResult(cacheResult pbsmetrics. func (me *DummyMetricsEngine) RecordStoredImpCacheResult(cacheResult pbsmetrics.CacheResult, inc int) { } +// RecordAccountCacheResult as a noop +func (me *DummyMetricsEngine) RecordAccountCacheResult(cacheResult pbsmetrics.CacheResult, inc int) { +} + // RecordPrebidCacheRequestTime as a noop func (me *DummyMetricsEngine) RecordPrebidCacheRequestTime(success bool, length time.Duration) { } @@ -316,6 +379,10 @@ func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType p func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) { } +// RecordRequestPrivacy as a noop +func (me *DummyMetricsEngine) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) { +} + // RecordAdapterDuplicateBidID as a noop func (me *DummyMetricsEngine) RecordAdapterDuplicateBidID(adaptor string, collisions int) { } diff --git a/pbsmetrics/config/metrics_test.go b/pbsmetrics/config/metrics_test.go index 26635569969..288a9e6ff11 100644 --- a/pbsmetrics/config/metrics_test.go +++ b/pbsmetrics/config/metrics_test.go @@ -116,6 +116,13 @@ func TestMultiMetricsEngine(t *testing.T) { metricsEngine.RecordImps(impTypeLabels) } + metricsEngine.RecordStoredReqCacheResult(pbsmetrics.CacheMiss, 1) + metricsEngine.RecordStoredImpCacheResult(pbsmetrics.CacheMiss, 2) + metricsEngine.RecordAccountCacheResult(pbsmetrics.CacheMiss, 3) + metricsEngine.RecordStoredReqCacheResult(pbsmetrics.CacheHit, 4) + metricsEngine.RecordStoredImpCacheResult(pbsmetrics.CacheHit, 5) + metricsEngine.RecordAccountCacheResult(pbsmetrics.CacheHit, 6) + 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 @@ -154,6 +161,13 @@ func TestMultiMetricsEngine(t *testing.T) { VerifyMetrics(t, "RecordRequestQueueTime.Video.Rejected", goEngine.RequestsQueueTimer[pbsmetrics.ReqTypeVideo][false].Count(), 1) VerifyMetrics(t, "RecordRequestQueueTime.Video.Accepted", goEngine.RequestsQueueTimer[pbsmetrics.ReqTypeVideo][true].Count(), 0) + + VerifyMetrics(t, "StoredReqCache.Miss", goEngine.StoredReqCacheMeter[pbsmetrics.CacheMiss].Count(), 1) + VerifyMetrics(t, "StoredImpCache.Miss", goEngine.StoredImpCacheMeter[pbsmetrics.CacheMiss].Count(), 2) + VerifyMetrics(t, "AccountCache.Miss", goEngine.AccountCacheMeter[pbsmetrics.CacheMiss].Count(), 3) + VerifyMetrics(t, "StoredReqCache.Hit", goEngine.StoredReqCacheMeter[pbsmetrics.CacheHit].Count(), 4) + VerifyMetrics(t, "StoredImpCache.Hit", goEngine.StoredImpCacheMeter[pbsmetrics.CacheHit].Count(), 5) + VerifyMetrics(t, "AccountCache.Hit", goEngine.AccountCacheMeter[pbsmetrics.CacheHit].Count(), 6) } func VerifyMetrics(t *testing.T, name string, actual int64, expected int64) { diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go index 621639683ee..aba17d621fc 100644 --- a/pbsmetrics/go_metrics.go +++ b/pbsmetrics/go_metrics.go @@ -27,8 +27,12 @@ type Metrics struct { RequestsQueueTimer map[RequestType]map[bool]metrics.Timer PrebidCacheRequestTimerSuccess metrics.Timer PrebidCacheRequestTimerError metrics.Timer + StoredDataFetchTimer map[StoredDataType]map[StoredDataFetchType]metrics.Timer + StoredDataErrorMeter map[StoredDataType]map[StoredDataError]metrics.Meter StoredReqCacheMeter map[CacheResult]metrics.Meter StoredImpCacheMeter map[CacheResult]metrics.Meter + AccountCacheMeter map[CacheResult]metrics.Meter + DNSLookupTimer metrics.Timer // Metrics for OpenRTB requests specifically. So we can track what % of RequestsMeter are OpenRTB // and know when legacy requests have been abandoned. @@ -48,9 +52,17 @@ type Metrics struct { ImpsTypeAudio metrics.Meter ImpsTypeNative metrics.Meter + // Notification timeout metrics TimeoutNotificationSuccess metrics.Meter TimeoutNotificationFailure metrics.Meter + // TCF adaption metrics + PrivacyCCPARequest metrics.Meter + PrivacyCCPARequestOptOut metrics.Meter + PrivacyCOPPARequest metrics.Meter + PrivacyLMTRequest metrics.Meter + PrivacyTCFRequestVersion map[TCFVersionValue]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 @@ -73,6 +85,9 @@ type AdapterMetrics struct { BidsReceivedMeter metrics.Meter PanicMeter metrics.Meter MarkupMetrics map[openrtb_ext.BidType]*MarkupDeliveryMetrics + ConnCreated metrics.Counter + ConnReused metrics.Counter + ConnWaitTime metrics.Timer } type MarkupDeliveryMetrics struct { @@ -98,7 +113,7 @@ const unknownBidder openrtb_ext.BidderName = "unknown" // rather than loading legacy metrics that never get filled. // This will also eventually let us configure metrics, such as setting a limited set of metrics // for a production instance, and then expanding again when we need more debugging. -func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disableMetrics config.DisabledMetrics) *Metrics { +func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, disabledMetrics config.DisabledMetrics) *Metrics { blankMeter := &metrics.NilMeter{} blankTimer := &metrics.NilTimer{} @@ -115,11 +130,15 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa SafariRequestMeter: blankMeter, SafariNoCookieMeter: blankMeter, RequestTimer: blankTimer, + DNSLookupTimer: blankTimer, RequestsQueueTimer: make(map[RequestType]map[bool]metrics.Timer), PrebidCacheRequestTimerSuccess: blankTimer, PrebidCacheRequestTimerError: blankTimer, + StoredDataFetchTimer: make(map[StoredDataType]map[StoredDataFetchType]metrics.Timer), + StoredDataErrorMeter: make(map[StoredDataType]map[StoredDataError]metrics.Meter), StoredReqCacheMeter: make(map[CacheResult]metrics.Meter), StoredImpCacheMeter: make(map[CacheResult]metrics.Meter), + AccountCacheMeter: make(map[CacheResult]metrics.Meter), AmpNoCookieMeter: blankMeter, CookieSyncMeter: blankMeter, CookieSyncGen: make(map[openrtb_ext.BidderName]metrics.Meter), @@ -137,14 +156,21 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa TimeoutNotificationSuccess: blankMeter, TimeoutNotificationFailure: blankMeter, + PrivacyCCPARequest: blankMeter, + PrivacyCCPARequestOptOut: blankMeter, + PrivacyCOPPARequest: blankMeter, + PrivacyLMTRequest: blankMeter, + PrivacyTCFRequestVersion: make(map[TCFVersionValue]metrics.Meter, len(TCFVersions())), + AdapterMetrics: make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)), accountMetrics: make(map[string]*accountMetrics), - MetricsDisabled: disableMetrics, + MetricsDisabled: disabledMetrics, exchanges: exchanges, } + for _, a := range exchanges { - newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics() + newMetrics.AdapterMetrics[a] = makeBlankAdapterMetrics(newMetrics.MetricsDisabled) } for _, t := range RequestTypes() { @@ -154,6 +180,27 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa } } + for _, c := range CacheResults() { + newMetrics.StoredReqCacheMeter[c] = blankMeter + newMetrics.StoredImpCacheMeter[c] = blankMeter + newMetrics.AccountCacheMeter[c] = blankMeter + } + + for _, v := range TCFVersions() { + newMetrics.PrivacyTCFRequestVersion[v] = blankMeter + } + + for _, dt := range StoredDataTypes() { + newMetrics.StoredDataFetchTimer[dt] = make(map[StoredDataFetchType]metrics.Timer) + newMetrics.StoredDataErrorMeter[dt] = make(map[StoredDataError]metrics.Meter) + for _, ft := range StoredDataFetchTypes() { + newMetrics.StoredDataFetchTimer[dt][ft] = blankTimer + } + for _, e := range StoredDataErrors() { + newMetrics.StoredDataErrorMeter[dt][e] = blankMeter + } + } + //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) @@ -185,9 +232,21 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d newMetrics.AppRequestMeter = metrics.GetOrRegisterMeter("app_requests", registry) newMetrics.SafariNoCookieMeter = metrics.GetOrRegisterMeter("safari_no_cookie_requests", registry) newMetrics.RequestTimer = metrics.GetOrRegisterTimer("request_time", registry) + newMetrics.DNSLookupTimer = metrics.GetOrRegisterTimer("dns_lookup_time", registry) newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry) newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry) + for _, dt := range StoredDataTypes() { + for _, ft := range StoredDataFetchTypes() { + timerName := fmt.Sprintf("stored_%s_fetch_time.%s", string(dt), string(ft)) + newMetrics.StoredDataFetchTimer[dt][ft] = metrics.GetOrRegisterTimer(timerName, registry) + } + for _, e := range StoredDataErrors() { + meterName := fmt.Sprintf("stored_%s_error.%s", string(dt), string(e)) + newMetrics.StoredDataErrorMeter[dt][e] = metrics.GetOrRegisterMeter(meterName, registry) + } + } + newMetrics.AmpNoCookieMeter = metrics.GetOrRegisterMeter("amp_no_cookie_requests", registry) newMetrics.CookieSyncMeter = metrics.GetOrRegisterMeter("cookie_sync_requests", registry) newMetrics.userSyncBadRequest = metrics.GetOrRegisterMeter("usersync.bad_requests", registry) @@ -208,6 +267,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d 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.AccountCacheMeter[cacheRes] = metrics.GetOrRegisterMeter(fmt.Sprintf("account_cache_%s", string(cacheRes)), registry) } newMetrics.RequestsQueueTimer["video"][true] = metrics.GetOrRegisterTimer("queued_requests.video.accepted", registry) @@ -218,11 +278,20 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry) newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry) + + newMetrics.PrivacyCCPARequest = metrics.GetOrRegisterMeter("privacy.request.ccpa.specified", registry) + newMetrics.PrivacyCCPARequestOptOut = metrics.GetOrRegisterMeter("privacy.request.ccpa.opt-out", registry) + newMetrics.PrivacyCOPPARequest = metrics.GetOrRegisterMeter("privacy.request.coppa", registry) + newMetrics.PrivacyLMTRequest = metrics.GetOrRegisterMeter("privacy.request.lmt", registry) + for _, version := range TCFVersions() { + newMetrics.PrivacyTCFRequestVersion[version] = metrics.GetOrRegisterMeter(fmt.Sprintf("privacy.request.tcf.%s", string(version)), registry) + } + return newMetrics } // Part of setting up blank metrics, the adapter metrics. -func makeBlankAdapterMetrics() *AdapterMetrics { +func makeBlankAdapterMetrics(disabledMetrics config.DisabledMetrics) *AdapterMetrics { blankMeter := &metrics.NilMeter{} newAdapter := &AdapterMetrics{ NoCookieMeter: blankMeter, @@ -235,6 +304,11 @@ func makeBlankAdapterMetrics() *AdapterMetrics { PanicMeter: blankMeter, MarkupMetrics: makeBlankBidMarkupMetrics(), } + if !disabledMetrics.AdapterConnectionMetrics { + newAdapter.ConnCreated = metrics.NilCounter{} + newAdapter.ConnReused = metrics.NilCounter{} + newAdapter.ConnWaitTime = &metrics.NilTimer{} + } for _, err := range AdapterErrors() { newAdapter.ErrorMeters[err] = blankMeter } @@ -269,6 +343,9 @@ func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string, openrtb_ext.BidTypeAudio: makeDeliveryMetrics(registry, adapterOrAccount+"."+exchange, openrtb_ext.BidTypeAudio), openrtb_ext.BidTypeNative: makeDeliveryMetrics(registry, adapterOrAccount+"."+exchange, openrtb_ext.BidTypeNative), } + am.ConnCreated = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_created", adapterOrAccount, exchange), registry) + am.ConnReused = metrics.GetOrRegisterCounter(fmt.Sprintf("%[1]s.%[2]s.connections_reused", adapterOrAccount, exchange), registry) + am.ConnWaitTime = metrics.GetOrRegisterTimer(fmt.Sprintf("%[1]s.%[2]s.connection_wait_time", adapterOrAccount, exchange), registry) for err := range am.ErrorMeters { am.ErrorMeters[err] = metrics.GetOrRegisterMeter(fmt.Sprintf("%s.%s.requests.%s", adapterOrAccount, exchange, err), registry) } @@ -315,7 +392,7 @@ func (me *Metrics) getAccountMetrics(id string) *accountMetrics { am.adapterMetrics = make(map[openrtb_ext.BidderName]*AdapterMetrics, len(me.exchanges)) if !me.MetricsDisabled.AccountAdapterDetails { for _, a := range me.exchanges { - am.adapterMetrics[a] = makeBlankAdapterMetrics() + am.adapterMetrics[a] = makeBlankAdapterMetrics(me.MetricsDisabled) registerAdapterMetrics(me.MetricsRegistry, fmt.Sprintf("account.%s", id), string(a), am.adapterMetrics[a]) } } @@ -397,6 +474,16 @@ func (me *Metrics) RecordRequestTime(labels Labels, length time.Duration) { } } +// RecordStoredDataFetchTime implements a part of the MetricsEngine interface +func (me *Metrics) RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration) { + me.StoredDataFetchTimer[labels.DataType][labels.DataFetchType].Update(length) +} + +// RecordStoredDataError implements a part of the MetricsEngine interface +func (me *Metrics) RecordStoredDataError(labels StoredDataLabels) { + me.StoredDataErrorMeter[labels.DataType][labels.Error].Mark(1) +} + // RecordAdapterPanic implements a part of the MetricsEngine interface func (me *Metrics) RecordAdapterPanic(labels AdapterLabels) { am, ok := me.AdapterMetrics[labels.Adapter] @@ -439,6 +526,34 @@ func (me *Metrics) RecordAdapterRequest(labels AdapterLabels) { } } +// Keeps track of created and reused connections to adapter bidders and the time from the +// connection request, to the connection creation, or reuse from the pool across all engines +func (me *Metrics) RecordAdapterConnections(adapterName openrtb_ext.BidderName, + connWasReused bool, + connWaitTime time.Duration) { + + if me.MetricsDisabled.AdapterConnectionMetrics { + return + } + + am, ok := me.AdapterMetrics[adapterName] + if !ok { + glog.Errorf("Trying to log adapter connection metrics for %s: adapter not found", string(adapterName)) + return + } + + if connWasReused { + am.ConnReused.Inc(1) + } else { + am.ConnCreated.Inc(1) + } + am.ConnWaitTime.Update(connWaitTime) +} + +func (me *Metrics) RecordDNSTime(dnsLookupTime time.Duration) { + me.DNSLookupTimer.Update(dnsLookupTime) +} + // RecordAdapterBidReceived implements a part of the MetricsEngine interface. // This tracks how many bids from each Bidder use `adm` vs. `nurl. func (me *Metrics) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { @@ -536,6 +651,12 @@ func (me *Metrics) RecordStoredImpCacheResult(cacheResult CacheResult, inc int) me.StoredImpCacheMeter[cacheResult].Mark(int64(inc)) } +// RecordAccountCacheResult implements a part of the MetricsEngine interface. Records the +// cache hits and misses when looking up accounts. +func (me *Metrics) RecordAccountCacheResult(cacheResult CacheResult, inc int) { + me.AccountCacheMeter[cacheResult].Mark(int64(inc)) +} + // RecordPrebidCacheRequestTime implements a part of the MetricsEngine interface. Records the // amount of time taken to store the auction result in Prebid Cache. func (me *Metrics) RecordPrebidCacheRequestTime(success bool, length time.Duration) { @@ -562,6 +683,32 @@ func (me *Metrics) RecordTimeoutNotice(success bool) { return } +func (me *Metrics) RecordRequestPrivacy(privacy PrivacyLabels) { + if privacy.CCPAProvided { + me.PrivacyCCPARequest.Mark(1) + if privacy.CCPAEnforced { + me.PrivacyCCPARequestOptOut.Mark(1) + } + } + + if privacy.COPPAEnforced { + me.PrivacyCOPPARequest.Mark(1) + } + + if privacy.GDPREnforced { + if metric, ok := me.PrivacyTCFRequestVersion[privacy.GDPRTCFVersion]; ok { + metric.Mark(1) + } else { + me.PrivacyTCFRequestVersion[TCFVersionErr].Mark(1) + } + } + + if privacy.LMTEnforced { + me.PrivacyLMTRequest.Mark(1) + } + return +} + // RecordAdapterDuplicateBidID as noop func (me *Metrics) RecordAdapterDuplicateBidID(adaptor string, collisions int) { } diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go index d888385da16..f55e5c9cecc 100644 --- a/pbsmetrics/go_metrics_test.go +++ b/pbsmetrics/go_metrics_test.go @@ -2,6 +2,7 @@ package pbsmetrics import ( "testing" + "time" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" @@ -56,6 +57,14 @@ func TestNewMetrics(t *testing.T) { ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess) ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure) + + ensureContains(t, registry, "privacy.request.ccpa.specified", m.PrivacyCCPARequest) + ensureContains(t, registry, "privacy.request.ccpa.opt-out", m.PrivacyCCPARequestOptOut) + ensureContains(t, registry, "privacy.request.coppa", m.PrivacyCOPPARequest) + ensureContains(t, registry, "privacy.request.lmt", m.PrivacyLMTRequest) + ensureContains(t, registry, "privacy.request.tcf.v1", m.PrivacyTCFRequestVersion[TCFVersionV1]) + ensureContains(t, registry, "privacy.request.tcf.v2", m.PrivacyTCFRequestVersion[TCFVersionV2]) + ensureContains(t, registry, "privacy.request.tcf.err", m.PrivacyTCFRequestVersion[TCFVersionErr]) } func TestRecordBidType(t *testing.T) { @@ -107,6 +116,10 @@ func ensureContainsAdapterMetrics(t *testing.T, registry metrics.Registry, name ensureContains(t, registry, name+".request_time", adapterMetrics.RequestTimer) ensureContains(t, registry, name+".prices", adapterMetrics.PriceHistogram) ensureContainsBidTypeMetrics(t, registry, name, adapterMetrics.MarkupMetrics) + + ensureContains(t, registry, name+".connections_created", adapterMetrics.ConnCreated) + ensureContains(t, registry, name+".connections_reused", adapterMetrics.ConnReused) + ensureContains(t, registry, name+".connection_wait_time", adapterMetrics.ConnWaitTime) } func TestRecordBidTypeDisabledConfig(t *testing.T) { @@ -171,6 +184,140 @@ func TestRecordBidTypeDisabledConfig(t *testing.T) { } } +func TestRecordDNSTime(t *testing.T) { + testCases := []struct { + description string + inDnsLookupDuration time.Duration + outExpDuration time.Duration + }{ + { + description: "Five second DNS lookup time", + inDnsLookupDuration: time.Second * 5, + outExpDuration: time.Second * 5, + }, + { + description: "Zero DNS lookup time", + inDnsLookupDuration: time.Duration(0), + outExpDuration: time.Duration(0), + }, + } + for _, test := range testCases { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true}) + + m.RecordDNSTime(test.inDnsLookupDuration) + + assert.Equal(t, test.outExpDuration.Nanoseconds(), m.DNSLookupTimer.Sum(), test.description) + } +} + +func TestRecordAdapterConnections(t *testing.T) { + var fakeBidder openrtb_ext.BidderName = "fooAdvertising" + + type testIn struct { + adapterName openrtb_ext.BidderName + connWasReused bool + connWait time.Duration + connMetricsDisabled bool + } + + type testOut struct { + expectedConnReusedCount int64 + expectedConnCreatedCount int64 + expectedConnWaitTime time.Duration + } + + testCases := []struct { + description string + in testIn + out testOut + }{ + { + description: "Successful, new connection created, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 5, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnReusedCount: 0, + expectedConnCreatedCount: 1, + expectedConnWaitTime: time.Second * 5, + }, + }, + { + description: "Successful, new connection created, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 4, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnCreatedCount: 1, + expectedConnWaitTime: time.Second * 4, + }, + }, + { + description: "Successful, was reused, no connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnWaitTime: 0, + }, + }, + { + description: "Successful, was reused, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + connWait: time.Second * 5, + connMetricsDisabled: false, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnWaitTime: time.Second * 5, + }, + }, + { + description: "Fake bidder, nothing gets updated", + in: testIn{ + adapterName: fakeBidder, + connWasReused: false, + connWait: 0, + connMetricsDisabled: false, + }, + out: testOut{}, + }, + { + description: "Adapter connection metrics are disabled, nothing gets updated", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 5, + connMetricsDisabled: true, + }, + out: testOut{}, + }, + } + + for i, test := range testCases { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AdapterConnectionMetrics: test.in.connMetricsDisabled}) + + m.RecordAdapterConnections(test.in.adapterName, test.in.connWasReused, test.in.connWait) + + assert.Equal(t, test.out.expectedConnReusedCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnReused.Count(), "Test [%d] incorrect number of reused connections to adapter", i) + assert.Equal(t, test.out.expectedConnCreatedCount, m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnCreated.Count(), "Test [%d] incorrect number of new connections to adapter created", i) + assert.Equal(t, test.out.expectedConnWaitTime.Nanoseconds(), m.AdapterMetrics[openrtb_ext.BidderAppnexus].ConnWaitTime.Sum(), "Test [%d] incorrect wait time in connection to adapter", i) + } +} + func TestNewMetricsWithDisabledConfig(t *testing.T) { registry := metrics.NewRegistry() m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true}) @@ -198,6 +345,206 @@ func TestRecordPrebidCacheRequestTimeWithNotSuccess(t *testing.T) { assert.Equal(t, m.PrebidCacheRequestTimerError.Count(), int64(1)) } +func TestRecordStoredDataFetchTime(t *testing.T) { + tests := []struct { + description string + dataType StoredDataType + fetchType StoredDataFetchType + }{ + { + description: "Update stored_account_fetch_time.all timer", + dataType: AccountDataType, + fetchType: FetchAll, + }, + { + description: "Update stored_amp_fetch_time.all timer", + dataType: AMPDataType, + fetchType: FetchAll, + }, + { + description: "Update stored_category_fetch_time.all timer", + dataType: CategoryDataType, + fetchType: FetchAll, + }, + { + description: "Update stored_request_fetch_time.all timer", + dataType: RequestDataType, + fetchType: FetchAll, + }, + { + description: "Update stored_video_fetch_time.all timer", + dataType: VideoDataType, + fetchType: FetchAll, + }, + { + description: "Update stored_account_fetch_time.delta timer", + dataType: AccountDataType, + fetchType: FetchDelta, + }, + { + description: "Update stored_amp_fetch_time.delta timer", + dataType: AMPDataType, + fetchType: FetchDelta, + }, + { + description: "Update stored_category_fetch_time.delta timer", + dataType: CategoryDataType, + fetchType: FetchDelta, + }, + { + description: "Update stored_request_fetch_time.delta timer", + dataType: RequestDataType, + fetchType: FetchDelta, + }, + { + description: "Update stored_video_fetch_time.delta timer", + dataType: VideoDataType, + fetchType: FetchDelta, + }, + } + + for _, tt := range tests { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true}) + m.RecordStoredDataFetchTime(StoredDataLabels{ + DataType: tt.dataType, + DataFetchType: tt.fetchType, + }, time.Duration(500)) + + actualCount := m.StoredDataFetchTimer[tt.dataType][tt.fetchType].Count() + assert.Equal(t, int64(1), actualCount, tt.description) + + actualDuration := m.StoredDataFetchTimer[tt.dataType][tt.fetchType].Sum() + assert.Equal(t, int64(500), actualDuration, tt.description) + } +} + +func TestRecordStoredDataError(t *testing.T) { + tests := []struct { + description string + dataType StoredDataType + errorType StoredDataError + }{ + { + description: "Increment stored_account_error.network meter", + dataType: AccountDataType, + errorType: StoredDataErrorNetwork, + }, + { + description: "Increment stored_amp_error.network meter", + dataType: AMPDataType, + errorType: StoredDataErrorNetwork, + }, + { + description: "Increment stored_category_error.network meter", + dataType: CategoryDataType, + errorType: StoredDataErrorNetwork, + }, + { + description: "Increment stored_request_error.network meter", + dataType: RequestDataType, + errorType: StoredDataErrorNetwork, + }, + { + description: "Increment stored_video_error.network meter", + dataType: VideoDataType, + errorType: StoredDataErrorNetwork, + }, + { + description: "Increment stored_account_error.undefined meter", + dataType: AccountDataType, + errorType: StoredDataErrorUndefined, + }, + { + description: "Increment stored_amp_error.undefined meter", + dataType: AMPDataType, + errorType: StoredDataErrorUndefined, + }, + { + description: "Increment stored_category_error.undefined meter", + dataType: CategoryDataType, + errorType: StoredDataErrorUndefined, + }, + { + description: "Increment stored_request_error.undefined meter", + dataType: RequestDataType, + errorType: StoredDataErrorUndefined, + }, + { + description: "Increment stored_video_error.undefined meter", + dataType: VideoDataType, + errorType: StoredDataErrorUndefined, + }, + } + + for _, tt := range tests { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true}) + m.RecordStoredDataError(StoredDataLabels{ + DataType: tt.dataType, + Error: tt.errorType, + }) + + actualCount := m.StoredDataErrorMeter[tt.dataType][tt.errorType].Count() + assert.Equal(t, int64(1), actualCount, tt.description) + } +} + +func TestRecordRequestPrivacy(t *testing.T) { + registry := metrics.NewRegistry() + m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true}) + + // CCPA + m.RecordRequestPrivacy(PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: true, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: false, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + CCPAEnforced: false, + CCPAProvided: true, + }) + + // COPPA + m.RecordRequestPrivacy(PrivacyLabels{ + COPPAEnforced: true, + }) + + // LMT + m.RecordRequestPrivacy(PrivacyLabels{ + LMTEnforced: true, + }) + + // GDPR + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionErr, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionV1, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionV2, + }) + m.RecordRequestPrivacy(PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: TCFVersionV1, + }) + + assert.Equal(t, m.PrivacyCCPARequest.Count(), int64(2), "CCPA") + assert.Equal(t, m.PrivacyCCPARequestOptOut.Count(), int64(1), "CCPA Opt Out") + assert.Equal(t, m.PrivacyCOPPARequest.Count(), int64(1), "COPPA") + assert.Equal(t, m.PrivacyLMTRequest.Count(), int64(1), "LMT") + assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionErr].Count(), int64(1), "TCF Err") + assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionV1].Count(), int64(2), "TCF V1") + assert.Equal(t, m.PrivacyTCFRequestVersion[TCFVersionV2].Count(), int64(1), "TCF V2") +} + func ensureContainsBidTypeMetrics(t *testing.T, registry metrics.Registry, prefix string, mdm map[openrtb_ext.BidType]*MarkupDeliveryMetrics) { ensureContains(t, registry, prefix+".banner.adm_bids_received", mdm[openrtb_ext.BidTypeBanner].AdmMeter) ensureContains(t, registry, prefix+".banner.nurl_bids_received", mdm[openrtb_ext.BidTypeBanner].NurlMeter) diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go index a00e24bc7a6..b65a4905296 100644 --- a/pbsmetrics/metrics.go +++ b/pbsmetrics/metrics.go @@ -50,6 +50,70 @@ type RequestLabels struct { RequestStatus RequestStatus } +// PrivacyLabels defines metrics describing the result of privacy enforcement. +type PrivacyLabels struct { + CCPAEnforced bool + CCPAProvided bool + COPPAEnforced bool + GDPREnforced bool + GDPRTCFVersion TCFVersionValue + LMTEnforced bool +} + +type StoredDataType string + +const ( + AccountDataType StoredDataType = "account" + AMPDataType StoredDataType = "amp" + CategoryDataType StoredDataType = "category" + RequestDataType StoredDataType = "request" + VideoDataType StoredDataType = "video" +) + +func StoredDataTypes() []StoredDataType { + return []StoredDataType{ + AccountDataType, + AMPDataType, + CategoryDataType, + RequestDataType, + VideoDataType, + } +} + +type StoredDataFetchType string + +const ( + FetchAll StoredDataFetchType = "all" + FetchDelta StoredDataFetchType = "delta" +) + +func StoredDataFetchTypes() []StoredDataFetchType { + return []StoredDataFetchType{ + FetchAll, + FetchDelta, + } +} + +type StoredDataLabels struct { + DataType StoredDataType + DataFetchType StoredDataFetchType + Error StoredDataError +} + +type StoredDataError string + +const ( + StoredDataErrorNetwork StoredDataError = "network" + StoredDataErrorUndefined StoredDataError = "undefined" +) + +func StoredDataErrors() []StoredDataError { + return []StoredDataError{ + StoredDataErrorNetwork, + StoredDataErrorUndefined, + } +} + // Label typecasting. Se below the type definitions for possible values // DemandSource : Demand source enumeration @@ -257,6 +321,35 @@ func RequestActions() []RequestAction { } } +// TCFVersionValue : The possible values for TCF versions +type TCFVersionValue string + +const ( + TCFVersionErr TCFVersionValue = "err" + TCFVersionV1 TCFVersionValue = "v1" + TCFVersionV2 TCFVersionValue = "v2" +) + +// TCFVersions returns the possible values for the TCF version +func TCFVersions() []TCFVersionValue { + return []TCFVersionValue{ + TCFVersionErr, + TCFVersionV1, + TCFVersionV2, + } +} + +// TCFVersionToValue takes an integer TCF version and returns the corresponding TCFVersionValue +func TCFVersionToValue(version int) TCFVersionValue { + switch { + case version == 1: + return TCFVersionV1 + case version == 2: + return TCFVersionV2 + } + return TCFVersionErr +} + // 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 number of incoming requests. The remaining 5 fire off per outgoing @@ -271,6 +364,8 @@ type MetricsEngine interface { RecordLegacyImps(labels Labels, numImps int) // RecordImps for the legacy engine RecordRequestTime(labels Labels, length time.Duration) // ignores adapter. only statusOk and statusErr fom status RecordAdapterRequest(labels AdapterLabels) + RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) + RecordDNSTime(dnsLookupTime time.Duration) RecordAdapterPanic(labels AdapterLabels) // This records whether or not a bid of a particular type uses `adm` or `nurl`. // Since the legacy endpoints don't have a bid type, it can only count bids from OpenRTB and AMP. @@ -282,9 +377,14 @@ type MetricsEngine interface { RecordUserIDSet(userLabels UserLabels) // Function should verify bidder values RecordStoredReqCacheResult(cacheResult CacheResult, inc int) RecordStoredImpCacheResult(cacheResult CacheResult, inc int) + RecordAccountCacheResult(cacheResult CacheResult, inc int) + RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration) + RecordStoredDataError(labels StoredDataLabels) RecordPrebidCacheRequestTime(success bool, length time.Duration) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) RecordTimeoutNotice(sucess bool) + RecordRequestPrivacy(privacy PrivacyLabels) + // RecordAdapterDuplicateBidID captures the bid.ID collisions when adaptor // gives the bid response with multiple bids containing same bid.ID RecordAdapterDuplicateBidID(adaptor string, collisions int) diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go index dac5c88e7ac..8f6710e6339 100644 --- a/pbsmetrics/metrics_mock.go +++ b/pbsmetrics/metrics_mock.go @@ -42,6 +42,16 @@ func (me *MetricsEngineMock) RecordRequestTime(labels Labels, length time.Durati me.Called(labels, length) } +// RecordStoredDataFetchTime mock +func (me *MetricsEngineMock) RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration) { + me.Called(labels, length) +} + +// RecordStoredDataError mock +func (me *MetricsEngineMock) RecordStoredDataError(labels StoredDataLabels) { + me.Called(labels) +} + // RecordAdapterPanic mock func (me *MetricsEngineMock) RecordAdapterPanic(labels AdapterLabels) { me.Called(labels) @@ -52,6 +62,16 @@ func (me *MetricsEngineMock) RecordAdapterRequest(labels AdapterLabels) { me.Called(labels) } +// RecordAdapterConnections mock +func (me *MetricsEngineMock) RecordAdapterConnections(bidderName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { + me.Called(bidderName, connWasReused, connWaitTime) +} + +// RecordDNSTime mock +func (me *MetricsEngineMock) RecordDNSTime(dnsLookupTime time.Duration) { + me.Called(dnsLookupTime) +} + // RecordAdapterBidReceived mock func (me *MetricsEngineMock) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) { me.Called(labels, bidType, hasAdm) @@ -92,6 +112,11 @@ func (me *MetricsEngineMock) RecordStoredImpCacheResult(cacheResult CacheResult, me.Called(cacheResult, inc) } +// RecordAccountCacheResult mock +func (me *MetricsEngineMock) RecordAccountCacheResult(cacheResult CacheResult, inc int) { + me.Called(cacheResult, inc) +} + // RecordPrebidCacheRequestTime mock func (me *MetricsEngineMock) RecordPrebidCacheRequestTime(success bool, length time.Duration) { me.Called(success, length) @@ -107,6 +132,11 @@ func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) { me.Called(success) } +// RecordRequestPrivacy mock +func (me *MetricsEngineMock) RecordRequestPrivacy(privacy PrivacyLabels) { + me.Called(privacy) +} + // RecordAdapterDuplicateBidID mock func (me *MetricsEngineMock) RecordAdapterDuplicateBidID(adaptor string, collisions int) { me.Called(adaptor, collisions) diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go index e27451c4bd6..4091d19ea3f 100644 --- a/pbsmetrics/prometheus/preload.go +++ b/pbsmetrics/prometheus/preload.go @@ -7,16 +7,19 @@ import ( func preloadLabelValues(m *Metrics) { var ( - actionValues = actionsAsString() - adapterValues = adaptersAsString() - adapterErrorValues = adapterErrorsAsString() - bidTypeValues = []string{markupDeliveryAdm, markupDeliveryNurl} - boolValues = boolValuesAsString() - cacheResultValues = cacheResultsAsString() - cookieValues = cookieTypesAsString() - connectionErrorValues = []string{connectionAcceptError, connectionCloseError} - requestStatusValues = requestStatusesAsString() - requestTypeValues = requestTypesAsString() + actionValues = actionsAsString() + adapterErrorValues = adapterErrorsAsString() + adapterValues = adaptersAsString() + bidTypeValues = []string{markupDeliveryAdm, markupDeliveryNurl} + boolValues = boolValuesAsString() + cacheResultValues = cacheResultsAsString() + connectionErrorValues = []string{connectionAcceptError, connectionCloseError} + cookieValues = cookieTypesAsString() + requestStatusValues = requestStatusesAsString() + requestTypeValues = requestTypesAsString() + storedDataFetchTypeValues = storedDataFetchTypesAsString() + storedDataErrorValues = storedDataErrorsAsString() + sourceValues = []string{sourceRequest} ) preloadLabelValuesForCounter(m.connectionsError, map[string][]string{ @@ -43,6 +46,46 @@ func preloadLabelValues(m *Metrics) { requestTypeLabel: requestTypeValues, }) + preloadLabelValuesForHistogram(m.storedAccountFetchTimer, map[string][]string{ + storedDataFetchTypeLabel: storedDataFetchTypeValues, + }) + + preloadLabelValuesForHistogram(m.storedAMPFetchTimer, map[string][]string{ + storedDataFetchTypeLabel: storedDataFetchTypeValues, + }) + + preloadLabelValuesForHistogram(m.storedCategoryFetchTimer, map[string][]string{ + storedDataFetchTypeLabel: storedDataFetchTypeValues, + }) + + preloadLabelValuesForHistogram(m.storedRequestFetchTimer, map[string][]string{ + storedDataFetchTypeLabel: storedDataFetchTypeValues, + }) + + preloadLabelValuesForHistogram(m.storedVideoFetchTimer, map[string][]string{ + storedDataFetchTypeLabel: storedDataFetchTypeValues, + }) + + preloadLabelValuesForCounter(m.storedAccountErrors, map[string][]string{ + storedDataErrorLabel: storedDataErrorValues, + }) + + preloadLabelValuesForCounter(m.storedAMPErrors, map[string][]string{ + storedDataErrorLabel: storedDataErrorValues, + }) + + preloadLabelValuesForCounter(m.storedCategoryErrors, map[string][]string{ + storedDataErrorLabel: storedDataErrorValues, + }) + + preloadLabelValuesForCounter(m.storedRequestErrors, map[string][]string{ + storedDataErrorLabel: storedDataErrorValues, + }) + + preloadLabelValuesForCounter(m.storedVideoErrors, map[string][]string{ + storedDataErrorLabel: storedDataErrorValues, + }) + preloadLabelValuesForCounter(m.requestsWithoutCookie, map[string][]string{ requestTypeLabel: requestTypeValues, }) @@ -55,6 +98,10 @@ func preloadLabelValues(m *Metrics) { cacheResultLabel: cacheResultValues, }) + preloadLabelValuesForCounter(m.accountCacheResult, map[string][]string{ + cacheResultLabel: cacheResultValues, + }) + preloadLabelValuesForCounter(m.adapterBids, map[string][]string{ adapterLabel: adapterValues, markupDeliveryLabel: bidTypeValues, @@ -84,6 +131,20 @@ func preloadLabelValues(m *Metrics) { hasBidsLabel: boolValues, }) + if !m.metricsDisabled.AdapterConnectionMetrics { + preloadLabelValuesForCounter(m.adapterCreatedConnections, map[string][]string{ + adapterLabel: adapterValues, + }) + + preloadLabelValuesForCounter(m.adapterReusedConnections, map[string][]string{ + adapterLabel: adapterValues, + }) + + preloadLabelValuesForHistogram(m.adapterConnectionWaitTime, map[string][]string{ + adapterLabel: adapterValues, + }) + } + preloadLabelValuesForHistogram(m.adapterRequestsTimer, map[string][]string{ adapterLabel: adapterValues, }) @@ -99,6 +160,24 @@ func preloadLabelValues(m *Metrics) { requestTypeLabel: {string(pbsmetrics.ReqTypeVideo)}, requestStatusLabel: {requestSuccessLabel, requestRejectLabel}, }) + + preloadLabelValuesForCounter(m.privacyCCPA, map[string][]string{ + sourceLabel: sourceValues, + optOutLabel: boolValues, + }) + + preloadLabelValuesForCounter(m.privacyCOPPA, map[string][]string{ + sourceLabel: sourceValues, + }) + + preloadLabelValuesForCounter(m.privacyLMT, map[string][]string{ + sourceLabel: sourceValues, + }) + + preloadLabelValuesForCounter(m.privacyTCF, map[string][]string{ + sourceLabel: sourceValues, + versionLabel: tcfVersionsAsString(), + }) } func preloadLabelValuesForCounter(counter *prometheus.CounterVec, labelsWithValues map[string][]string) { diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index fe621e779d7..54b7810fef4 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -28,6 +28,23 @@ type Metrics struct { requestsWithoutCookie *prometheus.CounterVec storedImpressionsCacheResult *prometheus.CounterVec storedRequestCacheResult *prometheus.CounterVec + accountCacheResult *prometheus.CounterVec + storedAccountFetchTimer *prometheus.HistogramVec + storedAccountErrors *prometheus.CounterVec + storedAMPFetchTimer *prometheus.HistogramVec + storedAMPErrors *prometheus.CounterVec + storedCategoryFetchTimer *prometheus.HistogramVec + storedCategoryErrors *prometheus.CounterVec + storedRequestFetchTimer *prometheus.HistogramVec + storedRequestErrors *prometheus.CounterVec + storedVideoFetchTimer *prometheus.HistogramVec + storedVideoErrors *prometheus.CounterVec + timeoutNotifications *prometheus.CounterVec + dnsLookupTimer prometheus.Histogram + privacyCCPA *prometheus.CounterVec + privacyCOPPA *prometheus.CounterVec + privacyLMT *prometheus.CounterVec + privacyTCF *prometheus.CounterVec timeout_notifications *prometheus.CounterVec requestsDuplicateBidIDCounter prometheus.Counter // total request having duplicate bid.id for given bidder @@ -40,6 +57,9 @@ type Metrics struct { adapterRequests *prometheus.CounterVec adapterRequestsTimer *prometheus.HistogramVec adapterUserSync *prometheus.CounterVec + adapterReusedConnections *prometheus.CounterVec + adapterCreatedConnections *prometheus.CounterVec + adapterConnectionWaitTime *prometheus.HistogramVec adapterDuplicateBidIDCounter *prometheus.CounterVec adapterVideoBidDuration *prometheus.HistogramVec @@ -59,6 +79,8 @@ type Metrics struct { // podCompExclTimer indicates time taken by compititve exclusion // algorithm to generate final pod response based on bid response and ad pod request podCompExclTimer *prometheus.HistogramVec + + metricsDisabled config.DisabledMetrics } const ( @@ -76,10 +98,12 @@ const ( isNativeLabel = "native" isVideoLabel = "video" markupDeliveryLabel = "delivery" + optOutLabel = "opt_out" privacyBlockedLabel = "privacy_blocked" requestStatusLabel = "request_status" requestTypeLabel = "request_type" successLabel = "success" + versionLabel = "version" ) const ( @@ -110,15 +134,26 @@ const ( podNoOfResponseBids = "no_of_response_bids" ) +const ( + sourceLabel = "source" + sourceRequest = "request" +) + +const ( + storedDataFetchTypeLabel = "stored_data_fetch_type" + storedDataErrorLabel = "stored_data_error" +) + // 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} +func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMetrics) *Metrics { + standardTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 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() + metrics.metricsDisabled = disabledMetrics metrics.connectionsClosed = newCounterWithoutLabels(cfg, metrics.Registry, "connections_closed", @@ -146,7 +181,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "impressions_requests_legacy", "Count of requested impressions to Prebid Server using the legacy endpoint.") - metrics.prebidCacheWriteTimer = newHistogram(cfg, metrics.Registry, + metrics.prebidCacheWriteTimer = newHistogramVec(cfg, metrics.Registry, "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}, @@ -157,11 +192,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of total requests to Prebid Server labeled by type and status.", []string{requestTypeLabel, requestStatusLabel}) - metrics.requestsTimer = newHistogram(cfg, metrics.Registry, + metrics.requestsTimer = newHistogramVec(cfg, metrics.Registry, "request_time_seconds", "Seconds to resolve successful Prebid Server requests labeled by type.", []string{requestTypeLabel}, - requestTimeBuckets) + standardTimeBuckets) metrics.requestsWithoutCookie = newCounter(cfg, metrics.Registry, "requests_without_cookie", @@ -178,11 +213,96 @@ 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, + metrics.accountCacheResult = newCounter(cfg, metrics.Registry, + "account_cache_performance", + "Count of account cache lookups by hits or miss.", + []string{cacheResultLabel}) + + metrics.storedAccountFetchTimer = newHistogramVec(cfg, metrics.Registry, + "stored_account_fetch_time_seconds", + "Seconds to fetch stored accounts labeled by fetch type", + []string{storedDataFetchTypeLabel}, + standardTimeBuckets) + + metrics.storedAccountErrors = newCounter(cfg, metrics.Registry, + "stored_account_errors", + "Count of stored account errors by error type", + []string{storedDataErrorLabel}) + + metrics.storedAMPFetchTimer = newHistogramVec(cfg, metrics.Registry, + "stored_amp_fetch_time_seconds", + "Seconds to fetch stored AMP requests labeled by fetch type", + []string{storedDataFetchTypeLabel}, + standardTimeBuckets) + + metrics.storedAMPErrors = newCounter(cfg, metrics.Registry, + "stored_amp_errors", + "Count of stored AMP errors by error type", + []string{storedDataErrorLabel}) + + metrics.storedCategoryFetchTimer = newHistogramVec(cfg, metrics.Registry, + "stored_category_fetch_time_seconds", + "Seconds to fetch stored categories labeled by fetch type", + []string{storedDataFetchTypeLabel}, + standardTimeBuckets) + + metrics.storedCategoryErrors = newCounter(cfg, metrics.Registry, + "stored_category_errors", + "Count of stored category errors by error type", + []string{storedDataErrorLabel}) + + metrics.storedRequestFetchTimer = newHistogramVec(cfg, metrics.Registry, + "stored_request_fetch_time_seconds", + "Seconds to fetch stored requests labeled by fetch type", + []string{storedDataFetchTypeLabel}, + standardTimeBuckets) + + metrics.storedRequestErrors = newCounter(cfg, metrics.Registry, + "stored_request_errors", + "Count of stored request errors by error type", + []string{storedDataErrorLabel}) + + metrics.storedVideoFetchTimer = newHistogramVec(cfg, metrics.Registry, + "stored_video_fetch_time_seconds", + "Seconds to fetch stored video labeled by fetch type", + []string{storedDataFetchTypeLabel}, + standardTimeBuckets) + + metrics.storedVideoErrors = newCounter(cfg, metrics.Registry, + "stored_video_errors", + "Count of stored video errors by error type", + []string{storedDataErrorLabel}) + + metrics.timeoutNotifications = newCounter(cfg, metrics.Registry, "timeout_notification", "Count of timeout notifications triggered, and if they were successfully sent.", []string{successLabel}) + metrics.dnsLookupTimer = newHistogram(cfg, metrics.Registry, + "dns_lookup_time", + "Seconds to resolve DNS", + standardTimeBuckets) + + metrics.privacyCCPA = newCounter(cfg, metrics.Registry, + "privacy_ccpa", + "Count of total requests to Prebid Server where CCPA was provided by source and opt-out .", + []string{sourceLabel, optOutLabel}) + + metrics.privacyCOPPA = newCounter(cfg, metrics.Registry, + "privacy_coppa", + "Count of total requests to Prebid Server where the COPPA flag was set by source", + []string{sourceLabel}) + + metrics.privacyTCF = newCounter(cfg, metrics.Registry, + "privacy_tcf", + "Count of TCF versions for requests where GDPR was enforced by source and version.", + []string{versionLabel, sourceLabel}) + + metrics.privacyLMT = newCounter(cfg, metrics.Registry, + "privacy_lmt", + "Count of total requests to Prebid Server where the LMT flag was set by source", + []string{sourceLabel}) + metrics.adapterBids = newCounter(cfg, metrics.Registry, "adapter_bids", "Count of bids labeled by adapter and markup delivery type (adm or nurl).", @@ -203,7 +323,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of panics labeled by adapter.", []string{adapterLabel}) - metrics.adapterPrices = newHistogram(cfg, metrics.Registry, + metrics.adapterPrices = newHistogramVec(cfg, metrics.Registry, "adapter_prices", "Monetary value of the bids labeled by adapter.", []string{adapterLabel}, @@ -214,11 +334,29 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of requests labeled by adapter, if has a cookie, and if it resulted in bids.", []string{adapterLabel, cookieLabel, hasBidsLabel}) - metrics.adapterRequestsTimer = newHistogram(cfg, metrics.Registry, + if !metrics.metricsDisabled.AdapterConnectionMetrics { + metrics.adapterCreatedConnections = newCounter(cfg, metrics.Registry, + "adapter_connection_created", + "Count that keeps track of new connections when contacting adapter bidder endpoints.", + []string{adapterLabel}) + + metrics.adapterReusedConnections = newCounter(cfg, metrics.Registry, + "adapter_connection_reused", + "Count that keeps track of reused connections when contacting adapter bidder endpoints.", + []string{adapterLabel}) + + metrics.adapterConnectionWaitTime = newHistogramVec(cfg, metrics.Registry, + "adapter_connection_wait", + "Seconds from when the connection was requested until it is either created or reused", + []string{adapterLabel}, + standardTimeBuckets) + } + + metrics.adapterRequestsTimer = newHistogramVec(cfg, metrics.Registry, "adapter_request_time_seconds", "Seconds to resolve each successful request labeled by adapter.", []string{adapterLabel}, - requestTimeBuckets) + standardTimeBuckets) metrics.adapterUserSync = newCounter(cfg, metrics.Registry, "adapter_user_sync", @@ -230,7 +368,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of total requests to Prebid Server labeled by account.", []string{accountLabel}) - metrics.requestsQueueTimer = newHistogram(cfg, metrics.Registry, + metrics.requestsQueueTimer = newHistogramVec(cfg, metrics.Registry, "request_queue_time", "Seconds request was waiting in queue", []string{requestTypeLabel, requestStatusLabel}, @@ -246,7 +384,7 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { "Count of number of request where bid collision is detected.") // adpod specific metrics - metrics.podImpGenTimer = newHistogram(cfg, metrics.Registry, + metrics.podImpGenTimer = newHistogramVec(cfg, metrics.Registry, "impr_gen", "Time taken by Ad Pod Impression Generator in seconds", []string{podAlgorithm, podNoOfImpressions}, // 200 µS, 250 µS, 275 µS, 300 µS @@ -254,14 +392,14 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics { // 100 µS, 200 µS, 300 µS, 400 µS, 500 µS, 600 µS, []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) - metrics.podCombGenTimer = newHistogram(cfg, metrics.Registry, + metrics.podCombGenTimer = newHistogramVec(cfg, metrics.Registry, "comb_gen", "Time taken by Ad Pod Combination Generator in seconds", []string{podAlgorithm, podTotalCombinations}, // 200 µS, 250 µS, 275 µS, 300 µS //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) - metrics.podCompExclTimer = newHistogram(cfg, metrics.Registry, + metrics.podCompExclTimer = newHistogramVec(cfg, metrics.Registry, "comp_excl", "Time taken by Ad Pod Compititve Exclusion in seconds", []string{podAlgorithm, podNoOfResponseBids}, // 200 µS, 250 µS, 275 µS, 300 µS @@ -301,7 +439,7 @@ func newCounterWithoutLabels(cfg config.PrometheusMetrics, registry *prometheus. return counter } -func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string, buckets []float64) *prometheus.HistogramVec { +func newHistogramVec(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, labels []string, buckets []float64) *prometheus.HistogramVec { opts := prometheus.HistogramOpts{ Namespace: cfg.Namespace, Subsystem: cfg.Subsystem, @@ -314,6 +452,19 @@ func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, n return histogram } +func newHistogram(cfg config.PrometheusMetrics, registry *prometheus.Registry, name, help string, buckets []float64) prometheus.Histogram { + opts := prometheus.HistogramOpts{ + Namespace: cfg.Namespace, + Subsystem: cfg.Subsystem, + Name: name, + Help: help, + Buckets: buckets, + } + histogram := prometheus.NewHistogram(opts) + registry.MustRegister(histogram) + return histogram +} + func (m *Metrics) RecordConnectionAccept(success bool) { if success { m.connectionsOpened.Inc() @@ -374,6 +525,56 @@ func (m *Metrics) RecordRequestTime(labels pbsmetrics.Labels, length time.Durati } } +func (m *Metrics) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) { + switch labels.DataType { + case pbsmetrics.AccountDataType: + m.storedAccountFetchTimer.With(prometheus.Labels{ + storedDataFetchTypeLabel: string(labels.DataFetchType), + }).Observe(length.Seconds()) + case pbsmetrics.AMPDataType: + m.storedAMPFetchTimer.With(prometheus.Labels{ + storedDataFetchTypeLabel: string(labels.DataFetchType), + }).Observe(length.Seconds()) + case pbsmetrics.CategoryDataType: + m.storedCategoryFetchTimer.With(prometheus.Labels{ + storedDataFetchTypeLabel: string(labels.DataFetchType), + }).Observe(length.Seconds()) + case pbsmetrics.RequestDataType: + m.storedRequestFetchTimer.With(prometheus.Labels{ + storedDataFetchTypeLabel: string(labels.DataFetchType), + }).Observe(length.Seconds()) + case pbsmetrics.VideoDataType: + m.storedVideoFetchTimer.With(prometheus.Labels{ + storedDataFetchTypeLabel: string(labels.DataFetchType), + }).Observe(length.Seconds()) + } +} + +func (m *Metrics) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) { + switch labels.DataType { + case pbsmetrics.AccountDataType: + m.storedAccountErrors.With(prometheus.Labels{ + storedDataErrorLabel: string(labels.Error), + }).Inc() + case pbsmetrics.AMPDataType: + m.storedAMPErrors.With(prometheus.Labels{ + storedDataErrorLabel: string(labels.Error), + }).Inc() + case pbsmetrics.CategoryDataType: + m.storedCategoryErrors.With(prometheus.Labels{ + storedDataErrorLabel: string(labels.Error), + }).Inc() + case pbsmetrics.RequestDataType: + m.storedRequestErrors.With(prometheus.Labels{ + storedDataErrorLabel: string(labels.Error), + }).Inc() + case pbsmetrics.VideoDataType: + m.storedVideoErrors.With(prometheus.Labels{ + storedDataErrorLabel: string(labels.Error), + }).Inc() + } +} + func (m *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) { m.adapterRequests.With(prometheus.Labels{ adapterLabel: string(labels.Adapter), @@ -389,6 +590,32 @@ func (m *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) { } } +// Keeps track of created and reused connections to adapter bidders and the time from the +// connection request, to the connection creation, or reuse from the pool across all engines +func (m *Metrics) RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration) { + if m.metricsDisabled.AdapterConnectionMetrics { + return + } + + if connWasReused { + m.adapterReusedConnections.With(prometheus.Labels{ + adapterLabel: string(adapterName), + }).Inc() + } else { + m.adapterCreatedConnections.With(prometheus.Labels{ + adapterLabel: string(adapterName), + }).Inc() + } + + m.adapterConnectionWaitTime.With(prometheus.Labels{ + adapterLabel: string(adapterName), + }).Observe(connWaitTime.Seconds()) +} + +func (m *Metrics) RecordDNSTime(dnsLookupTime time.Duration) { + m.dnsLookupTimer.Observe(dnsLookupTime.Seconds()) +} + func (m *Metrics) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) { m.adapterPanics.With(prometheus.Labels{ adapterLabel: string(labels.Adapter), @@ -454,6 +681,12 @@ func (m *Metrics) RecordStoredImpCacheResult(cacheResult pbsmetrics.CacheResult, }).Add(float64(inc)) } +func (m *Metrics) RecordAccountCacheResult(cacheResult pbsmetrics.CacheResult, inc int) { + m.accountCacheResult.With(prometheus.Labels{ + cacheResultLabel: string(cacheResult), + }).Add(float64(inc)) +} + func (m *Metrics) RecordPrebidCacheRequestTime(success bool, length time.Duration) { m.prebidCacheWriteTimer.With(prometheus.Labels{ successLabel: strconv.FormatBool(success), @@ -473,16 +706,44 @@ func (m *Metrics) RecordRequestQueueTime(success bool, requestType pbsmetrics.Re func (m *Metrics) RecordTimeoutNotice(success bool) { if success { - m.timeout_notifications.With(prometheus.Labels{ + m.timeoutNotifications.With(prometheus.Labels{ successLabel: requestSuccessful, }).Inc() } else { - m.timeout_notifications.With(prometheus.Labels{ + m.timeoutNotifications.With(prometheus.Labels{ successLabel: requestFailed, }).Inc() } } +func (m *Metrics) RecordRequestPrivacy(privacy pbsmetrics.PrivacyLabels) { + if privacy.CCPAProvided { + m.privacyCCPA.With(prometheus.Labels{ + sourceLabel: sourceRequest, + optOutLabel: strconv.FormatBool(privacy.CCPAEnforced), + }).Inc() + } + + if privacy.COPPAEnforced { + m.privacyCOPPA.With(prometheus.Labels{ + sourceLabel: sourceRequest, + }).Inc() + } + + if privacy.GDPREnforced { + m.privacyTCF.With(prometheus.Labels{ + versionLabel: string(privacy.GDPRTCFVersion), + sourceLabel: sourceRequest, + }).Inc() + } + + if privacy.LMTEnforced { + m.privacyLMT.With(prometheus.Labels{ + sourceLabel: sourceRequest, + }).Inc() + } +} + // RecordAdapterDuplicateBidID captures the bid.ID collisions when adaptor // gives the bid response with multiple bids containing same bid.ID // ensure collisions value is greater than 1. This function will not give any error diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go index d3e42ff636c..9f6a91f9384 100644 --- a/pbsmetrics/prometheus/prometheus_test.go +++ b/pbsmetrics/prometheus/prometheus_test.go @@ -1,7 +1,10 @@ package prometheusmetrics import ( + "fmt" + "strconv" + "testing" "time" @@ -18,7 +21,7 @@ func createMetricsForTesting() *Metrics { Port: 8080, Namespace: "prebid", Subsystem: "server", - }) + }, config.DisabledMetrics{}) } func TestMetricCountGatekeeping(t *testing.T) { @@ -62,7 +65,7 @@ func TestMetricCountGatekeeping(t *testing.T) { // Verify Per-Adapter Cardinality // - This assertion provides a warning for newly added adapter metrics. Threre are 40+ adapters which makes the // cost of new per-adapter metrics rather expensive. Thought should be given when adding new per-adapter metrics. - assert.True(t, perAdapterCardinalityCount <= 22, "Per-Adapter Cardinality") + assert.True(t, perAdapterCardinalityCount <= 27, "Per-Adapter Cardinality count equals %d \n", perAdapterCardinalityCount) } func TestConnectionMetrics(t *testing.T) { @@ -407,6 +410,193 @@ func TestRequestTimeMetric(t *testing.T) { } } +func TestRecordStoredDataFetchTime(t *testing.T) { + tests := []struct { + description string + dataType pbsmetrics.StoredDataType + fetchType pbsmetrics.StoredDataFetchType + }{ + { + description: "Update stored account histogram with all label", + dataType: pbsmetrics.AccountDataType, + fetchType: pbsmetrics.FetchAll, + }, + { + description: "Update stored AMP histogram with all label", + dataType: pbsmetrics.AMPDataType, + fetchType: pbsmetrics.FetchAll, + }, + { + description: "Update stored category histogram with all label", + dataType: pbsmetrics.CategoryDataType, + fetchType: pbsmetrics.FetchAll, + }, + { + description: "Update stored request histogram with all label", + dataType: pbsmetrics.RequestDataType, + fetchType: pbsmetrics.FetchAll, + }, + { + description: "Update stored video histogram with all label", + dataType: pbsmetrics.VideoDataType, + fetchType: pbsmetrics.FetchAll, + }, + { + description: "Update stored account histogram with delta label", + dataType: pbsmetrics.AccountDataType, + fetchType: pbsmetrics.FetchDelta, + }, + { + description: "Update stored AMP histogram with delta label", + dataType: pbsmetrics.AMPDataType, + fetchType: pbsmetrics.FetchDelta, + }, + { + description: "Update stored category histogram with delta label", + dataType: pbsmetrics.CategoryDataType, + fetchType: pbsmetrics.FetchDelta, + }, + { + description: "Update stored request histogram with delta label", + dataType: pbsmetrics.RequestDataType, + fetchType: pbsmetrics.FetchDelta, + }, + { + description: "Update stored video histogram with delta label", + dataType: pbsmetrics.VideoDataType, + fetchType: pbsmetrics.FetchDelta, + }, + } + + for _, tt := range tests { + m := createMetricsForTesting() + + fetchTime := time.Duration(0.5 * float64(time.Second)) + m.RecordStoredDataFetchTime(pbsmetrics.StoredDataLabels{ + DataType: tt.dataType, + DataFetchType: tt.fetchType, + }, fetchTime) + + var metricsTimer *prometheus.HistogramVec + switch tt.dataType { + case pbsmetrics.AccountDataType: + metricsTimer = m.storedAccountFetchTimer + case pbsmetrics.AMPDataType: + metricsTimer = m.storedAMPFetchTimer + case pbsmetrics.CategoryDataType: + metricsTimer = m.storedCategoryFetchTimer + case pbsmetrics.RequestDataType: + metricsTimer = m.storedRequestFetchTimer + case pbsmetrics.VideoDataType: + metricsTimer = m.storedVideoFetchTimer + } + + result := getHistogramFromHistogramVec( + metricsTimer, + storedDataFetchTypeLabel, + string(tt.fetchType)) + assertHistogram(t, tt.description, result, 1, 0.5) + } +} + +func TestRecordStoredDataError(t *testing.T) { + tests := []struct { + description string + dataType pbsmetrics.StoredDataType + errorType pbsmetrics.StoredDataError + metricName string + }{ + { + description: "Update stored_account_errors counter with network label", + dataType: pbsmetrics.AccountDataType, + errorType: pbsmetrics.StoredDataErrorNetwork, + metricName: "stored_account_errors", + }, + { + description: "Update stored_amp_errors counter with network label", + dataType: pbsmetrics.AMPDataType, + errorType: pbsmetrics.StoredDataErrorNetwork, + metricName: "stored_amp_errors", + }, + { + description: "Update stored_category_errors counter with network label", + dataType: pbsmetrics.CategoryDataType, + errorType: pbsmetrics.StoredDataErrorNetwork, + metricName: "stored_category_errors", + }, + { + description: "Update stored_request_errors counter with network label", + dataType: pbsmetrics.RequestDataType, + errorType: pbsmetrics.StoredDataErrorNetwork, + metricName: "stored_request_errors", + }, + { + description: "Update stored_video_errors counter with network label", + dataType: pbsmetrics.VideoDataType, + errorType: pbsmetrics.StoredDataErrorNetwork, + metricName: "stored_video_errors", + }, + { + description: "Update stored_account_errors counter with undefined label", + dataType: pbsmetrics.AccountDataType, + errorType: pbsmetrics.StoredDataErrorUndefined, + metricName: "stored_account_errors", + }, + { + description: "Update stored_amp_errors counter with undefined label", + dataType: pbsmetrics.AMPDataType, + errorType: pbsmetrics.StoredDataErrorUndefined, + metricName: "stored_amp_errors", + }, + { + description: "Update stored_category_errors counter with undefined label", + dataType: pbsmetrics.CategoryDataType, + errorType: pbsmetrics.StoredDataErrorUndefined, + metricName: "stored_category_errors", + }, + { + description: "Update stored_request_errors counter with undefined label", + dataType: pbsmetrics.RequestDataType, + errorType: pbsmetrics.StoredDataErrorUndefined, + metricName: "stored_request_errors", + }, + { + description: "Update stored_video_errors counter with undefined label", + dataType: pbsmetrics.VideoDataType, + errorType: pbsmetrics.StoredDataErrorUndefined, + metricName: "stored_video_errors", + }, + } + + for _, tt := range tests { + m := createMetricsForTesting() + m.RecordStoredDataError(pbsmetrics.StoredDataLabels{ + DataType: tt.dataType, + Error: tt.errorType, + }) + + var metricsCounter *prometheus.CounterVec + switch tt.dataType { + case pbsmetrics.AccountDataType: + metricsCounter = m.storedAccountErrors + case pbsmetrics.AMPDataType: + metricsCounter = m.storedAMPErrors + case pbsmetrics.CategoryDataType: + metricsCounter = m.storedCategoryErrors + case pbsmetrics.RequestDataType: + metricsCounter = m.storedRequestErrors + case pbsmetrics.VideoDataType: + metricsCounter = m.storedVideoErrors + } + + assertCounterVecValue(t, tt.description, tt.metricName, metricsCounter, + 1, + prometheus.Labels{ + storedDataErrorLabel: string(tt.errorType), + }) + } +} + func TestAdapterBidReceivedMetric(t *testing.T) { adapterName := "anyName" performTest := func(m *Metrics, hasAdm bool) { @@ -826,8 +1016,8 @@ func TestStoredReqCacheResultMetric(t *testing.T) { func TestStoredImpCacheResultMetric(t *testing.T) { m := createMetricsForTesting() - hitCount := 42 - missCount := 108 + hitCount := 41 + missCount := 107 m.RecordStoredImpCacheResult(pbsmetrics.CacheHit, hitCount) m.RecordStoredImpCacheResult(pbsmetrics.CacheMiss, missCount) @@ -843,6 +1033,26 @@ func TestStoredImpCacheResultMetric(t *testing.T) { }) } +func TestAccountCacheResultMetric(t *testing.T) { + m := createMetricsForTesting() + + hitCount := 37 + missCount := 92 + m.RecordAccountCacheResult(pbsmetrics.CacheHit, hitCount) + m.RecordAccountCacheResult(pbsmetrics.CacheMiss, missCount) + + assertCounterVecValue(t, "", "accountCacheResult:hit", m.accountCacheResult, + float64(hitCount), + prometheus.Labels{ + cacheResultLabel: string(pbsmetrics.CacheHit), + }) + assertCounterVecValue(t, "", "accountCacheResult:miss", m.accountCacheResult, + float64(missCount), + prometheus.Labels{ + cacheResultLabel: string(pbsmetrics.CacheMiss), + }) +} + func TestCookieMetric(t *testing.T) { m := createMetricsForTesting() @@ -931,13 +1141,13 @@ func TestTimeoutNotifications(t *testing.T) { m.RecordTimeoutNotice(true) m.RecordTimeoutNotice(false) - assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeout_notifications, + assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeoutNotifications, float64(2), prometheus.Labels{ successLabel: requestSuccessful, }) - assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeout_notifications, + assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeoutNotifications, float64(1), prometheus.Labels{ successLabel: requestFailed, @@ -945,6 +1155,268 @@ func TestTimeoutNotifications(t *testing.T) { } +func TestRecordDNSTime(t *testing.T) { + type testIn struct { + dnsLookupDuration time.Duration + } + type testOut struct { + expDuration float64 + expCount uint64 + } + testCases := []struct { + description string + in testIn + out testOut + }{ + { + description: "Five second DNS lookup time", + in: testIn{ + dnsLookupDuration: time.Second * 5, + }, + out: testOut{ + expDuration: 5, + expCount: 1, + }, + }, + { + description: "Zero DNS lookup time", + in: testIn{}, + out: testOut{ + expDuration: 0, + expCount: 1, + }, + }, + } + for i, test := range testCases { + pm := createMetricsForTesting() + pm.RecordDNSTime(test.in.dnsLookupDuration) + + m := dto.Metric{} + pm.dnsLookupTimer.Write(&m) + histogram := *m.GetHistogram() + + assert.Equal(t, test.out.expCount, histogram.GetSampleCount(), "[%d] Incorrect number of histogram entries. Desc: %s\n", i, test.description) + assert.Equal(t, test.out.expDuration, histogram.GetSampleSum(), "[%d] Incorrect number of histogram cumulative values. Desc: %s\n", i, test.description) + } +} + +func TestRecordAdapterConnections(t *testing.T) { + + type testIn struct { + adapterName openrtb_ext.BidderName + connWasReused bool + connWait time.Duration + } + + type testOut struct { + expectedConnReusedCount int64 + expectedConnCreatedCount int64 + expectedConnWaitCount uint64 + expectedConnWaitTime float64 + } + + testCases := []struct { + description string + in testIn + out testOut + }{ + { + description: "[1] Successful, new connection created, was idle, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 5, + }, + out: testOut{ + expectedConnReusedCount: 0, + expectedConnCreatedCount: 1, + expectedConnWaitCount: 1, + expectedConnWaitTime: 5, + }, + }, + { + description: "[2] Successful, new connection created, not idle, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: false, + connWait: time.Second * 4, + }, + out: testOut{ + expectedConnReusedCount: 0, + expectedConnCreatedCount: 1, + expectedConnWaitCount: 1, + expectedConnWaitTime: 4, + }, + }, + { + description: "[3] Successful, was reused, was idle, no connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnCreatedCount: 0, + expectedConnWaitCount: 1, + expectedConnWaitTime: 0, + }, + }, + { + description: "[4] Successful, was reused, not idle, has connection wait", + in: testIn{ + adapterName: openrtb_ext.BidderAppnexus, + connWasReused: true, + connWait: time.Second * 5, + }, + out: testOut{ + expectedConnReusedCount: 1, + expectedConnCreatedCount: 0, + expectedConnWaitCount: 1, + expectedConnWaitTime: 5, + }, + }, + } + + for i, test := range testCases { + m := createMetricsForTesting() + assertDesciptions := []string{ + fmt.Sprintf("[%d] Metric: adapterReusedConnections; Desc: %s", i+1, test.description), + fmt.Sprintf("[%d] Metric: adapterCreatedConnections; Desc: %s", i+1, test.description), + fmt.Sprintf("[%d] Metric: adapterWaitConnectionCount; Desc: %s", i+1, test.description), + fmt.Sprintf("[%d] Metric: adapterWaitConnectionTime; Desc: %s", i+1, test.description), + } + + m.RecordAdapterConnections(test.in.adapterName, test.in.connWasReused, test.in.connWait) + + // Assert number of reused connections + assertCounterVecValue(t, + assertDesciptions[0], + "adapter_connection_reused", + m.adapterReusedConnections, + float64(test.out.expectedConnReusedCount), + prometheus.Labels{adapterLabel: string(test.in.adapterName)}) + + // Assert number of new created connections + assertCounterVecValue(t, + assertDesciptions[1], + "adapter_connection_created", + m.adapterCreatedConnections, + float64(test.out.expectedConnCreatedCount), + prometheus.Labels{adapterLabel: string(test.in.adapterName)}) + + // Assert connection wait time + histogram := getHistogramFromHistogramVec(m.adapterConnectionWaitTime, adapterLabel, string(test.in.adapterName)) + assert.Equal(t, test.out.expectedConnWaitCount, histogram.GetSampleCount(), assertDesciptions[2]) + assert.Equal(t, test.out.expectedConnWaitTime, histogram.GetSampleSum(), assertDesciptions[3]) + } +} + +func TestDisableAdapterConnections(t *testing.T) { + prometheusMetrics := NewMetrics(config.PrometheusMetrics{ + Port: 8080, + Namespace: "prebid", + Subsystem: "server", + }, config.DisabledMetrics{AdapterConnectionMetrics: true}) + + // Assert counter vector was not initialized + assert.Nil(t, prometheusMetrics.adapterReusedConnections, "Counter Vector adapterReusedConnections should be nil") + assert.Nil(t, prometheusMetrics.adapterCreatedConnections, "Counter Vector adapterCreatedConnections should be nil") + assert.Nil(t, prometheusMetrics.adapterConnectionWaitTime, "Counter Vector adapterConnectionWaitTime should be nil") +} + +func TestRecordRequestPrivacy(t *testing.T) { + m := createMetricsForTesting() + + // CCPA + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: true, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + CCPAEnforced: true, + CCPAProvided: false, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + CCPAEnforced: false, + CCPAProvided: true, + }) + + // COPPA + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + COPPAEnforced: true, + }) + + // LMT + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + LMTEnforced: true, + }) + + // GDPR + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionErr, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV2, + }) + m.RecordRequestPrivacy(pbsmetrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: pbsmetrics.TCFVersionV1, + }) + + assertCounterVecValue(t, "", "privacy_ccpa", m.privacyCCPA, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + optOutLabel: "true", + }) + + assertCounterVecValue(t, "", "privacy_ccpa", m.privacyCCPA, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + optOutLabel: "false", + }) + + assertCounterVecValue(t, "", "privacy_coppa", m.privacyCOPPA, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + }) + + assertCounterVecValue(t, "", "privacy_lmt", m.privacyLMT, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + }) + + assertCounterVecValue(t, "", "privacy_tcf:err", m.privacyTCF, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + versionLabel: "err", + }) + + assertCounterVecValue(t, "", "privacy_tcf:v1", m.privacyTCF, + float64(2), + prometheus.Labels{ + sourceLabel: sourceRequest, + versionLabel: "v1", + }) + + assertCounterVecValue(t, "", "privacy_tcf:v2", m.privacyTCF, + float64(1), + prometheus.Labels{ + sourceLabel: sourceRequest, + versionLabel: "v2", + }) +} + // TestRecordRequestDuplicateBidID checks RecordRequestDuplicateBidID func TestRecordRequestDuplicateBidID(t *testing.T) { m := createMetricsForTesting() @@ -1130,6 +1602,8 @@ func getHistogramFromHistogramVecByTwoKeys(histogram *prometheus.HistogramVec, l valInd := ind if ind == 1 { valInd = 0 + } else { + valInd = 1 } if m.Label[valInd].GetName() == label2Key && m.Label[valInd].GetValue() == label2Value { result = *m.GetHistogram() diff --git a/pbsmetrics/prometheus/type_conversion.go b/pbsmetrics/prometheus/type_conversion.go index ad81e84e041..5dc6e9cf29e 100644 --- a/pbsmetrics/prometheus/type_conversion.go +++ b/pbsmetrics/prometheus/type_conversion.go @@ -76,3 +76,39 @@ func requestTypesAsString() []string { } return valuesAsString } + +func storedDataTypesAsString() []string { + values := pbsmetrics.StoredDataTypes() + valuesAsString := make([]string, len(values)) + for i, v := range values { + valuesAsString[i] = string(v) + } + return valuesAsString +} + +func storedDataFetchTypesAsString() []string { + values := pbsmetrics.StoredDataFetchTypes() + valuesAsString := make([]string, len(values)) + for i, v := range values { + valuesAsString[i] = string(v) + } + return valuesAsString +} + +func storedDataErrorsAsString() []string { + values := pbsmetrics.StoredDataErrors() + valuesAsString := make([]string, len(values)) + for i, v := range values { + valuesAsString[i] = string(v) + } + return valuesAsString +} + +func tcfVersionsAsString() []string { + values := pbsmetrics.TCFVersions() + valuesAsString := make([]string, len(values)) + for i, v := range values { + valuesAsString[i] = string(v) + } + return valuesAsString +} diff --git a/prebid/prebid.go b/prebid/prebid.go deleted file mode 100644 index 68c4a48c2c8..00000000000 --- a/prebid/prebid.go +++ /dev/null @@ -1,82 +0,0 @@ -package prebid - -import ( - "net" - "net/http" - "strings" -) - -var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") -var xRealIP = http.CanonicalHeaderKey("X-Real-IP") -var xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") - -// IsSecure attempts to detect whether the request is https -func IsSecure(r *http.Request) bool { - // lowercase for case-insensitive match for X-Forwarded-Proto header - if strings.ToLower(r.Header.Get(xForwardedProto)) == "https" { - return true - } - // ensure that URL.Scheme is lowercase (it should be "https") - if strings.ToLower(r.URL.Scheme) == "https" { - return true - } - // use strings.HasPrefix because a valid example is "HTTP/1.0" - if strings.HasPrefix(r.Proto, "HTTPS") { - return true - } - // check if TLS is not-nil as a final fallback - if r.TLS != nil { - return true - } - return false -} - -// GetIP will attempt to get the IP Address by first checking headers -// and then falling back on the RemoteAddr -func GetIP(r *http.Request) string { - // first check headers - if ip := GetForwardedIP(r); ip != "" { - return ip - } - // next try to parse the RemoteAddr. - // if err is not nil then weird hosts might appear as the ip: https://github.com/golang/go/issues/14827 - if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { - return ip - } - return "" -} - -// GetForwardedIP will return back X-Forwarded-For or X-Real-IP (if set) -func GetForwardedIP(r *http.Request) string { - // first attempt to parse X-Forwarded-For - if ip := getForwardedFor(r); ip != "" { - return ip - } - // if we don't have X-Forwarded-For then try X-Real-IP - if ip := getRealIP(r); ip != "" { - return ip - } - return "" -} - -// getForwardedFor will attempt to parse the X-Forwarded-For header -func getForwardedFor(r *http.Request) string { - if xff := r.Header.Get(xForwardedFor); xff != "" { - // X-Forwarded-For: client1, proxy1, proxy2 - i := strings.Index(xff, ", ") - if i == -1 { - i = len(xff) - } - return xff[:i] - } - return "" -} - -// getRealIP will attempt to parse the X-Real-IP header -// Header.Get is case-insensitive -func getRealIP(r *http.Request) string { - if xrip := r.Header.Get(xRealIP); xrip != "" { - return xrip - } - return "" -} diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go index 314cc3e3d42..df015645145 100644 --- a/prebid_cache_client/client.go +++ b/prebid_cache_client/client.go @@ -29,8 +29,8 @@ type Client interface { // logging any relevant errors to the app logs PutJson(ctx context.Context, values []Cacheable) ([]string, []error) - // Serves the purpose of a getter that returns the host and the cache of the prebid-server URL - GetExtCacheData() (string, string) + // GetExtCacheData gets the scheme, host, and path of the externally accessible cache url. + GetExtCacheData() (scheme string, host string, path string) } type PayloadType string @@ -41,31 +41,37 @@ const ( ) type Cacheable struct { - Type PayloadType - Data json.RawMessage - TTLSeconds int64 - Key string + Type PayloadType `json:"type,omitempty"` + Data json.RawMessage `json:"value,omitempty"` + TTLSeconds int64 `json:"ttlseconds,omitempty"` + Key string `json:"key,omitempty"` + + BidID string `json:"bidid,omitempty"` // this is "/vtrack" specific + Bidder string `json:"bidder,omitempty"` // this is "/vtrack" specific + Timestamp int64 `json:"timestamp,omitempty"` // this is "/vtrack" specific } func NewClient(httpClient *http.Client, conf *config.Cache, extCache *config.ExternalCache, metrics pbsmetrics.MetricsEngine) Client { return &clientImpl{ - httpClient: httpClient, - putUrl: conf.GetBaseURL() + "/cache", - externalCacheHost: extCache.Host, - externalCachePath: extCache.Path, - metrics: metrics, + httpClient: httpClient, + putUrl: conf.GetBaseURL() + "/cache", + externalCacheScheme: extCache.Scheme, + externalCacheHost: extCache.Host, + externalCachePath: extCache.Path, + metrics: metrics, } } type clientImpl struct { - httpClient *http.Client - putUrl string - externalCacheHost string - externalCachePath string - metrics pbsmetrics.MetricsEngine + httpClient *http.Client + putUrl string + externalCacheScheme string + externalCacheHost string + externalCachePath string + metrics pbsmetrics.MetricsEngine } -func (c *clientImpl) GetExtCacheData() (string, string) { +func (c *clientImpl) GetExtCacheData() (string, string, string) { path := c.externalCachePath if path == "/" { // Only the slash for the path, remove it to empty @@ -75,7 +81,7 @@ func (c *clientImpl) GetExtCacheData() (string, string) { path = "/" + path } - return c.externalCacheHost, path + return c.externalCacheScheme, c.externalCacheHost, path } func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []string, errs []error) { @@ -179,6 +185,25 @@ func encodeValueToBuffer(value Cacheable, leadingComma bool, buffer *bytes.Buffe buffer.WriteString(string(value.Key)) buffer.WriteString(`"`) } + + //vtrack specific + if len(value.BidID) > 0 { + buffer.WriteString(`,"bidid":"`) + buffer.WriteString(string(value.BidID)) + buffer.WriteString(`"`) + } + + if len(value.Bidder) > 0 { + buffer.WriteString(`,"bidder":"`) + buffer.WriteString(string(value.Bidder)) + buffer.WriteString(`"`) + } + + if value.Timestamp > 0 { + buffer.WriteString(`,"timestamp":`) + buffer.WriteString(strconv.FormatInt(value.Timestamp, 10)) + } + buffer.WriteByte('}') return nil } diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go index 393aacc2dfe..9dd30d228cf 100644 --- a/prebid_cache_client/client_test.go +++ b/prebid_cache_client/client_test.go @@ -174,8 +174,11 @@ func TestEncodeValueToBuffer(t *testing.T) { Type: TypeJSON, Data: json.RawMessage(`{}`), TTLSeconds: 300, + BidID: "bid", + Bidder: "bdr", + Timestamp: 123456789, } - expected := string(`{"type":"json","ttlseconds":300,"value":{}}`) + expected := string(`{"type":"json","ttlseconds":300,"value":{},"bidid":"bid","bidder":"bdr","timestamp":123456789}`) _ = encodeValueToBuffer(testCache, false, buf) actual := buf.String() assertStringEqual(t, expected, actual) @@ -186,58 +189,80 @@ func TestEncodeValueToBuffer(t *testing.T) { func TestStripCacheHostAndPath(t *testing.T) { inCacheURL := config.Cache{ExpectedTimeMillis: 10} type aTest struct { - inExtCacheURL config.ExternalCache - expectedHost string - expectedPath string + inExtCacheURL config.ExternalCache + expectedScheme string + expectedHost string + expectedPath string } testInput := []aTest{ { inExtCacheURL: config.ExternalCache{ - Host: "prebid-server.prebid.org", - Path: "/pbcache/endpoint", + Scheme: "", + Host: "prebid-server.prebid.org", + Path: "/pbcache/endpoint", }, - expectedHost: "prebid-server.prebid.org", - expectedPath: "/pbcache/endpoint", + expectedScheme: "", + expectedHost: "prebid-server.prebid.org", + expectedPath: "/pbcache/endpoint", }, { inExtCacheURL: config.ExternalCache{ - Host: "prebidcache.net", - Path: "", + Scheme: "https", + Host: "prebid-server.prebid.org", + Path: "/pbcache/endpoint", }, - expectedHost: "prebidcache.net", - expectedPath: "", + expectedScheme: "https", + expectedHost: "prebid-server.prebid.org", + expectedPath: "/pbcache/endpoint", }, { inExtCacheURL: config.ExternalCache{ - Host: "", - Path: "", + Scheme: "", + Host: "prebidcache.net", + Path: "", }, - expectedHost: "", - expectedPath: "", + expectedScheme: "", + expectedHost: "prebidcache.net", + expectedPath: "", }, { inExtCacheURL: config.ExternalCache{ - Host: "prebid-server.prebid.org", - Path: "pbcache/endpoint", + Scheme: "", + Host: "", + Path: "", }, - expectedHost: "prebid-server.prebid.org", - expectedPath: "/pbcache/endpoint", + expectedScheme: "", + expectedHost: "", + expectedPath: "", }, { inExtCacheURL: config.ExternalCache{ - Host: "prebidcache.net", - Path: "/", + Scheme: "", + Host: "prebid-server.prebid.org", + Path: "pbcache/endpoint", }, - expectedHost: "prebidcache.net", - expectedPath: "", + expectedScheme: "", + expectedHost: "prebid-server.prebid.org", + expectedPath: "/pbcache/endpoint", + }, + { + inExtCacheURL: config.ExternalCache{ + Scheme: "", + Host: "prebidcache.net", + Path: "/", + }, + expectedScheme: "", + expectedHost: "prebidcache.net", + expectedPath: "", }, } for _, test := range testInput { cacheClient := NewClient(&http.Client{}, &inCacheURL, &test.inExtCacheURL, &metricsConf.DummyMetricsEngine{}) - cHost, cPath := cacheClient.GetExtCacheData() + scheme, host, path := cacheClient.GetExtCacheData() - assert.Equal(t, test.expectedHost, cHost) - assert.Equal(t, test.expectedPath, cPath) + assert.Equal(t, test.expectedScheme, scheme) + assert.Equal(t, test.expectedHost, host) + assert.Equal(t, test.expectedPath, path) } } diff --git a/privacy/ccpa/consentwriter.go b/privacy/ccpa/consentwriter.go new file mode 100644 index 00000000000..4ef412fd3ef --- /dev/null +++ b/privacy/ccpa/consentwriter.go @@ -0,0 +1,25 @@ +package ccpa + +import ( + "github.com/PubMatic-OpenWrap/openrtb" +) + +// ConsentWriter implements the PolicyWriter interface for CCPA. +type ConsentWriter struct { + Consent string +} + +// Write mutates an OpenRTB bid request with the CCPA consent string. +func (c ConsentWriter) Write(req *openrtb.BidRequest) error { + if req == nil { + return nil + } + + regs, err := buildRegs(c.Consent, req.Regs) + if err != nil { + return err + } + req.Regs = regs + + return nil +} diff --git a/privacy/ccpa/consentwriter_test.go b/privacy/ccpa/consentwriter_test.go new file mode 100644 index 00000000000..1e491d9d167 --- /dev/null +++ b/privacy/ccpa/consentwriter_test.go @@ -0,0 +1,51 @@ +package ccpa + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestConsentWriter(t *testing.T) { + consent := "anyConsent" + testCases := []struct { + description string + request *openrtb.BidRequest + expected *openrtb.BidRequest + expectedError bool + }{ + { + description: "Nil Request", + request: nil, + expected: nil, + }, + { + description: "Success", + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + }, + }, + { + description: "Error With Regs.Ext - Does Not Mutate", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + expectedError: true, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + }, + } + + for _, test := range testCases { + writer := ConsentWriter{consent} + + err := writer.Write(test.request) + + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, test.request, test.description) + } +} diff --git a/privacy/ccpa/parsedpolicy.go b/privacy/ccpa/parsedpolicy.go new file mode 100644 index 00000000000..52977104716 --- /dev/null +++ b/privacy/ccpa/parsedpolicy.go @@ -0,0 +1,137 @@ +package ccpa + +import ( + "errors" + "fmt" + + "github.com/PubMatic-OpenWrap/prebid-server/errortypes" +) + +const ( + ccpaVersion1 = '1' + ccpaYes = 'Y' + ccpaNo = 'N' + ccpaNotApplicable = '-' +) + +const ( + indexVersion = 0 + indexExplicitNotice = 1 + indexOptOutSale = 2 + indexLSPACoveredTransaction = 3 +) + +const allBiddersMarker = "*" + +// ValidateConsent returns true if the consent string is empty or valid per the IAB CCPA spec. +func ValidateConsent(consent string) bool { + _, err := parseConsent(consent) + return err == nil +} + +// ParsedPolicy represents parsed and validated CCPA regulatory information. Use this struct +// to make enforcement decisions. +type ParsedPolicy struct { + consentSpecified bool + consentOptOutSale bool + noSaleForAllBidders bool + noSaleSpecificBidders map[string]struct{} +} + +// Parse returns a parsed and validated ParsedPolicy intended for use in enforcement decisions. +func (p Policy) Parse(validBidders map[string]struct{}) (ParsedPolicy, error) { + consentOptOut, err := parseConsent(p.Consent) + if err != nil { + msg := fmt.Sprintf("request.regs.ext.us_privacy %s", err.Error()) + return ParsedPolicy{}, &errortypes.InvalidPrivacyConsent{Message: msg} + } + + noSaleForAllBidders, noSaleSpecificBidders, err := parseNoSaleBidders(p.NoSaleBidders, validBidders) + if err != nil { + return ParsedPolicy{}, fmt.Errorf("request.ext.prebid.nosale is invalid: %s", err.Error()) + } + + return ParsedPolicy{ + consentSpecified: p.Consent != "", + consentOptOutSale: consentOptOut, + noSaleForAllBidders: noSaleForAllBidders, + noSaleSpecificBidders: noSaleSpecificBidders, + }, nil +} + +func parseConsent(consent string) (consentOptOutSale bool, err error) { + if consent == "" { + return false, nil + } + + if len(consent) != 4 { + return false, errors.New("must contain 4 characters") + } + + if consent[indexVersion] != ccpaVersion1 { + return false, errors.New("must specify version 1") + } + + var c byte + + c = consent[indexExplicitNotice] + if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable { + return false, errors.New("must specify 'N', 'Y', or '-' for the explicit notice") + } + + c = consent[indexOptOutSale] + if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable { + return false, errors.New("must specify 'N', 'Y', or '-' for the opt-out sale") + } + + c = consent[indexLSPACoveredTransaction] + if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable { + return false, errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement") + } + + return consent[indexOptOutSale] == ccpaYes, nil +} + +func parseNoSaleBidders(noSaleBidders []string, validBidders map[string]struct{}) (noSaleForAllBidders bool, noSaleSpecificBidders map[string]struct{}, err error) { + noSaleSpecificBidders = make(map[string]struct{}) + + if len(noSaleBidders) == 1 && noSaleBidders[0] == allBiddersMarker { + noSaleForAllBidders = true + return + } + + for _, bidder := range noSaleBidders { + if bidder == allBiddersMarker { + err = errors.New("can only specify all bidders if no other bidders are provided") + return + } + + if _, exists := validBidders[bidder]; exists { + noSaleSpecificBidders[bidder] = struct{}{} + } else { + err = fmt.Errorf("unrecognized bidder '%s'", bidder) + return + } + } + + return +} + +// CanEnforce returns true when consent is specifically provided by the publisher, as opposed to an empty string. +func (p ParsedPolicy) CanEnforce() bool { + return p.consentSpecified +} + +func (p ParsedPolicy) isNoSaleForBidder(bidder string) bool { + if p.noSaleForAllBidders { + return true + } + + _, exists := p.noSaleSpecificBidders[bidder] + return exists +} + +// ShouldEnforce returns true when the opt-out signal is explicitly detected. +func (p ParsedPolicy) ShouldEnforce(bidder string) bool { + return !p.isNoSaleForBidder(bidder) && p.consentOptOutSale +} diff --git a/privacy/ccpa/parsedpolicy_test.go b/privacy/ccpa/parsedpolicy_test.go new file mode 100644 index 00000000000..4fa9f92684d --- /dev/null +++ b/privacy/ccpa/parsedpolicy_test.go @@ -0,0 +1,391 @@ +package ccpa + +import ( + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestValidateConsent(t *testing.T) { + testCases := []struct { + description string + consent string + expected bool + }{ + { + description: "Empty String", + consent: "", + expected: true, + }, + { + description: "Valid Consent With Opt Out", + consent: "1NYN", + expected: true, + }, + { + description: "Valid Consent Without Opt Out", + consent: "1NNN", + expected: true, + }, + { + description: "Invalid", + consent: "malformed", + expected: false, + }, + } + + for _, test := range testCases { + result := ValidateConsent(test.consent) + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestParse(t *testing.T) { + validBidders := map[string]struct{}{"a": {}} + + testCases := []struct { + description string + consent string + noSaleBidders []string + expectedPolicy ParsedPolicy + expectedError string + }{ + { + description: "Consent Error", + consent: "malformed", + noSaleBidders: []string{}, + expectedPolicy: ParsedPolicy{}, + expectedError: "request.regs.ext.us_privacy must contain 4 characters", + }, + { + description: "No Sale Error", + consent: "1NYN", + noSaleBidders: []string{"b"}, + expectedPolicy: ParsedPolicy{}, + expectedError: "request.ext.prebid.nosale is invalid: unrecognized bidder 'b'", + }, + { + description: "Success", + consent: "1NYN", + noSaleBidders: []string{"a"}, + expectedPolicy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"a": {}}, + }, + }, + } + + for _, test := range testCases { + policy := Policy{test.consent, test.noSaleBidders} + + result, err := policy.Parse(validBidders) + + if test.expectedError == "" { + assert.NoError(t, err, test.description) + } else { + assert.EqualError(t, err, test.expectedError, test.description) + } + + assert.Equal(t, test.expectedPolicy, result, test.description) + } +} + +func TestParseConsent(t *testing.T) { + testCases := []struct { + description string + consent string + expectedResult bool + expectedError string + }{ + { + description: "Valid", + consent: "1NYN", + expectedResult: true, + }, + { + description: "Valid - Not Sale", + consent: "1NNN", + expectedResult: false, + }, + { + description: "Valid - Not Applicable", + consent: "1---", + expectedResult: false, + }, + { + description: "Valid - Empty", + consent: "", + expectedResult: false, + }, + { + description: "Wrong Length", + consent: "1NY", + expectedResult: false, + expectedError: "must contain 4 characters", + }, + { + description: "Wrong Version", + consent: "2---", + expectedResult: false, + expectedError: "must specify version 1", + }, + { + description: "Explicit Notice Char", + consent: "1X--", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + }, + { + description: "Invalid Explicit Notice Case", + consent: "1y--", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + }, + { + description: "Invalid Opt-Out Sale Char", + consent: "1-X-", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + }, + { + description: "Invalid Opt-Out Sale Case", + consent: "1-y-", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + }, + { + description: "Invalid LSPA Char", + consent: "1--X", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + }, + { + description: "Invalid LSPA Case", + consent: "1--y", + expectedResult: false, + expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + }, + } + + for _, test := range testCases { + result, err := parseConsent(test.consent) + + if test.expectedError == "" { + assert.NoError(t, err, test.description) + } else { + assert.EqualError(t, err, test.expectedError, test.description) + } + + assert.Equal(t, test.expectedResult, result, test.description) + } +} + +func TestParseNoSaleBidders(t *testing.T) { + testCases := []struct { + description string + noSaleBidders []string + validBidders []string + expectedNoSaleForAllBidders bool + expectedNoSaleSpecificBidders map[string]struct{} + expectedError string + }{ + { + description: "Valid - No Bidders", + noSaleBidders: []string{}, + validBidders: []string{"a"}, + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "Valid - 1 Bidder", + noSaleBidders: []string{"a"}, + validBidders: []string{"a"}, + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{"a": {}}, + }, + { + description: "Valid - 1+ Bidders", + noSaleBidders: []string{"a", "b"}, + validBidders: []string{"a", "b"}, + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{"a": {}, "b": {}}, + }, + { + description: "Valid - All Bidders", + noSaleBidders: []string{"*"}, + validBidders: []string{"a"}, + expectedNoSaleForAllBidders: true, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "Bidder Not Valid", + noSaleBidders: []string{"b"}, + validBidders: []string{"a"}, + expectedError: "unrecognized bidder 'b'", + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "All Bidder Mixed With Other Bidders Is Invalid", + noSaleBidders: []string{"*", "a"}, + validBidders: []string{"a"}, + expectedError: "can only specify all bidders if no other bidders are provided", + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + { + description: "Valid Bidders Case Sensitive", + noSaleBidders: []string{"a"}, + validBidders: []string{"A"}, + expectedError: "unrecognized bidder 'a'", + expectedNoSaleForAllBidders: false, + expectedNoSaleSpecificBidders: map[string]struct{}{}, + }, + } + + for _, test := range testCases { + validBiddersMap := make(map[string]struct{}) + for _, v := range test.validBidders { + validBiddersMap[v] = struct{}{} + } + + resultNoSaleForAllBidders, resultNoSaleSpecificBidders, err := parseNoSaleBidders(test.noSaleBidders, validBiddersMap) + + if test.expectedError == "" { + assert.NoError(t, err, test.description+":err") + } else { + assert.EqualError(t, err, test.expectedError, test.description+":err") + } + + assert.Equal(t, test.expectedNoSaleForAllBidders, resultNoSaleForAllBidders, test.description+":allBidders") + assert.Equal(t, test.expectedNoSaleSpecificBidders, resultNoSaleSpecificBidders, test.description+":specificBidders") + } +} + +func TestCanEnforce(t *testing.T) { + testCases := []struct { + description string + policy ParsedPolicy + expected bool + }{ + { + description: "Specified", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + expected: true, + }, + { + description: "Not Specified", + policy: ParsedPolicy{ + consentSpecified: false, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + expected: false, + }, + } + + for _, test := range testCases { + result := test.policy.CanEnforce() + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestShouldEnforce(t *testing.T) { + testCases := []struct { + description string + policy ParsedPolicy + bidder string + expected bool + }{ + { + description: "Not Enforced - All Bidders No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: true, + noSaleSpecificBidders: map[string]struct{}{}, + }, + bidder: "a", + expected: false, + }, + { + description: "Not Enforced - Specific Bidders No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"a": {}}, + }, + bidder: "a", + expected: false, + }, + { + description: "Not Enforced - No Bidder No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + bidder: "a", + expected: false, + }, + { + description: "Not Enforced - No Sale Case Sensitive", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: false, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"A": {}}, + }, + bidder: "a", + expected: false, + }, + { + description: "Enforced - No Bidder No Sale", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{}, + }, + bidder: "a", + expected: true, + }, + { + description: "Enforced - No Sale Case Sensitive", + policy: ParsedPolicy{ + consentSpecified: true, + consentOptOutSale: true, + noSaleForAllBidders: false, + noSaleSpecificBidders: map[string]struct{}{"A": {}}, + }, + bidder: "a", + expected: true, + }, + } + + for _, test := range testCases { + result := test.policy.ShouldEnforce(test.bidder) + assert.Equal(t, test.expected, result, test.description) + } +} + +type mockPolicWriter struct { + mock.Mock +} + +func (m *mockPolicWriter) Write(req *openrtb.BidRequest) error { + args := m.Called(req) + return args.Error(0) +} diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go index d4299af8cf2..3f5dd25c6bc 100644 --- a/privacy/ccpa/policy.go +++ b/privacy/ccpa/policy.go @@ -9,139 +9,190 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" ) -// Policy represents the CCPA regulation for an OpenRTB bid request. +// Policy represents the CCPA regulatory information from an OpenRTB bid request. type Policy struct { - Value string + Consent string + NoSaleBidders []string } -// ReadPolicy extracts the CCPA regulation policy from an OpenRTB request. -func ReadPolicy(req *openrtb.BidRequest) (Policy, error) { - policy := Policy{} +// ReadFromRequest extracts the CCPA regulatory information from an OpenRTB bid request. +func ReadFromRequest(req *openrtb.BidRequest) (Policy, error) { + var consent string + var noSaleBidders []string - if req != nil && req.Regs != nil && len(req.Regs.Ext) > 0 { + if req == nil { + return Policy{}, nil + } + + // Read consent from request.regs.ext + if req.Regs != nil && len(req.Regs.Ext) > 0 { var ext openrtb_ext.ExtRegs if err := json.Unmarshal(req.Regs.Ext, &ext); err != nil { - return policy, err + return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err) } - policy.Value = ext.USPrivacy + consent = ext.USPrivacy } - return policy, nil + // Read no sale bidders from request.ext.prebid + if len(req.Ext) > 0 { + var ext openrtb_ext.ExtRequest + if err := json.Unmarshal(req.Ext, &ext); err != nil { + return Policy{}, fmt.Errorf("error reading request.ext.prebid: %s", err) + } + noSaleBidders = ext.Prebid.NoSale + } + + return Policy{consent, noSaleBidders}, nil } -// Write mutates an OpenRTB bid request with the context of the CCPA policy. +// Write mutates an OpenRTB bid request with the CCPA regulatory information. func (p Policy) Write(req *openrtb.BidRequest) error { - if p.Value == "" { - return clearPolicy(req) - } - if req == nil { return nil } - if req.Regs == nil { - req.Regs = &openrtb.Regs{} + regs, err := buildRegs(p.Consent, req.Regs) + if err != nil { + return err } - - if req.Regs.Ext == nil { - ext, err := json.Marshal(openrtb_ext.ExtRegs{USPrivacy: p.Value}) - if err == nil { - req.Regs.Ext = ext - } + ext, err := buildExt(p.NoSaleBidders, req.Ext) + if err != nil { 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 - } + req.Regs = regs + req.Ext = ext + return nil +} + +func buildRegs(consent string, regs *openrtb.Regs) (*openrtb.Regs, error) { + if consent == "" { + return buildRegsClear(regs) } - return err + return buildRegsWrite(consent, regs) } -func clearPolicy(req *openrtb.BidRequest) error { - if req == nil { - return nil +func buildRegsClear(regs *openrtb.Regs) (*openrtb.Regs, error) { + if regs == nil || len(regs.Ext) == 0 { + return regs, nil } - if req.Regs == nil { - return nil + var extMap map[string]interface{} + if err := json.Unmarshal(regs.Ext, &extMap); err != nil { + return nil, err } - if len(req.Regs.Ext) == 0 { - return nil + delete(extMap, "us_privacy") + + // Remove entire ext if it's now empty + if len(extMap) == 0 { + regsResult := *regs + regsResult.Ext = nil + return ®sResult, nil } - var extMap map[string]interface{} - err := json.Unmarshal(req.Regs.Ext, &extMap) + // Marshal ext if there are still other fields + var regsResult openrtb.Regs + ext, err := json.Marshal(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 - } + regsResult = *regs + regsResult.Ext = ext } - - return err + return ®sResult, err } -// Validate returns an error if the CCPA policy does not adhere to the IAB spec. -func (p Policy) Validate() error { - if err := ValidateConsent(p.Value); err != nil { - return fmt.Errorf("request.regs.ext.us_privacy %s", err.Error()) +func buildRegsWrite(consent string, regs *openrtb.Regs) (*openrtb.Regs, error) { + if regs == nil { + return marshalRegsExt(openrtb.Regs{}, openrtb_ext.ExtRegs{USPrivacy: consent}) } - return nil + if regs.Ext == nil { + return marshalRegsExt(*regs, openrtb_ext.ExtRegs{USPrivacy: consent}) + } + + var extMap map[string]interface{} + if err := json.Unmarshal(regs.Ext, &extMap); err != nil { + return nil, err + } + + extMap["us_privacy"] = consent + return marshalRegsExt(*regs, extMap) } -// 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 +func marshalRegsExt(regs openrtb.Regs, ext interface{}) (*openrtb.Regs, error) { + extJSON, err := json.Marshal(ext) + if err == nil { + regs.Ext = extJSON } + return ®s, err +} - if len(consent) != 4 { - return errors.New("must contain 4 characters") +func buildExt(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) { + if len(noSaleBidders) == 0 { + return buildExtClear(ext) } + return buildExtWrite(noSaleBidders, ext) +} - if consent[0] != '1' { - return errors.New("must specify version 1") +func buildExtClear(ext json.RawMessage) (json.RawMessage, error) { + if len(ext) == 0 { + return ext, nil } - var c byte + var extMap map[string]interface{} + if err := json.Unmarshal(ext, &extMap); err != nil { + return nil, err + } - c = consent[1] - if c != 'N' && c != 'Y' && c != '-' { - return errors.New("must specify 'N', 'Y', or '-' for the explicit notice") + prebidExt, exists := extMap["prebid"] + if !exists { + return ext, nil } - c = consent[2] - if c != 'N' && c != 'Y' && c != '-' { - return errors.New("must specify 'N', 'Y', or '-' for the opt-out sale") + // Verify prebid is an object + prebidExtMap, ok := prebidExt.(map[string]interface{}) + if !ok { + return nil, errors.New("request.ext.prebid is not a json object") } - c = consent[3] - if c != 'N' && c != 'Y' && c != '-' { - return errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement") + // Remove no sale member + delete(prebidExtMap, "nosale") + if len(prebidExtMap) == 0 { + delete(extMap, "prebid") } - return nil + // Remove entire ext if it's empty + if len(extMap) == 0 { + return nil, nil + } + + return json.Marshal(extMap) } -// ShouldEnforce returns true when the opt-out signal is explicitly detected. -func (p Policy) ShouldEnforce() bool { - if err := p.Validate(); err != nil { - return false +func buildExtWrite(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) { + if len(ext) == 0 { + return json.Marshal(openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{NoSale: noSaleBidders}}) + } + + var extMap map[string]interface{} + if err := json.Unmarshal(ext, &extMap); err != nil { + return nil, err + } + + var prebidExt map[string]interface{} + if prebidExtInterface, exists := extMap["prebid"]; exists { + // Reference Existing Prebid Ext Map + if prebidExtMap, ok := prebidExtInterface.(map[string]interface{}); ok { + prebidExt = prebidExtMap + } else { + return nil, errors.New("request.ext.prebid is not a json object") + } + } else { + // Create New Empty Prebid Ext Map + prebidExt = make(map[string]interface{}) + extMap["prebid"] = prebidExt } - return p.Value != "" && p.Value[2] == 'Y' + prebidExt["nosale"] = noSaleBidders + return json.Marshal(extMap) } diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go index 647f85481b3..c1fdd9cd903 100644 --- a/privacy/ccpa/policy_test.go +++ b/privacy/ccpa/policy_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRead(t *testing.T) { +func TestReadFromRequest(t *testing.T) { testCases := []struct { description string request *openrtb.BidRequest @@ -18,83 +18,146 @@ func TestRead(t *testing.T) { { description: "Success", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"ABC"}`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "ABC", + Consent: "ABC", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Empty - No Request", + description: "Nil Request", request: nil, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: nil, }, }, { - description: "Empty - No Regs", + description: "Nil Regs", request: &openrtb.BidRequest{ Regs: nil, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Empty - No Ext", + description: "Nil Regs.Ext", request: &openrtb.BidRequest{ Regs: &openrtb.Regs{}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Empty - No Value", + description: "Empty Regs.Ext", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"anythingElse":"42"}`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), }, expectedPolicy: Policy{ - Value: "", + Consent: "", + NoSaleBidders: []string{"a", "b"}, }, }, { - description: "Serialization Issue", + description: "Missing Regs.Ext USPrivacy Value", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`malformed`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"anythingElse":"42"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + }, + expectedPolicy: Policy{ + Consent: "", + NoSaleBidders: []string{"a", "b"}, + }, + }, + { + description: "Malformed Regs.Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + }, + expectedError: true, + }, + { + description: "Invalid Regs.Ext Type", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + }, + expectedError: true, + }, + { + description: "Nil Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: nil, + }, + expectedPolicy: Policy{ + Consent: "ABC", + NoSaleBidders: nil, + }, + }, + { + description: "Empty Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{}`), + }, + expectedPolicy: Policy{ + Consent: "ABC", + NoSaleBidders: nil, + }, + }, + { + description: "Missing Ext.Prebid No Sale Value", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{"anythingElse":"42"}`), + }, + expectedPolicy: Policy{ + Consent: "ABC", + NoSaleBidders: nil, + }, + }, + { + description: "Malformed Ext", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`malformed`), + }, + expectedError: true, + }, + { + description: "Invalid Ext.Prebid.NoSale Type", + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":"wrongtype"}}`), }, expectedError: true, }, { description: "Injection Attack", request: &openrtb.BidRequest{ - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`), - }, + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`)}, }, expectedPolicy: Policy{ - Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", + Consent: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"", }, }, } for _, test := range testCases { - - p, e := ReadPolicy(test.request) - - if test.expectedError { - assert.Error(t, e, test.description) - } else { - assert.NoError(t, e, test.description) - } - - assert.Equal(t, test.expectedPolicy, p, test.description) + result, err := ReadFromRequest(test.request) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expectedPolicy, result, test.description) } } @@ -107,313 +170,422 @@ func TestWrite(t *testing.T) { expectedError bool }{ { - description: "Disabled", - policy: Policy{Value: ""}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{}, - }, - { - description: "Disabled - Nil Request", - policy: Policy{Value: ""}, + description: "Nil Request", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, 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: "Success", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, + Ext: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, }, { - 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: "Error Regs.Ext - No Partial Update To Request", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, + request: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, + expectedError: true, + expected: &openrtb.BidRequest{ + Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)}, + }, }, { - 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: "Error Ext - No Partial Update To Request", + policy: Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}}, + request: &openrtb.BidRequest{ + Ext: json.RawMessage(`malformed}`), + }, + expectedError: true, + expected: &openrtb.BidRequest{ + Ext: json.RawMessage(`malformed}`), + }, }, + } + + for _, test := range testCases { + err := test.policy.Write(test.request) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, test.request, test.description) + } +} + +func TestBuildRegs(t *testing.T) { + testCases := []struct { + description string + consent string + regs *openrtb.Regs + expected *openrtb.Regs + expectedError bool + }{ { - 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: "Clear", + consent: "", + regs: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"ABC"}`), + }, + expected: &openrtb.Regs{}, }, { - description: "Enabled - Nil Request", - policy: Policy{Value: "anyValue"}, - request: nil, - expected: nil, + description: "Clear - Error", + consent: "", + regs: &openrtb.Regs{ + Ext: json.RawMessage(`malformed`), + }, + expectedError: true, }, { - description: "Enabled With Nil Request Regs Object", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}}, + description: "Write", + consent: "anyConsent", + regs: nil, + expected: &openrtb.Regs{ + Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`), + }, }, { - description: "Enabled With Nil Request Regs Ext Object", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{}}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}}, + description: "Write - Error", + consent: "anyConsent", + regs: &openrtb.Regs{ + Ext: json.RawMessage(`malformed`), + }, + expectedError: true, }, + } + + for _, test := range testCases { + result, err := buildRegs(test.consent, test.regs) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestBuildRegsClear(t *testing.T) { + testCases := []struct { + description string + regs *openrtb.Regs + expected *openrtb.Regs + expectedError bool + }{ { - description: "Enabled With Existing Request Regs Ext Object - Doesn't Overwrite", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any"}`)}}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}}, + description: "Nil Regs", + regs: nil, + expected: nil, }, { - description: "Enabled With Existing Request Regs Ext Object - Overwrites", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeOverwritten"}`)}}, - expected: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}}, + description: "Nil Regs.Ext", + regs: &openrtb.Regs{Ext: nil}, + expected: &openrtb.Regs{Ext: nil}, }, { - description: "Enabled With Existing Malformed Request Regs Ext Object", - policy: Policy{Value: "anyValue"}, - request: &openrtb.BidRequest{Regs: &openrtb.Regs{ - Ext: json.RawMessage(`malformed`)}}, - expectedError: true, + description: "Empty Regs.Ext", + regs: &openrtb.Regs{Ext: json.RawMessage(`{}`)}, + expected: &openrtb.Regs{}, }, { - 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: "Removes Regs.Ext Entirely", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + expected: &openrtb.Regs{}, + }, + { + description: "Leaves Other Regs.Ext Values", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC", "other":"any"}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"other":"any"}`)}, }, { - 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: "Invalid Regs.Ext Type - Still Cleared", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expected: &openrtb.Regs{}, }, { - 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\":\""}`), - }}, + description: "Malformed Regs.Ext", + regs: &openrtb.Regs{Ext: json.RawMessage(`malformed`)}, + expectedError: true, }, } for _, test := range testCases { - err := test.policy.Write(test.request) - - if test.expectedError { - assert.Error(t, err, test.description) - } else { - assert.NoError(t, err, test.description) - assert.Equal(t, test.expected, test.request, test.description) - } + result, err := buildRegsClear(test.regs) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) } } -func TestValidate(t *testing.T) { +func TestBuildRegsWrite(t *testing.T) { testCases := []struct { description string - policy Policy - expectedError string + consent string + regs *openrtb.Regs + expected *openrtb.Regs + expectedError bool }{ { - description: "Valid", - policy: Policy{Value: "1NYN"}, - expectedError: "", + description: "Nil Regs", + consent: "anyConsent", + regs: nil, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Valid - Not Applicable", - policy: Policy{Value: "1---"}, - expectedError: "", + description: "Nil Regs.Ext", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: nil}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Valid - Empty", - policy: Policy{Value: ""}, - expectedError: "", + description: "Empty Regs.Ext", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Invalid Length", - policy: Policy{Value: "1NY"}, - expectedError: "request.regs.ext.us_privacy must contain 4 characters", + description: "Overwrites Existing", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - description: "Invalid Version", - policy: Policy{Value: "2---"}, - expectedError: "request.regs.ext.us_privacy must specify version 1", + description: "Leaves Other Ext Values", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"other":"any"}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"other":"any","us_privacy":"anyConsent"}`)}, }, { - 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 Regs.Ext Type - Still Overwrites", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)}, + expected: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)}, }, { - 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: "Malformed Regs.Ext", + consent: "anyConsent", + regs: &openrtb.Regs{Ext: json.RawMessage(`malformed`)}, + expectedError: true, }, + } + + for _, test := range testCases { + result, err := buildRegsWrite(test.consent, test.regs) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestBuildExt(t *testing.T) { + testCases := []struct { + description string + noSaleBidders []string + ext json.RawMessage + expected json.RawMessage + expectedError bool + }{ { - 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: "Clear - Nil", + noSaleBidders: nil, + ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + expected: nil, }, { - 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: "Clear - Empty", + noSaleBidders: []string{}, + ext: json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`), + expected: nil, }, { - 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: "Clear - Error", + noSaleBidders: []string{}, + ext: json.RawMessage(`malformed`), + expectedError: true, }, { - 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", + description: "Write", + noSaleBidders: []string{"a", "b"}, + ext: nil, + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Write - Error", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`malformed`), + expectedError: true, }, } for _, test := range testCases { - result := test.policy.Validate() - - if test.expectedError == "" { - assert.NoError(t, result, test.description) - } else { - assert.EqualError(t, result, test.expectedError, test.description) - } + result, err := buildExt(test.noSaleBidders, test.ext) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) } } -func TestValidateConsent(t *testing.T) { +func TestBuildExtClear(t *testing.T) { testCases := []struct { description string - consent string - expectedError string + ext json.RawMessage + expected json.RawMessage + expectedError bool }{ { - description: "Valid", - consent: "1NYN", - expectedError: "", + description: "Nil Ext", + ext: nil, + expected: nil, }, { - description: "Valid - Not Applicable", - consent: "1---", - expectedError: "", + description: "Empty Ext", + ext: json.RawMessage(``), + expected: json.RawMessage(``), }, { - description: "Invalid Empty", - consent: "", - expectedError: "", + description: "Empty Ext Object", + ext: json.RawMessage(`{}`), + expected: json.RawMessage(`{}`), }, { - description: "Invalid Length", - consent: "1NY", - expectedError: "must contain 4 characters", + description: "Empty Ext.Prebid", + ext: json.RawMessage(`{"prebid":{}}`), + expected: nil, }, { - description: "Invalid Version", - consent: "2---", - expectedError: "must specify version 1", + description: "Removes Ext Entirely", + ext: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + expected: nil, }, { - description: "Invalid Explicit Notice Char", - consent: "1X--", - expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + description: "Leaves Other Ext Values", + ext: json.RawMessage(`{"other":"any","prebid":{"nosale":["a","b"]}}`), + expected: json.RawMessage(`{"other":"any"}`), }, { - description: "Invalid Explicit Notice Case", - consent: "1y--", - expectedError: "must specify 'N', 'Y', or '-' for the explicit notice", + description: "Leaves Other Ext.Prebid Values", + ext: json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`), + expected: json.RawMessage(`{"prebid":{"other":"any"}}`), }, { - description: "Invalid Opt-Out Sale Char", - consent: "1-X-", - expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + description: "Leaves All Other Values", + ext: json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`), + expected: json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`), }, { - description: "Invalid Opt-Out Sale Case", - consent: "1-y-", - expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale", + description: "Malformed Ext", + ext: json.RawMessage(`malformed`), + expectedError: true, }, { - description: "Invalid LSPA Char", - consent: "1--X", - expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + description: "Malformed Ext.Prebid", + ext: json.RawMessage(`{"prebid":malformed}`), + expectedError: true, }, { - description: "Invalid LSPA Case", - consent: "1--y", - expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement", + description: "Invalid Ext.Prebid Type", + ext: json.RawMessage(`{"prebid":123}`), + expectedError: true, }, } for _, test := range testCases { - result := ValidateConsent(test.consent) - - if test.expectedError == "" { - assert.NoError(t, result, test.description) - } else { - assert.EqualError(t, result, test.expectedError, test.description) - } + result, err := buildExtClear(test.ext) + assertError(t, test.expectedError, err, test.description) + assert.Equal(t, test.expected, result, test.description) } } -func TestShouldEnforce(t *testing.T) { +func TestBuildExtWrite(t *testing.T) { testCases := []struct { - description string - policy Policy - expected bool + description string + noSaleBidders []string + ext json.RawMessage + expected json.RawMessage + expectedError bool }{ { - description: "Enforceable", - policy: Policy{Value: "1-Y-"}, - expected: true, + description: "Nil Ext", + noSaleBidders: []string{"a", "b"}, + ext: nil, + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Empty Ext", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(``), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Not Enforceable - Not Present", - policy: Policy{Value: ""}, - expected: false, + description: "Empty Ext Object", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Not Enforceable - Opt-Out Unknown", - policy: Policy{Value: "1---"}, - expected: false, + description: "Empty Ext.Prebid", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Not Enforceable - Opt-Out Explicitly No", - policy: Policy{Value: "1-N-"}, - expected: false, + description: "Overwrites Existing", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{"nosale":["x","y"]}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), }, { - description: "Invalid", - policy: Policy{Value: "2---"}, - expected: false, + description: "Leaves Other Ext Values", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"other":"any"}`), + expected: json.RawMessage(`{"other":"any","prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Leaves Other Ext.Prebid Values", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{"other":"any"}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`), + }, + { + description: "Leaves All Other Values", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`), + expected: json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`), + }, + { + description: "Invalid Ext.Prebid No Sale Type - Still Overrides", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":{"nosale":123}}`), + expected: json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`), + }, + { + description: "Invalid Ext.Prebid Type ", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":"wrongtype"}`), + expectedError: true, + }, + { + description: "Malformed Ext", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{malformed`), + expectedError: true, + }, + { + description: "Malformed Ext.Prebid", + noSaleBidders: []string{"a", "b"}, + ext: json.RawMessage(`{"prebid":malformed}`), + expectedError: true, }, } for _, test := range testCases { - result := test.policy.ShouldEnforce() + result, err := buildExtWrite(test.noSaleBidders, test.ext) + assertError(t, test.expectedError, err, test.description) assert.Equal(t, test.expected, result, test.description) } } + +func assertError(t *testing.T, expectError bool, err error, description string) { + t.Helper() + if expectError { + assert.Error(t, err, description) + } else { + assert.NoError(t, err, description) + } +} diff --git a/privacy/enforcement.go b/privacy/enforcement.go index fe81848181e..9da67bb2b15 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -8,14 +8,14 @@ import ( type Enforcement struct { CCPA bool COPPA bool - GDPR bool GDPRGeo bool + GDPRID 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 || e.GDPRGeo || e.LMT + return e.CCPA || e.COPPA || e.GDPRGeo || e.GDPRID || e.LMT } // Apply cleans personally identifiable information from an OpenRTB bid request. @@ -25,17 +25,33 @@ func (e Enforcement) Apply(bidRequest *openrtb.BidRequest, ampGDPRException bool func (e Enforcement) apply(bidRequest *openrtb.BidRequest, ampGDPRException bool, scrubber Scrubber) { if bidRequest != nil && e.Any() { - bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) + bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(ampGDPRException), e.getGeoScrubStrategy()) } } +func (e Enforcement) getDeviceIDScrubStrategy() ScrubStrategyDeviceID { + if e.COPPA || e.GDPRID || e.CCPA || e.LMT { + return ScrubStrategyDeviceIDAll + } + + return ScrubStrategyDeviceIDNone +} + +func (e Enforcement) getIPv4ScrubStrategy() ScrubStrategyIPV4 { + if e.COPPA || e.GDPRGeo || e.CCPA || e.LMT { + return ScrubStrategyIPV4Lowest8 + } + + return ScrubStrategyIPV4None +} + func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 { if e.COPPA { return ScrubStrategyIPV6Lowest32 } - if e.GDPR || e.CCPA || e.LMT { + if e.GDPRGeo || e.CCPA || e.LMT { return ScrubStrategyIPV6Lowest16 } @@ -59,12 +75,11 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs return ScrubStrategyUserIDAndDemographic } - if e.GDPR && ampGDPRException { - return ScrubStrategyUserNone + if e.CCPA || e.LMT { + return ScrubStrategyUserID } - // If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above) - if e.CCPA || e.GDPR || e.LMT { + if e.GDPRID && !ampGDPRException { return ScrubStrategyUserID } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 90af24b27ea..c332f39dfd8 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -19,8 +19,8 @@ func TestAny(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: false, }, expected: false, @@ -30,8 +30,8 @@ func TestAny(t *testing.T) { enforcement: Enforcement{ CCPA: true, COPPA: true, - GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: true, }, expected: true, @@ -41,8 +41,8 @@ func TestAny(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: true, - GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: true, }, expected: true, @@ -60,6 +60,8 @@ func TestApply(t *testing.T) { description string enforcement Enforcement ampGDPRException bool + expectedDeviceID ScrubStrategyDeviceID + expectedDeviceIPv4 ScrubStrategyIPV4 expectedDeviceIPv6 ScrubStrategyIPV6 expectedDeviceGeo ScrubStrategyGeo expectedUser ScrubStrategyUser @@ -70,11 +72,12 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: true, COPPA: true, - GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: true, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, expectedUser: ScrubStrategyUserIDAndDemographic, @@ -85,11 +88,12 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: true, COPPA: false, - GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: false, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserID, @@ -100,102 +104,98 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: true, - GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: false, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, expectedDeviceGeo: ScrubStrategyGeoFull, expectedUser: ScrubStrategyUserIDAndDemographic, expectedUserGeo: ScrubStrategyGeoFull, }, { - description: "GDPR Only", + description: "GDPR Only - Full", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: false, }, ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserID, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { - description: "GDPR Only, ampGDPRException", + description: "GDPR Only - Full - AMP Exception", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: true, GDPRGeo: true, + GDPRID: true, LMT: false, }, ampGDPRException: true, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserNone, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { - description: "CCPA Only, ampGDPRException", + description: "GDPR Only - ID Only", enforcement: Enforcement{ - CCPA: true, + CCPA: false, COPPA: false, - GDPR: false, GDPRGeo: false, + GDPRID: true, LMT: false, }, - ampGDPRException: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, + ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4None, + expectedDeviceIPv6: ScrubStrategyIPV6None, + expectedDeviceGeo: ScrubStrategyGeoNone, expectedUser: ScrubStrategyUserID, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - }, - { - description: "COPPA and GDPR, ampGDPRException", - enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPR: true, - GDPRGeo: true, - LMT: false, - }, - ampGDPRException: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, - expectedDeviceGeo: ScrubStrategyGeoFull, - expectedUser: ScrubStrategyUserIDAndDemographic, - expectedUserGeo: ScrubStrategyGeoFull, + expectedUserGeo: ScrubStrategyGeoNone, }, { - description: "GDPR Only, no Geo", + description: "GDPR Only - ID Only - AMP Exception", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: true, GDPRGeo: false, + GDPRID: true, LMT: false, }, - ampGDPRException: false, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, + ampGDPRException: true, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4None, + expectedDeviceIPv6: ScrubStrategyIPV6None, expectedDeviceGeo: ScrubStrategyGeoNone, - expectedUser: ScrubStrategyUserID, + expectedUser: ScrubStrategyUserNone, expectedUserGeo: ScrubStrategyGeoNone, }, { - description: "GDPR Only, Geo only", + description: "GDPR Only - Geo Only", enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: false, GDPRGeo: true, + GDPRID: false, LMT: false, }, ampGDPRException: false, - expectedDeviceIPv6: ScrubStrategyIPV6None, + expectedDeviceID: ScrubStrategyDeviceIDNone, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserNone, expectedUserGeo: ScrubStrategyGeoReducedPrecision, @@ -205,30 +205,50 @@ func TestApply(t *testing.T) { enforcement: Enforcement{ CCPA: false, COPPA: false, - GDPR: false, GDPRGeo: false, + GDPRID: false, LMT: true, }, - ampGDPRException: false, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, expectedUser: ScrubStrategyUserID, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { - description: "LMT Only, ampGDPRException", + description: "Interactions: COPPA Only + AMP Exception", enforcement: Enforcement{ CCPA: false, - COPPA: false, - GDPR: false, + COPPA: true, GDPRGeo: false, - LMT: true, + GDPRID: false, + LMT: false, }, ampGDPRException: true, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserID, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceGeo: ScrubStrategyGeoFull, + expectedUser: ScrubStrategyUserIDAndDemographic, + expectedUserGeo: ScrubStrategyGeoFull, + }, + { + description: "Interactions: COPPA + GDPR Full + AMP Exception", + enforcement: Enforcement{ + CCPA: false, + COPPA: true, + GDPRGeo: true, + GDPRID: true, + LMT: false, + }, + ampGDPRException: true, + expectedDeviceID: ScrubStrategyDeviceIDAll, + expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, + expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, + expectedDeviceGeo: ScrubStrategyGeoFull, + expectedUser: ScrubStrategyUserIDAndDemographic, + expectedUserGeo: ScrubStrategyGeoFull, }, } @@ -241,7 +261,7 @@ func TestApply(t *testing.T) { replacedUser := &openrtb.User{} m := &mockScrubber{} - m.On("ScrubDevice", req.Device, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(replacedDevice).Once() + m.On("ScrubDevice", req.Device, test.expectedDeviceID, test.expectedDeviceIPv4, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(replacedDevice).Once() m.On("ScrubUser", req.User, test.expectedUser, test.expectedUserGeo).Return(replacedUser).Once() test.enforcement.apply(req, test.ampGDPRException, m) @@ -258,10 +278,11 @@ func TestApplyNoneApplicable(t *testing.T) { m := &mockScrubber{} enforcement := Enforcement{ - CCPA: false, - COPPA: false, - GDPR: false, - LMT: false, + CCPA: false, + COPPA: false, + GDPRGeo: false, + GDPRID: false, + LMT: false, } enforcement.apply(req, false, m) @@ -283,8 +304,8 @@ type mockScrubber struct { mock.Mock } -func (m *mockScrubber) ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { - args := m.Called(device, ipv6, geo) +func (m *mockScrubber) ScrubDevice(device *openrtb.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { + args := m.Called(device, id, ipv4, ipv6, geo) return args.Get(0).(*openrtb.Device) } diff --git a/privacy/enforcer.go b/privacy/enforcer.go new file mode 100644 index 00000000000..0d5ecad5309 --- /dev/null +++ b/privacy/enforcer.go @@ -0,0 +1,43 @@ +package privacy + +// PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy. +type PolicyEnforcer interface { + // CanEnforce returns true when policy information is specifically provided by the publisher. + CanEnforce() bool + + // ShouldEnforce returns true when the OpenRTB request should have personally identifiable + // information (PII) removed or anonymized per the policy. + ShouldEnforce(bidder string) bool +} + +// NilPolicyEnforcer implements the PolicyEnforcer interface but will always return false. +type NilPolicyEnforcer struct{} + +// CanEnforce is hardcoded to always return false. +func (NilPolicyEnforcer) CanEnforce() bool { + return false +} + +// ShouldEnforce is hardcoded to always return false. +func (NilPolicyEnforcer) ShouldEnforce(bidder string) bool { + return false +} + +// EnabledPolicyEnforcer decorates a PolicyEnforcer with an enabled flag. +type EnabledPolicyEnforcer struct { + Enabled bool + PolicyEnforcer PolicyEnforcer +} + +// CanEnforce returns true when the PolicyEnforcer can enforce. +func (p EnabledPolicyEnforcer) CanEnforce() bool { + return p.PolicyEnforcer.CanEnforce() +} + +// ShouldEnforce returns true when the enforcer is enabled the PolicyEnforcer allows enforcement. +func (p EnabledPolicyEnforcer) ShouldEnforce(bidder string) bool { + if p.Enabled { + return p.PolicyEnforcer.ShouldEnforce(bidder) + } + return false +} diff --git a/privacy/enforcer_test.go b/privacy/enforcer_test.go new file mode 100644 index 00000000000..b0c4032c714 --- /dev/null +++ b/privacy/enforcer_test.go @@ -0,0 +1,18 @@ +package privacy + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNilEnforcerCanEnforce(t *testing.T) { + nilEnforcer := &NilPolicyEnforcer{} + assert.False(t, nilEnforcer.CanEnforce()) +} + +func TestNilEnforcerShouldEnforce(t *testing.T) { + nilEnforcer := &NilPolicyEnforcer{} + assert.False(t, nilEnforcer.ShouldEnforce("")) + assert.False(t, nilEnforcer.ShouldEnforce("anyBidder")) +} diff --git a/privacy/gdpr/consentwriter.go b/privacy/gdpr/consentwriter.go new file mode 100644 index 00000000000..f1cc2ce12f7 --- /dev/null +++ b/privacy/gdpr/consentwriter.go @@ -0,0 +1,44 @@ +package gdpr + +import ( + "encoding/json" + + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + + "github.com/PubMatic-OpenWrap/openrtb" +) + +// ConsentWriter implements the PolicyWriter interface for GDPR TCF. +type ConsentWriter struct { + Consent string +} + +// Write mutates an OpenRTB bid request with the GDPR TCF consent. +func (c ConsentWriter) Write(req *openrtb.BidRequest) error { + if c.Consent == "" { + return nil + } + + if req.User == nil { + req.User = &openrtb.User{} + } + + if req.User.Ext == nil { + ext, err := json.Marshal(openrtb_ext.ExtUser{Consent: c.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"] = c.Consent + ext, err := json.Marshal(extMap) + if err == nil { + req.User.Ext = ext + } + } + return err +} diff --git a/privacy/gdpr/consentwriter_test.go b/privacy/gdpr/consentwriter_test.go new file mode 100644 index 00000000000..65df8051d02 --- /dev/null +++ b/privacy/gdpr/consentwriter_test.go @@ -0,0 +1,101 @@ +package gdpr + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestConsentWriter(t *testing.T) { + testCases := []struct { + description string + consent string + request *openrtb.BidRequest + expected *openrtb.BidRequest + expectedError bool + }{ + { + description: "Empty", + consent: "", + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{}, + }, + { + description: "Enabled With Nil Request User Object", + consent: "anyConsent", + request: &openrtb.BidRequest{}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, + }, + { + description: "Enabled With Nil Request User Ext Object", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{}}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, + }, + { + description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"existing":"any"}`)}}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, + }, + { + description: "Enabled With Existing Request User Ext Object - Overwrites", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}}, + expected: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, + }, + { + description: "Enabled With Existing Malformed Request User Ext Object", + consent: "anyConsent", + request: &openrtb.BidRequest{User: &openrtb.User{ + Ext: json.RawMessage(`malformed`)}}, + expectedError: true, + }, + { + description: "Injection Attack With Nil Request User Object", + 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", + 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", + 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 { + writer := ConsentWriter{test.consent} + err := writer.Write(test.request) + + if test.expectedError { + assert.Error(t, err, test.description) + } else { + assert.NoError(t, err, test.description) + assert.Equal(t, test.expected, test.request, test.description) + } + } +} diff --git a/privacy/gdpr/policy.go b/privacy/gdpr/policy.go index 9c910b5e6f2..0464a9ff979 100644 --- a/privacy/gdpr/policy.go +++ b/privacy/gdpr/policy.go @@ -1,10 +1,6 @@ package gdpr import ( - "encoding/json" - "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" - - "github.com/PubMatic-OpenWrap/openrtb" "github.com/prebid/go-gdpr/vendorconsent" ) @@ -14,38 +10,8 @@ type Policy struct { Consent string } -// Write mutates an OpenRTB bid request with the context of the GDPR policy. -func (p Policy) Write(req *openrtb.BidRequest) error { - if p.Consent == "" { - return nil - } - - if req.User == nil { - req.User = &openrtb.User{} - } - - if req.User.Ext == 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 -} - -// ValidateConsent returns an error if the GDPR consent string does not adhere to the IAB TCF spec. -func ValidateConsent(consent string) error { +// ValidateConsent returns true if the consent string is empty or valid per the IAB TCF spec. +func ValidateConsent(consent string) bool { _, err := vendorconsent.ParseString(consent) - return err + return err == nil } diff --git a/privacy/gdpr/policy_test.go b/privacy/gdpr/policy_test.go index ff1b8827a2f..dc8f56425c5 100644 --- a/privacy/gdpr/policy_test.go +++ b/privacy/gdpr/policy_test.go @@ -1,129 +1,36 @@ package gdpr import ( - "encoding/json" "testing" - "github.com/PubMatic-OpenWrap/openrtb" "github.com/stretchr/testify/assert" ) -func TestWrite(t *testing.T) { - testCases := []struct { - description string - policy Policy - request *openrtb.BidRequest - expected *openrtb.BidRequest - expectedError bool - }{ - { - description: "Disabled", - policy: Policy{Consent: ""}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{}, - }, - { - description: "Enabled With Nil Request User Object", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, - }, - { - description: "Enabled With Nil Request User Ext Object", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{}}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent"}`)}}, - }, - { - description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"existing":"any"}`)}}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, - }, - { - description: "Enabled With Existing Request User Ext Object - Overwrites", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}}, - expected: &openrtb.BidRequest{User: &openrtb.User{ - Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}}, - }, - { - description: "Enabled With Existing Malformed Request User Ext Object", - policy: Policy{Consent: "anyConsent"}, - request: &openrtb.BidRequest{User: &openrtb.User{ - 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 { - err := test.policy.Write(test.request) - - if test.expectedError { - assert.Error(t, err, test.description) - } else { - assert.NoError(t, err, test.description) - assert.Equal(t, test.expected, test.request, test.description) - } - } -} - func TestValidateConsent(t *testing.T) { testCases := []struct { description string consent string - expectError bool + expected bool }{ { description: "Invalid", consent: "", - expectError: true, + expected: false, }, { - description: "Valid", + description: "TCF1 Valid", consent: "BONV8oqONXwgmADACHENAO7pqzAAppY", - expectError: false, + expected: true, + }, + { + description: "TCF2 Valid", + consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + expected: true, }, } for _, test := range testCases { result := ValidateConsent(test.consent) - - if test.expectError { - assert.Error(t, result, test.description) - } else { - assert.NoError(t, result, test.description) - } + assert.Equal(t, test.expected, result, test.description) } } diff --git a/privacy/lmt/policy.go b/privacy/lmt/policy.go index bdbc1a2b34b..5f23b9a3eef 100644 --- a/privacy/lmt/policy.go +++ b/privacy/lmt/policy.go @@ -15,19 +15,21 @@ type Policy struct { SignalProvided bool } -// ReadPolicy extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request. -func ReadPolicy(req *openrtb.BidRequest) Policy { - policy := Policy{} - +// ReadFromRequest extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request. +func ReadFromRequest(req *openrtb.BidRequest) (policy Policy) { if req != nil && req.Device != nil && req.Device.Lmt != nil { policy.Signal = int(*req.Device.Lmt) policy.SignalProvided = true } + return +} - return policy +// CanEnforce returns true the LMT (Limit Ad Tracking) signal is provided by the publisher. +func (p Policy) CanEnforce() bool { + return p.SignalProvided } // ShouldEnforce returns true when the LMT (Limit Ad Tracking) policy is in effect. -func (p Policy) ShouldEnforce() bool { +func (p Policy) ShouldEnforce(bidder string) bool { return p.SignalProvided && p.Signal == trackingRestricted } diff --git a/privacy/lmt/policy_test.go b/privacy/lmt/policy_test.go index 12ea1870d2f..9d0e3b6aa9a 100644 --- a/privacy/lmt/policy_test.go +++ b/privacy/lmt/policy_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRead(t *testing.T) { +func TestReadFromRequest(t *testing.T) { var one int8 = 1 testCases := []struct { @@ -60,11 +60,73 @@ func TestRead(t *testing.T) { } for _, test := range testCases { - p := ReadPolicy(test.request) + p := ReadFromRequest(test.request) assert.Equal(t, test.expectedPolicy, p, test.description) } } +func TestCanEnforce(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: true, + }, + { + description: "Signal Provided - One", + policy: Policy{ + Signal: 1, + SignalProvided: true, + }, + expected: true, + }, + { + description: "Signal Provided - Other", + policy: Policy{ + Signal: 42, + SignalProvided: true, + }, + expected: true, + }, + } + + for _, test := range testCases { + result := test.policy.CanEnforce() + assert.Equal(t, test.expected, result, test.description) + } +} + func TestShouldEnforce(t *testing.T) { testCases := []struct { description string @@ -122,7 +184,7 @@ func TestShouldEnforce(t *testing.T) { } for _, test := range testCases { - result := test.policy.ShouldEnforce() + result := test.policy.ShouldEnforce("") assert.Equal(t, test.expected, result, test.description) } } diff --git a/privacy/policies.go b/privacy/policies.go index 837d2fa05c3..a1c3fca49be 100644 --- a/privacy/policies.go +++ b/privacy/policies.go @@ -1,60 +1,14 @@ package privacy import ( - "github.com/PubMatic-OpenWrap/openrtb" - "github.com/PubMatic-OpenWrap/prebid-server/privacy/ccpa" "github.com/PubMatic-OpenWrap/prebid-server/privacy/gdpr" + "github.com/PubMatic-OpenWrap/prebid-server/privacy/lmt" ) // Policies represents the privacy regulations for an OpenRTB bid request. type Policies struct { - GDPR gdpr.Policy CCPA ccpa.Policy -} - -type policyWriter interface { - Write(req *openrtb.BidRequest) error -} - -// Write mutates an OpenRTB bid request with the policies applied. -func (p Policies) Write(req *openrtb.BidRequest) error { - return writePolicies(req, []policyWriter{ - p.GDPR, p.CCPA, - }) -} - -func writePolicies(req *openrtb.BidRequest, writers []policyWriter) error { - for _, writer := range writers { - if err := writer.Write(req); err != nil { - return err - } - } - - 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 + GDPR gdpr.Policy + LMT lmt.Policy } diff --git a/privacy/policies_test.go b/privacy/policies_test.go deleted file mode 100644 index a7650193892..00000000000 --- a/privacy/policies_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package privacy - -import ( - "errors" - "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" -) - -func TestWritePoliciesNone(t *testing.T) { - request := &openrtb.BidRequest{} - policyWriters := []policyWriter{} - - err := writePolicies(request, policyWriters) - - assert.NoError(t, err) -} - -func TestWritePoliciesOne(t *testing.T) { - request := &openrtb.BidRequest{} - mockWriter := new(mockPolicyWriter) - policyWriters := []policyWriter{ - mockWriter, - } - - mockWriter.On("Write", request).Return(nil).Once() - - err := writePolicies(request, policyWriters) - - assert.NoError(t, err) - mockWriter.AssertExpectations(t) -} - -func TestWritePoliciesMany(t *testing.T) { - request := &openrtb.BidRequest{} - mockWriter1 := new(mockPolicyWriter) - mockWriter2 := new(mockPolicyWriter) - policyWriters := []policyWriter{ - mockWriter1, mockWriter2, - } - - mockWriter1.On("Write", request).Return(nil).Once() - mockWriter2.On("Write", request).Return(nil).Once() - - err := writePolicies(request, policyWriters) - - assert.NoError(t, err) - mockWriter1.AssertExpectations(t) - mockWriter2.AssertExpectations(t) -} - -func TestWritePoliciesError(t *testing.T) { - request := &openrtb.BidRequest{} - mockWriter := new(mockPolicyWriter) - policyWriters := []policyWriter{ - mockWriter, - } - - expectedErr := errors.New("anyError") - mockWriter.On("Write", request).Return(expectedErr).Once() - - err := writePolicies(request, policyWriters) - - assert.Error(t, err, expectedErr) - mockWriter.AssertExpectations(t) -} - -type mockPolicyWriter struct { - mock.Mock -} - -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 0bb1029faf5..aea5c9008f4 100644 --- a/privacy/scrubber.go +++ b/privacy/scrubber.go @@ -7,6 +7,17 @@ import ( "github.com/PubMatic-OpenWrap/openrtb" ) +// ScrubStrategyIPV4 defines the approach to scrub PII from an IPV4 address. +type ScrubStrategyIPV4 int + +const ( + // ScrubStrategyIPV4None does not remove any part of an IPV4 address. + ScrubStrategyIPV4None ScrubStrategyIPV4 = iota + + // ScrubStrategyIPV4Lowest8 zeroes out the last 8 bits of an IPV4 address. + ScrubStrategyIPV4Lowest8 +) + // ScrubStrategyIPV6 defines the approach to scrub PII from an IPV6 address. type ScrubStrategyIPV6 int @@ -49,9 +60,20 @@ const ( ScrubStrategyUserID ) +// ScrubStrategyDeviceID defines the approach to remove hardware id and device id data. +type ScrubStrategyDeviceID int + +const ( + // ScrubStrategyDeviceIDNone does not remove hardware id and device id data. + ScrubStrategyDeviceIDNone ScrubStrategyDeviceID = iota + + // ScrubStrategyDeviceIDAll removes all hardware and device id data (ifa, mac hashes device id hashes) + ScrubStrategyDeviceIDAll +) + // Scrubber removes PII from parts of an OpenRTB request. type Scrubber interface { - ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device + ScrubDevice(device *openrtb.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User } @@ -62,20 +84,28 @@ func NewScrubber() Scrubber { return scrubber{} } -func (scrubber) ScrubDevice(device *openrtb.Device, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device { +func (scrubber) ScrubDevice(device *openrtb.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, 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) + + switch id { + case ScrubStrategyDeviceIDAll: + deviceCopy.DIDMD5 = "" + deviceCopy.DIDSHA1 = "" + deviceCopy.DPIDMD5 = "" + deviceCopy.DPIDSHA1 = "" + deviceCopy.IFA = "" + deviceCopy.MACMD5 = "" + deviceCopy.MACSHA1 = "" + } + + switch ipv4 { + case ScrubStrategyIPV4Lowest8: + deviceCopy.IP = scrubIPV4Lowest8(device.IP) + } switch ipv6 { case ScrubStrategyIPV6Lowest16: @@ -124,7 +154,7 @@ func (scrubber) ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo Sc return &userCopy } -func scrubIPV4(ip string) string { +func scrubIPV4Lowest8(ip string) string { i := strings.LastIndex(ip, ".") if i == -1 { return "" diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go index f33bb5fd996..4d989e1c5a1 100644 --- a/privacy/scrubber_test.go +++ b/privacy/scrubber_test.go @@ -31,28 +31,21 @@ func TestScrubDevice(t *testing.T) { testCases := []struct { description string expected *openrtb.Device + id ScrubStrategyDeviceID + ipv4 ScrubStrategyIPV4 ipv6 ScrubStrategyIPV6 geo ScrubStrategyGeo }{ { - description: "IPv6 Lowest 32 & Geo Full", - expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: &openrtb.Geo{}, - }, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoFull, + description: "All Strageties - None", + expected: device, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 16 & Geo Full", + description: "All Strageties - Strictest", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -62,14 +55,16 @@ func TestScrubDevice(t *testing.T) { MACMD5: "", IFA: "", IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", + IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", Geo: &openrtb.Geo{}, }, - ipv6: ScrubStrategyIPV6Lowest16, + id: ScrubStrategyDeviceIDAll, + ipv4: ScrubStrategyIPV4Lowest8, + ipv6: ScrubStrategyIPV6Lowest32, geo: ScrubStrategyGeoFull, }, { - description: "IPv6 None & Geo Full", + description: "Isolated - ID - All", expected: &openrtb.Device{ DIDMD5: "", DIDSHA1: "", @@ -78,161 +73,126 @@ func TestScrubDevice(t *testing.T) { MACSHA1: "", MACMD5: "", IFA: "", - IP: "1.2.3.0", + IP: "1.2.3.4", IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", - Geo: &openrtb.Geo{}, + Geo: device.Geo, }, + id: ScrubStrategyDeviceIDAll, + ipv4: ScrubStrategyIPV4None, ipv6: ScrubStrategyIPV6None, - geo: ScrubStrategyGeoFull, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 32 & Geo Reduced", + description: "Isolated - IPv4 - Lowest 8", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: &openrtb.Geo{ - Lat: 123.46, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + Geo: device.Geo, }, - ipv6: ScrubStrategyIPV6Lowest32, - geo: ScrubStrategyGeoReducedPrecision, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4Lowest8, + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 16 & Geo Reduced", + description: "Isolated - IPv6 - Lowest 16", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", 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", - }, + Geo: device.Geo, }, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, 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, + geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 32 & Geo None", + description: "Isolated - IPv6 - Lowest 32", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", IPv6: "2001:0db8:0000:0000:0000:ff00:0:0", - Geo: &openrtb.Geo{ - Lat: 123.456, - Lon: 678.89, - Metro: "some metro", - City: "some city", - ZIP: "some zip", - }, + Geo: device.Geo, }, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, ipv6: ScrubStrategyIPV6Lowest32, geo: ScrubStrategyGeoNone, }, { - description: "IPv6 Lowest 16 & Geo None", + description: "Isolated - Geo - Reduced Precision", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", - IPv6: "2001:0db8:0000:0000:0000:ff00:0042:0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", + IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329", Geo: &openrtb.Geo{ - Lat: 123.456, + Lat: 123.46, Lon: 678.89, Metro: "some metro", City: "some city", ZIP: "some zip", }, }, - ipv6: ScrubStrategyIPV6Lowest16, - geo: ScrubStrategyGeoNone, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, + ipv6: ScrubStrategyIPV6None, + geo: ScrubStrategyGeoReducedPrecision, }, { - description: "IPv6 None & Geo None", + description: "Isolated - Geo - Full", expected: &openrtb.Device{ - DIDMD5: "", - DIDSHA1: "", - DPIDMD5: "", - DPIDSHA1: "", - MACSHA1: "", - MACMD5: "", - IFA: "", - IP: "1.2.3.0", + DIDMD5: "anyDIDMD5", + DIDSHA1: "anyDIDSHA1", + DPIDMD5: "anyDPIDMD5", + DPIDSHA1: "anyDPIDSHA1", + MACSHA1: "anyMACSHA1", + MACMD5: "anyMACMD5", + IFA: "anyIFA", + IP: "1.2.3.4", 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", - }, + Geo: &openrtb.Geo{}, }, + id: ScrubStrategyDeviceIDNone, + ipv4: ScrubStrategyIPV4None, ipv6: ScrubStrategyIPV6None, - geo: ScrubStrategyGeoNone, + geo: ScrubStrategyGeoFull, }, } for _, test := range testCases { - result := NewScrubber().ScrubDevice(device, test.ipv6, test.geo) + result := NewScrubber().ScrubDevice(device, test.id, test.ipv4, test.ipv6, test.geo) assert.Equal(t, test.expected, result, test.description) } } func TestScrubDeviceNil(t *testing.T) { - result := NewScrubber().ScrubDevice(nil, ScrubStrategyIPV6None, ScrubStrategyGeoNone) + result := NewScrubber().ScrubDevice(nil, ScrubStrategyDeviceIDNone, ScrubStrategyIPV4None, ScrubStrategyIPV6None, ScrubStrategyGeoNone) assert.Nil(t, result) } @@ -458,7 +418,7 @@ func TestScrubIPV4(t *testing.T) { } for _, test := range testCases { - result := scrubIPV4(test.IP) + result := scrubIPV4Lowest8(test.IP) assert.Equal(t, test.cleanedIP, result, test.description) } } diff --git a/privacy/writer.go b/privacy/writer.go new file mode 100644 index 00000000000..a68a158ced8 --- /dev/null +++ b/privacy/writer.go @@ -0,0 +1,18 @@ +package privacy + +import ( + "github.com/PubMatic-OpenWrap/openrtb" +) + +// PolicyWriter mutates an OpenRTB bid request with a policy's regulatory information. +type PolicyWriter interface { + Write(req *openrtb.BidRequest) error +} + +// NilPolicyWriter implements the PolicyWriter interface but performs no action. +type NilPolicyWriter struct{} + +// Write is hardcoded to perform no action with the OpenRTB bid request. +func (NilPolicyWriter) Write(req *openrtb.BidRequest) error { + return nil +} diff --git a/privacy/writer_test.go b/privacy/writer_test.go new file mode 100644 index 00000000000..754e6ffe2c9 --- /dev/null +++ b/privacy/writer_test.go @@ -0,0 +1,25 @@ +package privacy + +import ( + "encoding/json" + "testing" + + "github.com/PubMatic-OpenWrap/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestNilWriter(t *testing.T) { + request := &openrtb.BidRequest{ + ID: "anyID", + Ext: json.RawMessage(`{"anyJson":"anyValue"}`), + } + expectedRequest := &openrtb.BidRequest{ + ID: "anyID", + Ext: json.RawMessage(`{"anyJson":"anyValue"}`), + } + + nilWriter := &NilPolicyWriter{} + nilWriter.Write(request) + + assert.Equal(t, expectedRequest, request) +} diff --git a/router/admin.go b/router/admin.go index 608c7869e99..05281d56e15 100644 --- a/router/admin.go +++ b/router/admin.go @@ -3,12 +3,13 @@ package router import ( "net/http" "net/http/pprof" + "time" "github.com/PubMatic-OpenWrap/prebid-server/currencies" "github.com/PubMatic-OpenWrap/prebid-server/endpoints" ) -func Admin(revision string, rateConverter *currencies.RateConverter) *http.ServeMux { +func Admin(revision string, rateConverter *currencies.RateConverter, rateConverterFetchingInterval time.Duration) *http.ServeMux { // Add endpoints to the admin server // Making sure to add pprof routes mux := http.NewServeMux() @@ -19,7 +20,7 @@ func Admin(revision string, rateConverter *currencies.RateConverter) *http.Serve mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) mux.HandleFunc("/debug/pprof/trace", pprof.Trace) // Register prebid-server defined admin handlers - mux.HandleFunc("/currency/rates", endpoints.NewCurrencyRatesEndpoint(rateConverter)) + mux.HandleFunc("/currency/rates", endpoints.NewCurrencyRatesEndpoint(rateConverter, rateConverterFetchingInterval)) mux.HandleFunc("/version", endpoints.NewVersionEndpoint(revision)) return mux } diff --git a/router/router.go b/router/router.go index 755c18a452b..33ef6b89528 100644 --- a/router/router.go +++ b/router/router.go @@ -12,9 +12,12 @@ import ( "strings" "time" - "github.com/prometheus/client_golang/prometheus" + "github.com/PubMatic-OpenWrap/prebid-server/endpoints" + "github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/PubMatic-OpenWrap/prebid-server/usersync" + "github.com/prometheus/client_golang/prometheus" "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adform" @@ -34,8 +37,6 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/cache/postgrescache" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/currencies" - "github.com/PubMatic-OpenWrap/prebid-server/endpoints" - "github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2" "github.com/PubMatic-OpenWrap/prebid-server/exchange" "github.com/PubMatic-OpenWrap/prebid-server/gdpr" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" @@ -44,7 +45,6 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/ssl" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" 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" "github.com/golang/glog" @@ -59,6 +59,7 @@ var ( g_syncers map[openrtb_ext.BidderName]usersync.Usersyncer g_cfg *config.Configuration g_ex exchange.Exchange + g_accounts stored_requests.AccountFetcher g_paramsValidator openrtb_ext.BidderParamValidator g_storedReqFetcher stored_requests.Fetcher g_gdprPerms gdpr.Permissions @@ -207,6 +208,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r generalHttpClient := &http.Client{ Transport: &http.Transport{ + MaxConnsPerHost: cfg.Client.MaxConnsPerHost, MaxIdleConns: cfg.Client.MaxIdleConns, MaxIdleConnsPerHost: cfg.Client.MaxIdleConnsPerHost, IdleConnTimeout: time.Duration(cfg.Client.IdleConnTimeout) * time.Second, @@ -216,6 +218,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r cacheHttpClient := &http.Client{ Transport: &http.Transport{ + MaxConnsPerHost: cfg.CacheClient.MaxConnsPerHost, MaxIdleConns: cfg.CacheClient.MaxIdleConns, MaxIdleConnsPerHost: cfg.CacheClient.MaxIdleConnsPerHost, IdleConnTimeout: time.Duration(cfg.CacheClient.IdleConnTimeout) * time.Second, @@ -230,7 +233,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r var db *sql.DB // Metrics engine g_metrics = metricsConf.NewMetricsEngine(cfg, legacyBidderList) - db, _, g_storedReqFetcher, _, g_categoriesFetcher, g_videoFetcher = storedRequestsConf.NewStoredRequests(cfg, g_metrics, generalHttpClient, r.Router) + db, _, g_storedReqFetcher, _, g_accounts, g_categoriesFetcher, g_videoFetcher = storedRequestsConf.NewStoredRequests(cfg, g_metrics, generalHttpClient, r.Router) // todo(zachbadgett): better shutdown //r.Shutdown = shutdown @@ -260,22 +263,22 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r exchanges = newExchangeMap(cfg) 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) + g_ex = exchange.NewExchange(generalHttpClient, g_cacheClient, cfg, g_metrics, bidderInfos, g_gdprPerms, rateConvertor, g_categoriesFetcher) /* - openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, categoriesFetcher, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap) + openrtbEndpoint, err := openrtb2.NewEndpoint(theExchange, paramsValidator, fetcher, accounts, 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) + ampEndpoint, err := openrtb2.NewAmpEndpoint(theExchange, paramsValidator, ampFetcher, accounts, 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) + videoEndpoint, err := openrtb2.NewVideoEndpoint(theExchange, paramsValidator, fetcher, videoFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBiddersMap, cacheClient) if err != nil { glog.Fatalf("Failed to create the video endpoint handler. %v", err) } @@ -297,6 +300,16 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r r.GET("/", serveIndex) r.ServeFiles("/static/*filepath", http.Dir("static")) + // vtrack endpoint + if cfg.VTrack.Enabled { + vtrackEndpoint := events.NewVTrackEndpoint(cfg, accounts, cacheClient, bidderInfos) + r.POST("/vtrack", vtrackEndpoint) + } + + // event endpoint + eventEndpoint := events.NewEventEndpoint(cfg, accounts, pbsAnalytics) + r.GET("/event", eventEndpoint) + userSyncDeps := &pbs.UserSyncDeps{ HostCookieConfig: &(cfg.HostCookie), ExternalUrl: cfg.ExternalURL, @@ -309,13 +322,14 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r 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) + ortbAuctionEndpoint, err := openrtb2.NewEndpoint(g_ex, g_paramsValidator, g_storedReqFetcher, g_accounts, g_cfg, g_metrics, g_analytics, g_disabledBidders, g_defReqJSON, g_bidderMap) if err != nil { return err } @@ -325,7 +339,7 @@ func OrtbAuctionEndpointWrapper(w http.ResponseWriter, r *http.Request) error { //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, g_videoFetcher, 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_accounts, g_cfg, g_metrics, g_analytics, g_disabledBidders, g_defReqJSON, g_bidderMap) if err != nil { return err } diff --git a/static/bidder-info/33across.yaml b/static/bidder-info/33across.yaml index 84ba6d68611..67e6996accf 100644 --- a/static/bidder-info/33across.yaml +++ b/static/bidder-info/33across.yaml @@ -4,6 +4,8 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner + - video diff --git a/static/bidder-info/acuityads.yaml b/static/bidder-info/acuityads.yaml new file mode 100644 index 00000000000..9da1446d918 --- /dev/null +++ b/static/bidder-info/acuityads.yaml @@ -0,0 +1,14 @@ +maintainer: + email: "integrations@acuityads.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native + diff --git a/static/bidder-info/adform.yaml b/static/bidder-info/adform.yaml index 8aafd9f6815..4dce10b9af8 100644 --- a/static/bidder-info/adform.yaml +++ b/static/bidder-info/adform.yaml @@ -4,6 +4,8 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner + - video diff --git a/static/bidder-info/adman.yaml b/static/bidder-info/adman.yaml new file mode 100644 index 00000000000..932ef2e4242 --- /dev/null +++ b/static/bidder-info/adman.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "prebid@admanmedia.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-info/adprime.yaml b/static/bidder-info/adprime.yaml new file mode 100644 index 00000000000..9759ed63be7 --- /dev/null +++ b/static/bidder-info/adprime.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "rafal@adprime.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-info/adtelligent.yaml b/static/bidder-info/adtelligent.yaml index fe791343daf..7a20d52b266 100644 --- a/static/bidder-info/adtelligent.yaml +++ b/static/bidder-info/adtelligent.yaml @@ -4,6 +4,7 @@ capabilities: app: mediaTypes: - banner + - video site: mediaTypes: - banner diff --git a/static/bidder-info/amx.yaml b/static/bidder-info/amx.yaml new file mode 100644 index 00000000000..3e20d2095f6 --- /dev/null +++ b/static/bidder-info/amx.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "prebid@amxrtb.com" +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-info/audienceNetwork.yaml b/static/bidder-info/audienceNetwork.yaml index 56230bf3f9a..324e5c6dff8 100644 --- a/static/bidder-info/audienceNetwork.yaml +++ b/static/bidder-info/audienceNetwork.yaml @@ -1,11 +1,6 @@ maintainer: email: "none" capabilities: - site: - mediaTypes: - - banner - - video - - native app: mediaTypes: - banner diff --git a/static/bidder-info/between.yaml b/static/bidder-info/between.yaml new file mode 100644 index 00000000000..d317d275c59 --- /dev/null +++ b/static/bidder-info/between.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "buying@betweenx.com" +capabilities: + site: + mediaTypes: + - banner + app: + mediaTypes: + - banner \ No newline at end of file diff --git a/static/bidder-info/colossus.yaml b/static/bidder-info/colossus.yaml new file mode 100644 index 00000000000..901c824c603 --- /dev/null +++ b/static/bidder-info/colossus.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "support@huddledmasses.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/connectad.yaml b/static/bidder-info/connectad.yaml new file mode 100644 index 00000000000..1b3e593d78d --- /dev/null +++ b/static/bidder-info/connectad.yaml @@ -0,0 +1,9 @@ +maintainer: + email: "support@connectad.io" +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner diff --git a/static/bidder-info/emx_digital.yaml b/static/bidder-info/emx_digital.yaml index 40f73fd000f..49a068093eb 100644 --- a/static/bidder-info/emx_digital.yaml +++ b/static/bidder-info/emx_digital.yaml @@ -4,3 +4,8 @@ capabilities: site: mediaTypes: - banner + - video + app: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-info/grid.yaml b/static/bidder-info/grid.yaml index 9594830c0d0..325421a2c05 100644 --- a/static/bidder-info/grid.yaml +++ b/static/bidder-info/grid.yaml @@ -1,7 +1,11 @@ maintainer: email: "grid-tech@themediagrid.com" capabilities: - site: + app: mediaTypes: - banner - video + site: + mediaTypes: + - banner + - video \ No newline at end of file diff --git a/static/bidder-info/gumgum.yaml b/static/bidder-info/gumgum.yaml index 0feca7cdf73..6ba563b4c1c 100644 --- a/static/bidder-info/gumgum.yaml +++ b/static/bidder-info/gumgum.yaml @@ -4,3 +4,4 @@ capabilities: site: mediaTypes: - banner + - video \ No newline at end of file diff --git a/static/bidder-info/inmobi.yaml b/static/bidder-info/inmobi.yaml new file mode 100644 index 00000000000..3f8cdd8cb91 --- /dev/null +++ b/static/bidder-info/inmobi.yaml @@ -0,0 +1,8 @@ +maintainer: + email: "prebid-support@inmobi.com" + +capabilities: + app: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/invibes.yaml b/static/bidder-info/invibes.yaml new file mode 100644 index 00000000000..1432529787e --- /dev/null +++ b/static/bidder-info/invibes.yaml @@ -0,0 +1,6 @@ +maintainer: + email: "system_operations@invibes.com" +capabilities: + site: + mediaTypes: + - banner diff --git a/static/bidder-info/krushmedia.yaml b/static/bidder-info/krushmedia.yaml new file mode 100644 index 00000000000..342e11df2c7 --- /dev/null +++ b/static/bidder-info/krushmedia.yaml @@ -0,0 +1,13 @@ +maintainer: + email: "adapter@krushmedia.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-info/logicad.yaml b/static/bidder-info/logicad.yaml new file mode 100644 index 00000000000..c087516c061 --- /dev/null +++ b/static/bidder-info/logicad.yaml @@ -0,0 +1,10 @@ +maintainer: + email: "prebid@so-netmedia.jp" +capabilities: + site: + mediaTypes: + - banner + app: + mediaTypes: + - banner + diff --git a/static/bidder-info/nobid.yaml b/static/bidder-info/nobid.yaml new file mode 100644 index 00000000000..51a55de46bc --- /dev/null +++ b/static/bidder-info/nobid.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "developers@nobid.io" +capabilities: + site: + mediaTypes: + - banner + - video + app: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/silvermob.yaml b/static/bidder-info/silvermob.yaml new file mode 100644 index 00000000000..5f1e4809dd3 --- /dev/null +++ b/static/bidder-info/silvermob.yaml @@ -0,0 +1,8 @@ +maintainer: + email: "support@silvermob.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-info/smaato.yaml b/static/bidder-info/smaato.yaml new file mode 100644 index 00000000000..db3e61e5cc6 --- /dev/null +++ b/static/bidder-info/smaato.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "prebid@smaato.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/smartadserver.yaml b/static/bidder-info/smartadserver.yaml new file mode 100644 index 00000000000..626b7dac00d --- /dev/null +++ b/static/bidder-info/smartadserver.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "support@smartadserver.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-info/smartyads.yaml b/static/bidder-info/smartyads.yaml new file mode 100644 index 00000000000..df4c1b7ffb5 --- /dev/null +++ b/static/bidder-info/smartyads.yaml @@ -0,0 +1,14 @@ +maintainer: + email: "support@smartyads.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native + diff --git a/static/bidder-info/yieldmo.yaml b/static/bidder-info/yieldmo.yaml index 514f17455ea..64cda519acd 100644 --- a/static/bidder-info/yieldmo.yaml +++ b/static/bidder-info/yieldmo.yaml @@ -1,6 +1,11 @@ maintainer: email: "prebid@yieldmo.com" capabilities: + app: + mediaTypes: + - banner + - video site: mediaTypes: - banner + - video diff --git a/static/bidder-params/acuityads.json b/static/bidder-params/acuityads.json new file mode 100644 index 00000000000..bae86ad2103 --- /dev/null +++ b/static/bidder-params/acuityads.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AcuityAds Adapter Params", + "description": "A schema which validates params accepted by the AcuityAds adapter", + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Network host to send request", + "minLength": 1 + }, + "accountid": { + "type": "string", + "description": "Account id", + "minLength": 1 + } + }, + "required": ["host", "accountid"] +} \ No newline at end of file diff --git a/static/bidder-params/adform.json b/static/bidder-params/adform.json index 67f09623ee4..f0b8c7a6be0 100644 --- a/static/bidder-params/adform.json +++ b/static/bidder-params/adform.json @@ -22,6 +22,20 @@ "type": "string", "description": "Comma-separated keywords. Forbidden symbols: &.", "pattern": "^[^&]*$" + }, + "cdims": { + "type": "string", + "description": "Comma-separated creative dimentions.", + "pattern": "(^\\d+x\\d+)(,\\d+x\\d+)*$" + }, + "minp": { + "type": "number", + "description": "The minimum CPM price.", + "minimum": 0 + }, + "url": { + "type": "string", + "description": "Custom URL for targeting." } }, "required": ["mid"] diff --git a/static/bidder-params/adman.json b/static/bidder-params/adman.json new file mode 100644 index 00000000000..90021e2cdfd --- /dev/null +++ b/static/bidder-params/adman.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adman Adapter Params", + "description": "A schema which validates params accepted by the Adman adapter", + + "type": "object", + "properties": { + "TagID": { + "type": "string", + "description": "An ID which identifies the adman ad tag" + } + }, + "required" : [ "TagID" ] + } + \ No newline at end of file diff --git a/static/bidder-params/adoppler.json b/static/bidder-params/adoppler.json index c2bdde4f60f..508eef478c0 100644 --- a/static/bidder-params/adoppler.json +++ b/static/bidder-params/adoppler.json @@ -7,6 +7,10 @@ "adunit": { "type": "string", "description": "AdUnit to bid against to." + }, + "client": { + "type": "string", + "description": "Client name." } }, "required": ["adunit"] diff --git a/static/bidder-params/adprime.json b/static/bidder-params/adprime.json new file mode 100644 index 00000000000..d527056597d --- /dev/null +++ b/static/bidder-params/adprime.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adprime Adapter Params", + "description": "A schema which validates params accepted by the Adprime adapter", + + "type": "object", + "properties": { + "TagID": { + "type": "string", + "description": "An ID which identifies the adprime ad tag" + } + }, + "required" : [ "TagID" ] + } \ No newline at end of file diff --git a/static/bidder-params/amx.json b/static/bidder-params/amx.json new file mode 100644 index 00000000000..f9b1b26b3db --- /dev/null +++ b/static/bidder-params/amx.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AMX RTB Adapter Params", + "description": "A schema to validate params accepted by the AMX adapter", + "type": "object", + "properties": { + "tagId" : { + "type": "string", + "description": "Set a tagId (overrides site.publisher.id, or app.publisher.id)" + }, + "adUnitId": { + "type": "string", + "description": "Override imp.tagid value to provide a custom value in AMX ad unit ID reporting" + } + } +} diff --git a/static/bidder-params/between.json b/static/bidder-params/between.json new file mode 100644 index 00000000000..88bbf087be9 --- /dev/null +++ b/static/bidder-params/between.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "BetweenDigital Adapter Params", + "description": "A schema which validates params accepted by the BetweenDigital adapter", + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Network Host to request from" + } + + }, + "required": ["host"] +} diff --git a/static/bidder-params/colossus.json b/static/bidder-params/colossus.json new file mode 100644 index 00000000000..f2732fa0854 --- /dev/null +++ b/static/bidder-params/colossus.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Colossus Adapter Params", + "description": "A schema which validates params accepted by the Colossus adapter", + + "type": "object", + "properties": { + "TagID": { + "type": "string", + "description": "An ID which identifies the colossus ad tag" + } + }, + "required" : [ "TagID" ] + } diff --git a/static/bidder-params/connectad.json b/static/bidder-params/connectad.json new file mode 100644 index 00000000000..961b3b71202 --- /dev/null +++ b/static/bidder-params/connectad.json @@ -0,0 +1,24 @@ + +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "ConnectAd S2S dapter Params", + "description": "A schema which validates params accepted by the ConnectAd Adapter", + + "type": "object", + "properties": { + "networkId": { + "type": "integer", + "description": "NetworkId" + }, + "siteId": { + "type": "integer", + "description": "SiteId" + }, + "bidfloor": { + "type": "number", + "description": "Requestes Floorprice" + } + }, + "required": ["networkId", "siteId"] + } + \ No newline at end of file diff --git a/static/bidder-params/conversant.json b/static/bidder-params/conversant.json index 4f7200acd3b..ba9c6bd584d 100644 --- a/static/bidder-params/conversant.json +++ b/static/bidder-params/conversant.json @@ -24,10 +24,6 @@ "type": "integer", "description": "Ad position on screen." }, - "mobile": { - "type": "integer", - "description": "Indicate if the site is mobile optimized." - }, "mimes": { "type": "array", "description": "Array of content MIME types. For videos only.", diff --git a/static/bidder-params/inmobi.json b/static/bidder-params/inmobi.json new file mode 100644 index 00000000000..631b3137b72 --- /dev/null +++ b/static/bidder-params/inmobi.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "InMobi Adapter Params", + "description": "A schema which validates params accepted by the InMobi adapter", + "type": "object", + "properties": { + "plc": { + "type": ["string"], + "description": "An ID corresponding to the placement selling the impression" + } + }, + "required": ["plc"] +} diff --git a/static/bidder-params/invibes.json b/static/bidder-params/invibes.json new file mode 100644 index 00000000000..11d276f8d3e --- /dev/null +++ b/static/bidder-params/invibes.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Invibes Adapter Params", + "description": "A schema which validates params accepted by the Invibes adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "An ID which identifies the site selling the impression" + }, + "domainId": { + "type": "integer", + "description": "Ad domain id" + }, + "debug": { + "type": "object", + "properties": { + "testBvid": { + "type": "string" + }, + "testLog": { + "type": "boolean" + } + }, + "description": "Parameters used for debugging porposes" + } + }, + "required": ["placementId"] +} diff --git a/static/bidder-params/krushmedia.json b/static/bidder-params/krushmedia.json new file mode 100644 index 00000000000..e395da85617 --- /dev/null +++ b/static/bidder-params/krushmedia.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Krushmedia Adapter Params", + "description": "A schema which validates params accepted by the Krushmedia adapter", + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "ssp key" + } + }, + "required": ["key"] + } \ No newline at end of file diff --git a/static/bidder-params/kubient.json b/static/bidder-params/kubient.json index a75dd734ff2..9b975289a7b 100644 --- a/static/bidder-params/kubient.json +++ b/static/bidder-params/kubient.json @@ -3,5 +3,11 @@ "title": "Kubient Adapter Params", "description": "A schema which validates params accepted by the Kubient adapter", "type": "object", - "properties": { } + "properties": { + "zoneid": { + "type": "string", + "description": "Zone ID identifies Kubient placement ID.", + "minLength": 1 + } + } } diff --git a/static/bidder-params/logicad.json b/static/bidder-params/logicad.json new file mode 100644 index 00000000000..2a892f91266 --- /dev/null +++ b/static/bidder-params/logicad.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Logicad Adapter Params", + "description": "A schema which validates params accepted by the Logicad adapter", + "type": "object", + "properties": { + "tid": { + "type": "string", + "description": "Logicad for Publishers placement ID" + } + }, + "required": ["tid"] +} diff --git a/static/bidder-params/nobid.json b/static/bidder-params/nobid.json new file mode 100644 index 00000000000..576dbfecb5c --- /dev/null +++ b/static/bidder-params/nobid.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "NoBid Adapter Params", + "description": "A schema which validates params accepted by the NoBid adapter", + + "type": "object", + "properties": { + "siteId": { + "type": "integer", + "description": "A Required ID which identifies the NoBid site. The siteId paramerter is provided by your NoBid account manager." + }, "placementId": { + "type": "integer", + "description": "An oprional ID which identifies an adunit in a site. The placementId paramerter is provided by your NoBid account manager." + } + }, + "required": ["siteId"] + } + \ No newline at end of file diff --git a/static/bidder-params/openx.json b/static/bidder-params/openx.json index 93a672ed629..6dbd10178e4 100644 --- a/static/bidder-params/openx.json +++ b/static/bidder-params/openx.json @@ -16,6 +16,11 @@ "pattern": "\\.[a-zA-Z]{2,3}$", "format": "hostname" }, + "platform": { + "type": "string", + "description": "The platform id for the customer.", + "format": "uuid" + }, "customFloor": { "type": "number", "description": "The minimum CPM price in USD.", @@ -26,6 +31,19 @@ "description": "User-defined targeting key-value pairs." } }, - - "required": ["unit", "delDomain"] + "required": [ + "unit" + ], + "anyOf": [ + { + "required": [ + "delDomain" + ] + }, + { + "required": [ + "platform" + ] + } + ] } diff --git a/static/bidder-params/silvermob.json b/static/bidder-params/silvermob.json new file mode 100644 index 00000000000..8ebc85a2ab7 --- /dev/null +++ b/static/bidder-params/silvermob.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "SilverMob Adapter Params", + "description": "A schema which validates params accepted by the SilverMob adapter", + "type": "object", + "properties": { + "zoneid": { + "type": "string", + "description": "Zone ID" + }, + "host": { + "type": "string", + "description": "Host" + } + }, + "required": ["zoneid", "host"] + } \ No newline at end of file diff --git a/static/bidder-params/smaato.json b/static/bidder-params/smaato.json new file mode 100644 index 00000000000..aa91c4bacc5 --- /dev/null +++ b/static/bidder-params/smaato.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smaato Adapter Params", + "description": "A schema which validates params accepted by the Smaato adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "A unique identifier for this impression within the context of the bid request" + }, + "adspaceId": { + "type": "string", + "description": "Identifier for specific ad placement is SOMA `adspaceId`" + } + }, + "required": ["publisherId","adspaceId"] +} \ No newline at end of file diff --git a/static/bidder-params/smartadserver.json b/static/bidder-params/smartadserver.json new file mode 100644 index 00000000000..b76a3bd6ac9 --- /dev/null +++ b/static/bidder-params/smartadserver.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smartadserver Adapter Params", + "description": "A schema which validates params accepted by the Smartadserver adapter", + + "type": "object", + "properties": { + "siteId": { + "type": "integer", + "description": "The site id.", + "minimum": 1 + }, + "pageId": { + "type": "integer", + "description": "The page id.", + "minimum": 1 + }, + "formatId": { + "type": "integer", + "description": "The format id.", + "minimum": 1 + }, + "networkId": { + "type": "integer", + "description": "The network id.", + "minimum": 1 + } + }, + "dependencies": { + "siteId": { "required": ["pageId", "formatId"] }, + "pageId": { "required": ["siteId", "formatId"] }, + "formatId": { "required": ["siteId", "pageId"] } + }, + "required": ["networkId"] +} \ No newline at end of file diff --git a/static/bidder-params/smartyads.json b/static/bidder-params/smartyads.json new file mode 100644 index 00000000000..629fdde3263 --- /dev/null +++ b/static/bidder-params/smartyads.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "SmartyAds Adapter Params", + "description": "A schema which validates params accepted by the SmartyAds adapter", + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Network host to send request" + }, + "sourceid": { + "type": "string", + "description": "Partner id" + }, + "accountid": { + "type": "string", + "description": "Account id" + } + }, + "required": ["host", "sourceid", "accountid"] +} \ No newline at end of file diff --git a/static/category-mapping/freewheel/freewheel.json b/static/category-mapping/freewheel/freewheel.json index 1c4a4fa2471..1b849b5392d 100644 --- a/static/category-mapping/freewheel/freewheel.json +++ b/static/category-mapping/freewheel/freewheel.json @@ -1178,5 +1178,81 @@ "IAB22-3": { "id": "410", "name": "Product" + }, + "IAB1": { + "id": "392", + "name": "Entertainment" + }, + "IAB2": { + "id": "399", + "name": "Automotive" + }, + "IAB3": { + "id": "393", + "name": "Business Services" + }, + "IAB4": { + "id": "405", + "name": "Educational Services" + }, + "IAB5": { + "id": "405", + "name": "Educational Services" + }, + "IAB7": { + "id": "406", + "name": "Health Care Services" + }, + "IAB8": { + "id": "394", + "name": "Food" + }, + "IAB9": { + "id": "392", + "name": "Entertainment" + }, + "IAB10": { + "id": "434", + "name": "Home Furnishings" + }, + "IAB11": { + "id": "398", + "name": "Government/Municipal" + }, + "IAB12": { + "id": "438", + "name": "News" + }, + "IAB13": { + "id": "393", + "name": "Business Services" + }, + "IAB16": { + "id": "423", + "name": "Pet Food/Supplies" + }, + "IAB17": { + "id": "425", + "name": "Professional Sports" + }, + "IAB18": { + "id": "397", + "name": "Apparel" + }, + "IAB19": { + "id": "409", + "name": "Computing Product" + }, + "IAB20": { + "id": "395", + "name": "Travel/Hotel/Airlines" + }, + "IAB21": { + "id": "416", + "name": "Real Estate" + }, + "IAB22": { + "id": "403", + "name": "Retail Stores/Chains" } -} \ No newline at end of file +} diff --git a/static/tcf1/fallback_gvl.json b/static/tcf1/fallback_gvl.json new file mode 100644 index 00000000000..9f1c8506b32 --- /dev/null +++ b/static/tcf1/fallback_gvl.json @@ -0,0 +1 @@ +{"vendorListVersion":215,"lastUpdated":"2020-08-13T16:00:19Z","purposes":[{"id":1,"name":"Information storage and access","description":"The storage of information, or access to information that is already stored, on your device such as advertising identifiers, device identifiers, cookies, and similar technologies."},{"id":2,"name":"Personalisation","description":"The collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as on other websites or apps, over time. Typically, the content of the site or app is used to make inferences about your interests, which inform future selection of advertising and/or content."},{"id":3,"name":"Ad selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver advertisements for you, and to measure the delivery and effectiveness of such advertisements. This includes using previously collected information about your interests to select ads, processing data about what advertisements were shown, how often they were shown, when and where they were shown, and whether you took any action related to the advertisement, including for example clicking an ad or making a purchase. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as websites or apps, over time."},{"id":4,"name":"Content selection, delivery, reporting","description":"The collection of information, and combination with previously collected information, to select and deliver content for you, and to measure the delivery and effectiveness of such content. This includes using previously collected information about your interests to select content, processing data about what content was shown, how often or how long it was shown, when and where it was shown, and whether the you took any action related to the content, including for example clicking on content. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, such as websites or apps, over time."},{"id":5,"name":"Measurement","description":"The collection of information about your use of the content, and combination with previously collected information, used to measure, understand, and report on your usage of the service. This does not include personalisation, the collection of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, i.e. on other service, such as websites or apps, over time."}],"features":[{"id":1,"name":"Matching Data to Offline Sources","description":"Combining data from offline sources that were initially collected in other contexts."},{"id":2,"name":"Linking Devices","description":"Allow processing of a user's data to connect such user across multiple devices."},{"id":3,"name":"Precise Geographic Location Data","description":"Allow processing of a user's precise geographic location data in support of a purpose for which that certain third party has consent."}],"vendors":[{"id":8,"name":"Emerse Sverige AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.emerse.com/privacy-policy/"},{"id":9,"name":"AdMaxim Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.admaxim.com/admaxim-privacy-policy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":12,"name":"BeeswaxIO Corporation","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beeswax.com/privacy/"},{"id":28,"name":"TripleLift, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://triplelift.com/privacy/"},{"id":27,"name":"ADventori SAS","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adventori.com/with-us/legal-notice/"},{"id":25,"name":"Verizon Media EMEA Limited","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.verizonmedia.com/policies/ie/en/verizonmedia/privacy/index.html"},{"id":26,"name":"Venatus Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.venatusmedia.com/privacy/"},{"id":1,"name":"Exponential Interactive, Inc d/b/a VDX.tv","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://vdx.tv/privacy/"},{"id":6,"name":"AdSpirit GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adspirit.de/privacy"},{"id":30,"name":"BidTheatre AB","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.bidtheatre.com/privacy-policy"},{"id":24,"name":"Epsilon","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.conversantmedia.eu/legal/privacy-policy"},{"id":29,"name":"Etarget SE","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.etarget.sk/privacy.php","deletedDate":"2020-06-01T00:00:00Z"},{"id":39,"name":"ADITION technologies AG","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.adition.com/datenschutz"},{"id":11,"name":"Quantcast International Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.quantcast.com/privacy/"},{"id":15,"name":"Adikteev","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adikteev.com/privacy-policy-eng/"},{"id":4,"name":"Roq.ad Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.roq.ad/privacy-policy"},{"id":7,"name":"Vibrant Media Limited","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vibrantmedia.com/en/privacy-policy/"},{"id":2,"name":"Captify Technologies Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.captify.co.uk/privacy-policy/"},{"id":37,"name":"NEURAL.ONE","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://web.neural.one/privacy-policy/"},{"id":13,"name":"Sovrn Holdings Inc","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sovrn.com/sovrn-privacy/"},{"id":34,"name":"NEORY GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.neory.com/privacy.html"},{"id":32,"name":"Xandr, Inc.","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.xandr.com/privacy/platform-privacy-policy/"},{"id":10,"name":"Index Exchange, Inc. ","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[2,3],"policyUrl":"https://www.indexexchange.com/privacy"},{"id":57,"name":"ADARA MEDIA UNLIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://adara.com/privacy-promise/"},{"id":63,"name":"Avocet Systems Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://avocet.io/privacy-portal"},{"id":51,"name":"xAd, Inc. dba GroundTruth","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.groundtruth.com/privacy-policy/"},{"id":49,"name":"TRADELAB","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://tradelab.com/en/privacy/"},{"id":45,"name":"Smart Adserver","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://smartadserver.com/end-user-privacy-policy/"},{"id":52,"name":"The Rubicon Project, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[3],"policyUrl":"http://www.rubiconproject.com/rubicon-project-yield-optimization-privacy-policy/"},{"id":71,"name":"Roku Advertising Services","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://docs.roku.com/published/userprivacypolicy/en/us"},{"id":79,"name":"MediaMath, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.mediamath.com/privacy-policy/"},{"id":91,"name":"Criteo SA","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.criteo.com/privacy/"},{"id":85,"name":"Crimtan Holdings Limited","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[1,3],"policyUrl":"https://crimtan.com/privacy/"},{"id":16,"name":"RTB House S.A.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.rtbhouse.com/privacy-center/services-privacy-policy/"},{"id":86,"name":"Scene Stealer Limited","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"http://scenestealer.tv/privacy-policy/"},{"id":94,"name":"Blis Media Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.blis.com/privacy/"},{"id":73,"name":"Simplifi Holdings Inc.","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2,3],"policyUrl":"https://simpli.fi/site-privacy-policy/"},{"id":33,"name":"ShareThis, Inc","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://sharethis.com/privacy/"},{"id":20,"name":"N Technologies Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://n.rich/privacy-notice"},{"id":55,"name":"Madison Logic, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.madisonlogic.com/privacy/"},{"id":53,"name":"Sirdata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.sirdata.com/privacy/"},{"id":69,"name":"OpenX","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.openx.com/legal/privacy-policy/"},{"id":98,"name":"GroupM UK Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.groupm.com/privacy-notice"},{"id":62,"name":"Justpremium BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://justpremium.com/privacy-policy/"},{"id":19,"name":"Intent Media, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://intentmedia.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":43,"name":"Vdopia DBA Chocolate Platform","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://chocolateplatform.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":36,"name":"RhythmOne DBA Unruly Group Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.rhythmone.com/privacy-policy"},{"id":80,"name":"Sharethrough, Inc","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://platform-cdn.sharethrough.com/privacy-policy"},{"id":81,"name":"PulsePoint, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pulsepoint.com/privacy-policy/website","deletedDate":"2020-07-06T00:00:00Z"},{"id":23,"name":"Amobee, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.amobee.com/trust/privacy-guidelines"},{"id":35,"name":"Purch Group, Inc.","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://www.purch.com/privacy-policy/","deletedDate":"2019-05-30T00:00:00Z"},{"id":3,"name":"affilinet","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.affili.net/de/footeritem/datenschutz","deletedDate":"2019-06-21T00:00:00Z"},{"id":74,"name":"Admotion SRL","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.admotion.com/policy/","deletedDate":"2019-07-24T00:00:00Z"},{"id":191,"name":"realzeit GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://realzeitmedia.com/privacy.html","deletedDate":"2019-04-29T00:00:00Z"},{"id":197,"name":"Switch Concepts Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.switchconcepts.com/privacy-policy","deletedDate":"2019-07-26T00:00:00Z"},{"id":390,"name":"Parsec Media Inc.","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,3],"policyUrl":"www.parsec.media/privacy-policy","deletedDate":"2019-06-27T00:00:00Z"},{"id":459,"name":"uppr GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://netzwerk.uppr.de/privacy-policy.do","deletedDate":"2019-06-17T00:00:00Z"},{"id":221,"name":"LEMO MEDIA GROUP LIMITED","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.lemomedia.com/terms.pdf","deletedDate":"2019-06-28T00:00:00Z"},{"id":478,"name":"RevLifter Ltd","purposeIds":[1],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.revlifter.com/privacy-policy","deletedDate":"2019-07-15T00:00:00Z"},{"id":500,"name":"Turbo","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.turboadv.com/white-rabbit-privacy-policy/","deletedDate":"2019-07-12T00:00:00Z"},{"id":68,"name":"Sizmek by Amazon","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.sizmek.com/privacy-policy/"},{"id":75,"name":"M32 Connect Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://m32.media/privacy-cookie-policy/"},{"id":17,"name":"Greenhouse Group BV (with its trademark LemonPI)","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.lemonpi.io/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":61,"name":"GumGum, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://gumgum.com/privacy-policy"},{"id":40,"name":"Active Agent (ADITION technologies AG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.active-agent.com/de/unternehmen/datenschutzerklaerung/"},{"id":76,"name":"PubMatic, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://pubmatic.com/privacy-policy/"},{"id":89,"name":"Tapad, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://www.tapad.com/eu-privacy-policy"},{"id":46,"name":"Skimbit Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://skimlinks.com/pages/privacy-policy"},{"id":66,"name":"adsquare GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adsquare.com/privacy"},{"id":105,"name":"Impression Desk Technologies Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://impressiondesk.com/privacy-policy/","deletedDate":"2019-08-06T00:00:00Z"},{"id":41,"name":"Adverline","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.adverline.com/privacy/"},{"id":82,"name":"Smaato, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.smaato.com/privacy/"},{"id":60,"name":"Rakuten Marketing LLC","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://rakutenadvertising.com/legal-notices/services-privacy-policy/"},{"id":70,"name":"Yieldlab AG","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[3],"policyUrl":"http://www.yieldlab.de/meta-navigation/datenschutz/"},{"id":50,"name":"Adform","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://site.adform.com/privacy-center/platform-privacy/product-and-services-privacy-policy/"},{"id":48,"name":"NetSuccess, s.r.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inres.sk/pp/"},{"id":100,"name":"Fifty Technology Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://fifty.io/privacy-policy.php"},{"id":21,"name":"The Trade Desk","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2,3],"policyUrl":"https://www.thetradedesk.com/general/privacy-policy"},{"id":110,"name":"Dynata LLC","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.opinionoutpost.co.uk/en-gb/policies/privacy"},{"id":42,"name":"Taboola Europe Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.taboola.com/privacy-policy"},{"id":112,"name":"Maytrics GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://maytrics.com/privacy.php","deletedDate":"2019-09-17T00:00:00Z"},{"id":77,"name":"comScore, Inc.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.scorecardresearch.com/privacy.aspx?newlanguage=1"},{"id":109,"name":"LoopMe Limited","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://loopme.com/privacy-policy/"},{"id":120,"name":"Eyeota Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.eyeota.com/privacy-center"},{"id":93,"name":"Adloox SA","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://adloox.com/disclaimer"},{"id":132,"name":"Teads ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.teads.com/privacy-policy/"},{"id":22,"name":"admetrics GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://admetrics.io/en/privacy_policy/"},{"id":102,"name":"Telaria SAS","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":108,"name":"Rich Audience Technologies SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://richaudience.com/privacy/"},{"id":18,"name":"Widespace AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.widespace.com/legal/privacy-policy-notice/"},{"id":122,"name":"Avid Media Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.avidglobalmedia.eu/privacy-policy.html"},{"id":97,"name":"LiveRamp, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.liveramp.com/service-privacy-policy/"},{"id":138,"name":"ConnectAd Realtime GmbH","purposeIds":[1,2],"legIntPurposeIds":[3,4],"featureIds":[],"policyUrl":"http://connectadrealtime.com/privacy/"},{"id":72,"name":"Nano Interactive GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.nanointeractive.com/privacy"},{"id":127,"name":"PIXIMEDIA SAS","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://piximedia.com/privacy/"},{"id":136,"name":"Str\u00f6er SSP GmbH (SSP)","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[2,3],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":111,"name":"Showheroes SE","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://showheroes.com/privacy/"},{"id":56,"name":"Confiant Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.confiant.com/privacy","deletedDate":"2020-05-18T00:00:00Z"},{"id":124,"name":"Teemo SA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://teemo.co/fr/confidentialite/"},{"id":154,"name":"YOC AG","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://yoc.com/privacy/"},{"id":38,"name":"Beemray Oy","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.beemray.com/privacy-policy/","deletedDate":"2020-06-19T00:00:00Z"},{"id":101,"name":"MiQ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://wearemiq.com/privacy-policy/"},{"id":149,"name":"ADman Interactive SLU","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://admanmedia.com/politica.html?setLng=es"},{"id":151,"name":"Admedo Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[3],"policyUrl":"https://www.admedo.com/privacy-policy","deletedDate":"2020-07-17T00:00:00Z"},{"id":153,"name":"MADVERTISE MEDIA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://madvertise.com/en/gdpr/"},{"id":159,"name":"Underdog Media LLC ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://underdogmedia.com/privacy-policy/"},{"id":157,"name":"Seedtag Advertising S.L","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.seedtag.com/en/privacy-policy/"},{"id":145,"name":"Snapsort Inc., operating as Sortable","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://help.sortable.com/help/privacy-policy"},{"id":131,"name":"ID5 Technology SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.id5.io/privacy"},{"id":158,"name":"Reveal Mobile, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://revealmobile.com/privacy"},{"id":147,"name":"Adacado Technologies Inc. (DBA Adacado)","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adacado.com/privacy-policy-april-25-2018/"},{"id":130,"name":"NextRoll, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.nextroll.com/privacy"},{"id":129,"name":"IPONWEB GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.iponweb.com/privacy-policy/"},{"id":128,"name":"BIDSWITCH GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bidswitch.com/privacy-policy/"},{"id":168,"name":"EASYmedia GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://login.rtbmarket.com/gdpr"},{"id":164,"name":"Outbrain UK Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.outbrain.com/legal/privacy#privacy-policy"},{"id":144,"name":"district m inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://districtm.net/en/page/platforms-data-and-privacy-policy/"},{"id":163,"name":"Bombora Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://bombora.com/privacy"},{"id":173,"name":"Yieldmo, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.yieldmo.com/privacy/"},{"id":88,"name":"TreSensa, Inc.","purposeIds":[1,3],"legIntPurposeIds":[2,5],"featureIds":[1],"policyUrl":"https://www.tresensa.com/eu-privacy"},{"id":78,"name":"Flashtalking, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.flashtalking.com/privacypolicy/"},{"id":59,"name":"Sift Media, Inc","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.sift.co/privacy"},{"id":114,"name":"Sublime","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://ayads.co/privacy.php"},{"id":175,"name":"FORTVISION","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://fortvision.com/POC/index.html","deletedDate":"2019-08-09T00:00:00Z"},{"id":133,"name":"digitalAudience","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://digitalaudience.io/legal/privacy-cookies/"},{"id":14,"name":"Adkernel LLC","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://adkernel.com/privacy-policy/"},{"id":180,"name":"Thirdpresence Oy","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"http://www.thirdpresence.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":183,"name":"EMX Digital LLC","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://emxdigital.com/privacy/"},{"id":58,"name":"33Across","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.33across.com/privacy-policy"},{"id":140,"name":"Platform161","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://platform161.com/cookie-and-privacy-policy/"},{"id":90,"name":"Teroa S.A.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.e-planning.net/en/privacy.html"},{"id":141,"name":"1020, Inc. dba Placecast and Ericsson Emodo","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.emodoinc.com/privacy-policy/"},{"id":142,"name":"Media.net Advertising FZ-LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.media.net/en/privacy-policy"},{"id":209,"name":"Delta Projects AB","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[3],"policyUrl":"https://deltaprojects.com/data-collection-policy"},{"id":195,"name":"advanced store GmbH","purposeIds":[2,3],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.advanced-store.com/de/datenschutz/"},{"id":190,"name":"video intelligence AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.vi.ai/privacy-policy/"},{"id":84,"name":"Semasio GmbH","purposeIds":[],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"http://www.semasio.com/privacy-policy/"},{"id":65,"name":"Location Sciences AI Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.locationsciences.ai/privacy-policy/"},{"id":210,"name":"Zemanta, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1],"policyUrl":"http://www.zemanta.com/legal/privacy"},{"id":200,"name":"Tapjoy, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.tapjoy.com/legal/#privacy-policy"},{"id":188,"name":"Sellpoints Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://retargeter.com/service-privacy-policy/","deletedDate":"2019-09-17T00:00:00Z"},{"id":217,"name":"2KDirect, Inc. (dba iPromote)","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.ipromote.com/privacy-policy/"},{"id":156,"name":"Centro, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.centro.net/privacy-policy/"},{"id":194,"name":"Rezonence Limited","purposeIds":[3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://rezonence.com/privacy-policy/"},{"id":226,"name":"Publicis Media GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.publicismedia.de/datenschutz/"},{"id":198,"name":"SYNC","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://redirect.sync.tv/privacy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":227,"name":"ORTEC B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.ortecadscience.com/privacy-policy/"},{"id":225,"name":"Ligatus GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.ligatus.com/en/privacy-policy","deletedDate":"2020-06-19T00:00:00Z"},{"id":205,"name":"Adssets AB","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"http://adssets.com/policy/"},{"id":179,"name":"Collective Europe Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.collectiveuk.com/privacy.html"},{"id":31,"name":"Ogury Ltd.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://www.ogury.com/privacy-policy/"},{"id":92,"name":"1plusX AG","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.1plusx.com/privacy-policy/"},{"id":155,"name":"AntVoice","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.antvoice.com/en/privacypolicy/"},{"id":115,"name":"smartclip Europe GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[2],"policyUrl":"https://privacy-portal.smartclip.net/"},{"id":126,"name":"DoubleVerify Inc.\u200b","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.doubleverify.com/privacy/"},{"id":193,"name":"Mediasmart Mobile S.L.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://mediasmart.io/privacy/"},{"id":245,"name":"IgnitionOne","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.ignitionone.com/privacy-policy/","deletedDate":"2020-06-30T00:00:00Z"},{"id":213,"name":"emetriq GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.emetriq.com/datenschutz/"},{"id":244,"name":"Temelio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://temelio.com/vie-privee"},{"id":224,"name":"adrule mobile GmbH","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.adrule.net/de/datenschutz/"},{"id":174,"name":"A Million Ads Ltd","purposeIds":[2],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://www.amillionads.com/privacy-policy"},{"id":192,"name":"remerge GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://remerge.io/privacy-policy.html"},{"id":232,"name":"Rockerbox, Inc","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"http://rockerbox.com/privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":256,"name":"Bounce Exchange, Inc","purposeIds":[1],"legIntPurposeIds":[2,4,5],"featureIds":[1,2],"policyUrl":"https://www.bouncex.com/privacy/"},{"id":234,"name":"ZBO Media","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zbo.media/mentions-legales/politique-de-confidentialite-service-publicitaire/"},{"id":246,"name":"Smartology Limited","purposeIds":[3],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://www.smartology.net/privacy-policy/"},{"id":241,"name":"OneTag Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.onetag.com/privacy/"},{"id":254,"name":"LiquidM Technology GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liquidm.com/privacy-policy/"},{"id":215,"name":"ARMIS SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://armis.tech/en/armis-personal-data-privacy-policy/"},{"id":167,"name":"Audiens S.r.l.","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.audiens.com/privacy"},{"id":240,"name":"7Hops.com Inc. (ZergNet)","purposeIds":[],"legIntPurposeIds":[1,4,5],"featureIds":[],"policyUrl":"https://zergnet.com/privacy"},{"id":235,"name":"Bucksense Inc","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.bucksense.com/platform-privacy-policy/"},{"id":185,"name":"Bidtellect, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.bidtellect.com/privacy-policy/"},{"id":258,"name":"Adello Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[3],"policyUrl":"https://www.adello.com/privacy-policy/"},{"id":169,"name":"RTK.IO, Inc","purposeIds":[1,4],"legIntPurposeIds":[2,3,5],"featureIds":[1,3],"policyUrl":"http://www.rtk.io/privacy.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":208,"name":"Spotad","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.spotad.co/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":211,"name":"AdTheorent, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://adtheorent.com/privacy-policy"},{"id":229,"name":"Digitize New Media Ltd","purposeIds":[2,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitize.ie/online-privacy","deletedDate":"2020-07-17T00:00:00Z"},{"id":273,"name":"Bannerflow AB","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.bannerflow.com/privacy "},{"id":104,"name":"Sonobi, Inc","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"http://sonobi.com/privacy-policy/"},{"id":162,"name":"Unruly Group Ltd","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://unruly.co/privacy/"},{"id":249,"name":"Spolecznosci Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.spolecznosci.pl/polityka-prywatnosci"},{"id":125,"name":"Research Now Group, Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.valuedopinions.co.uk/privacy","deletedDate":"2019-09-17T00:00:00Z"},{"id":170,"name":"Goodway Group, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://goodwaygroup.com/privacy-policy/"},{"id":160,"name":"Netsprint SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://netsprint.eu/privacy.html"},{"id":189,"name":"Intowow Innovation Ltd.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.intowow.com/privacy/","deletedDate":"2019-08-12T00:00:00Z"},{"id":279,"name":"Mirando GmbH & Co KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://wwwmirando.de/datenschutz/"},{"id":269,"name":"Sanoma Media Finland","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://sanoma.fi/tietoa-meista/tietosuoja/","deletedDate":"2019-08-07T00:00:00Z"},{"id":276,"name":"Viralize SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://viralize.com/privacy-policy"},{"id":87,"name":"Genius Sports Media Limited","purposeIds":[2,4],"legIntPurposeIds":[1,3,5],"featureIds":[2,3],"policyUrl":"https://www.geniussports.com/privacy-policy"},{"id":182,"name":"Collective, Inc. dba Visto","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vistohub.com/privacy-policy/","deletedDate":"2019-07-26T00:00:00Z"},{"id":255,"name":"Onnetwork Sp. z o.o.","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.onnetwork.tv/pp_services.php"},{"id":203,"name":"Revcontent, LLC","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://intercom.help/revcontent2/en/articles/2290675-revcontent-s-privacy-policy"},{"id":260,"name":"RockYou, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,5],"featureIds":[3],"policyUrl":"https://rockyou.com/privacy-policy/","deletedDate":"2019-08-09T00:00:00Z"},{"id":237,"name":"LKQD, a division of Nexstar Digital, LLC.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.lkqd.com/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":274,"name":"Golden Bees","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.goldenbees.fr/en/privacy-charter/"},{"id":280,"name":"Spot.IM LTD","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.spot.im/privacy/"},{"id":239,"name":"Triton Digital Canada Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.tritondigital.com/privacy-policies"},{"id":177,"name":"plista GmbH","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.plista.com/about/privacy/"},{"id":201,"name":"TimeOne","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://privacy.timeonegroup.com/en/","deletedDate":"2020-05-15T00:00:00Z"},{"id":150,"name":"Inskin Media LTD","purposeIds":[2,3,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"http://www.inskinmedia.com/privacy-policy.html"},{"id":252,"name":"Jaduda GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.jadudamobile.com/datenschutzerklaerung/"},{"id":248,"name":"Converge-Digital","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://converge-digital.com/privacy-policy/"},{"id":161,"name":"Smadex SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://smadex.com/end-user-privacy-policy/"},{"id":285,"name":"Comcast International France SAS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.freewheel.com/privacy-policy"},{"id":228,"name":"McCann Discipline LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.primis.tech/privacy-policy/"},{"id":299,"name":"AdClear GmbH","purposeIds":[1,5],"legIntPurposeIds":[2,3,4],"featureIds":[1,2],"policyUrl":"https://www.adclear.de/datenschutzerklaerung/"},{"id":277,"name":"Codewise VL Sp. z o.o. Sp. k","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://voluumdsp.com/end-user-privacy-policy/"},{"id":259,"name":"ADYOULIKE SA","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.adyoulike.com/privacy_policy.php"},{"id":272,"name":"A.Mob","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.we-are-adot.com/privacy-policy/"},{"id":230,"name":"Steel House, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://steelhouse.com/privacy-policy/"},{"id":253,"name":"Improve Digital BV","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.improvedigital.com/platform-privacy-policy"},{"id":304,"name":"On Device Research Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://s.on-device.com/privacyPolicy"},{"id":314,"name":"Keymantics","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.keymantics.com/assets/privacy-policy.pdf"},{"id":257,"name":"R-TARGET","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"http://www.r-target.com/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":317,"name":"mainADV Srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.mainad.com/privacy-policy/"},{"id":278,"name":"Integral Ad Science, Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://integralads.com/privacy-policy/"},{"id":291,"name":"Qwertize","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.qwertize.com/en/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":295,"name":"Sojern, Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.sojern.com/privacy/product-privacy-policy/"},{"id":315,"name":"Celtra, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.celtra.com/privacy-policy/"},{"id":165,"name":"SpotX, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1],"policyUrl":"https://www.spotx.tv/privacy-policy/"},{"id":47,"name":"ADMAN - Phaistos Networks, S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.adman.gr/privacy"},{"id":134,"name":"SMARTSTREAM.TV GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2],"policyUrl":"https://www.smartstream.tv/en/productprivacy"},{"id":325,"name":"Knorex","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.knorex.com/privacy"},{"id":316,"name":"Gamned","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.gamned.com/privacy-policy/"},{"id":318,"name":"Accorp Sp. z o.o.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"http://www.instytut-pollster.pl/privacy-policy/"},{"id":199,"name":"ADUX","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adux.com/donnees-personelles/"},{"id":236,"name":"PowerLinks Media Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[3],"policyUrl":"https://www.powerlinks.com/privacy-policy/"},{"id":294,"name":"Jivox Corporation","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.jivox.com/privacy"},{"id":143,"name":"Connatix Native Exchange Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://connatix.com/privacy-policy/"},{"id":297,"name":"Polar Mobile Group Inc.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://privacy.polar.me"},{"id":319,"name":"Clipcentric, Inc.","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://clipcentric.com/privacy.bhtml"},{"id":290,"name":"Readpeak Oy","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://readpeak.com/privacy-policy/"},{"id":323,"name":"DAZN Media Services Limited","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://www.goal.com/en-gb/legal/privacy-policy"},{"id":119,"name":"Fusio by S4M","purposeIds":[1,2,5],"legIntPurposeIds":[3],"featureIds":[1,3],"policyUrl":"http://www.s4m.io/privacy-policy/"},{"id":302,"name":"Mobile Professionals BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mobpro.com/privacy.html"},{"id":212,"name":"usemax advertisement (Emego GmbH)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.usemax.de/?l=privacy"},{"id":264,"name":"Adobe Advertising Cloud","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.adobe.com/privacy/experience-cloud.html"},{"id":44,"name":"The ADEX GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://theadex.com/privacy-opt-out/"},{"id":282,"name":"Welect GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.welect.de/datenschutz"},{"id":238,"name":"StackAdapt","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.stackadapt.com/privacy"},{"id":284,"name":"WEBORAMA","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://weborama.com/privacy_en/"},{"id":148,"name":"Liveintent Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://liveintent.com/services-privacy-policy/"},{"id":64,"name":"DigiTrust / IAB Tech Lab","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.digitru.st/privacy-policy/"},{"id":301,"name":"zeotap GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://zeotap.com/privacy_policy"},{"id":275,"name":"TabMo SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://static.tabmo.io.s3.amazonaws.com/privacy-policy/index.html"},{"id":310,"name":"Adevinta Spain S.L.U.","purposeIds":[],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"https://www.adevinta.com/about/privacy/"},{"id":139,"name":"Permodo GmbH","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://permodo.com/de/privacy.html"},{"id":326,"name":"AdTiming Technology Company Limited","purposeIds":[3,5],"legIntPurposeIds":[1,2,4],"featureIds":[],"policyUrl":"http://www.adtiming.com/en/privacypolicy.html"},{"id":262,"name":"Fyber ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.fyber.com/legal/privacy-policy/"},{"id":331,"name":"ad6media","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.ad6media.fr/privacy"},{"id":345,"name":"The Kantar Group Limited","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.kantar.com/cookies-policies"},{"id":308,"name":"Rockabox Media Ltd","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[],"policyUrl":"http://scoota.com/privacy-policy"},{"id":270,"name":"Marfeel Solutions, SL","purposeIds":[],"legIntPurposeIds":[2],"featureIds":[],"policyUrl":"https://www.marfeel.com/privacy-policy/"},{"id":333,"name":"InMobi Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":202,"name":"Telaria, Inc","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://telaria.com/privacy-policy/"},{"id":328,"name":"Gemius SA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.gemius.com/cookie-policy.html"},{"id":281,"name":"Wizaly","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.wizaly.com/terms-of-use#privacy-policy"},{"id":354,"name":"Apester Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://apester.com/privacy-policy/"},{"id":320,"name":"Adelphic LLC","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://adelphic.com/platform/privacy/"},{"id":359,"name":"AerServ LLC","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.inmobi.com/privacy-policy-for-eea"},{"id":265,"name":"Instinctive, Inc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://instinctive.io/privacy"},{"id":349,"name":"Optomaton UG","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://optomaton.com/privacy.html"},{"id":288,"name":"Video Media Groep B.V.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://www.videomediagroup.com/wp-content/uploads/2016/01/Privacy-policy-VMG.pdf","deletedDate":"2019-09-17T00:00:00Z"},{"id":266,"name":"Digilant Spain, SLU","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.digilant.com/es/politica-privacidad/"},{"id":339,"name":"Vuble","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.vuble.tv/us/privacy","deletedDate":"2019-08-26T00:00:00Z"},{"id":303,"name":"Orion Semantics","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://static.orion-semantics.com/privacy.html"},{"id":261,"name":"Signal Digital Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.signal.co/privacy-policy/"},{"id":83,"name":"Visarity Technologies GmbH","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://primo.design/docs/PrivacyPolicyPrimo.html"},{"id":343,"name":"DIGITEKA Technologies","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.ultimedia.com/POLICY.html"},{"id":330,"name":"Linicom","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://www.linicom.com/privacy/","deletedDate":"2020-06-08T00:00:00Z"},{"id":231,"name":"AcuityAds Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.acuityads.com/corporate-privacy-policy.html"},{"id":216,"name":"Mindlytix SAS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://mindlytix.com/privacy/"},{"id":311,"name":"Mobfox US LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobfox.com/privacy-policy/"},{"id":358,"name":"MGID Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mgid.com/privacy-policy"},{"id":152,"name":"Meetrics GmbH","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.meetrics.com/en/data-privacy/"},{"id":251,"name":"Yieldlove GmbH","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"http://www.yieldlove.com/cookie-policy"},{"id":344,"name":"My6sense Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[2,4],"featureIds":[],"policyUrl":"https://my6sense.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":347,"name":"Ezoic Inc.","purposeIds":[2,4,5],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.ezoic.com/terms/"},{"id":218,"name":"Bigabid Media ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://www.bigabid.com/privacy-policy"},{"id":350,"name":"Free Stream Media Corp. dba Samba TV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":351,"name":"Samba TV UK Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://samba.tv/legal/privacy-policy/"},{"id":341,"name":"Somo Audience Corp","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[1,2,3],"policyUrl":"https://somoaudience.com/legal/","deletedDate":"2020-07-06T00:00:00Z"},{"id":380,"name":"Vidoomy Media SL","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"http://vidoomy.com/privacy-policy.html"},{"id":378,"name":"communicationAds GmbH & Co. KG","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.communicationads.net/aboutus/privacy/"},{"id":369,"name":"Getintent USA, inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://getintent.com/privacy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":184,"name":"mediarithmics SAS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mediarithmics.com/en-us/content/privacy-policy"},{"id":368,"name":"VECTAURY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.vectaury.io/en/personal-data"},{"id":373,"name":"Nielsen Marketing Cloud","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"http://www.nielsen.com/us/en/privacy-statement/exelate-privacy-policy.html"},{"id":214,"name":"Digital Control GmbH & Co. KG","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://advolution.de/privacy.php","deletedDate":"2020-05-06T00:00:00Z"},{"id":388,"name":"numberly","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://numberly.com/en/privacy/"},{"id":250,"name":"Qriously Ltd","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.brandwatch.com/legal/qriously-privacy-notice/"},{"id":223,"name":"Audience Trading Platform Ltd.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://atp.io/privacy-policy"},{"id":387,"name":"Triapodi Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appreciate.mobi/page.html#/end-user-privacy-policy"},{"id":312,"name":"Exactag GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.exactag.com/en/data-privacy/"},{"id":178,"name":"Hybrid Theory","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://hybridtheory.com/privacy-policy/"},{"id":377,"name":"AddApptr GmbH","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.addapptr.com/data-privacy"},{"id":382,"name":"The Reach Group GmbH","purposeIds":[1,2,4,5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://trg.de/en/privacy-statement/"},{"id":206,"name":"Hybrid Adtech GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://hybrid.ai/data_protection_policy"},{"id":403,"name":"Mobusi Mobile Advertising S.L.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mobusi.com/privacy.en.html","deletedDate":"2020-07-17T00:00:00Z"},{"id":385,"name":"Oracle Data Cloud","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://www.oracle.com/legal/privacy/marketing-cloud-data-cloud-privacy-policy.html"},{"id":404,"name":"Duplo Media AS","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.easy-ads.com/privacypolicy.htm"},{"id":242,"name":"twiago GmbH","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.twiago.com/datenschutz/"},{"id":376,"name":"Pocketmath Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.pocketmath.com/privacy-policy"},{"id":402,"name":"Effiliation","purposeIds":[2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://inter.effiliation.com/politique-confidentialite.html"},{"id":413,"name":"Eulerian Technologies","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.eulerian.com/en/privacy/"},{"id":400,"name":"Whenever Media Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.whenevermedia.com/privacy-policy","deletedDate":"2019-07-29T00:00:00Z"},{"id":171,"name":"Webedia","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webedia-group.com/site/privacy-policy","deletedDate":"2020-07-01T00:00:00Z"},{"id":398,"name":"Yormedia Solutions Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.yormedia.com/privacy-and-cookies-notice/","deletedDate":"2019-08-06T00:00:00Z"},{"id":415,"name":"Seenthis AB","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://seenthis.co/privacy-notice-2018-04-18.pdf"},{"id":263,"name":"Nativo, Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.nativo.com/interest-based-ads"},{"id":329,"name":"Browsi Mobile Ltd","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://gobrowsi.com/browsi-privacy-policy/"},{"id":389,"name":"Bidmanagement GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adspert.net/en/privacy/","deletedDate":"2020-07-01T00:00:00Z"},{"id":337,"name":"SheMedia, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shemedia.com/ad-services-privacy-policy"},{"id":422,"name":"Brand Metrics Sweden AB","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://collector.brandmetrics.com/brandmetrics_privacypolicy.pdf"},{"id":421,"name":"LeftsnRight, Inc. dba LIQWID","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://liqwid.solutions/privacy-policy","deletedDate":"2020-06-30T00:00:00Z"},{"id":426,"name":"TradeTracker","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[2],"policyUrl":"https://tradetracker.com/privacy-policy/","deletedDate":"2019-08-21T00:00:00Z"},{"id":394,"name":"AudienceProject Aps","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://privacy.audienceproject.com"},{"id":287,"name":"Avazu Inc.","purposeIds":[],"legIntPurposeIds":[1,3,4],"featureIds":[3],"policyUrl":"http://avazuinc.com/opt-out/","deletedDate":"2020-08-03T00:00:00Z"},{"id":243,"name":"Cloud Technologies S.A.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cloudtechnologies.pl/en/internet-advertising-privacy-policy"},{"id":113,"name":"iotec global Ltd.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.iotecglobal.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":338,"name":"dunnhumby Germany GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.sociomantic.com/privacy/en/","deletedDate":"2020-07-17T00:00:00Z"},{"id":405,"name":"IgnitionAi Ltd","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[2],"policyUrl":"https://www.isitelab.io/default.aspx","deletedDate":"2020-07-03T00:00:00Z"},{"id":416,"name":"Commanders Act","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.commandersact.com/en/privacy/"},{"id":434,"name":"DynAdmic","purposeIds":[1,3],"legIntPurposeIds":[2,4],"featureIds":[1,3],"policyUrl":"http://eu.dynadmic.com/privacy-policy/"},{"id":435,"name":"SINGLESPOT SAS ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.singlespot.com/privacy_policy?locale=fr"},{"id":409,"name":"Arrivalist Co.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[1,2],"policyUrl":"https://www.arrivalist.com/privacy"},{"id":321,"name":"Ziff Davis LLC","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.ziffdavis.com/privacy-policy"},{"id":436,"name":"INVIBES GROUP","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[1,2,3],"policyUrl":"http://www.invibes.com/terms"},{"id":442,"name":"R-Advertising","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-20T00:00:00Z"},{"id":362,"name":"Myntelligence S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://myntelligence.com/privacy-page/"},{"id":418,"name":"PROXISTORE","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://www.proxistore.com/common/en/cgv"},{"id":449,"name":"Mobile Journey B.V.","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://mobilejourney.com/Privacy-Policy","deletedDate":"2019-09-05T00:00:00Z"},{"id":443,"name":"Tradedoubler AB","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[2],"policyUrl":"https://www.tradedoubler.com/en/privacy-policy/","deletedDate":"2019-08-13T00:00:00Z"},{"id":429,"name":"Signals","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://signalsdata.com/platform-cookie-policy/"},{"id":335,"name":"Beachfront Media LLC","purposeIds":[],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"http://beachfront.com/privacy-policy/"},{"id":407,"name":"Publishers Internationale Pty Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pi-rate.com.au/privacy.html","deletedDate":"2019-11-08T00:00:00Z"},{"id":427,"name":"Proxi.cloud Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://proxi.cloud/info/privacy-policy/"},{"id":374,"name":"Bmind a Sales Maker Company, S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.bmind.es/legal-notice/"},{"id":438,"name":"INVIDI technologies AB","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"http://www.invidi.com/wp-content/uploads/2020/02/ad-tech-services-privacy-policy.pdf"},{"id":450,"name":"Neodata Group srl","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.neodatagroup.com/en/security-policy"},{"id":452,"name":"Innovid Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.innovid.com/privacy-policy"},{"id":444,"name":"Playbuzz Ltd (aka EX.CO)","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://ex.co/privacy-policy/"},{"id":412,"name":"Cxense ASA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.cxense.com/about-us/privacy-policy"},{"id":454,"name":"Adimo","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://adimo.co/privacy-policy/","deletedDate":"2019-09-12T00:00:00Z"},{"id":455,"name":"GDMServices, Inc. d/b/a FiksuDSP","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://fiksu.com/privacy-policy/"},{"id":298,"name":"Cuebiq Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.cuebiq.com/privacypolicy/","deletedDate":"2019-08-30T00:00:00Z"},{"id":423,"name":"travel audience GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://travelaudience.com/product-privacy-policy/"},{"id":397,"name":"Demandbase, Inc. ","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.demandbase.com/privacy-policy/"},{"id":381,"name":"Solocal","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://frontend.adhslx.com/privacy.html?"},{"id":425,"name":"ADRINO Sp. z o.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.adrino.pl/ciasteczkowa-polityka/","deletedDate":"2019-09-05T00:00:00Z"},{"id":365,"name":"Forensiq LLC","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1,3],"policyUrl":"https://impact.com/privacy-policy/"},{"id":447,"name":"Adludio Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.adludio.com/privacy-policy/"},{"id":410,"name":"Adtelligent Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtelligent.com/privacy-policy/"},{"id":137,"name":"Str\u00f6er SSP GmbH (DSP)","purposeIds":[],"legIntPurposeIds":[1,2,3],"featureIds":[],"policyUrl":"https://www.stroeer.de/fileadmin/de/Konvergenz_und_Konzepte/Daten_und_Technologien/Stroeer_SSP/Downloads/Datenschutz_Stroeer_SSP.pdf"},{"id":395,"name":"PREX Programmatic Exchange GmbH&Co KG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[],"policyUrl":"http://www.programmatic-exchange.com/privacy","deletedDate":"2020-07-03T00:00:00Z"},{"id":462,"name":"Bidstack Limited","purposeIds":[1,2,5],"legIntPurposeIds":[3,4],"featureIds":[2],"policyUrl":"https://www.bidstack.com/privacy-policy/"},{"id":466,"name":"TACTIC\u2122 Real-Time Marketing AS","purposeIds":[],"legIntPurposeIds":[4,5],"featureIds":[],"policyUrl":"https://tacticrealtime.com/privacy/"},{"id":340,"name":"Yieldr UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.yieldr.com/privacy"},{"id":336,"name":"Telecoming S.A.","purposeIds":[3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.telecoming.com/privacy-policy/"},{"id":430,"name":"Ad Unity Ltd","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"http://www.adunity.com/privacy-policy.html","deletedDate":"2019-08-13T00:00:00Z"},{"id":346,"name":"Cybba, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://cybba.com/about/legal/data-processing-agreement/","deletedDate":"2020-08-03T00:00:00Z"},{"id":469,"name":"Zeta Global","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://zetaglobal.com/privacy-policy/"},{"id":440,"name":"DEFINE MEDIA GMBH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.definemedia.de/datenschutz-conative/"},{"id":375,"name":"Affle International","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://affle.com/privacy-policy "},{"id":196,"name":"AdElement Media Solutions Pvt Ltd","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"http://adelement.com/privacy-policy.html"},{"id":268,"name":"Social Tokens Ltd. ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://woobi.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":475,"name":"TAPTAP Digital SL","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1,2,3],"policyUrl":"http://www.taptapnetworks.com/privacy_policy/"},{"id":474,"name":"hbfsTech","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.hbfstech.com/fr/privacy.html"},{"id":448,"name":"Targetspot Belgium SPRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://marketing.targetspot.com/Targetspot/Legal/TargetSpot%20Privacy%20Policy%20-%20June%202018.pdf"},{"id":428,"name":"Internet BillBoard a.s.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"http://www.ibillboard.com/en/privacy-information/"},{"id":461,"name":"B2B Media Group EMEA GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selfcampaign.com/static/privacy","deletedDate":"2019-08-14T00:00:00Z"},{"id":476,"name":"HIRO Media Ltd","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"http://hiro-media.com/privacy.php"},{"id":480,"name":"pilotx.tv","purposeIds":[2,3],"legIntPurposeIds":[1,4,5],"featureIds":[1,2,3],"policyUrl":"https://pilotx.tv/privacy/"},{"id":366,"name":"CerebroAd.com s.r.o.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.cerebroad.com/privacy-policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":392,"name":"Str\u00f6er Mobile Performance GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[3],"policyUrl":"https://stroeermobileperformance.com/?dl=privacy"},{"id":357,"name":"Totaljobs Group Ltd ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.totaljobs.com/privacy-policy"},{"id":486,"name":"Madington","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://delivered-by-madington.com/dat-privacy-policy/"},{"id":468,"name":"NeuStar, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,5],"featureIds":[1,2],"policyUrl":"https://www.home.neustar/privacy"},{"id":458,"name":"AdColony, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1],"policyUrl":"adcolony.com/privacy-policy/"},{"id":489,"name":"YellowHammer Media Group","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.yhmg.com/privacy-policy/","deletedDate":"2019-11-27T00:00:00Z"},{"id":293,"name":"SpringServe, LLC","purposeIds":[],"legIntPurposeIds":[1,3],"featureIds":[],"policyUrl":"https://springserve.com/privacy-policy/"},{"id":484,"name":"STRIATUM SAS","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://adledge.com/data-privacy/"},{"id":493,"name":"Carbon (AI) Limited","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"https://carbonrmp.com/privacy.html"},{"id":495,"name":"Arcspire Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://public.arcspire.io/privacy.pdf"},{"id":496,"name":"Automattic Inc.","purposeIds":[1,2,3,4],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://en.blog.wordpress.com/2017/12/04/updated-privacy-policy/"},{"id":424,"name":"KUPONA GmbH","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.kupona.de/dsgvo/"},{"id":408,"name":"Fidelity Media","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://fidelity-media.com/privacy-policy/"},{"id":473,"name":"Sub2 Technologies Ltd","purposeIds":[3,4,5],"legIntPurposeIds":[1,2],"featureIds":[],"policyUrl":"http://www.sub2tech.com/privacy-policy/"},{"id":467,"name":"Haensel AMS GmbH","purposeIds":[3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://haensel-ams.com/data-privacy/"},{"id":490,"name":"PLAYGROUND XYZ EMEA LTD","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://playground.xyz/privacy"},{"id":464,"name":"Oracle AddThis","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[2],"policyUrl":"http://www.addthis.com/privacy/privacy-policy/","deletedDate":"2020-02-12T00:00:00Z"},{"id":491,"name":"Triboo Data Analytics","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shinystat.com/it/informativa_privacy_generale.html"},{"id":499,"name":"PurposeLab, LLC","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://purposelab.com/privacy/","deletedDate":"2019-10-02T00:00:00Z"},{"id":502,"name":"NEXD","purposeIds":[5],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://nexd.com/privacy-policy"},{"id":465,"name":"Schibsted Product and Tech UK","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.schibsted.com/","deletedDate":"2019-07-26T00:00:00Z"},{"id":497,"name":"Little Big Data sp.z.o.o.","purposeIds":[1,2,4],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://dtxngr.com/legal/"},{"id":492,"name":"LotaData, Inc.","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[1],"policyUrl":"https://lotadata.com/privacy_policy","deletedDate":"2019-10-02T00:00:00Z"},{"id":512,"name":"PubNative GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://pubnative.net/privacy-notice/"},{"id":471,"name":"FlexOffers.com, LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.flexoffers.com/privacy-policy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":494,"name":"Cablato Limited","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://cablato.com/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":516,"name":"Pexi B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://pexi.nl/privacy-policy/"},{"id":507,"name":"AdsWizz Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://www.adswizz.com/our-privacy-policy/"},{"id":482,"name":"UberMedia, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ubermedia.com/summary-of-privacy-policy/"},{"id":505,"name":"Shopalyst Inc","purposeIds":[1,2],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.shortlyst.com/eu/privacy_terms.html"},{"id":517,"name":"SunMedia ","purposeIds":[1,2],"legIntPurposeIds":[3],"featureIds":[2],"policyUrl":"https://www.sunmedia.tv/en/cookies"},{"id":518,"name":"Accelerize Inc.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[2,3],"policyUrl":"https://getcake.com/privacy-policy/","deletedDate":"2020-07-17T00:00:00Z"},{"id":511,"name":"Admixer EU GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://admixer.com/privacy/"},{"id":479,"name":"INFINIA MOBILE S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.infiniamobile.com/privacy_policy"},{"id":513,"name":"Shopstyle","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.shopstyle.co.uk/privacy","deletedDate":"2019-10-02T00:00:00Z"},{"id":509,"name":"ATG Ad Tech Group GmbH","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://ad-tech-group.com/privacy-policy/"},{"id":521,"name":"netzeffekt GmbH","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://www.netzeffekt.de/en/imprint"},{"id":487,"name":"nugg.ad GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1],"policyUrl":"https://www.nugg.ad/en/privacy/general-information.html","deletedDate":"2019-10-03T00:00:00Z"},{"id":515,"name":"ZighZag","purposeIds":[1,3],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://zighzag.com/privacy"},{"id":520,"name":"ChannelSight ","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.channelsight.com/privacypolicy/"},{"id":524,"name":"The Ozone Project Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://ozoneproject.com/privacy-policy"},{"id":529,"name":"Fidzup","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.fidzup.com/en/privacy/","deletedDate":"2019-11-18T00:00:00Z"},{"id":528,"name":"Kayzen","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://kayzen.io/data-privacy-policy"},{"id":527,"name":"Jampp LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://jampp.com/privacy.html"},{"id":506,"name":"salesforce.com, inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.salesforce.com/company/privacy/"},{"id":534,"name":"SmartyAds Inc.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://smartyads.com/privacy-policy"},{"id":535,"name":"INNITY","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.innity.com/privacy-policy.php"},{"id":514,"name":"Uprival LLC","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://uprival.com/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":522,"name":"Tealium Inc.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://tealium.com/privacy-policy/"},{"id":530,"name":"Near Pte Ltd","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://near.co/privacy"},{"id":539,"name":"AdDefend GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.addefend.com/en/privacy-policy/"},{"id":501,"name":"Alliance Gravity Data Media","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.alliancegravity.com/politiquedeprotectiondesdonneespersonnelles"},{"id":519,"name":"Chargeads","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.chargeplatform.com/privacy"},{"id":523,"name":"X-Mode Social, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://xmode.io/privacy-policy.html"},{"id":537,"name":"RUN, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.runads.com/privacy-policy"},{"id":531,"name":"Smartclip Hispania SL","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://rgpd-smartclip.com/"},{"id":536,"name":"GlobalWebIndex","purposeIds":[1],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"http://legal.trendstream.net/non-panellist_privacy_policy"},{"id":542,"name":"Densou Trading Desk ApS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://densou.dk/Policy.html","deletedDate":"2020-01-21T00:00:00Z"},{"id":525,"name":"PUB OCEAN LIMITED","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://rta.pubocean.com/privacy-policy/","deletedDate":"2019-10-03T00:00:00Z"},{"id":544,"name":"Kochava Inc.","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1,2],"policyUrl":"https://www.kochava.com/support-privacy/"},{"id":543,"name":"PaperG, Inc. dba Thunder Industries","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.makethunder.com/privacy"},{"id":334,"name":"Cydersoft","purposeIds":[],"legIntPurposeIds":[1,2,3,4],"featureIds":[2,3],"policyUrl":"http://www.videmob.com/privacy.html"},{"id":551,"name":"Illuma Technology Limited","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.weareilluma.com/endddd","deletedDate":"2019-11-14T00:00:00Z"},{"id":540,"name":"Tunnl BV","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://tunnl.com/privacy.html","deletedDate":"2019-12-20T00:00:00Z"},{"id":547,"name":"Video Reach","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.videoreach.de/about/privacy-policy/","deletedDate":"2020-01-21T00:00:00Z"},{"id":546,"name":"Smart Traffik","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://okube-attribution.com/politique-de-confidentialite/"},{"id":541,"name":"DeepIntent, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://www.deepintent.com/privacypolicy"},{"id":545,"name":"Reignn Platform Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://reignn.com/user-privacy-policy"},{"id":439,"name":"Bit Q Holdings Limited","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.rippll.com/privacy"},{"id":553,"name":"Adhese","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://adhese.com/privacy-and-cookie-policy"},{"id":556,"name":"adhood.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://v3.adhood.com/en/site/politikavekurallar/gizlilik.php?lang=en"},{"id":550,"name":"Happydemics","purposeIds":[5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.iubenda.com/privacy-policy/69056167/full-legal"},{"id":560,"name":"Leiki Ltd.","purposeIds":[1,2,3],"legIntPurposeIds":[4],"featureIds":[],"policyUrl":"http://www.leiki.com/privacy","deletedDate":"2020-01-07T00:00:00Z"},{"id":554,"name":"RMSi Radio Marketing Service interactive GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.rms.de/datenschutz/"},{"id":498,"name":"Mediakeys Platform","purposeIds":[1],"legIntPurposeIds":[3],"featureIds":[3],"policyUrl":"https://drbanner.com/privacypolicy_en/"},{"id":565,"name":"Adobe Audience Manager","purposeIds":[1,2,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adobe.com/privacy/policy.html"},{"id":118,"name":"Drawbridge, Inc.","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.drawbridge.com/privacy/","deletedDate":"2020-03-06T00:00:00Z"},{"id":572,"name":"CHEQ AI TECHNOLOGIES LTD.","purposeIds":[1],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"http://www.cheq.ai/privacy"},{"id":571,"name":"ViewPay","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://viewpay.tv/mentions-legales/"},{"id":568,"name":"Jointag S.r.l.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.jointag.com/privacy/kariboo/publisher/third/"},{"id":570,"name":"Czech Publisher Exchange z.s.p.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.cpex.cz/pro-uzivatele/ochrana-soukromi/"},{"id":559,"name":"Otto (GmbH & Co KG)","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2],"policyUrl":"https://www.otto.de/shoppages/service/datenschutz"},{"id":548,"name":"LBC France","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.leboncoin.fr/dc/cookies","deletedDate":"2020-04-23T00:00:00Z"},{"id":569,"name":"Kairos Fire","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.kairosfire.com/privacy"},{"id":577,"name":"Neustar on behalf of The Procter & Gamble Company","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.pg.com/privacy/english/privacy_statement.shtml"},{"id":590,"name":"Sourcepoint Technologies, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.sourcepoint.com/privacy-policy"},{"id":587,"name":"Localsensor B.V.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[],"policyUrl":"https://www.localsensor.com/privacy.html"},{"id":578,"name":"MAIRDUMONT NETLETIX GmbH&Co. KG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://mairdumont-netletix.com/datenschutz"},{"id":580,"name":"Goldbach Group AG","purposeIds":[],"legIntPurposeIds":[1,2,3,5],"featureIds":[1,2,3],"policyUrl":"https://goldbach.com/ch/de/datenschutz"},{"id":593,"name":"Programatica de publicidad S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://datmean.com/politica-privacidad/"},{"id":574,"name":"Realeyes OU","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://realview.realeyesit.com/privacy"},{"id":581,"name":"Mobilewalla, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[1,2,3],"policyUrl":"https://www.mobilewalla.com/business-services-privacy-policy"},{"id":598,"name":"audio content & control GmbH","purposeIds":[1],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://www.audio-cc.com/audiocc_privacy_policy.pdf"},{"id":596,"name":"InsurAds Technologies SA.","purposeIds":[3],"legIntPurposeIds":[5],"featureIds":[3],"policyUrl":"https://www.insurads.com/privacy.html"},{"id":576,"name":"StartApp Inc.","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[3],"policyUrl":"https://www.startapp.com/policy/privacy-policy/","deletedDate":"2020-04-23T00:00:00Z"},{"id":592,"name":"Colpirio.com","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy-policy.colpirio.com/en/","deletedDate":"2020-03-18T00:00:00Z"},{"id":549,"name":"Bandsintown Amplified LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://corp.bandsintown.com/privacy"},{"id":597,"name":"Better Banners A/S","purposeIds":[],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://betterbanners.com/en/privacy"},{"id":601,"name":"WebAds B.V","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://privacy.webads.eu/"},{"id":599,"name":"Maximus Live LLC","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[3],"policyUrl":"https://maximusx.com/privacy-policy/"},{"id":604,"name":"Join","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.teamjoin.fr/privacy.html","deletedDate":"2020-04-23T00:00:00Z"},{"id":606,"name":"Impactify ","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://impactify.io/privacy-policy/"},{"id":608,"name":"News and Media Holding, a.s.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.newsandmedia.sk/gdpr/"},{"id":602,"name":"Online Solution Int Limited","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2],"policyUrl":"https://adsafety.net/privacy.html"},{"id":591,"name":"Consumable, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://consumable.com/privacy-policy.html"},{"id":614,"name":"Market Resource Partners LLC","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.mrpfd.com/privacy-policy/"},{"id":615,"name":"Adsolutions BV","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.adsolutions.com/privacy-policy/"},{"id":607,"name":"ucfunnel Co., Ltd.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.ucfunnel.com/privacy-policy"},{"id":609,"name":"Predicio","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.predic.io/privacy"},{"id":617,"name":"Onfocus (Adagio)","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adagio.io/privacy"},{"id":620,"name":"Blue","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"http://www.getblue.io/privacy/"},{"id":610,"name":"Azerion Holding B.V.","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[2,3],"policyUrl":"https://azerion.com/business/privacy.html"},{"id":621,"name":"Seznam.cz, a.s.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://www.seznam.cz/ochranaudaju"},{"id":624,"name":"Norstat AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.norstatpanel.com/en/data-protection"},{"id":623,"name":"Adprime Media Inc. ","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adprimehealth.com/privacy/","deletedDate":"2020-06-17T00:00:00Z"},{"id":95,"name":"Lotame Solutions, inc","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[2],"policyUrl":"https://www.lotame.com/about-lotame/privacy/lotame-corporate-websites-privacy-policy/"},{"id":618,"name":"BEINTOO SPA","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.beintoo.com/privacy-cookie-policy/"},{"id":619,"name":"Capitaldata","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.capitaldata.fr/privacy"},{"id":625,"name":"BILENDI SA","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.maximiles.com/privacy-policy"},{"id":628,"name":": Tappx","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.tappx.com/en/privacy-policy/"},{"id":626,"name":"Hivestack Inc.","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[3],"policyUrl":"https://hivestack.com/privacy-policy"},{"id":631,"name":"Relay42 Netherlands B.V.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://relay42.com/privacy"},{"id":627,"name":"D-Edge","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.d-edge.com/privacy-policy/","deletedDate":"2020-07-06T00:00:00Z"},{"id":644,"name":"Gamoshi LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.gamoshi.com/privacy-policy"},{"id":639,"name":"Smile Wanted Group","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.smilewanted.com/privacy.php"},{"id":635,"name":"WebMediaRM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.webmediarm.com/vie_privee_et_opposition_en.php"},{"id":579,"name":"Ve Global","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.ve.com/privacy-policy"},{"id":645,"name":"Noster Finance S.L.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.finect.com/terminos-legales/politica-de-cookies"},{"id":653,"name":"Smartme Analytics","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"http://smartmeapp.com/info/smartme/aviso_legal.php","deletedDate":"2020-07-03T00:00:00Z"},{"id":613,"name":"Adserve.zone / Artworx AS","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://adserve.zone/adserveprivacypolicy.html"},{"id":573,"name":"Dailymotion SA","purposeIds":[2,3,4,5],"legIntPurposeIds":[1],"featureIds":[2],"policyUrl":"https://www.dailymotion.com/legal/privacy"},{"id":652,"name":"Skaze","purposeIds":[1,2],"legIntPurposeIds":[3,4,5],"featureIds":[1,2,3],"policyUrl":"http://www.skaze.fr/rgpd/"},{"id":646,"name":"Notify","purposeIds":[1,2],"legIntPurposeIds":[5],"featureIds":[1],"policyUrl":"https://notify-group.com/en/mentions-legales/"},{"id":648,"name":"TrueData Solutions, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.truedata.co/privacy-policy/"},{"id":647,"name":"Axel Springer Teaser Ad GmbH","purposeIds":[2],"legIntPurposeIds":[1,3,5],"featureIds":[],"policyUrl":"https://www.adup-tech.com/privacy"},{"id":654,"name":"GRAPHINIUM","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.graphinium.com/privacy/"},{"id":659,"name":"Research and Analysis of Media in Sweden AB","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www2.rampanel.com/privacy-policy/"},{"id":656,"name":"Think Clever Media","purposeIds":[1,2,3,4],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.contentignite.com/privacy-policy/"},{"id":504,"name":"Alive & Kicking Global Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.mcsaatchiplc.com/legal/privacy-cookies","deletedDate":"2020-07-27T00:00:00Z"},{"id":657,"name":"GP One GmbH","purposeIds":[],"legIntPurposeIds":[1,3,5],"featureIds":[3],"policyUrl":"https://www.gsi-one.org/de/privacy-policy.html"},{"id":655,"name":"Sportradar AG","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.sportradar.com/about-us/privacy/"},{"id":662,"name":"SoundCast","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://soundcast.fm/en/data-privacy"},{"id":665,"name":"Digital East GmbH","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.digitaleast.mobi/en/legal/privacy-policy/"},{"id":650,"name":"Telefonica Investigaci\u00f3n y Desarrollo S.A.U","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://www.cognitivemarketing.tid.es/"},{"id":666,"name":"BeOp","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://beop.io/privacy"},{"id":663,"name":"Mobsuccess","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.mobsuccess.com/en/privacy"},{"id":658,"name":"BLIINK SAS","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://bliink.io/privacy-policy"},{"id":667,"name":"Liftoff Mobile, Inc.","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[3],"policyUrl":"https://liftoff.io/privacy-policy/"},{"id":668,"name":"WhatRocks Inc. ","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.whatrocks.co/en/privacy-policy "},{"id":670,"name":"Timehop, Inc.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.timehop.com/privacy"},{"id":674,"name":"Duration Media, LLC.","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.durationmedia.net/privacy-policy"},{"id":675,"name":"Instreamatic inc.","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://instreamatic.com/privacy-policy/"},{"id":676,"name":"BusinessClick","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.businessclick.com/documents/RegulaminProgramuBusinessClick-2019.pdf"},{"id":677,"name":"Intercept Interactive Inc. dba Undertone","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.undertone.com/privacy/"},{"id":660,"name":"Schibsted Norge AS","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[2,3],"policyUrl":"https://static.vg.no/privacy/","deletedDate":"2019-09-16T00:00:00Z"},{"id":673,"name":"TTNET AS","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"http://www.programattik.com/en/privacy-policy.aspx"},{"id":664,"name":"adMarketplace, Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"https://www.admarketplace.com/privacy-policy/"},{"id":671,"name":"Mediaforce LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,3],"policyUrl":"http://casino.mindthebet.co.uk/themes/mindthebetv2-casino/privacy.php"},{"id":561,"name":"AuDigent","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://audigent.com/platform-privacy-policy"},{"id":682,"name":"Radio Net Media Limited","purposeIds":[1,2,3],"legIntPurposeIds":[5],"featureIds":[1,2,3],"policyUrl":"https://www.adtonos.com/service-privacy-policy/"},{"id":684,"name":"Blue Billywig BV","purposeIds":[],"legIntPurposeIds":[5],"featureIds":[],"policyUrl":"https://www.bluebillywig.com/privacy-statement/"},{"id":686,"name":"The MediaGrid Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://www.themediagrid.com/privacy-policy/"},{"id":685,"name":"Arkeero","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://arkeero.com/privacy-2/"},{"id":687,"name":"MISSENA","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://missena.com/confidentialite/"},{"id":690,"name":"Go.pl sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://go.pl/polityka-prywatnosci/"},{"id":691,"name":"Lifesight Pte. Ltd.","purposeIds":[1,2,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.lifesight.io/privacy-policy/"},{"id":697,"name":"ADWAYS SAS","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://www.adways.com/confidentialite/?lang=en"},{"id":681,"name":"MyTraffic","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://www.mytraffic.io/en/privacy"},{"id":649,"name":"adality GmbH","purposeIds":[],"legIntPurposeIds":[1,2],"featureIds":[1],"policyUrl":"https://adality.de/en/privacy/"},{"id":712,"name":"Inspired Mobile Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://byinspired.com/privacypolicy.pdf"},{"id":688,"name":"Effinity","purposeIds":[],"legIntPurposeIds":[1],"featureIds":[],"policyUrl":"https://www.effiliation.com/politique-de-confidentialite/"},{"id":702,"name":"Kwanko","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.kwanko.com/fr/rgpd/"},{"id":715,"name":"BidBerry SRL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.bidberrymedia.com/privacy-policy/"},{"id":713,"name":"Dataseat Ltd","purposeIds":[2,5],"legIntPurposeIds":[1,3,4],"featureIds":[],"policyUrl":"https://dataseat.com/privacy-policy"},{"id":716,"name":"OnAudience Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.onaudience.com/internet-advertising-privacy-policy"},{"id":708,"name":"Dugout Limited ","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://dugout.com/privacy-policy"},{"id":717,"name":"Audience Network","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.en.audiencenetwork.pl/internet-advertising-privacy-policy"},{"id":718,"name":"AppConsent Xchange","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://appconsent.io/en/privacy-policy"},{"id":720,"name":"AAX LLC","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[3],"policyUrl":"https://aax.media/privacy/"},{"id":678,"name":"Axonix LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://axonix.com/privacy-cookie-policy/"},{"id":719,"name":"Online Advertising Network Sp. z o.o.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://www.oan.pl/en/privacy-policy"},{"id":707,"name":"Dentsu Aegis Network Italia SpA","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.dentsuaegisnetwork.com/it/it/policies/info-cookie"},{"id":721,"name":"Beaconspark Ltd","purposeIds":[1,2,3],"legIntPurposeIds":[4,5],"featureIds":[1],"policyUrl":"https://www.engageya.com/privacy"},{"id":724,"name":"Between Exchange","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[2,3],"policyUrl":"https://en.betweenx.com/pdata.pdf"},{"id":728,"name":"Appier PTE Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.appier.com/privacy-policy/"},{"id":729,"name":"Cavai AS & UK ","purposeIds":[],"legIntPurposeIds":[3],"featureIds":[],"policyUrl":"https://cav.ai/privacy-policy/"},{"id":723,"name":"Adzymic Pte Ltd","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"http://www.adzymic.co/privacy"},{"id":737,"name":"Monet Engine Inc","purposeIds":[1,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://appmonet.com/privacy-policy/"},{"id":740,"name":"6Sense Insights, Inc.","purposeIds":[1],"legIntPurposeIds":[2,3,4,5],"featureIds":[1,2],"policyUrl":"https://6sense.com/privacy-policy/"},{"id":744,"name":"Vidazoo Ltd","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[2],"policyUrl":"https://vidazoo.gitbook.io/vidazoo-legal/privacy-policy"},{"id":731,"name":"GeistM Technologies LTD","purposeIds":[],"legIntPurposeIds":[3,4,5],"featureIds":[],"policyUrl":"https://www.geistm.com/privacy"},{"id":741,"name":"Brand Advance Limited","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.wearebrandadvance.com/website-privacy-policy"},{"id":734,"name":"Cint AB","purposeIds":[1,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://www.cint.com/participant-privacy-notice"},{"id":709,"name":"NC Audience Exchange, LLC (NewsIQ)","purposeIds":[1,2],"legIntPurposeIds":[3,5],"featureIds":[1,2],"policyUrl":"https://www.ncaudienceexchange.com/privacy/"},{"id":739,"name":"Blingby LLC","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://blingby.com/privacy"},{"id":732,"name":"Performax.cz, s.r.o.","purposeIds":[2,4,5],"legIntPurposeIds":[1,3],"featureIds":[2,3],"policyUrl":"https://reg.tiscali.cz/privacy-policy"},{"id":736,"name":"BidMachine Inc.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://explorestack.com/privacy-policy/"},{"id":738,"name":"adbility media GmbH","purposeIds":[2,3],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.adbility-media.com/datenschutzerklaerung/"},{"id":742,"name":"Audiencerate LTD","purposeIds":[],"legIntPurposeIds":[1,2,5],"featureIds":[],"policyUrl":"https://www.audiencerate.com/privacy/"},{"id":743,"name":"MOVIads Sp. z o.o. Sp. k.","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://moviads.pl/polityka-prywatnosci/"},{"id":746,"name":"Adxperience SAS","purposeIds":[2],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://adxperience.com/privacy-policy/"},{"id":747,"name":"Kairion GmbH","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://kairion.de/datenschutzbestimmungen/"},{"id":748,"name":"AUDIOMOB LTD","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[2,3],"policyUrl":"https://www.audiomob.io/privacy"},{"id":749,"name":"Good-Loop Ltd","purposeIds":[2],"legIntPurposeIds":[1,3,4,5],"featureIds":[],"policyUrl":"https://doc.good-loop.com/policy/privacy-policy.html"},{"id":754,"name":"DistroScale, Inc.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"http://www.distroscale.com/privacy-policy/"},{"id":756,"name":"Fandom, Inc.","purposeIds":[3],"legIntPurposeIds":[1,2,4,5],"featureIds":[],"policyUrl":"https://www.fandom.com/privacy-policy"},{"id":758,"name":"GfK Netherlands B.V.","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://gfkpanel.nl/privacy"},{"id":759,"name":"RevJet","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.revjet.com/privacy"},{"id":760,"name":"VEXPRO TECHNOLOGIES LTD","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2],"policyUrl":"https://onedash.com/privacy-policy.html"},{"id":761,"name":"Digiseg ApS","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[1],"policyUrl":"https://digiseg.io/privacy-center/"},{"id":763,"name":"Delidatax SL","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"https://www.delidatax.net/privacy.htm"},{"id":764,"name":"Lucidity","purposeIds":[1,3,4,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://golucidity.com/privacy-policy/"},{"id":765,"name":"Grabit Interactive Media Inc dba KERV Interctive","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[2],"policyUrl":"https://kervit.com/privacy-policy/"},{"id":766,"name":"ADCELL | Firstlead GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.adcell.de/agb#sector_6"},{"id":768,"name":"Global Media & Entertainment Limited","purposeIds":[1,2,3,4,5],"legIntPurposeIds":[],"featureIds":[1,2,3],"policyUrl":"http://global.com/privacy-policy/"},{"id":770,"name":"MARKETPERF CORP","purposeIds":[1,2,4],"legIntPurposeIds":[3,5],"featureIds":[2,3],"policyUrl":"https://www.marketperf.com/assets/images/app/marketperf/pdf/privacy-policy.pdf"},{"id":773,"name":"360e-com Sp. z o.o.","purposeIds":[1,2,3,5],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.clickonometrics.com/optout/"},{"id":775,"name":"SelectMedia International LTD","purposeIds":[1],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.selectmedia.asia/terms-and-privacy/"},{"id":778,"name":"Discover-Tech ltd","purposeIds":[2,5],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://discover-tech.io/dsp-privacy-policy/"},{"id":779,"name":"Adtarget Medya A.S.","purposeIds":[1,3],"legIntPurposeIds":[],"featureIds":[3],"policyUrl":"https://adtarget.com.tr/adtarget-privacy-policy-2020.pdf"},{"id":780,"name":"Aniview LTD","purposeIds":[1,2,3],"legIntPurposeIds":[],"featureIds":[],"policyUrl":"https://www.aniview.com/privacy-policy/"},{"id":781,"name":"FeedAd GmbH","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[2,3],"policyUrl":"https://feedad.com/privacy/"},{"id":784,"name":"Nubo LTD","purposeIds":[],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://www.recod3.com/privacypolicy.php"},{"id":786,"name":"TargetVideo GmbH","purposeIds":[],"legIntPurposeIds":[1,2,3,4,5],"featureIds":[],"policyUrl":"https://www.target-video.com/datenschutz/"},{"id":798,"name":"Adverticum cPlc.","purposeIds":[2,3,4],"legIntPurposeIds":[1,5],"featureIds":[],"policyUrl":"https://adverticum.net/english/privacy-and-data-processing-information/"},{"id":803,"name":"Click Tech Limited","purposeIds":[1],"legIntPurposeIds":[2,3],"featureIds":[1],"policyUrl":"https://en.yeahmobi.com/html/privacypolicy/"},{"id":808,"name":"Pure Local Media GmbH","purposeIds":[],"legIntPurposeIds":[3,5],"featureIds":[],"policyUrl":"https://purelocalmedia.de/?page_id=593"}]} \ 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 33009e2dc73..c3b71a3be67 100644 --- a/stored_requests/backends/db_fetcher/fetcher.go +++ b/stored_requests/backends/db_fetcher/fetcher.go @@ -93,6 +93,10 @@ func (fetcher *dbFetcher) FetchRequests(ctx context.Context, requestIDs []string return storedRequestData, storedImpData, errs } +func (fetcher *dbFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} +} + func (fetcher *dbFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } diff --git a/stored_requests/backends/empty_fetcher/fetcher.go b/stored_requests/backends/empty_fetcher/fetcher.go index 48ef468abca..6edf3cc4d00 100644 --- a/stored_requests/backends/empty_fetcher/fetcher.go +++ b/stored_requests/backends/empty_fetcher/fetcher.go @@ -3,6 +3,7 @@ package empty_fetcher import ( "context" "encoding/json" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" ) @@ -27,6 +28,10 @@ func (fetcher EmptyFetcher) FetchRequests(ctx context.Context, requestIDs []stri return } +func (fetcher EmptyFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} +} + func (fetcher EmptyFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } diff --git a/stored_requests/backends/file_fetcher/fetcher.go b/stored_requests/backends/file_fetcher/fetcher.go index 6d7cb9caf77..bff94b21e79 100644 --- a/stored_requests/backends/file_fetcher/fetcher.go +++ b/stored_requests/backends/file_fetcher/fetcher.go @@ -33,6 +33,21 @@ func (fetcher *eagerFetcher) FetchRequests(ctx context.Context, requestIDs []str return storedRequests, storedImpressions, errs } +// FetchAccount fetches the host account configuration for a publisher +func (fetcher *eagerFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + if len(accountID) == 0 { + return nil, []error{fmt.Errorf("Cannot look up an empty accountID")} + } + accountJSON, ok := fetcher.FileSystem.Directories["accounts"].Files[accountID] + if !ok { + return nil, []error{stored_requests.NotFoundError{ + ID: accountID, + DataType: "Account", + }} + } + return accountJSON, nil +} + func (fetcher *eagerFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { fileName := primaryAdServer diff --git a/stored_requests/backends/file_fetcher/fetcher_test.go b/stored_requests/backends/file_fetcher/fetcher_test.go index 76f5e494a64..f0900002c8c 100644 --- a/stored_requests/backends/file_fetcher/fetcher_test.go +++ b/stored_requests/backends/file_fetcher/fetcher_test.go @@ -24,6 +24,20 @@ func TestFileFetcher(t *testing.T) { validateImp(t, storedImps) } +func TestAccountFetcher(t *testing.T) { + fetcher, err := NewFileFetcher("./test") + assert.NoError(t, err, "Failed to create test fetcher") + + account, errs := fetcher.FetchAccount(context.Background(), "valid") + assertErrorCount(t, 0, errs) + assert.JSONEq(t, `{"disabled":false, "id":"valid"}`, string(account)) + + account, errs = fetcher.FetchAccount(context.Background(), "nonexistent") + assertErrorCount(t, 1, errs) + assert.Error(t, errs[0]) + assert.Equal(t, stored_requests.NotFoundError{"nonexistent", "Account"}, errs[0]) +} + func TestInvalidDirectory(t *testing.T) { _, err := NewFileFetcher("./nonexistant-directory") if err == nil { diff --git a/stored_requests/backends/file_fetcher/test/accounts/valid.json b/stored_requests/backends/file_fetcher/test/accounts/valid.json new file mode 100644 index 00000000000..2c8bd12af3c --- /dev/null +++ b/stored_requests/backends/file_fetcher/test/accounts/valid.json @@ -0,0 +1,4 @@ +{ + "id": "valid", + "disabled": false +} diff --git a/stored_requests/backends/http_fetcher/fetcher.go b/stored_requests/backends/http_fetcher/fetcher.go index efd85a001e0..5a7d8fa2878 100644 --- a/stored_requests/backends/http_fetcher/fetcher.go +++ b/stored_requests/backends/http_fetcher/fetcher.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "strings" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" @@ -19,9 +20,13 @@ import ( // // This file expects the endpoint to satisfy the following API: // +// Stored requests // GET {endpoint}?request-ids=["req1","req2"]&imp-ids=["imp1","imp2","imp3"] // -// This endpoint should return a payload like: +// Accounts +// GET {endpoint}?account-ids=["acc1","acc2"] +// +// The above endpoints should return a payload like: // // { // "requests": { @@ -34,12 +39,25 @@ import ( // "imp3": null // If imp3 is not found // } // } +// or +// { +// "accounts": { +// "acc1": { ... config data for acc1 ... }, +// "acc2": { ... config data for acc2 ... }, +// }, +// } // // func NewFetcher(client *http.Client, endpoint string) *HttpFetcher { // Do some work up-front to figure out if the (configurable) endpoint has a query string or not. // When we build requests, we'll either want to add `?request-ids=...&imp-ids=...` _or_ - // `&request-ids=...&imp-ids=...`, depending. + // `&request-ids=...&imp-ids=...`. + + if _, err := url.Parse(endpoint); err != nil { + glog.Fatalf(`Invalid endpoint "%s": %v`, endpoint, err) + } + glog.Infof("Making http_fetcher for endpoint %v", endpoint) + urlPrefix := endpoint if strings.Contains(endpoint, "?") { urlPrefix = urlPrefix + "&" @@ -47,8 +65,6 @@ func NewFetcher(client *http.Client, endpoint string) *HttpFetcher { urlPrefix = urlPrefix + "?" } - glog.Info("Making http_fetcher which calls GET " + urlPrefix + "request-ids=%REQUEST_ID_LIST%&imp-ids=%IMP_ID_LIST%") - return &HttpFetcher{ client: client, Endpoint: urlPrefix, @@ -81,6 +97,70 @@ func (fetcher *HttpFetcher) FetchRequests(ctx context.Context, requestIDs []stri return } +// FetchAccounts retrieves account configurations +// +// Request format is similar to the one for requests: +// GET {endpoint}?account-ids=["account1","account2",...] +// +// The endpoint is expected to respond with a JSON map with accountID -> json.RawMessage +// { +// "account1": { ... account json ... } +// } +// The JSON contents of account config is returned as-is (NOT validated) +func (fetcher *HttpFetcher) FetchAccounts(ctx context.Context, accountIDs []string) (map[string]json.RawMessage, []error) { + if len(accountIDs) == 0 { + return nil, nil + } + httpReq, err := http.NewRequestWithContext(ctx, "GET", fetcher.Endpoint+"account-ids=[\""+strings.Join(accountIDs, "\",\"")+"\"]", nil) + if err != nil { + return nil, []error{ + fmt.Errorf(`Error fetching accounts %v via http: build request failed with %v`, accountIDs, err), + } + } + httpResp, err := ctxhttp.Do(ctx, fetcher.client, httpReq) + if err != nil { + return nil, []error{ + fmt.Errorf(`Error fetching accounts %v via http: %v`, accountIDs, err), + } + } + defer httpResp.Body.Close() + respBytes, err := ioutil.ReadAll(httpResp.Body) + if err != nil { + return nil, []error{ + fmt.Errorf(`Error fetching accounts %v via http: error reading response: %v`, accountIDs, err), + } + } + if httpResp.StatusCode != http.StatusOK { + return nil, []error{ + fmt.Errorf(`Error fetching accounts %v via http: unexpected response status %d`, accountIDs, httpResp.StatusCode), + } + } + var responseData accountsResponseContract + if err = json.Unmarshal(respBytes, &responseData); err != nil { + return nil, []error{ + fmt.Errorf(`Error fetching accounts %v via http: failed to parse response: %v`, accountIDs, err), + } + } + errs := convertNullsToErrs(responseData.Accounts, "Account", []error{}) + return responseData.Accounts, errs +} + +// FetchAccount fetchers a single accountID and returns its corresponding json +func (fetcher *HttpFetcher) FetchAccount(ctx context.Context, accountID string) (accountJSON json.RawMessage, errs []error) { + accountData, errs := fetcher.FetchAccounts(ctx, []string{accountID}) + if len(errs) > 0 { + return nil, errs + } + accountJSON, ok := accountData[accountID] + if !ok { + return nil, []error{stored_requests.NotFoundError{ + ID: accountID, + DataType: "Account", + }} + } + return accountJSON, nil +} + func (fetcher *HttpFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { if fetcher.Categories == nil { fetcher.Categories = make(map[string]map[string]stored_requests.Category) @@ -186,3 +266,7 @@ type responseContract struct { Requests map[string]json.RawMessage `json:"requests"` Imps map[string]json.RawMessage `json:"imps"` } + +type accountsResponseContract struct { + Accounts map[string]json.RawMessage `json:"accounts"` +} diff --git a/stored_requests/backends/http_fetcher/fetcher_test.go b/stored_requests/backends/http_fetcher/fetcher_test.go index dc4076fd4d9..30933181e1d 100644 --- a/stored_requests/backends/http_fetcher/fetcher_test.go +++ b/stored_requests/backends/http_fetcher/fetcher_test.go @@ -9,6 +9,9 @@ import ( "net/http/httptest" "strings" "testing" + "time" + + "github.com/stretchr/testify/assert" ) func TestSingleReq(t *testing.T) { @@ -16,9 +19,9 @@ func TestSingleReq(t *testing.T) { defer close() reqData, impData, errs := fetcher.FetchRequests(context.Background(), []string{"req-1"}, nil) + assert.Empty(t, errs, "Unexpected errors fetching known requests") assertMapKeys(t, reqData, "req-1") - assertMapKeys(t, impData) - assertErrLength(t, errs, 0) + assert.Empty(t, impData, "Unexpected imps returned fetching just requests") } func TestSeveralReqs(t *testing.T) { @@ -26,9 +29,9 @@ func TestSeveralReqs(t *testing.T) { defer close() reqData, impData, errs := fetcher.FetchRequests(context.Background(), []string{"req-1", "req-2"}, nil) + assert.Empty(t, errs, "Unexpected errors fetching known requests") assertMapKeys(t, reqData, "req-1", "req-2") - assertMapKeys(t, impData) - assertErrLength(t, errs, 0) + assert.Empty(t, impData, "Unexpected imps returned fetching just requests") } func TestSingleImp(t *testing.T) { @@ -36,9 +39,9 @@ func TestSingleImp(t *testing.T) { defer close() reqData, impData, errs := fetcher.FetchRequests(context.Background(), nil, []string{"imp-1"}) - assertMapKeys(t, reqData) + assert.Empty(t, errs, "Unexpected errors fetching known imps") + assert.Empty(t, reqData, "Unexpected requests returned fetching just imps") assertMapKeys(t, impData, "imp-1") - assertErrLength(t, errs, 0) } func TestSeveralImps(t *testing.T) { @@ -46,9 +49,9 @@ func TestSeveralImps(t *testing.T) { defer close() reqData, impData, errs := fetcher.FetchRequests(context.Background(), nil, []string{"imp-1", "imp-2"}) - assertMapKeys(t, reqData) + assert.Empty(t, errs, "Unexpected errors fetching known imps") + assert.Empty(t, reqData, "Unexpected requests returned fetching just imps") assertMapKeys(t, impData, "imp-1", "imp-2") - assertErrLength(t, errs, 0) } func TestReqsAndImps(t *testing.T) { @@ -56,9 +59,9 @@ func TestReqsAndImps(t *testing.T) { defer close() reqData, impData, errs := fetcher.FetchRequests(context.Background(), []string{"req-1"}, []string{"imp-1"}) + assert.Empty(t, errs, "Unexpected errors fetching known reqs and imps") assertMapKeys(t, reqData, "req-1") assertMapKeys(t, impData, "imp-1") - assertErrLength(t, errs, 0) } func TestMissingValues(t *testing.T) { @@ -66,9 +69,94 @@ func TestMissingValues(t *testing.T) { defer close() reqData, impData, errs := fetcher.FetchRequests(context.Background(), []string{"req-1", "req-2"}, []string{"imp-1"}) - assertMapKeys(t, reqData) - assertMapKeys(t, impData) - assertErrLength(t, errs, 3) + assert.Empty(t, reqData, "Fetching unknown reqs should return no reqs") + assert.Empty(t, impData, "Fetching unknown imps should return no imps") + assert.Len(t, errs, 3, "Fetching 3 unknown reqs+imps should return 3 errors") +} + +func TestFetchAccounts(t *testing.T) { + fetcher, close := newTestAccountFetcher(t, []string{"acc-1", "acc-2"}) + defer close() + + accData, errs := fetcher.FetchAccounts(context.Background(), []string{"acc-1", "acc-2"}) + assert.Empty(t, errs, "Unexpected error fetching known accounts") + assertMapKeys(t, accData, "acc-1", "acc-2") +} + +func TestFetchAccountsNoData(t *testing.T) { + fetcher, close := newFetcherBrokenBackend() + defer close() + + accData, errs := fetcher.FetchAccounts(context.Background(), []string{"req-1"}) + assert.Len(t, errs, 1, "Fetching unknown account should have returned an error") + assert.Nil(t, accData, "Fetching unknown account should return nil account map") +} + +func TestFetchAccountsBadJSON(t *testing.T) { + fetcher, close := newFetcherBadJSON() + defer close() + + accData, errs := fetcher.FetchAccounts(context.Background(), []string{"req-1"}) + assert.Len(t, errs, 1, "Fetching account with broken json should have returned an error") + assert.Nil(t, accData, "Fetching account with broken json should return nil account map") +} + +func TestFetchAccountsNoIDsProvided(t *testing.T) { + fetcher, close := newTestAccountFetcher(t, []string{"acc-1", "acc-2"}) + defer close() + + accData, errs := fetcher.FetchAccounts(nil, []string{}) + assert.Empty(t, errs, "Unexpected error fetching empty account list") + assert.Nil(t, accData, "Fetching empty account list should return nil") +} + +// Force build request failure by not providing a context +func TestFetchAccountsFailedBuildRequest(t *testing.T) { + fetcher, close := newTestAccountFetcher(t, []string{"acc-1", "acc-2"}) + defer close() + + accData, errs := fetcher.FetchAccounts(nil, []string{"acc-1"}) + assert.Len(t, errs, 1, "Fetching accounts without context should result in error ") + assert.Nil(t, accData, "Fetching accounts without context should return nil") +} + +// Force http error via request timeout +func TestFetchAccountsContextTimeout(t *testing.T) { + fetcher, close := newTestAccountFetcher(t, []string{"acc-1", "acc-2"}) + defer close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(0)) + defer cancel() + accData, errs := fetcher.FetchAccounts(ctx, []string{"acc-1"}) + assert.Len(t, errs, 1, "Expected context timeout error") + assert.Nil(t, accData, "Unexpected account data returned instead of timeout") +} + +func TestFetchAccount(t *testing.T) { + fetcher, close := newTestAccountFetcher(t, []string{"acc-1"}) + defer close() + + account, errs := fetcher.FetchAccount(context.Background(), "acc-1") + assert.Empty(t, errs, "Unexpected error fetching existing account") + assert.JSONEq(t, `"acc-1"`, string(account), "Unexpected account data fetching existing account") +} + +func TestFetchAccountNoData(t *testing.T) { + fetcher, close := newFetcherBrokenBackend() + defer close() + + unknownAccount, errs := fetcher.FetchAccount(context.Background(), "unknown-acc") + assert.NotEmpty(t, errs, "Retrieving unknown account should return error") + assert.Nil(t, unknownAccount, "Retrieving unknown account should return nil json.RawMessage") +} + +func TestFetchAccountNoIDProvided(t *testing.T) { + fetcher, close := newTestAccountFetcher(t, nil) + defer close() + + account, errs := fetcher.FetchAccount(context.Background(), "") + assert.Len(t, errs, 1, "Fetching account with empty id should error") + assert.Nil(t, account, "Fetching account with empty id should return nil") } func TestErrResponse(t *testing.T) { @@ -77,7 +165,7 @@ func TestErrResponse(t *testing.T) { reqData, impData, errs := fetcher.FetchRequests(context.Background(), []string{"req-1"}, []string{"imp-1"}) assertMapKeys(t, reqData) assertMapKeys(t, impData) - assertErrLength(t, errs, 1) + assert.Len(t, errs, 1) } func assertSameContents(t *testing.T, expected map[string]json.RawMessage, actual map[string]json.RawMessage) { @@ -124,6 +212,14 @@ func newFetcherBrokenBackend() (fetcher *HttpFetcher, closer func()) { return NewFetcher(server.Client(), server.URL), server.Close } +func newFetcherBadJSON() (fetcher *HttpFetcher, closer func()) { + handler := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`broken JSON`)) + } + server := httptest.NewServer(http.HandlerFunc(handler)) + return NewFetcher(server.Client(), server.URL), server.Close +} + func newEmptyFetcher(t *testing.T, expectReqIDs []string, expectImpIDs []string) (fetcher *HttpFetcher, closer func()) { handler := newHandler(t, expectReqIDs, expectImpIDs, jsonifyToNull) server := httptest.NewServer(http.HandlerFunc(handler)) @@ -139,12 +235,12 @@ func newTestFetcher(t *testing.T, expectReqIDs []string, expectImpIDs []string) func newHandler(t *testing.T, expectReqIDs []string, expectImpIDs []string, jsonifier func(string) json.RawMessage) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() - assertMatches(t, query.Get("request-ids"), expectReqIDs) - assertMatches(t, query.Get("imp-ids"), expectImpIDs) - gotReqIDs := richSplit(query.Get("request-ids")) gotImpIDs := richSplit(query.Get("imp-ids")) + assertMatches(t, gotReqIDs, expectReqIDs) + assertMatches(t, gotImpIDs, expectImpIDs) + reqIDResponse := make(map[string]json.RawMessage, len(gotReqIDs)) impIDResponse := make(map[string]json.RawMessage, len(gotImpIDs)) @@ -174,10 +270,43 @@ func newHandler(t *testing.T, expectReqIDs []string, expectImpIDs []string, json } } -func assertMatches(t *testing.T, query string, expected []string) { +func newTestAccountFetcher(t *testing.T, expectAccIDs []string) (fetcher *HttpFetcher, closer func()) { + handler := newAccountHandler(t, expectAccIDs, jsonifyID) + server := httptest.NewServer(http.HandlerFunc(handler)) + return NewFetcher(server.Client(), server.URL), server.Close +} + +func newAccountHandler(t *testing.T, expectAccIDs []string, jsonifier func(string) json.RawMessage) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + gotAccIDs := richSplit(query.Get("account-ids")) + + assertMatches(t, gotAccIDs, expectAccIDs) + + accIDResponse := make(map[string]json.RawMessage, len(gotAccIDs)) + + for _, accID := range gotAccIDs { + if accID != "" { + accIDResponse[accID] = jsonifier(accID) + } + } + + respObj := accountsResponseContract{ + Accounts: accIDResponse, + } + + if respBytes, err := json.Marshal(respObj); err != nil { + t.Errorf("failed to marshal responseContract in test: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } else { + w.Write(respBytes) + } + } +} + +func assertMatches(t *testing.T, queryVals []string, expected []string) { t.Helper() - queryVals := richSplit(query) if len(queryVals) == 1 && queryVals[0] == "" { if len(expected) != 0 { t.Errorf("Expected no query vals, but got %v", queryVals) @@ -250,11 +379,3 @@ func assertMapKeys(t *testing.T, m map[string]json.RawMessage, keys ...string) { } } } - -func assertErrLength(t *testing.T, errs []error, expected int) { - t.Helper() - - if len(errs) != expected { - t.Errorf("Expected %d errors. Got: %v", expected, errs) - } -} diff --git a/stored_requests/caches/cachestest/reliable.go b/stored_requests/caches/cachestest/reliable.go index 59e6683f8b0..a0ab07df431 100644 --- a/stored_requests/caches/cachestest/reliable.go +++ b/stored_requests/caches/cachestest/reliable.go @@ -11,8 +11,6 @@ import ( const ( reqCacheKey = "known-req" reqCacheVal = `{"req":true}` - impCacheKey = "known-imp" - impCacheVal = `{"imp":true}` ) // AssertCacheRobustness runs tests which can be used to validate any Cache that is 100% reliable. @@ -20,84 +18,41 @@ const ( // // The cacheSupplier should be a function which returns a new Cache (with no data inside) on every call. // This will be called from separate Goroutines to make sure that different tests don't conflict. -func AssertCacheRobustness(t *testing.T, cacheSupplier func() stored_requests.Cache) { +func AssertCacheRobustness(t *testing.T, cacheSupplier func() stored_requests.CacheJSON) { t.Run("TestCacheMiss", cacheMissTester(cacheSupplier())) t.Run("TestCacheHit", cacheHitTester(cacheSupplier())) - t.Run("TestCacheMixed", cacheMixedTester(cacheSupplier())) - t.Run("TestCacheOverlap", cacheOverlapTester(cacheSupplier())) t.Run("TestCacheSaveInvalidate", cacheSaveInvalidateTester(cacheSupplier())) } -func cacheMissTester(cache stored_requests.Cache) func(*testing.T) { +func cacheMissTester(cache stored_requests.CacheJSON) func(*testing.T) { return func(t *testing.T) { - storedReqs, storedImps := cache.Get(context.Background(), []string{"unknown"}, nil) - assertMapLength(t, 0, storedReqs) - assertMapLength(t, 0, storedImps) + storedData := cache.Get(context.Background(), []string{"unknown"}) + assertMapLength(t, 0, storedData) } } -func cacheHitTester(cache stored_requests.Cache) func(*testing.T) { +func cacheHitTester(cache stored_requests.CacheJSON) func(*testing.T) { return func(t *testing.T) { cache.Save(context.Background(), map[string]json.RawMessage{ reqCacheKey: json.RawMessage(reqCacheVal), - }, map[string]json.RawMessage{ - impCacheKey: json.RawMessage(impCacheVal), }) - reqData, impData := cache.Get(context.Background(), []string{reqCacheKey}, []string{impCacheKey}) - if len(reqData) != 1 { - t.Errorf("The cache should have returned the data.") - } + reqData := cache.Get(context.Background(), []string{reqCacheKey}) assertMapLength(t, 1, reqData) assertHasValue(t, reqData, reqCacheKey, reqCacheVal) - - assertMapLength(t, 1, impData) - assertHasValue(t, impData, impCacheKey, impCacheVal) - } -} - -func cacheMixedTester(cache stored_requests.Cache) func(*testing.T) { - return func(t *testing.T) { - cache.Save(context.Background(), map[string]json.RawMessage{ - reqCacheKey: json.RawMessage(reqCacheVal), - }, nil) - reqData, impData := cache.Get(context.Background(), []string{reqCacheKey, "unknown-req"}, nil) - assertMapLength(t, 1, reqData) - assertHasValue(t, reqData, reqCacheKey, reqCacheVal) - assertMapLength(t, 0, impData) } } -func cacheOverlapTester(cache stored_requests.Cache) func(*testing.T) { - commonKey := "id" +func cacheSaveInvalidateTester(cache stored_requests.CacheJSON) func(*testing.T) { return func(t *testing.T) { cache.Save(context.Background(), map[string]json.RawMessage{ - commonKey: json.RawMessage(reqCacheVal), - }, map[string]json.RawMessage{ - commonKey: json.RawMessage(impCacheVal), - }) - reqData, impData := cache.Get(context.Background(), []string{commonKey}, []string{commonKey}) - assertMapLength(t, 1, reqData) - assertHasValue(t, reqData, commonKey, reqCacheVal) - assertMapLength(t, 1, impData) - assertHasValue(t, impData, commonKey, impCacheVal) - } -} - -func cacheSaveInvalidateTester(cache stored_requests.Cache) func(*testing.T) { - return func(t *testing.T) { - cache.Save(context.Background(), map[string]json.RawMessage{ - reqCacheKey: json.RawMessage(reqCacheVal), - }, map[string]json.RawMessage{ reqCacheKey: json.RawMessage(reqCacheVal), }) - reqData, impData := cache.Get(context.Background(), []string{reqCacheKey}, []string{reqCacheKey}) + reqData := cache.Get(context.Background(), []string{reqCacheKey}) assertMapLength(t, 1, reqData) - assertMapLength(t, 1, impData) - cache.Invalidate(context.Background(), []string{reqCacheKey}, []string{reqCacheKey}) - reqData, impData = cache.Get(context.Background(), []string{reqCacheKey}, []string{reqCacheKey}) + cache.Invalidate(context.Background(), []string{reqCacheKey}) + reqData = cache.Get(context.Background(), []string{reqCacheKey}) assertMapLength(t, 0, reqData) - assertMapLength(t, 0, impData) } } diff --git a/stored_requests/caches/memory/cache.go b/stored_requests/caches/memory/cache.go index 4262ea21021..288e6c26b71 100644 --- a/stored_requests/caches/memory/cache.go +++ b/stored_requests/caches/memory/cache.go @@ -5,7 +5,6 @@ import ( "encoding/json" "sync" - "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/coocood/freecache" "github.com/golang/glog" @@ -17,68 +16,52 @@ import ( // 2. The cache is too large. This will cause the least recently used items to be evicted. // // For no TTL, use ttlSeconds <= 0 -func NewCache(cfg *config.InMemoryCache) stored_requests.Cache { - return &cache{ - requestDataCache: newCacheForWithLimits(cfg.RequestCacheSize, cfg.TTL, "Request"), - impDataCache: newCacheForWithLimits(cfg.ImpCacheSize, cfg.TTL, "Imp"), - } -} - -func newCacheForWithLimits(size int, ttl int, dataType string) mapLike { +func NewCache(size int, ttl int, dataType string) stored_requests.CacheJSON { if ttl > 0 && size <= 0 { - glog.Fatal("No in-memory caches defined with a finite TTL but unbounded size. Config validation should have caught this. Failing fast because something is buggy.") + // a positive ttl indicates "LRU" cache type, while unlimited size indicates an "unbounded" cache type + glog.Fatalf("unbounded in-memory %s cache with TTL not allowed. Config validation should have caught this. Failing fast because something is buggy.", dataType) } if size > 0 { glog.Infof("Using a Stored %s in-memory cache. Max size: %d bytes. TTL: %d seconds.", dataType, size, ttl) - return &pbsLRUCache{ - Cache: freecache.NewCache(size), - ttlSeconds: ttl, + return &cache{ + dataType: dataType, + cache: &pbsLRUCache{ + Cache: freecache.NewCache(size), + ttlSeconds: ttl, + }, } } else { glog.Infof("Using an unbounded Stored %s in-memory cache.", dataType) - return &pbsSyncMap{&sync.Map{}} + return &cache{ + dataType: dataType, + cache: &pbsSyncMap{&sync.Map{}}, + } } } type cache struct { - requestDataCache mapLike - impDataCache mapLike + dataType string + cache mapLike } -func (c *cache) Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) { - requestData = doGet(c.requestDataCache, requestIDs) - impData = doGet(c.impDataCache, impIDs) - return -} - -func doGet(cache mapLike, ids []string) (data map[string]json.RawMessage) { +func (c *cache) Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) { data = make(map[string]json.RawMessage, len(ids)) for _, id := range ids { - if val, ok := cache.Get(id); ok { + if val, ok := c.cache.Get(id); ok { data[id] = val } } return } -func (c *cache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) { - c.doSave(c.requestDataCache, storedRequests) - c.doSave(c.impDataCache, storedImps) -} - -func (c *cache) doSave(cache mapLike, values map[string]json.RawMessage) { - for id, data := range values { - cache.Set(id, data) +func (c *cache) Save(ctx context.Context, data map[string]json.RawMessage) { + for id, data := range data { + c.cache.Set(id, data) } } -func (c *cache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { - doInvalidate(c.requestDataCache, requestIDs) - doInvalidate(c.impDataCache, impIDs) -} - -func doInvalidate(cache mapLike, ids []string) { +func (c *cache) Invalidate(ctx context.Context, ids []string) { for _, id := range ids { - cache.Delete(id) + c.cache.Delete(id) } } diff --git a/stored_requests/caches/memory/cache_test.go b/stored_requests/caches/memory/cache_test.go index 673b9a0c8fe..20ec1239cd2 100644 --- a/stored_requests/caches/memory/cache_test.go +++ b/stored_requests/caches/memory/cache_test.go @@ -7,52 +7,34 @@ import ( "strconv" "testing" - "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/caches/cachestest" ) func TestLRURobustness(t *testing.T) { - cachestest.AssertCacheRobustness(t, func() stored_requests.Cache { - return NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) + cachestest.AssertCacheRobustness(t, func() stored_requests.CacheJSON { + return NewCache(256*1024, -1, "TestData") }) } func TestUnboundedRobustness(t *testing.T) { - cachestest.AssertCacheRobustness(t, func() stored_requests.Cache { - return NewCache(&config.InMemoryCache{ - RequestCacheSize: 0, - ImpCacheSize: 0, - TTL: -1, - }) + cachestest.AssertCacheRobustness(t, func() stored_requests.CacheJSON { + return NewCache(0, -1, "TestData") }) } func TestRaceLRUConcurrency(t *testing.T) { - cache := NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) - + cache := NewCache(256*1024, -1, "TestData") doRaceTest(t, cache) } func TestRaceUnboundedConcurrency(t *testing.T) { - cache := NewCache(&config.InMemoryCache{ - RequestCacheSize: 0, - ImpCacheSize: 0, - TTL: -1, - }) + cache := NewCache(0, -1, "TestData") doRaceTest(t, cache) } -func doRaceTest(t *testing.T, cache stored_requests.Cache) { +func doRaceTest(t *testing.T, cache stored_requests.CacheJSON) { done := make(chan struct{}) sets := [][]int{rand.Perm(100), rand.Perm(100), rand.Perm(100)} @@ -70,26 +52,26 @@ func doRaceTest(t *testing.T, cache stored_requests.Cache) { } } -func readLots(cache stored_requests.Cache, done chan<- struct{}, reads []int) { +func readLots(cache stored_requests.CacheJSON, done chan<- struct{}, reads []int) { var s struct{} for _, i := range reads { - cache.Get(context.Background(), sliceForVal(i), sliceForVal(-i)) + cache.Get(context.Background(), sliceForVal(i)) } done <- s } -func writeLots(cache stored_requests.Cache, done chan<- struct{}, writes []int) { +func writeLots(cache stored_requests.CacheJSON, done chan<- struct{}, writes []int) { var s struct{} for _, i := range writes { - cache.Save(context.Background(), mapForVal(i), mapForVal(-i)) + cache.Save(context.Background(), mapForVal(i)) } done <- s } -func invalidateLots(cache stored_requests.Cache, done chan<- struct{}, invalidates []int) { +func invalidateLots(cache stored_requests.CacheJSON, done chan<- struct{}, invalidates []int) { var s struct{} for _, i := range invalidates { - cache.Invalidate(context.Background(), sliceForVal(i), sliceForVal(-i)) + cache.Invalidate(context.Background(), sliceForVal(i)) } done <- s } diff --git a/stored_requests/caches/nil_cache/nil_cache.go b/stored_requests/caches/nil_cache/nil_cache.go index de29156e3c9..d043ae55c96 100644 --- a/stored_requests/caches/nil_cache/nil_cache.go +++ b/stored_requests/caches/nil_cache/nil_cache.go @@ -8,13 +8,14 @@ import ( // NilCache is a no-op cache which does nothing useful. type NilCache struct{} -func (c *NilCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage) { - return nil, nil +func (c *NilCache) Get(ctx context.Context, ids []string) map[string]json.RawMessage { + return make(map[string]json.RawMessage) } -func (c *NilCache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) { + +func (c *NilCache) Save(ctx context.Context, data map[string]json.RawMessage) { return } -func (c *NilCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { +func (c *NilCache) Invalidate(ctx context.Context, ids []string) { return } diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go index 2d979e4cd35..9310b2522a1 100644 --- a/stored_requests/config/config.go +++ b/stored_requests/config/config.go @@ -20,6 +20,7 @@ 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/PubMatic-OpenWrap/prebid-server/util/task" "github.com/golang/glog" "github.com/julienschmidt/httprouter" ) @@ -41,29 +42,30 @@ type dbConnection struct { // // As a side-effect, it will add some endpoints to the router if the config calls for it. // In the future we should look for ways to simplify this so that it's not doing two things. -func CreateStoredRequests(cfg *config.StoredRequestsSlim, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router, dbc *dbConnection) (fetcher stored_requests.AllFetcher, shutdown func()) { +func CreateStoredRequests(cfg *config.StoredRequests, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router, dbc *dbConnection) (fetcher stored_requests.AllFetcher, shutdown func()) { // Create database connection if given options for one if cfg.Postgres.ConnectionInfo.Database != "" { conn := cfg.Postgres.ConnectionInfo.ConnString() if dbc.conn == "" { - glog.Infof("Connecting to Postgres for Stored Requests. DB=%s, host=%s, port=%d, user=%s", + glog.Infof("Connecting to Postgres for Stored %s. DB=%s, host=%s, port=%d, user=%s", + cfg.DataType(), cfg.Postgres.ConnectionInfo.Database, cfg.Postgres.ConnectionInfo.Host, cfg.Postgres.ConnectionInfo.Port, cfg.Postgres.ConnectionInfo.Username) - db := newPostgresDB(cfg.Postgres.ConnectionInfo) + db := newPostgresDB(cfg.DataType(), cfg.Postgres.ConnectionInfo) dbc.conn = conn dbc.db = db } // Error out if config is trying to use multiple database connections for different stored requests (not supported yet) if conn != dbc.conn { - glog.Fatal("Multiple database connection settings found in Stored Requests config, only a single database connection is currently supported.") + glog.Fatal("Multiple database connection settings found in config, only a single database connection is currently supported.") } } - eventProducers := newEventProducers(cfg, client, dbc.db, router) + eventProducers := newEventProducers(cfg, client, dbc.db, metricsEngine, router) fetcher = newFetcher(cfg, client, dbc.db) var shutdown1 func() @@ -105,10 +107,7 @@ func CreateStoredRequests(cfg *config.StoredRequestsSlim, metricsEngine pbsmetri // // As a side-effect, it will add some endpoints to the router if the config calls for it. // In the future we should look for ways to simplify this so that it's not doing two things. -func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router) (db *sql.DB, shutdown func(), fetcher stored_requests.Fetcher, ampFetcher stored_requests.Fetcher, categoriesFetcher stored_requests.CategoryFetcher, videoFetcher stored_requests.Fetcher) { - // Build individual slim options from combined config struct - slimAuction, slimAmp := resolvedStoredRequestsConfig(cfg) - +func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, client *http.Client, router *httprouter.Router) (db *sql.DB, shutdown func(), fetcher stored_requests.Fetcher, ampFetcher stored_requests.Fetcher, accountsFetcher stored_requests.AccountFetcher, categoriesFetcher stored_requests.CategoryFetcher, videoFetcher stored_requests.Fetcher) { // TODO: Switch this to be set in config defaults //if cfg.CategoryMapping.CacheEvents.Enabled && cfg.CategoryMapping.CacheEvents.Endpoint == "" { // cfg.CategoryMapping.CacheEvents.Endpoint = "/storedrequest/categorymapping" @@ -116,10 +115,11 @@ func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.Metri var dbc dbConnection - fetcher1, shutdown1 := CreateStoredRequests(&slimAuction, metricsEngine, client, router, &dbc) - fetcher2, shutdown2 := CreateStoredRequests(&slimAmp, metricsEngine, client, router, &dbc) + fetcher1, shutdown1 := CreateStoredRequests(&cfg.StoredRequests, metricsEngine, client, router, &dbc) + fetcher2, shutdown2 := CreateStoredRequests(&cfg.StoredRequestsAMP, metricsEngine, client, router, &dbc) fetcher3, shutdown3 := CreateStoredRequests(&cfg.CategoryMapping, metricsEngine, client, router, &dbc) fetcher4, shutdown4 := CreateStoredRequests(&cfg.StoredVideo, metricsEngine, client, router, &dbc) + fetcher5, shutdown5 := CreateStoredRequests(&cfg.Accounts, metricsEngine, client, router, &dbc) db = dbc.db @@ -127,59 +127,19 @@ func NewStoredRequests(cfg *config.Configuration, metricsEngine pbsmetrics.Metri ampFetcher = fetcher2.(stored_requests.Fetcher) categoriesFetcher = fetcher3.(stored_requests.CategoryFetcher) videoFetcher = fetcher4.(stored_requests.Fetcher) + accountsFetcher = fetcher5.(stored_requests.AccountFetcher) shutdown = func() { shutdown1() shutdown2() shutdown3() shutdown4() + shutdown5() } return } -func resolvedStoredRequestsConfig(cfg *config.Configuration) (auc, amp config.StoredRequestsSlim) { - sr := &cfg.StoredRequests - - // Auction endpoint uses non-Amp fields so can just copy the slin data - auc.Files.Enabled = sr.Files - auc.Files.Path = sr.Path - auc.Postgres.ConnectionInfo = sr.Postgres.ConnectionInfo - auc.Postgres.FetcherQueries.QueryTemplate = sr.Postgres.FetcherQueries.QueryTemplate - auc.Postgres.CacheInitialization.Timeout = sr.Postgres.CacheInitialization.Timeout - auc.Postgres.CacheInitialization.Query = sr.Postgres.CacheInitialization.Query - auc.Postgres.PollUpdates.RefreshRate = sr.Postgres.PollUpdates.RefreshRate - auc.Postgres.PollUpdates.Timeout = sr.Postgres.PollUpdates.Timeout - auc.Postgres.PollUpdates.Query = sr.Postgres.PollUpdates.Query - auc.HTTP.Endpoint = sr.HTTP.Endpoint - auc.InMemoryCache = sr.InMemoryCache - auc.CacheEvents.Enabled = sr.CacheEventsAPI - auc.CacheEvents.Endpoint = "/storedrequests/openrtb2" - auc.HTTPEvents.RefreshRate = sr.HTTPEvents.RefreshRate - auc.HTTPEvents.Timeout = sr.HTTPEvents.Timeout - auc.HTTPEvents.Endpoint = sr.HTTPEvents.Endpoint - - // Amp endpoint uses all the slim data but some fields get replacyed by Amp* version of similar fields - amp.Files.Enabled = sr.Files - amp.Files.Path = sr.Path - amp.Postgres.ConnectionInfo = sr.Postgres.ConnectionInfo - amp.Postgres.FetcherQueries.QueryTemplate = sr.Postgres.FetcherQueries.AmpQueryTemplate - amp.Postgres.CacheInitialization.Timeout = sr.Postgres.CacheInitialization.Timeout - amp.Postgres.CacheInitialization.Query = sr.Postgres.CacheInitialization.AmpQuery - amp.Postgres.PollUpdates.RefreshRate = sr.Postgres.PollUpdates.RefreshRate - amp.Postgres.PollUpdates.Timeout = sr.Postgres.PollUpdates.Timeout - amp.Postgres.PollUpdates.Query = sr.Postgres.PollUpdates.AmpQuery - amp.HTTP.Endpoint = sr.HTTP.AmpEndpoint - amp.InMemoryCache = sr.InMemoryCache - amp.CacheEvents.Enabled = sr.CacheEventsAPI - amp.CacheEvents.Endpoint = "/storedrequests/amp" - amp.HTTPEvents.RefreshRate = sr.HTTPEvents.RefreshRate - amp.HTTPEvents.Timeout = sr.HTTPEvents.Timeout - amp.HTTPEvents.Endpoint = sr.HTTPEvents.AmpEndpoint - - return -} - func addListeners(cache stored_requests.Cache, eventProducers []events.EventProducer) (shutdown func()) { listeners := make([]*events.EventListener, 0, len(eventProducers)) @@ -196,36 +156,41 @@ func addListeners(cache stored_requests.Cache, eventProducers []events.EventProd } } -func newFetcher(cfg *config.StoredRequestsSlim, client *http.Client, db *sql.DB) (fetcher stored_requests.AllFetcher) { +func newFetcher(cfg *config.StoredRequests, client *http.Client, db *sql.DB) (fetcher stored_requests.AllFetcher) { idList := make(stored_requests.MultiFetcher, 0, 3) if cfg.Files.Enabled { - fFetcher := newFilesystem(cfg.Files.Path) + fFetcher := newFilesystem(cfg.DataType(), cfg.Files.Path) idList = append(idList, fFetcher) } if cfg.Postgres.FetcherQueries.QueryTemplate != "" { - glog.Infof("Loading Stored Requests via Postgres.\nQuery: %s", cfg.Postgres.FetcherQueries.QueryTemplate) + glog.Infof("Loading Stored %s data via Postgres.\nQuery: %s", cfg.DataType(), cfg.Postgres.FetcherQueries.QueryTemplate) idList = append(idList, db_fetcher.NewFetcher(db, cfg.Postgres.FetcherQueries.MakeQuery)) } if cfg.HTTP.Endpoint != "" { - glog.Infof("Loading Stored Requests via HTTP. endpoint=%s", cfg.HTTP.Endpoint) + glog.Infof("Loading Stored %s data via HTTP. endpoint=%s", cfg.DataType(), cfg.HTTP.Endpoint) idList = append(idList, http_fetcher.NewFetcher(client, cfg.HTTP.Endpoint)) } - fetcher = consolidate(idList) + fetcher = consolidate(cfg.DataType(), idList) return } -func newCache(cfg *config.StoredRequestsSlim) stored_requests.Cache { - if cfg.InMemoryCache.Type == "none" { - glog.Info("No Stored Request cache configured. The Fetcher backend will be used for all Stored Requests.") - return &nil_cache.NilCache{} +func newCache(cfg *config.StoredRequests) stored_requests.Cache { + cache := stored_requests.Cache{&nil_cache.NilCache{}, &nil_cache.NilCache{}, &nil_cache.NilCache{}} + switch { + case cfg.InMemoryCache.Type == "none": + glog.Warningf("No %s cache configured. The %s Fetcher backend will be used for all data requests", cfg.DataType(), cfg.DataType()) + case cfg.DataType() == config.AccountDataType: + cache.Accounts = memory.NewCache(cfg.InMemoryCache.Size, cfg.InMemoryCache.TTL, "Accounts") + default: + cache.Requests = memory.NewCache(cfg.InMemoryCache.RequestCacheSize, cfg.InMemoryCache.TTL, "Requests") + cache.Imps = memory.NewCache(cfg.InMemoryCache.ImpCacheSize, cfg.InMemoryCache.TTL, "Imps") } - - return memory.NewCache(&cfg.InMemoryCache) + return cache } -func newEventProducers(cfg *config.StoredRequestsSlim, client *http.Client, db *sql.DB, router *httprouter.Router) (eventProducers []events.EventProducer) { +func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, metricsEngine pbsmetrics.MetricsEngine, router *httprouter.Router) (eventProducers []events.EventProducer) { if cfg.CacheEvents.Enabled { eventProducers = append(eventProducers, newEventsAPI(router, cfg.CacheEvents.Endpoint)) } @@ -233,28 +198,24 @@ func newEventProducers(cfg *config.StoredRequestsSlim, client *http.Client, db * eventProducers = append(eventProducers, newHttpEvents(client, cfg.HTTPEvents.TimeoutDuration(), cfg.HTTPEvents.RefreshRateDuration(), cfg.HTTPEvents.Endpoint)) } if cfg.Postgres.CacheInitialization.Query != "" { - // Make sure we don't miss any updates in between the initial fetch and the "update" polling. - updateStartTime := time.Now() - timeout := time.Duration(cfg.Postgres.CacheInitialization.Timeout) * time.Millisecond - ctx, cancel := context.WithTimeout(context.Background(), timeout) - eventProducers = append(eventProducers, postgresEvents.LoadAll(ctx, db, cfg.Postgres.CacheInitialization.Query)) - cancel() - - if cfg.Postgres.PollUpdates.Query != "" { - eventProducers = append(eventProducers, newPostgresPolling(cfg.Postgres.PollUpdates, db, updateStartTime)) + pgEventCfg := postgresEvents.PostgresEventProducerConfig{ + DB: db, + RequestType: cfg.DataType(), + CacheInitQuery: cfg.Postgres.CacheInitialization.Query, + CacheInitTimeout: time.Duration(cfg.Postgres.CacheInitialization.Timeout) * time.Millisecond, + CacheUpdateQuery: cfg.Postgres.PollUpdates.Query, + CacheUpdateTimeout: time.Duration(cfg.Postgres.PollUpdates.Timeout) * time.Millisecond, + MetricsEngine: metricsEngine, } + pgEventProducer := postgresEvents.NewPostgresEventProducer(pgEventCfg) + fetchInterval := time.Duration(cfg.Postgres.PollUpdates.RefreshRate) * time.Second + pgEventTickerTask := task.NewTickerTask(fetchInterval, pgEventProducer) + pgEventTickerTask.Start() + eventProducers = append(eventProducers, pgEventProducer) } return } -func newPostgresPolling(cfg config.PostgresUpdatePollingSlim, db *sql.DB, startTime time.Time) events.EventProducer { - timeout := time.Duration(cfg.Timeout) * time.Millisecond - ctxProducer := func() (ctx context.Context, canceller func()) { - return context.WithTimeout(context.Background(), timeout) - } - return postgresEvents.PollForUpdates(ctxProducer, db, cfg.Query, startTime, time.Duration(cfg.RefreshRate)*time.Second) -} - func newEventsAPI(router *httprouter.Router, endpoint string) events.EventProducer { producer, handler := apiEvents.NewEventsAPI() router.POST(endpoint, handler) @@ -269,32 +230,37 @@ func newHttpEvents(client *http.Client, timeout time.Duration, refreshRate time. return httpEvents.NewHTTPEvents(client, endpoint, ctxProducer, refreshRate) } -func newFilesystem(configPath string) stored_requests.AllFetcher { - glog.Infof("Loading Stored Requests from filesystem at path %s", configPath) +func newFilesystem(dataType config.DataType, configPath string) stored_requests.AllFetcher { + glog.Infof("Loading Stored %s data from filesystem at path %s", dataType, configPath) fetcher, err := file_fetcher.NewFileFetcher(configPath) if err != nil { - glog.Fatalf("Failed to create a FileFetcher: %v", err) + glog.Fatalf("Failed to create a %s FileFetcher: %v", dataType, err) } return fetcher } -func newPostgresDB(cfg config.PostgresConnection) *sql.DB { +func newPostgresDB(dataType config.DataType, cfg config.PostgresConnection) *sql.DB { db, err := sql.Open("postgres", cfg.ConnString()) if err != nil { - glog.Fatalf("Failed to open postgres connection: %v", err) + glog.Fatalf("Failed to open %s postgres connection: %v", dataType, err) } if err := db.Ping(); err != nil { - glog.Fatalf("Failed to ping postgres: %v", err) + glog.Fatalf("Failed to ping %s postgres: %v", dataType, err) } return db } // consolidate returns a single Fetcher from an array of fetchers of any size. -func consolidate(fetchers []stored_requests.AllFetcher) stored_requests.AllFetcher { +func consolidate(dataType config.DataType, fetchers []stored_requests.AllFetcher) stored_requests.AllFetcher { if len(fetchers) == 0 { - glog.Warning("No Stored Request support configured. request.imp[i].ext.prebid.storedrequest will be ignored. If you need this, check your app config") + switch dataType { + case config.RequestDataType: + glog.Warning("No Stored Request support configured. request.imp[i].ext.prebid.storedrequest will be ignored. If you need this, check your app config") + default: + glog.Warningf("No Stored %s support configured. If you need this, check your app config", dataType) + } return empty_fetcher.EmptyFetcher{} } else if len(fetchers) == 1 { return fetchers[0] diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go index e40c0fea733..5c393bb7047 100644 --- a/stored_requests/config/config_test.go +++ b/stored_requests/config/config_test.go @@ -9,141 +9,60 @@ import ( "regexp" "testing" + "github.com/stretchr/testify/assert" + sqlmock "github.com/DATA-DOG/go-sqlmock" "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "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/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" + "github.com/stretchr/testify/mock" ) +func typedConfig(dataType config.DataType, sr *config.StoredRequests) *config.StoredRequests { + sr.SetDataType(dataType) + return sr +} + +func isEmptyCacheType(cache stored_requests.CacheJSON) bool { + cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}) + objs := cache.Get(context.Background(), []string{"foo"}) + return len(objs) == 0 +} + +func isMemoryCacheType(cache stored_requests.CacheJSON) bool { + cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}) + objs := cache.Get(context.Background(), []string{"foo"}) + return len(objs) == 1 +} + func TestNewEmptyFetcher(t *testing.T) { - fetcher := newFetcher(&config.StoredRequestsSlim{}, nil, nil) - ampFetcher := newFetcher(&config.StoredRequestsSlim{}, nil, nil) - if fetcher == nil || ampFetcher == nil { - t.Errorf("The fetchers should be non-nil, even with an empty config.") + fetcher := newFetcher(&config.StoredRequests{}, nil, nil) + if fetcher == nil { + t.Errorf("The fetcher should be non-nil, even with an empty config.") } if _, ok := fetcher.(empty_fetcher.EmptyFetcher); !ok { t.Errorf("If the config is empty, and EmptyFetcher should be returned") } - if _, ok := ampFetcher.(empty_fetcher.EmptyFetcher); !ok { - t.Errorf("If the config is empty, and EmptyFetcher should be returned for AMP") - } } func TestNewHTTPFetcher(t *testing.T) { - fetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ - Endpoint: "stored-requests.prebid.com", - }, - }, nil, nil) - ampFetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ - Endpoint: "stored-requests.prebid.com?type=amp", - }, - }, nil, nil) - if httpFetcher, ok := fetcher.(*http_fetcher.HttpFetcher); ok { - if httpFetcher.Endpoint != "stored-requests.prebid.com?" { - t.Errorf("The HTTP fetcher is using the wrong endpoint. Expected %s, got %s", "stored-requests.prebid.com?", httpFetcher.Endpoint) - } - } else { - t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", ampFetcher) - } - if httpFetcher, ok := ampFetcher.(*http_fetcher.HttpFetcher); ok { - if httpFetcher.Endpoint != "stored-requests.prebid.com?type=amp&" { - t.Errorf("The AMP HTTP fetcher is using the wrong endpoint. Expected %s, got %s", "stored-requests.prebid.com?type=amp&", httpFetcher.Endpoint) - } - } else { - t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", ampFetcher) - } -} - -func TestNewHTTPFetcherNoAmp(t *testing.T) { - fetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ + fetcher := newFetcher(&config.StoredRequests{ + HTTP: config.HTTPFetcherConfig{ Endpoint: "stored-requests.prebid.com", }, }, nil, nil) - ampFetcher := newFetcher(&config.StoredRequestsSlim{ - HTTP: config.HTTPFetcherConfigSlim{ - Endpoint: "", - }, - }, nil, nil) if httpFetcher, ok := fetcher.(*http_fetcher.HttpFetcher); ok { if httpFetcher.Endpoint != "stored-requests.prebid.com?" { t.Errorf("The HTTP fetcher is using the wrong endpoint. Expected %s, got %s", "stored-requests.prebid.com?", httpFetcher.Endpoint) } } else { - t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", ampFetcher) - } - if httpAmpFetcher, ok := ampFetcher.(*http_fetcher.HttpFetcher); ok && httpAmpFetcher == nil { - t.Errorf("An HTTP Fetching config should not return an Amp HTTP fetcher in this case. Got %v (%v)", ampFetcher, httpAmpFetcher) - } -} - -func TestResolveConfig(t *testing.T) { - cfg := &config.Configuration{ - StoredRequests: config.StoredRequests{ - Files: true, - Path: "/test-path", - Postgres: config.PostgresConfig{ - ConnectionInfo: config.PostgresConnection{ - Database: "db", - Host: "pghost", - Port: 5, - Username: "user", - Password: "pass", - }, - FetcherQueries: config.PostgresFetcherQueries{ - AmpQueryTemplate: "amp-fetcher-query", - }, - CacheInitialization: config.PostgresCacheInitializer{ - AmpQuery: "amp-cache-init-query", - }, - PollUpdates: config.PostgresUpdatePolling{ - AmpQuery: "amp-poll-query", - }, - }, - HTTP: config.HTTPFetcherConfig{ - AmpEndpoint: "amp-http-fetcher-endpoint", - }, - InMemoryCache: config.InMemoryCache{ - Type: "none", - TTL: 50, - RequestCacheSize: 1, - ImpCacheSize: 2, - }, - CacheEventsAPI: true, - HTTPEvents: config.HTTPEventsConfig{ - AmpEndpoint: "amp-http-events-endpoint", - }, - }, + t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", fetcher) } - - cfg.StoredRequests.Postgres.FetcherQueries.QueryTemplate = "auc-fetcher-query" - cfg.StoredRequests.Postgres.CacheInitialization.Query = "auc-cache-init-query" - cfg.StoredRequests.Postgres.PollUpdates.Query = "auc-poll-query" - cfg.StoredRequests.HTTP.Endpoint = "auc-http-fetcher-endpoint" - cfg.StoredRequests.HTTPEvents.Endpoint = "auc-http-events-endpoint" - - auc, amp := resolvedStoredRequestsConfig(cfg) - - // Auction slim should have the non-amp values in it - assertStringsEqual(t, auc.Postgres.FetcherQueries.QueryTemplate, cfg.StoredRequests.Postgres.FetcherQueries.QueryTemplate) - assertStringsEqual(t, auc.Postgres.CacheInitialization.Query, cfg.StoredRequests.Postgres.CacheInitialization.Query) - assertStringsEqual(t, auc.Postgres.PollUpdates.Query, cfg.StoredRequests.Postgres.PollUpdates.Query) - assertStringsEqual(t, auc.HTTP.Endpoint, cfg.StoredRequests.HTTP.Endpoint) - assertStringsEqual(t, auc.HTTPEvents.Endpoint, cfg.StoredRequests.HTTPEvents.Endpoint) - assertStringsEqual(t, auc.CacheEvents.Endpoint, "/storedrequests/openrtb2") - - // Amp slim should have the amp values in it - assertStringsEqual(t, amp.Postgres.FetcherQueries.QueryTemplate, cfg.StoredRequests.Postgres.FetcherQueries.AmpQueryTemplate) - assertStringsEqual(t, amp.Postgres.CacheInitialization.Query, cfg.StoredRequests.Postgres.CacheInitialization.AmpQuery) - assertStringsEqual(t, amp.Postgres.PollUpdates.Query, cfg.StoredRequests.Postgres.PollUpdates.AmpQuery) - assertStringsEqual(t, amp.HTTP.Endpoint, cfg.StoredRequests.HTTP.AmpEndpoint) - assertStringsEqual(t, amp.HTTPEvents.Endpoint, cfg.StoredRequests.HTTPEvents.AmpEndpoint) - assertStringsEqual(t, amp.CacheEvents.Endpoint, "/storedrequests/amp") } func TestNewHTTPEvents(t *testing.T) { @@ -152,84 +71,83 @@ func TestNewHTTPEvents(t *testing.T) { } server1 := httptest.NewServer(http.HandlerFunc(handler)) - cfg := &config.StoredRequestsSlim{ - HTTPEvents: config.HTTPEventsConfigSlim{ + cfg := &config.StoredRequests{ + HTTPEvents: config.HTTPEventsConfig{ Endpoint: server1.URL, RefreshRate: 100, Timeout: 1000, }, } - evProducers := newEventProducers(cfg, server1.Client(), nil, nil) + + metricsMock := &pbsmetrics.MetricsEngineMock{} + + evProducers := newEventProducers(cfg, server1.Client(), nil, metricsMock, nil) assertSliceLength(t, evProducers, 1) assertHttpWithURL(t, evProducers[0], server1.URL) } func TestNewEmptyCache(t *testing.T) { - cache := newCache(&config.StoredRequestsSlim{InMemoryCache: config.InMemoryCache{Type: "none"}}) - cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}, nil) - reqs, _ := cache.Get(context.Background(), []string{"foo"}, nil) - if len(reqs) != 0 { - t.Errorf("The newCache method should return an empty cache if the config asks for it.") - } + cache := newCache(&config.StoredRequests{InMemoryCache: config.InMemoryCache{Type: "none"}}) + assert.True(t, isEmptyCacheType(cache.Requests), "The newCache method should return an empty Request cache") + assert.True(t, isEmptyCacheType(cache.Imps), "The newCache method should return an empty Imp cache") + assert.True(t, isEmptyCacheType(cache.Accounts), "The newCache method should return an empty Account cache") } func TestNewInMemoryCache(t *testing.T) { - cache := newCache(&config.StoredRequestsSlim{ + cache := newCache(&config.StoredRequests{ InMemoryCache: config.InMemoryCache{ TTL: 60, RequestCacheSize: 100, ImpCacheSize: 100, }, }) - cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}, nil) - reqs, _ := cache.Get(context.Background(), []string{"foo"}, nil) - if len(reqs) != 1 { - t.Errorf("The newCache method should return an in-memory cache if the config asks for it.") - } + assert.True(t, isMemoryCacheType(cache.Requests), "The newCache method should return an in-memory Request cache for StoredRequests config") + assert.True(t, isMemoryCacheType(cache.Imps), "The newCache method should return an in-memory Imp cache for StoredRequests config") + assert.True(t, isEmptyCacheType(cache.Accounts), "The newCache method should return an empty Account cache for StoredRequests config") +} + +func TestNewInMemoryAccountCache(t *testing.T) { + cache := newCache(typedConfig(config.AccountDataType, &config.StoredRequests{ + InMemoryCache: config.InMemoryCache{ + TTL: 60, + Size: 100, + }, + })) + assert.True(t, isMemoryCacheType(cache.Accounts), "The newCache method should return an in-memory Account cache for Accounts config") + assert.True(t, isEmptyCacheType(cache.Requests), "The newCache method should return an empty Request cache for Accounts config") + assert.True(t, isEmptyCacheType(cache.Imps), "The newCache method should return an empty Imp cache for Accounts config") } func TestNewPostgresEventProducers(t *testing.T) { - cfg := &config.StoredRequestsSlim{ - Postgres: config.PostgresConfigSlim{ - CacheInitialization: config.PostgresCacheInitializerSlim{ + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordStoredDataFetchTime", mock.Anything, mock.Anything).Return() + metricsMock.Mock.On("RecordStoredDataError", mock.Anything).Return() + + cfg := &config.StoredRequests{ + Postgres: config.PostgresConfig{ + CacheInitialization: config.PostgresCacheInitializer{ Timeout: 50, Query: "SELECT id, requestData, type FROM stored_data", }, - PollUpdates: config.PostgresUpdatePollingSlim{ + PollUpdates: config.PostgresUpdatePolling{ RefreshRate: 20, Timeout: 50, Query: "SELECT id, requestData, type FROM stored_data WHERE last_updated > $1", }, }, } - ampCfg := &config.StoredRequestsSlim{ - Postgres: config.PostgresConfigSlim{ - CacheInitialization: config.PostgresCacheInitializerSlim{ - Timeout: 50, - Query: "SELECT id, requestData, type FROM stored_amp_data", - }, - PollUpdates: config.PostgresUpdatePollingSlim{ - RefreshRate: 20, - Timeout: 50, - Query: "SELECT id, requestData, type FROM stored_amp_data WHERE last_updated > $1", - }, - }, - } client := &http.Client{} db, mock, err := sqlmock.New() if err != nil { t.Fatalf("Failed to create mock: %v", err) } mock.ExpectQuery("^" + regexp.QuoteMeta(cfg.Postgres.CacheInitialization.Query) + "$").WillReturnError(errors.New("Query failed")) - mock.ExpectQuery("^" + regexp.QuoteMeta(ampCfg.Postgres.CacheInitialization.Query) + "$").WillReturnError(errors.New("Query failed")) - - evProducers := newEventProducers(cfg, client, db, nil) - assertProducerLength(t, evProducers, 2) - ampEvProducers := newEventProducers(ampCfg, client, db, nil) - assertProducerLength(t, ampEvProducers, 2) + evProducers := newEventProducers(cfg, client, db, metricsMock, nil) + assertProducerLength(t, evProducers, 1) assertExpectationsMet(t, mock) + metricsMock.AssertExpectations(t) } func TestNewEventsAPI(t *testing.T) { diff --git a/stored_requests/data/by_id/accounts/test.json b/stored_requests/data/by_id/accounts/test.json new file mode 100644 index 00000000000..76bafff7f1c --- /dev/null +++ b/stored_requests/data/by_id/accounts/test.json @@ -0,0 +1,14 @@ +{ + "id": "test", + "name": "test account", + "disabled": true, + "cache_ttl": { + "banner": 600, + "video": 3600, + "native": 3600, + "audio": 3600 + }, + "events": { + "enabled": true + } +} diff --git a/stored_requests/events/api/api_test.go b/stored_requests/events/api/api_test.go index eee6143de10..74e02e69e4d 100644 --- a/stored_requests/events/api/api_test.go +++ b/stored_requests/events/api/api_test.go @@ -9,22 +9,22 @@ import ( "strings" "testing" - "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/caches/memory" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" ) func TestGoodRequests(t *testing.T) { - cache := memory.NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) - + cache := stored_requests.Cache{ + Requests: memory.NewCache(256*1024, -1, "Request"), + Imps: memory.NewCache(256*1024, -1, "Imp"), + Accounts: memory.NewCache(256*1024, -1, "Account"), + } id := "1" config := fmt.Sprintf(`{"id": "%s"}`, id) initialValue := map[string]json.RawMessage{id: json.RawMessage(config)} - cache.Save(context.Background(), initialValue, initialValue) + cache.Requests.Save(context.Background(), initialValue) + cache.Imps.Save(context.Background(), initialValue) apiEvents, endpoint := NewEventsAPI() @@ -51,7 +51,8 @@ func TestGoodRequests(t *testing.T) { } <-updateOccurred - reqData, impData := cache.Get(context.Background(), []string{id}, []string{id}) + reqData := cache.Requests.Get(context.Background(), []string{id}) + impData := cache.Imps.Get(context.Background(), []string{id}) assertHasValue(t, reqData, id, config) assertHasValue(t, impData, id, config) @@ -66,18 +67,17 @@ func TestGoodRequests(t *testing.T) { } <-invalidateOccurred - reqData, impData = cache.Get(context.Background(), []string{id}, []string{id}) + reqData = cache.Requests.Get(context.Background(), []string{id}) + impData = cache.Imps.Get(context.Background(), []string{id}) assertMapLength(t, 0, reqData) assertMapLength(t, 0, impData) } func TestBadRequests(t *testing.T) { - cache := memory.NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) - + cache := stored_requests.Cache{ + Requests: memory.NewCache(256*1024, -1, "Requests"), + Imps: memory.NewCache(256*1024, -1, "Imps"), + } apiEvents, endpoint := NewEventsAPI() listener := events.SimpleEventListener() go listener.Listen(cache, apiEvents) diff --git a/stored_requests/events/events.go b/stored_requests/events/events.go index 2e8dd07c880..60909a0d426 100644 --- a/stored_requests/events/events.go +++ b/stored_requests/events/events.go @@ -11,12 +11,14 @@ import ( type Save struct { Requests map[string]json.RawMessage `json:"requests"` Imps map[string]json.RawMessage `json:"imps"` + Accounts map[string]json.RawMessage `json:"accounts"` } // Invalidation represents a bulk invalidation type Invalidation struct { Requests []string `json:"requests"` Imps []string `json:"imps"` + Accounts []string `json:"accounts"` } // EventProducer will produce cache update and invalidation events on its channels @@ -61,12 +63,16 @@ func (e *EventListener) Listen(cache stored_requests.Cache, events EventProducer for { select { case save := <-events.Saves(): - cache.Save(context.Background(), save.Requests, save.Imps) + cache.Requests.Save(context.Background(), save.Requests) + cache.Imps.Save(context.Background(), save.Imps) + cache.Accounts.Save(context.Background(), save.Accounts) if e.onSave != nil { e.onSave() } case invalidation := <-events.Invalidations(): - cache.Invalidate(context.Background(), invalidation.Requests, invalidation.Imps) + cache.Requests.Invalidate(context.Background(), invalidation.Requests) + cache.Imps.Invalidate(context.Background(), invalidation.Imps) + cache.Accounts.Invalidate(context.Background(), invalidation.Accounts) if e.onInvalidate != nil { e.onInvalidate() } diff --git a/stored_requests/events/events_test.go b/stored_requests/events/events_test.go index 84bbc1c6b13..0a48b4cc365 100644 --- a/stored_requests/events/events_test.go +++ b/stored_requests/events/events_test.go @@ -7,7 +7,7 @@ import ( "reflect" "testing" - "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests" "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/caches/memory" ) @@ -16,12 +16,11 @@ func TestListen(t *testing.T) { saves: make(chan Save), invalidations: make(chan Invalidation), } - - cache := memory.NewCache(&config.InMemoryCache{ - RequestCacheSize: 256 * 1024, - ImpCacheSize: 256 * 1024, - TTL: -1, - }) + cache := stored_requests.Cache{ + Requests: memory.NewCache(256*1024, -1, "Requests"), + Imps: memory.NewCache(256*1024, -1, "Imps"), + Accounts: memory.NewCache(256*1024, -1, "Account"), + } // create channels to synchronize saveOccurred := make(chan struct{}) @@ -41,34 +40,43 @@ func TestListen(t *testing.T) { save := Save{ Requests: data, Imps: data, + Accounts: data, } - cache.Save(context.Background(), save.Requests, save.Imps) + cache.Requests.Save(context.Background(), save.Requests) + cache.Imps.Save(context.Background(), save.Imps) + cache.Accounts.Save(context.Background(), save.Accounts) config = fmt.Sprintf(`{"id": "%s", "updated": true}`, id) data = map[string]json.RawMessage{id: json.RawMessage(config)} save = Save{ Requests: data, Imps: data, + Accounts: data, } ep.saves <- save <-saveOccurred - requestData, impData := cache.Get(context.Background(), idSlice, idSlice) - if !reflect.DeepEqual(requestData, data) || !reflect.DeepEqual(impData, data) { + requestData := cache.Requests.Get(context.Background(), idSlice) + impData := cache.Imps.Get(context.Background(), idSlice) + accountData := cache.Accounts.Get(context.Background(), idSlice) + if !reflect.DeepEqual(requestData, data) || !reflect.DeepEqual(impData, data) || !reflect.DeepEqual(accountData, data) { t.Error("Update failed") } invalidation := Invalidation{ Requests: idSlice, Imps: idSlice, + Accounts: idSlice, } ep.invalidations <- invalidation <-invalidateOccurred - requestData, impData = cache.Get(context.Background(), idSlice, idSlice) - if len(requestData) > 0 || len(impData) > 0 { + requestData = cache.Requests.Get(context.Background(), idSlice) + impData = cache.Imps.Get(context.Background(), idSlice) + accountData = cache.Accounts.Get(context.Background(), idSlice) + if len(requestData) > 0 || len(impData) > 0 || len(accountData) > 0 { t.Error("Invalidate failed") } } diff --git a/stored_requests/events/http/http.go b/stored_requests/events/http/http.go index a9f26d0c9d2..790c247e368 100644 --- a/stored_requests/events/http/http.go +++ b/stored_requests/events/http/http.go @@ -42,6 +42,13 @@ import ( // "imp2": { ... stored data for imp2 ... }, // } // } +// or +// { +// "accounts": { +// "acc1": { ... config data for acc1 ... }, +// "acc2": { ... config data for acc2 ... }, +// }, +// } // // To signal deletions, the endpoint may return { "deleted": true } // in place of the Stored Data if the "last-modified" param existed. @@ -82,10 +89,11 @@ func (e *HTTPEvents) fetchAll() { defer cancel() resp, err := ctxhttp.Get(ctx, e.client, e.Endpoint) if respObj, ok := e.parse(e.Endpoint, resp, err); ok && - (len(respObj.StoredRequests) > 0 || len(respObj.StoredImps) > 0) { + (len(respObj.StoredRequests) > 0 || len(respObj.StoredImps) > 0 || len(respObj.Accounts) > 0) { e.saves <- events.Save{ Requests: respObj.StoredRequests, Imps: respObj.StoredImps, + Accounts: respObj.Accounts, } } } @@ -125,14 +133,16 @@ func (e *HTTPEvents) refresh(ticker <-chan time.Time) { invalidations := events.Invalidation{ Requests: extractInvalidations(respObj.StoredRequests), Imps: extractInvalidations(respObj.StoredImps), + Accounts: extractInvalidations(respObj.Accounts), } - if len(respObj.StoredRequests) > 0 || len(respObj.StoredImps) > 0 { + if len(respObj.StoredRequests) > 0 || len(respObj.StoredImps) > 0 || len(respObj.Accounts) > 0 { e.saves <- events.Save{ Requests: respObj.StoredRequests, Imps: respObj.StoredImps, + Accounts: respObj.Accounts, } } - if len(invalidations.Requests) > 0 || len(invalidations.Imps) > 0 { + if len(invalidations.Requests) > 0 || len(invalidations.Imps) > 0 || len(invalidations.Accounts) > 0 { e.invalidations <- invalidations } e.lastUpdate = thisTimeInUTC @@ -193,4 +203,5 @@ func (e *HTTPEvents) Invalidations() <-chan events.Invalidation { type responseContract struct { StoredRequests map[string]json.RawMessage `json:"requests"` StoredImps map[string]json.RawMessage `json:"imps"` + Accounts map[string]json.RawMessage `json:"accounts"` } diff --git a/stored_requests/events/http/http_test.go b/stored_requests/events/http/http_test.go index fdba84cd6fe..2a1aa5d8dfc 100644 --- a/stored_requests/events/http/http_test.go +++ b/stored_requests/events/http/http_test.go @@ -1,145 +1,161 @@ package http import ( - "bytes" "context" "encoding/json" + "fmt" httpCore "net/http" "net/http/httptest" "testing" "time" -) - -func TestStartupReqsOnly(t *testing.T) { - server := httptest.NewServer(&mockResponseHandler{ - statusCode: httpCore.StatusOK, - response: `{"requests":{"request1":{"value":1}, "request2":{"value":2}}}`, - }) - defer server.Close() - - ev := NewHTTPEvents(server.Client(), server.URL, nil, -1) - theSave := <-ev.Saves() - - assertLen(t, theSave.Requests, 2) - assertHasValue(t, theSave.Requests, "request1", `{"value":1}`) - assertHasValue(t, theSave.Requests, "request2", `{"value":2}`) - - assertLen(t, theSave.Imps, 0) -} - -func TestStartupImpsOnly(t *testing.T) { - server := httptest.NewServer(&mockResponseHandler{ - statusCode: httpCore.StatusOK, - response: `{"imps":{"imp1":{"value":1}}}`, - }) - defer server.Close() - - ev := NewHTTPEvents(server.Client(), server.URL, nil, -1) - theSave := <-ev.Saves() - - assertLen(t, theSave.Requests, 0) - - assertLen(t, theSave.Imps, 1) - assertHasValue(t, theSave.Imps, "imp1", `{"value":1}`) -} - -func TestStartupBothTypes(t *testing.T) { - server := httptest.NewServer(&mockResponseHandler{ - statusCode: httpCore.StatusOK, - response: `{"requests":{"request1":{"value":1}, "request2":{"value":2}},"imps":{"imp1":{"value":1}}}`, - }) - defer server.Close() - - ev := NewHTTPEvents(server.Client(), server.URL, nil, -1) - theSave := <-ev.Saves() - - assertLen(t, theSave.Requests, 2) - assertHasValue(t, theSave.Requests, "request1", `{"value":1}`) - assertHasValue(t, theSave.Requests, "request2", `{"value":2}`) - - assertLen(t, theSave.Imps, 1) - assertHasValue(t, theSave.Imps, "imp1", `{"value":1}`) -} - -func TestUpdates(t *testing.T) { - handler := &mockResponseHandler{ - statusCode: httpCore.StatusOK, - response: `{"requests":{"request1":{"value":1}, "request2":{"value":2}},"imps":{"imp1":{"value":3},"imp2":{"value":4}}}`, - } - server := httptest.NewServer(handler) - defer server.Close() - - ev := NewHTTPEvents(server.Client(), server.URL, nil, -1) - - handler.response = `{"requests":{"request1":{"value":5}, "request2":{"deleted":true}},"imps":{"imp1":{"deleted":true},"imp2":{"value":6}}}` - timeChan := make(chan time.Time, 1) - timeChan <- time.Now() - go ev.refresh(timeChan) - firstSave := <-ev.Saves() - secondSave := <-ev.Saves() - inv := <-ev.Invalidations() - - assertLen(t, firstSave.Requests, 2) - assertHasValue(t, firstSave.Requests, "request1", `{"value":1}`) - assertHasValue(t, firstSave.Requests, "request2", `{"value":2}`) - assertLen(t, firstSave.Imps, 2) - assertHasValue(t, firstSave.Imps, "imp1", `{"value":3}`) - assertHasValue(t, firstSave.Imps, "imp2", `{"value":4}`) - - assertLen(t, secondSave.Requests, 1) - assertHasValue(t, secondSave.Requests, "request1", `{"value":5}`) - assertLen(t, secondSave.Imps, 1) - assertHasValue(t, secondSave.Imps, "imp2", `{"value":6}`) - - assertArrLen(t, inv.Requests, 1) - assertArrContains(t, inv.Requests, "request2") - assertArrLen(t, inv.Imps, 1) - assertArrContains(t, inv.Imps, "imp1") -} -func TestErrorResponse(t *testing.T) { - handler := &mockResponseHandler{ - statusCode: httpCore.StatusInternalServerError, - response: "Something horrible happened.", - } - server := httptest.NewServer(handler) - defer server.Close() + "github.com/stretchr/testify/assert" +) - ev := NewHTTPEvents(server.Client(), server.URL, nil, -1) - if len(ev.Saves()) != 0 { - t.Errorf("No saves should be emitted if the HTTP call fails. Got %d", len(ev.Saves())) - } +func ctxProducer() (context.Context, func()) { + return context.WithTimeout(context.Background(), -1) } -func TestExpiredContext(t *testing.T) { - handler := &mockResponseHandler{ - statusCode: httpCore.StatusInternalServerError, - response: "Something horrible happened.", - } - server := httptest.NewServer(handler) - defer server.Close() - - ctxProducer := func() (context.Context, func()) { - return context.WithTimeout(context.Background(), -1) +func TestStartup(t *testing.T) { + type testStep struct { + statusCode int + response string + timeout bool + saves string + invalidations string } - - ev := NewHTTPEvents(server.Client(), server.URL, ctxProducer, -1) - if len(ev.Saves()) != 0 { - t.Errorf("No saves should be emitted if the HTTP call is cancelled. Got %d", len(ev.Saves())) - } -} - -func TestMalformedResponse(t *testing.T) { - handler := &mockResponseHandler{ - statusCode: httpCore.StatusOK, - response: "This isn't JSON.", + testCases := []struct { + description string + tests []testStep + }{ + { + description: "Load requests at startup", + tests: []testStep{ + { + statusCode: httpCore.StatusOK, + response: `{"requests": {"request1": {"value":1}, "request2": {"value":2}}}`, + saves: `{"requests": {"request1": {"value":1}, "request2": {"value":2}}, "imps": null, "accounts": null}`, + }, + }, + }, + { + description: "Load imps at startup", + tests: []testStep{ + { + statusCode: httpCore.StatusOK, + response: `{"imps": {"imp1": {"value":1}}}`, + saves: `{"imps": {"imp1": {"value":1}}, "requests": null, "accounts": null}`, + }, + }, + }, + { + description: "Load requests and imps then update", + tests: []testStep{ + { + statusCode: httpCore.StatusOK, + response: `{"requests": {"request1": {"value":1}, "request2": {"value":2}}, "imps": {"imp1": {"value":3}, "imp2": {"value":4}}}`, + saves: `{"requests": {"request1": {"value":1}, "request2": {"value":2}}, "imps": {"imp1": {"value":3}, "imp2": {"value":4}}, "accounts":null}`, + }, + { + statusCode: httpCore.StatusOK, + response: `{"requests": {"request1": {"value":5}, "request2": {"deleted":true}}, "imps": {"imp1": {"deleted":true}, "imp2": {"value":6}}}`, + saves: `{"requests": {"request1": {"value":5}}, "imps": {"imp2": {"value":6}}, "accounts":null}`, + invalidations: `{"requests": ["request2"], "imps": ["imp1"], "accounts": []}`, + }, + }, + }, + { + description: "Load accounts then update", + tests: []testStep{ + { + statusCode: httpCore.StatusOK, + response: `{"accounts":{"account1":{"value":1}, "account2":{"value":2}}}`, + saves: `{"accounts":{"account1":{"value":1}, "account2":{"value":2}}, "imps": null, "requests": null}`, + }, + { + statusCode: httpCore.StatusOK, + response: `{"accounts":{"account1":{"value":5}, "account2":{"deleted": true}}}`, + saves: `{"accounts":{"account1":{"value":5}}, "imps": null, "requests": null}`, + invalidations: `{"accounts":["account2"], "requests": [], "imps": []}`, + }, + }, + }, + { + description: "Load nothing at startup", + tests: []testStep{ + { + statusCode: httpCore.StatusOK, + response: `{}`, + }, + }, + }, + { + description: "Malformed response at startup", + tests: []testStep{ + { + statusCode: httpCore.StatusOK, + response: `{some bad json`, + }, + }, + }, + { + description: "Server error at startup", + tests: []testStep{ + { + statusCode: httpCore.StatusInternalServerError, + response: ``, + }, + }, + }, + { + description: "HTTP timeout error at startup", + tests: []testStep{ + { + timeout: true, + }, + }, + }, } - server := httptest.NewServer(handler) - defer server.Close() - - ev := NewHTTPEvents(server.Client(), server.URL, nil, -1) - if len(ev.Saves()) != 0 { - t.Errorf("No updates should be emitted if the HTTP call fails. Got %d", len(ev.Saves())) + for _, tests := range testCases { + t.Run(tests.description, func(t *testing.T) { + handler := &mockResponseHandler{} + server := httptest.NewServer(handler) + defer server.Close() + + var ev *HTTPEvents + + for i, test := range tests.tests { + handler.statusCode = test.statusCode + handler.response = test.response + if i == 0 { // NewHTTPEvents() calls the API immediately + if test.timeout { + ev = NewHTTPEvents(server.Client(), server.URL, ctxProducer, -1) // force timeout + } else { + ev = NewHTTPEvents(server.Client(), server.URL, nil, -1) + } + } else { // Second test triggers API call by initiating a 1s refresh loop + timeChan := make(chan time.Time, 1) + timeChan <- time.Now() + go ev.refresh(timeChan) + } + t.Run(fmt.Sprintf("Step %d", i+1), func(t *testing.T) { + // Check expected Saves + if len(test.saves) > 0 { + saves, err := json.Marshal(<-ev.Saves()) + assert.NoError(t, err, `Failed to marshal event.Save object: %v`, err) + assert.JSONEq(t, test.saves, string(saves)) + } + assert.Empty(t, ev.Saves(), "Unexpected additional messages in save channel") + // Check expected Invalidations + if len(test.invalidations) > 0 { + invalidations, err := json.Marshal(<-ev.Invalidations()) + assert.NoError(t, err, `Failed to marshal event.Invalidation object: %v`, err) + assert.JSONEq(t, test.invalidations, string(invalidations)) + } + assert.Empty(t, ev.Invalidations(), "Unexpected additional messages in invalidations channel") + }) + } + }) } } @@ -152,38 +168,3 @@ func (m *mockResponseHandler) ServeHTTP(rw httpCore.ResponseWriter, r *httpCore. rw.WriteHeader(m.statusCode) rw.Write([]byte(m.response)) } - -func assertLen(t *testing.T, m map[string]json.RawMessage, length int) { - t.Helper() - if len(m) != length { - t.Errorf("Expected map with %d elements, but got %v", length, m) - } -} - -func assertArrLen(t *testing.T, list []string, length int) { - t.Helper() - if len(list) != length { - t.Errorf("Expected list with %d elements, but got %v", length, list) - } -} - -func assertArrContains(t *testing.T, haystack []string, needle string) { - t.Helper() - for _, elm := range haystack { - if elm == needle { - return - } - } - t.Errorf("expected element %s to be in list %v", needle, haystack) -} - -func assertHasValue(t *testing.T, m map[string]json.RawMessage, key string, val string) { - t.Helper() - if mapVal, ok := m[key]; ok { - if !bytes.Equal(mapVal, []byte(val)) { - t.Errorf("expected map[%s] to be %s, but got %s", key, val, string(mapVal)) - } - } else { - t.Errorf("map missing expected key: %s", key) - } -} diff --git a/stored_requests/events/postgres/database.go b/stored_requests/events/postgres/database.go new file mode 100644 index 00000000000..54495ce42b0 --- /dev/null +++ b/stored_requests/events/postgres/database.go @@ -0,0 +1,225 @@ +package postgres + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "net" + "time" + + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" + "github.com/PubMatic-OpenWrap/prebid-server/util/timeutil" + "github.com/golang/glog" +) + +func bytesNull() []byte { + return []byte{'n', 'u', 'l', 'l'} +} + +var storedDataTypeMetricMap = map[config.DataType]pbsmetrics.StoredDataType{ + config.RequestDataType: pbsmetrics.RequestDataType, + config.CategoryDataType: pbsmetrics.CategoryDataType, + config.VideoDataType: pbsmetrics.VideoDataType, + config.AMPRequestDataType: pbsmetrics.AMPDataType, + config.AccountDataType: pbsmetrics.AccountDataType, +} + +type PostgresEventProducerConfig struct { + DB *sql.DB + RequestType config.DataType + CacheInitQuery string + CacheInitTimeout time.Duration + CacheUpdateQuery string + CacheUpdateTimeout time.Duration + MetricsEngine pbsmetrics.MetricsEngine +} + +type PostgresEventProducer struct { + cfg PostgresEventProducerConfig + lastUpdate time.Time + invalidations chan events.Invalidation + saves chan events.Save + time timeutil.Time +} + +func NewPostgresEventProducer(cfg PostgresEventProducerConfig) (eventProducer *PostgresEventProducer) { + if cfg.DB == nil { + glog.Fatalf("The Postgres Stored %s Loader needs a database connection to work.", cfg.RequestType) + } + + return &PostgresEventProducer{ + cfg: cfg, + lastUpdate: time.Time{}, + saves: make(chan events.Save, 1), + invalidations: make(chan events.Invalidation, 1), + time: &timeutil.RealTime{}, + } +} + +func (e *PostgresEventProducer) Run() error { + if e.lastUpdate.IsZero() { + return e.fetchAll() + } + + return e.fetchDelta() +} + +func (e *PostgresEventProducer) Saves() <-chan events.Save { + return e.saves +} + +func (e *PostgresEventProducer) Invalidations() <-chan events.Invalidation { + return e.invalidations +} + +func (e *PostgresEventProducer) fetchAll() (fetchErr error) { + timeout := e.cfg.CacheInitTimeout * time.Millisecond + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + startTime := e.time.Now().UTC() + rows, err := e.cfg.DB.QueryContext(ctx, e.cfg.CacheInitQuery) + elapsedTime := time.Since(startTime) + e.recordFetchTime(elapsedTime, pbsmetrics.FetchAll) + + if err != nil { + glog.Warningf("Failed to fetch all Stored %s data from the DB: %v", e.cfg.RequestType, err) + if _, ok := err.(net.Error); ok { + e.recordError(pbsmetrics.StoredDataErrorNetwork) + } else { + e.recordError(pbsmetrics.StoredDataErrorUndefined) + } + return err + } + + defer func() { + if err := rows.Close(); err != nil { + glog.Warningf("Failed to close the Stored %s DB connection: %v", e.cfg.RequestType, err) + e.recordError(pbsmetrics.StoredDataErrorUndefined) + fetchErr = err + } + }() + if err := e.sendEvents(rows); err != nil { + glog.Warningf("Failed to load all Stored %s data from the DB: %v", e.cfg.RequestType, err) + e.recordError(pbsmetrics.StoredDataErrorUndefined) + return err + } + + e.lastUpdate = startTime + return nil +} + +func (e *PostgresEventProducer) fetchDelta() (fetchErr error) { + timeout := e.cfg.CacheUpdateTimeout * time.Millisecond + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + startTime := e.time.Now().UTC() + rows, err := e.cfg.DB.QueryContext(ctx, e.cfg.CacheUpdateQuery, e.lastUpdate) + elapsedTime := time.Since(startTime) + e.recordFetchTime(elapsedTime, pbsmetrics.FetchDelta) + + if err != nil { + glog.Warningf("Failed to fetch updated Stored %s data from the DB: %v", e.cfg.RequestType, err) + if _, ok := err.(net.Error); ok { + e.recordError(pbsmetrics.StoredDataErrorNetwork) + } else { + e.recordError(pbsmetrics.StoredDataErrorUndefined) + } + return err + } + + defer func() { + if err := rows.Close(); err != nil { + glog.Warningf("Failed to close the Stored %s DB connection: %v", e.cfg.RequestType, err) + e.recordError(pbsmetrics.StoredDataErrorUndefined) + fetchErr = err + } + }() + if err := e.sendEvents(rows); err != nil { + glog.Warningf("Failed to load updated Stored %s data from the DB: %v", e.cfg.RequestType, err) + e.recordError(pbsmetrics.StoredDataErrorUndefined) + return err + } + + e.lastUpdate = startTime + return nil +} + +func (e *PostgresEventProducer) recordFetchTime(elapsedTime time.Duration, fetchType pbsmetrics.StoredDataFetchType) { + e.cfg.MetricsEngine.RecordStoredDataFetchTime( + pbsmetrics.StoredDataLabels{ + DataType: storedDataTypeMetricMap[e.cfg.RequestType], + DataFetchType: fetchType, + }, elapsedTime) +} + +func (e *PostgresEventProducer) recordError(errorType pbsmetrics.StoredDataError) { + e.cfg.MetricsEngine.RecordStoredDataError( + pbsmetrics.StoredDataLabels{ + DataType: storedDataTypeMetricMap[e.cfg.RequestType], + Error: errorType, + }) +} + +// sendEvents reads the rows and sends notifications into the channel for any updates. +// If it returns an error, then callers can be certain that no events were sent to the channels. +func (e *PostgresEventProducer) sendEvents(rows *sql.Rows) (err error) { + storedRequestData := make(map[string]json.RawMessage) + storedImpData := make(map[string]json.RawMessage) + + var requestInvalidations []string + var impInvalidations []string + + for rows.Next() { + var id string + var data []byte + var dataType string + + // discard corrupted data so it is not saved in the cache + if err := rows.Scan(&id, &data, &dataType); err != nil { + return err + } + + switch dataType { + case "request": + if len(data) == 0 || bytes.Equal(data, bytesNull()) { + requestInvalidations = append(requestInvalidations, id) + } else { + storedRequestData[id] = data + } + case "imp": + if len(data) == 0 || bytes.Equal(data, bytesNull()) { + impInvalidations = append(impInvalidations, id) + } else { + storedImpData[id] = data + } + default: + glog.Warningf("Stored Data with id=%s has invalid type: %s. This will be ignored.", id, dataType) + } + } + + // discard corrupted data so it is not saved in the cache + if rows.Err() != nil { + return rows.Err() + } + + if len(storedRequestData) > 0 || len(storedImpData) > 0 { + e.saves <- events.Save{ + Requests: storedRequestData, + Imps: storedImpData, + } + } + + if (len(requestInvalidations) > 0 || len(impInvalidations) > 0) && !e.lastUpdate.IsZero() { + e.invalidations <- events.Invalidation{ + Requests: requestInvalidations, + Imps: impInvalidations, + } + } + + return +} diff --git a/stored_requests/events/postgres/database_test.go b/stored_requests/events/postgres/database_test.go new file mode 100644 index 00000000000..c3a9b79c7b9 --- /dev/null +++ b/stored_requests/events/postgres/database_test.go @@ -0,0 +1,444 @@ +package postgres + +import ( + "encoding/json" + "errors" + "regexp" + "testing" + "time" + + "github.com/PubMatic-OpenWrap/prebid-server/config" + "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/events" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + sqlmock "github.com/DATA-DOG/go-sqlmock" +) + +// FakeTime implements the Time interface +type FakeTime struct { + time time.Time +} + +func (mc *FakeTime) Now() time.Time { + return mc.time +} + +const fakeQuery = "SELECT id, requestData, type FROM stored_data" + +func fakeQueryRegex() string { + return "^" + regexp.QuoteMeta(fakeQuery) + "$" +} + +func TestFetchAllSuccess(t *testing.T) { + tests := []struct { + description string + giveFakeTime time.Time + giveMockRows *sqlmock.Rows + wantLastUpdate time.Time + wantSavedReqs map[string]json.RawMessage + wantSavedImps map[string]json.RawMessage + wantInvalidatedReqs []string + wantInvalidatedImps []string + }{ + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs = 0, invalidated imps = 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + }, + { + description: "saved reqs > 0, saved imps = 0, invalidated reqs = 0, invalidated imps = 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("req-1", "true", "request"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantSavedReqs: map[string]json.RawMessage{"req-1": json.RawMessage(`true`)}, + wantSavedImps: map[string]json.RawMessage{}, + }, + { + description: "saved reqs = 0, saved imps > 0, invalidated reqs = 0, invalidated imps = 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("imp-1", "true", "imp"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantSavedReqs: map[string]json.RawMessage{}, + wantSavedImps: map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)}, + }, + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs > 0, invalidated imps = 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("req-1", "", "request"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + }, + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs = 0, invalidated imps > 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("imp-1", "", "imp"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + }, + { + description: "saved reqs > 0, saved imps > 0, invalidated reqs > 0, invalidated imps > 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}). + AddRow("req-1", "true", "request"). + AddRow("imp-1", "true", "imp"). + AddRow("req-2", "", "request"). + AddRow("imp-2", "", "imp"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantSavedReqs: map[string]json.RawMessage{"req-1": json.RawMessage(`true`)}, + wantSavedImps: map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)}, + }, + } + + for _, tt := range tests { + db, dbMock, _ := sqlmock.New() + dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows) + + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{ + DataType: pbsmetrics.RequestDataType, + DataFetchType: pbsmetrics.FetchAll, + }, mock.Anything).Return() + + eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{ + DB: db, + RequestType: config.RequestDataType, + CacheInitTimeout: 100 * time.Millisecond, + CacheInitQuery: fakeQuery, + MetricsEngine: metricsMock, + }) + eventProducer.time = &FakeTime{time: tt.giveFakeTime} + err := eventProducer.Run() + + assert.Nil(t, err, tt.description) + assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description) + + var saves events.Save + // Read data from saves channel with timeout to avoid test suite deadlock + select { + case saves = <-eventProducer.Saves(): + case <-time.After(20 * time.Millisecond): + } + var invalidations events.Invalidation + // Read data from invalidations channel with timeout to avoid test suite deadlock + select { + case invalidations = <-eventProducer.Invalidations(): + case <-time.After(20 * time.Millisecond): + } + + assert.Equal(t, tt.wantSavedReqs, saves.Requests, tt.description) + assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description) + assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description) + assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description) + + metricsMock.AssertExpectations(t) + } +} + +func TestFetchAllErrors(t *testing.T) { + tests := []struct { + description string + giveFakeTime time.Time + giveTimeoutMS int + giveMockRows *sqlmock.Rows + wantRecordedError pbsmetrics.StoredDataError + wantLastUpdate time.Time + }{ + { + description: "fetch all timeout", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: nil, + wantRecordedError: pbsmetrics.StoredDataErrorNetwork, + wantLastUpdate: time.Time{}, + }, + { + description: "fetch all query error", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveTimeoutMS: 100, + giveMockRows: nil, + wantRecordedError: pbsmetrics.StoredDataErrorUndefined, + wantLastUpdate: time.Time{}, + }, + { + description: "fetch all row error", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveTimeoutMS: 100, + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}). + AddRow("stored-req-id", "true", "request"). + RowError(0, errors.New("Some row error.")), + wantRecordedError: pbsmetrics.StoredDataErrorUndefined, + wantLastUpdate: time.Time{}, + }, + } + + for _, tt := range tests { + db, dbMock, _ := sqlmock.New() + if tt.giveMockRows == nil { + dbMock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed.")) + } else { + dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows) + } + + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{ + DataType: pbsmetrics.RequestDataType, + DataFetchType: pbsmetrics.FetchAll, + }, mock.Anything).Return() + metricsMock.Mock.On("RecordStoredDataError", pbsmetrics.StoredDataLabels{ + DataType: pbsmetrics.RequestDataType, + Error: tt.wantRecordedError, + }).Return() + + eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{ + DB: db, + RequestType: config.RequestDataType, + CacheInitTimeout: time.Duration(tt.giveTimeoutMS) * time.Millisecond, + CacheInitQuery: fakeQuery, + MetricsEngine: metricsMock, + }) + eventProducer.time = &FakeTime{time: tt.giveFakeTime} + err := eventProducer.Run() + + assert.NotNil(t, err, tt.description) + assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description) + + var saves events.Save + // Read data from saves channel with timeout to avoid test suite deadlock + select { + case saves = <-eventProducer.Saves(): + case <-time.After(10 * time.Millisecond): + } + var invalidations events.Invalidation + // Read data from invalidations channel with timeout to avoid test suite deadlock + select { + case invalidations = <-eventProducer.Invalidations(): + case <-time.After(10 * time.Millisecond): + } + + assert.Nil(t, saves.Requests, tt.description) + assert.Nil(t, saves.Imps, tt.description) + assert.Nil(t, invalidations.Requests, tt.description) + assert.Nil(t, invalidations.Requests, tt.description) + + metricsMock.AssertExpectations(t) + } +} + +func TestFetchDeltaSuccess(t *testing.T) { + tests := []struct { + description string + giveFakeTime time.Time + giveMockRows *sqlmock.Rows + wantLastUpdate time.Time + wantSavedReqs map[string]json.RawMessage + wantSavedImps map[string]json.RawMessage + wantInvalidatedReqs []string + wantInvalidatedImps []string + }{ + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs = 0, invalidated imps = 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + }, + { + description: "saved reqs > 0, saved imps = 0, invalidated reqs = 0, invalidated imps = 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("req-1", "true", "request"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantSavedReqs: map[string]json.RawMessage{"req-1": json.RawMessage(`true`)}, + wantSavedImps: map[string]json.RawMessage{}, + }, + { + description: "saved reqs = 0, saved imps > 0, invalidated reqs = 0, invalidated imps = 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("imp-1", "true", "imp"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantSavedReqs: map[string]json.RawMessage{}, + wantSavedImps: map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)}, + }, + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs > 0, invalidated imps = 0, empty data", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("req-1", "", "request"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantInvalidatedReqs: []string{"req-1"}, + wantInvalidatedImps: nil, + }, + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs > 0, invalidated imps = 0, null data", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("req-1", "null", "request"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantInvalidatedReqs: []string{"req-1"}, + wantInvalidatedImps: nil, + }, + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs = 0, invalidated imps > 0, empty data", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("imp-1", "", "imp"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantInvalidatedImps: []string{"imp-1"}, + }, + { + description: "saved reqs = 0, saved imps = 0, invalidated reqs = 0, invalidated imps > 0, null data", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).AddRow("imp-1", "null", "imp"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantInvalidatedImps: []string{"imp-1"}, + }, + { + description: "saved reqs > 0, saved imps > 0, invalidated reqs > 0, invalidated imps > 0", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}). + AddRow("req-1", "true", "request"). + AddRow("imp-1", "true", "imp"). + AddRow("req-2", "", "request"). + AddRow("imp-2", "", "imp"), + wantLastUpdate: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + wantSavedReqs: map[string]json.RawMessage{"req-1": json.RawMessage(`true`)}, + wantSavedImps: map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)}, + wantInvalidatedReqs: []string{"req-2"}, + wantInvalidatedImps: []string{"imp-2"}, + }, + } + + for _, tt := range tests { + db, dbMock, _ := sqlmock.New() + dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows) + + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{ + DataType: pbsmetrics.RequestDataType, + DataFetchType: pbsmetrics.FetchDelta, + }, mock.Anything).Return() + + eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{ + DB: db, + RequestType: config.RequestDataType, + CacheUpdateTimeout: 100 * time.Millisecond, + CacheUpdateQuery: fakeQuery, + MetricsEngine: metricsMock, + }) + eventProducer.lastUpdate = time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC) + eventProducer.time = &FakeTime{time: tt.giveFakeTime} + err := eventProducer.Run() + + assert.Nil(t, err, tt.description) + assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description) + + var saves events.Save + // Read data from saves channel with timeout to avoid test suite deadlock + select { + case saves = <-eventProducer.Saves(): + case <-time.After(20 * time.Millisecond): + } + var invalidations events.Invalidation + // Read data from invalidations channel with timeout to avoid test suite deadlock + select { + case invalidations = <-eventProducer.Invalidations(): + case <-time.After(20 * time.Millisecond): + } + + assert.Equal(t, tt.wantSavedReqs, saves.Requests, tt.description) + assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description) + assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description) + assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description) + + metricsMock.AssertExpectations(t) + } +} + +func TestFetchDeltaErrors(t *testing.T) { + tests := []struct { + description string + giveFakeTime time.Time + giveTimeoutMS int + giveLastUpdate time.Time + giveMockRows *sqlmock.Rows + wantRecordedError pbsmetrics.StoredDataError + wantLastUpdate time.Time + }{ + { + description: "fetch delta timeout", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC), + giveMockRows: nil, + wantRecordedError: pbsmetrics.StoredDataErrorNetwork, + wantLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC), + }, + { + description: "fetch delta query error", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveTimeoutMS: 100, + giveLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC), + giveMockRows: nil, + wantRecordedError: pbsmetrics.StoredDataErrorUndefined, + wantLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC), + }, + { + description: "fetch delta row error", + giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC), + giveTimeoutMS: 100, + giveLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC), + giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}). + AddRow("stored-req-id", "true", "request"). + RowError(0, errors.New("Some row error.")), + wantRecordedError: pbsmetrics.StoredDataErrorUndefined, + wantLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC), + }, + } + + for _, tt := range tests { + db, dbMock, _ := sqlmock.New() + if tt.giveMockRows == nil { + dbMock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed.")) + } else { + dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows) + } + + metricsMock := &pbsmetrics.MetricsEngineMock{} + metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{ + DataType: pbsmetrics.RequestDataType, + DataFetchType: pbsmetrics.FetchDelta, + }, mock.Anything).Return() + metricsMock.Mock.On("RecordStoredDataError", pbsmetrics.StoredDataLabels{ + DataType: pbsmetrics.RequestDataType, + Error: tt.wantRecordedError, + }).Return() + + eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{ + DB: db, + RequestType: config.RequestDataType, + CacheUpdateTimeout: time.Duration(tt.giveTimeoutMS) * time.Millisecond, + CacheUpdateQuery: fakeQuery, + MetricsEngine: metricsMock, + }) + eventProducer.lastUpdate = tt.giveLastUpdate + eventProducer.time = &FakeTime{time: tt.giveFakeTime} + err := eventProducer.Run() + + assert.NotNil(t, err, tt.description) + assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description) + + var saves events.Save + // Read data from saves channel with timeout to avoid test suite deadlock + select { + case saves = <-eventProducer.Saves(): + case <-time.After(10 * time.Millisecond): + } + var invalidations events.Invalidation + // Read data from invalidations channel with timeout to avoid test suite deadlock + select { + case invalidations = <-eventProducer.Invalidations(): + case <-time.After(10 * time.Millisecond): + } + + assert.Nil(t, saves.Requests, tt.description) + assert.Nil(t, saves.Imps, tt.description) + assert.Nil(t, invalidations.Requests, tt.description) + assert.Nil(t, invalidations.Requests, tt.description) + + metricsMock.AssertExpectations(t) + } +} diff --git a/stored_requests/events/postgres/polling.go b/stored_requests/events/postgres/polling.go deleted file mode 100644 index f6d388ead70..00000000000 --- a/stored_requests/events/postgres/polling.go +++ /dev/null @@ -1,160 +0,0 @@ -package postgres - -import ( - "bytes" - "context" - "database/sql" - "encoding/json" - "time" - - "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. -// -// This object will prioritize thoroughness over efficiency. In rare cases it may produce two "update" events for -// the same DB save, but it should never "miss" a database update either. -// -// The Queries should return a ResultSet with the following columns and types: -// -// 1. id: string -// 2. data: JSON -// 3. type: string ("request" or "imp") -// -// If data is empty or the JSON "null", then the ID will be invalidated (e.g. a deletion). -// If data is not empty, it should be the Stored Request or Stored Imp data associated with the given ID. -func PollForUpdates(ctxProducer func() (ctx context.Context, canceller func()), db *sql.DB, query string, startUpdatesFrom time.Time, refreshRate time.Duration) (eventProducer *PostgresPoller) { - // If we're not given a function to produce Contexts, use the Background one. - if ctxProducer == nil { - ctxProducer = func() (ctx context.Context, canceller func()) { - return context.Background(), func() {} - } - } - if db == nil { - glog.Fatal("The Stored Request Postgres Poller needs a database connection to work.") - } - - e := &PostgresPoller{ - db: db, - ctxProducer: ctxProducer, - updateQuery: query, - lastUpdate: startUpdatesFrom, - invalidations: make(chan events.Invalidation, 1), - saves: make(chan events.Save, 1), - } - - glog.Infof("Stored Requests will be refreshed from Postgres every %f seconds with: %s", refreshRate.Seconds(), query) - - if refreshRate > 0 { - go e.refresh(time.Tick(refreshRate)) - } else { - glog.Warningf("Postgres Stored Event polling refreshRate was %d. This must be positive. No updates will occur.", refreshRate) - } - return e -} - -type PostgresPoller struct { - db *sql.DB - ctxProducer func() (ctx context.Context, canceller func()) - updateQuery string - lastUpdate time.Time - invalidations chan events.Invalidation - saves chan events.Save -} - -func (e *PostgresPoller) refresh(ticker <-chan time.Time) { - for { - select { - case thisTime := <-ticker: - // Make sure to log the time now, *before* running the query, - // so that next tick's query won't miss any new updates which were made at the same time. - // This may duplicate some updates, but safety > efficiency. - thisTimeInUTC := thisTime.UTC() - ctx, cancel := e.ctxProducer() - rows, err := e.db.QueryContext(ctx, e.updateQuery, e.lastUpdate) - if err != nil { - glog.Warningf("Failed to update Stored Request data: %v", err) - cancel() - continue - } - if err := sendEvents(rows, e.saves, e.invalidations); err != nil { - glog.Warningf("Failed to update Stored Request data: %v", err) - } else { - e.lastUpdate = thisTimeInUTC - } - if err := rows.Close(); err != nil { - glog.Warningf("Failed to close DB connection: %v", err) - } - cancel() - } - } -} - -// sendEvents reads the rows and sends notifications into the channel for any updates. -// If it returns an error, then callers can be certain that no events were sent to the channels. -func sendEvents(rows *sql.Rows, saves chan<- events.Save, invalidations chan<- events.Invalidation) (err error) { - storedRequestData := make(map[string]json.RawMessage) - storedImpData := make(map[string]json.RawMessage) - - var requestInvalidations []string - var impInvalidations []string - - for rows.Next() { - var id string - var data []byte - var dataType string - // Beware #338... we really don't want to save corrupt data - if err := rows.Scan(&id, &data, &dataType); err != nil { - return err - } - - switch dataType { - case "request": - if len(data) == 0 || bytes.Equal(data, []byte("null")) { - requestInvalidations = append(requestInvalidations, id) - } else { - storedRequestData[id] = data - } - case "imp": - if len(data) == 0 || bytes.Equal(data, []byte("null")) { - impInvalidations = append(impInvalidations, id) - } else { - storedImpData[id] = data - } - default: - glog.Warningf("Stored Data with id=%s has invalid type: %s. This will be ignored.", id, dataType) - } - } - - // Beware #338... we really don't want to save corrupt data - if rows.Err() != nil { - return rows.Err() - } - - if len(storedRequestData) > 0 || len(storedImpData) > 0 && saves != nil { - saves <- events.Save{ - Requests: storedRequestData, - Imps: storedImpData, - } - } - - // There shouldn't be any invalidations with a nil channel (a "startup" query), - // but... if there are, we certainly don't want to block forever. - if len(requestInvalidations) > 0 || len(impInvalidations) > 0 && invalidations != nil { - invalidations <- events.Invalidation{ - Requests: requestInvalidations, - Imps: impInvalidations, - } - } - - return nil -} - -func (e *PostgresPoller) Saves() <-chan events.Save { - return e.saves -} - -func (e *PostgresPoller) Invalidations() <-chan events.Invalidation { - return e.invalidations -} diff --git a/stored_requests/events/postgres/polling_test.go b/stored_requests/events/postgres/polling_test.go deleted file mode 100644 index 7dd7c325fe7..00000000000 --- a/stored_requests/events/postgres/polling_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package postgres - -import ( - "regexp" - "testing" - "time" - - sqlmock "github.com/DATA-DOG/go-sqlmock" -) - -const updateQuery = "SELECT id, requestData, type FROM stored_data" - -func updateQueryRegex() string { - return "^" + regexp.QuoteMeta(updateQuery) + "$" -} - -func TestSuccessfulUpdates(t *testing.T) { - db, mock := newMock(t) - mockRows := sqlmock.NewRows([]string{"id", "data", "dataType"}). - AddRow("stored-req-1", "true", "request"). - AddRow("stored-req-2", "null", "request"). - AddRow("stored-imp-1", `{"id":1}`, "imp"). - AddRow("stored-imp-2", `{"id":2}`, "imp"). - AddRow("stored-imp-3", "", "imp") - - updateStart := time.Now() - - mock.ExpectQuery(initialQueryRegex()).WillReturnRows(mockRows) - - evs := PollForUpdates(nil, db, updateQuery, updateStart, time.Duration(-1)) - timeChan := make(chan time.Time) - go evs.refresh(timeChan) - timeChan <- time.Now() - - save := <-evs.Saves() - assertMapLength(t, 1, save.Requests) - assertMapValue(t, save.Requests, "stored-req-1", "true") - assertMapLength(t, 2, save.Imps) - assertMapValue(t, save.Imps, "stored-imp-1", `{"id":1}`) - assertMapValue(t, save.Imps, "stored-imp-2", `{"id":2}`) - - invalidate := <-evs.Invalidations() - assertNumInvalidations(t, 1, invalidate.Requests) - assertSliceContains(t, invalidate.Requests, "stored-req-2") - assertNumInvalidations(t, 1, invalidate.Imps) - assertSliceContains(t, invalidate.Imps, "stored-imp-3") -} - -func assertNumInvalidations(t *testing.T, expected int, vals []string) { - t.Helper() - - if len(vals) != expected { - t.Errorf("Expected %d invalidations. Got: %v", expected, vals) - } -} - -func assertSliceContains(t *testing.T, haystack []string, needle string) { - t.Helper() - for _, elm := range haystack { - if elm == needle { - return - } - } - t.Errorf("expected element %s to be in list %v", needle, haystack) -} diff --git a/stored_requests/events/postgres/startup.go b/stored_requests/events/postgres/startup.go deleted file mode 100644 index c65d117e78b..00000000000 --- a/stored_requests/events/postgres/startup.go +++ /dev/null @@ -1,61 +0,0 @@ -package postgres - -import ( - "context" - "database/sql" - - "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 -// an EventProducer with a single "events.Save" object already in the channel before returning. -// -// The string query should return Rows with the following columns and types: -// -// 1. id: string -// 2. data: JSON -// 3. type: string ("request" or "imp") -// -func LoadAll(ctx context.Context, db *sql.DB, query string) (eventProducer *PostgresLoader) { - if db == nil { - glog.Fatal("The Stored Request Postgres Startup needs a database connection to work.") - } - eventProducer = &PostgresLoader{ - saves: make(chan events.Save, 1), - } - eventProducer.doFetch(ctx, db, query) - return -} - -type PostgresLoader struct { - saves chan events.Save -} - -func (loader *PostgresLoader) doFetch(ctx context.Context, db *sql.DB, query string) { - glog.Infof("Loading all Stored Requests from Postgres with: %s", query) - rows, err := db.QueryContext(ctx, query) - if err != nil { - glog.Warningf("Failed to fetch Stored Requests from Postgres on startup. The app might be a bit slow to start. Error was: %v", err) - loader.saves <- events.Save{} - return - } - defer func() { - if err := rows.Close(); err != nil { - glog.Warningf("Failed to close DB connection: %v", err) - } - }() - - if err := sendEvents(rows, loader.saves, nil); err != nil { - glog.Warningf("Failed to fetch Stored Requests from Postgres on startup. Things might be a bit slow to start: %v", err) - loader.saves <- events.Save{} - } -} - -func (e *PostgresLoader) Saves() <-chan events.Save { - return e.saves -} - -func (e *PostgresLoader) Invalidations() <-chan events.Invalidation { - return nil -} diff --git a/stored_requests/events/postgres/startup_test.go b/stored_requests/events/postgres/startup_test.go deleted file mode 100644 index d0b99412b23..00000000000 --- a/stored_requests/events/postgres/startup_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package postgres - -import ( - "bytes" - "context" - "database/sql" - "encoding/json" - "errors" - "regexp" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" -) - -func TestSuccessfulFetch(t *testing.T) { - db, mock := newMock(t) - mockRows := sqlmock.NewRows([]string{"id", "data", "dataType"}). - AddRow("stored-req-id", "true", "request"). - AddRow("stored-imp-1", `{"id":1}`, "imp"). - AddRow("stored-imp-2", `{"id":2}`, "imp") - - mock.ExpectQuery(initialQueryRegex()).WillReturnRows(mockRows) - - evs := LoadAll(context.Background(), db, initialQuery) - save := <-evs.Saves() - assertMapLength(t, 1, save.Requests) - assertMapValue(t, save.Requests, "stored-req-id", "true") - - assertMapLength(t, 2, save.Imps) - assertMapValue(t, save.Imps, "stored-imp-1", `{"id":1}`) - assertMapValue(t, save.Imps, "stored-imp-2", `{"id":2}`) - assertExpectationsMet(t, mock) -} - -// Make sure that an empty save still gets sent on the channel if the SQL query fails. -func TestQueryError(t *testing.T) { - db, mock := newMock(t) - mock.ExpectQuery(initialQueryRegex()).WillReturnError(errors.New("Query failed.")) - - evs := LoadAll(context.Background(), db, initialQuery) - save := <-evs.Saves() - assertMapLength(t, 0, save.Requests) - assertMapLength(t, 0, save.Imps) - assertExpectationsMet(t, mock) -} - -func TestRowError(t *testing.T) { - db, mock := newMock(t) - mockRows := sqlmock.NewRows([]string{"id", "data", "dataType"}). - AddRow("stored-req-id", "true", "request"). - AddRow("stored-imp-1", `{"id":1}`, "imp"). - RowError(1, errors.New("Some row error.")) - mock.ExpectQuery(initialQueryRegex()).WillReturnRows(mockRows) - - evs := LoadAll(context.Background(), db, initialQuery) - save := <-evs.Saves() - assertMapLength(t, 0, save.Requests) - assertMapLength(t, 0, save.Imps) - assertExpectationsMet(t, mock) -} - -func TestRowCloseError(t *testing.T) { - db, mock := newMock(t) - mockRows := sqlmock.NewRows([]string{"id", "data", "dataType"}). - AddRow("stored-req-id", "true", "request"). - AddRow("stored-imp-id", `{"id":1}`, "imp"). - CloseError(errors.New("Failed to close rows.")) - mock.ExpectQuery(initialQueryRegex()).WillReturnRows(mockRows) - - evs := LoadAll(context.Background(), db, initialQuery) - save := <-evs.Saves() - assertMapLength(t, 1, save.Requests) - assertMapLength(t, 1, save.Imps) - assertExpectationsMet(t, mock) -} - -func newMock(t *testing.T) (db *sql.DB, mock sqlmock.Sqlmock) { - db, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("Failed to create mock: %v", err) - } - return -} - -const initialQuery = "SELECT id, requestData, type FROM stored_data" - -func initialQueryRegex() string { - return "^" + regexp.QuoteMeta(initialQuery) + "$" -} - -type result struct { - id string - data json.RawMessage - dataType string -} - -func assertMapLength(t *testing.T, expectedLen int, theMap map[string]json.RawMessage) { - t.Helper() - if len(theMap) != expectedLen { - t.Errorf("Wrong map length. Expected %d, Got %d.", expectedLen, len(theMap)) - } -} - -func assertMapValue(t *testing.T, m map[string]json.RawMessage, key string, val string) { - t.Helper() - if mapVal, ok := m[key]; ok { - if !bytes.Equal(mapVal, []byte(val)) { - t.Errorf("expected map[%s] to be %s, but got %s", key, val, string(mapVal)) - } - } else { - t.Errorf("map missing expected key: %s", key) - } -} - -func assertExpectationsMet(t *testing.T, mock sqlmock.Sqlmock) { - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("sqlmock expectations were not met: %v", err) - } -} diff --git a/stored_requests/fetcher.go b/stored_requests/fetcher.go index d3dc44bb65b..b37de04a2ab 100644 --- a/stored_requests/fetcher.go +++ b/stored_requests/fetcher.go @@ -25,6 +25,11 @@ type Fetcher interface { FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) } +type AccountFetcher interface { + // FetchAccount fetches the host account configuration for a publisher + FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) +} + type CategoryFetcher interface { // FetchCategories fetches the ad-server/publisher specific category for the given IAB category FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) @@ -32,8 +37,9 @@ type CategoryFetcher interface { // 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) + Fetcher + AccountFetcher + CategoryFetcher } // NotFoundError is an error type to flag that an ID was not found by the Fetcher. @@ -56,7 +62,12 @@ func (e NotFoundError) Error() string { // Cache is an intermediate layer which can be used to create more complex Fetchers by composition. // Implementations must be safe for concurrent access by multiple goroutines. // To add a Cache layer in front of a Fetcher, see WithCache() -type Cache interface { +type Cache struct { + Requests CacheJSON + Imps CacheJSON + Accounts CacheJSON +} +type CacheJSON interface { // Get works much like Fetcher.FetchRequests, with a few exceptions: // // 1. Any (actionable) errors should be logged by the implementation, rather than returned. @@ -67,37 +78,33 @@ type Cache interface { // // Nil slices and empty strings are treated as "no ops". That is, a nil requestID will always produce a nil // "stored request data" in the response. - Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) + Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) // Invalidate will ensure that all values associated with the given IDs // are no longer returned by the cache until new values are saved via Update - Invalidate(ctx context.Context, requestIDs []string, impIDs []string) + Invalidate(ctx context.Context, ids []string) // Save will add or overwrite the data in the cache at the given keys - Save(ctx context.Context, requestData map[string]json.RawMessage, impData map[string]json.RawMessage) + Save(ctx context.Context, data map[string]json.RawMessage) } // ComposedCache creates an interface to treat a slice of caches as a single cache -type ComposedCache []Cache +type ComposedCache []CacheJSON // Get will attempt to Get from the caches in the order in which they are in the slice, // stopping as soon as a value is found (or when all caches have been exhausted) -func (c ComposedCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) { - requestData = make(map[string]json.RawMessage, len(requestIDs)) - impData = make(map[string]json.RawMessage, len(impIDs)) +func (c ComposedCache) Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) { + data = make(map[string]json.RawMessage, len(ids)) - remainingReqIDs := requestIDs - remainingImpIDs := impIDs + remainingIDs := ids for _, cache := range c { - cachedReqData, cachedImpData := cache.Get(ctx, remainingReqIDs, remainingImpIDs) - - requestData, remainingReqIDs = updateFromCache(requestData, remainingReqIDs, cachedReqData) - impData, remainingImpIDs = updateFromCache(impData, remainingImpIDs, cachedImpData) + cachedData := cache.Get(ctx, remainingIDs) + data, remainingIDs = updateFromCache(data, remainingIDs, cachedData) - // return if all ids filled - if len(remainingReqIDs) == 0 && len(remainingImpIDs) == 0 { - return + // finish early if all ids filled + if len(remainingIDs) == 0 { + break } } @@ -123,16 +130,16 @@ func updateFromCache(data map[string]json.RawMessage, ids []string, newData map[ } // Invalidate will propagate invalidations to all underlying caches -func (c ComposedCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { +func (c ComposedCache) Invalidate(ctx context.Context, ids []string) { for _, cache := range c { - cache.Invalidate(ctx, requestIDs, impIDs) + cache.Invalidate(ctx, ids) } } // Save will propagate saves to all underlying caches -func (c ComposedCache) Save(ctx context.Context, requestData map[string]json.RawMessage, impData map[string]json.RawMessage) { +func (c ComposedCache) Save(ctx context.Context, data map[string]json.RawMessage) { for _, cache := range c { - cache.Save(ctx, requestData, impData) + cache.Save(ctx, data) } } @@ -142,7 +149,7 @@ type fetcherWithCache struct { metricsEngine pbsmetrics.MetricsEngine } -// WithCache returns a Fetcher which uses the given Cache before delegating to the original. +// WithCache returns a Fetcher which uses the given Caches before delegating to the original. // This can be called multiple times to compose Cache layers onto the backing Fetcher, though // it is usually more desirable to first compose caches with Compose, ensuring propagation of updates // and invalidations through all cache layers. @@ -155,7 +162,9 @@ func WithCache(fetcher AllFetcher, cache Cache, metricsEngine pbsmetrics.Metrics } func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) { - requestData, impData = f.cache.Get(ctx, requestIDs, impIDs) + + requestData = f.cache.Requests.Get(ctx, requestIDs) + impData = f.cache.Imps.Get(ctx, impIDs) // Fixes #311 leftoverImps := findLeftovers(impIDs, impData) @@ -172,7 +181,8 @@ func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []strin fetcherReqData, fetcherImpData, fetcherErrs := f.fetcher.FetchRequests(ctx, leftoverReqs, leftoverImps) errs = fetcherErrs - f.cache.Save(ctx, fetcherReqData, fetcherImpData) + f.cache.Requests.Save(ctx, fetcherReqData) + f.cache.Imps.Save(ctx, fetcherImpData) requestData = mergeData(requestData, fetcherReqData) impData = mergeData(impData, fetcherImpData) @@ -181,6 +191,22 @@ func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []strin return } +func (f *fetcherWithCache) FetchAccount(ctx context.Context, accountID string) (account json.RawMessage, errs []error) { + accountData := f.cache.Accounts.Get(ctx, []string{accountID}) + // TODO: add metrics + if account, ok := accountData[accountID]; ok { + f.metricsEngine.RecordAccountCacheResult(pbsmetrics.CacheHit, 1) + return account, errs + } else { + f.metricsEngine.RecordAccountCacheResult(pbsmetrics.CacheMiss, 1) + } + account, errs = f.fetcher.FetchAccount(ctx, accountID) + if len(errs) == 0 { + f.cache.Accounts.Save(ctx, map[string]json.RawMessage{accountID: account}) + } + return account, errs +} + func (f *fetcherWithCache) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } diff --git a/stored_requests/fetcher_test.go b/stored_requests/fetcher_test.go index 2e505d35a88..6285542fd85 100644 --- a/stored_requests/fetcher_test.go +++ b/stored_requests/fetcher_test.go @@ -7,30 +7,33 @@ import ( "testing" "github.com/PubMatic-OpenWrap/prebid-server/pbsmetrics" + "github.com/PubMatic-OpenWrap/prebid-server/stored_requests/caches/nil_cache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -func setupFetcherWithCacheDeps() (*mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) { - cache := &mockCache{} +func setupFetcherWithCacheDeps() (*mockCache, *mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) { + reqCache := &mockCache{} + impCache := &mockCache{} metricsEngine := &pbsmetrics.MetricsEngineMock{} fetcher := &mockFetcher{} - afetcherWithCache := WithCache(fetcher, cache, metricsEngine) + afetcherWithCache := WithCache(fetcher, Cache{reqCache, impCache, &nil_cache.NilCache{}}, metricsEngine) - return cache, fetcher, afetcherWithCache, metricsEngine + return reqCache, impCache, fetcher, afetcherWithCache, metricsEngine } func TestPerfectCache(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"known"} reqIDs := []string{"req-id"} ctx := context.Background() - cache.On("Get", ctx, reqIDs, impIDs).Return( + reqCache.On("Get", ctx, reqIDs).Return( map[string]json.RawMessage{ "req-id": json.RawMessage(`{"req":true}`), - }, + }) + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{ "known": json.RawMessage(`{}`), }) @@ -41,7 +44,8 @@ func TestPerfectCache(t *testing.T) { reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, reqIDs, impIDs) - cache.AssertExpectations(t) + reqCache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.JSONEq(t, `{"req":true}`, string(reqData["req-id"]), "Fetch requests should fetch the right request data") @@ -50,15 +54,16 @@ func TestPerfectCache(t *testing.T) { } func TestImperfectCache(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"cached", "uncached"} ctx := context.Background() - cache.On("Get", ctx, []string(nil), impIDs).Return( - map[string]json.RawMessage{}, + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{ "cached": json.RawMessage(`true`), }) + reqCache.On("Get", ctx, []string(nil)).Return( + map[string]json.RawMessage{}) fetcher.On("FetchRequests", ctx, []string{}, []string{"uncached"}).Return( map[string]json.RawMessage{}, @@ -67,11 +72,11 @@ func TestImperfectCache(t *testing.T) { }, []error{}, ) - cache.On("Save", ctx, - map[string]json.RawMessage{}, + impCache.On("Save", ctx, map[string]json.RawMessage{ "uncached": json.RawMessage(`false`), }) + reqCache.On("Save", ctx, map[string]json.RawMessage{}) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0) metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 1) @@ -79,7 +84,7 @@ func TestImperfectCache(t *testing.T) { reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, impIDs) - cache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, reqData, 0, "Fetch requests should return nil if no request IDs were passed") @@ -89,14 +94,15 @@ func TestImperfectCache(t *testing.T) { } func TestMissingData(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"unknown"} ctx := context.Background() - cache.On("Get", ctx, []string(nil), impIDs).Return( - map[string]json.RawMessage{}, + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{}, ) + reqCache.On("Get", ctx, []string(nil)).Return( + map[string]json.RawMessage{}) fetcher.On("FetchRequests", ctx, []string{}, impIDs).Return( map[string]json.RawMessage{}, map[string]json.RawMessage{}, @@ -104,8 +110,10 @@ func TestMissingData(t *testing.T) { errors.New("Data not found"), }, ) - cache.On("Save", ctx, + impCache.On("Save", ctx, map[string]json.RawMessage{}, + ) + reqCache.On("Save", ctx, map[string]json.RawMessage{}, ) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0) @@ -115,7 +123,8 @@ func TestMissingData(t *testing.T) { reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, impIDs) - cache.AssertExpectations(t) + reqCache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, errs, 1, "FetchRequests for missing data should return an error") @@ -125,15 +134,16 @@ func TestMissingData(t *testing.T) { // Prevents #311 func TestCacheSaves(t *testing.T) { - cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() + reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps() impIDs := []string{"abc", "abc"} ctx := context.Background() - cache.On("Get", ctx, []string(nil), impIDs).Return( - map[string]json.RawMessage{}, + impCache.On("Get", ctx, impIDs).Return( map[string]json.RawMessage{ "abc": json.RawMessage(`{}`), }) + reqCache.On("Get", ctx, []string(nil)).Return( + map[string]json.RawMessage{}) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0) metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 2) @@ -141,7 +151,7 @@ func TestCacheSaves(t *testing.T) { _, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, []string{"abc", "abc"}) - cache.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, impData, 1, "FetchRequests should return data only once for duplicate requests") @@ -149,43 +159,92 @@ func TestCacheSaves(t *testing.T) { assert.Len(t, errs, 0, "FetchRequests with duplicate IDs shouldn't return an error") } +func setupAccountFetcherWithCacheDeps() (*mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) { + accCache := &mockCache{} + metricsEngine := &pbsmetrics.MetricsEngineMock{} + fetcher := &mockFetcher{} + afetcherWithCache := WithCache(fetcher, Cache{&nil_cache.NilCache{}, &nil_cache.NilCache{}, accCache}, metricsEngine) + + return accCache, fetcher, afetcherWithCache, metricsEngine +} + +func TestAccountCacheHit(t *testing.T) { + accCache, fetcher, aFetcherWithCache, metricsEngine := setupAccountFetcherWithCacheDeps() + cachedAccounts := []string{"known"} + ctx := context.Background() + + // Test read from cache + accCache.On("Get", ctx, cachedAccounts).Return( + map[string]json.RawMessage{ + "known": json.RawMessage(`true`), + }) + + metricsEngine.On("RecordAccountCacheResult", pbsmetrics.CacheHit, 1) + account, errs := aFetcherWithCache.FetchAccount(ctx, "known") + + accCache.AssertExpectations(t) + fetcher.AssertExpectations(t) + metricsEngine.AssertExpectations(t) + assert.JSONEq(t, `true`, string(account), "FetchAccount should fetch the right account data") + assert.Len(t, errs, 0, "FetchAccount shouldn't return any errors") +} + +func TestAccountCacheMiss(t *testing.T) { + accCache, fetcher, aFetcherWithCache, metricsEngine := setupAccountFetcherWithCacheDeps() + uncachedAccounts := []string{"uncached"} + uncachedAccountsData := map[string]json.RawMessage{ + "uncached": json.RawMessage(`true`), + } + ctx := context.Background() + + // Test read from cache + accCache.On("Get", ctx, uncachedAccounts).Return(map[string]json.RawMessage{}) + accCache.On("Save", ctx, uncachedAccountsData) + fetcher.On("FetchAccount", ctx, "uncached").Return(uncachedAccountsData["uncached"], []error{}) + metricsEngine.On("RecordAccountCacheResult", pbsmetrics.CacheMiss, 1) + + account, errs := aFetcherWithCache.FetchAccount(ctx, "uncached") + + accCache.AssertExpectations(t) + fetcher.AssertExpectations(t) + metricsEngine.AssertExpectations(t) + assert.JSONEq(t, `true`, string(account), "FetchAccount should fetch the right account data") + assert.Len(t, errs, 0, "FetchAccount shouldn't return any errors") +} + func TestComposedCache(t *testing.T) { c1 := &mockCache{} c2 := &mockCache{} c3 := &mockCache{} c4 := &mockCache{} - cache := ComposedCache{c1, c2, c3, c4} + impCache := &mockCache{} + cache := Cache{ + Requests: ComposedCache{c1, c2, c3, c4}, + Imps: impCache, + } metricsEngine := &pbsmetrics.MetricsEngineMock{} fetcher := &mockFetcher{} aFetcherWithCache := WithCache(fetcher, cache, metricsEngine) - impIDs := []string{"1", "2", "3"} reqIDs := []string{"1", "2", "3"} + impIDs := []string{} ctx := context.Background() - c1.On("Get", ctx, reqIDs, impIDs).Return( - map[string]json.RawMessage{ - "1": json.RawMessage(`{"id": "1"}`), - }, + c1.On("Get", ctx, reqIDs).Return( map[string]json.RawMessage{ "1": json.RawMessage(`{"id": "1"}`), }) - c2.On("Get", ctx, []string{"2", "3"}, []string{"2", "3"}).Return( - map[string]json.RawMessage{ - "2": json.RawMessage(`{"id": "2"}`), - }, + c2.On("Get", ctx, []string{"2", "3"}).Return( map[string]json.RawMessage{ "2": json.RawMessage(`{"id": "2"}`), }) - c3.On("Get", ctx, []string{"3"}, []string{"3"}).Return( - map[string]json.RawMessage{ - "3": json.RawMessage(`{"id": "3"}`), - }, + c3.On("Get", ctx, []string{"3"}).Return( map[string]json.RawMessage{ "3": json.RawMessage(`{"id": "3"}`), }) + impCache.On("Get", ctx, []string{}).Return(map[string]json.RawMessage{}) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 3) metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0) - metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 3) + metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 0) metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheMiss, 0) reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, reqIDs, impIDs) @@ -193,14 +252,12 @@ func TestComposedCache(t *testing.T) { c1.AssertExpectations(t) c2.AssertExpectations(t) c3.AssertExpectations(t) + impCache.AssertExpectations(t) fetcher.AssertExpectations(t) metricsEngine.AssertExpectations(t) assert.Len(t, reqData, len(reqIDs), "FetchRequests should be able to return all request data from a composed cache") assert.Len(t, impData, len(impIDs), "FetchRequests should be able to return all imp data from a composed cache") assert.Len(t, errs, 0, "FetchRequests shouldn't return an error when trying to use a composed cache") - assert.JSONEq(t, `{"id": "1"}`, string(impData["1"]), "FetchRequests should fetch the right imp data") - assert.JSONEq(t, `{"id": "2"}`, string(impData["2"]), "FetchRequests should fetch the right imp data") - assert.JSONEq(t, `{"id": "3"}`, string(impData["3"]), "FetchRequests should fetch the right imp data") assert.JSONEq(t, `{"id": "1"}`, string(reqData["1"]), "FetchRequests should fetch the right req data") assert.JSONEq(t, `{"id": "2"}`, string(reqData["2"]), "FetchRequests should fetch the right req data") assert.JSONEq(t, `{"id": "3"}`, string(reqData["3"]), "FetchRequests should fetch the right req data") @@ -215,6 +272,11 @@ func (f *mockFetcher) FetchRequests(ctx context.Context, requestIDs []string, im return args.Get(0).(map[string]json.RawMessage), args.Get(1).(map[string]json.RawMessage), args.Get(2).([]error) } +func (a *mockFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) { + args := a.Called(ctx, accountID) + return args.Get(0).(json.RawMessage), args.Get(1).([]error) +} + func (f *mockFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } @@ -223,15 +285,15 @@ type mockCache struct { mock.Mock } -func (c *mockCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage) { - args := c.Called(ctx, requestIDs, impIDs) - return args.Get(0).(map[string]json.RawMessage), args.Get(1).(map[string]json.RawMessage) +func (c *mockCache) Get(ctx context.Context, ids []string) map[string]json.RawMessage { + args := c.Called(ctx, ids) + return args.Get(0).(map[string]json.RawMessage) } -func (c *mockCache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) { - c.Called(ctx, storedRequests, storedImps) +func (c *mockCache) Save(ctx context.Context, data map[string]json.RawMessage) { + c.Called(ctx, data) } -func (c *mockCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) { - c.Called(ctx, requestIDs, impIDs) +func (c *mockCache) Invalidate(ctx context.Context, ids []string) { + c.Called(ctx, ids) } diff --git a/stored_requests/multifetcher.go b/stored_requests/multifetcher.go index 24cf848448c..2d08fd45337 100644 --- a/stored_requests/multifetcher.go +++ b/stored_requests/multifetcher.go @@ -36,6 +36,21 @@ func (mf MultiFetcher) FetchRequests(ctx context.Context, requestIDs []string, i return } +func (mf MultiFetcher) FetchAccount(ctx context.Context, accountID string) (account json.RawMessage, errs []error) { + for _, f := range mf { + if af, ok := f.(AccountFetcher); ok { + if account, accErrs := af.FetchAccount(ctx, accountID); len(accErrs) == 0 { + return account, nil + } else { + accErrs = dropMissingIDs(accErrs) + errs = append(errs, accErrs...) + } + } + } + errs = append(errs, NotFoundError{accountID, "Account"}) + return nil, errs +} + func (mf MultiFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { for _, f := range mf { if cf, ok := f.(CategoryFetcher); ok { diff --git a/stored_requests/multifetcher_test.go b/stored_requests/multifetcher_test.go index e703c2c9dcc..5035cfba82e 100644 --- a/stored_requests/multifetcher_test.go +++ b/stored_requests/multifetcher_test.go @@ -125,3 +125,54 @@ func TestOtherError(t *testing.T) { assert.JSONEq(t, `{"req_id": "def"}`, string(reqData["def"]), "MultiFetcher should return the right request data") assert.JSONEq(t, `{"imp_id": "imp-1"}`, string(impData["imp-1"]), "MultiFetcher should return the right imp data") } + +func TestMultiFetcherAccountFoundInFirstFetcher(t *testing.T) { + f1 := &mockFetcher{} + f2 := &mockFetcher{} + fetcher := &MultiFetcher{f1, f2} + ctx := context.Background() + + f1.On("FetchAccount", ctx, "ONE").Once().Return(json.RawMessage(`{"id": "ONE"}`), []error{}) + + account, errs := fetcher.FetchAccount(ctx, "ONE") + + f1.AssertExpectations(t) + f2.AssertNotCalled(t, "FetchAccount") + assert.Empty(t, errs) + assert.JSONEq(t, `{"id": "ONE"}`, string(account)) +} + +func TestMultiFetcherAccountFoundInSecondFetcher(t *testing.T) { + f1 := &mockFetcher{} + f2 := &mockFetcher{} + fetcher := &MultiFetcher{f1, f2} + ctx := context.Background() + + f1.On("FetchAccount", ctx, "TWO").Once().Return(json.RawMessage(``), []error{NotFoundError{"TWO", "Account"}}) + f2.On("FetchAccount", ctx, "TWO").Once().Return(json.RawMessage(`{"id": "TWO"}`), []error{}) + + account, errs := fetcher.FetchAccount(ctx, "TWO") + + f1.AssertExpectations(t) + f2.AssertExpectations(t) + assert.Empty(t, errs) + assert.JSONEq(t, `{"id": "TWO"}`, string(account)) +} + +func TestMultiFetcherAccountNotFound(t *testing.T) { + f1 := &mockFetcher{} + f2 := &mockFetcher{} + fetcher := &MultiFetcher{f1, f2} + ctx := context.Background() + + f1.On("FetchAccount", ctx, "MISSING").Once().Return(json.RawMessage(``), []error{NotFoundError{"TWO", "Account"}}) + f2.On("FetchAccount", ctx, "MISSING").Once().Return(json.RawMessage(``), []error{NotFoundError{"TWO", "Account"}}) + + account, errs := fetcher.FetchAccount(ctx, "MISSING") + + f1.AssertExpectations(t) + f2.AssertExpectations(t) + assert.Len(t, errs, 1) + assert.Nil(t, account) + assert.EqualError(t, errs[0], NotFoundError{"MISSING", "Account"}.Error()) +} diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index aaead65de33..482d7ba0286 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -5,9 +5,11 @@ import ( "text/template" ttx "github.com/PubMatic-OpenWrap/prebid-server/adapters/33across" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/acuityads" "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/adman" "github.com/PubMatic-OpenWrap/prebid-server/adapters/admixer" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adocean" "github.com/PubMatic-OpenWrap/prebid-server/adapters/adpone" @@ -15,12 +17,15 @@ import ( "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/amx" "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/colossus" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/connectad" "github.com/PubMatic-OpenWrap/prebid-server/adapters/consumable" "github.com/PubMatic-OpenWrap/prebid-server/adapters/conversant" "github.com/PubMatic-OpenWrap/prebid-server/adapters/cpmstar" @@ -34,14 +39,18 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters/grid" "github.com/PubMatic-OpenWrap/prebid-server/adapters/gumgum" "github.com/PubMatic-OpenWrap/prebid-server/adapters/improvedigital" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/invibes" "github.com/PubMatic-OpenWrap/prebid-server/adapters/ix" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/krushmedia" "github.com/PubMatic-OpenWrap/prebid-server/adapters/lifestreet" "github.com/PubMatic-OpenWrap/prebid-server/adapters/lockerdome" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/logicad" "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/nobid" "github.com/PubMatic-OpenWrap/prebid-server/adapters/openx" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pubmatic" "github.com/PubMatic-OpenWrap/prebid-server/adapters/pulsepoint" @@ -49,7 +58,9 @@ 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/smartadserver" "github.com/PubMatic-OpenWrap/prebid-server/adapters/smartrtb" + "github.com/PubMatic-OpenWrap/prebid-server/adapters/smartyads" "github.com/PubMatic-OpenWrap/prebid-server/adapters/somoaudience" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sonobi" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sovrn" @@ -80,9 +91,11 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync syncers := make(map[openrtb_ext.BidderName]usersync.Usersyncer, len(cfg.Adapters)) insertIntoMap(cfg, syncers, openrtb_ext.Bidder33Across, ttx.New33AcrossSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderAcuityAds, acuityads.NewAcuityAdsSyncer) 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.BidderAdman, adman.NewAdmanSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdmixer, admixer.NewAdmixerSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdOcean, adocean.NewAdOceanSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAdpone, adpone.NewadponeSyncer) @@ -90,11 +103,14 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync 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.BidderAMX, amx.NewAMXSyncer) 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.BidderColossus, colossus.NewColossusSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderConnectAd, connectad.NewConnectAdSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderConsumable, consumable.NewConsumableSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderConversant, conversant.NewConversantSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderCpmstar, cpmstar.NewCpmstarSyncer) @@ -109,14 +125,18 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderGrid, grid.NewGridSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderGumGum, gumgum.NewGumGumSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderImprovedigital, improvedigital.NewImprovedigitalSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderInvibes, invibes.NewInvibesSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderIx, ix.NewIxSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderKrushmedia, krushmedia.NewKrushmediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderLifestreet, lifestreet.NewLifestreetSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderLockerDome, lockerdome.NewLockerDomeSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderLogicad, logicad.NewLogicadSyncer) 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.BidderNoBid, nobid.NewNoBidSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderOpenx, openx.NewOpenxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderPubmatic, pubmatic.NewPubmaticSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderPulsepoint, pulsepoint.NewPulsepointSyncer) @@ -127,7 +147,9 @@ 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.BidderSmartadserver, smartadserver.NewSmartadserverSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderSmartRTB, smartrtb.NewSmartRTBSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderSmartyAds, smartyads.NewSmartyAdsSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderSynacormedia, synacormedia.NewSynacorMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderTelaria, telaria.NewTelariaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderTriplelift, triplelift.NewTripleliftSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 0bc2f6a458d..7833605ea76 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -15,9 +15,11 @@ func TestNewSyncerMap(t *testing.T) { cfg := &config.Configuration{ Adapters: map[string]config.Adapter{ string(openrtb_ext.Bidder33Across): syncConfig, + string(openrtb_ext.BidderAcuityAds): syncConfig, string(openrtb_ext.BidderAdform): syncConfig, string(openrtb_ext.BidderAdkernel): syncConfig, string(openrtb_ext.BidderAdkernelAdn): syncConfig, + string(openrtb_ext.BidderAdman): syncConfig, string(openrtb_ext.BidderAdmixer): syncConfig, string(openrtb_ext.BidderAdOcean): syncConfig, string(openrtb_ext.BidderAdpone): syncConfig, @@ -25,11 +27,14 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderAdtelligent): syncConfig, string(openrtb_ext.BidderAdvangelists): syncConfig, string(openrtb_ext.BidderAJA): syncConfig, + string(openrtb_ext.BidderAMX): 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.BidderColossus): syncConfig, + string(openrtb_ext.BidderConnectAd): syncConfig, string(openrtb_ext.BidderConsumable): syncConfig, string(openrtb_ext.BidderConversant): syncConfig, string(openrtb_ext.BidderCpmstar): syncConfig, @@ -44,14 +49,18 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderGrid): syncConfig, string(openrtb_ext.BidderGumGum): syncConfig, string(openrtb_ext.BidderImprovedigital): syncConfig, + string(openrtb_ext.BidderInvibes): syncConfig, string(openrtb_ext.BidderIx): syncConfig, + string(openrtb_ext.BidderKrushmedia): syncConfig, string(openrtb_ext.BidderLifestreet): syncConfig, string(openrtb_ext.BidderLockerDome): syncConfig, + string(openrtb_ext.BidderLogicad): 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.BidderNoBid): syncConfig, string(openrtb_ext.BidderOpenx): syncConfig, string(openrtb_ext.BidderPubmatic): syncConfig, string(openrtb_ext.BidderPulsepoint): syncConfig, @@ -62,7 +71,9 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderSomoaudience): syncConfig, string(openrtb_ext.BidderSonobi): syncConfig, string(openrtb_ext.BidderSovrn): syncConfig, + string(openrtb_ext.BidderSmartadserver): syncConfig, string(openrtb_ext.BidderSmartRTB): syncConfig, + string(openrtb_ext.BidderSmartyAds): syncConfig, string(openrtb_ext.BidderSynacormedia): syncConfig, string(openrtb_ext.BidderTelaria): syncConfig, string(openrtb_ext.BidderTriplelift): syncConfig, @@ -85,14 +96,19 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderAdhese: true, openrtb_ext.BidderAdoppler: true, openrtb_ext.BidderApplogy: true, + openrtb_ext.BidderInMobi: true, openrtb_ext.BidderKidoz: true, openrtb_ext.BidderKubient: true, openrtb_ext.BidderMobileFuse: true, openrtb_ext.BidderOrbidder: true, openrtb_ext.BidderPubnative: true, + openrtb_ext.BidderSilverMob: true, + openrtb_ext.BidderSmaato: true, openrtb_ext.BidderSpotX: true, openrtb_ext.BidderTappx: true, openrtb_ext.BidderYeahmobi: true, + openrtb_ext.BidderAdprime: true, + openrtb_ext.BidderBetween: true, } for bidder, config := range cfg.Adapters { diff --git a/util/httputil/httputil.go b/util/httputil/httputil.go new file mode 100644 index 00000000000..93bcca2a8c5 --- /dev/null +++ b/util/httputil/httputil.go @@ -0,0 +1,99 @@ +package httputil + +import ( + "net" + "net/http" + "strings" + + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" +) + +var ( + trueClientIP = http.CanonicalHeaderKey("True-Client-IP") + xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") + xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") + xRealIP = http.CanonicalHeaderKey("X-Real-IP") +) + +const ( + https = "https" +) + +// IsSecure determines if a http request uses https. +func IsSecure(r *http.Request) bool { + if strings.EqualFold(r.Header.Get(xForwardedProto), https) { + return true + } + + if strings.EqualFold(r.URL.Scheme, https) { + return true + } + + if r.TLS != nil { + return true + } + + return false +} + +// FindIP returns the first ip address found in the http request matching the predicate v. +func FindIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if ip, ver := findTrueClientIP(r, v); ip != nil { + return ip, ver + } + + if ip, ver := findForwardedFor(r, v); ip != nil { + return ip, ver + } + + if ip, ver := findRealIP(r, v); ip != nil { + return ip, ver + } + + if ip, ver := findRemoteAddr(r, v); ip != nil { + return ip, ver + } + + return nil, iputil.IPvUnknown +} + +func findTrueClientIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if value := r.Header.Get(trueClientIP); value != "" { + value = strings.TrimSpace(value) + if ip, ver := iputil.ParseIP(value); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + return nil, iputil.IPvUnknown +} + +func findForwardedFor(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if value := r.Header.Get(xForwardedFor); value != "" { + for _, p := range strings.Split(value, ",") { + p = strings.TrimSpace(p) + if ip, ver := iputil.ParseIP(p); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + } + return nil, iputil.IPvUnknown +} + +func findRealIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if value := r.Header.Get(xRealIP); value != "" { + value = strings.TrimSpace(value) + if ip, ver := iputil.ParseIP(value); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + return nil, iputil.IPvUnknown +} + +func findRemoteAddr(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) { + if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + if ip, ver := iputil.ParseIP(host); ip != nil && v.IsValid(ip, ver) { + return ip, ver + } + } + return nil, iputil.IPvUnknown +} diff --git a/util/httputil/httputil_test.go b/util/httputil/httputil_test.go new file mode 100644 index 00000000000..7b6a9a504f1 --- /dev/null +++ b/util/httputil/httputil_test.go @@ -0,0 +1,327 @@ +package httputil + +import ( + "crypto/tls" + "net" + "net/http" + "testing" + + "github.com/PubMatic-OpenWrap/prebid-server/util/iputil" + "github.com/stretchr/testify/assert" +) + +func TestIsSecure(t *testing.T) { + testCases := []struct { + description string + url string + xForwardedProto string + tls bool + expectIsSecure bool + }{ + { + description: "HTTP", + url: "http://host.com", + expectIsSecure: false, + }, + { + description: "HTTPS - Forwarded Protocol", + url: "http://host.com", + xForwardedProto: "https", + expectIsSecure: true, + }, + { + description: "HTTPS - Forwarded Protocol - Case Insensitive", + url: "http://host.com", + xForwardedProto: "HTTPS", + expectIsSecure: true, + }, + { + description: "HTTPS - Protocol", + url: "https://host.com", + expectIsSecure: true, + }, + { + description: "HTTPS - Protocol - Case Insensitive", + url: "HTTPS://host.com", + expectIsSecure: true, + }, + { + description: "HTTPS - TLS", + url: "http://host.com", + tls: true, + expectIsSecure: true, + }, + } + + for _, test := range testCases { + request, err := http.NewRequest("GET", test.url, nil) + if err != nil { + t.Fatalf("Unable to create test http request. Err: %v", err) + } + if test.xForwardedProto != "" { + request.Header.Add("X-Forwarded-Proto", test.xForwardedProto) + } + if test.tls { + request.TLS = &tls.ConnectionState{} + } + + result := IsSecure(request) + + assert.Equal(t, test.expectIsSecure, result, test.description) + } +} + +func TestFindIP(t *testing.T) { + alwaysTrue := hardcodedResponseIPValidator{response: true} + alwaysFalse := hardcodedResponseIPValidator{response: false} + + testCases := []struct { + description string + trueClientIP string + xForwardedFor string + xRealIP string + remoteAddr string + validator iputil.IPValidator + expectedIP net.IP + expectedVer iputil.IPVersion + }{ + { + description: "No Address", + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "False Validator - IPv4", + trueClientIP: "1.1.1.1", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysFalse, + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "False Validator - IPv6", + trueClientIP: "1111::", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5]", + validator: alwaysFalse, + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "True Validator - IPv4 - True Client IP", + trueClientIP: "1.1.1.1", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1.1.1.1"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - True Client IP - Ignore Whitespace", + trueClientIP: " 1.1.1.1 ", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1.1.1.1"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Forwarded For", + trueClientIP: "", + xForwardedFor: "2.2.2.2, 3.3.3.3", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2.2.2.2"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Forwarded For - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: " 2.2.2.2, 3.3.3.3 ", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2.2.2.2"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Real IP", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "4.4.4.4", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - X Real IP - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: "", + xRealIP: " 4.4.4.4 ", + remoteAddr: "5.5.5.5:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv4 - Remote Address", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "", + remoteAddr: "5.5.5.5:80", + validator: alwaysTrue, + expectedIP: net.ParseIP("5.5.5.5"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - IPv6 - True Client IP", + trueClientIP: "1111::", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1111::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - True Client IP - Ignore Whitespace", + trueClientIP: " 1111:: ", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("1111::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Forwarded For", + trueClientIP: "", + xForwardedFor: "2222::, 3333::", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2222::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Forwarded For - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: " 2222::, 3333:: ", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("2222::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Real IP", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "4444::", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4444::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - X Real IP - Ignore Whitespace", + trueClientIP: "", + xForwardedFor: "", + xRealIP: " 4444:: ", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("4444::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - IPv6 - Remote Address", + trueClientIP: "", + xForwardedFor: "", + xRealIP: "", + remoteAddr: "[5555::]:5", + validator: alwaysTrue, + expectedIP: net.ParseIP("5555::"), + expectedVer: iputil.IPv6, + }, + { + description: "True Validator - Malformed - All", + trueClientIP: "malformed", + xForwardedFor: "malformed", + xRealIP: "malformed", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: nil, + expectedVer: iputil.IPvUnknown, + }, + { + description: "True Validator - Malformed - Some", + trueClientIP: "malformed", + xForwardedFor: "malformed", + xRealIP: "4.4.4.4", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - Malformed - X Forwarded For - IPv4", + trueClientIP: "malformed", + xForwardedFor: "malformed, 4.4.4.4, 3333::, malformed", + xRealIP: "malformed", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: net.ParseIP("4.4.4.4"), + expectedVer: iputil.IPv4, + }, + { + description: "True Validator - Malformed - X Forwarded For - IPv6", + trueClientIP: "malformed", + xForwardedFor: "malformed, 3333::, 4.4.4.4, malformed", + xRealIP: "malformed", + remoteAddr: "malformed", + validator: alwaysTrue, + expectedIP: net.ParseIP("3333::"), + expectedVer: iputil.IPv6, + }, + } + + for _, test := range testCases { + // Build Request + request, err := http.NewRequest("GET", "http://anyurl.com", nil) + if err != nil { + t.Fatalf("Unable to create test http request. Err: %v", err) + } + if test.trueClientIP != "" { + request.Header.Add("True-Client-IP", test.trueClientIP) + } + if test.xForwardedFor != "" { + request.Header.Add("X-Forwarded-For", test.xForwardedFor) + } + if test.xRealIP != "" { + request.Header.Add("X-Real-IP", test.xRealIP) + } + request.RemoteAddr = test.remoteAddr + + // Run Test + ip, ver := FindIP(request, test.validator) + + // Assertions + assert.Equal(t, test.expectedIP, ip, test.description+":ip") + assert.Equal(t, test.expectedVer, ver, test.description+":ver") + } +} + +type hardcodedResponseIPValidator struct { + response bool +} + +func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool { + return v.response +} diff --git a/util/iputil/parse.go b/util/iputil/parse.go new file mode 100644 index 00000000000..bcb00760e22 --- /dev/null +++ b/util/iputil/parse.go @@ -0,0 +1,27 @@ +package iputil + +import ( + "net" + "strings" +) + +// IPVersion is the numerical version of the IP address spec (4 or 6). +type IPVersion int + +// IP address versions. +const ( + IPvUnknown IPVersion = 0 + IPv4 IPVersion = 4 + IPv6 IPVersion = 6 +) + +// ParseIP parses v as an ip address returning the result and version, or nil and unknown if invalid. +func ParseIP(v string) (net.IP, IPVersion) { + if ip := net.ParseIP(v); ip != nil { + if strings.ContainsRune(v, ':') { + return ip, IPv6 + } + return ip, IPv4 + } + return nil, IPvUnknown +} diff --git a/util/iputil/parse_test.go b/util/iputil/parse_test.go new file mode 100644 index 00000000000..53431b0f2a9 --- /dev/null +++ b/util/iputil/parse_test.go @@ -0,0 +1,30 @@ +package iputil + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseIP(t *testing.T) { + testCases := []struct { + input string + expectedVer IPVersion + expectedIP net.IP + }{ + {"", IPvUnknown, nil}, + {"1.1.1.1", IPv4, net.IPv4(1, 1, 1, 1)}, + {"-1.-1.-1.-1", IPvUnknown, nil}, + {"256.256.256.256", IPvUnknown, nil}, + {"::ffff:1.1.1.1", IPv6, net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 1, 1, 1, 1}}, + {"0101::", IPv6, net.IP{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, + {"zzzz::", IPvUnknown, nil}, + } + + for _, test := range testCases { + ip, ver := ParseIP(test.input) + assert.Equal(t, test.expectedVer, ver) + assert.Equal(t, test.expectedIP, ip) + } +} diff --git a/util/iputil/validator.go b/util/iputil/validator.go new file mode 100644 index 00000000000..e4b822f0c7c --- /dev/null +++ b/util/iputil/validator.go @@ -0,0 +1,48 @@ +package iputil + +import ( + "net" +) + +// IPValidator is the interface for validating an ip address and version. +type IPValidator interface { + // IsValid returns true when an IP address is determined to be valid. + IsValid(net.IP, IPVersion) bool +} + +// PublicNetworkIPValidator validates an ip address which is not contained in the list of known private networks. +type PublicNetworkIPValidator struct { + IPv4PrivateNetworks []net.IPNet + IPv6PrivateNetworks []net.IPNet +} + +// IsValid implements the IPValidator interface. +func (v PublicNetworkIPValidator) IsValid(ip net.IP, ver IPVersion) bool { + var privateNetworks []net.IPNet + switch ver { + case IPv4: + privateNetworks = v.IPv4PrivateNetworks + case IPv6: + privateNetworks = v.IPv6PrivateNetworks + default: + return false + } + + for _, ipNet := range privateNetworks { + if ipNet.Contains(ip) { + return false + } + } + + return true +} + +// VersionIPValidator validates an ip address based on the desired ip version. +type VersionIPValidator struct { + Version IPVersion +} + +// IsValid implements the IPValidator interface. +func (v VersionIPValidator) IsValid(ip net.IP, ver IPVersion) bool { + return ver == v.Version +} diff --git a/util/iputil/validator_test.go b/util/iputil/validator_test.go new file mode 100644 index 00000000000..4419af22c04 --- /dev/null +++ b/util/iputil/validator_test.go @@ -0,0 +1,222 @@ +package iputil + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPublicNetworkIPValidator(t *testing.T) { + ipv4Network1 := net.IPNet{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)} + ipv4Network2 := net.IPNet{IP: net.ParseIP("2.0.0.0"), Mask: net.CIDRMask(8, 32)} + + ipv6Network1 := net.IPNet{IP: net.ParseIP("3300::"), Mask: net.CIDRMask(8, 128)} + ipv6Network2 := net.IPNet{IP: net.ParseIP("4400::"), Mask: net.CIDRMask(8, 128)} + + testCases := []struct { + description string + ip net.IP + ver IPVersion + ipv4PrivateNetworks []net.IPNet + ipv6PrivateNetworks []net.IPNet + expected bool + }{ + { + description: "IPv4 - Public - None", + ip: net.ParseIP("1.1.1.1"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv4 - Public - One", + ip: net.ParseIP("2.2.2.2"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv4 - Public - Many", + ip: net.ParseIP("3.3.3.3"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network2}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv4 - Private - One", + ip: net.ParseIP("1.1.1.1"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: false, + }, + { + description: "IPv4 - Private - Many", + ip: net.ParseIP("2.2.2.2"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network2}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: false, + }, + { + description: "IPv6 - Public - None", + ip: net.ParseIP("3333::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{}, + expected: true, + }, + { + description: "IPv6 - Public - One", + ip: net.ParseIP("4444::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1}, + expected: true, + }, + { + description: "IPv6 - Public - Many", + ip: net.ParseIP("5555::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: true, + }, + { + description: "IPv6 - Private - One", + ip: net.ParseIP("3333::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1}, + expected: false, + }, + { + description: "IPv6 - Private - Many", + ip: net.ParseIP("4444::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Unknown", + ip: net.ParseIP("3.3.3.3"), + ver: IPvUnknown, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Public - IPv4", + ip: net.ParseIP("3.3.3.3"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: true, + }, + { + description: "Mixed - Public - IPv6", + ip: net.ParseIP("5555::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: true, + }, + { + description: "Mixed - Private - IPv4", + ip: net.ParseIP("1.1.1.1"), + ver: IPv4, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Private - IPv6", + ip: net.ParseIP("3333::"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1}, + ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2}, + expected: false, + }, + { + description: "Mixed - Public - IPv6 Encoded IPv4", + ip: net.ParseIP("::FFFF:1.1.1.1"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)}}, + ipv6PrivateNetworks: []net.IPNet{{IP: net.ParseIP("::FFFF:2.0.0.0"), Mask: net.CIDRMask(108, 128)}}, + expected: true, + }, + { + description: "Mixed - Private - IPv6 Encoded IPv4", + ip: net.ParseIP("::FFFF:2.2.2.2"), + ver: IPv6, + ipv4PrivateNetworks: []net.IPNet{{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)}}, + ipv6PrivateNetworks: []net.IPNet{{IP: net.ParseIP("::FFFF:2.0.0.0"), Mask: net.CIDRMask(108, 128)}}, + expected: false, + }, + } + + for _, test := range testCases { + requestValidation := PublicNetworkIPValidator{ + IPv4PrivateNetworks: test.ipv4PrivateNetworks, + IPv6PrivateNetworks: test.ipv6PrivateNetworks, + } + + result := requestValidation.IsValid(test.ip, test.ver) + + assert.Equal(t, test.expected, result, test.description) + } +} + +func TestVersionIPValidator(t *testing.T) { + testCases := []struct { + description string + validatorVersion IPVersion + ip net.IP + ipVer IPVersion + expected bool + }{ + { + description: "IPv4", + validatorVersion: IPv4, + ip: net.ParseIP("1.1.1.1"), + ipVer: IPv4, + expected: true, + }, + { + description: "IPv4 - Given Unknown", + validatorVersion: IPv4, + ip: nil, + ipVer: IPvUnknown, + expected: false, + }, + { + description: "IPv6", + validatorVersion: IPv6, + ip: net.ParseIP("1111::"), + ipVer: IPv6, + expected: true, + }, + { + description: "IPv6 - Given Unknown", + validatorVersion: IPv6, + ip: nil, + ipVer: IPvUnknown, + expected: false, + }, + } + + for _, test := range testCases { + m := VersionIPValidator{ + Version: test.validatorVersion, + } + + result := m.IsValid(test.ip, test.ipVer) + + assert.Equal(t, test.expected, result) + } +} diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go new file mode 100644 index 00000000000..0d1d7dbb51c --- /dev/null +++ b/util/maputil/maputil.go @@ -0,0 +1,21 @@ +package maputil + +// ReadEmbeddedMap reads element k from the map m as a map[string]interface{}. +func ReadEmbeddedMap(m map[string]interface{}, k string) (map[string]interface{}, bool) { + if v, ok := m[k]; ok { + vCasted, ok := v.(map[string]interface{}) + return vCasted, ok + } + + return nil, false +} + +// ReadEmbeddedSlice reads element k from the map m as a []interface{}. +func ReadEmbeddedSlice(m map[string]interface{}, k string) ([]interface{}, bool) { + if v, ok := m[k]; ok { + vCasted, ok := v.([]interface{}) + return vCasted, ok + } + + return nil, false +} diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go new file mode 100644 index 00000000000..2e6955cec9b --- /dev/null +++ b/util/maputil/maputil_test.go @@ -0,0 +1,113 @@ +package maputil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadEmbeddedMap(t *testing.T) { + testCases := []struct { + description string + value map[string]interface{} + key string + expectedMap map[string]interface{} + expectedOK bool + }{ + { + description: "Nil", + value: nil, + key: "", + expectedMap: nil, + expectedOK: false, + }, + { + description: "Empty", + value: map[string]interface{}{}, + key: "foo", + expectedMap: nil, + expectedOK: false, + }, + { + description: "Success", + value: map[string]interface{}{"foo": map[string]interface{}{"bar": 42}}, + key: "foo", + expectedMap: map[string]interface{}{"bar": 42}, + expectedOK: true, + }, + { + description: "Not Found", + value: map[string]interface{}{"foo": map[string]interface{}{"bar": 42}}, + key: "notFound", + expectedMap: nil, + expectedOK: false, + }, + { + description: "Wrong Type", + value: map[string]interface{}{"foo": 42}, + key: "foo", + expectedMap: nil, + expectedOK: false, + }, + } + + for _, test := range testCases { + resultMap, resultOK := ReadEmbeddedMap(test.value, test.key) + + assert.Equal(t, test.expectedMap, resultMap, test.description+":map") + assert.Equal(t, test.expectedOK, resultOK, test.description+":ok") + } +} + +func TestReadEmbeddedSlice(t *testing.T) { + testCases := []struct { + description string + value map[string]interface{} + key string + expectedSlice []interface{} + expectedOK bool + }{ + { + description: "Nil", + value: nil, + key: "", + expectedSlice: nil, + expectedOK: false, + }, + { + description: "Empty", + value: map[string]interface{}{}, + key: "foo", + expectedSlice: nil, + expectedOK: false, + }, + { + description: "Success", + value: map[string]interface{}{"foo": []interface{}{42}}, + key: "foo", + expectedSlice: []interface{}{42}, + expectedOK: true, + }, + { + description: "Not Found", + value: map[string]interface{}{"foo": []interface{}{42}}, + key: "notFound", + expectedSlice: nil, + expectedOK: false, + }, + { + description: "Wrong Type", + value: map[string]interface{}{"foo": 42}, + key: "foo", + expectedSlice: nil, + expectedOK: false, + }, + } + + for _, test := range testCases { + resultSlice, resultOK := ReadEmbeddedSlice(test.value, test.key) + + assert.Equal(t, test.expectedSlice, resultSlice, test.description+":slicd") + assert.Equal(t, test.expectedOK, resultOK, test.description+":ok") + } +} diff --git a/util/task/ticker_task.go b/util/task/ticker_task.go new file mode 100644 index 00000000000..a8d523b75d5 --- /dev/null +++ b/util/task/ticker_task.go @@ -0,0 +1,53 @@ +package task + +import ( + "time" +) + +type Runner interface { + Run() error +} + +type TickerTask struct { + interval time.Duration + runner Runner + done chan struct{} +} + +func NewTickerTask(interval time.Duration, runner Runner) *TickerTask { + return &TickerTask{ + interval: interval, + runner: runner, + done: make(chan struct{}), + } +} + +// Start runs the task immediately and then schedules the task to run periodically +// if a positive fetching interval has been specified. +func (t *TickerTask) Start() { + t.runner.Run() + + if t.interval > 0 { + go t.runRecurring() + } +} + +// Stop stops the periodic task but the task runner maintains state +func (t *TickerTask) Stop() { + close(t.done) +} + +// run creates a ticker that ticks at the specified interval. On each tick, +// the task is executed +func (t *TickerTask) runRecurring() { + ticker := time.NewTicker(t.interval) + + for { + select { + case <-ticker.C: + t.runner.Run() + case <-t.done: + return + } + } +} diff --git a/util/task/ticker_task_test.go b/util/task/ticker_task_test.go new file mode 100644 index 00000000000..92cf6835ea6 --- /dev/null +++ b/util/task/ticker_task_test.go @@ -0,0 +1,63 @@ +package task_test + +import ( + "testing" + "time" + + "github.com/PubMatic-OpenWrap/prebid-server/util/task" + "github.com/stretchr/testify/assert" +) + +type MockRunner struct { + RunCount int +} + +func (mcc *MockRunner) Run() error { + mcc.RunCount++ + return nil +} + +func TestStartWithSingleRun(t *testing.T) { + // Setup: + runner := &MockRunner{RunCount: 0} + interval := 0 * time.Millisecond + ticker := task.NewTickerTask(interval, runner) + + // Execute: + ticker.Start() + time.Sleep(10 * time.Millisecond) + + // Verify: + assert.Equal(t, runner.RunCount, 1, "runner should have run one time") +} + +func TestStartWithPeriodicRun(t *testing.T) { + // Setup: + runner := &MockRunner{RunCount: 0} + interval := 10 * time.Millisecond + ticker := task.NewTickerTask(interval, runner) + + // Execute: + ticker.Start() + time.Sleep(25 * time.Millisecond) + ticker.Stop() + + // Verify: + assert.Equal(t, runner.RunCount, 3, "runner should have run three times") +} + +func TestStop(t *testing.T) { + // Setup: + runner := &MockRunner{RunCount: 0} + interval := 10 * time.Millisecond + ticker := task.NewTickerTask(interval, runner) + + // Execute: + ticker.Start() + time.Sleep(25 * time.Millisecond) + ticker.Stop() + time.Sleep(25 * time.Millisecond) // wait in case stop failed so additional runs can happen + + // Verify: + assert.Equal(t, runner.RunCount, 3, "runner should have run three times") +} diff --git a/util/timeutil/time.go b/util/timeutil/time.go new file mode 100644 index 00000000000..e8eaae7d61f --- /dev/null +++ b/util/timeutil/time.go @@ -0,0 +1,16 @@ +package timeutil + +import ( + "time" +) + +type Time interface { + Now() time.Time +} + +// RealTime wraps the time package for testability +type RealTime struct{} + +func (c *RealTime) Now() time.Time { + return time.Now() +} From c42a7f4c9906ca911b16a1ae9991b3aa9e8c6dd1 Mon Sep 17 00:00:00 2001 From: PubMatic-OpenWrap Date: Wed, 23 Dec 2020 19:00:11 +0530 Subject: [PATCH 320/381] Prebid server upgrade to version 0.138.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add admixer adapter (#1195) * Adding copying of gdpr consent string to openrtb bid request (#1189) * Adding copying of gdpr consent string to openrtb bid request * Updated video request to use OpenRTB Video and User objects * Fixing unit test failure message * Updates from code review comments * Updating unit test initialization * Updated mimes array construction * fix conversant sync pixel (#1208) * openx adapter: forward bid response currency in openx adapter if set (#1211) it was always set to the default USD before * add ucfunnel adapter (#1192) * Update required params for TheMediaGrid adapter (#1188) * add zeroclickfraud adapter (#1207) * add zeroclickfraud adapter * fixes for PR * fix casing of Zeroclickfraud * Fix Adform's parameters regex (#1214) * Added adform info file * Added Adform adapter and bidder * Updates from master * Removed usersyncInfo from Adform adapter. Inverted Imp type check. * Removed excessive loop * Updated with the last master * Create readme file for adform * Fix Adform's parameters regex Motivation: catastrophic backtracking during regex execution Details: - https://regex101.com/r/NNQrWq/1 - string to check "url_domain:keskustelu.suomi24.fi,url_path:/matkailu/matkakohteet/aasia,layout:lg,categories:Matkailu,main_category:Matkailu" Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich * If Device.UA is not present in request body, init it with user-agent from header (#1219) * If Device.UA is not present in request body, init it with user-agent from request header if it's present * Moved User-Agent handler to parseVideoRequest func and added unit test * Minor clean up Co-authored-by: Veronika Solovei * Queued request timeout (#1217) Co-authored-by: Veronika Solovei * docs: adding currency support section (#1199) * Add ValueImpression Adapter (#1204) * Kidoz adapter (#1210) Co-authored-by: Ryan Haksi * Update auction.md (#1224) Fix type * Update auction.md (#1225) Fix typo. * Added logging to cache for video endpoint (#1220) * WIP added logging to cache for video endpoint * Updating cache call to use TTL from config * Updates from initial feedback * Log now includes HTTP headers * Fixed caching to use a new cache entry rather than appending to the VAST * Added feature where is query is set, the test flag is set in the request * Updated recorded response and handleError * Updates from code review comments * Changed recorded output to be only the debug ext * Removed extra marhal calls * Changed cache to be an endpoint dependency * Added debugLog struct to hold all debug related info * Numerous smaller changes * Further code cleanup and added unit tests for debug changes * Added missing error checks * Added unit test for error case * added VISX vendor ID for usersyncing (#1229) Co-authored-by: Aadesh Patel * First pass at phase 1 TCF 2.0 support (#1228) * First pass at phase 1 TCF 2.0 support * minor fixes * Update go-gdpr library and fix stuff * Fixes for PR comments * Updated price granularity unmarshal to accept empty values and ranges (#1230) * Update vendorID for TheMediaGrid s2s Bid Adapter (#1232) * treat 204 from FAN as a no bids response (#1233) Co-authored-by: Aadesh Patel * AMP CCPA Fix (#1187) * Update rubicon.md (#1234) * adding schain interface (#1203) * added Rewarded Video section (#1200) also edited all examples so they include the full openRTB context * nanointeractive adapter (#1213) * nanointeractive adapter * nanointeractive adapter, changes after review * nanointeractive adapter * nanointeractive adapter, changes after review * formatting * Typos Fix (#1236) * Fix Typo * Fixed More Typos * Moved hb_pc_cat_dur modification to be before caching (#1250) * replacing info@prebid.org maintainer email addrs (#1256) * aligning maintainer info (#1258) * Add kidoz bidder info (#1257) got this info from email communication with kidoz * Add Cropping of BAdv for Rubicon Adapter (#1254) * Add Cropping of BAdv for Rubicon Adapter BAdv size is limited to 50 * Fix after review Co-authored-by: Harbar Dmytro * Added metrics support to endpoint aspect (#1226) Co-authored-by: Veronika Solovei * Prebid Server adapter for Telaria (#1231) * TELARIA adapter. First Pass * Some refactoring * added the json files * fixed some tests and added the bidder info * fixed some tests and added the bidder info * added default user sync ur; * - Handling gzipped responses from our server * - more refactoring. * added the proper user sync default URL * changed the urls from dev to prod * changed up the required fields. Now AdCode in the Imp.Ext isn't required but Bid.SeatCode is required * change in the return type after decompressing * some refactoring * change in our config url * using pbs.yml to switch between our production and test URLs * setting default endpoint * - fixed the issue that was preventing telaria test cases to run. - added more test cases * - Modifications as per the changes requested by the maintainers. * Moved the seat code to imp.ext * Moved the seat code to imp.ext * Added 'Telaria: ' prefix for error messages * - Fixes for race conditions. Was modifying the original request object instead of a copy * cosmetic changes. * added params_test.go Co-authored-by: Vinay Prasad * #615 Beachfront URLs from config (#1238) * Add nil check errors when setting native asset types (#1260) * Bugfix: no bids from bidder handling (#1252) Co-authored-by: Veronika Solovei * Add missing categories to AppNexus -> IAB mapping file. (#1264) * Add missing categories to AppNexus -> IAB mapping file. * Remove entry for category 38 which was set to a primary IAB category instead of a sub-category. * Fix order of category 22 * Yieldone s2s Bid Adapter (#1242) * Added new Yieldone Bid s2s Adapter * Update endpoint for yieldone bid adapter * Fixes after review for Yieldone Bid s2s Adapter * Fix typeo in Yieldone s2s Bid Adapter * Fix: URL de sync (#1261) * populate the app ID in the FAN timeout notif url with the publisher ID (#1265) and the auction with the request ID Co-authored-by: Aadesh Patel * Added header User Agent decoding (#1268) * Added header User Agent decoding * Added header User Agent decoding: unit tests * Added header User Agent decoding: unit tests * Added check UA is encoded to avoid `+` converted to space Co-authored-by: Veronika Solovei * Ad Generation Adapter Integration. (#1253) * AdGeneration Integration. * update AdGeneration adapter. fix: some methods of the adgAdapter replace to functions. fix: unmarshal functions return a pointer. fix: header is defined once. fix: return when imps is appended * update AdGeneration Adapter. add: Added a comment in usersync. add: Added a test for parameters whose ID does not exist in params_test. change: Change to query creation by net/url. Added getRawQuery Test. fix: Changed variable names related to bidRequest. * Fix Go 1.14 Error Message Changes (#1271) * NinthDecimal Adapter (#1249) Co-authored-by: Chandra Prakash * * Add PubMatic bidder doc file (#1255) * Add app video capability to PubMatic bidder info file * Appnexus adapter: Add category mapping for government. (#1278) * Update a Freewheel mapping to Gaming category. (#1280) * Add AJA adapter (#1269) * OpenX adapter: Pass gdpr and gdpr_consent to user sync endpoint (#1282) I've also updated the test to avoid any confusion. * OpenX adapter: Enable video for app (#1281) * fix conversant sync pixel (#1284) * Add AdOcean adapter (#1273) * [ADOCEAN-20132] AdOcean adapter * [ADOCEAN-20132] AdOcean adapter - support for gdpr * [ADOCEAN-20132] AdOcean adapter - tests * [ADOCEAN-20132] AdOcean adapter - user sync * [ADOCEAN-20132] AdOcean adapter - formatting * [ADOCEAN-20132] AdOcean adapter - send uuid to emitter * [ADOCEAN-20132] adocean adapter - return nil if there is no creative * [ADOCEAN-20132] AdOcean adapter - add version parameter * [ADOCEAN-20132] AdOcean adapter - optimization * [ADOCEAN-20132] AdOcean adapter - add to syncer_test.go * [ADOCEAN-20132] AdOcean adapter - changes after review: * remove whitespaces in js code on adapter initialization instead on every request * check if request.Site is not nil * reuse newUri variable * [ADOCEAN-20132] AdOcean adapter - changes after review: * do not terminate the auction on a single faulty bid * [ADOCEAN-20132] AdOcean adapter - changes after review: * remove unnecessary input parameters check * small optimization * LunaMedia Adapter (#1285) Co-authored-by: Chandra Prakash * [Sharethrough] Add CCPA support (#1263) * Handle gzip responses from ad server correctly * Bump to version 8 * [Go Modules] Add proxy (#1079) * Add SSL cert for accessing stored request API (#1087) * [misspell] fix a misspell (#1102) * update static bidder params for rubicon video to follow the json marshalling names (#1100) * Switching yieldmo auction endpoint from http to https (#1103) * Add Datablocks Adapter (#1095) * datablocks bid adapter * ttx * add test json * add coverage * redo ttx * formatted * better error handling * additional tests and recomended fixes * Adding translatecategories flag to includebrandcategory (#1098) * Making IAB category translation optional with translatecategories boolean in request * Updating exchange unit tests to remove extra bids * Updates from code review comments * Removed comment about default TranslateCategories value * Changed translateCat to translateCategories in tests * Combined helper functions in exchange_test related to TranslateCategories * Bid floor (#1085) * Currency handling fix (#1097) * facebook adapter refactor (#1064) * Kubient adapter (#1094) * [synacormedia] Update user sync url to be https (#1115) This detail was missed while setting up the adapter, but we would like to use https for the user sync. * Remove Go 1.11 Build Target (#1109) * Set "Secure" on Same SIte cookies (#1119) * TripleliftNative Adapter (#1114) * ignore swp files * start small * start really small * add a user sync * justify * triplelift adapter * add our endpoint * fix syntax * config stuff * compiler fixes * more config * add params * making progress * make our ext more exty * start making responses * more logic * fix compilation errors * can we just nil this out? * augment our json * radically simplify our json * fix errs * infer the bid type * fix syntax * fix comilation errors * rename * fix compilation error * config stuff * simplify params * more config stuff * fixes * revert this * fix up the extension * getting closer * add a test * update config * update bidder params * add the floor here, too * add a usersync test * validation, ws, and a test * update tests * fix test * update email * why not * change email * preprocess requests * do some parsing * take care of some errors * floor is optional * ws * remove native * everything is either banner or video * this should be a float * floor to floor * fix compilation errors * add some tests * more tests * more tests * simplify * more progress * format * ws * rm * don't need this * fix test * fix test * don't ignore swap * change line back * report an error if there are no valid impressions for triplelift * check for either a Banner or Video object on the impression * more tests * mv * more tests * update triplelift end point * send native * ws * start changing tests * fix more tests * update config * add redirect to triplelift usersync * fix supplier id in triplelift_test * update tl usersync endpoint and test * fix tl supplier id in test json * update usersync test template * adjust inconsistency with test and sync url * mv * update packages * mv * mv * update * fix compilation errors * rename * rename some stuff * rename * rename * fix some compilation errors * ws * ws * add the extra info * add some extra info * add some files back * ws and such * updates * ws * fix compilation error * mv * rename * Revert "rename" This reverts commit 1b77c72e1eeee580148540fbdd880e70bf699709. * Revert "mv" This reverts commit 52a134ddfaf531fe6235e4751935d4266a36e78f. * it builds * cp a file * cp another file * fix a test * fix test * add the extra info * ws * add some logic * edit comment * it compiles * this is now public * call this * add the function * return nil * seems to be working * ws * seems to be working * ws * mv * starting to work * ws * add a new function * ws * fix tests * bug fix * update some stuff * revert * take out prints * fix up diff * fix up diff * update ws * fix * ws * omit the triplelift endppint * Revert "omit the triplelift endppint" This reverts commit 7abc3e46f0fbba39041da6fff7bb2335adc1fece. * populate the endpoint through the extinfo * ws * set disabled to be default * ws * update types * fixing tests * making progres * fix tests * fix tests * more fixes for tests * fixed tests * just use a comment * get rid of endpoint * restore endpoint * add some errors around unmarshalling * ws * ws * use the literal * ws * ws * update json * simplify * ws * restore tests * fail fast when grabbing invcode * use the right type * use a different error type * bump code coverage * add a new test * change error type * ws * break out test into its own function * JSON block that has a full data-center specific URL cache info (#1104) * Update Dockerfile and Makefile (#1099) * Add option for running tests as part of the docker image building * Update Makefile - Add ability to execute adapter specific tests - Execute targets for "all" rather than just printing the target name and usage - Remove use of non-existing "install" target from .PHONY targets - Remove "build" as a dependency for "image" * enable app requests for audience network (#1122) * [docs] fix markdown title (#1124) * Prometheus Refactor (#1108) * update default sync url (#1127) * Update sync url for BidderGrid adapter (#1120) * [SonarCloud] Legacy auction endpoint (#1017) * [currency converter] allow to deduce reverse rate (#1126) This CL allows the currency rate currency to deduce a currency rate even if not directly defined in the table but the reverse rate is present. E.q. USD => EUR is 1.0897 EUR => USD is not set Old behavior when asking rate from EUR to USD will not be found, New behavior is using the known reverse rate to deduce the rate. Rate for 2 USD will be 2 * (1 / 1.0897) * Updated handleError arguments to be pointers for video endpoint (#1128) * Updated handleError arguments to be pointers for video endpoint * Removing unneeded pointer to http.ResponseWriter * Adding units test for update to handleError * Revert changes to GetExtCacheData() made in #1104 (#1130) (#1131) * Better native request validation (#1132) * require the caller to define native assets[...].ID (#1123) * require the caller to define native assets[...].ID * Update assets-with-partial-ids.json * CCPA Phase 1: AMP Endpoint (#1125) * facebook: removed Auth-Token from header (replaced by authentication_id in the request body) (#1113) * Setuid Fix (#1121) * Update http refresh to use url builder. Fixes #1065 (#1133) * Add mapping of user.ext.eids[] for LiveIntent in Rubicon bidder (#1089) * support facebook app_secret config param (#1139) * CCPA Phase 1: Cookie Sync (#1135) * null check banner.h (#1142) * Add Pubnative Adapter (#1134) * Adding the passing of CCPA value to the bid request for video endpoint (#1143) * first draft (#1137) * CCPA Phase 2: Enforcement (#1138) * Gamoshi Adapter: Update cookie sync (#1146) * Simplify static/bidder-params/triplelift_native.json (#1152) * Added US Privacy support in TheMediaGrid server adapter (#1147) * Add TheMediaGrid server adapter * Add video support in TheMediaGrid s2s adapter * Update sync url for TheMediaGrid s2s adapter * Added CCPA support for TheMediaGrid s2s adapter * Fix sync url for TheMediaGrid adapter * CCPA User Sync Updates (#1153) * Marsmedia - add new bidder (#1118) * Add Applogy adapter (#1151) * enforce video.size_id for video imps in rubicon adapter (#1101) * Updated PubMatic endpoint to use https (#1155) * Update Example AppNexus Placement ID (#1160) * Fix Currency Converter Doesn't Output CUR (#1154) * Add custom JSON req/resp data to the analytics logging… (#1145) * Add custom JSON req/resp data to the analytics logging for the /openrtb2/video endpoint. * Add calls in unit tests to cover logging and jsonify of video object. * CCPA User Sync URL Updates (#1157) * Fixes audienceNetwork adapter ignoring banner.format sizes. (#1164) * adding yieldmo vendor id to usersync (#1166) * Add SmartRTB adapter (#1071) * Added new adapter for CPMStar ad network banners and video (#1159) * Update the Conversant sync pixel (#1161) * Add imp.ext.is_rewarded_inventory flag for rewarded video in Rubicon (#1170) * [currencies] fix GetInfo() null ref issue (#1169) This CL fixes the null ref on `RateConverter.GetInfo()` when rates are nil. Issue: #1136 * Fix triplelift User Sync (#1173) * Enhance Message For Cache Errors (#1175) * Fix PubMatic Usersync URL (#1178) Co-authored-by: pm-isha-bharti * [Synacormedia] Add tagId bidder parameter (#1165) * Remove all non-secure calls from eplanning adapter (#1179) * Expose Cache HTTP Settings (#1184) * Adding bid rejection messages to debug response (#1181) * Adds timeout notifications for Facebook (#1182) * VIS.X: added app type support (#1194) * Add Adoppler bidder support. (#1186) * Add Adoppler bidder support. * Address code review comments. Use JSON-templates for testing. * Fix misprint; Add url.PathEscape call for adunit URL parameter. * Adding support for deal prefixes (#1183) * updating default hard-coded list of certs (#1201) Co-authored-by: Shalmali Patil * add admixer adapter (#1195) * Adding copying of gdpr consent string to openrtb bid request (#1189) * Adding copying of gdpr consent string to openrtb bid request * Updated video request to use OpenRTB Video and User objects * Fixing unit test failure message * Updates from code review comments * Updating unit test initialization * Updated mimes array construction * fix conversant sync pixel (#1208) * openx adapter: forward bid response currency in openx adapter if set (#1211) it was always set to the default USD before * add ucfunnel adapter (#1192) * Update required params for TheMediaGrid adapter (#1188) * add zeroclickfraud adapter (#1207) * add zeroclickfraud adapter * fixes for PR * fix casing of Zeroclickfraud * Fix Adform's parameters regex (#1214) * Added adform info file * Added Adform adapter and bidder * Updates from master * Removed usersyncInfo from Adform adapter. Inverted Imp type check. * Removed excessive loop * Updated with the last master * Create readme file for adform * Fix Adform's parameters regex Motivation: catastrophic backtracking during regex execution Details: - https://regex101.com/r/NNQrWq/1 - string to check "url_domain:keskustelu.suomi24.fi,url_path:/matkailu/matkakohteet/aasia,layout:lg,categories:Matkailu,main_category:Matkailu" Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich * If Device.UA is not present in request body, init it with user-agent from header (#1219) * If Device.UA is not present in request body, init it with user-agent from request header if it's present * Moved User-Agent handler to parseVideoRequest func and added unit test * Minor clean up Co-authored-by: Veronika Solovei * Queued request timeout (#1217) Co-authored-by: Veronika Solovei * docs: adding currency support section (#1199) * Add ValueImpression Adapter (#1204) * Kidoz adapter (#1210) Co-authored-by: Ryan Haksi * Update auction.md (#1224) Fix type * Update auction.md (#1225) Fix typo. * Added logging to cache for video endpoint (#1220) * WIP added logging to cache for video endpoint * Updating cache call to use TTL from config * Updates from initial feedback * Log now includes HTTP headers * Fixed caching to use a new cache entry rather than appending to the VAST * Added feature where is query is set, the test flag is set in the request * Updated recorded response and handleError * Updates from code review comments * Changed recorded output to be only the debug ext * Removed extra marhal calls * Changed cache to be an endpoint dependency * Added debugLog struct to hold all debug related info * Numerous smaller changes * Further code cleanup and added unit tests for debug changes * Added missing error checks * Added unit test for error case * added VISX vendor ID for usersyncing (#1229) Co-authored-by: Aadesh Patel * First pass at phase 1 TCF 2.0 support (#1228) * First pass at phase 1 TCF 2.0 support * minor fixes * Update go-gdpr library and fix stuff * Fixes for PR comments * Updated price granularity unmarshal to accept empty values and ranges (#1230) * Update vendorID for TheMediaGrid s2s Bid Adapter (#1232) * treat 204 from FAN as a no bids response (#1233) Co-authored-by: Aadesh Patel * AMP CCPA Fix (#1187) * Update rubicon.md (#1234) * adding schain interface (#1203) * added Rewarded Video section (#1200) also edited all examples so they include the full openRTB context * nanointeractive adapter (#1213) * nanointeractive adapter * nanointeractive adapter, changes after review * nanointeractive adapter * nanointeractive adapter, changes after review * formatting * Typos Fix (#1236) * Fix Typo * Fixed More Typos * Moved hb_pc_cat_dur modification to be before caching (#1250) * Handle CCPA + enable gzip response [#169984259] * Addressing review (#273) [#169984259] * Remove custom gzip logic (#280) * Getting rid of custom gzip logic [#169984259] * Restore prod ad server url [#169984259] Co-authored-by: Benjamin Co-authored-by: guscarreon Co-authored-by: Aadesh Co-authored-by: Winston-Yieldmo <46379634+Winston-Yieldmo@users.noreply.github.com> Co-authored-by: htang555 Co-authored-by: Cameron Rice <37162584+camrice@users.noreply.github.com> Co-authored-by: ah-tappx <46002207+ah-tappx@users.noreply.github.com> Co-authored-by: hhhjort <31041505+hhhjort@users.noreply.github.com> Co-authored-by: Marsel Co-authored-by: Corey Kress Co-authored-by: Scott Kay Co-authored-by: Kevin Kerr Co-authored-by: Mansi Nahar Co-authored-by: Benjamin Co-authored-by: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Co-authored-by: Austin Bischoff Co-authored-by: rpanchyk Co-authored-by: Florian Hartwig Co-authored-by: Salomon Rada Co-authored-by: vladi-mmg Co-authored-by: Aleksei Lin Co-authored-by: PubMatic-OpenWrap Co-authored-by: jmaynardxandr <46759873+jmaynardxandr@users.noreply.github.com> Co-authored-by: evanmsmrtb Co-authored-by: CPMStar Co-authored-by: johnwier <49074029+johnwier@users.noreply.github.com> Co-authored-by: pm-isha-bharti Co-authored-by: Seba Perez Co-authored-by: Michael Kuryshev Co-authored-by: Viacheslav Chimishuk Co-authored-by: Shalmali Patil Co-authored-by: DmitryStashkevich <34479135+DmitryStashkevich@users.noreply.github.com> Co-authored-by: vstatkevich Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich Co-authored-by: Veronika Solovei Co-authored-by: Veronika Solovei Co-authored-by: bretg Co-authored-by: thuyhq <61451682+thuyhq@users.noreply.github.com> Co-authored-by: rhaksi-kidoz <61601767+rhaksi-kidoz@users.noreply.github.com> Co-authored-by: Ryan Haksi Co-authored-by: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Co-authored-by: Aadesh Patel Co-authored-by: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> * Remove Outdated GDPR AMP Special Case (#1283) * Stricter Privacy Scrubbing (#1286) * Stricter Privacy Scrubbing * Update Unit Test Style * Fixed Whitespace * Add Adapter Orbidder (#1275) Co-authored-by: Volk, Rainer Co-authored-by: RainerVolk4014 <53347752+RainerVolk4014@users.noreply.github.com> Co-authored-by: rvolk <> Co-authored-by: Hendrik Iseke Co-authored-by: hendrikiseke1979 <53309111+hendrikiseke1979@users.noreply.github.com> * Added OpenX Bidder adapter documentation (#1291) * OpenX adapter: Pass rewarded video flag (#1290) * Bugfix for missing fields in imp.video (#1297) Co-authored-by: Veronika Solovei * Add cpmOverride (#1289) * Add cpmOverride Enabled `request.ext.rubicon.debug.cpmOverride` and `request.imp[].ext.rubicon.debug.cpmOverride` processing. Updates tests * Remove unnecessary error checks and add shallow copy * Fixed same pointer * Add Beintoo adapter (#1274) * Add Beintoo adapter * Yeahmobi adapter (#1279) Co-authored-by: junping.zhao * advangelists: Vendor id update (#1307) Co-authored-by: Chandra Prakash * Consumable: Support GDPR and US Privacy consent (#1300) * Restore the AMP privacy exception as an option. (#1311) * Restore the AMP privacy exception as an option. * Adds missing test case * More PR feedback * Remove unused constant * Comment tweak * consumable: Correct GDPR vendor ID to 591. (#1309) fixes #1299. * VIS.X: fix bid.ID, bid.CrID and set default currency value (#1296) * Fix debug log error messages (#1270) * Fixing missing error messages for debug logging * Updated formatting of debug log message * Updated unit tests for debug log to have test flag enabled * Cleaned up debug log implementation * Updates from review comments * Cleaned up field and function names * Added replacer for <> characters * Added cache string unit test * Moved regex from function to struct field * Moved debug regex to endpoint deps * Moving regex initialization to NewVideoEndpoint * MobileFuse Adapter (#1303) Co-authored-by: Dan Barnett * eplanning: Support for apps (#1306) * Introduce Adhese adapter (#1292) Co-authored-by: Mateusz * privacy: Potential JSON injection (#1304) * Updating bidder params for Advangelists (#1316) * Updating placement info on bidder params Co-authored-by: Chandra Prakash * Change placement of cpmoverride for Rubicon (#1310) * increasing the stale period to 2 months (#1305) * Add Go 1.14 Build Target (#1314) * Privacy: Remove user.ext.eids (#1294) * Privacy: Remove user.ext.eids * Extract To A Method * Minor Refactor + More Tests * Performance Tweak * Removed some redundant methods (#1320) * TELARIA adapter. First Pass * Some refactoring * added the json files * fixed some tests and added the bidder info * fixed some tests and added the bidder info * added default user sync ur; * - Handling gzipped responses from our server * - more refactoring. * added the proper user sync default URL * changed the urls from dev to prod * changed up the required fields. Now AdCode in the Imp.Ext isn't required but Bid.SeatCode is required * change in the return type after decompressing * some refactoring * change in our config url * using pbs.yml to switch between our production and test URLs * setting default endpoint * - fixed the issue that was preventing telaria test cases to run. - added more test cases * - Modifications as per the changes requested by the maintainers. * Moved the seat code to imp.ext * Moved the seat code to imp.ext * Added 'Telaria: ' prefix for error messages * - Fixes for race conditions. Was modifying the original request object instead of a copy * cosmetic changes. * added params_test.go * Removed some redundant methods. * Removed a comment Co-authored-by: Vinay Prasad * Beachfront: GDPR id (issue 1301) and documentation updates (#1321) * Defined cookie sync URL in config, cleared deprecated comment in usersync * Update beachfront.md * editing documentation * updated gdpr id - issue 1301 * Add Yieldlab Adapter (#1287) Co-authored-by: Mirko Feddern Signed-off-by: Alex Klinkert Co-authored-by: Alexander Pinnecke Co-authored-by: Alex Klinkert Co-authored-by: Mirko Feddern * Update adtelligent ortb endpoint (#1318) * Change on eplanning endpoint (#1327) * Enable full TCF2 support (#1302) * New config options * Enble TCF2 fields and logic * Resolves some PR comments * More tests * gofmt * Added enforcement tests for split GDPR/GDPRGeo * Testing tweaks * No longer ignore enforce purpose 1 on allowSync() * Removes Purpose 4 * Change on eplanning endpoint (hostname) (#1328) * Districtm Dmx: new adapter (#1209) Co-authored-by: steve-a-districtm * Fix sync url for Yieldone s2s Bid Adapter (#1336) * Fix typo in Yieldone sync url * CCPA Video Bug (#1333) * Add Pubnative bidder documentation (#1340) * Timeout notification monitoring and debugging (#1322) * Add Adtarget server adapter (#1319) * Add Adtarget server adapter * Suggested changes for Adtarget * Update Auction OpenRTB Sample (#1342) * Update Auction OpenRTB Sample * Removed Extra "Or" * Triplelift: Add SRA Support (#1347) * Privacy: Limit Ad Tracking (#1334) * Avoid overriding AMP request original size with mutli-size (#1352) * Extra logging for timeout notifications (#1349) * Consumable: Correct bid type, should always be "banner". (#1359) * Build With Go 1.14 (#1350) * Category mapping changes from product team. (#1348) * Adds Avocet adapter (#1354) * AdOcean adapter - Support for sizes defined in prebid configuration. (#1339) support for multiple sizes bump version to 1.1.0 * Log account id and all bidder names when recovering from OpenRTB auction bidder… (#1358) * Adding Smartadserver adapter (#1346) Co-authored-by: tadam * Added additional Ext Param (#1357) Co-authored-by: Vinay Prasad * Adman adapter (#1356) Co-authored-by: Aiholkin * PBS-632 add max connections per host config setting to general http a… (#1366) * Add ext.bidder.zoneid for Kubient adapater (#1367) * Add ext.bidder.zoneid for Kubient adapater * Check the number of Imps. zoneid is optional. * Improved IPv6 Support + Private Network Filtering (#1362) * Change endpont address (#1370) * Adman adapter * add adman line to syner test * add tests * fix issues * fix web banner test * add 404 banner * fmt * rase coverage * del redundant files * change endpont address * change config endpoint Co-authored-by: Aiholkin * Don't override test parameter (#1373) * OpenX + Facebook Hardening (#1368) * Updating Conversant endpoint url (#1376) * Metrics for TCF 2 adoption (#1360) * Fall back to constant rates when the currency rates endpoint i… (#1364) * TheMediaGrid: added app type support (#1377) * user.ext.eids support in adform adapter (#1381) * Add Logicad adapter (#1382) * Fix Previous Merge Conflict (#1392) * Kubient: Change default endpont address (#1398) * Add support for multiple root schain nodes (#1374) * Update endpoint for latest release by districtm (#1401) Co-authored-by: steve-a-districtm * Set OpenRTB DNT From HTTP Header (#1397) * Add video for InApp support (#1399) * Timeout fix (#1390) * Privacy Request Metrics (#1400) * Privacy Request Metrics * Fix Bug + Add Unit Tests * Fixed Tests * Fix Typo * Parse Site.Publisher.ID from Amp Auction HTTP Req Query Parameter "account" (#1403) * Facebook Only Supports App Impressions (#1396) * fix: Change currency of ad-generation's bidResponse according to bidRequest (#1383) * Adding primary categories to freewheel mapping (#1407) * Add Outgoing Connection Metrics (#1343) * Pubmatic: Support for video duration and primary category (#1384) * Adding suport for video duration and primary category in pubmatic adapter * Adding code review changes for PR-1384 * Adding changes for syntaxNode suggestion Co-authored-by: Isha Bharti * Add IPv6 Non-Public Network (#1417) * GumGum: adds support for video (#1408) * OpenX adapter: pass optional platform (PBID-598) (#1421) * Adds keyvalue hb_format support (#1414) * feat: Add new logger module - Pubstack Analytics Module (#1331) * Pubstack Analytics V1 (#11) * V1 Pubstack (#7) * feat: Add Pubstack Logger (#6) * first version of pubstack analytics * bypass viperconfig * commit #1 * gofmt * update configuration and make the tests pass * add readme on how to configure the adapter and update the network calls * update logging and fix intake url definition * feat: Pubstack Analytics Connector * fixing go mod * fix: bad behaviour on appending path to auction url * add buffering * support bootstyrap like configuration * implement route for all the objects * supports termination signal handling for goroutines * move readme to the correct location * wording * enable configuration reload + add tests * fix logs messages * fix tests * fix log line * conclude merge * merge * update go mod Co-authored-by: Amaury Ravanel * fix duplicated channel keys Co-authored-by: Amaury Ravanel * first pass - PR reviews * rename channel* -> eventChannel * dead code * Review (#10) * use json.Decoder * update documentation * use nil instead []byte("") * clean code * do not use http.DefaultClient * fix race condition (need validation) * separate the sender and buffer logics * refactor the default configuration * remove error counter * Review GP + AR * updating default config * add more logs * remove alias fields in json * fix json serializer * close event channels Co-authored-by: Amaury Ravanel * fix race condition * first pass (pr reviews) * refactor: store enabled modules into a dedicated struct * stop goroutine * test: improve coverage * PR Review * Revert "refactor: store enabled modules into a dedicated struct" This reverts commit f57d9d61680c74244effc39a5d96d6cbb2f19f7d. # Conflicts: # analytics/config/config_test.go Co-authored-by: Amaury Ravanel * New bid adapter for Smaato (#1413) Co-authored-by: vikram Co-authored-by: Stephan * New Adprime adapter (#1418) Co-authored-by: Aiholkin * Separate "debug" behavior from "billable" behavior (#1387) * Remove redundad struct (#1432) * Tcf2 id support (#1420) * Default TCF1 GVL in anticipation of IAB no longer hosting the v1 GVL (#1433) * update to the latest go-gdpr release (#1436) * Video endpoint bid selection enhancements (#1419) Co-authored-by: Veronika Solovei * [WIP] Bid deduplication enhancement (#1430) Co-authored-by: Veronika Solovei * Refactor rate converter separating scheduler from converter logic to improve testability (#1394) * Fix TCF1 Fetcher Fallback (#1438) * Eplanning adapter: Get domain from page (#1434) * Fix no bid debug log (#1375) * Update the fallback GVL to last version (#1440) * Enable geo activation of GDPR flag (#1427) * Validate External Cache Host (#1422) * first draft * Little tweaks * Scott's review part 1 * Scott's review corrections part 2 * Scotts refactor * correction in config_test.go * Correction and refactor * Multiple return statements * Test case refactor Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon * Fixes bug (#1448) * Fixes bug * shortens list * Added adpod_id to request extension (#1444) * Added adpod_id to request -> ext -> appnexus and modified requests splitting based on pod * Unit test fix * Unit test fix * Minor unit test fixes * Code refactoring * Minor code and unit tests refactoring * Unit tests refactoring Co-authored-by: Veronika Solovei * Adform adapter: additional targeting params added (#1424) * Fix minor error message spelling mistake "vastml" -> "vastxml" (#1455) * Fixing comment for usage of deal priority field (#1451) * moving docs to website repo (#1443) * Fix bid dedup (#1456) Co-authored-by: Veronika Solovei * consumable: Correct width and height reported in response. (#1459) Prebid Server now responds with the width and height specified in the Bid Response from Consumable. Previously it would reuse the width and height specified in the Bid Request. That older behaviour was ported from an older version of the prebid.js adapter but is no longer valid. * Panics happen when left with zero length []Imp (#1462) * Add Scheme Option To External Cache URL (#1460) * Update gamma adapter (#1447) * Gamma SSP Adapter * Add Gamma SSP server adapter * increase coverage * Fix conflict with base master * Add check MediaType for Imp * Implement Multi Imps request * Changes requested * remove bad-request * increase coverage * Remove duplicate test file * Update gamma.go * Update gamma.go * Update gamma.go * Update config.go Remove Gamma User Sync Url from config * Gamma SSP Adapter * Add Gamma SSP server adapter * increase coverage * Fix conflict with base master * Add check MediaType for Imp * Implement Multi Imps request * Changes requested * remove bad-request * increase coverage * Remove duplicate test file * Update gamma.go * Update gamma.go * update gamma adapter * return nil when have No-Bid Signaling * add missing-adm.json * discard the bid that's missing adm * discard the bid that's missing adm * escape vast instead of encoded it * expand test coverage Co-authored-by: Easy Life * fix: avoid unexpected EOF on gz writer (#1449) * Smaato adapter: support for video mediaType (#1463) Co-authored-by: vikram * Rubicon liveramp param (#1466) Add liveramp mapping to user.ext should translate the "liveramp.com" id from the "user.ext.eids" array to "user.ext.liveramp_idl" as follows: ``` { "user": { "ext": { "eids": [{ "source": 'liveramp.com', "uids": [{ "id": "T7JiRRvsRAmh88" }] }] } } } ``` to XAPI: ``` { "user": { "ext": { "liveramp_idl": "T7JiRRvsRAmh88" } } } ``` * Consolidate StoredRequest configs, add validation for all data types (#1453) * Fix Test TestEventChannel_OutputFormat (#1468) * Add ability to randomly generate source.TID if empty and set publisher.ID to resolved account ID (#1439) * Add support for Account configuration (PBID-727, #1395) (#1426) * Minor changes to accounts test coverage (#1475) * Brightroll adapter - adding config support (#1461) * Refactor TCF 1/2 Vendor List Fetcher Tests (#1441) * Add validation checker for PRs and merges with github actions (#1476) * Cache refactor (#1431) Reason: Cache has Fetcher-like functionality to handle both requests and imps at a time. Internally, it still uses two caches configured and searched separately, causing some code repetition. Reusing this code to cache other objects like accounts is not easy. Keeping the req/imp repetition in fetcher and out of cache allows for a reusable simpler cache, preserving existing fetcher functionality. Changes in this set: Cache is now a simple generic id->RawMessage store fetcherWithCache handles the separate req and imp caches ComposedCache handles single caches - but it does not appear to be used Removed cache overlap tests since they do not apply now Slightly less code * Pass Through First Party Context Data (#1479) * Added new size 640x360 (Id: 198) (#1490) * Refactor: move getAccount to accounts package (from openrtb2) (#1483) * Fixed TCF2 Geo Only Enforcement (#1492) * New colossus adapter [Clean branch] (#1495) Co-authored-by: Aiholkin * New: InMobi Prebid Server Adapter (#1489) * Adding InMobi adapter * code review feedback, also explicitly working with Imp[0], as we don't support multiple impressions * less tolerant bidder params due to sneaky 1.13 -> 1.14+ change * Revert "Added new size 640x360 (Id: 198) (#1490)" (#1501) This reverts commit fa23f5c226df99a9a4ef318100fdb7d84d3e40fa. * CCPA Publisher No Sale Relationships (#1465) * Fix Merge Conflict (#1502) * Update conversant adapter for new prebid-server interface (#1484) * Implement returnCreative (#1493) * Working solution * clean-up * Test copy/paste error Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon * ConnectAd S2S Adapter (#1505) * between adapter (#1437) Co-authored-by: Alexey Elymanov * Invibes adapter (#1469) Co-authored-by: aurel.vasile * Refactor postgres event producer so it will run either the full or de… (#1485) * Refactor postgres event producer so it will run either the full or delta query periodically * Minor cleanup, follow golang conventions, declare const slice, add test cases * Remove comments * Bidder Uniqueness Gatekeeping Test (#1506) * ucfunnel adapter update end point (#1511) * Refactor EEAC map to be more in line with the nonstandard publisher map (#1514) * Added bunch of new sizes (#1516) * New krushmedia bid adapter (#1504) * Invibes: Generic domainId parameter (#1512) * Smarty ads adapter (#1500) Co-authored-by: Kushneryk Pavlo Co-authored-by: user * Add vscode remote container development files (#1481) * First commit (#1510) Co-authored-by: Gus Carreon * Vtrack and event endpoints (#1467) * Rework pubstack module tests to remove race conditions (#1522) * Rework pubstack module tests to remove race conditions * PR feedback * Remove event count and add helper methods to assert events received on channel * Updating smartadserver endpoint configuration. (#1531) Co-authored-by: tadam * Add new size 500x1000 (ID: 548) (#1536) * Fix missing Request parameter for Adgeneration Adapter (#1525) * Fix endpoint url for TheMediaGrid Bid Adapter (#1541) * Add Account cache (#1519) * Add bidder name key support (#1496) * Simplifying exchange module: bidResponseExt gets built anyway (#1518) * first draft * Scott's feedback * stepping out real quick * add cache errors to bidResponseExt before marshalling * Removed vim's swp file Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon * Correct GetCpmStringValue's second return value (#1520) * Add metrics to capture stored data fetch all/delta durations with fetch status (#1515) * Adds preferDeals support (#1528) * Emxd 3336 add app video ctv (#1529) * Adapter changes for app and video support * adding ctv devicetype test case * Adding whitespace * Updates based on feedback from Prebid team * protocol bug fix and testing * Modifying test cases to accomodate new imp.ext field * bidtype bug fix and additonal testcase for storeUrl Co-authored-by: Rakesh Balakrishnan Co-authored-by: Dan Bogdan * Add http api for fetching accounts (#1545) * Add missing postgres cache init config validation * Acuity ads adapter (#1537) Co-authored-by: Kushneryk Pavlo * Yieldmo app support in yaml file (#1542) Co-authored-by: Winston * Add metrics for account cache (#1543) * [Invibes] remove user sync for invibes (#1550) * [invibes] new bidder stub * [invibes] make request * [invibes] bid request parameters * [invibes] fix errors, add tests * [invibes] new version of MakeBids * cleaning code * [invibes] production urls, isamp flag * [invibes] fix parameters * [invibes] new test parameter * [invibes] change maintainer email * [invibes] PR fixes * [invibes] fix parameters test * [invibes] refactor endpoint template and bidVersion * [Invibes] fix tests * [invibes] resolve PR * [invibes] fix test * [invibes] fix test * [invibes] generic domainId parameter * [invibes] remove invibes cookie sync * [Invibes] comment missing Usersync Co-authored-by: aurel.vasile * Add Support For imp.ext.prebid For DealTiers (#1539) * Add Support For imp.ext.prebid For DealTiers * Remove Normalization * Add Accounts to http cache events (#1553) * Fix JSON tests ignore expected message field (#1450) * NoBid version 1.0. Initial commit. (#1547) Co-authored-by: Reda Guermas * Added dealTierSatisfied parameters in exchange.pbsOrtbBid and openrtb_ext.ExtBidPrebid and dealPriority in openrtb_ext.ExtBidPrebid (#1558) Co-authored-by: Shriprasad * Add client/AccountID support into Adoppler adapter. (#1535) * Optionally read IFA value and add it the the request url (Adhese) (#1563) * Add AMX RTB adapter (#1549) * update Datablocks usersync.go (#1572) * 33Across: Add video support in adapter (#1557) * SilverMob adapter (#1561) * SilverMob adapter * Fixes andchanges according to notes in PR * Remaining fixes: multibids, expectedMakeRequestsErrors * removed log * removed log * Multi-bid test * Removed unnesesary block Co-authored-by: Anton Nikityuk * Updated ePlanning GVL ID (#1574) * update adpone google vendor id (#1577) * ADtelligent gvlid (#1581) * Add account/ host GDPR enabled flags & account per request type GDPR enabled flags (#1564) * Add account level request type specific and general GDPR enabled flags * Clean up test TestAccountLevelGDPREnabled * Add host-level GDPR enabled flag * Move account GDPR enable check as receiver method on accountGDPR * Remove mapstructure annotations on account structs * Minor test updates * Re-add mapstructure annotations on account structs * Change RequestType to IntegrationType and struct annotation formatting * Update comment * Update account IntegrationType comments * Remove extra space in config/accounts.go via gofmt * DMX Bidfloor fix (#1579) * adform bidder video bid response support (#1573) * Fix Beachfront JSON tests (#1578) * Add account CCPA enabled and per-request-type enabled flags (#1566) * Add account level request-type-specific and general CCPA enabled flags * Remove mapstructure annotations on CCPA account structs and clean up CCPA tests * Adjust account/host CCPA enabled flag logic to incorporate feedback on similar GDPR feature * Add shared privacy policy account integration data structure * Refactor EnabledForIntegrationType methods on account privacy objects * Minor test refactor * Simplify logic in EnabledForIntegrationType methods * Refactored HoldAuction Arguments (#1570) * Fix bug in request.imp.ext Validation (#1575) * First draft * Brian's reivew * Removed leftover comments Co-authored-by: Gus Carreon * Updating import statements for v0.138.0 upgrade * UOE-5690: Fixing merging issues * UOE-5690 Fixing merging issues * prebid-server v0.138 upgrade: fixing merging issue * Prebid-upgrade Fixing test cases * Prebid-server upgrade: removing unwanted files * Prebid upgrade: Fixing merging issue with ci Co-authored-by: DmitryStashkevich <34479135+DmitryStashkevich@users.noreply.github.com> Co-authored-by: Cameron Rice <37162584+camrice@users.noreply.github.com> Co-authored-by: johnwier <49074029+johnwier@users.noreply.github.com> Co-authored-by: Scott Kay Co-authored-by: guscarreon Co-authored-by: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Co-authored-by: htang555 Co-authored-by: vstatkevich Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich Co-authored-by: Veronika Solovei Co-authored-by: Veronika Solovei Co-authored-by: bretg Co-authored-by: thuyhq <61451682+thuyhq@users.noreply.github.com> Co-authored-by: rhaksi-kidoz <61601767+rhaksi-kidoz@users.noreply.github.com> Co-authored-by: Ryan Haksi Co-authored-by: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Co-authored-by: Aadesh Co-authored-by: Aadesh Patel Co-authored-by: hhhjort <31041505+hhhjort@users.noreply.github.com> Co-authored-by: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> Co-authored-by: Dmitriy Co-authored-by: Harbar Dmytro Co-authored-by: Telaria Engineering <36203956+telariaEng@users.noreply.github.com> Co-authored-by: Vinay Prasad Co-authored-by: Krzysztof Desput Co-authored-by: Mansi Nahar Co-authored-by: jmaynardxandr <46759873+jmaynardxandr@users.noreply.github.com> Co-authored-by: hbanalytics <55453525+hbanalytics@users.noreply.github.com> Co-authored-by: chino117 Co-authored-by: Ad Generation Co-authored-by: trchandraprakash <47793448+trchandraprakash@users.noreply.github.com> Co-authored-by: Chandra Prakash Co-authored-by: Mike Chowla Co-authored-by: Taiki Sakamoto Co-authored-by: Laurentiu Badea Co-authored-by: Marcin Muras <47107445+mmuras@users.noreply.github.com> Co-authored-by: Mathieu Pheulpin Co-authored-by: Benjamin Co-authored-by: Winston-Yieldmo <46379634+Winston-Yieldmo@users.noreply.github.com> Co-authored-by: ah-tappx <46002207+ah-tappx@users.noreply.github.com> Co-authored-by: Marsel Co-authored-by: Corey Kress Co-authored-by: Kevin Kerr Co-authored-by: Benjamin Co-authored-by: Austin Bischoff Co-authored-by: rpanchyk Co-authored-by: Florian Hartwig Co-authored-by: Salomon Rada Co-authored-by: vladi-mmg Co-authored-by: Aleksei Lin Co-authored-by: evanmsmrtb Co-authored-by: CPMStar Co-authored-by: pm-isha-bharti Co-authored-by: Seba Perez Co-authored-by: Michael Kuryshev Co-authored-by: Viacheslav Chimishuk Co-authored-by: Shalmali Patil Co-authored-by: Arne Schulz Co-authored-by: Volk, Rainer Co-authored-by: RainerVolk4014 <53347752+RainerVolk4014@users.noreply.github.com> Co-authored-by: Hendrik Iseke Co-authored-by: hendrikiseke1979 <53309111+hendrikiseke1979@users.noreply.github.com> Co-authored-by: Jimmy Tu Co-authored-by: ddantuonobeintoo <58686785+ddantuonobeintoo@users.noreply.github.com> Co-authored-by: zhaojp <327199034@qq.com> Co-authored-by: junping.zhao Co-authored-by: Daniel Cassidy Co-authored-by: dtbarne <7635750+dtbarne@users.noreply.github.com> Co-authored-by: Dan Barnett Co-authored-by: Sander Co-authored-by: Mateusz Co-authored-by: Jim Naumann Co-authored-by: Mirko Feddern <3244291+mirkorean@users.noreply.github.com> Co-authored-by: Alexander Pinnecke Co-authored-by: Alex Klinkert Co-authored-by: Mirko Feddern Co-authored-by: Gena Co-authored-by: Steve Alliance Co-authored-by: steve-a-districtm Co-authored-by: Artur Aleksanyan Co-authored-by: Brandon Ling <51931757+blingster7@users.noreply.github.com> Co-authored-by: Richard Lee <14349+dlackty@users.noreply.github.com> Co-authored-by: Simon Critchley Co-authored-by: Brian Sardo <1168933+bsardo@users.noreply.github.com> Co-authored-by: tadam75 Co-authored-by: tadam Co-authored-by: SmartyAdman <59048845+SmartyAdman@users.noreply.github.com> Co-authored-by: Aiholkin Co-authored-by: Marsel Co-authored-by: AaronColbyPrice <67345931+AaronColbyPrice@users.noreply.github.com> Co-authored-by: Jurij Sinickij Co-authored-by: logicad Co-authored-by: Daniel Barrigas Co-authored-by: susyt Co-authored-by: gpolaert Co-authored-by: Amaury Ravanel Co-authored-by: Vikram Co-authored-by: vikram Co-authored-by: Stephan Co-authored-by: Adprime <64427228+Adprime@users.noreply.github.com> Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Jurij Sinickij Co-authored-by: Rob Hazan Co-authored-by: GammaSSP <35954362+gammassp@users.noreply.github.com> Co-authored-by: Easy Life Co-authored-by: smithaammassamveettil <39389834+smithaammassamveettil@users.noreply.github.com> Co-authored-by: hdeodhar <35999856+hdeodhar@users.noreply.github.com> Co-authored-by: Bill Newman Co-authored-by: Daniel Lawrence Co-authored-by: rtuschkany <35923908+rtuschkany@users.noreply.github.com> Co-authored-by: Alexey Elymanov Co-authored-by: Alexey Elymanov Co-authored-by: invibes <51820283+invibes@users.noreply.github.com> Co-authored-by: aurel.vasile Co-authored-by: ucfunnel <39581136+ucfunnel@users.noreply.github.com> Co-authored-by: Krushmedia <71434282+Krushmedia@users.noreply.github.com> Co-authored-by: Kushneryk Pavel Co-authored-by: Kushneryk Pavlo Co-authored-by: user Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Dan Bogdan <43830380+EMXDigital@users.noreply.github.com> Co-authored-by: Rakesh Balakrishnan Co-authored-by: Dan Bogdan Co-authored-by: AcuityAdsIntegrations <72594990+AcuityAdsIntegrations@users.noreply.github.com> Co-authored-by: Winston Co-authored-by: redaguermas Co-authored-by: Reda Guermas Co-authored-by: ShriprasadM Co-authored-by: Shriprasad Co-authored-by: Nick Jacob Co-authored-by: Aparna Rao Co-authored-by: silvermob <73727464+silvermob@users.noreply.github.com> Co-authored-by: Anton Nikityuk Co-authored-by: Sergio --- pbsmetrics/prometheus/prometheus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go index 54b7810fef4..e076f339b28 100644 --- a/pbsmetrics/prometheus/prometheus.go +++ b/pbsmetrics/prometheus/prometheus.go @@ -406,7 +406,7 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet //[]float64{0.000200000, 0.000250000, 0.000275000, 0.000300000}) []float64{0.000100000, 0.000200000, 0.000300000, 0.000400000, 0.000500000, 0.000600000}) - metrics.adapterVideoBidDuration = newHistogram(cfg, metrics.Registry, + metrics.adapterVideoBidDuration = newHistogramVec(cfg, metrics.Registry, "adapter_vidbid_dur", "Video Ad durations returned by the bidder", []string{adapterLabel}, []float64{4, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60}) From 2202392ba6870ac2943a60d933d89d5681b42474 Mon Sep 17 00:00:00 2001 From: PubMatic-OpenWrap Date: Wed, 23 Dec 2020 22:27:35 +0530 Subject: [PATCH 321/381] Prebid server 0.138.0 fix (#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding copying of gdpr consent string to openrtb bid request (#1189) * Adding copying of gdpr consent string to openrtb bid request * Updated video request to use OpenRTB Video and User objects * Fixing unit test failure message * Updates from code review comments * Updating unit test initialization * Updated mimes array construction * fix conversant sync pixel (#1208) * openx adapter: forward bid response currency in openx adapter if set (#1211) it was always set to the default USD before * add ucfunnel adapter (#1192) * Update required params for TheMediaGrid adapter (#1188) * add zeroclickfraud adapter (#1207) * add zeroclickfraud adapter * fixes for PR * fix casing of Zeroclickfraud * Fix Adform's parameters regex (#1214) * Added adform info file * Added Adform adapter and bidder * Updates from master * Removed usersyncInfo from Adform adapter. Inverted Imp type check. * Removed excessive loop * Updated with the last master * Create readme file for adform * Fix Adform's parameters regex Motivation: catastrophic backtracking during regex execution Details: - https://regex101.com/r/NNQrWq/1 - string to check "url_domain:keskustelu.suomi24.fi,url_path:/matkailu/matkakohteet/aasia,layout:lg,categories:Matkailu,main_category:Matkailu" Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich * If Device.UA is not present in request body, init it with user-agent from header (#1219) * If Device.UA is not present in request body, init it with user-agent from request header if it's present * Moved User-Agent handler to parseVideoRequest func and added unit test * Minor clean up Co-authored-by: Veronika Solovei * Queued request timeout (#1217) Co-authored-by: Veronika Solovei * docs: adding currency support section (#1199) * Add ValueImpression Adapter (#1204) * Kidoz adapter (#1210) Co-authored-by: Ryan Haksi * Update auction.md (#1224) Fix type * Update auction.md (#1225) Fix typo. * Added logging to cache for video endpoint (#1220) * WIP added logging to cache for video endpoint * Updating cache call to use TTL from config * Updates from initial feedback * Log now includes HTTP headers * Fixed caching to use a new cache entry rather than appending to the VAST * Added feature where is query is set, the test flag is set in the request * Updated recorded response and handleError * Updates from code review comments * Changed recorded output to be only the debug ext * Removed extra marhal calls * Changed cache to be an endpoint dependency * Added debugLog struct to hold all debug related info * Numerous smaller changes * Further code cleanup and added unit tests for debug changes * Added missing error checks * Added unit test for error case * added VISX vendor ID for usersyncing (#1229) Co-authored-by: Aadesh Patel * First pass at phase 1 TCF 2.0 support (#1228) * First pass at phase 1 TCF 2.0 support * minor fixes * Update go-gdpr library and fix stuff * Fixes for PR comments * Updated price granularity unmarshal to accept empty values and ranges (#1230) * Update vendorID for TheMediaGrid s2s Bid Adapter (#1232) * treat 204 from FAN as a no bids response (#1233) Co-authored-by: Aadesh Patel * AMP CCPA Fix (#1187) * Update rubicon.md (#1234) * adding schain interface (#1203) * added Rewarded Video section (#1200) also edited all examples so they include the full openRTB context * nanointeractive adapter (#1213) * nanointeractive adapter * nanointeractive adapter, changes after review * nanointeractive adapter * nanointeractive adapter, changes after review * formatting * Typos Fix (#1236) * Fix Typo * Fixed More Typos * Moved hb_pc_cat_dur modification to be before caching (#1250) * replacing info@prebid.org maintainer email addrs (#1256) * aligning maintainer info (#1258) * Add kidoz bidder info (#1257) got this info from email communication with kidoz * Add Cropping of BAdv for Rubicon Adapter (#1254) * Add Cropping of BAdv for Rubicon Adapter BAdv size is limited to 50 * Fix after review Co-authored-by: Harbar Dmytro * Added metrics support to endpoint aspect (#1226) Co-authored-by: Veronika Solovei * Prebid Server adapter for Telaria (#1231) * TELARIA adapter. First Pass * Some refactoring * added the json files * fixed some tests and added the bidder info * fixed some tests and added the bidder info * added default user sync ur; * - Handling gzipped responses from our server * - more refactoring. * added the proper user sync default URL * changed the urls from dev to prod * changed up the required fields. Now AdCode in the Imp.Ext isn't required but Bid.SeatCode is required * change in the return type after decompressing * some refactoring * change in our config url * using pbs.yml to switch between our production and test URLs * setting default endpoint * - fixed the issue that was preventing telaria test cases to run. - added more test cases * - Modifications as per the changes requested by the maintainers. * Moved the seat code to imp.ext * Moved the seat code to imp.ext * Added 'Telaria: ' prefix for error messages * - Fixes for race conditions. Was modifying the original request object instead of a copy * cosmetic changes. * added params_test.go Co-authored-by: Vinay Prasad * #615 Beachfront URLs from config (#1238) * Add nil check errors when setting native asset types (#1260) * Bugfix: no bids from bidder handling (#1252) Co-authored-by: Veronika Solovei * Add missing categories to AppNexus -> IAB mapping file. (#1264) * Add missing categories to AppNexus -> IAB mapping file. * Remove entry for category 38 which was set to a primary IAB category instead of a sub-category. * Fix order of category 22 * Yieldone s2s Bid Adapter (#1242) * Added new Yieldone Bid s2s Adapter * Update endpoint for yieldone bid adapter * Fixes after review for Yieldone Bid s2s Adapter * Fix typeo in Yieldone s2s Bid Adapter * Fix: URL de sync (#1261) * populate the app ID in the FAN timeout notif url with the publisher ID (#1265) and the auction with the request ID Co-authored-by: Aadesh Patel * Added header User Agent decoding (#1268) * Added header User Agent decoding * Added header User Agent decoding: unit tests * Added header User Agent decoding: unit tests * Added check UA is encoded to avoid `+` converted to space Co-authored-by: Veronika Solovei * Ad Generation Adapter Integration. (#1253) * AdGeneration Integration. * update AdGeneration adapter. fix: some methods of the adgAdapter replace to functions. fix: unmarshal functions return a pointer. fix: header is defined once. fix: return when imps is appended * update AdGeneration Adapter. add: Added a comment in usersync. add: Added a test for parameters whose ID does not exist in params_test. change: Change to query creation by net/url. Added getRawQuery Test. fix: Changed variable names related to bidRequest. * Fix Go 1.14 Error Message Changes (#1271) * NinthDecimal Adapter (#1249) Co-authored-by: Chandra Prakash * * Add PubMatic bidder doc file (#1255) * Add app video capability to PubMatic bidder info file * Appnexus adapter: Add category mapping for government. (#1278) * Update a Freewheel mapping to Gaming category. (#1280) * Add AJA adapter (#1269) * OpenX adapter: Pass gdpr and gdpr_consent to user sync endpoint (#1282) I've also updated the test to avoid any confusion. * OpenX adapter: Enable video for app (#1281) * fix conversant sync pixel (#1284) * Add AdOcean adapter (#1273) * [ADOCEAN-20132] AdOcean adapter * [ADOCEAN-20132] AdOcean adapter - support for gdpr * [ADOCEAN-20132] AdOcean adapter - tests * [ADOCEAN-20132] AdOcean adapter - user sync * [ADOCEAN-20132] AdOcean adapter - formatting * [ADOCEAN-20132] AdOcean adapter - send uuid to emitter * [ADOCEAN-20132] adocean adapter - return nil if there is no creative * [ADOCEAN-20132] AdOcean adapter - add version parameter * [ADOCEAN-20132] AdOcean adapter - optimization * [ADOCEAN-20132] AdOcean adapter - add to syncer_test.go * [ADOCEAN-20132] AdOcean adapter - changes after review: * remove whitespaces in js code on adapter initialization instead on every request * check if request.Site is not nil * reuse newUri variable * [ADOCEAN-20132] AdOcean adapter - changes after review: * do not terminate the auction on a single faulty bid * [ADOCEAN-20132] AdOcean adapter - changes after review: * remove unnecessary input parameters check * small optimization * LunaMedia Adapter (#1285) Co-authored-by: Chandra Prakash * [Sharethrough] Add CCPA support (#1263) * Handle gzip responses from ad server correctly * Bump to version 8 * [Go Modules] Add proxy (#1079) * Add SSL cert for accessing stored request API (#1087) * [misspell] fix a misspell (#1102) * update static bidder params for rubicon video to follow the json marshalling names (#1100) * Switching yieldmo auction endpoint from http to https (#1103) * Add Datablocks Adapter (#1095) * datablocks bid adapter * ttx * add test json * add coverage * redo ttx * formatted * better error handling * additional tests and recomended fixes * Adding translatecategories flag to includebrandcategory (#1098) * Making IAB category translation optional with translatecategories boolean in request * Updating exchange unit tests to remove extra bids * Updates from code review comments * Removed comment about default TranslateCategories value * Changed translateCat to translateCategories in tests * Combined helper functions in exchange_test related to TranslateCategories * Bid floor (#1085) * Currency handling fix (#1097) * facebook adapter refactor (#1064) * Kubient adapter (#1094) * [synacormedia] Update user sync url to be https (#1115) This detail was missed while setting up the adapter, but we would like to use https for the user sync. * Remove Go 1.11 Build Target (#1109) * Set "Secure" on Same SIte cookies (#1119) * TripleliftNative Adapter (#1114) * ignore swp files * start small * start really small * add a user sync * justify * triplelift adapter * add our endpoint * fix syntax * config stuff * compiler fixes * more config * add params * making progress * make our ext more exty * start making responses * more logic * fix compilation errors * can we just nil this out? * augment our json * radically simplify our json * fix errs * infer the bid type * fix syntax * fix comilation errors * rename * fix compilation error * config stuff * simplify params * more config stuff * fixes * revert this * fix up the extension * getting closer * add a test * update config * update bidder params * add the floor here, too * add a usersync test * validation, ws, and a test * update tests * fix test * update email * why not * change email * preprocess requests * do some parsing * take care of some errors * floor is optional * ws * remove native * everything is either banner or video * this should be a float * floor to floor * fix compilation errors * add some tests * more tests * more tests * simplify * more progress * format * ws * rm * don't need this * fix test * fix test * don't ignore swap * change line back * report an error if there are no valid impressions for triplelift * check for either a Banner or Video object on the impression * more tests * mv * more tests * update triplelift end point * send native * ws * start changing tests * fix more tests * update config * add redirect to triplelift usersync * fix supplier id in triplelift_test * update tl usersync endpoint and test * fix tl supplier id in test json * update usersync test template * adjust inconsistency with test and sync url * mv * update packages * mv * mv * update * fix compilation errors * rename * rename some stuff * rename * rename * fix some compilation errors * ws * ws * add the extra info * add some extra info * add some files back * ws and such * updates * ws * fix compilation error * mv * rename * Revert "rename" This reverts commit 1b77c72e1eeee580148540fbdd880e70bf699709. * Revert "mv" This reverts commit 52a134ddfaf531fe6235e4751935d4266a36e78f. * it builds * cp a file * cp another file * fix a test * fix test * add the extra info * ws * add some logic * edit comment * it compiles * this is now public * call this * add the function * return nil * seems to be working * ws * seems to be working * ws * mv * starting to work * ws * add a new function * ws * fix tests * bug fix * update some stuff * revert * take out prints * fix up diff * fix up diff * update ws * fix * ws * omit the triplelift endppint * Revert "omit the triplelift endppint" This reverts commit 7abc3e46f0fbba39041da6fff7bb2335adc1fece. * populate the endpoint through the extinfo * ws * set disabled to be default * ws * update types * fixing tests * making progres * fix tests * fix tests * more fixes for tests * fixed tests * just use a comment * get rid of endpoint * restore endpoint * add some errors around unmarshalling * ws * ws * use the literal * ws * ws * update json * simplify * ws * restore tests * fail fast when grabbing invcode * use the right type * use a different error type * bump code coverage * add a new test * change error type * ws * break out test into its own function * JSON block that has a full data-center specific URL cache info (#1104) * Update Dockerfile and Makefile (#1099) * Add option for running tests as part of the docker image building * Update Makefile - Add ability to execute adapter specific tests - Execute targets for "all" rather than just printing the target name and usage - Remove use of non-existing "install" target from .PHONY targets - Remove "build" as a dependency for "image" * enable app requests for audience network (#1122) * [docs] fix markdown title (#1124) * Prometheus Refactor (#1108) * update default sync url (#1127) * Update sync url for BidderGrid adapter (#1120) * [SonarCloud] Legacy auction endpoint (#1017) * [currency converter] allow to deduce reverse rate (#1126) This CL allows the currency rate currency to deduce a currency rate even if not directly defined in the table but the reverse rate is present. E.q. USD => EUR is 1.0897 EUR => USD is not set Old behavior when asking rate from EUR to USD will not be found, New behavior is using the known reverse rate to deduce the rate. Rate for 2 USD will be 2 * (1 / 1.0897) * Updated handleError arguments to be pointers for video endpoint (#1128) * Updated handleError arguments to be pointers for video endpoint * Removing unneeded pointer to http.ResponseWriter * Adding units test for update to handleError * Revert changes to GetExtCacheData() made in #1104 (#1130) (#1131) * Better native request validation (#1132) * require the caller to define native assets[...].ID (#1123) * require the caller to define native assets[...].ID * Update assets-with-partial-ids.json * CCPA Phase 1: AMP Endpoint (#1125) * facebook: removed Auth-Token from header (replaced by authentication_id in the request body) (#1113) * Setuid Fix (#1121) * Update http refresh to use url builder. Fixes #1065 (#1133) * Add mapping of user.ext.eids[] for LiveIntent in Rubicon bidder (#1089) * support facebook app_secret config param (#1139) * CCPA Phase 1: Cookie Sync (#1135) * null check banner.h (#1142) * Add Pubnative Adapter (#1134) * Adding the passing of CCPA value to the bid request for video endpoint (#1143) * first draft (#1137) * CCPA Phase 2: Enforcement (#1138) * Gamoshi Adapter: Update cookie sync (#1146) * Simplify static/bidder-params/triplelift_native.json (#1152) * Added US Privacy support in TheMediaGrid server adapter (#1147) * Add TheMediaGrid server adapter * Add video support in TheMediaGrid s2s adapter * Update sync url for TheMediaGrid s2s adapter * Added CCPA support for TheMediaGrid s2s adapter * Fix sync url for TheMediaGrid adapter * CCPA User Sync Updates (#1153) * Marsmedia - add new bidder (#1118) * Add Applogy adapter (#1151) * enforce video.size_id for video imps in rubicon adapter (#1101) * Updated PubMatic endpoint to use https (#1155) * Update Example AppNexus Placement ID (#1160) * Fix Currency Converter Doesn't Output CUR (#1154) * Add custom JSON req/resp data to the analytics logging… (#1145) * Add custom JSON req/resp data to the analytics logging for the /openrtb2/video endpoint. * Add calls in unit tests to cover logging and jsonify of video object. * CCPA User Sync URL Updates (#1157) * Fixes audienceNetwork adapter ignoring banner.format sizes. (#1164) * adding yieldmo vendor id to usersync (#1166) * Add SmartRTB adapter (#1071) * Added new adapter for CPMStar ad network banners and video (#1159) * Update the Conversant sync pixel (#1161) * Add imp.ext.is_rewarded_inventory flag for rewarded video in Rubicon (#1170) * [currencies] fix GetInfo() null ref issue (#1169) This CL fixes the null ref on `RateConverter.GetInfo()` when rates are nil. Issue: #1136 * Fix triplelift User Sync (#1173) * Enhance Message For Cache Errors (#1175) * Fix PubMatic Usersync URL (#1178) Co-authored-by: pm-isha-bharti * [Synacormedia] Add tagId bidder parameter (#1165) * Remove all non-secure calls from eplanning adapter (#1179) * Expose Cache HTTP Settings (#1184) * Adding bid rejection messages to debug response (#1181) * Adds timeout notifications for Facebook (#1182) * VIS.X: added app type support (#1194) * Add Adoppler bidder support. (#1186) * Add Adoppler bidder support. * Address code review comments. Use JSON-templates for testing. * Fix misprint; Add url.PathEscape call for adunit URL parameter. * Adding support for deal prefixes (#1183) * updating default hard-coded list of certs (#1201) Co-authored-by: Shalmali Patil * add admixer adapter (#1195) * Adding copying of gdpr consent string to openrtb bid request (#1189) * Adding copying of gdpr consent string to openrtb bid request * Updated video request to use OpenRTB Video and User objects * Fixing unit test failure message * Updates from code review comments * Updating unit test initialization * Updated mimes array construction * fix conversant sync pixel (#1208) * openx adapter: forward bid response currency in openx adapter if set (#1211) it was always set to the default USD before * add ucfunnel adapter (#1192) * Update required params for TheMediaGrid adapter (#1188) * add zeroclickfraud adapter (#1207) * add zeroclickfraud adapter * fixes for PR * fix casing of Zeroclickfraud * Fix Adform's parameters regex (#1214) * Added adform info file * Added Adform adapter and bidder * Updates from master * Removed usersyncInfo from Adform adapter. Inverted Imp type check. * Removed excessive loop * Updated with the last master * Create readme file for adform * Fix Adform's parameters regex Motivation: catastrophic backtracking during regex execution Details: - https://regex101.com/r/NNQrWq/1 - string to check "url_domain:keskustelu.suomi24.fi,url_path:/matkailu/matkakohteet/aasia,layout:lg,categories:Matkailu,main_category:Matkailu" Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich * If Device.UA is not present in request body, init it with user-agent from header (#1219) * If Device.UA is not present in request body, init it with user-agent from request header if it's present * Moved User-Agent handler to parseVideoRequest func and added unit test * Minor clean up Co-authored-by: Veronika Solovei * Queued request timeout (#1217) Co-authored-by: Veronika Solovei * docs: adding currency support section (#1199) * Add ValueImpression Adapter (#1204) * Kidoz adapter (#1210) Co-authored-by: Ryan Haksi * Update auction.md (#1224) Fix type * Update auction.md (#1225) Fix typo. * Added logging to cache for video endpoint (#1220) * WIP added logging to cache for video endpoint * Updating cache call to use TTL from config * Updates from initial feedback * Log now includes HTTP headers * Fixed caching to use a new cache entry rather than appending to the VAST * Added feature where is query is set, the test flag is set in the request * Updated recorded response and handleError * Updates from code review comments * Changed recorded output to be only the debug ext * Removed extra marhal calls * Changed cache to be an endpoint dependency * Added debugLog struct to hold all debug related info * Numerous smaller changes * Further code cleanup and added unit tests for debug changes * Added missing error checks * Added unit test for error case * added VISX vendor ID for usersyncing (#1229) Co-authored-by: Aadesh Patel * First pass at phase 1 TCF 2.0 support (#1228) * First pass at phase 1 TCF 2.0 support * minor fixes * Update go-gdpr library and fix stuff * Fixes for PR comments * Updated price granularity unmarshal to accept empty values and ranges (#1230) * Update vendorID for TheMediaGrid s2s Bid Adapter (#1232) * treat 204 from FAN as a no bids response (#1233) Co-authored-by: Aadesh Patel * AMP CCPA Fix (#1187) * Update rubicon.md (#1234) * adding schain interface (#1203) * added Rewarded Video section (#1200) also edited all examples so they include the full openRTB context * nanointeractive adapter (#1213) * nanointeractive adapter * nanointeractive adapter, changes after review * nanointeractive adapter * nanointeractive adapter, changes after review * formatting * Typos Fix (#1236) * Fix Typo * Fixed More Typos * Moved hb_pc_cat_dur modification to be before caching (#1250) * Handle CCPA + enable gzip response [#169984259] * Addressing review (#273) [#169984259] * Remove custom gzip logic (#280) * Getting rid of custom gzip logic [#169984259] * Restore prod ad server url [#169984259] Co-authored-by: Benjamin Co-authored-by: guscarreon Co-authored-by: Aadesh Co-authored-by: Winston-Yieldmo <46379634+Winston-Yieldmo@users.noreply.github.com> Co-authored-by: htang555 Co-authored-by: Cameron Rice <37162584+camrice@users.noreply.github.com> Co-authored-by: ah-tappx <46002207+ah-tappx@users.noreply.github.com> Co-authored-by: hhhjort <31041505+hhhjort@users.noreply.github.com> Co-authored-by: Marsel Co-authored-by: Corey Kress Co-authored-by: Scott Kay Co-authored-by: Kevin Kerr Co-authored-by: Mansi Nahar Co-authored-by: Benjamin Co-authored-by: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Co-authored-by: Austin Bischoff Co-authored-by: rpanchyk Co-authored-by: Florian Hartwig Co-authored-by: Salomon Rada Co-authored-by: vladi-mmg Co-authored-by: Aleksei Lin Co-authored-by: PubMatic-OpenWrap Co-authored-by: jmaynardxandr <46759873+jmaynardxandr@users.noreply.github.com> Co-authored-by: evanmsmrtb Co-authored-by: CPMStar Co-authored-by: johnwier <49074029+johnwier@users.noreply.github.com> Co-authored-by: pm-isha-bharti Co-authored-by: Seba Perez Co-authored-by: Michael Kuryshev Co-authored-by: Viacheslav Chimishuk Co-authored-by: Shalmali Patil Co-authored-by: DmitryStashkevich <34479135+DmitryStashkevich@users.noreply.github.com> Co-authored-by: vstatkevich Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich Co-authored-by: Veronika Solovei Co-authored-by: Veronika Solovei Co-authored-by: bretg Co-authored-by: thuyhq <61451682+thuyhq@users.noreply.github.com> Co-authored-by: rhaksi-kidoz <61601767+rhaksi-kidoz@users.noreply.github.com> Co-authored-by: Ryan Haksi Co-authored-by: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Co-authored-by: Aadesh Patel Co-authored-by: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> * Remove Outdated GDPR AMP Special Case (#1283) * Stricter Privacy Scrubbing (#1286) * Stricter Privacy Scrubbing * Update Unit Test Style * Fixed Whitespace * Add Adapter Orbidder (#1275) Co-authored-by: Volk, Rainer Co-authored-by: RainerVolk4014 <53347752+RainerVolk4014@users.noreply.github.com> Co-authored-by: rvolk <> Co-authored-by: Hendrik Iseke Co-authored-by: hendrikiseke1979 <53309111+hendrikiseke1979@users.noreply.github.com> * Added OpenX Bidder adapter documentation (#1291) * OpenX adapter: Pass rewarded video flag (#1290) * Bugfix for missing fields in imp.video (#1297) Co-authored-by: Veronika Solovei * Add cpmOverride (#1289) * Add cpmOverride Enabled `request.ext.rubicon.debug.cpmOverride` and `request.imp[].ext.rubicon.debug.cpmOverride` processing. Updates tests * Remove unnecessary error checks and add shallow copy * Fixed same pointer * Add Beintoo adapter (#1274) * Add Beintoo adapter * Yeahmobi adapter (#1279) Co-authored-by: junping.zhao * advangelists: Vendor id update (#1307) Co-authored-by: Chandra Prakash * Consumable: Support GDPR and US Privacy consent (#1300) * Restore the AMP privacy exception as an option. (#1311) * Restore the AMP privacy exception as an option. * Adds missing test case * More PR feedback * Remove unused constant * Comment tweak * consumable: Correct GDPR vendor ID to 591. (#1309) fixes #1299. * VIS.X: fix bid.ID, bid.CrID and set default currency value (#1296) * Fix debug log error messages (#1270) * Fixing missing error messages for debug logging * Updated formatting of debug log message * Updated unit tests for debug log to have test flag enabled * Cleaned up debug log implementation * Updates from review comments * Cleaned up field and function names * Added replacer for <> characters * Added cache string unit test * Moved regex from function to struct field * Moved debug regex to endpoint deps * Moving regex initialization to NewVideoEndpoint * MobileFuse Adapter (#1303) Co-authored-by: Dan Barnett * eplanning: Support for apps (#1306) * Introduce Adhese adapter (#1292) Co-authored-by: Mateusz * privacy: Potential JSON injection (#1304) * Updating bidder params for Advangelists (#1316) * Updating placement info on bidder params Co-authored-by: Chandra Prakash * Change placement of cpmoverride for Rubicon (#1310) * increasing the stale period to 2 months (#1305) * Add Go 1.14 Build Target (#1314) * Privacy: Remove user.ext.eids (#1294) * Privacy: Remove user.ext.eids * Extract To A Method * Minor Refactor + More Tests * Performance Tweak * Removed some redundant methods (#1320) * TELARIA adapter. First Pass * Some refactoring * added the json files * fixed some tests and added the bidder info * fixed some tests and added the bidder info * added default user sync ur; * - Handling gzipped responses from our server * - more refactoring. * added the proper user sync default URL * changed the urls from dev to prod * changed up the required fields. Now AdCode in the Imp.Ext isn't required but Bid.SeatCode is required * change in the return type after decompressing * some refactoring * change in our config url * using pbs.yml to switch between our production and test URLs * setting default endpoint * - fixed the issue that was preventing telaria test cases to run. - added more test cases * - Modifications as per the changes requested by the maintainers. * Moved the seat code to imp.ext * Moved the seat code to imp.ext * Added 'Telaria: ' prefix for error messages * - Fixes for race conditions. Was modifying the original request object instead of a copy * cosmetic changes. * added params_test.go * Removed some redundant methods. * Removed a comment Co-authored-by: Vinay Prasad * Beachfront: GDPR id (issue 1301) and documentation updates (#1321) * Defined cookie sync URL in config, cleared deprecated comment in usersync * Update beachfront.md * editing documentation * updated gdpr id - issue 1301 * Add Yieldlab Adapter (#1287) Co-authored-by: Mirko Feddern Signed-off-by: Alex Klinkert Co-authored-by: Alexander Pinnecke Co-authored-by: Alex Klinkert Co-authored-by: Mirko Feddern * Update adtelligent ortb endpoint (#1318) * Change on eplanning endpoint (#1327) * Enable full TCF2 support (#1302) * New config options * Enble TCF2 fields and logic * Resolves some PR comments * More tests * gofmt * Added enforcement tests for split GDPR/GDPRGeo * Testing tweaks * No longer ignore enforce purpose 1 on allowSync() * Removes Purpose 4 * Change on eplanning endpoint (hostname) (#1328) * Districtm Dmx: new adapter (#1209) Co-authored-by: steve-a-districtm * Fix sync url for Yieldone s2s Bid Adapter (#1336) * Fix typo in Yieldone sync url * CCPA Video Bug (#1333) * Add Pubnative bidder documentation (#1340) * Timeout notification monitoring and debugging (#1322) * Add Adtarget server adapter (#1319) * Add Adtarget server adapter * Suggested changes for Adtarget * Update Auction OpenRTB Sample (#1342) * Update Auction OpenRTB Sample * Removed Extra "Or" * Triplelift: Add SRA Support (#1347) * Privacy: Limit Ad Tracking (#1334) * Avoid overriding AMP request original size with mutli-size (#1352) * Extra logging for timeout notifications (#1349) * Consumable: Correct bid type, should always be "banner". (#1359) * Build With Go 1.14 (#1350) * Category mapping changes from product team. (#1348) * Adds Avocet adapter (#1354) * AdOcean adapter - Support for sizes defined in prebid configuration. (#1339) support for multiple sizes bump version to 1.1.0 * Log account id and all bidder names when recovering from OpenRTB auction bidder… (#1358) * Adding Smartadserver adapter (#1346) Co-authored-by: tadam * Added additional Ext Param (#1357) Co-authored-by: Vinay Prasad * Adman adapter (#1356) Co-authored-by: Aiholkin * PBS-632 add max connections per host config setting to general http a… (#1366) * Add ext.bidder.zoneid for Kubient adapater (#1367) * Add ext.bidder.zoneid for Kubient adapater * Check the number of Imps. zoneid is optional. * Improved IPv6 Support + Private Network Filtering (#1362) * Change endpont address (#1370) * Adman adapter * add adman line to syner test * add tests * fix issues * fix web banner test * add 404 banner * fmt * rase coverage * del redundant files * change endpont address * change config endpoint Co-authored-by: Aiholkin * Don't override test parameter (#1373) * OpenX + Facebook Hardening (#1368) * Updating Conversant endpoint url (#1376) * Metrics for TCF 2 adoption (#1360) * Fall back to constant rates when the currency rates endpoint i… (#1364) * TheMediaGrid: added app type support (#1377) * user.ext.eids support in adform adapter (#1381) * Add Logicad adapter (#1382) * Fix Previous Merge Conflict (#1392) * Kubient: Change default endpont address (#1398) * Add support for multiple root schain nodes (#1374) * Update endpoint for latest release by districtm (#1401) Co-authored-by: steve-a-districtm * Set OpenRTB DNT From HTTP Header (#1397) * Add video for InApp support (#1399) * Timeout fix (#1390) * Privacy Request Metrics (#1400) * Privacy Request Metrics * Fix Bug + Add Unit Tests * Fixed Tests * Fix Typo * Parse Site.Publisher.ID from Amp Auction HTTP Req Query Parameter "account" (#1403) * Facebook Only Supports App Impressions (#1396) * fix: Change currency of ad-generation's bidResponse according to bidRequest (#1383) * Adding primary categories to freewheel mapping (#1407) * Add Outgoing Connection Metrics (#1343) * Pubmatic: Support for video duration and primary category (#1384) * Adding suport for video duration and primary category in pubmatic adapter * Adding code review changes for PR-1384 * Adding changes for syntaxNode suggestion Co-authored-by: Isha Bharti * Add IPv6 Non-Public Network (#1417) * GumGum: adds support for video (#1408) * OpenX adapter: pass optional platform (PBID-598) (#1421) * Adds keyvalue hb_format support (#1414) * feat: Add new logger module - Pubstack Analytics Module (#1331) * Pubstack Analytics V1 (#11) * V1 Pubstack (#7) * feat: Add Pubstack Logger (#6) * first version of pubstack analytics * bypass viperconfig * commit #1 * gofmt * update configuration and make the tests pass * add readme on how to configure the adapter and update the network calls * update logging and fix intake url definition * feat: Pubstack Analytics Connector * fixing go mod * fix: bad behaviour on appending path to auction url * add buffering * support bootstyrap like configuration * implement route for all the objects * supports termination signal handling for goroutines * move readme to the correct location * wording * enable configuration reload + add tests * fix logs messages * fix tests * fix log line * conclude merge * merge * update go mod Co-authored-by: Amaury Ravanel * fix duplicated channel keys Co-authored-by: Amaury Ravanel * first pass - PR reviews * rename channel* -> eventChannel * dead code * Review (#10) * use json.Decoder * update documentation * use nil instead []byte("") * clean code * do not use http.DefaultClient * fix race condition (need validation) * separate the sender and buffer logics * refactor the default configuration * remove error counter * Review GP + AR * updating default config * add more logs * remove alias fields in json * fix json serializer * close event channels Co-authored-by: Amaury Ravanel * fix race condition * first pass (pr reviews) * refactor: store enabled modules into a dedicated struct * stop goroutine * test: improve coverage * PR Review * Revert "refactor: store enabled modules into a dedicated struct" This reverts commit f57d9d61680c74244effc39a5d96d6cbb2f19f7d. # Conflicts: # analytics/config/config_test.go Co-authored-by: Amaury Ravanel * New bid adapter for Smaato (#1413) Co-authored-by: vikram Co-authored-by: Stephan * New Adprime adapter (#1418) Co-authored-by: Aiholkin * Separate "debug" behavior from "billable" behavior (#1387) * Remove redundad struct (#1432) * Tcf2 id support (#1420) * Default TCF1 GVL in anticipation of IAB no longer hosting the v1 GVL (#1433) * update to the latest go-gdpr release (#1436) * Video endpoint bid selection enhancements (#1419) Co-authored-by: Veronika Solovei * [WIP] Bid deduplication enhancement (#1430) Co-authored-by: Veronika Solovei * Refactor rate converter separating scheduler from converter logic to improve testability (#1394) * Fix TCF1 Fetcher Fallback (#1438) * Eplanning adapter: Get domain from page (#1434) * Fix no bid debug log (#1375) * Update the fallback GVL to last version (#1440) * Enable geo activation of GDPR flag (#1427) * Validate External Cache Host (#1422) * first draft * Little tweaks * Scott's review part 1 * Scott's review corrections part 2 * Scotts refactor * correction in config_test.go * Correction and refactor * Multiple return statements * Test case refactor Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon * Fixes bug (#1448) * Fixes bug * shortens list * Added adpod_id to request extension (#1444) * Added adpod_id to request -> ext -> appnexus and modified requests splitting based on pod * Unit test fix * Unit test fix * Minor unit test fixes * Code refactoring * Minor code and unit tests refactoring * Unit tests refactoring Co-authored-by: Veronika Solovei * Adform adapter: additional targeting params added (#1424) * Fix minor error message spelling mistake "vastml" -> "vastxml" (#1455) * Fixing comment for usage of deal priority field (#1451) * moving docs to website repo (#1443) * Fix bid dedup (#1456) Co-authored-by: Veronika Solovei * consumable: Correct width and height reported in response. (#1459) Prebid Server now responds with the width and height specified in the Bid Response from Consumable. Previously it would reuse the width and height specified in the Bid Request. That older behaviour was ported from an older version of the prebid.js adapter but is no longer valid. * Panics happen when left with zero length []Imp (#1462) * Add Scheme Option To External Cache URL (#1460) * Update gamma adapter (#1447) * Gamma SSP Adapter * Add Gamma SSP server adapter * increase coverage * Fix conflict with base master * Add check MediaType for Imp * Implement Multi Imps request * Changes requested * remove bad-request * increase coverage * Remove duplicate test file * Update gamma.go * Update gamma.go * Update gamma.go * Update config.go Remove Gamma User Sync Url from config * Gamma SSP Adapter * Add Gamma SSP server adapter * increase coverage * Fix conflict with base master * Add check MediaType for Imp * Implement Multi Imps request * Changes requested * remove bad-request * increase coverage * Remove duplicate test file * Update gamma.go * Update gamma.go * update gamma adapter * return nil when have No-Bid Signaling * add missing-adm.json * discard the bid that's missing adm * discard the bid that's missing adm * escape vast instead of encoded it * expand test coverage Co-authored-by: Easy Life * fix: avoid unexpected EOF on gz writer (#1449) * Smaato adapter: support for video mediaType (#1463) Co-authored-by: vikram * Rubicon liveramp param (#1466) Add liveramp mapping to user.ext should translate the "liveramp.com" id from the "user.ext.eids" array to "user.ext.liveramp_idl" as follows: ``` { "user": { "ext": { "eids": [{ "source": 'liveramp.com', "uids": [{ "id": "T7JiRRvsRAmh88" }] }] } } } ``` to XAPI: ``` { "user": { "ext": { "liveramp_idl": "T7JiRRvsRAmh88" } } } ``` * Consolidate StoredRequest configs, add validation for all data types (#1453) * Fix Test TestEventChannel_OutputFormat (#1468) * Add ability to randomly generate source.TID if empty and set publisher.ID to resolved account ID (#1439) * Add support for Account configuration (PBID-727, #1395) (#1426) * Minor changes to accounts test coverage (#1475) * Brightroll adapter - adding config support (#1461) * Refactor TCF 1/2 Vendor List Fetcher Tests (#1441) * Add validation checker for PRs and merges with github actions (#1476) * Cache refactor (#1431) Reason: Cache has Fetcher-like functionality to handle both requests and imps at a time. Internally, it still uses two caches configured and searched separately, causing some code repetition. Reusing this code to cache other objects like accounts is not easy. Keeping the req/imp repetition in fetcher and out of cache allows for a reusable simpler cache, preserving existing fetcher functionality. Changes in this set: Cache is now a simple generic id->RawMessage store fetcherWithCache handles the separate req and imp caches ComposedCache handles single caches - but it does not appear to be used Removed cache overlap tests since they do not apply now Slightly less code * Pass Through First Party Context Data (#1479) * Added new size 640x360 (Id: 198) (#1490) * Refactor: move getAccount to accounts package (from openrtb2) (#1483) * Fixed TCF2 Geo Only Enforcement (#1492) * New colossus adapter [Clean branch] (#1495) Co-authored-by: Aiholkin * New: InMobi Prebid Server Adapter (#1489) * Adding InMobi adapter * code review feedback, also explicitly working with Imp[0], as we don't support multiple impressions * less tolerant bidder params due to sneaky 1.13 -> 1.14+ change * Revert "Added new size 640x360 (Id: 198) (#1490)" (#1501) This reverts commit fa23f5c226df99a9a4ef318100fdb7d84d3e40fa. * CCPA Publisher No Sale Relationships (#1465) * Fix Merge Conflict (#1502) * Update conversant adapter for new prebid-server interface (#1484) * Implement returnCreative (#1493) * Working solution * clean-up * Test copy/paste error Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon * ConnectAd S2S Adapter (#1505) * between adapter (#1437) Co-authored-by: Alexey Elymanov * Invibes adapter (#1469) Co-authored-by: aurel.vasile * Refactor postgres event producer so it will run either the full or de… (#1485) * Refactor postgres event producer so it will run either the full or delta query periodically * Minor cleanup, follow golang conventions, declare const slice, add test cases * Remove comments * Bidder Uniqueness Gatekeeping Test (#1506) * ucfunnel adapter update end point (#1511) * Refactor EEAC map to be more in line with the nonstandard publisher map (#1514) * Added bunch of new sizes (#1516) * New krushmedia bid adapter (#1504) * Invibes: Generic domainId parameter (#1512) * Smarty ads adapter (#1500) Co-authored-by: Kushneryk Pavlo Co-authored-by: user * Add vscode remote container development files (#1481) * First commit (#1510) Co-authored-by: Gus Carreon * Vtrack and event endpoints (#1467) * Rework pubstack module tests to remove race conditions (#1522) * Rework pubstack module tests to remove race conditions * PR feedback * Remove event count and add helper methods to assert events received on channel * Updating smartadserver endpoint configuration. (#1531) Co-authored-by: tadam * Add new size 500x1000 (ID: 548) (#1536) * Fix missing Request parameter for Adgeneration Adapter (#1525) * Fix endpoint url for TheMediaGrid Bid Adapter (#1541) * Add Account cache (#1519) * Add bidder name key support (#1496) * Simplifying exchange module: bidResponseExt gets built anyway (#1518) * first draft * Scott's feedback * stepping out real quick * add cache errors to bidResponseExt before marshalling * Removed vim's swp file Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon * Correct GetCpmStringValue's second return value (#1520) * Add metrics to capture stored data fetch all/delta durations with fetch status (#1515) * Adds preferDeals support (#1528) * Emxd 3336 add app video ctv (#1529) * Adapter changes for app and video support * adding ctv devicetype test case * Adding whitespace * Updates based on feedback from Prebid team * protocol bug fix and testing * Modifying test cases to accomodate new imp.ext field * bidtype bug fix and additonal testcase for storeUrl Co-authored-by: Rakesh Balakrishnan Co-authored-by: Dan Bogdan * Add http api for fetching accounts (#1545) * Add missing postgres cache init config validation * Acuity ads adapter (#1537) Co-authored-by: Kushneryk Pavlo * Yieldmo app support in yaml file (#1542) Co-authored-by: Winston * Add metrics for account cache (#1543) * [Invibes] remove user sync for invibes (#1550) * [invibes] new bidder stub * [invibes] make request * [invibes] bid request parameters * [invibes] fix errors, add tests * [invibes] new version of MakeBids * cleaning code * [invibes] production urls, isamp flag * [invibes] fix parameters * [invibes] new test parameter * [invibes] change maintainer email * [invibes] PR fixes * [invibes] fix parameters test * [invibes] refactor endpoint template and bidVersion * [Invibes] fix tests * [invibes] resolve PR * [invibes] fix test * [invibes] fix test * [invibes] generic domainId parameter * [invibes] remove invibes cookie sync * [Invibes] comment missing Usersync Co-authored-by: aurel.vasile * Add Support For imp.ext.prebid For DealTiers (#1539) * Add Support For imp.ext.prebid For DealTiers * Remove Normalization * Add Accounts to http cache events (#1553) * Fix JSON tests ignore expected message field (#1450) * NoBid version 1.0. Initial commit. (#1547) Co-authored-by: Reda Guermas * Added dealTierSatisfied parameters in exchange.pbsOrtbBid and openrtb_ext.ExtBidPrebid and dealPriority in openrtb_ext.ExtBidPrebid (#1558) Co-authored-by: Shriprasad * Add client/AccountID support into Adoppler adapter. (#1535) * Optionally read IFA value and add it the the request url (Adhese) (#1563) * Add AMX RTB adapter (#1549) * update Datablocks usersync.go (#1572) * 33Across: Add video support in adapter (#1557) * SilverMob adapter (#1561) * SilverMob adapter * Fixes andchanges according to notes in PR * Remaining fixes: multibids, expectedMakeRequestsErrors * removed log * removed log * Multi-bid test * Removed unnesesary block Co-authored-by: Anton Nikityuk * Updated ePlanning GVL ID (#1574) * update adpone google vendor id (#1577) * ADtelligent gvlid (#1581) * Add account/ host GDPR enabled flags & account per request type GDPR enabled flags (#1564) * Add account level request type specific and general GDPR enabled flags * Clean up test TestAccountLevelGDPREnabled * Add host-level GDPR enabled flag * Move account GDPR enable check as receiver method on accountGDPR * Remove mapstructure annotations on account structs * Minor test updates * Re-add mapstructure annotations on account structs * Change RequestType to IntegrationType and struct annotation formatting * Update comment * Update account IntegrationType comments * Remove extra space in config/accounts.go via gofmt * DMX Bidfloor fix (#1579) * adform bidder video bid response support (#1573) * Fix Beachfront JSON tests (#1578) * Add account CCPA enabled and per-request-type enabled flags (#1566) * Add account level request-type-specific and general CCPA enabled flags * Remove mapstructure annotations on CCPA account structs and clean up CCPA tests * Adjust account/host CCPA enabled flag logic to incorporate feedback on similar GDPR feature * Add shared privacy policy account integration data structure * Refactor EnabledForIntegrationType methods on account privacy objects * Minor test refactor * Simplify logic in EnabledForIntegrationType methods * Refactored HoldAuction Arguments (#1570) * Fix bug in request.imp.ext Validation (#1575) * First draft * Brian's reivew * Removed leftover comments Co-authored-by: Gus Carreon * Updating import statements for v0.138.0 upgrade * UOE-5690: Fixing merging issues * UOE-5690 Fixing merging issues * prebid-server v0.138 upgrade: fixing merging issue * Prebid-upgrade Fixing test cases * Prebid-server upgrade: removing unwanted files * Prebid upgrade: Fixing merging issue with ci * prebid-server upgrade: Fixing formating issues Co-authored-by: Cameron Rice <37162584+camrice@users.noreply.github.com> Co-authored-by: johnwier <49074029+johnwier@users.noreply.github.com> Co-authored-by: Scott Kay Co-authored-by: guscarreon Co-authored-by: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Co-authored-by: htang555 Co-authored-by: vstatkevich Co-authored-by: v.statkevich Co-authored-by: Olga Linkevich Co-authored-by: Veronika Solovei Co-authored-by: Veronika Solovei Co-authored-by: bretg Co-authored-by: thuyhq <61451682+thuyhq@users.noreply.github.com> Co-authored-by: rhaksi-kidoz <61601767+rhaksi-kidoz@users.noreply.github.com> Co-authored-by: Ryan Haksi Co-authored-by: ACannuniRP <57228257+ACannuniRP@users.noreply.github.com> Co-authored-by: Aadesh Co-authored-by: Aadesh Patel Co-authored-by: hhhjort <31041505+hhhjort@users.noreply.github.com> Co-authored-by: Rade Popovic <32302052+nanointeractive@users.noreply.github.com> Co-authored-by: Dmitriy Co-authored-by: Harbar Dmytro Co-authored-by: Telaria Engineering <36203956+telariaEng@users.noreply.github.com> Co-authored-by: Vinay Prasad Co-authored-by: Krzysztof Desput Co-authored-by: Mansi Nahar Co-authored-by: jmaynardxandr <46759873+jmaynardxandr@users.noreply.github.com> Co-authored-by: hbanalytics <55453525+hbanalytics@users.noreply.github.com> Co-authored-by: chino117 Co-authored-by: Ad Generation Co-authored-by: trchandraprakash <47793448+trchandraprakash@users.noreply.github.com> Co-authored-by: Chandra Prakash Co-authored-by: Mike Chowla Co-authored-by: Taiki Sakamoto Co-authored-by: Laurentiu Badea Co-authored-by: Marcin Muras <47107445+mmuras@users.noreply.github.com> Co-authored-by: Mathieu Pheulpin Co-authored-by: Benjamin Co-authored-by: Winston-Yieldmo <46379634+Winston-Yieldmo@users.noreply.github.com> Co-authored-by: ah-tappx <46002207+ah-tappx@users.noreply.github.com> Co-authored-by: Marsel Co-authored-by: Corey Kress Co-authored-by: Kevin Kerr Co-authored-by: Benjamin Co-authored-by: Austin Bischoff Co-authored-by: rpanchyk Co-authored-by: Florian Hartwig Co-authored-by: Salomon Rada Co-authored-by: vladi-mmg Co-authored-by: Aleksei Lin Co-authored-by: evanmsmrtb Co-authored-by: CPMStar Co-authored-by: pm-isha-bharti Co-authored-by: Seba Perez Co-authored-by: Michael Kuryshev Co-authored-by: Viacheslav Chimishuk Co-authored-by: Shalmali Patil Co-authored-by: DmitryStashkevich <34479135+DmitryStashkevich@users.noreply.github.com> Co-authored-by: Arne Schulz Co-authored-by: Volk, Rainer Co-authored-by: RainerVolk4014 <53347752+RainerVolk4014@users.noreply.github.com> Co-authored-by: Hendrik Iseke Co-authored-by: hendrikiseke1979 <53309111+hendrikiseke1979@users.noreply.github.com> Co-authored-by: Jimmy Tu Co-authored-by: ddantuonobeintoo <58686785+ddantuonobeintoo@users.noreply.github.com> Co-authored-by: zhaojp <327199034@qq.com> Co-authored-by: junping.zhao Co-authored-by: Daniel Cassidy Co-authored-by: dtbarne <7635750+dtbarne@users.noreply.github.com> Co-authored-by: Dan Barnett Co-authored-by: Sander Co-authored-by: Mateusz Co-authored-by: Jim Naumann Co-authored-by: Mirko Feddern <3244291+mirkorean@users.noreply.github.com> Co-authored-by: Alexander Pinnecke Co-authored-by: Alex Klinkert Co-authored-by: Mirko Feddern Co-authored-by: Gena Co-authored-by: Steve Alliance Co-authored-by: steve-a-districtm Co-authored-by: Artur Aleksanyan Co-authored-by: Brandon Ling <51931757+blingster7@users.noreply.github.com> Co-authored-by: Richard Lee <14349+dlackty@users.noreply.github.com> Co-authored-by: Simon Critchley Co-authored-by: Brian Sardo <1168933+bsardo@users.noreply.github.com> Co-authored-by: tadam75 Co-authored-by: tadam Co-authored-by: SmartyAdman <59048845+SmartyAdman@users.noreply.github.com> Co-authored-by: Aiholkin Co-authored-by: Marsel Co-authored-by: AaronColbyPrice <67345931+AaronColbyPrice@users.noreply.github.com> Co-authored-by: Jurij Sinickij Co-authored-by: logicad Co-authored-by: Daniel Barrigas Co-authored-by: susyt Co-authored-by: gpolaert Co-authored-by: Amaury Ravanel Co-authored-by: Vikram Co-authored-by: vikram Co-authored-by: Stephan Co-authored-by: Adprime <64427228+Adprime@users.noreply.github.com> Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Jurij Sinickij Co-authored-by: Rob Hazan Co-authored-by: GammaSSP <35954362+gammassp@users.noreply.github.com> Co-authored-by: Easy Life Co-authored-by: smithaammassamveettil <39389834+smithaammassamveettil@users.noreply.github.com> Co-authored-by: hdeodhar <35999856+hdeodhar@users.noreply.github.com> Co-authored-by: Bill Newman Co-authored-by: Daniel Lawrence Co-authored-by: rtuschkany <35923908+rtuschkany@users.noreply.github.com> Co-authored-by: Alexey Elymanov Co-authored-by: Alexey Elymanov Co-authored-by: invibes <51820283+invibes@users.noreply.github.com> Co-authored-by: aurel.vasile Co-authored-by: ucfunnel <39581136+ucfunnel@users.noreply.github.com> Co-authored-by: Krushmedia <71434282+Krushmedia@users.noreply.github.com> Co-authored-by: Kushneryk Pavel Co-authored-by: Kushneryk Pavlo Co-authored-by: user Co-authored-by: Gus Carreon Co-authored-by: Gus Carreon Co-authored-by: Dan Bogdan <43830380+EMXDigital@users.noreply.github.com> Co-authored-by: Rakesh Balakrishnan Co-authored-by: Dan Bogdan Co-authored-by: AcuityAdsIntegrations <72594990+AcuityAdsIntegrations@users.noreply.github.com> Co-authored-by: Winston Co-authored-by: redaguermas Co-authored-by: Reda Guermas Co-authored-by: ShriprasadM Co-authored-by: Shriprasad Co-authored-by: Nick Jacob Co-authored-by: Aparna Rao Co-authored-by: silvermob <73727464+silvermob@users.noreply.github.com> Co-authored-by: Anton Nikityuk Co-authored-by: Sergio --- endpoints/openrtb2/ctv_auction.go | 3 +-- endpoints/openrtb2/ctv_auction_test.go | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go index 551257c2599..bfa8a05cea1 100644 --- a/endpoints/openrtb2/ctv_auction.go +++ b/endpoints/openrtb2/ctv_auction.go @@ -884,7 +884,6 @@ func getAdPodBidExtension(adpod *types.AdPodBid) json.RawMessage { return rawExt } - //getAdDuration determines the duration of video ad from given bid. //it will try to get the actual ad duration returned by the bidder using prebid.video.duration //if prebid.video.duration = 0 or there is error occured in determing it then @@ -896,7 +895,7 @@ func getAdDuration(bid openrtb.Bid, defaultDuration int64) int { } return int(duration) } - + func addTargetingKey(bid *openrtb.Bid, key openrtb_ext.TargetingKey, value string) error { if nil == bid { return errors.New("Invalid bid") diff --git a/endpoints/openrtb2/ctv_auction_test.go b/endpoints/openrtb2/ctv_auction_test.go index a7faa050fa9..1e6a0907c72 100644 --- a/endpoints/openrtb2/ctv_auction_test.go +++ b/endpoints/openrtb2/ctv_auction_test.go @@ -2,9 +2,10 @@ package openrtb2 import ( "encoding/json" - "testing" + "testing" + "github.com/PubMatic-OpenWrap/openrtb" - "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" ) From 0bf43ccb214dedac6ecb36d4b6df2b269f7cf844 Mon Sep 17 00:00:00 2001 From: Viral Vala Date: Tue, 29 Dec 2020 15:31:41 +0530 Subject: [PATCH 322/381] OTT-66 Fixing Selecting DealBids over Normal Bids --- endpoints/openrtb2/ctv/response/adpod_generator.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/endpoints/openrtb2/ctv/response/adpod_generator.go b/endpoints/openrtb2/ctv/response/adpod_generator.go index 68469c5fccd..274c51254bf 100644 --- a/endpoints/openrtb2/ctv/response/adpod_generator.go +++ b/endpoints/openrtb2/ctv/response/adpod_generator.go @@ -179,8 +179,13 @@ func (o *AdPodGenerator) getMaxAdPodBid(results []*highestCombination) *types.Ad rc.bid.FilterReasonCode = rc.reasonCode } } + if len(result.bidIDs) == 0 { + continue + } - if len(result.bidIDs) > 0 && (nil == maxResult || maxResult.nDealBids < result.nDealBids || maxResult.price < result.price) { + if nil == maxResult || + (maxResult.nDealBids < result.nDealBids) || + (maxResult.nDealBids == result.nDealBids && maxResult.price < result.price) { maxResult = result } } From b17213cc6257ba5b477462a122f0e4d0ad92adf1 Mon Sep 17 00:00:00 2001 From: ShriprasadM Date: Mon, 4 Jan 2021 12:39:46 +0530 Subject: [PATCH 323/381] OTT-58: Extract Duration and Creative Id and update Prebid Server objects (#95) * OTT-58: Added empty function getBidDuration. Added unit tests around it * OTT-58: Few trials for converting VAST bid duration into seconds when input is HH:MM:SS.mmm * OTT-58: Added unit tests and functionality for determining the video ad duration * OTT-58: Refactored and added some more unit tests * OTT-58: Added Benchmark testcase * OTT-58: Removed version argument from tests. Its not required for now. Fixed test issue * OTT-53: typo fix * OTT-58: Modifed regexp for millis to accept value max upto 999. Added unit tests around it * OTT-58: Added handling for detecting creative.id and updating it in typedBid.Bid.CrID * OTT-58: Addressed code review comments * OTT-58: Reverted changes for getCreativeID. It will now return empty string to creative id is not present. Instead caller will generate the random creative id with cr as prefix Co-authored-by: Shriprasad --- .../tagbidder/vast_tag_response_handler.go | 82 +++++++++++- .../vast_tag_response_handler_test.go | 120 +++++++++++++++++- 2 files changed, 197 insertions(+), 5 deletions(-) diff --git a/adapters/tagbidder/vast_tag_response_handler.go b/adapters/tagbidder/vast_tag_response_handler.go index 750871cdc2e..1f4a09cce63 100644 --- a/adapters/tagbidder/vast_tag_response_handler.go +++ b/adapters/tagbidder/vast_tag_response_handler.go @@ -5,7 +5,9 @@ import ( "fmt" "math/rand" "net/http" + "regexp" "strconv" + "time" "github.com/PubMatic-OpenWrap/etree" "github.com/PubMatic-OpenWrap/openrtb" @@ -13,6 +15,8 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" ) +var durationRegExp = regexp.MustCompile(`^([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)(\.(\d{1,3}))?$`) + //IVASTTagResponseHandler to parse VAST Tag type IVASTTagResponseHandler interface { ITagResponseHandler @@ -79,8 +83,25 @@ func (handler *VASTTagResponseHandler) vastTagToBidderResponse(internalRequest * } typedBid := &adapters.TypedBid{ - Bid: &openrtb.Bid{}, - BidType: openrtb_ext.BidTypeVideo, + Bid: &openrtb.Bid{}, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, + } + + creatives := adElement.FindElements("Creatives/Creative") + if nil != creatives { + for _, creative := range creatives { + // get creative id + typedBid.Bid.CrID = getCreativeID(creative) + // get duration. Ignore errors + dur, _ := getDuration(creative) + typedBid.BidVideo.Duration = int(dur) // prebid expects int value + } + } + + // generate random creative id if not present + if "" == typedBid.Bid.CrID { + typedBid.Bid.CrID = "cr_" + getRandomID() } bidResponse := &adapters.BidderResponse{ @@ -176,3 +197,60 @@ func getCreativeID(ad *etree.Element) string { var getRandomID = func() string { return strconv.FormatInt(rand.Int63(), intBase) } + +// getDuration extracts the duration of the bid from input creative of Linear type. +// The lookup may vary from vast version provided in the input +// returns duration in seconds or error if failed to obtained the duration. +// If multple Linear tags are present, onlyfirst one will be used +// +// It will lookup for duration only in case of creative type is Linear. +// If creative type other than Linear then this function will return error +// For Linear Creative it will lookup for Duration attribute.Duration value will be in hh:mm:ss.mmm format as per VAST specifications +// If Duration attribute not present this will return error +// +// After extracing the duration it will convert it into seconds +// +// The ad server uses the element to denote +// the intended playback duration for the video or audio component of the ad. +// Time value may be in the format HH:MM:SS.mmm where .mmm indicates milliseconds. +// Providing milliseconds is optional. +// +// Reference +// 1.https://iabtechlab.com/wp-content/uploads/2019/06/VAST_4.2_final_june26.pdf +// 2.https://iabtechlab.com/wp-content/uploads/2018/11/VAST4.1-final-Nov-8-2018.pdf +// 3.https://iabtechlab.com/wp-content/uploads/2016/05/VAST4.0_Updated_April_2016.pdf +// 4.https://iabtechlab.com/wp-content/uploads/2016/04/VASTv3_0.pdf +func getDuration(creative *etree.Element) (float64, error) { + if nil == creative { + return 0, errors.New("Invalid Creative") + } + node := creative.FindElement("./Linear/Duration") + if nil == node { + return 0, errors.New("Invalid Duration") + } + duration := node.Text() + // check if milliseconds is provided + match := durationRegExp.FindStringSubmatch(duration) + if nil == match { + return 0, errors.New("Invalid Duration") + } + repl := "${1}h${2}m${3}s" + ms := match[5] + if "" != ms { + repl += "${5}ms" + } + duration = durationRegExp.ReplaceAllString(duration, repl) + dur, err := time.ParseDuration(duration) + if err != nil { + return 0, err + } + return dur.Seconds(), err +} + +//getCreativeID looks for ID inside input creative tag +func getCreativeID(creative *etree.Element) string { + if nil == creative { + return "" + } + return creative.SelectAttrValue("id", "") +} diff --git a/adapters/tagbidder/vast_tag_response_handler_test.go b/adapters/tagbidder/vast_tag_response_handler_test.go index 40dd0a18988..80d7836cd80 100644 --- a/adapters/tagbidder/vast_tag_response_handler_test.go +++ b/adapters/tagbidder/vast_tag_response_handler_test.go @@ -1,8 +1,11 @@ package tagbidder import ( + "errors" "testing" + "github.com/PubMatic-OpenWrap/etree" + "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" @@ -32,7 +35,7 @@ func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) { internalRequest: &openrtb.BidRequest{ ID: `request_id_1`, Imp: []openrtb.Imp{ - openrtb.Imp{ + { ID: `imp_id_1`, }, }, @@ -47,14 +50,16 @@ func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) { want: want{ bidderResponse: &adapters.BidderResponse{ Bids: []*adapters.TypedBid{ - &adapters.TypedBid{ + { Bid: &openrtb.Bid{ ID: `1234`, ImpID: `imp_id_1`, Price: 0.05, AdM: ` `, + CrID: "cr_1234", }, - BidType: openrtb_ext.BidTypeVideo, + BidType: openrtb_ext.BidTypeVideo, + BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, }, }, Currency: `USD`, @@ -76,3 +81,112 @@ func TestVASTTagResponseHandler_vastTagToBidderResponse(t *testing.T) { }) } } + +//TestGetDurationInSeconds ... +// hh:mm:ss.mmm => 3:40:43.5 => 3 hours, 40 minutes, 43 seconds and 5 milliseconds +// => 3*60*60 + 40*60 + 43 + 5*0.001 => 10800 + 2400 + 43 + 0.005 => 13243.005 +func TestGetDurationInSeconds(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + duration float64 // seconds (will converted from string with format as HH:MM:SS.mmm) + durationInt int + err error + } + tests := []struct { + name string + args args + want want + }{ + // duration validation tests + {name: "duration 00:00:25 (= 25 seconds)", want: want{duration: 25, durationInt: 25}, args: args{creativeTag: ` 00:00:25 `}}, + {name: "duration 00:00:-25 (= -25 seconds)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:-25 `}}, + {name: "duration 00:00:30.999 (= 30.990 seconds (int -> 30 seconds))", want: want{duration: 30.999, durationInt: 30}, args: args{creativeTag: ` 00:00:30.999 `}}, + {name: "duration 00:01:08 (1 min 8 seconds = 68 seconds)", want: want{duration: 68, durationInt: 68}, args: args{creativeTag: ` 00:01:08 `}}, + {name: "duration 02:13:12 (2 hrs 13 min 12 seconds) = 7992 seconds)", want: want{duration: 7992, durationInt: 7992}, args: args{creativeTag: ` 02:13:12 `}}, + {name: "duration 3:40:43.5 (3 hrs 40 min 43 seconds 5 ms) = 6043.005 seconds (int -> 6043 seconds))", want: want{duration: 13243.005, durationInt: 13243}, args: args{creativeTag: ` 3:40:43.5 `}}, + {name: "duration 00:00:25.0005458 (0 hrs 0 min 25 seconds 0005458 ms) - invalid max ms is 999", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 00:00:25.0005458 `}}, + {name: "invalid duration 3:13:900 (3 hrs 13 min 900 seconds) = Invalid seconds )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:900 `}}, + {name: "invalid duration 3:13:34:44 (3 hrs 13 min 34 seconds :44=invalid) = ?? )", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 3:13:34:44 `}}, + {name: "duration = 0:0:45.038 , with milliseconds duration (0 hrs 0 min 45 seconds and 038 millseconds) = 45.038 seconds (int -> 45 seconds) )", want: want{duration: 45.038, durationInt: 45}, args: args{creativeTag: ` 0:0:45.038 `}}, + {name: "duration = 0:0:48.50 = 48.050 seconds (int -> 48 seconds))", want: want{duration: 48.050, durationInt: 48}, args: args{creativeTag: ` 0:0:48.50 `}}, + {name: "duration = 0:0:28.59 = 28.059 seconds (int -> 28 seconds))", want: want{duration: 28.059, durationInt: 28}, args: args{creativeTag: ` 0:0:28.59 `}}, + {name: "duration = 56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56 `}}, + {name: "duration = :56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56 `}}, + {name: "duration = :56: (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` :56: `}}, + {name: "duration = ::56 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` ::56 `}}, + {name: "duration = 56.445 (ambiguity w.r.t. HH:MM:SS.mmm format)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` 56.445 `}}, + {name: "duration = a:b:c.d (no numbers)", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ` a:b:c.d `}}, + + // tag validations tests + {name: "Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Companion Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Non-Linear Creative no duration", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: ``}}, + {name: "Invalid Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ``}}, + {name: "Nil Creative tag", want: want{err: errors.New("Invalid Creative")}, args: args{creativeTag: ""}}, + + // multiple linear tags in creative + {name: "Multiple Linear Ads within Creative", want: want{duration: 25, durationInt: 25}, args: args{creativeTag: `0:0:250:0:30`}}, + // Case sensitivity check - passing DURATION (vast is case-sensitive as per https://vastvalidator.iabtechlab.com/dash) + {name: " all caps", want: want{err: errors.New("Invalid Duration")}, args: args{creativeTag: `0:0:10`}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + dur, err := getDuration(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.duration, dur) + assert.Equal(t, tt.want.durationInt, int(dur)) + assert.Equal(t, tt.want.err, err) + // if error expects 0 value for duration + if nil != err { + assert.Equal(t, 0.0, dur) + } + }) + } +} + +func BenchmarkGetDuration(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` 0:0:56.3 `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getDuration(creative) + } +} + +func TestGetCreativeId(t *testing.T) { + type args struct { + creativeTag string // ad element + } + type want struct { + id string + } + tests := []struct { + name string + args args + want want + }{ + {name: "creative tag with id", want: want{id: "233ff44"}, args: args{creativeTag: ``}}, + {name: "creative tag without id", want: want{id: ""}, args: args{creativeTag: ``}}, + {name: "no creative tag", want: want{id: ""}, args: args{creativeTag: ""}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString(tt.args.creativeTag) + id := getCreativeID(doc.FindElement("./Creative")) + assert.Equal(t, tt.want.id, id) + }) + } +} + +func BenchmarkGetCreativeID(b *testing.B) { + doc := etree.NewDocument() + doc.ReadFromString(` `) + creative := doc.FindElement("/Creative") + for n := 0; n < b.N; n++ { + getCreativeID(creative) + } +} From 9f69202886ee656cc101603dc26748e184d9629e Mon Sep 17 00:00:00 2001 From: Viral Vala Date: Tue, 29 Dec 2020 12:27:41 +0530 Subject: [PATCH 324/381] OTT-54 Adding Macro Support for Tag Bidder --- adapters/tagbidder/bidder_config.go | 15 - adapters/tagbidder/bidder_macro.go | 72 +-- adapters/tagbidder/bidder_mapper.go | 13 - adapters/tagbidder/bidders.go | 5 - adapters/tagbidder/constant.go | 242 ++++---- adapters/tagbidder/default_mapper.go | 154 ----- adapters/tagbidder/ibidder_macro.go | 9 +- adapters/tagbidder/macro_processor.go | 48 +- adapters/tagbidder/macro_processor_test.go | 586 ------------------ adapters/tagbidder/mapper.go | 185 ++++-- adapters/tagbidder/spotxtag/constant.go.bak | 6 - .../tagbidder/spotxtag/spotx_adapter.go.bak | 34 - .../tagbidder/spotxtag/spotx_macro.go.bak | 99 --- .../tagbidder/spotxtag/spotx_mapper.go.bak | 16 - adapters/tagbidder/tagbidder.go | 9 +- adapters/tagbidder/util.go | 33 + .../tagbidder/vast_tag_response_handler.go | 9 - pbsmetrics/config/metrics.go | 1 - privacy/enforcement_test.go | 4 +- router/router_test.go | 2 +- static/tagbidder-params/spotx.json | 38 +- 21 files changed, 360 insertions(+), 1220 deletions(-) delete mode 100644 adapters/tagbidder/bidder_mapper.go delete mode 100644 adapters/tagbidder/bidders.go delete mode 100644 adapters/tagbidder/default_mapper.go delete mode 100644 adapters/tagbidder/macro_processor_test.go delete mode 100644 adapters/tagbidder/spotxtag/constant.go.bak delete mode 100644 adapters/tagbidder/spotxtag/spotx_adapter.go.bak delete mode 100644 adapters/tagbidder/spotxtag/spotx_macro.go.bak delete mode 100644 adapters/tagbidder/spotxtag/spotx_mapper.go.bak diff --git a/adapters/tagbidder/bidder_config.go b/adapters/tagbidder/bidder_config.go index 9ab9d86b81e..6ee9997e3fb 100644 --- a/adapters/tagbidder/bidder_config.go +++ b/adapters/tagbidder/bidder_config.go @@ -16,19 +16,11 @@ type Flags struct { RemoveEmptyParam bool `json:"remove_empty,omitempty"` } -//Keys each macro mapping key definition -type Keys struct { - Cached *bool `json:"cached,omitempty"` - Value string `json:"value,omitempty"` - ValueType MacroValueType `json:"type,omitempty"` -} - //BidderConfig mapper json type BidderConfig struct { URL string `json:"url,omitempty"` ResponseType ResponseHandlerType `json:"response,omitempty"` Flags Flags `json:"flags,omitempty"` - Keys map[string]Keys `json:"keys,omitempty"` } var bidderConfig = map[string]*BidderConfig{} @@ -76,15 +68,8 @@ func InitTagBidderConfig(schemaDirectory string, tagBidderMap map[string]openrtb glog.Fatalf("error parsing json in file %s: %v", schemaDirectory+"/"+bidderName+".json", err) } - //reading its tag parameter mapper from config - mapper := NewMapperFromConfig(&bidderConfig) - if nil == mapper { - glog.Fatalf("no query parameters mapper for bidder " + bidderName) - } - //register tag bidder configurations RegisterBidderConfig(bidderName, &bidderConfig) - RegisterBidderMapper(bidderName, mapper) } return nil } diff --git a/adapters/tagbidder/bidder_macro.go b/adapters/tagbidder/bidder_macro.go index 3aa4e25a199..8271ff34ed6 100644 --- a/adapters/tagbidder/bidder_macro.go +++ b/adapters/tagbidder/bidder_macro.go @@ -8,8 +8,7 @@ import ( "strings" "time" - "github.com/buger/jsonparser" - + "github.com/PubMatic-OpenWrap/prebid-server/adapters" "github.com/PubMatic-OpenWrap/prebid-server/config" "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" @@ -36,20 +35,10 @@ type BidderMacro struct { } //NewBidderMacro contains definition for all openrtb macro's -func NewBidderMacro() *BidderMacro { +func NewBidderMacro() IBidderMacro { return &BidderMacro{} } -//NewIBidderMacro contains definition for all openrtb macro's -func NewIBidderMacro() IBidderMacro { - return NewBidderMacro() -} - -//RegisterDefaultBidderMacro will register new tag bidder -func RegisterDefaultBidderMacro(bidderName openrtb_ext.BidderName) { - RegisterNewBidderMacro(string(bidderName), NewIBidderMacro) -} - func (tag *BidderMacro) init() { if nil != tag.Request.App { tag.IsApp = true @@ -92,6 +81,21 @@ func (tag *BidderMacro) LoadImpression(imp *openrtb.Imp) error { return nil } +//GetBidderKeys will set bidder level keys +func (tag *BidderMacro) GetBidderKeys() map[string]string { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(tag.Imp.Ext, &bidderExt); err != nil { + return nil + } + + ext := map[string]interface{}{} + if err := json.Unmarshal(bidderExt.Bidder, &ext); err != nil { + return nil + } + + return normalizeJSON(ext) +} + //SetAdapterConfig will set Adapter config func (tag *BidderMacro) SetAdapterConfig(conf *config.Adapter) { tag.Conf = conf @@ -146,8 +150,8 @@ func (tag *BidderMacro) MacroWhitelistLang(key string) string { return strings.Join(tag.Request.WLang, comma) } -//MacroBlockedseat contains definition for Blockedseat Parameter -func (tag *BidderMacro) MacroBlockedseat(key string) string { +//MacroBlockedSeat contains definition for Blockedseat Parameter +func (tag *BidderMacro) MacroBlockedSeat(key string) string { return strings.Join(tag.Request.BSeat, comma) } @@ -1052,41 +1056,3 @@ func (tag *BidderMacro) MacroCacheBuster(key string) string { //change implementation return strconv.FormatInt(time.Now().UnixNano(), intBase) } - -//ConstantValue contains definition for CacheBuster Parameter -func (tag *BidderMacro) ConstantValue(key string) string { - if k, ok := tag.BidderConf.Keys[key]; ok { - return k.Value - } - return "" -} - -//JSONKey contains definition for CacheBuster Parameter -func (tag *BidderMacro) JSONKey(key string) string { - if k, ok := tag.BidderConf.Keys[key]; ok { - //get key parts - keys := strings.Split(k.Value, ".") - - //check for request ext, imp ext - var ext json.RawMessage - switch keys[0] { - case RequestExtPrefix: - ext = tag.Request.Ext - case ImpressionExtPrefix: - ext = tag.Imp.Ext - } - - if len(ext) > 0 && len(keys) > 1 { - value, jsontype, _, err := jsonparser.Get(ext, keys[1:]...) - if nil != err || - jsontype == jsonparser.NotExist || - jsontype == jsonparser.Null || - jsontype == jsonparser.Object || - jsontype == jsonparser.Array { - return "" - } - return string(value) - } - } - return "" -} diff --git a/adapters/tagbidder/bidder_mapper.go b/adapters/tagbidder/bidder_mapper.go deleted file mode 100644 index 5e1d90d1a58..00000000000 --- a/adapters/tagbidder/bidder_mapper.go +++ /dev/null @@ -1,13 +0,0 @@ -package tagbidder - -var bidderMapper = map[string]Mapper{} - -//RegisterBidderMapper will be used by each bidder to set its respective macro Mapper -func RegisterBidderMapper(bidder string, bidderMap Mapper) { - bidderMapper[bidder] = bidderMap -} - -//GetBidderMapper will return Mapper of specific bidder -func GetBidderMapper(bidder string) Mapper { - return bidderMapper[bidder] -} diff --git a/adapters/tagbidder/bidders.go b/adapters/tagbidder/bidders.go deleted file mode 100644 index c6e5f102590..00000000000 --- a/adapters/tagbidder/bidders.go +++ /dev/null @@ -1,5 +0,0 @@ -package tagbidder - -func init() { - RegisterDefaultBidderMacro(`spotx`) -} diff --git a/adapters/tagbidder/constant.go b/adapters/tagbidder/constant.go index f71ab336b4f..4cf269accb7 100644 --- a/adapters/tagbidder/constant.go +++ b/adapters/tagbidder/constant.go @@ -8,155 +8,150 @@ const ( //List of Tag Bidder Macros const ( //Request - MacroTest = `MacroTest` - MacroTimeout = `MacroTimeout` - MacroWhitelistSeat = `MacroWhitelistSeat` - MacroWhitelistLang = `MacroWhitelistLang` - MacroBlockedseat = `MacroBlockedseat` - MacroCurrency = `MacroCurrency` - MacroBlockedCategory = `MacroBlockedCategory` - MacroBlockedAdvertiser = `MacroBlockedAdvertiser` - MacroBlockedApp = `MacroBlockedApp` + MacroTest = `test` + MacroTimeout = `timeout` + MacroWhitelistSeat = `wseat` + MacroWhitelistLang = `wlang` + MacroBlockedSeat = `bseat` + MacroCurrency = `cur` + MacroBlockedCategory = `bcat` + MacroBlockedAdvertiser = `badv` + MacroBlockedApp = `bapp` //Source - MacroFD = `MacroFD` - MacroTransactionID = `MacroTransactionID` - MacroPaymentIDChain = `MacroPaymentIDChain` + MacroFD = `fd` + MacroTransactionID = `tid` + MacroPaymentIDChain = `pchain` //Regs - MacroCoppa = `MacroCoppa` + MacroCoppa = `coppa` //Impression - MacroDisplayManager = `MacroDisplayManager` - MacroDisplayManagerVersion = `MacroDisplayManagerVersion` - MacroInterstitial = `MacroInterstitial` - MacroTagID = `MacroTagID` - MacroBidFloor = `MacroBidFloor` - MacroBidFloorCurrency = `MacroBidFloorCurrency` - MacroSecure = `MacroSecure` - MacroPMP = `MacroPMP` + MacroDisplayManager = `displaymanager` + MacroDisplayManagerVersion = `displaymanagerver` + MacroInterstitial = `instl` + MacroTagID = `tagid` + MacroBidFloor = `bidfloor` + MacroBidFloorCurrency = `bidfloorcur` + MacroSecure = `secure` + MacroPMP = `pmp` //Video - MacroVideoMIMES = `MacroVideoMIMES` - MacroVideoMinimumDuration = `MacroVideoMinimumDuration` - MacroVideoMaximumDuration = `MacroVideoMaximumDuration` - MacroVideoProtocols = `MacroVideoProtocols` - MacroVideoPlayerWidth = `MacroVideoPlayerWidth` - MacroVideoPlayerHeight = `MacroVideoPlayerHeight` - MacroVideoStartDelay = `MacroVideoStartDelay` - MacroVideoPlacement = `MacroVideoPlacement` - MacroVideoLinearity = `MacroVideoLinearity` - MacroVideoSkip = `MacroVideoSkip` - MacroVideoSkipMinimum = `MacroVideoSkipMinimum` - MacroVideoSkipAfter = `MacroVideoSkipAfter` - MacroVideoSequence = `MacroVideoSequence` - MacroVideoBlockedAttribute = `MacroVideoBlockedAttribute` - MacroVideoMaximumExtended = `MacroVideoMaximumExtended` - MacroVideoMinimumBitRate = `MacroVideoMinimumBitRate` - MacroVideoMaximumBitRate = `MacroVideoMaximumBitRate` - MacroVideoBoxing = `MacroVideoBoxing` - MacroVideoPlaybackMethod = `MacroVideoPlaybackMethod` - MacroVideoDelivery = `MacroVideoDelivery` - MacroVideoPosition = `MacroVideoPosition` - MacroVideoAPI = `MacroVideoAPI` + MacroVideoMIMES = `mimes` + MacroVideoMinimumDuration = `minduration` + MacroVideoMaximumDuration = `maxduration` + MacroVideoProtocols = `protocols` + MacroVideoPlayerWidth = `playerwidth` + MacroVideoPlayerHeight = `playerheight` + MacroVideoStartDelay = `startdelay` + MacroVideoPlacement = `placement` + MacroVideoLinearity = `linearity` + MacroVideoSkip = `skip` + MacroVideoSkipMinimum = `skipmin` + MacroVideoSkipAfter = `skipafter` + MacroVideoSequence = `sequence` + MacroVideoBlockedAttribute = `battr` + MacroVideoMaximumExtended = `maxextend` + MacroVideoMinimumBitRate = `minbitrate` + MacroVideoMaximumBitRate = `maxbitrate` + MacroVideoBoxing = `boxingallowed` + MacroVideoPlaybackMethod = `playbackmethod` + MacroVideoDelivery = `delivery` + MacroVideoPosition = `position` + MacroVideoAPI = `api` //Site - MacroSiteID = `MacroSiteID` - MacroSiteName = `MacroSiteName` - MacroSitePage = `MacroSitePage` - MacroSiteReferrer = `MacroSiteReferrer` - MacroSiteSearch = `MacroSiteSearch` - MacroSiteMobile = `MacroSiteMobile` + MacroSiteID = `siteid` + MacroSiteName = `sitename` + MacroSitePage = `page` + MacroSiteReferrer = `ref` + MacroSiteSearch = `search` + MacroSiteMobile = `mobile` //App - MacroAppID = `MacroAppID` - MacroAppName = `MacroAppName` - MacroAppBundle = `MacroAppBundle` - MacroAppStoreURL = `MacroAppStoreURL` - MacroAppVersion = `MacroAppVersion` - MacroAppPaid = `MacroAppPaid` + MacroAppID = `appid` + MacroAppName = `appname` + MacroAppBundle = `bundle` + MacroAppStoreURL = `storeurl` + MacroAppVersion = `appver` + MacroAppPaid = `paid` //SiteAppCommon - MacroCategory = `MacroCategory` - MacroDomain = `MacroDomain` - MacroSectionCategory = `MacroSectionCategory` - MacroPageCategory = `MacroPageCategory` - MacroPrivacyPolicy = `MacroPrivacyPolicy` - MacroKeywords = `MacroKeywords` + MacroCategory = `cat` + MacroDomain = `domain` + MacroSectionCategory = `sectioncat` + MacroPageCategory = `pagecat` + MacroPrivacyPolicy = `privacypolicy` + MacroKeywords = `keywords` //Publisher - MacroPubID = `MacroPubID` - MacroPubName = `MacroPubName` - MacroPubDomain = `MacroPubDomain` + MacroPubID = `pubid` + MacroPubName = `pubname` + MacroPubDomain = `pubdomain` //Content - MacroContentID = `MacroContentID` - MacroContentEpisode = `MacroContentEpisode` - MacroContentTitle = `MacroContentTitle` - MacroContentSeries = `MacroContentSeries` - MacroContentSeason = `MacroContentSeason` - MacroContentArtist = `MacroContentArtist` - MacroContentGenre = `MacroContentGenre` - MacroContentAlbum = `MacroContentAlbum` - MacroContentISrc = `MacroContentISrc` - MacroContentURL = `MacroContentURL` - MacroContentCategory = `MacroContentCategory` - MacroContentProductionQuality = `MacroContentProductionQuality` - MacroContentVideoQuality = `MacroContentVideoQuality` - MacroContentContext = `MacroContentContext` + MacroContentID = `contentid` + MacroContentEpisode = `episode` + MacroContentTitle = `title` + MacroContentSeries = `series` + MacroContentSeason = `season` + MacroContentArtist = `artist` + MacroContentGenre = `genre` + MacroContentAlbum = `album` + MacroContentISrc = `isrc` + MacroContentURL = `contenturl` + MacroContentCategory = `contentcat` + MacroContentProductionQuality = `contentprodq` + MacroContentVideoQuality = `contentvideoquality` + MacroContentContext = `context` //Producer - MacroProducerID = `MacroProducerID` - MacroProducerName = `MacroProducerName` + MacroProducerID = `prodid` + MacroProducerName = `prodname` //Device - MacroUserAgent = `MacroUserAgent` - MacroDNT = `MacroDNT` - MacroLMT = `MacroLMT` - MacroIP = `MacroIP` - MacroDeviceType = `MacroDeviceType` - MacroMake = `MacroMake` - MacroModel = `MacroModel` - MacroDeviceOS = `MacroDeviceOS` - MacroDeviceOSVersion = `MacroDeviceOSVersion` - MacroDeviceWidth = `MacroDeviceWidth` - MacroDeviceHeight = `MacroDeviceHeight` - MacroDeviceJS = `MacroDeviceJS` - MacroDeviceLanguage = `MacroDeviceLanguage` - MacroDeviceIFA = `MacroDeviceIFA` - MacroDeviceDIDSHA1 = `MacroDeviceDIDSHA1` - MacroDeviceDIDMD5 = `MacroDeviceDIDMD5` - MacroDeviceDPIDSHA1 = `MacroDeviceDPIDSHA1` - MacroDeviceDPIDMD5 = `MacroDeviceDPIDMD5` - MacroDeviceMACSHA1 = `MacroDeviceMACSHA1` - MacroDeviceMACMD5 = `MacroDeviceMACMD5` + MacroUserAgent = `useragent` + MacroDNT = `dnt` + MacroLMT = `lmt` + MacroIP = `ip` + MacroDeviceType = `devicetype` + MacroMake = `make` + MacroModel = `model` + MacroDeviceOS = `os` + MacroDeviceOSVersion = `osv` + MacroDeviceWidth = `devicewidth` + MacroDeviceHeight = `deviceheight` + MacroDeviceJS = `js` + MacroDeviceLanguage = `lang` + MacroDeviceIFA = `ifa` + MacroDeviceDIDSHA1 = `didsha1` + MacroDeviceDIDMD5 = `didmd5` + MacroDeviceDPIDSHA1 = `dpidsha1` + MacroDeviceDPIDMD5 = `dpidmd5` + MacroDeviceMACSHA1 = `macsha1` + MacroDeviceMACMD5 = `macmd5` //Geo - MacroLatitude = `MacroLatitude` - MacroLongitude = `MacroLongitude` - MacroCountry = `MacroCountry` - MacroRegion = `MacroRegion` - MacroCity = `MacroCity` - MacroZip = `MacroZip` - MacroUTCOffset = `MacroUTCOffset` + MacroLatitude = `lat` + MacroLongitude = `long` + MacroCountry = `country` + MacroRegion = `region` + MacroCity = `city` + MacroZip = `aip` + MacroUTCOffset = `utcoffset` //User - MacroUserID = `MacroUserID` - MacroYearOfBirth = `MacroYearOfBirth` - MacroGender = `MacroGender` + MacroUserID = `uid` + MacroYearOfBirth = `yob` + MacroGender = `gender` //Extension - MacroGDPRConsent = `MacroGDPRConsent` - MacroGDPR = `MacroGDPR` - MacroUSPrivacy = `MacroUSPrivacy` + MacroGDPRConsent = `consent` + MacroGDPR = `gdpr` + MacroUSPrivacy = `usprivacy` //Additional - MacroCacheBuster = `MacroCacheBuster` -) - -const ( - RequestExtPrefix = `reqext` - ImpressionExtPrefix = `impext` + MacroCacheBuster = `cachebuster` ) //ResponseHandlerType list of tag based response handlers @@ -166,12 +161,3 @@ const ( OpenRTBResponseHandlerType ResponseHandlerType = `openrtb` VASTTagResponseHandlerType ResponseHandlerType = `vasttag` ) - -//MacroValueType list of values allowed for macro's -type MacroValueType string - -const ( - ConstantValueType MacroValueType = `constant` - JSONKeyValueType MacroValueType = `jsonkey` - CallBackValueType MacroValueType = `callback` -) diff --git a/adapters/tagbidder/default_mapper.go b/adapters/tagbidder/default_mapper.go deleted file mode 100644 index a3b1f804f8d..00000000000 --- a/adapters/tagbidder/default_mapper.go +++ /dev/null @@ -1,154 +0,0 @@ -package tagbidder - -var _defaultMapper = Mapper{ - //Request - MacroTest: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTest}, - MacroTimeout: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTimeout}, - MacroWhitelistSeat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistSeat}, - MacroWhitelistLang: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistLang}, - MacroBlockedseat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedseat}, - MacroCurrency: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCurrency}, - MacroBlockedCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedCategory}, - MacroBlockedAdvertiser: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedAdvertiser}, - MacroBlockedApp: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedApp}, - - //Source - MacroFD: ¯oCallBack{cached: true, callback: IBidderMacro.MacroFD}, - MacroTransactionID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTransactionID}, - MacroPaymentIDChain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPaymentIDChain}, - - //Regs - MacroCoppa: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCoppa}, - - //Impression - MacroDisplayManager: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManager}, - MacroDisplayManagerVersion: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManagerVersion}, - MacroInterstitial: ¯oCallBack{cached: false, callback: IBidderMacro.MacroInterstitial}, - MacroTagID: ¯oCallBack{cached: false, callback: IBidderMacro.MacroTagID}, - MacroBidFloor: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloor}, - MacroBidFloorCurrency: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloorCurrency}, - MacroSecure: ¯oCallBack{cached: false, callback: IBidderMacro.MacroSecure}, - MacroPMP: ¯oCallBack{cached: false, callback: IBidderMacro.MacroPMP}, - - //Video - MacroVideoMIMES: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMIMES}, - MacroVideoMinimumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumDuration}, - MacroVideoMaximumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumDuration}, - MacroVideoProtocols: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoProtocols}, - MacroVideoPlayerWidth: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerWidth}, - MacroVideoPlayerHeight: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerHeight}, - MacroVideoStartDelay: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoStartDelay}, - MacroVideoPlacement: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlacement}, - MacroVideoLinearity: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoLinearity}, - MacroVideoSkip: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkip}, - MacroVideoSkipMinimum: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipMinimum}, - MacroVideoSkipAfter: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipAfter}, - MacroVideoSequence: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSequence}, - MacroVideoBlockedAttribute: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBlockedAttribute}, - MacroVideoMaximumExtended: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumExtended}, - MacroVideoMinimumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumBitRate}, - MacroVideoMaximumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumBitRate}, - MacroVideoBoxing: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBoxing}, - MacroVideoPlaybackMethod: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlaybackMethod}, - MacroVideoDelivery: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoDelivery}, - MacroVideoPosition: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPosition}, - MacroVideoAPI: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoAPI}, - - //Site - MacroSiteID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteID}, - MacroSiteName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteName}, - MacroSitePage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSitePage}, - MacroSiteReferrer: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteReferrer}, - MacroSiteSearch: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteSearch}, - MacroSiteMobile: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteMobile}, - - //App - MacroAppID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppID}, - MacroAppName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppName}, - MacroAppBundle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppBundle}, - MacroAppStoreURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppStoreURL}, - MacroAppVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppVersion}, - MacroAppPaid: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppPaid}, - - //SiteAppCommon - MacroCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCategory}, - MacroDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDomain}, - MacroSectionCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSectionCategory}, - MacroPageCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPageCategory}, - MacroPrivacyPolicy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPrivacyPolicy}, - MacroKeywords: ¯oCallBack{cached: true, callback: IBidderMacro.MacroKeywords}, - - //Publisher - MacroPubID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubID}, - MacroPubName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubName}, - MacroPubDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubDomain}, - - //Content - MacroContentID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentID}, - MacroContentEpisode: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEpisode}, - MacroContentTitle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentTitle}, - MacroContentSeries: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeries}, - MacroContentSeason: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeason}, - MacroContentArtist: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentArtist}, - MacroContentGenre: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentGenre}, - MacroContentAlbum: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentAlbum}, - MacroContentISrc: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentISrc}, - MacroContentURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentURL}, - MacroContentCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentCategory}, - MacroContentProductionQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentProductionQuality}, - MacroContentVideoQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentVideoQuality}, - MacroContentContext: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentContext}, - - //Producer - MacroProducerID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerID}, - MacroProducerName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerName}, - - //Device - MacroUserAgent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserAgent}, - MacroDNT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDNT}, - MacroLMT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLMT}, - MacroIP: ¯oCallBack{cached: true, callback: IBidderMacro.MacroIP}, - MacroDeviceType: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceType}, - MacroMake: ¯oCallBack{cached: true, callback: IBidderMacro.MacroMake}, - MacroModel: ¯oCallBack{cached: true, callback: IBidderMacro.MacroModel}, - MacroDeviceOS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOS}, - MacroDeviceOSVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOSVersion}, - MacroDeviceWidth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceWidth}, - MacroDeviceHeight: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceHeight}, - MacroDeviceJS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceJS}, - MacroDeviceLanguage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceLanguage}, - MacroDeviceIFA: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceIFA}, - MacroDeviceDIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDSHA1}, - MacroDeviceDIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDMD5}, - MacroDeviceDPIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDSHA1}, - MacroDeviceDPIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDMD5}, - MacroDeviceMACSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACSHA1}, - MacroDeviceMACMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACMD5}, - - //Geo - MacroLatitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLatitude}, - MacroLongitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLongitude}, - MacroCountry: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCountry}, - MacroRegion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroRegion}, - MacroCity: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCity}, - MacroZip: ¯oCallBack{cached: true, callback: IBidderMacro.MacroZip}, - MacroUTCOffset: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUTCOffset}, - - //User - MacroUserID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserID}, - MacroYearOfBirth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroYearOfBirth}, - MacroGender: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGender}, - - //Extension - MacroGDPRConsent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPRConsent}, - MacroGDPR: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPR}, - MacroUSPrivacy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUSPrivacy}, - - //Additional - MacroCacheBuster: ¯oCallBack{cached: false, callback: IBidderMacro.MacroCacheBuster}, -} - -//GetNewDefaultMapper will return clone of default Mapper function -func GetNewDefaultMapper() Mapper { - return _defaultMapper.clone() -} diff --git a/adapters/tagbidder/ibidder_macro.go b/adapters/tagbidder/ibidder_macro.go index ff3806b13f7..27081391181 100644 --- a/adapters/tagbidder/ibidder_macro.go +++ b/adapters/tagbidder/ibidder_macro.go @@ -13,6 +13,7 @@ type IBidderMacro interface { //Helper Function InitBidRequest(request *openrtb.BidRequest) LoadImpression(imp *openrtb.Imp) error + GetBidderKeys() map[string]string SetAdapterConfig(*config.Adapter) SetBidderConfig(*BidderConfig) GetURI() string @@ -23,7 +24,7 @@ type IBidderMacro interface { MacroTimeout(string) string MacroWhitelistSeat(string) string MacroWhitelistLang(string) string - MacroBlockedseat(string) string + MacroBlockedSeat(string) string MacroCurrency(string) string MacroBlockedCategory(string) string MacroBlockedAdvertiser(string) string @@ -163,8 +164,6 @@ type IBidderMacro interface { //Additional MacroCacheBuster(string) string - ConstantValue(string) string - JSONKey(string) string } var bidderMacroMap = map[string]func() IBidderMacro{} @@ -182,3 +181,7 @@ func GetNewBidderMacro(bidder string) (IBidderMacro, error) { } return nil, errors.New(`missing bidder macro`) } + +func init() { + RegisterNewBidderMacro(`spotx`, NewBidderMacro) +} diff --git a/adapters/tagbidder/macro_processor.go b/adapters/tagbidder/macro_processor.go index 3d6314bf5c4..7576868c694 100644 --- a/adapters/tagbidder/macro_processor.go +++ b/adapters/tagbidder/macro_processor.go @@ -2,7 +2,6 @@ package tagbidder import ( "bytes" - "encoding/json" "net/url" "strings" @@ -25,6 +24,7 @@ type MacroProcessor struct { bidderMacro IBidderMacro mapper Mapper macroCache map[string]string + bidderKeys map[string]string } //NewMacroProcessor will process macro's of openrtb bid request @@ -36,11 +36,16 @@ func NewMacroProcessor(bidderMacro IBidderMacro, mapper Mapper) *MacroProcessor } } -//SetMacro : Adding Custom Macro Manually +//SetMacro Adding Custom Macro Manually func (mp *MacroProcessor) SetMacro(key, value string) { mp.macroCache[key] = value } +//SetBidderKeys will flush and set bidder specific keys +func (mp *MacroProcessor) SetBidderKeys(keys map[string]string) { + mp.bidderKeys = keys +} + //processKey : returns value of key macro and status found or not func (mp *MacroProcessor) processKey(key string) (string, bool) { var valueCallback *macroCallBack @@ -50,20 +55,29 @@ func (mp *MacroProcessor) processKey(key string) (string, bool) { found := false for { - value, found = mp.macroCache[tmpKey] - if false == found { - valueCallback, found = mp.mapper[tmpKey] - if found { - //found callback function - value = valueCallback.callback(mp.bidderMacro, tmpKey) + //Search in macro cache + if value, found = mp.macroCache[tmpKey]; found { + break + } + + //Search for bidder keys + if nil != mp.bidderKeys { + if value, found = mp.bidderKeys[tmpKey]; found { break - } else if strings.HasSuffix(tmpKey, macroEscapeSuffix) { - //escaping macro found - tmpKey = tmpKey[0 : len(tmpKey)-macroEscapeSuffixLen] - nEscaping++ - continue } } + + valueCallback, found = mp.mapper[tmpKey] + if found { + //found callback function + value = valueCallback.callback(mp.bidderMacro, tmpKey) + break + } else if strings.HasSuffix(tmpKey, macroEscapeSuffix) { + //escaping macro found + tmpKey = tmpKey[0 : len(tmpKey)-macroEscapeSuffixLen] + nEscaping++ + continue + } break } @@ -191,14 +205,6 @@ func (mp *MacroProcessor) processURLValues(values url.Values, flags Flags) (resp return out.String() } -//Dump : will print all cached macro and its values -func (mp *MacroProcessor) Dump() { - if glog.V(3) { - cacheStr, _ := json.Marshal(mp.macroCache) - glog.Infof("[MACRO]: Map:[%s]", string(cacheStr)) - } -} - //GetMacroKey will return macro formatted key func GetMacroKey(key string) string { return macroPrefix + key + macroSuffix diff --git a/adapters/tagbidder/macro_processor_test.go b/adapters/tagbidder/macro_processor_test.go deleted file mode 100644 index eaed38100f6..00000000000 --- a/adapters/tagbidder/macro_processor_test.go +++ /dev/null @@ -1,586 +0,0 @@ -package tagbidder - -import ( - "encoding/json" - "net/url" - "testing" - - "github.com/PubMatic-OpenWrap/openrtb" - "github.com/stretchr/testify/assert" -) - -func getBidRequest(requestJSON string) *openrtb.BidRequest { - bidRequest := &openrtb.BidRequest{} - json.Unmarshal([]byte(requestJSON), bidRequest) - return bidRequest -} -func TestMacroProcessor_ProcessString(t *testing.T) { - testMacroValues := map[string]string{ - MacroPubID: `pubID`, - MacroTagID: `tagid value`, - MacroTagID + macroEscapeSuffix: `tagid+value`, - MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, - } - - sampleBidRequest := &openrtb.BidRequest{ - Imp: []openrtb.Imp{ - openrtb.Imp{TagID: testMacroValues[MacroTagID]}, - }, - Site: &openrtb.Site{ - Publisher: &openrtb.Publisher{ - ID: testMacroValues[MacroPubID], - }, - }, - } - - type fields struct { - bidRequest *openrtb.BidRequest - } - tests := []struct { - name string - in string - expected string - }{ - { - name: "Empty Input", - in: "", - expected: "", - }, - { - name: "No Macro Replacement", - in: "Hello Test No Macro", - expected: "Hello Test No Macro", - }, - { - name: "Start Macro", - in: GetMacroKey(MacroTagID) + "HELLO", - expected: testMacroValues[MacroTagID] + "HELLO", - }, - { - name: "End Macro", - in: "HELLO" + GetMacroKey(MacroTagID), - expected: "HELLO" + testMacroValues[MacroTagID], - }, - { - name: "Start-End Macro", - in: GetMacroKey(MacroTagID) + "HELLO" + GetMacroKey(MacroTagID), - expected: testMacroValues[MacroTagID] + "HELLO" + testMacroValues[MacroTagID], - }, - { - name: "Half Start Macro", - in: macroPrefix + GetMacroKey(MacroTagID) + "HELLO", - expected: macroPrefix + testMacroValues[MacroTagID] + "HELLO", - }, - { - name: "Half End Macro", - in: "HELLO" + GetMacroKey(MacroTagID) + macroSuffix, - expected: "HELLO" + testMacroValues[MacroTagID] + macroSuffix, - }, - { - name: "Concatenated Macro", - in: GetMacroKey(MacroTagID) + GetMacroKey(MacroTagID) + "HELLO", - expected: testMacroValues[MacroTagID] + testMacroValues[MacroTagID] + "HELLO", - }, - { - name: "Incomplete Concatenation Macro", - in: GetMacroKey(MacroTagID) + macroSuffix + "LINKHELLO", - expected: testMacroValues[MacroTagID] + macroSuffix + "LINKHELLO", - }, - { - name: "Concatenation with Suffix Macro", - in: GetMacroKey(MacroTagID) + macroPrefix + GetMacroKey(MacroTagID) + "HELLO", - expected: testMacroValues[MacroTagID] + macroPrefix + testMacroValues[MacroTagID] + "HELLO", - }, - { - name: "Unknown Macro", - in: GetMacroKey(`UNKNOWN`) + `ABC`, - expected: GetMacroKey(`UNKNOWN`) + `ABC`, - }, - { - name: "Incomplete macro suffix", - in: "START" + macroSuffix, - expected: "START" + macroSuffix, - }, - { - name: "Incomplete Start and End", - in: string(macroPrefix[0]) + GetMacroKey(MacroTagID) + " Value " + GetMacroKey(MacroTagID) + string(macroSuffix[0]), - expected: string(macroPrefix[0]) + testMacroValues[MacroTagID] + " Value " + testMacroValues[MacroTagID] + string(macroSuffix[0]), - }, - { - name: "Special Character", - in: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + GetMacroKey(MacroTagID) + "\" Data", - expected: macroPrefix + MacroTagID + `\n` + macroSuffix + "Sample \"" + testMacroValues[MacroTagID] + "\" Data", - }, - { - name: "Empty Value", - in: GetMacroKey(MacroTimeout) + "Hello", - expected: "Hello", - }, - { - name: "EscapingMacræo", - in: GetMacroKey(MacroTagID), - expected: testMacroValues[MacroTagID], - }, - { - name: "SingleEscapingMacro", - in: GetMacroKey(MacroTagID + macroEscapeSuffix), - expected: testMacroValues[MacroTagID+macroEscapeSuffix], - }, - { - name: "DoubleEscapingMacro", - in: GetMacroKey(MacroTagID + macroEscapeSuffix + macroEscapeSuffix), - expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], - }, - - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bidderMacro := NewBidderMacro() - mapper := GetNewDefaultMapper() - mp := NewMacroProcessor(bidderMacro, mapper) - - //Init Bidder Macro - bidderMacro.InitBidRequest(sampleBidRequest) - bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) - - gotResponse := mp.ProcessString(tt.in) - assert.Equal(t, tt.expected, gotResponse) - }) - } -} - -func TestMacroProcessor_processKey(t *testing.T) { - testMacroValues := map[string]string{ - MacroPubID: `pub id`, - MacroPubID + macroEscapeSuffix: `pub+id`, - MacroTagID: `tagid value`, - MacroTagID + macroEscapeSuffix: `tagid+value`, - MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, - } - - sampleBidRequest := &openrtb.BidRequest{ - Imp: []openrtb.Imp{ - openrtb.Imp{TagID: testMacroValues[MacroTagID]}, - }, - Site: &openrtb.Site{ - Publisher: &openrtb.Publisher{ - ID: testMacroValues[MacroPubID], - }, - }, - } - type args struct { - cache map[string]string - key string - } - type want struct { - expected string - ok bool - cache map[string]string - } - tests := []struct { - name string - args args - want want - }{ - { - name: `emptyKey`, - args: args{}, - want: want{ - expected: "", - ok: false, - cache: map[string]string{}, - }, - }, - { - name: `cachedKeyFound`, - args: args{ - cache: map[string]string{ - MacroPubID: testMacroValues[MacroPubID], - }, - key: MacroPubID, - }, - want: want{ - expected: testMacroValues[MacroPubID], - ok: true, - cache: map[string]string{ - MacroPubID: testMacroValues[MacroPubID], - }, - }, - }, - { - name: `valueFound`, - args: args{ - key: MacroTagID, - }, - want: want{ - expected: testMacroValues[MacroTagID], - ok: true, - cache: map[string]string{}, - }, - }, - { - name: `2TimesEscaping`, - args: args{ - key: MacroTagID + macroEscapeSuffix + macroEscapeSuffix, - }, - want: want{ - expected: testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], - ok: true, - cache: map[string]string{}, - }, - }, - { - name: `macroNotPresent`, - args: args{ - key: `Unknown`, - }, - want: want{ - expected: "", - ok: false, - cache: map[string]string{}, - }, - }, - { - name: `macroNotPresentInEscaping`, - args: args{ - key: `Unknown` + macroEscapeSuffix, - }, - want: want{ - expected: "", - ok: false, - cache: map[string]string{}, - }, - }, - { - name: `cachedKey`, - args: args{ - key: MacroPubID, - }, - want: want{ - expected: testMacroValues[MacroPubID], - ok: true, - cache: map[string]string{ - MacroPubID: testMacroValues[MacroPubID], - }, - }, - }, - { - name: `cachedEscapingKey`, - args: args{ - key: MacroPubID + macroEscapeSuffix, - }, - want: want{ - expected: testMacroValues[MacroPubID+macroEscapeSuffix], - ok: true, - cache: map[string]string{ - MacroPubID + macroEscapeSuffix: testMacroValues[MacroPubID+macroEscapeSuffix], - }, - }, - }, - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bidderMacro := NewBidderMacro() - mapper := GetNewDefaultMapper() - mp := NewMacroProcessor(bidderMacro, mapper) - - //init bidder macro - bidderMacro.InitBidRequest(sampleBidRequest) - bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) - - //init cache of macro processor - if nil != tt.args.cache { - mp.macroCache = tt.args.cache - } - - actual, ok := mp.processKey(tt.args.key) - assert.Equal(t, tt.want.expected, actual) - assert.Equal(t, tt.want.ok, ok) - assert.Equal(t, tt.want.cache, mp.macroCache) - }) - } -} - -func TestMacroProcessor_processURLValues(t *testing.T) { - testMacroValues := map[string]string{ - MacroPubID: `pub id`, - MacroPubID + macroEscapeSuffix: `pub+id`, - MacroTagID: `tagid value`, - MacroTagID + macroEscapeSuffix: `tagid+value`, - MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, - } - - sampleBidRequest := &openrtb.BidRequest{ - Imp: []openrtb.Imp{ - openrtb.Imp{TagID: testMacroValues[MacroTagID]}, - }, - Site: &openrtb.Site{ - Publisher: &openrtb.Publisher{ - ID: testMacroValues[MacroPubID], - }, - }, - } - type args struct { - values url.Values - flags Flags - } - tests := []struct { - name string - args args - want url.Values - }{ - { - name: `AllEmptyParamsRemovedEmptyParams`, - args: args{ - values: url.Values{ - `k1`: []string{GetMacroKey(MacroPubName)}, - `k2`: []string{GetMacroKey(MacroPubName)}, - `k3`: []string{GetMacroKey(MacroPubName)}, - }, - flags: Flags{ - RemoveEmptyParam: true, - }, - }, - want: url.Values{}, - }, - { - name: `AllEmptyParamsKeepEmptyParams`, - args: args{ - values: url.Values{ - `k1`: []string{GetMacroKey(MacroPubName)}, - `k2`: []string{GetMacroKey(MacroPubName)}, - `k3`: []string{GetMacroKey(MacroPubName)}, - }, - flags: Flags{ - RemoveEmptyParam: false, - }, - }, - want: url.Values{ - `k1`: []string{""}, - `k2`: []string{""}, - `k3`: []string{""}, - }, - }, - { - name: `MixedParamsRemoveEmptyParams`, - args: args{ - values: url.Values{ - `k1`: []string{GetMacroKey(MacroPubID)}, - `k2`: []string{GetMacroKey(MacroPubName)}, - `k3`: []string{GetMacroKey(MacroTagID)}, - }, - flags: Flags{ - RemoveEmptyParam: true, - }, - }, - want: url.Values{ - `k1`: []string{testMacroValues[MacroPubID]}, - `k3`: []string{testMacroValues[MacroTagID]}, - }, - }, - { - name: `MixedParamsKeepEmptyParams`, - args: args{ - values: url.Values{ - `k1`: []string{GetMacroKey(MacroPubID)}, - `k2`: []string{GetMacroKey(MacroPubName)}, - `k3`: []string{GetMacroKey(MacroTagID)}, - `k4`: []string{`UNKNOWN`}, - `k5`: []string{GetMacroKey(`UNKNOWN`)}, - }, - flags: Flags{ - RemoveEmptyParam: false, - }, - }, - want: url.Values{ - `k1`: []string{testMacroValues[MacroPubID]}, - `k2`: []string{""}, - `k3`: []string{testMacroValues[MacroTagID]}, - `k4`: []string{`UNKNOWN`}, - `k5`: []string{GetMacroKey(`UNKNOWN`)}, - }, - }, - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bidderMacro := NewBidderMacro() - mapper := GetNewDefaultMapper() - mp := NewMacroProcessor(bidderMacro, mapper) - - //init bidder macro - bidderMacro.InitBidRequest(sampleBidRequest) - bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) - - actual := mp.processURLValues(tt.args.values, tt.args.flags) - - actualValues, _ := url.ParseQuery(actual) - assert.Equal(t, tt.want, actualValues) - }) - } -} - -func TestMacroProcessor_processURLValuesEscapingKeys(t *testing.T) { - testMacroValues := map[string]string{ - MacroPubID: `pub id`, - MacroPubID + macroEscapeSuffix: `pub+id`, - MacroTagID: `tagid value`, - MacroTagID + macroEscapeSuffix: `tagid+value`, - MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, - } - - sampleBidRequest := &openrtb.BidRequest{ - Imp: []openrtb.Imp{ - openrtb.Imp{TagID: testMacroValues[MacroTagID]}, - }, - Site: &openrtb.Site{ - Publisher: &openrtb.Publisher{ - ID: testMacroValues[MacroPubID], - }, - }, - } - type args struct { - key string - value string - } - tests := []struct { - name string - args args - want string - }{ - { - name: `EmptyKeyValue`, - args: args{}, - want: ``, - }, - { - name: `WithoutEscaping`, - args: args{key: `k1`, value: GetMacroKey(MacroTagID)}, - want: `k1=` + testMacroValues[MacroTagID], - }, - { - name: `WithEscaping`, - args: args{key: `k1`, value: GetMacroKey(MacroTagID + macroEscapeSuffix)}, - want: `k1=` + testMacroValues[MacroTagID+macroEscapeSuffix], - }, - { - name: `With2LevelEscaping`, - args: args{key: `k1`, value: GetMacroKey(MacroTagID + macroEscapeSuffix + macroEscapeSuffix)}, - want: `k1=` + testMacroValues[MacroTagID+macroEscapeSuffix+macroEscapeSuffix], - }, - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bidderMacro := NewBidderMacro() - mapper := GetNewDefaultMapper() - mp := NewMacroProcessor(bidderMacro, mapper) - - //init bidder macro - bidderMacro.InitBidRequest(sampleBidRequest) - bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) - - values := url.Values{} - if len(tt.args.key) > 0 { - values.Add(tt.args.key, tt.args.value) - } - - actual := mp.processURLValues(values, Flags{}) - assert.Equal(t, tt.want, actual) - }) - } -} - -func TestMacroProcessor_ProcessURL(t *testing.T) { - testMacroValues := map[string]string{ - MacroPubID: `123`, - MacroPubID + macroEscapeSuffix: `123`, - MacroSiteID: `567`, - MacroTagID: `tagid value`, - MacroTagID + macroEscapeSuffix: `tagid+value`, - MacroTagID + macroEscapeSuffix + macroEscapeSuffix: `tagid%2Bvalue`, - } - - sampleBidRequest := &openrtb.BidRequest{ - Imp: []openrtb.Imp{ - openrtb.Imp{TagID: testMacroValues[MacroTagID]}, - }, - Site: &openrtb.Site{ - ID: testMacroValues[MacroSiteID], - Publisher: &openrtb.Publisher{ - ID: testMacroValues[MacroPubID], - }, - }, - } - - type args struct { - uri string - flags Flags - } - tests := []struct { - name string - args args - wantResponse string - }{ - { - name: "EmptyURI", - args: args{ - uri: ``, - flags: Flags{RemoveEmptyParam: true}, - }, - wantResponse: ``, - }, - { - name: "RemovedEmptyParams", - args: args{ - uri: `http://xyz.domain.com/` + GetMacroKey(MacroPubID) + `/` + GetMacroKey(MacroSiteID) + `?tagID=` + GetMacroKey(MacroTagID+macroEscapeSuffix) + `¬found=` + GetMacroKey(MacroTimeout) + `&k1=v1&k2=v2`, - flags: Flags{RemoveEmptyParam: true}, - }, - wantResponse: `http://xyz.domain.com/123/567?tagID=tagid+value&k1=v1&k2=v2`, - }, - { - name: "RemovedEmptyParams", - args: args{ - uri: `http://xyz.domain.com/` + GetMacroKey(MacroPubID) + `/` + GetMacroKey(MacroSiteID) + `?tagID=` + GetMacroKey(MacroTagID+macroEscapeSuffix) + `¬found=` + GetMacroKey(MacroTimeout) + `&k1=v1&k2=v2`, - flags: Flags{RemoveEmptyParam: false}, - }, - wantResponse: `http://xyz.domain.com/123/567?tagID=tagid+value¬found=&k1=v1&k2=v2`, - }, - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bidderMacro := NewBidderMacro() - mapper := GetNewDefaultMapper() - mp := NewMacroProcessor(bidderMacro, mapper) - - //init bidder macro - bidderMacro.InitBidRequest(sampleBidRequest) - bidderMacro.LoadImpression(&sampleBidRequest.Imp[0]) - - gotResponse := mp.ProcessURL(tt.args.uri, tt.args.flags) - assertURL(t, tt.wantResponse, gotResponse) - }) - } -} - -func assertURL(t *testing.T, expected, actual string) { - actualURL, _ := url.Parse(actual) - expectedURL, _ := url.Parse(expected) - - if nil == actualURL || nil == expectedURL { - assert.True(t, (nil == actualURL) == (nil == expectedURL), `actual or expected url parsing failed`) - } else { - assert.Equal(t, expectedURL.Scheme, actualURL.Scheme) - assert.Equal(t, expectedURL.Opaque, actualURL.Opaque) - assert.Equal(t, expectedURL.User, actualURL.User) - assert.Equal(t, expectedURL.Host, actualURL.Host) - assert.Equal(t, expectedURL.Path, actualURL.Path) - assert.Equal(t, expectedURL.RawPath, actualURL.RawPath) - assert.Equal(t, expectedURL.ForceQuery, actualURL.ForceQuery) - assert.Equal(t, expectedURL.Query(), actualURL.Query()) - assert.Equal(t, expectedURL.Fragment, actualURL.Fragment) - } -} diff --git a/adapters/tagbidder/mapper.go b/adapters/tagbidder/mapper.go index 0c55d34d9df..79a5eca4708 100644 --- a/adapters/tagbidder/mapper.go +++ b/adapters/tagbidder/mapper.go @@ -17,42 +17,155 @@ func (obj Mapper) clone() Mapper { return cloned } -//NewMapperFromConfig returns new Mapper from JSON details -func NewMapperFromConfig(config *BidderConfig) Mapper { - newMapper := GetNewDefaultMapper() - for macro, key := range config.Keys { - macroCB, ok := newMapper[macro] - if !ok { - //create new entry if not present - macroCB = ¯oCallBack{cached: false, callback: IBidderMacro.ConstantValue} - newMapper[macro] = macroCB - } - - //default definition - switch key.ValueType { - case JSONKeyValueType: /*json key value*/ - macroCB.callback = IBidderMacro.JSONKey - case ConstantValueType: /*constant*/ - macroCB.callback = IBidderMacro.ConstantValue - } - - //Cache Key - if nil != key.Cached && *key.Cached { - macroCB.cached = true - } - } - return newMapper -} +var _defaultMapper = Mapper{ + //Request + MacroTest: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTest}, + MacroTimeout: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTimeout}, + MacroWhitelistSeat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistSeat}, + MacroWhitelistLang: ¯oCallBack{cached: true, callback: IBidderMacro.MacroWhitelistLang}, + MacroBlockedSeat: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedSeat}, + MacroCurrency: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCurrency}, + MacroBlockedCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedCategory}, + MacroBlockedAdvertiser: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedAdvertiser}, + MacroBlockedApp: ¯oCallBack{cached: true, callback: IBidderMacro.MacroBlockedApp}, -/* -//SetCache value to specific key -func (obj *Mapper) SetCache(key string, value bool) { - if value, ok := (*obj)[key]; ok { - value.cached = true - } + //Source + MacroFD: ¯oCallBack{cached: true, callback: IBidderMacro.MacroFD}, + MacroTransactionID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroTransactionID}, + MacroPaymentIDChain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPaymentIDChain}, + + //Regs + MacroCoppa: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCoppa}, + + //Impression + MacroDisplayManager: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManager}, + MacroDisplayManagerVersion: ¯oCallBack{cached: false, callback: IBidderMacro.MacroDisplayManagerVersion}, + MacroInterstitial: ¯oCallBack{cached: false, callback: IBidderMacro.MacroInterstitial}, + MacroTagID: ¯oCallBack{cached: false, callback: IBidderMacro.MacroTagID}, + MacroBidFloor: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloor}, + MacroBidFloorCurrency: ¯oCallBack{cached: false, callback: IBidderMacro.MacroBidFloorCurrency}, + MacroSecure: ¯oCallBack{cached: false, callback: IBidderMacro.MacroSecure}, + MacroPMP: ¯oCallBack{cached: false, callback: IBidderMacro.MacroPMP}, + + //Video + MacroVideoMIMES: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMIMES}, + MacroVideoMinimumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumDuration}, + MacroVideoMaximumDuration: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumDuration}, + MacroVideoProtocols: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoProtocols}, + MacroVideoPlayerWidth: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerWidth}, + MacroVideoPlayerHeight: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlayerHeight}, + MacroVideoStartDelay: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoStartDelay}, + MacroVideoPlacement: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlacement}, + MacroVideoLinearity: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoLinearity}, + MacroVideoSkip: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkip}, + MacroVideoSkipMinimum: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipMinimum}, + MacroVideoSkipAfter: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSkipAfter}, + MacroVideoSequence: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoSequence}, + MacroVideoBlockedAttribute: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBlockedAttribute}, + MacroVideoMaximumExtended: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumExtended}, + MacroVideoMinimumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMinimumBitRate}, + MacroVideoMaximumBitRate: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoMaximumBitRate}, + MacroVideoBoxing: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoBoxing}, + MacroVideoPlaybackMethod: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPlaybackMethod}, + MacroVideoDelivery: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoDelivery}, + MacroVideoPosition: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoPosition}, + MacroVideoAPI: ¯oCallBack{cached: false, callback: IBidderMacro.MacroVideoAPI}, + + //Site + MacroSiteID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteID}, + MacroSiteName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteName}, + MacroSitePage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSitePage}, + MacroSiteReferrer: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteReferrer}, + MacroSiteSearch: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteSearch}, + MacroSiteMobile: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSiteMobile}, + + //App + MacroAppID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppID}, + MacroAppName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppName}, + MacroAppBundle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppBundle}, + MacroAppStoreURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppStoreURL}, + MacroAppVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppVersion}, + MacroAppPaid: ¯oCallBack{cached: true, callback: IBidderMacro.MacroAppPaid}, + + //SiteAppCommon + MacroCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCategory}, + MacroDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDomain}, + MacroSectionCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroSectionCategory}, + MacroPageCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPageCategory}, + MacroPrivacyPolicy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPrivacyPolicy}, + MacroKeywords: ¯oCallBack{cached: true, callback: IBidderMacro.MacroKeywords}, + + //Publisher + MacroPubID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubID}, + MacroPubName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubName}, + MacroPubDomain: ¯oCallBack{cached: true, callback: IBidderMacro.MacroPubDomain}, + + //Content + MacroContentID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentID}, + MacroContentEpisode: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentEpisode}, + MacroContentTitle: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentTitle}, + MacroContentSeries: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeries}, + MacroContentSeason: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentSeason}, + MacroContentArtist: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentArtist}, + MacroContentGenre: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentGenre}, + MacroContentAlbum: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentAlbum}, + MacroContentISrc: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentISrc}, + MacroContentURL: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentURL}, + MacroContentCategory: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentCategory}, + MacroContentProductionQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentProductionQuality}, + MacroContentVideoQuality: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentVideoQuality}, + MacroContentContext: ¯oCallBack{cached: true, callback: IBidderMacro.MacroContentContext}, + + //Producer + MacroProducerID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerID}, + MacroProducerName: ¯oCallBack{cached: true, callback: IBidderMacro.MacroProducerName}, + + //Device + MacroUserAgent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserAgent}, + MacroDNT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDNT}, + MacroLMT: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLMT}, + MacroIP: ¯oCallBack{cached: true, callback: IBidderMacro.MacroIP}, + MacroDeviceType: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceType}, + MacroMake: ¯oCallBack{cached: true, callback: IBidderMacro.MacroMake}, + MacroModel: ¯oCallBack{cached: true, callback: IBidderMacro.MacroModel}, + MacroDeviceOS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOS}, + MacroDeviceOSVersion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceOSVersion}, + MacroDeviceWidth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceWidth}, + MacroDeviceHeight: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceHeight}, + MacroDeviceJS: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceJS}, + MacroDeviceLanguage: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceLanguage}, + MacroDeviceIFA: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceIFA}, + MacroDeviceDIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDSHA1}, + MacroDeviceDIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDIDMD5}, + MacroDeviceDPIDSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDSHA1}, + MacroDeviceDPIDMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceDPIDMD5}, + MacroDeviceMACSHA1: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACSHA1}, + MacroDeviceMACMD5: ¯oCallBack{cached: true, callback: IBidderMacro.MacroDeviceMACMD5}, + + //Geo + MacroLatitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLatitude}, + MacroLongitude: ¯oCallBack{cached: true, callback: IBidderMacro.MacroLongitude}, + MacroCountry: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCountry}, + MacroRegion: ¯oCallBack{cached: true, callback: IBidderMacro.MacroRegion}, + MacroCity: ¯oCallBack{cached: true, callback: IBidderMacro.MacroCity}, + MacroZip: ¯oCallBack{cached: true, callback: IBidderMacro.MacroZip}, + MacroUTCOffset: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUTCOffset}, + + //User + MacroUserID: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUserID}, + MacroYearOfBirth: ¯oCallBack{cached: true, callback: IBidderMacro.MacroYearOfBirth}, + MacroGender: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGender}, + + //Extension + MacroGDPRConsent: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPRConsent}, + MacroGDPR: ¯oCallBack{cached: true, callback: IBidderMacro.MacroGDPR}, + MacroUSPrivacy: ¯oCallBack{cached: true, callback: IBidderMacro.MacroUSPrivacy}, + + //Additional + MacroCacheBuster: ¯oCallBack{cached: false, callback: IBidderMacro.MacroCacheBuster}, } -//AddCustomMacro for adding custom macro whose definition will be present in IBidderMacro.Custom method -func (obj *Mapper) AddCustomMacro(key string, isCached bool) { - (*obj)[key] = ¯oCallBack{cached: isCached, callback: IBidderMacro.Custom} -}*/ +//GetNewDefaultMapper will return clone of default Mapper function +func GetNewDefaultMapper() Mapper { + return _defaultMapper.clone() +} diff --git a/adapters/tagbidder/spotxtag/constant.go.bak b/adapters/tagbidder/spotxtag/constant.go.bak deleted file mode 100644 index 066d14b6306..00000000000 --- a/adapters/tagbidder/spotxtag/constant.go.bak +++ /dev/null @@ -1,6 +0,0 @@ -package spotxtag - -const ( - spotxURL = `https://search.spotxchange.com/vast/2.00/85394?VPI=MP4&app[bundle]=[REPLACE_ME]&app[name]=[REPLACE_ME]&app[cat]=[REPLACE_ME]&app[domain]=[REPLACE_ME]&app[privacypolicy]=[REPLACE_ME]&app[storeurl]=[REPLACE_ME]&app[ver]=[REPLACE_ME]&cb=[REPLACE_ME]&device[devicetype]=[REPLACE_ME]&device[ifa]=[REPLACE_ME]&device[make]=[REPLACE_ME]&device[model]=[REPLACE_ME]&device[dnt]=[REPLACE_ME]&player_height=[REPLACE_ME]&player_width=[REPLACE_ME]&ip_addr=[REPLACE_ME]&device[ua]=[REPLACE_ME]]&schain=[REPLACE_ME]` - spotxURLWithMacros = `https://search.spotxchange.com/vast/2.00/%%Channel%%?VPI=MP4&app[bundle]=%%MacroAppBundle%%&app[name]=%%MacroAppName%%&app[cat]=%%MacroCategory%%&app[domain]=%%MacroDomain%%&app[privacypolicy]=%%MacroPrivacyPolicy%%&app[storeurl]=%%MacroAppStoreURL%%&app[ver]=%%MacroAppVersion%%&cb=%%MacroCacheBuster%%&device[devicetype]=%%MacroDeviceType%%&device[ifa]=%%MacroDeviceIFA%%&device[make]=%%MacroMake%%&device[model]=%%MacroModel%%&device[dnt]=%%MacroDNT%%&player_height=%%MacroVideoPlayerHeight%%&player_width=%%MacroVideoPlayerWidth%%&ip_addr=%%MacroIP%%&device[ua]=%%MacroUserAgent%%` -) diff --git a/adapters/tagbidder/spotxtag/spotx_adapter.go.bak b/adapters/tagbidder/spotxtag/spotx_adapter.go.bak deleted file mode 100644 index 80157dc402f..00000000000 --- a/adapters/tagbidder/spotxtag/spotx_adapter.go.bak +++ /dev/null @@ -1,34 +0,0 @@ -package spotxtag - -/* -import ( - "github.com/PubMatic-OpenWrap/prebid-server/adapters/tagbidder" -) - -//SpotxAdapter partner adapter -type SpotxAdapter struct { - *tagbidder.TagBidder - uri string -} - -//NewSpotxAdapter new object -func NewSpotxAdapter(bidderName, uri string) *SpotxAdapter { - return &SpotxAdapter{ - TagBidder: tagbidder.NewTagBidder(bidderName), - uri: uri, - } -} - -//MakeRequests make new requests -func (a *SpotxAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - //request validation can be done here independently - return a.TagBidder.MakeRequests(request, reqInfo) -} - -//MakeBids makes bids -func (a *SpotxAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { - //response validation can be done here independently - return a.TagBidder.MakeBids(internalRequest, externalRequest, response) -} - -*/ diff --git a/adapters/tagbidder/spotxtag/spotx_macro.go.bak b/adapters/tagbidder/spotxtag/spotx_macro.go.bak deleted file mode 100644 index 7639beba782..00000000000 --- a/adapters/tagbidder/spotxtag/spotx_macro.go.bak +++ /dev/null @@ -1,99 +0,0 @@ -package spotxtag - -import ( - "encoding/json" - - "github.com/PubMatic-OpenWrap/openrtb" - "github.com/PubMatic-OpenWrap/prebid-server/adapters" - "github.com/PubMatic-OpenWrap/prebid-server/adapters/tagbidder" - "github.com/PubMatic-OpenWrap/prebid-server/errortypes" - "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext" -) - -//SpotxMacro contains openrtb macros for spotx adapter -type SpotxMacro struct { - *tagbidder.BidderMacro - - /*bidder specific extensions*/ - ext *openrtb_ext.ExtImpSpotX -} - -//NewSpotxMacro contains spotx specific parameter parsing -func NewSpotxMacro() tagbidder.IBidderMacro { - bidder := &SpotxMacro{ - BidderMacro: tagbidder.NewBidderMacro(), - } - return bidder -} - -//LoadImpression will set current imp -func (tag *SpotxMacro) LoadImpression(imp *openrtb.Imp) error { - tag.BidderMacro.LoadImpression(imp) - - //reload ext object - var bidderExt adapters.ExtImpBidder - if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { - return &errortypes.BadInput{Message: err.Error()} - } - - var spotxExt openrtb_ext.ExtImpSpotX - if err := json.Unmarshal(bidderExt.Bidder, &spotxExt); err != nil { - return &errortypes.BadInput{Message: err.Error()} - } - - tag.ext = &spotxExt - return nil -} - -//Custom contains definition for CacheBuster Parameter -func (tag *SpotxMacro) Custom(key string) string { - //Second Method - switch key { - case `channel_id`: - //do processing - return tag.ext.ChannelID - } - return "" -} - -//MacroVideoAPI overriding default behaviour of MacroVideoAPI -func (tag *SpotxMacro) MacroVideoAPI(key string) string { - return "MP4" -} - -func init() { - tagbidder.RegisterNewBidderMacro(`spotx`, NewSpotxMacro) -} - -/* -Custom Mapper Example -var spotxCustomMapper map[string]func(*SpotxMacro) string - -//Second Method of Adding Custom Macro's -func addCustomMacro(key string, cached bool, callback func(*SpotxMacro) string) { - spotxMapper.AddCustomMacro(key, cached) - spotxCustomMapper[key] = callback -} - -//Second Method -addCustomMacro(`channel_id`, false, channelID) - -//Custom contains definition for CacheBuster Parameter -func (tag *SpotxMacro) Custom(key string) string { - //First Method - if callback, ok := spotxCustomMapper[key]; ok { - return callback(tag) - } -} - -func channelID(tag *SpotxMacro) string { - return tag.ext.ChannelID -} - -*/ - -/* -https://search.spotxchange.com/vast/2.00/85394?VPI=MP4&app[bundle]=[REPLACE_ME]&app[name]=[REPLACE_ME]&app[cat]=[REPLACE_ME]&app[domain]=[REPLACE_ME]&app[privacypolicy]=[REPLACE_ME]&app[storeurl]=[REPLACE_ME]&app[ver]=[REPLACE_ME]&cb=[REPLACE_ME]&device[devicetype]=[REPLACE_ME]&device[ifa]=[REPLACE_ME]&device[make]=[REPLACE_ME]&device[model]=[REPLACE_ME]&device[dnt]=[REPLACE_ME]&player_height=[REPLACE_ME]&player_width=[REPLACE_ME]&ip_addr=[REPLACE_ME]&device[ua]=[REPLACE_ME]]&schain=[REPLACE_ME] - -https://search.spotxchange.com/vast/2.00/85394?VPI=MP4&app[bundle]=roku.weatherapp&app[name]=myctvapp&app[cat]=IAB6-8&app[domain]=http%3A%2F%2Fpublishername.com/appname&app[privacypolicy]=1&app[storeurl]=http%3A%2F%2Fchannelstore.roku.com/details/11055/weatherapp&app[ver]=1.2.1&cb=7437276459847&device[devicetype]=7&device[ifa]=236A005B-700F-4889-B9CE-999EAB2B605D&device[make]=Roku&device[model]=Roku&device[dnt]=0&player_height=1080&player_width=1920&ip_addr=165.23.234.23&device[ua]=Roku%2FDVP-7.10%2520(047.10E04062A)]&schain=1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com,ext_stuff!exchange2.com,abcd,1,bid-request2,intermediary,intermediary.com,other_ext_stuff -*/ diff --git a/adapters/tagbidder/spotxtag/spotx_mapper.go.bak b/adapters/tagbidder/spotxtag/spotx_mapper.go.bak deleted file mode 100644 index 4288596e6d6..00000000000 --- a/adapters/tagbidder/spotxtag/spotx_mapper.go.bak +++ /dev/null @@ -1,16 +0,0 @@ -package spotxtag - -/* -var spotxMapper tagbidder.Mapper -func init() { - spotxMapper = tagbidder.GetNewDefaultMapper() - - //updating parameter caching status - spotxMapper.SetCache(MacroTest, true) - - //adding custom macros - spotxMapper.AddCustomMacro(`ad_unit"`,false) - - tagbidder.RegisterBidderMapper(`spotx`, spotxMapper) -} -*/ diff --git a/adapters/tagbidder/tagbidder.go b/adapters/tagbidder/tagbidder.go index 2fe16fd0686..26bf9b45687 100644 --- a/adapters/tagbidder/tagbidder.go +++ b/adapters/tagbidder/tagbidder.go @@ -2,6 +2,7 @@ package tagbidder import ( "errors" + "fmt" "github.com/PubMatic-OpenWrap/openrtb" "github.com/PubMatic-OpenWrap/prebid-server/adapters" @@ -50,7 +51,8 @@ func (a *TagBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters. return nil, []error{err} } - bidderMapper := GetBidderMapper(a.bidderName) + //bidderMapper := GetBidderMapper(a.bidderName) + bidderMapper := GetNewDefaultMapper() if nil == bidderMapper { return nil, []error{errors.New(`missing bidder mapper`)} } @@ -68,6 +70,11 @@ func (a *TagBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters. continue } + //Setting Bidder Level Keys + bidderKeys := bidderMacro.GetBidderKeys() + macroProcessor.SetBidderKeys(bidderKeys) + fmt.Printf("\n[V2] Bidder Keys:%v", bidderKeys) + uri := macroProcessor.ProcessURL(bidderMacro.GetURI(), a.bidderConfig.Flags) requestData = append(requestData, &adapters.RequestData{ diff --git a/adapters/tagbidder/util.go b/adapters/tagbidder/util.go index a7cb2c8ce78..e59199d943e 100644 --- a/adapters/tagbidder/util.go +++ b/adapters/tagbidder/util.go @@ -2,6 +2,9 @@ package tagbidder import ( "bytes" + "fmt" + "math/rand" + "strconv" ) func objectArrayToString(len int, separator string, cb func(i int) string) string { @@ -18,3 +21,33 @@ func objectArrayToString(len int, separator string, cb func(i int) string) strin } return out.String() } + +func normalizeObject(prefix string, out map[string]string, obj map[string]interface{}) { + for k, value := range obj { + key := k + if len(prefix) > 0 { + key = prefix + "." + k + } + + switch val := value.(type) { + case string: + out[key] = val + case []interface{}: //array + continue + case map[string]interface{}: //object + normalizeObject(key, out, val) + default: //all int, float + out[key] = fmt.Sprint(value) + } + } +} + +func normalizeJSON(obj map[string]interface{}) map[string]string { + out := map[string]string{} + normalizeObject("", out, obj) + return out +} + +var getRandomID = func() string { + return strconv.FormatInt(rand.Int63(), intBase) +} diff --git a/adapters/tagbidder/vast_tag_response_handler.go b/adapters/tagbidder/vast_tag_response_handler.go index 1f4a09cce63..34244501293 100644 --- a/adapters/tagbidder/vast_tag_response_handler.go +++ b/adapters/tagbidder/vast_tag_response_handler.go @@ -3,7 +3,6 @@ package tagbidder import ( "errors" "fmt" - "math/rand" "net/http" "regexp" "strconv" @@ -190,14 +189,6 @@ func getPricingDetails(version string, ad *etree.Element) (float64, string, bool return priceValue, currency, true } -func getCreativeID(ad *etree.Element) string { - return getRandomID() -} - -var getRandomID = func() string { - return strconv.FormatInt(rand.Int63(), intBase) -} - // getDuration extracts the duration of the bid from input creative of Linear type. // The lookup may vary from vast version provided in the input // returns duration in seconds or error if failed to obtained the duration. diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go index 88b49a3428f..704f09de780 100644 --- a/pbsmetrics/config/metrics.go +++ b/pbsmetrics/config/metrics.go @@ -309,7 +309,6 @@ func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType p func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) { } - // RecordAdapterDuplicateBidID as a noop func (me *DummyMetricsEngine) RecordAdapterDuplicateBidID(adaptor string, collisions int) { } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 90af24b27ea..3cb13525647 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -247,8 +247,8 @@ func TestApply(t *testing.T) { test.enforcement.apply(req, test.ampGDPRException, m) m.AssertExpectations(t) - assert.Same(t, replacedDevice, req.Device, "Device") - assert.Same(t, replacedUser, req.User, "User") + assert.Equal(t, replacedDevice, req.Device, "Device") + assert.Equal(t, replacedUser, req.User, "User") } } diff --git a/router/router_test.go b/router/router_test.go index 32aec6cc9d3..866b93de980 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -53,7 +53,7 @@ func TestNewJsonDirectoryServer(t *testing.T) { } for _, adapterFile := range adapterFiles { - if adapterFile.IsDir() && adapterFile.Name() != "adapterstest" { + if adapterFile.IsDir() && adapterFile.Name() != "adapterstest" && adapterFile.Name() != "tagbidder" { ensureHasKey(t, data, adapterFile.Name()) } } diff --git a/static/tagbidder-params/spotx.json b/static/tagbidder-params/spotx.json index 185a1642da5..d8f8fcf5d51 100644 --- a/static/tagbidder-params/spotx.json +++ b/static/tagbidder-params/spotx.json @@ -1,43 +1,7 @@ { - "url": "https://search.spotxchange.com/vast/2.00/{{channel_id}}?VPI=MP4&app[bundle]={{MacroAppBundle}}&app[name]={{MacroAppName}}&app[cat]={{MacroCategory}}&app[domain]={{MacroDomain}}&app[privacypolicy]={{MacroPrivacyPolicy}}&app[storeurl]={{MacroAppStoreURL_ESC}}&app[ver]={{MacroAppVersion}}&cb={{MacroCacheBuster}}&device[devicetype]={{MacroDeviceType}}&device[ifa]={{MacroDeviceIFA}}&device[make]={{MacroMake}}&device[model]={{MacroModel}}&device[dnt]={{MacroDNT}}&player_height={{MacroVideoPlayerHeight}}&player_width={{MacroVideoPlayerWidth}}&ip_addr={{MacroIP}}&device[ua]={{MacroUserAgent_ESC}}", + "url": "https://search.spotxchange.com/vast/2.00/{{channel_id}}?VPI=MP4&app[bundle]={{bundle}}&app[name]={{appname}}&app[cat]={{cat}}&app[domain]={{domain}}&app[privacypolicy]={{privacypolicy}}&app[storeurl]={{storeurl_ESC}}&app[ver]={{appver}}&cb={{cachebuster}}&device[devicetype]={{devicetype}}&device[ifa]={{ifa}}&device[make]={{make}}&device[model]={{model}}&device[dnt]={{dnt}}&player_height={{playerheight}}&player_width={{playerwidth}}&ip_addr={{ip}}&device[ua]={{useragent_ESC}}", "response": "vasttag", "flags": { "remove_empty": true - }, - "keys": { - "MacroVideoAPI": { - "value": "MP4", - "type": "constant" - }, - "channel_id": { - "cached": false, - "value": "impext.bidder.channel_id", - "type": "jsonkey" - }, - "ad_unit": { - "cached": false, - "value": "impext.bidder.ad_unit", - "type": "jsonkey" - }, - "secure": { - "cached": false, - "value": "impext.bidder.secure", - "type": "jsonkey" - }, - "ad_volume": { - "cached": false, - "value": "impext.bidder.ad_volume", - "type": "jsonkey" - }, - "price_floor": { - "cached": false, - "value": "impext.bidder.price_floor", - "type": "jsonkey" - }, - "hide_skin": { - "cached": false, - "value": "impext.bidder.hide_skin", - "type": "jsonkey" - } } } \ No newline at end of file From 12a87111be89970e5ee1b742bdcc0cecf15b355c Mon Sep 17 00:00:00 2001 From: Viral Vala Date: Mon, 4 Jan 2021 16:41:24 +0530 Subject: [PATCH 325/381] OTT-55 Reverting Merge Conflicts --- privacy/enforcement_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 3cb13525647..90af24b27ea 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -247,8 +247,8 @@ func TestApply(t *testing.T) { test.enforcement.apply(req, test.ampGDPRException, m) m.AssertExpectations(t) - assert.Equal(t, replacedDevice, req.Device, "Device") - assert.Equal(t, replacedUser, req.User, "User") + assert.Same(t, replacedDevice, req.Device, "Device") + assert.Same(t, replacedUser, req.User, "User") } } From 277762b1cb2144d048e4912e22b08266b074629c Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 5 Jan 2021 10:21:33 -0500 Subject: [PATCH 326/381] Remove legacy GDPR AMP config flag used to prevent buyer ID scrub on AMP requests (#1565) --- config/config.go | 2 +- endpoints/auction_test.go | 4 --- endpoints/cookie_sync_test.go | 4 --- endpoints/setuid_test.go | 4 --- exchange/utils.go | 3 +- exchange/utils_test.go | 4 --- gdpr/gdpr.go | 3 -- gdpr/impl.go | 12 ------- privacy/enforcement.go | 12 +++---- privacy/enforcement_test.go | 64 +++-------------------------------- 10 files changed, 12 insertions(+), 100 deletions(-) diff --git a/config/config.go b/config/config.go index db9adf4f279..ef1e05245a1 100755 --- a/config/config.go +++ b/config/config.go @@ -214,7 +214,7 @@ func (cfg *GDPR) validate(errs []error) []error { glog.Warning("gdpr.host_vendor_id was not specified. Host company GDPR checks will be skipped.") } if cfg.AMPException == true { - glog.Warning("gdpr.amp_exception is deprecated and will be removed in a future version. If you need to disable GDPR for AMP, you may do so per-account (gdpr.integration_enabled.amp) or at the host level for the default account (account_defaults.gdpr.integration_enabled.amp).") + errs = append(errs, fmt.Errorf("gdpr.amp_exception has been discontinued and must be removed from your config. If you need to disable GDPR for AMP, you may do so per-account (gdpr.integration_enabled.amp) or at the host level for the default account (account_defaults.gdpr.integration_enabled.amp)")) } return errs } diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index f7f915cbaa5..b5f0989ed28 100644 --- a/endpoints/auction_test.go +++ b/endpoints/auction_test.go @@ -424,10 +424,6 @@ func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder return m.allowPI, m.allowGeo, m.allowID, nil } -func (m *auctionMockPermissions) AMPException() bool { - return false -} - func TestBidSizeValidate(t *testing.T) { bids := make(pbs.PBSBidSlice, 0) // bid1 will be rejected due to undefined size when adunit has multiple sizes diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 77be25907c6..ee29125b908 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -257,7 +257,3 @@ func (g *gdprPerms) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { return true, true, true, nil } - -func (g *gdprPerms) AMPException() bool { - return false -} diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index ae0636770da..1f2c4040d59 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -441,10 +441,6 @@ func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrt return g.allowPI, g.allowPI, g.allowPI, nil } -func (g *mockPermsSetUID) AMPException() bool { - return false -} - func newFakeSyncer(familyName string) usersync.Usersyncer { return fakeSyncer{ familyName: familyName, diff --git a/exchange/utils.go b/exchange/utils.go index d70829ff90c..a803e4e242b 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -77,7 +77,6 @@ func cleanOpenRTBRequests(ctx context.Context, gdpr := extractGDPR(req.BidRequest, usersyncIfAmbiguous) consent := extractConsent(req.BidRequest) - ampGDPRException := (req.LegacyLabels.RType == metrics.ReqTypeAMP) && gDPR.AMPException() ccpaEnforcer, err := extractCCPA(req.BidRequest, privacyConfig, &req.Account, aliases, integrationTypeMap[req.LegacyLabels.RType]) if err != nil { @@ -125,7 +124,7 @@ func cleanOpenRTBRequests(ctx context.Context, privacyEnforcement.GDPRID = false } - privacyEnforcement.Apply(bidderRequest.BidRequest, ampGDPRException) + privacyEnforcement.Apply(bidderRequest.BidRequest) } return diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 5fb5707c07b..757eb658222 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -34,10 +34,6 @@ func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrt return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil } -func (p *permissionsMock) AMPException() bool { - return false -} - func assertReq(t *testing.T, bidderRequests []BidderRequest, applyCOPPA bool, consentedVendors map[string]bool) { // assert individual bidder requests diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 6d447beb438..60a7cc1e2c0 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -24,9 +24,6 @@ type Permissions interface { // // 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, bool, error) - - // Exposes the AMP execption flag - AMPException() bool } // Versions of the GDPR TCF technical specification. diff --git a/gdpr/impl.go b/gdpr/impl.go index 2fbd9c5a07c..5b3b86fe557 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -60,10 +60,6 @@ func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrt return false, 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) { // If we're not given a consent string, respect the preferences in the app config. if consent == "" { @@ -225,10 +221,6 @@ func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext return true, true, true, nil } -func (a AlwaysAllow) AMPException() bool { - return false -} - // Exporting to allow for easy test setups type AlwaysFail struct{} @@ -243,7 +235,3 @@ func (a AlwaysFail) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { return false, false, false, nil } - -func (a AlwaysFail) AMPException() bool { - return false -} diff --git a/privacy/enforcement.go b/privacy/enforcement.go index 3f157329cf6..64070ae3a6a 100644 --- a/privacy/enforcement.go +++ b/privacy/enforcement.go @@ -19,14 +19,14 @@ func (e Enforcement) Any() bool { } // Apply cleans personally identifiable information from an OpenRTB bid request. -func (e Enforcement) Apply(bidRequest *openrtb.BidRequest, ampGDPRException bool) { - e.apply(bidRequest, ampGDPRException, NewScrubber()) +func (e Enforcement) Apply(bidRequest *openrtb.BidRequest) { + e.apply(bidRequest, NewScrubber()) } -func (e Enforcement) apply(bidRequest *openrtb.BidRequest, ampGDPRException bool, scrubber Scrubber) { +func (e Enforcement) apply(bidRequest *openrtb.BidRequest, scrubber Scrubber) { if bidRequest != nil && e.Any() { bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy()) - bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(ampGDPRException), e.getGeoScrubStrategy()) + bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy()) } } @@ -70,7 +70,7 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo { return ScrubStrategyGeoNone } -func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUser { +func (e Enforcement) getUserScrubStrategy() ScrubStrategyUser { if e.COPPA { return ScrubStrategyUserIDAndDemographic } @@ -79,7 +79,7 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs return ScrubStrategyUserID } - if e.GDPRID && !ampGDPRException { + if e.GDPRID { return ScrubStrategyUserID } diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go index 0cf36a614c4..9240aafc2c3 100644 --- a/privacy/enforcement_test.go +++ b/privacy/enforcement_test.go @@ -59,7 +59,6 @@ func TestApply(t *testing.T) { testCases := []struct { description string enforcement Enforcement - ampGDPRException bool expectedDeviceID ScrubStrategyDeviceID expectedDeviceIPv4 ScrubStrategyIPV4 expectedDeviceIPv6 ScrubStrategyIPV6 @@ -124,7 +123,6 @@ func TestApply(t *testing.T) { GDPRID: true, LMT: false, }, - ampGDPRException: false, expectedDeviceID: ScrubStrategyDeviceIDAll, expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -132,23 +130,6 @@ func TestApply(t *testing.T) { expectedUser: ScrubStrategyUserID, expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, - { - description: "GDPR Only - Full - AMP Exception", - enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPRGeo: true, - GDPRID: true, - LMT: false, - }, - ampGDPRException: true, - expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, - expectedDeviceGeo: ScrubStrategyGeoReducedPrecision, - expectedUser: ScrubStrategyUserNone, - expectedUserGeo: ScrubStrategyGeoReducedPrecision, - }, { description: "GDPR Only - ID Only", enforcement: Enforcement{ @@ -158,7 +139,6 @@ func TestApply(t *testing.T) { GDPRID: true, LMT: false, }, - ampGDPRException: false, expectedDeviceID: ScrubStrategyDeviceIDAll, expectedDeviceIPv4: ScrubStrategyIPV4None, expectedDeviceIPv6: ScrubStrategyIPV6None, @@ -166,23 +146,6 @@ func TestApply(t *testing.T) { expectedUser: ScrubStrategyUserID, expectedUserGeo: ScrubStrategyGeoNone, }, - { - description: "GDPR Only - ID Only - AMP Exception", - enforcement: Enforcement{ - CCPA: false, - COPPA: false, - GDPRGeo: false, - GDPRID: true, - LMT: false, - }, - ampGDPRException: true, - expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4None, - expectedDeviceIPv6: ScrubStrategyIPV6None, - expectedDeviceGeo: ScrubStrategyGeoNone, - expectedUser: ScrubStrategyUserNone, - expectedUserGeo: ScrubStrategyGeoNone, - }, { description: "GDPR Only - Geo Only", enforcement: Enforcement{ @@ -192,7 +155,6 @@ func TestApply(t *testing.T) { GDPRID: false, LMT: false, }, - ampGDPRException: false, expectedDeviceID: ScrubStrategyDeviceIDNone, expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest16, @@ -217,24 +179,7 @@ func TestApply(t *testing.T) { expectedUserGeo: ScrubStrategyGeoReducedPrecision, }, { - description: "Interactions: COPPA Only + AMP Exception", - enforcement: Enforcement{ - CCPA: false, - COPPA: true, - GDPRGeo: false, - GDPRID: false, - LMT: false, - }, - ampGDPRException: true, - expectedDeviceID: ScrubStrategyDeviceIDAll, - expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, - expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, - expectedDeviceGeo: ScrubStrategyGeoFull, - expectedUser: ScrubStrategyUserIDAndDemographic, - expectedUserGeo: ScrubStrategyGeoFull, - }, - { - description: "Interactions: COPPA + GDPR Full + AMP Exception", + description: "Interactions: COPPA + GDPR Full", enforcement: Enforcement{ CCPA: false, COPPA: true, @@ -242,7 +187,6 @@ func TestApply(t *testing.T) { GDPRID: true, LMT: false, }, - ampGDPRException: true, expectedDeviceID: ScrubStrategyDeviceIDAll, expectedDeviceIPv4: ScrubStrategyIPV4Lowest8, expectedDeviceIPv6: ScrubStrategyIPV6Lowest32, @@ -264,7 +208,7 @@ func TestApply(t *testing.T) { m.On("ScrubDevice", req.Device, test.expectedDeviceID, test.expectedDeviceIPv4, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(replacedDevice).Once() m.On("ScrubUser", req.User, test.expectedUser, test.expectedUserGeo).Return(replacedUser).Once() - test.enforcement.apply(req, test.ampGDPRException, m) + test.enforcement.apply(req, m) m.AssertExpectations(t) assert.Same(t, replacedDevice, req.Device, "Device") @@ -284,7 +228,7 @@ func TestApplyNoneApplicable(t *testing.T) { GDPRID: false, LMT: false, } - enforcement.apply(req, false, m) + enforcement.apply(req, m) m.AssertNotCalled(t, "ScrubDevice") m.AssertNotCalled(t, "ScrubUser") @@ -294,7 +238,7 @@ func TestApplyNil(t *testing.T) { m := &mockScrubber{} enforcement := Enforcement{} - enforcement.apply(nil, false, m) + enforcement.apply(nil, m) m.AssertNotCalled(t, "ScrubDevice") m.AssertNotCalled(t, "ScrubUser") From ef967eff650c00cc4f5c0977a987fc5654d11907 Mon Sep 17 00:00:00 2001 From: bretg Date: Tue, 5 Jan 2021 10:22:36 -0500 Subject: [PATCH 327/381] Updating contact info for adprime (#1640) --- static/bidder-info/adprime.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/bidder-info/adprime.yaml b/static/bidder-info/adprime.yaml index 9759ed63be7..d702521ec75 100644 --- a/static/bidder-info/adprime.yaml +++ b/static/bidder-info/adprime.yaml @@ -1,5 +1,5 @@ maintainer: - email: "rafal@adprime.com" + email: "prebid@adprime.com" capabilities: app: mediaTypes: @@ -8,4 +8,4 @@ capabilities: site: mediaTypes: - banner - - video \ No newline at end of file + - video From 1dda07dd4b6836cfba599cf232a824f5bcd261bd Mon Sep 17 00:00:00 2001 From: ucfunnel <39581136+ucfunnel@users.noreply.github.com> Date: Tue, 5 Jan 2021 23:23:12 +0800 Subject: [PATCH 328/381] ucfunnel adapter update end point (#1639) --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index ef1e05245a1..c49de91a5f7 100755 --- a/config/config.go +++ b/config/config.go @@ -873,7 +873,7 @@ func SetupViper(v *viper.Viper, filename string) { 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?sra=1&supplier_id=20") - v.SetDefault("adapters.ucfunnel.endpoint", "http://pbs.aralego.com/prebid") + v.SetDefault("adapters.ucfunnel.endpoint", "https://pbs.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) From 0284a70e8fdef045e338a55337fea6759a5a2bd8 Mon Sep 17 00:00:00 2001 From: mobfxoHB <74364234+mobfxoHB@users.noreply.github.com> Date: Tue, 5 Jan 2021 23:28:19 +0200 Subject: [PATCH 329/381] New Adapter: Mobfox (#1585) Co-authored-by: mobfox --- adapters/mobfoxpb/mobfoxpb.go | 131 +++++++++++++++++ adapters/mobfoxpb/mobfoxpb_test.go | 18 +++ .../mobfoxpbtest/exemplary/simple-banner.json | 132 ++++++++++++++++++ .../mobfoxpbtest/exemplary/simple-video.json | 119 ++++++++++++++++ .../exemplary/simple-web-banner.json | 130 +++++++++++++++++ .../mobfoxpbtest/params/race/banner.json | 3 + .../mobfoxpbtest/params/race/video.json | 3 + .../supplemental/bad-imp-ext.json | 42 ++++++ .../supplemental/bad_response.json | 87 ++++++++++++ .../supplemental/bad_status_code.json | 81 +++++++++++ .../supplemental/imp_ext_empty_object.json | 38 +++++ .../supplemental/imp_ext_string.json | 38 +++++ .../mobfoxpbtest/supplemental/status-204.json | 82 +++++++++++ .../mobfoxpbtest/supplemental/status-404.json | 87 ++++++++++++ adapters/mobfoxpb/params_test.go | 47 +++++++ config/config.go | 1 + exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + static/bidder-info/mobfoxpb.yaml | 11 ++ static/bidder-params/mobfoxpb.json | 14 ++ usersync/usersyncers/syncer_test.go | 1 + 21 files changed, 1069 insertions(+) create mode 100644 adapters/mobfoxpb/mobfoxpb.go create mode 100644 adapters/mobfoxpb/mobfoxpb_test.go create mode 100644 adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-banner.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-video.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-web-banner.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/params/race/banner.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/params/race/video.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/supplemental/bad-imp-ext.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/supplemental/bad_response.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/supplemental/bad_status_code.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_empty_object.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_string.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/supplemental/status-204.json create mode 100644 adapters/mobfoxpb/mobfoxpbtest/supplemental/status-404.json create mode 100644 adapters/mobfoxpb/params_test.go create mode 100644 static/bidder-info/mobfoxpb.yaml create mode 100644 static/bidder-params/mobfoxpb.json diff --git a/adapters/mobfoxpb/mobfoxpb.go b/adapters/mobfoxpb/mobfoxpb.go new file mode 100644 index 00000000000..5369c082c52 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpb.go @@ -0,0 +1,131 @@ +package mobfoxpb + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/buger/jsonparser" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type adapter struct { + URI string +} + +// Builder builds a new instance of the Mobfox adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + bidder := &adapter{ + URI: config.Endpoint, + } + return bidder, nil +} + +// MakeRequests create bid request for mobfoxpb demand +func (a *adapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var err error + var tagID string + + var adapterRequests []*adapters.RequestData + + reqCopy := *request + imp := request.Imp[0] + tagID, err = jsonparser.GetString(imp.Ext, "bidder", "TagID") + if err != nil { + errs = append(errs, err) + return nil, errs + } + imp.TagID = tagID + reqCopy.Imp = []openrtb.Imp{imp} + adapterReq, err := a.makeRequest(&reqCopy) + if err != nil { + errs = append(errs, err) + } + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + return adapterRequests, errs +} + +func (a *adapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.URI, + Body: reqJSON, + Headers: headers, + }, nil +} + +// MakeBids makes the bids +func (a *adapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + var errs []error + + if response.StatusCode == http.StatusNoContent { + return nil, nil + } + + 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 _, bid := range sb.Bid { + bidType, err := getMediaTypeForImp(bid.ImpID, internalRequest.Imp) + if err != nil { + errs = append(errs, err) + } else { + b := &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + } + return bidResponse, errs +} + +func getMediaTypeForImp(impID string, imps []openrtb.Imp) (openrtb_ext.BidType, error) { + mediaType := openrtb_ext.BidTypeBanner + for _, imp := range imps { + if imp.ID == impID { + if imp.Banner != nil { + mediaType = openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + mediaType = openrtb_ext.BidTypeVideo + } else if imp.Native != nil { + mediaType = openrtb_ext.BidTypeNative + } + return mediaType, nil + } + } + + // This shouldnt happen. Lets handle it just incase by returning an error. + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to find impression \"%s\" ", impID), + } +} diff --git a/adapters/mobfoxpb/mobfoxpb_test.go b/adapters/mobfoxpb/mobfoxpb_test.go new file mode 100644 index 00000000000..271d30a97af --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpb_test.go @@ -0,0 +1,18 @@ +package mobfoxpb + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderMobfoxpb, config.Adapter{ + Endpoint: "http://example.com/?c=o&m=ortb"}) + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + adapterstest.RunJSONBidderTest(t, "mobfoxpbtest", bidder) +} diff --git a/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-banner.json b/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-banner.json new file mode 100644 index 00000000000..b1936661a71 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-banner.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "6", + "ext": { + "bidder": { + "TagID": "6" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } +}, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "6", + "ext": { + "bidder": { + "TagID": "6" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-video.json b/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-video.json new file mode 100644 index 00000000000..6cdcdc5a6cc --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-video.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "TagID": "7" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "protocols": [2, 5], + "w": 1024, + "h": 576 + }, + "tagid": "7", + "ext": { + "bidder": { + "TagID": "7" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "mobfoxpb" + } + ], + "cur": "USD" + } + } + } + ], + + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-web-banner.json b/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..bba728ac1e9 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/exemplary/simple-web-banner.json @@ -0,0 +1,130 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "8", + "ext": { + "bidder": { + "TagID": "8" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "8", + "ext": { + "bidder": { + "TagID": "8" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "mobfoxpb" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/params/race/banner.json b/adapters/mobfoxpb/mobfoxpbtest/params/race/banner.json new file mode 100644 index 00000000000..dbdac1ad995 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "TagID": "6" +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/params/race/video.json b/adapters/mobfoxpb/mobfoxpbtest/params/race/video.json new file mode 100644 index 00000000000..6e2e0b3803b --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "TagID": "7" +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad-imp-ext.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad-imp-ext.json new file mode 100644 index 00000000000..ac3dce598eb --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad-imp-ext.json @@ -0,0 +1,42 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "6", + "ext": { + "mobfoxpb": { + "TagID": "6" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad_response.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad_response.json new file mode 100644 index 00000000000..2f834c92be7 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad_response.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "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/mobfoxpb/mobfoxpbtest/supplemental/bad_status_code.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad_status_code.json new file mode 100644 index 00000000000..96d3a649109 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/bad_status_code.json @@ -0,0 +1,81 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": {} + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_empty_object.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_empty_object.json new file mode 100644 index 00000000000..cc56fa25c2c --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_empty_object.json @@ -0,0 +1,38 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "6", + "ext": {} + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_string.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_string.json new file mode 100644 index 00000000000..464c9e31e39 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/imp_ext_string.json @@ -0,0 +1,38 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "6", + "ext": "" + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "Key path not found", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/supplemental/status-204.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/status-204.json new file mode 100644 index 00000000000..c1091969991 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/status-204.json @@ -0,0 +1,82 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "17", + "ext": { + "bidder": { + "TagID": "17" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "expectedBidResponses": [], + "mockResponse": { + "status": 204, + "body": {} + } + } + ] +} \ No newline at end of file diff --git a/adapters/mobfoxpb/mobfoxpbtest/supplemental/status-404.json b/adapters/mobfoxpb/mobfoxpbtest/supplemental/status-404.json new file mode 100644 index 00000000000..d9ef7108017 --- /dev/null +++ b/adapters/mobfoxpb/mobfoxpbtest/supplemental/status-404.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?c=o&m=ortb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "100000000", + "ext": { + "bidder": { + "TagID": "100000000" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/mobfoxpb/params_test.go b/adapters/mobfoxpb/params_test.go new file mode 100644 index 00000000000..59b9ec383c8 --- /dev/null +++ b/adapters/mobfoxpb/params_test.go @@ -0,0 +1,47 @@ +package mobfoxpb + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +// TestValidParams makes sure that the mobfoxpb 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.BidderMobfoxpb, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected mobfoxpb params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the mobfoxpb 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.BidderMobfoxpb, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"TagID": "6"}`, +} + +var invalidParams = []string{ + `{"id": "123"}`, + `{"tagid": "123"}`, + `{"TagID": 16}`, + `{"TagID": ""}`, +} diff --git a/config/config.go b/config/config.go index c49de91a5f7..f48ce9dae84 100755 --- a/config/config.go +++ b/config/config.go @@ -846,6 +846,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.mobilefuse.com/openrtb?pub_id={{.PublisherID}}") + v.SetDefault("adapters.mobfoxpb.endpoint", "http://bes.mobfox.com/?c=o&m=ortb") 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.nobid.endpoint", "https://ads.servenobid.com/ortb_adreq?tek=pbs&ver=1") diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 2d73b823bfc..32311ef640a 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -54,6 +54,7 @@ import ( "github.com/prebid/prebid-server/adapters/lunamedia" "github.com/prebid/prebid-server/adapters/marsmedia" "github.com/prebid/prebid-server/adapters/mgid" + "github.com/prebid/prebid-server/adapters/mobfoxpb" "github.com/prebid/prebid-server/adapters/mobilefuse" "github.com/prebid/prebid-server/adapters/nanointeractive" "github.com/prebid/prebid-server/adapters/ninthdecimal" @@ -150,6 +151,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderLunaMedia: lunamedia.Builder, openrtb_ext.BidderMarsmedia: marsmedia.Builder, openrtb_ext.BidderMgid: mgid.Builder, + openrtb_ext.BidderMobfoxpb: mobfoxpb.Builder, openrtb_ext.BidderMobileFuse: mobilefuse.Builder, openrtb_ext.BidderNanoInteractive: nanointeractive.Builder, openrtb_ext.BidderNinthDecimal: ninthdecimal.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 6f426c5245c..f7f8312508f 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -94,6 +94,7 @@ const ( BidderLunaMedia BidderName = "lunamedia" BidderMarsmedia BidderName = "marsmedia" BidderMgid BidderName = "mgid" + BidderMobfoxpb BidderName = "mobfoxpb" BidderMobileFuse BidderName = "mobilefuse" BidderNanoInteractive BidderName = "nanointeractive" BidderNinthDecimal BidderName = "ninthdecimal" @@ -190,6 +191,7 @@ func CoreBidderNames() []BidderName { BidderLunaMedia, BidderMarsmedia, BidderMgid, + BidderMobfoxpb, BidderMobileFuse, BidderNanoInteractive, BidderNinthDecimal, diff --git a/static/bidder-info/mobfoxpb.yaml b/static/bidder-info/mobfoxpb.yaml new file mode 100644 index 00000000000..ba3bb60d554 --- /dev/null +++ b/static/bidder-info/mobfoxpb.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "platform@mobfox.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-params/mobfoxpb.json b/static/bidder-params/mobfoxpb.json new file mode 100644 index 00000000000..0cc7a16c026 --- /dev/null +++ b/static/bidder-params/mobfoxpb.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Mobfox Adapter Params", + "description": "A schema which validates params accepted by the Mobfox adapter", + "type": "object", + "properties": { + "TagID": { + "type": "string", + "minLength": 1, + "description": "An ID which identifies the mobfox ad tag" + } + }, + "required" : [ "TagID" ] +} diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index dc4ca0a6cfd..60ab2478a93 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -101,6 +101,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderInMobi: true, openrtb_ext.BidderKidoz: true, openrtb_ext.BidderKubient: true, + openrtb_ext.BidderMobfoxpb: true, openrtb_ext.BidderMobileFuse: true, openrtb_ext.BidderOrbidder: true, openrtb_ext.BidderPubnative: true, From a709baa4f842b11d4711b642c68ee245639dde07 Mon Sep 17 00:00:00 2001 From: Index Exchange 3 Prebid Team Date: Wed, 6 Jan 2021 10:19:22 -0500 Subject: [PATCH 330/381] IX: Implement Bidder interface, update endpoint. (#1569) Co-authored-by: Index Exchange Prebid Team --- adapters/ix/ix.go | 272 ++++++++++++++---- adapters/ix/ix_test.go | 261 +++++++++++------ .../ixtest/exemplary/additional-consent.json | 124 ++++++++ .../ix/ixtest/exemplary/banner-no-format.json | 108 +++++++ .../ix/ixtest/exemplary/max-requests.json | 255 ++++++++++++++++ adapters/ix/ixtest/exemplary/no-pub-id.json | 121 ++++++++ adapters/ix/ixtest/exemplary/no-pub.json | 117 ++++++++ .../ix/ixtest/exemplary/simple-audio.json | 102 +++++++ .../exemplary/simple-banner-multi-size.json | 201 +++++++++++++ .../ix/ixtest/exemplary/simple-native.json | 104 +++++++ .../ix/ixtest/exemplary/simple-video.json | 134 +++++++++ adapters/ix/ixtest/params/race/audio.json | 4 + adapters/ix/ixtest/params/race/banner.json | 3 +- adapters/ix/ixtest/params/race/native.json | 4 + adapters/ix/ixtest/params/race/video.json | 3 +- .../ixtest/supplemental/bad-ext-bidder.json | 22 ++ .../ix/ixtest/supplemental/bad-ext-ix.json | 21 ++ .../ix/ixtest/supplemental/bad-imp-id.json | 118 ++++++++ .../ix/ixtest/supplemental/bad-request.json | 63 ++++ .../supplemental/bad-response-body.json | 65 +++++ .../ix/ixtest/supplemental/no-content.json | 57 ++++ adapters/ix/ixtest/supplemental/no-imp.json | 6 + .../ix/ixtest/supplemental/not-found.json | 63 ++++ config/config.go | 2 +- exchange/adapter_builders.go | 2 + exchange/adapter_util.go | 11 +- exchange/adapter_util_test.go | 42 ++- openrtb_ext/imp_ix.go | 7 + static/bidder-info/ix.yaml | 9 + 29 files changed, 2122 insertions(+), 179 deletions(-) create mode 100644 adapters/ix/ixtest/exemplary/additional-consent.json create mode 100644 adapters/ix/ixtest/exemplary/banner-no-format.json create mode 100644 adapters/ix/ixtest/exemplary/max-requests.json create mode 100644 adapters/ix/ixtest/exemplary/no-pub-id.json create mode 100644 adapters/ix/ixtest/exemplary/no-pub.json create mode 100644 adapters/ix/ixtest/exemplary/simple-audio.json create mode 100644 adapters/ix/ixtest/exemplary/simple-banner-multi-size.json create mode 100644 adapters/ix/ixtest/exemplary/simple-native.json create mode 100644 adapters/ix/ixtest/exemplary/simple-video.json create mode 100644 adapters/ix/ixtest/params/race/audio.json create mode 100644 adapters/ix/ixtest/params/race/native.json create mode 100644 adapters/ix/ixtest/supplemental/bad-ext-bidder.json create mode 100644 adapters/ix/ixtest/supplemental/bad-ext-ix.json create mode 100644 adapters/ix/ixtest/supplemental/bad-imp-id.json create mode 100644 adapters/ix/ixtest/supplemental/bad-request.json create mode 100644 adapters/ix/ixtest/supplemental/bad-response-body.json create mode 100644 adapters/ix/ixtest/supplemental/no-content.json create mode 100644 adapters/ix/ixtest/supplemental/no-imp.json create mode 100644 adapters/ix/ixtest/supplemental/not-found.json create mode 100644 openrtb_ext/imp_ix.go diff --git a/adapters/ix/ix.go b/adapters/ix/ix.go index 7052d5ac088..96cd988de54 100644 --- a/adapters/ix/ix.go +++ b/adapters/ix/ix.go @@ -8,26 +8,22 @@ import ( "io/ioutil" "net/http" - "github.com/prebid/prebid-server/openrtb_ext" - - "github.com/prebid/prebid-server/pbs" - "golang.org/x/net/context/ctxhttp" "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/prebid/prebid-server/pbs" ) -// maximum number of bid requests -const requestLimit = 20 - type IxAdapter struct { - http *adapters.HTTPAdapter - URI string + http *adapters.HTTPAdapter + URI string + maxRequests int } -// Name is used for cookies and such func (a *IxAdapter) Name() string { return string(openrtb_ext.BidderIx) } @@ -52,34 +48,21 @@ type callOneObject struct { requestJSON bytes.Buffer width uint64 height uint64 -} - -func isValidIXSize(f openrtb.Format, s [2]uint64) bool { - if f.W != s[0] || f.H != s[1] { - return false - } - return true + bidType string } func (a *IxAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.PBSBidder) (pbs.PBSBidSlice, error) { var prioritizedRequests, requests []callOneObject - if req.App != nil { - return nil, &errortypes.BadInput{ - Message: "Index doesn't support apps", - } - } - mediaTypes := []pbs.MediaType{pbs.MEDIA_TYPE_BANNER} + mediaTypes := []pbs.MediaType{pbs.MEDIA_TYPE_BANNER, pbs.MEDIA_TYPE_VIDEO} indexReq, err := adapters.MakeOpenRTBGeneric(req, bidder, a.Name(), mediaTypes) - if err != nil { return nil, err } indexReqImp := indexReq.Imp for i, unit := range bidder.AdUnits { - - // Fixes some segfaults. Since this is legacy code, I'm not looking into it too deeply + // Supposedly fixes some segfaults if len(indexReqImp) <= i { break } @@ -99,29 +82,35 @@ func (a *IxAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.P } for sizeIndex, format := range unit.Sizes { - // Only grab this ad unit - // Not supporting multi-media-type adunit yet + // Only grab this ad unit. Not supporting multi-media-type adunit yet. thisImp := indexReqImp[i] thisImp.TagID = unit.Code - thisImp.Banner.Format = []openrtb.Format{format} - thisImp.Banner.W = &format.W - thisImp.Banner.H = &format.H + if thisImp.Banner != nil { + thisImp.Banner.Format = []openrtb.Format{format} + thisImp.Banner.W = &format.W + thisImp.Banner.H = &format.H + } indexReq.Imp = []openrtb.Imp{thisImp} // Index spec says "adunit path representing ad server inventory" but we don't have this // ext is DFP div ID and KV pairs if avail //indexReq.Imp[i].Ext = json.RawMessage("{}") - // Any objects pointed to by indexReq *must not be mutated*, or we will get race conditions. - siteCopy := *indexReq.Site - siteCopy.Publisher = &openrtb.Publisher{ID: params.SiteID} - indexReq.Site = &siteCopy + if indexReq.Site != nil { + // Any objects pointed to by indexReq *must not be mutated*, or we will get race conditions. + siteCopy := *indexReq.Site + siteCopy.Publisher = &openrtb.Publisher{ID: params.SiteID} + indexReq.Site = &siteCopy + } - // spec also asks for publisher id if set - // ext object on request for prefetch + bidType := "" + if thisImp.Banner != nil { + bidType = string(openrtb_ext.BidTypeBanner) + } else if thisImp.Video != nil { + bidType = string(openrtb_ext.BidTypeVideo) + } j, _ := json.Marshal(indexReq) - - request := callOneObject{requestJSON: *bytes.NewBuffer(j), width: format.W, height: format.H} + request := callOneObject{requestJSON: *bytes.NewBuffer(j), width: format.W, height: format.H, bidType: bidType} // prioritize slots over sizes if sizeIndex == 0 { @@ -132,10 +121,10 @@ func (a *IxAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.P } } - // cap the number of requests to requestLimit + // cap the number of requests to maxRequests requests = append(prioritizedRequests, requests...) - if len(requests) > requestLimit { - requests = requests[:requestLimit] + if len(requests) > a.maxRequests { + requests = requests[:a.maxRequests] } if len(requests) == 0 { @@ -155,6 +144,7 @@ func (a *IxAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.P result.Bid.BidID = bidder.LookupBidID(result.Bid.AdUnitCode) result.Bid.Width = request.width result.Bid.Height = request.height + result.Bid.CreativeMediaType = request.bidType if result.Bid.BidID == "" { result.Error = &errortypes.BadServerResponse{ @@ -247,23 +237,195 @@ func (a *IxAdapter) callOne(ctx context.Context, reqJSON bytes.Buffer) (ixBidRes } bid := bidResp.SeatBid[0].Bid[0] - result.Bid = &pbs.PBSBid{ - AdUnitCode: bid.ImpID, - Price: bid.Price, - Adm: bid.AdM, - Creative_id: bid.CrID, - Width: bid.W, - Height: bid.H, - DealId: bid.DealID, - CreativeMediaType: string(openrtb_ext.BidTypeBanner), + pbid := pbs.PBSBid{ + AdUnitCode: bid.ImpID, + Price: bid.Price, + Adm: bid.AdM, + Creative_id: bid.CrID, + Width: bid.W, + Height: bid.H, + DealId: bid.DealID, } + + result.Bid = &pbid return result, nil } -func NewIxLegacyAdapter(config *adapters.HTTPAdapterConfig, uri string) *IxAdapter { - a := adapters.NewHTTPAdapter(config) +func (a *IxAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + nImp := len(request.Imp) + if nImp > a.maxRequests { + request.Imp = request.Imp[:a.maxRequests] + nImp = a.maxRequests + } + + // Multi-size banner imps are split into single-size requests. + // The first size imp requests are added to the first slice. + // Additional size requests are added to the second slice and are merged with the first at the end. + // Preallocate the max possible size to avoid reallocating arrays. + requests := make([]*adapters.RequestData, 0, a.maxRequests) + multiSizeRequests := make([]*adapters.RequestData, 0, a.maxRequests-nImp) + errs := make([]error, 0, 1) + + headers := http.Header{ + "Content-Type": {"application/json;charset=utf-8"}, + "Accept": {"application/json"}} + + imps := request.Imp + for iImp := range imps { + request.Imp = imps[iImp : iImp+1] + if request.Site != nil { + if err := setSitePublisherId(request, iImp); err != nil { + errs = append(errs, err) + continue + } + } + + if request.Imp[0].Banner != nil { + banner := *request.Imp[0].Banner + request.Imp[0].Banner = &banner + formats := getBannerFormats(&banner) + for iFmt := range formats { + banner.Format = formats[iFmt : iFmt+1] + banner.W = openrtb.Uint64Ptr(banner.Format[0].W) + banner.H = openrtb.Uint64Ptr(banner.Format[0].H) + if requestData, err := createRequestData(a, request, &headers); err == nil { + if iFmt == 0 { + requests = append(requests, requestData) + } else { + multiSizeRequests = append(multiSizeRequests, requestData) + } + } else { + errs = append(errs, err) + } + if len(multiSizeRequests) == cap(multiSizeRequests) { + break + } + } + } else if requestData, err := createRequestData(a, request, &headers); err == nil { + requests = append(requests, requestData) + } else { + errs = append(errs, err) + } + } + request.Imp = imps + + return append(requests, multiSizeRequests...), errs +} + +func setSitePublisherId(request *openrtb.BidRequest, iImp int) error { + if iImp == 0 { + // first impression - create a site and pub copy + site := *request.Site + if site.Publisher == nil { + site.Publisher = &openrtb.Publisher{} + } else { + publisher := *site.Publisher + site.Publisher = &publisher + } + request.Site = &site + } + + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil { + return err + } + + var ixExt openrtb_ext.ExtImpIx + if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil { + return err + } + + request.Site.Publisher.ID = ixExt.SiteId + return nil +} + +func getBannerFormats(banner *openrtb.Banner) []openrtb.Format { + if len(banner.Format) == 0 && banner.W != nil && banner.H != nil { + banner.Format = []openrtb.Format{{W: *banner.W, H: *banner.H}} + } + return banner.Format +} + +func createRequestData(a *IxAdapter, request *openrtb.BidRequest, headers *http.Header) (*adapters.RequestData, error) { + body, err := json.Marshal(request) + return &adapters.RequestData{ + Method: "POST", + Uri: a.URI, + Body: body, + Headers: *headers, + }, err +} + +func (a *IxAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + switch { + case response.StatusCode == http.StatusNoContent: + return nil, nil + case 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), + }} + case 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 bidResponse openrtb.BidResponse + if err := json.Unmarshal(response.Body, &bidResponse); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("JSON parsing error: %v", err), + }} + } + + // Until the time we support multi-format ad units, we'll use a bid request impression media type + // as a bid response bid type. They are linked by the impression id. + impMediaType := map[string]openrtb_ext.BidType{} + for _, imp := range internalRequest.Imp { + if imp.Banner != nil { + impMediaType[imp.ID] = openrtb_ext.BidTypeBanner + } else if imp.Video != nil { + impMediaType[imp.ID] = openrtb_ext.BidTypeVideo + } else if imp.Native != nil { + impMediaType[imp.ID] = openrtb_ext.BidTypeNative + } else if imp.Audio != nil { + impMediaType[imp.ID] = openrtb_ext.BidTypeAudio + } + } + + bidderResponse := adapters.NewBidderResponseWithBidsCapacity(5) + bidderResponse.Currency = bidResponse.Cur + + var errs []error + + for _, seatBid := range bidResponse.SeatBid { + for _, bid := range seatBid.Bid { + bidType, ok := impMediaType[bid.ImpID] + if !ok { + errs = append(errs, fmt.Errorf("Unmatched impression id: %s.", bid.ImpID)) + } + bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + }) + } + } + + return bidderResponse, errs +} + +func NewIxLegacyAdapter(config *adapters.HTTPAdapterConfig, endpoint string) *IxAdapter { return &IxAdapter{ - http: a, - URI: uri, + http: adapters.NewHTTPAdapter(config), + URI: endpoint, + maxRequests: 20, + } +} + +// Builder builds a new instance of the Ix adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + bidder := &IxAdapter{ + URI: config.Endpoint, + maxRequests: 20, } + return bidder, nil } diff --git a/adapters/ix/ix_test.go b/adapters/ix/ix_test.go index 1a9abd24305..8a75e6852b7 100644 --- a/adapters/ix/ix_test.go +++ b/adapters/ix/ix_test.go @@ -13,10 +13,26 @@ import ( "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" "github.com/prebid/prebid-server/pbs" ) -const url string = "http://appnexus-us-east.lb.indexww.com/bidder?p=184932" +const endpoint string = "http://host/endpoint" + +func TestJsonSamples(t *testing.T) { + if bidder, err := Builder(openrtb_ext.BidderIx, config.Adapter{Endpoint: endpoint}); err == nil { + ixBidder := bidder.(*IxAdapter) + ixBidder.maxRequests = 2 + adapterstest.RunJSONBidderTest(t, "ixtest", bidder) + } else { + t.Fatalf("Builder returned unexpected error %v", err) + } +} + +// Tests for the legacy, non-openrtb code. +// They can be removed after the legacy interface is deprecated. func getAdUnit() pbs.PBSAdUnit { return pbs.PBSAdUnit{ @@ -33,22 +49,50 @@ func getAdUnit() pbs.PBSAdUnit { } } +func getVideoAdUnit() pbs.PBSAdUnit { + return pbs.PBSAdUnit{ + Code: "unitCodeVideo", + MediaTypes: []pbs.MediaType{pbs.MEDIA_TYPE_VIDEO}, + BidID: "bididvideo", + Sizes: []openrtb.Format{ + { + W: 100, + H: 75, + }, + }, + Video: pbs.PBSVideo{ + Mimes: []string{"video/mp4"}, + Minduration: 15, + Maxduration: 30, + Startdelay: 5, + Skippable: 0, + PlaybackMethod: 1, + Protocols: []int8{2, 3}, + }, + Params: json.RawMessage("{\"siteId\":\"12\"}"), + } +} + func getOpenRTBBid(i openrtb.Imp) openrtb.Bid { return openrtb.Bid{ - ID: fmt.Sprintf("%d", rand.Intn(1000)), - ImpID: i.ID, - Price: 1.0, - AdM: "Content", - CrID: fmt.Sprintf("%d", rand.Intn(1000)), - W: *i.Banner.W, - H: *i.Banner.H, - DealID: "5", + ID: fmt.Sprintf("%d", rand.Intn(1000)), + ImpID: i.ID, + Price: 1.0, + AdM: "Content", } } +func newAdapter(endpoint string) *IxAdapter { + return NewIxLegacyAdapter(adapters.DefaultHTTPAdapterConfig, endpoint) +} + func dummyIXServer(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } var breq openrtb.BidRequest err = json.Unmarshal(body, &breq) @@ -78,43 +122,36 @@ func dummyIXServer(w http.ResponseWriter, r *http.Request) { w.Write(js) } -func TestIxInvalidCall(t *testing.T) { - - an := NewIxLegacyAdapter(adapters.DefaultHTTPAdapterConfig, url) - an.URI = "blah" +func TestIxSkipNoCookies(t *testing.T) { + if newAdapter(endpoint).SkipNoCookies() { + t.Fatalf("SkipNoCookies must return false") + } +} +func TestIxInvalidCall(t *testing.T) { ctx := context.TODO() pbReq := pbs.PBSRequest{} pbBidder := pbs.PBSBidder{} - _, err := an.Call(ctx, &pbReq, &pbBidder) + _, err := newAdapter(endpoint).Call(ctx, &pbReq, &pbBidder) if err == nil { t.Fatalf("No error received for invalid request") } } func TestIxInvalidCallReqAppNil(t *testing.T) { - - an := NewIxLegacyAdapter(adapters.DefaultHTTPAdapterConfig, url) - an.URI = "blah" - ctx := context.TODO() pbReq := pbs.PBSRequest{ App: &openrtb.App{}, } - pbBidder := pbs.PBSBidder{} - _, err := an.Call(ctx, &pbReq, &pbBidder) + _, err := newAdapter(endpoint).Call(ctx, &pbReq, &pbBidder) if err == nil { t.Fatalf("No error received for invalid request") } } func TestIxInvalidCallMissingSiteID(t *testing.T) { - - an := NewIxLegacyAdapter(adapters.DefaultHTTPAdapterConfig, url) - an.URI = "blah" - ctx := context.TODO() pbReq := pbs.PBSRequest{} adUnit := getAdUnit() @@ -126,7 +163,7 @@ func TestIxInvalidCallMissingSiteID(t *testing.T) { adUnit, }, } - _, err := an.Call(ctx, &pbReq, &pbBidder) + _, err := newAdapter(endpoint).Call(ctx, &pbReq, &pbBidder) if err == nil { t.Fatalf("No error received for request with missing siteId") } @@ -141,8 +178,6 @@ func TestIxTimeout(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx, cancel := context.WithTimeout(context.Background(), 0) defer cancel() @@ -153,7 +188,7 @@ func TestIxTimeout(t *testing.T) { getAdUnit(), }, } - _, err := an.Call(ctx, &pbReq, &pbBidder) + _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err == nil || err != context.DeadlineExceeded { t.Fatalf("Invalid timeout error received") } @@ -207,8 +242,6 @@ func TestIxTimeoutMultipleSlots(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) pbReq := pbs.PBSRequest{} adUnit1 := getAdUnit() @@ -227,7 +260,7 @@ func TestIxTimeoutMultipleSlots(t *testing.T) { adUnit2, }, } - bids, err := an.Call(ctx, &pbReq, &pbBidder) + bids, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err != nil { t.Fatalf("Should not have gotten an error: %v", err) @@ -252,8 +285,6 @@ func TestIxInvalidJsonResponse(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} pbBidder := pbs.PBSBidder{ @@ -262,7 +293,7 @@ func TestIxInvalidJsonResponse(t *testing.T) { getAdUnit(), }, } - _, err := an.Call(ctx, &pbReq, &pbBidder) + _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err == nil { t.Fatalf("No error received for invalid request") } @@ -278,17 +309,15 @@ func TestIxInvalidStatusCode(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() - pbReq := pbs.PBSRequest{} + pbReq := pbs.PBSRequest{IsDebug: true} pbBidder := pbs.PBSBidder{ BidderCode: "bannerCode", AdUnits: []pbs.PBSAdUnit{ getAdUnit(), }, } - _, err := an.Call(ctx, &pbReq, &pbBidder) + _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err == nil { t.Fatalf("No error received for invalid request") } @@ -304,8 +333,6 @@ func TestIxBadRequest(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} pbBidder := pbs.PBSBidder{ @@ -314,7 +341,7 @@ func TestIxBadRequest(t *testing.T) { getAdUnit(), }, } - _, err := an.Call(ctx, &pbReq, &pbBidder) + _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err == nil { t.Fatalf("No error received for bad request") } @@ -330,8 +357,6 @@ func TestIxNoContent(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} pbBidder := pbs.PBSBidder{ @@ -341,7 +366,7 @@ func TestIxNoContent(t *testing.T) { }, } - bids, err := an.Call(ctx, &pbReq, &pbBidder) + bids, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err != nil || bids != nil { t.Fatalf("Must return nil for no content") } @@ -354,8 +379,6 @@ func TestIxInvalidCallMissingSize(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} adUnit := getAdUnit() @@ -366,7 +389,7 @@ func TestIxInvalidCallMissingSize(t *testing.T) { adUnit, }, } - if _, err := an.Call(ctx, &pbReq, &pbBidder); err == nil { + if _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder); err == nil { t.Fatalf("Should not have gotten an error for missing/invalid size: %v", err) } } @@ -378,8 +401,6 @@ func TestIxInvalidCallEmptyBidIDResponse(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} adUnit := getAdUnit() @@ -390,7 +411,7 @@ func TestIxInvalidCallEmptyBidIDResponse(t *testing.T) { adUnit, }, } - if _, err := an.Call(ctx, &pbReq, &pbBidder); err == nil { + if _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder); err == nil { t.Fatalf("Should have gotten an error for unknown adunit code") } } @@ -414,14 +435,12 @@ func TestIxMismatchUnitCode(t *testing.T) { { Bid: []openrtb.Bid{ { - ID: fmt.Sprintf("%d", rand.Intn(1000)), - ImpID: "unitCode_bogus", - Price: 1.0, - AdM: "Content", - CrID: "567", - W: 10, - H: 12, - DealID: "5", + ID: fmt.Sprintf("%d", rand.Intn(1000)), + ImpID: "unitCode_bogus", + Price: 1.0, + AdM: "Content", + W: 10, + H: 12, }, }, }, @@ -439,8 +458,6 @@ func TestIxMismatchUnitCode(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} pbBidder := pbs.PBSBidder{ @@ -449,11 +466,93 @@ func TestIxMismatchUnitCode(t *testing.T) { getAdUnit(), }, } - if _, err := an.Call(ctx, &pbReq, &pbBidder); err == nil { + if _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder); err == nil { t.Fatalf("Should have gotten an error for unknown adunit code") } } +func TestNoSeatBid(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + + var breq openrtb.BidRequest + err = json.Unmarshal(body, &breq) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp := openrtb.BidResponse{} + + js, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(js) + }), + ) + defer server.Close() + + ctx := context.TODO() + pbReq := pbs.PBSRequest{} + pbBidder := pbs.PBSBidder{ + BidderCode: "bannerCode", + AdUnits: []pbs.PBSAdUnit{ + getAdUnit(), + }, + } + if _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder); err != nil { + t.Fatalf("Should not have gotten an error: %v", err) + } +} + +func TestNoSeatBidBid(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + + var breq openrtb.BidRequest + err = json.Unmarshal(body, &breq) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp := openrtb.BidResponse{ + SeatBid: []openrtb.SeatBid{ + {}, + }, + } + + js, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(js) + }), + ) + defer server.Close() + + ctx := context.TODO() + pbReq := pbs.PBSRequest{} + pbBidder := pbs.PBSBidder{ + BidderCode: "bannerCode", + AdUnits: []pbs.PBSAdUnit{ + getAdUnit(), + }, + } + if _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder); err != nil { + t.Fatalf("Should not have gotten an error: %v", err) + } +} + func TestIxInvalidParam(t *testing.T) { server := httptest.NewServer( @@ -461,8 +560,6 @@ func TestIxInvalidParam(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} adUnit := getAdUnit() @@ -473,7 +570,7 @@ func TestIxInvalidParam(t *testing.T) { adUnit, }, } - if _, err := an.Call(ctx, &pbReq, &pbBidder); err == nil { + if _, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder); err == nil { t.Fatalf("Should have gotten an error for unrecognized params") } } @@ -485,8 +582,6 @@ func TestIxSingleSlotSingleValidSize(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} pbBidder := pbs.PBSBidder{ @@ -495,7 +590,7 @@ func TestIxSingleSlotSingleValidSize(t *testing.T) { getAdUnit(), }, } - bids, err := an.Call(ctx, &pbReq, &pbBidder) + bids, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err != nil { t.Fatalf("Should not have gotten an error: %v", err) } @@ -512,19 +607,10 @@ func TestIxTwoSlotValidSize(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} adUnit1 := getAdUnit() - adUnit2 := getAdUnit() - adUnit2.Code = "unitCode2" - adUnit2.Sizes = []openrtb.Format{ - { - W: 8, - H: 10, - }, - } + adUnit2 := getVideoAdUnit() adUnit2.Params = json.RawMessage("{\"siteId\":\"1111\"}") pbBidder := pbs.PBSBidder{ @@ -534,7 +620,7 @@ func TestIxTwoSlotValidSize(t *testing.T) { adUnit2, }, } - bids, err := an.Call(ctx, &pbReq, &pbBidder) + bids, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err != nil { t.Fatalf("Should not have gotten an error: %v", err) } @@ -561,8 +647,6 @@ func TestIxTwoSlotMultiSizeOnlyValidIXSizeResponse(t *testing.T) { ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} adUnit := getAdUnit() @@ -574,8 +658,7 @@ func TestIxTwoSlotMultiSizeOnlyValidIXSizeResponse(t *testing.T) { adUnit, }, } - bids, err := an.Call(ctx, &pbReq, &pbBidder) - + bids, err := newAdapter(server.URL).Call(ctx, &pbReq, &pbBidder) if err != nil { t.Fatalf("Should not have gotten an error: %v", err) } @@ -609,20 +692,19 @@ func findBidByAdUnitCode(bids pbs.PBSBidSlice, c string) *pbs.PBSBid { return &pbs.PBSBid{} } -func TestIxRequestLimit(t *testing.T) { +func TestIxMaxRequests(t *testing.T) { server := httptest.NewServer( http.HandlerFunc(dummyIXServer), ) defer server.Close() - conf := *adapters.DefaultHTTPAdapterConfig - an := NewIxLegacyAdapter(&conf, server.URL) + adapter := newAdapter(server.URL) ctx := context.TODO() pbReq := pbs.PBSRequest{} adUnits := []pbs.PBSAdUnit{} - for i := 0; i < requestLimit+1; i++ { + for i := 0; i < adapter.maxRequests+1; i++ { adUnits = append(adUnits, getAdUnit()) } @@ -631,13 +713,12 @@ func TestIxRequestLimit(t *testing.T) { AdUnits: adUnits, } - bids, err := an.Call(ctx, &pbReq, &pbBidder) - + bids, err := adapter.Call(ctx, &pbReq, &pbBidder) if err != nil { t.Fatalf("Should not have gotten an error: %v", err) } - if len(bids) != requestLimit { - t.Fatalf("Should have received %d bid", requestLimit) + if len(bids) != adapter.maxRequests { + t.Fatalf("Should have received %d bid", adapter.maxRequests) } } diff --git a/adapters/ix/ixtest/exemplary/additional-consent.json b/adapters/ix/ixtest/exemplary/additional-consent.json new file mode 100644 index 00000000000..697c8697061 --- /dev/null +++ b/adapters/ix/ixtest/exemplary/additional-consent.json @@ -0,0 +1,124 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "user": { + "ext": { + "consent": "COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw", + "consented_providers_settings": { + "consented_providers": [1] + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "user": { + "ext": { + "consent": "COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw", + "consented_providers_settings": { + "consented_providers": [1] + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/banner-no-format.json b/adapters/ix/ixtest/exemplary/banner-no-format.json new file mode 100644 index 00000000000..84f8499f2aa --- /dev/null +++ b/adapters/ix/ixtest/exemplary/banner-no-format.json @@ -0,0 +1,108 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/max-requests.json b/adapters/ix/ixtest/exemplary/max-requests.json new file mode 100644 index 00000000000..46d9ec5d6b7 --- /dev/null +++ b/adapters/ix/ixtest/exemplary/max-requests.json @@ -0,0 +1,255 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-1", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id-2", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/no-pub-id.json b/adapters/ix/ixtest/exemplary/no-pub-id.json new file mode 100644 index 00000000000..838d3c5ee2e --- /dev/null +++ b/adapters/ix/ixtest/exemplary/no-pub-id.json @@ -0,0 +1,121 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "site": { + "page": "https://www.example.com/", + "publisher": { + "name": "publisher-name" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "site": { + "page": "https://www.example.com/", + "publisher": { + "id": "569749", + "name": "publisher-name" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/no-pub.json b/adapters/ix/ixtest/exemplary/no-pub.json new file mode 100644 index 00000000000..bc113468be9 --- /dev/null +++ b/adapters/ix/ixtest/exemplary/no-pub.json @@ -0,0 +1,117 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "site": { + "page": "https://www.example.com/" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "site": { + "page": "https://www.example.com/", + "publisher": { + "id": "569749" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/simple-audio.json b/adapters/ix/ixtest/exemplary/simple-audio.json new file mode 100644 index 00000000000..6bfe62a415b --- /dev/null +++ b/adapters/ix/ixtest/exemplary/simple-audio.json @@ -0,0 +1,102 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": [ + "audio/mp4" + ], + "protocols": [ + 9, + 10 + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": [ + "audio/mp4" + ], + "protocols": [ + 9, + 10 + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110" + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110" + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/simple-banner-multi-size.json b/adapters/ix/ixtest/exemplary/simple-banner-multi-size.json new file mode 100644 index 00000000000..5e0a311a91b --- /dev/null +++ b/adapters/ix/ixtest/exemplary/simple-banner-multi-size.json @@ -0,0 +1,201 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 600 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 600, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 600, + "ext": { + "ix": {} + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/simple-native.json b/adapters/ix/ixtest/exemplary/simple-native.json new file mode 100644 index 00000000000..845ba7cd80a --- /dev/null +++ b/adapters/ix/ixtest/exemplary/simple-native.json @@ -0,0 +1,104 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "{}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpcalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "{}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "cat": [ + "IAB3-1" + ], + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "cat": [ + "IAB3-1" + ], + "ext": { + "ix": {} + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/exemplary/simple-video.json b/adapters/ix/ixtest/exemplary/simple-video.json new file mode 100644 index 00000000000..051eaa98b7d --- /dev/null +++ b/adapters/ix/ixtest/exemplary/simple-video.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 15, + "maxduration": 30, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 940, + "h": 560 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": [ + "IAB9-1" + ], + "ext": { + "ix": {} + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/ix/ixtest/params/race/audio.json b/adapters/ix/ixtest/params/race/audio.json new file mode 100644 index 00000000000..703df52e0dc --- /dev/null +++ b/adapters/ix/ixtest/params/race/audio.json @@ -0,0 +1,4 @@ +{ + "siteId": "569749", + "size": [300, 250] +} diff --git a/adapters/ix/ixtest/params/race/banner.json b/adapters/ix/ixtest/params/race/banner.json index e90050ef4a9..703df52e0dc 100644 --- a/adapters/ix/ixtest/params/race/banner.json +++ b/adapters/ix/ixtest/params/race/banner.json @@ -1,3 +1,4 @@ { - "siteId": 500 + "siteId": "569749", + "size": [300, 250] } diff --git a/adapters/ix/ixtest/params/race/native.json b/adapters/ix/ixtest/params/race/native.json new file mode 100644 index 00000000000..703df52e0dc --- /dev/null +++ b/adapters/ix/ixtest/params/race/native.json @@ -0,0 +1,4 @@ +{ + "siteId": "569749", + "size": [300, 250] +} diff --git a/adapters/ix/ixtest/params/race/video.json b/adapters/ix/ixtest/params/race/video.json index e90050ef4a9..993f0f6a4fb 100644 --- a/adapters/ix/ixtest/params/race/video.json +++ b/adapters/ix/ixtest/params/race/video.json @@ -1,3 +1,4 @@ { - "siteId": 500 + "siteId": "569749", + "size": [940, 560] } diff --git a/adapters/ix/ixtest/supplemental/bad-ext-bidder.json b/adapters/ix/ixtest/supplemental/bad-ext-bidder.json new file mode 100644 index 00000000000..e08da1e1a84 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/bad-ext-bidder.json @@ -0,0 +1,22 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "prebid": 1 + } + } + ], + "site": { + "page": "https://www.example.com/" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "json: cannot unmarshal number into Go struct field ExtImpBidder.prebid of type openrtb_ext.ExtImpPrebid", + "comparison": "literal" + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/bad-ext-ix.json b/adapters/ix/ixtest/supplemental/bad-ext-ix.json new file mode 100644 index 00000000000..a9a0383b1a1 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/bad-ext-ix.json @@ -0,0 +1,21 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + } + } + ], + "site": { + "page": "https://www.example.com/" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/bad-imp-id.json b/adapters/ix/ixtest/supplemental/bad-imp-id.json new file mode 100644 index 00000000000..0b852c85d2b --- /dev/null +++ b/adapters/ix/ixtest/supplemental/bad-imp-id.json @@ -0,0 +1,118 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "bad-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {} + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "bad-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {} + } + }, + "type": "" + } + ] + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unmatched impression id: bad-imp-id.", + "comparison": "literal" + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/bad-request.json b/adapters/ix/ixtest/supplemental/bad-request.json new file mode 100644 index 00000000000..f520d7851f4 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/bad-request.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/bad-response-body.json b/adapters/ix/ixtest/supplemental/bad-response-body.json new file mode 100644 index 00000000000..80c2860e3b6 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/bad-response-body.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": 1 + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "JSON parsing error: json: cannot unmarshal number into Go struct field BidResponse.id of type string", + "comparison": "literal" + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/no-content.json b/adapters/ix/ixtest/supplemental/no-content.json new file mode 100644 index 00000000000..fcdf02cbecc --- /dev/null +++ b/adapters/ix/ixtest/supplemental/no-content.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ] +} diff --git a/adapters/ix/ixtest/supplemental/no-imp.json b/adapters/ix/ixtest/supplemental/no-imp.json new file mode 100644 index 00000000000..308decd2ccc --- /dev/null +++ b/adapters/ix/ixtest/supplemental/no-imp.json @@ -0,0 +1,6 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [] + } +} diff --git a/adapters/ix/ixtest/supplemental/not-found.json b/adapters/ix/ixtest/supplemental/not-found.json new file mode 100644 index 00000000000..980bd942e77 --- /dev/null +++ b/adapters/ix/ixtest/supplemental/not-found.json @@ -0,0 +1,63 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ] + } + }, + "mockResponse": { + "status": 404, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/config/config.go b/config/config.go index f48ce9dae84..f720d84106a 100755 --- a/config/config.go +++ b/config/config.go @@ -834,7 +834,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.inmobi.endpoint", "https://api.w.inmobi.com/showad/openrtb/bidder/prebid") - v.SetDefault("adapters.ix.endpoint", "http://appnexus-us-east.lb.indexww.com/transbidder?p=184932") + v.SetDefault("adapters.ix.endpoint", "http://exchange.indexww.com/pbs?p=192919") v.SetDefault("adapters.krushmedia.endpoint", "http://ads4.krushmedia.com/?c=rtb&m=req&key={{.AccountID}}") v.SetDefault("adapters.invibes.endpoint", "https://{{.Host}}/bid/ServerBidAdContent") v.SetDefault("adapters.kidoz.endpoint", "http://prebid-adapter.kidoz.net/openrtb2/auction?src=prebid-server") diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 32311ef640a..9095ec1e76e 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -46,6 +46,7 @@ import ( "github.com/prebid/prebid-server/adapters/improvedigital" "github.com/prebid/prebid-server/adapters/inmobi" "github.com/prebid/prebid-server/adapters/invibes" + "github.com/prebid/prebid-server/adapters/ix" "github.com/prebid/prebid-server/adapters/kidoz" "github.com/prebid/prebid-server/adapters/krushmedia" "github.com/prebid/prebid-server/adapters/kubient" @@ -143,6 +144,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderImprovedigital: improvedigital.Builder, openrtb_ext.BidderInMobi: inmobi.Builder, openrtb_ext.BidderInvibes: invibes.Builder, + openrtb_ext.BidderIx: ix.Builder, openrtb_ext.BidderKidoz: kidoz.Builder, openrtb_ext.BidderKrushmedia: krushmedia.Builder, openrtb_ext.BidderKubient: kubient.Builder, diff --git a/exchange/adapter_util.go b/exchange/adapter_util.go index f361508e91e..6e12ed00536 100644 --- a/exchange/adapter_util.go +++ b/exchange/adapter_util.go @@ -7,7 +7,6 @@ import ( "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/adapters" - "github.com/prebid/prebid-server/adapters/ix" "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/pulsepoint" "github.com/prebid/prebid-server/config" @@ -59,7 +58,7 @@ func buildBidders(adapterConfig map[string]config.Adapter, infos adapters.Bidder } // Ignore Legacy Bidders - if bidderName == openrtb_ext.BidderIx || bidderName == openrtb_ext.BidderLifestreet || bidderName == openrtb_ext.BidderPulsepoint { + if bidderName == openrtb_ext.BidderLifestreet || bidderName == openrtb_ext.BidderPulsepoint { continue } @@ -92,13 +91,7 @@ func buildBidders(adapterConfig map[string]config.Adapter, infos adapters.Bidder } func buildExchangeBiddersLegacy(adapterConfig map[string]config.Adapter, infos adapters.BidderInfos) map[openrtb_ext.BidderName]adaptedBidder { - bidders := make(map[openrtb_ext.BidderName]adaptedBidder, 3) - - // Index - if infos[string(openrtb_ext.BidderIx)].Status == adapters.StatusActive { - adapter := ix.NewIxLegacyAdapter(adapters.DefaultHTTPAdapterConfig, adapterConfig[string(openrtb_ext.BidderIx)].Endpoint) - bidders[openrtb_ext.BidderIx] = adaptLegacyAdapter(adapter) - } + bidders := make(map[openrtb_ext.BidderName]adaptedBidder, 2) // Lifestreet if infos[string(openrtb_ext.BidderLifestreet)].Status == adapters.StatusActive { diff --git a/exchange/adapter_util_test.go b/exchange/adapter_util_test.go index f001092245e..d7059648877 100644 --- a/exchange/adapter_util_test.go +++ b/exchange/adapter_util_test.go @@ -9,7 +9,6 @@ import ( "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/adapters" "github.com/prebid/prebid-server/adapters/appnexus" - "github.com/prebid/prebid-server/adapters/ix" "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/pulsepoint" "github.com/prebid/prebid-server/adapters/rubicon" @@ -29,12 +28,12 @@ var ( func TestBuildAdaptersSuccess(t *testing.T) { client := &http.Client{} cfg := &config.Configuration{Adapters: map[string]config.Adapter{ - "appnexus": {}, - "ix": {Endpoint: "anyEndpoint"}, + "appnexus": {}, + "lifestreet": {Endpoint: "anyEndpoint"}, }} infos := map[string]adapters.BidderInfo{ - "appnexus": infoActive, - "ix": infoActive, + "appnexus": infoActive, + "lifestreet": infoActive, } metricEngine := &metrics.DummyMetricsEngine{} @@ -45,12 +44,12 @@ func TestBuildAdaptersSuccess(t *testing.T) { appnexusBidderAdapted := adaptBidder(appnexusBidderWithInfo, client, &config.Configuration{}, metricEngine, openrtb_ext.BidderAppnexus) appnexusBidderValidated := addValidatedBidderMiddleware(appnexusBidderAdapted) - idLegacyAdapted := &adaptedAdapter{ix.NewIxLegacyAdapter(adapters.DefaultHTTPAdapterConfig, "anyEndpoint")} + idLegacyAdapted := &adaptedAdapter{lifestreet.NewLifestreetLegacyAdapter(adapters.DefaultHTTPAdapterConfig, "anyEndpoint")} idLegacyValidated := addValidatedBidderMiddleware(idLegacyAdapted) expectedBidders := map[openrtb_ext.BidderName]adaptedBidder{ - openrtb_ext.BidderAppnexus: appnexusBidderValidated, - openrtb_ext.BidderIx: idLegacyValidated, + openrtb_ext.BidderAppnexus: appnexusBidderValidated, + openrtb_ext.BidderLifestreet: idLegacyValidated, } assert.Equal(t, expectedBidders, bidders) @@ -215,9 +214,9 @@ func TestBuildBidders(t *testing.T) { }, { description: "Success - Ignores Legacy", - adapterConfig: map[string]config.Adapter{"appnexus": {}, "ix": {}, "lifestreet": {}, "pulsepoint": {}}, - bidderInfos: map[string]adapters.BidderInfo{"appnexus": infoActive, "ix": infoActive, "lifestreet": infoActive, "pulsepoint": infoActive}, - builders: map[openrtb_ext.BidderName]adapters.Builder{openrtb_ext.BidderAppnexus: appnexusBuilder, openrtb_ext.BidderIx: inconsequentialBuilder, openrtb_ext.BidderLifestreet: inconsequentialBuilder, openrtb_ext.BidderPulsepoint: inconsequentialBuilder}, + adapterConfig: map[string]config.Adapter{"appnexus": {}, "lifestreet": {}, "pulsepoint": {}}, + bidderInfos: map[string]adapters.BidderInfo{"appnexus": infoActive, "lifestreet": infoActive, "pulsepoint": infoActive}, + builders: map[openrtb_ext.BidderName]adapters.Builder{openrtb_ext.BidderAppnexus: appnexusBuilder, openrtb_ext.BidderLifestreet: inconsequentialBuilder, openrtb_ext.BidderPulsepoint: inconsequentialBuilder}, expectedBidders: map[openrtb_ext.BidderName]adapters.Bidder{ openrtb_ext.BidderAppnexus: adapters.EnforceBidderInfo(appnexusBidder, infoActive), }, @@ -267,7 +266,6 @@ func TestBuildBidders(t *testing.T) { func TestBuildExchangeBiddersLegacy(t *testing.T) { cfg := config.Adapter{Endpoint: "anyEndpoint"} - expectedIx := &adaptedAdapter{ix.NewIxLegacyAdapter(adapters.DefaultHTTPAdapterConfig, "anyEndpoint")} expectedLifestreet := &adaptedAdapter{lifestreet.NewLifestreetLegacyAdapter(adapters.DefaultHTTPAdapterConfig, "anyEndpoint")} expectedPulsepoint := &adaptedAdapter{pulsepoint.NewPulsePointLegacyAdapter(adapters.DefaultHTTPAdapterConfig, "anyEndpoint")} @@ -279,27 +277,27 @@ func TestBuildExchangeBiddersLegacy(t *testing.T) { }{ { description: "All Active", - adapterConfig: map[string]config.Adapter{"ix": cfg, "lifestreet": cfg, "pulsepoint": cfg}, - bidderInfos: map[string]adapters.BidderInfo{"ix": infoActive, "lifestreet": infoActive, "pulsepoint": infoActive}, - expected: map[openrtb_ext.BidderName]adaptedBidder{"ix": expectedIx, "lifestreet": expectedLifestreet, "pulsepoint": expectedPulsepoint}, + adapterConfig: map[string]config.Adapter{"lifestreet": cfg, "pulsepoint": cfg}, + bidderInfos: map[string]adapters.BidderInfo{"lifestreet": infoActive, "pulsepoint": infoActive}, + expected: map[openrtb_ext.BidderName]adaptedBidder{"lifestreet": expectedLifestreet, "pulsepoint": expectedPulsepoint}, }, { description: "All Disabled", - adapterConfig: map[string]config.Adapter{"ix": cfg, "lifestreet": cfg, "pulsepoint": cfg}, - bidderInfos: map[string]adapters.BidderInfo{"ix": infoDisabled, "lifestreet": infoDisabled, "pulsepoint": infoDisabled}, + adapterConfig: map[string]config.Adapter{"lifestreet": cfg, "pulsepoint": cfg}, + bidderInfos: map[string]adapters.BidderInfo{"lifestreet": infoDisabled, "pulsepoint": infoDisabled}, expected: map[openrtb_ext.BidderName]adaptedBidder{}, }, { description: "All Unknown", - adapterConfig: map[string]config.Adapter{"ix": cfg, "lifestreet": cfg, "pulsepoint": cfg}, - bidderInfos: map[string]adapters.BidderInfo{"ix": infoUnknown, "lifestreet": infoUnknown, "pulsepoint": infoUnknown}, + adapterConfig: map[string]config.Adapter{"lifestreet": cfg, "pulsepoint": cfg}, + bidderInfos: map[string]adapters.BidderInfo{"lifestreet": infoUnknown, "pulsepoint": infoUnknown}, expected: map[openrtb_ext.BidderName]adaptedBidder{}, }, { description: "Mixed", - adapterConfig: map[string]config.Adapter{"ix": cfg, "lifestreet": cfg, "pulsepoint": cfg}, - bidderInfos: map[string]adapters.BidderInfo{"ix": infoActive, "lifestreet": infoDisabled, "pulsepoint": infoUnknown}, - expected: map[openrtb_ext.BidderName]adaptedBidder{"ix": expectedIx}, + adapterConfig: map[string]config.Adapter{"lifestreet": cfg, "pulsepoint": cfg}, + bidderInfos: map[string]adapters.BidderInfo{"lifestreet": infoActive, "pulsepoint": infoUnknown}, + expected: map[openrtb_ext.BidderName]adaptedBidder{"lifestreet": expectedLifestreet}, }, } diff --git a/openrtb_ext/imp_ix.go b/openrtb_ext/imp_ix.go new file mode 100644 index 00000000000..99cd3a215e4 --- /dev/null +++ b/openrtb_ext/imp_ix.go @@ -0,0 +1,7 @@ +package openrtb_ext + +// ExtImpIx defines the contract for bidrequest.imp[i].ext.ix +type ExtImpIx struct { + SiteId string `json:"siteId"` + Size []int `json:"size"` +} diff --git a/static/bidder-info/ix.yaml b/static/bidder-info/ix.yaml index 326989ae9fe..2f00ceb952f 100644 --- a/static/bidder-info/ix.yaml +++ b/static/bidder-info/ix.yaml @@ -1,6 +1,15 @@ maintainer: email: "pdu-supply-prebid@indexexchange.com" capabilities: + app: + mediaTypes: + - banner + - video + - native + - audio site: mediaTypes: - banner + - video + - native + - audio From 601a746c4767d05b920de1ca5b877e45c40d0c00 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Thu, 7 Jan 2021 08:54:20 -0500 Subject: [PATCH 331/381] =?UTF-8?q?Fix=20GDPR=20consent=20assumption=20whe?= =?UTF-8?q?n=20gdpr=20req=20signal=20is=20unambiguous=20and=20s=E2=80=A6?= =?UTF-8?q?=20(#1591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix GDPR consent assumption when gdpr req signal is unambiguous and set to 1 and consent string is blank * Refactor TestAllowPersonalInfo to make it table-based and add empty consent string cases * Update PersonalInfoAllowed to allow PI if the request indicates GDPR does not apply * Update test descriptions * Update default vendor permissions to only allow PI based on UserSyncIfAmbiguous if request GDPR signal is ambiguous * Change GDPR request signal type name and other PR feedback code style changes * Rename GDPR package signal constants and consolidate gdprEnforced and gdprEnabled variables * Hoist GDPR signal/empty consent checks before vendor list check * Rename gdpr to gdprSignal in Permissions interface and implementations * Fix merge mistakes * Update gdpr logic to set the gdpr signal when ambiguous according to the config flag * Add userSyncIfAmbiguous to all test cases in TestAllowPersonalInfo * Simplify TestAllowPersonalInfo --- endpoints/auction_test.go | 2 +- endpoints/cookie_sync_test.go | 2 +- endpoints/setuid_test.go | 3 +- exchange/gdpr.go | 12 ++-- exchange/gdpr_test.go | 109 +++++++++++++++++++++++------- exchange/utils.go | 11 +-- exchange/utils_test.go | 35 +++++++++- gdpr/gdpr.go | 2 +- gdpr/impl.go | 47 ++++++++----- gdpr/impl_test.go | 123 ++++++++++++++++++++++++++++------ 10 files changed, 264 insertions(+), 82 deletions(-) diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go index b5f0989ed28..6a237e39d4a 100644 --- a/endpoints/auction_test.go +++ b/endpoints/auction_test.go @@ -420,7 +420,7 @@ 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, bool, bool, error) { +func (m *auctionMockPermissions) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string) (bool, bool, bool, error) { return m.allowPI, m.allowGeo, m.allowID, nil } diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index ee29125b908..c790fcd9d74 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -254,6 +254,6 @@ 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, bool, bool, error) { +func (g *gdprPerms) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string) (bool, bool, bool, error) { return true, true, true, nil } diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go index 1f2c4040d59..602859582c0 100644 --- a/endpoints/setuid_test.go +++ b/endpoints/setuid_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/privacy" "github.com/prebid/prebid-server/usersync" @@ -437,7 +438,7 @@ 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, bool, bool, error) { +func (g *mockPermsSetUID) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal gdpr.Signal, consent string) (bool, bool, bool, error) { return g.allowPI, g.allowPI, g.allowPI, nil } diff --git a/exchange/gdpr.go b/exchange/gdpr.go index 2e2343d6db5..f464cac2bee 100644 --- a/exchange/gdpr.go +++ b/exchange/gdpr.go @@ -4,25 +4,21 @@ import ( "encoding/json" "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/gdpr" ) // ExtractGDPR will pull the gdpr flag from an openrtb request -func extractGDPR(bidRequest *openrtb.BidRequest, usersyncIfAmbiguous bool) (gdpr int) { +func extractGDPR(bidRequest *openrtb.BidRequest) gdpr.Signal { var re regsExt var err error if bidRequest.Regs != nil { err = json.Unmarshal(bidRequest.Regs.Ext, &re) } if re.GDPR == nil || err != nil { - if usersyncIfAmbiguous { - gdpr = 0 - } else { - gdpr = 1 - } + return gdpr.SignalAmbiguous } else { - gdpr = *re.GDPR + return gdpr.Signal(*re.GDPR) } - return } // ExtractConsent will pull the consent string from an openrtb request diff --git a/exchange/gdpr_test.go b/exchange/gdpr_test.go index 2d8fff86bd2..f79061044a2 100644 --- a/exchange/gdpr_test.go +++ b/exchange/gdpr_test.go @@ -5,40 +5,97 @@ import ( "testing" "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/gdpr" "github.com/stretchr/testify/assert" ) -func TestExtractGDPRFound(t *testing.T) { - gdprTest := openrtb.BidRequest{ - User: &openrtb.User{ - Ext: json.RawMessage(`{"consent": "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}`), +func TestExtractGDPR(t *testing.T) { + tests := []struct { + description string + giveRegs *openrtb.Regs + wantGDPR gdpr.Signal + }{ + { + description: "Regs Ext GDPR = 0", + giveRegs: &openrtb.Regs{Ext: json.RawMessage(`{"gdpr": 0}`)}, + wantGDPR: gdpr.SignalNo, }, - Regs: &openrtb.Regs{ - Ext: json.RawMessage(`{"gdpr": 1}`), + { + description: "Regs Ext GDPR = 1", + giveRegs: &openrtb.Regs{Ext: json.RawMessage(`{"gdpr": 1}`)}, + wantGDPR: gdpr.SignalYes, + }, + { + description: "Regs Ext GDPR = null", + giveRegs: &openrtb.Regs{Ext: json.RawMessage(`{"gdpr": null}`)}, + wantGDPR: gdpr.SignalAmbiguous, + }, + { + description: "Regs is nil", + giveRegs: nil, + wantGDPR: gdpr.SignalAmbiguous, + }, + { + description: "Regs Ext is nil", + giveRegs: &openrtb.Regs{Ext: nil}, + wantGDPR: gdpr.SignalAmbiguous, + }, + { + description: "JSON unmarshal error", + giveRegs: &openrtb.Regs{Ext: json.RawMessage(`{"`)}, + wantGDPR: gdpr.SignalAmbiguous, }, } - gdpr := extractGDPR(&gdprTest, false) - consent := extractConsent(&gdprTest) - assert.Equal(t, 1, gdpr) - assert.Equal(t, "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", consent) - - gdprTest.Regs.Ext = json.RawMessage(`{"gdpr": 0}`) - gdpr = extractGDPR(&gdprTest, true) - consent = extractConsent(&gdprTest) - assert.Equal(t, 0, gdpr) - assert.Equal(t, "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", consent) -} -func TestGDPRUnknown(t *testing.T) { - gdprTest := openrtb.BidRequest{} + for _, tt := range tests { + bidReq := openrtb.BidRequest{ + Regs: tt.giveRegs, + } - gdpr := extractGDPR(&gdprTest, false) - consent := extractConsent(&gdprTest) - assert.Equal(t, 1, gdpr) - assert.Equal(t, "", consent) + result := extractGDPR(&bidReq) + assert.Equal(t, tt.wantGDPR, result, tt.description) + } +} - gdpr = extractGDPR(&gdprTest, true) - consent = extractConsent(&gdprTest) - assert.Equal(t, 0, gdpr) +func TestExtractConsent(t *testing.T) { + tests := []struct { + description string + giveUser *openrtb.User + wantConsent string + }{ + { + description: "User Ext Consent is not empty", + giveUser: &openrtb.User{Ext: json.RawMessage(`{"consent": "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}`)}, + wantConsent: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA", + }, + { + description: "User Ext Consent is empty", + giveUser: &openrtb.User{Ext: json.RawMessage(`{"consent": ""}`)}, + wantConsent: "", + }, + { + description: "User Ext is nil", + giveUser: &openrtb.User{Ext: nil}, + wantConsent: "", + }, + { + description: "User is nil", + giveUser: nil, + wantConsent: "", + }, + { + description: "JSON unmarshal error", + giveUser: &openrtb.User{Ext: json.RawMessage(`{`)}, + wantConsent: "", + }, + } + + for _, tt := range tests { + bidReq := openrtb.BidRequest{ + User: tt.giveUser, + } + result := extractConsent(&bidReq) + assert.Equal(t, tt.wantConsent, result, tt.description) + } } diff --git a/exchange/utils.go b/exchange/utils.go index a803e4e242b..21c31a290b4 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -75,8 +75,9 @@ func cleanOpenRTBRequests(ctx context.Context, return } - gdpr := extractGDPR(req.BidRequest, usersyncIfAmbiguous) + gdprSignal := extractGDPR(req.BidRequest) consent := extractConsent(req.BidRequest) + gdprEnforced := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && !usersyncIfAmbiguous) ccpaEnforcer, err := extractCCPA(req.BidRequest, privacyConfig, &req.Account, aliases, integrationTypeMap[req.LegacyLabels.RType]) if err != nil { @@ -97,9 +98,9 @@ func cleanOpenRTBRequests(ctx context.Context, privacyLabels.COPPAEnforced = privacyEnforcement.COPPA privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder) - gdprEnabled := gdprEnabled(&req.Account, privacyConfig, integrationTypeMap[req.LegacyLabels.RType]) + gdprEnforced = gdprEnforced && gdprEnabled(&req.Account, privacyConfig, integrationTypeMap[req.LegacyLabels.RType]) - if gdpr == 1 && gdprEnabled { + if gdprEnforced { privacyLabels.GDPREnforced = true parsedConsent, err := vendorconsent.ParseString(consent) if err == nil { @@ -114,9 +115,9 @@ func cleanOpenRTBRequests(ctx context.Context, privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String()) // GDPR - if gdpr == 1 && gdprEnabled { + if gdprEnforced { var publisherID = req.LegacyLabels.PubID - _, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, consent) + _, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent) privacyEnforcement.GDPRGeo = !geo && err == nil privacyEnforcement.GDPRID = !id && err == nil } else { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 757eb658222..f9a04f25e40 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -10,6 +10,7 @@ import ( "github.com/mxmCherry/openrtb" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/gdpr" "github.com/prebid/prebid-server/metrics" "github.com/prebid/prebid-server/openrtb_ext" "github.com/stretchr/testify/assert" @@ -30,7 +31,7 @@ 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, bool, bool, error) { +func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string) (bool, bool, bool, error) { return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil } @@ -1053,6 +1054,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr string gdprConsent string gdprScrub bool + userSyncIfAmbiguous bool expectPrivacyLabels metrics.PrivacyLabels }{ { @@ -1151,6 +1153,32 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { GDPRTCFVersion: "", }, }, + { + description: "Enforce - Ambiguous signal, don't sync user if ambiguous", + gdprAccountEnabled: nil, + gdprHostEnabled: true, + gdpr: "null", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: true, + userSyncIfAmbiguous: false, + expectPrivacyLabels: metrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: metrics.TCFVersionV1, + }, + }, + { + description: "Not Enforce - Ambiguous signal, sync user if ambiguous", + gdprAccountEnabled: nil, + gdprHostEnabled: true, + gdpr: "null", + gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprScrub: false, + userSyncIfAmbiguous: true, + expectPrivacyLabels: metrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + }, } for _, test := range testCases { @@ -1162,7 +1190,8 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { privacyConfig := config.Privacy{ GDPR: config.GDPR{ - Enabled: test.gdprHostEnabled, + Enabled: test.gdprHostEnabled, + UsersyncIfAmbiguous: test.userSyncIfAmbiguous, TCF2: config.TCF2{ Enabled: true, }, @@ -1186,7 +1215,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { auctionReq, nil, &permissionsMock{personalInfoAllowed: !test.gdprScrub}, - true, + test.userSyncIfAmbiguous, privacyConfig) result := results[0] diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index 60a7cc1e2c0..eea7fa5a64d 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -23,7 +23,7 @@ type Permissions interface { // Determines whether or not to send PI information to a bidder, or mask it out. // // 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, bool, error) + PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (bool, bool, bool, error) } // Versions of the GDPR TCF technical specification. diff --git a/gdpr/impl.go b/gdpr/impl.go index 5b3b86fe557..e1822ece530 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -19,6 +19,14 @@ import ( // // Nothing in this file is exported. Public APIs can be found in gdpr.go +type Signal int + +const ( + SignalAmbiguous Signal = -1 + SignalNo Signal = 0 + SignalYes Signal = 1 +) + type permissionsImpl struct { cfg config.GDPR vendorIDs map[openrtb_ext.BidderName]uint16 @@ -42,21 +50,35 @@ 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, bool, bool, error) { - _, ok := p.cfg.NonStandardPublisherMap[PublisherID] - if ok { +func (p *permissionsImpl) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (allowPI bool, allowGeo bool, allowID bool, err error) { + if _, ok := p.cfg.NonStandardPublisherMap[PublisherID]; ok { return true, true, true, nil } - id, ok := p.vendorIDs[bidder] - if ok { - return p.allowPI(ctx, id, consent) + if gdprSignal == SignalAmbiguous { + if p.cfg.UsersyncIfAmbiguous { + gdprSignal = SignalNo + } else { + gdprSignal = SignalYes + } } - if consent == "" { - return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil + if gdprSignal == SignalNo { + return true, true, true, nil + } + + if consent == "" && gdprSignal == SignalYes { + return false, false, false, nil + } + + if id, ok := p.vendorIDs[bidder]; ok { + return p.allowPI(ctx, id, consent) } + return p.defaultVendorPermissions() +} + +func (p *permissionsImpl) defaultVendorPermissions() (allowPI bool, allowGeo bool, allowID bool, err error) { return false, false, false, nil } @@ -95,11 +117,6 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen } func (p *permissionsImpl) allowPI(ctx context.Context, vendorID uint16, consent string) (bool, bool, bool, error) { - // If we're not given a consent string, respect the preferences in the app config. - if consent == "" { - return p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, p.cfg.UsersyncIfAmbiguous, nil - } - parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, consent) if err != nil { return false, false, false, err @@ -217,7 +234,7 @@ 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, bool, bool, error) { +func (a AlwaysAllow) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (bool, bool, bool, error) { return true, true, true, nil } @@ -232,6 +249,6 @@ func (a AlwaysFail) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.Bi return false, nil } -func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, consent string) (bool, bool, bool, error) { +func (a AlwaysFail) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdprSignal Signal, consent string) (bool, bool, bool, error) { return false, false, false, nil } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 793eb96753f..a7d4b26af67 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -177,20 +177,107 @@ func TestMalformedConsent(t *testing.T) { } func TestAllowPersonalInfo(t *testing.T) { + bidderAllowedByConsent := openrtb_ext.BidderAppnexus + bidderBlockedByConsent := openrtb_ext.BidderRubicon + consent := "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA" + + tests := []struct { + description string + bidderName openrtb_ext.BidderName + publisherID string + userSyncIfAmbiguous bool + gdpr Signal + consent string + allowPI bool + }{ + { + description: "Allow PI - Non standard publisher", + bidderName: bidderBlockedByConsent, + publisherID: "appNexusAppID", + userSyncIfAmbiguous: false, + gdpr: SignalYes, + consent: consent, + allowPI: true, + }, + { + description: "Allow PI - known vendor with No GDPR", + bidderName: bidderBlockedByConsent, + userSyncIfAmbiguous: false, + gdpr: SignalNo, + consent: consent, + allowPI: true, + }, + { + description: "Allow PI - known vendor with Yes GDPR", + bidderName: bidderAllowedByConsent, + userSyncIfAmbiguous: false, + gdpr: SignalYes, + consent: consent, + allowPI: true, + }, + { + description: "PI allowed according to host setting UserSyncIfAmbiguous true - known vendor with ambiguous GDPR and empty consent", + bidderName: bidderAllowedByConsent, + userSyncIfAmbiguous: true, + gdpr: SignalAmbiguous, + consent: "", + allowPI: true, + }, + { + description: "PI allowed according to host setting UserSyncIfAmbiguous true - known vendor with ambiguous GDPR and non-empty consent", + bidderName: bidderAllowedByConsent, + userSyncIfAmbiguous: true, + gdpr: SignalAmbiguous, + consent: consent, + allowPI: true, + }, + { + description: "PI allowed according to host setting UserSyncIfAmbiguous false - known vendor with ambiguous GDPR and empty consent", + bidderName: bidderAllowedByConsent, + userSyncIfAmbiguous: false, + gdpr: SignalAmbiguous, + consent: "", + allowPI: false, + }, + { + description: "PI allowed according to host setting UserSyncIfAmbiguous false - known vendor with ambiguous GDPR and non-empty consent", + bidderName: bidderAllowedByConsent, + userSyncIfAmbiguous: false, + gdpr: SignalAmbiguous, + consent: consent, + allowPI: true, + }, + { + description: "Don't allow PI - known vendor with Yes GDPR and empty consent", + bidderName: bidderAllowedByConsent, + userSyncIfAmbiguous: false, + gdpr: SignalYes, + consent: "", + allowPI: false, + }, + { + description: "Don't allow PI - default vendor with Yes GDPR and non-empty consent", + bidderName: bidderBlockedByConsent, + userSyncIfAmbiguous: false, + gdpr: SignalYes, + consent: consent, + allowPI: false, + }, + } + vendorListData := tcf1MarshalVendorList(tcf1VendorList{ VendorListVersion: 1, Vendors: []tcf1Vendor{ - {ID: 2, Purposes: []int{1}}, // cookie reads/writes - {ID: 3, Purposes: []int{1, 3}}, // ad personalization + {ID: 2, Purposes: []int{1, 3}}, }, }) perms := permissionsImpl{ cfg: config.GDPR{ - HostVendorID: 2, + HostVendorID: 2, + NonStandardPublisherMap: map[string]struct{}{"appNexusAppID": {}}, }, vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, - openrtb_ext.BidderPubmatic: 3, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ @@ -202,20 +289,14 @@ func TestAllowPersonalInfo(t *testing.T) { }, } - // PI needs both purposes to succeed - allowPI, _, _, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") - assertNilErr(t, err) - assertBoolsEqual(t, false, allowPI) + for _, tt := range tests { + perms.cfg.UsersyncIfAmbiguous = tt.userSyncIfAmbiguous - allowPI, _, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderPubmatic, "", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") - assertNilErr(t, err) - assertBoolsEqual(t, true, allowPI) + allowPI, _, _, err := perms.PersonalInfoAllowed(context.Background(), tt.bidderName, tt.publisherID, tt.gdpr, tt.consent) - // 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]struct{}{"appNexusAppID": {}} - allowPI, _, _, err = perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA") - assertNilErr(t, err) - assertBoolsEqual(t, true, allowPI) + assert.Nil(t, err, tt.description) + assert.Equal(t, tt.allowPI, allowPI, tt.description) + } } func buildTCF2VendorList34() tcf2VendorList { @@ -318,7 +399,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) { } for _, td := range testDefs { - allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, 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) @@ -344,7 +425,7 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) { } // 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]struct{}{"appNexusAppID": {}} - allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") + allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") assert.NoErrorf(t, err, "Error processing PersonalInfoAllowed") assert.EqualValuesf(t, true, allowPI, "AllowPI failure") assert.EqualValuesf(t, true, allowGeo, "AllowGeo failure") @@ -398,7 +479,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) { } for _, td := range testDefs { - allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, 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) @@ -454,7 +535,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) { } for _, td := range testDefs { - allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, 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) @@ -511,7 +592,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) { } for _, td := range testDefs { - allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", td.consent) + allowPI, allowGeo, allowID, err := perms.PersonalInfoAllowed(context.Background(), td.bidder, "", SignalYes, 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) From 2d76926da9fbef10f8db2e63aa7350cbe2acb90f Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Thu, 7 Jan 2021 12:03:37 -0500 Subject: [PATCH 332/381] Fix appnexus adapter not setting currency in the bid response (#1642) Co-authored-by: Gus --- adapters/appnexus/appnexus.go | 3 + .../simple-banner-foreign-currency.json | 141 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go index e6398b4a010..0a8146fdc41 100644 --- a/adapters/appnexus/appnexus.go +++ b/adapters/appnexus/appnexus.go @@ -604,6 +604,9 @@ func (a *AppNexusAdapter) MakeBids(internalRequest *openrtb.BidRequest, external } } } + if bidResp.Cur != "" { + bidResponse.Currency = bidResp.Cur + } return bidResponse, errs } diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json b/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json new file mode 100644 index 00000000000..b46c6f5f76f --- /dev/null +++ b/adapters/appnexus/appnexustest/exemplary/simple-banner-foreign-currency.json @@ -0,0 +1,141 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placement_id": 1 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://ib.adnxs.com/openrtb2", + "body": { + "id": "test-request-id", + "ext": { + "appnexus": { + "hb_source": 5 + }, + "prebid": {} + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "appnexus": { + "placement_id": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [{ + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }] + } + ], + "bidid": "5778926625248726496", + "cur": "MXN" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "MXN", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": ["appnexus.com"], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "cat": ["IAB20-3"], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 8189378542222915032, + "bid_ad_type": 0, + "bidder_id": 2, + "ranking_price": 0.000000, + "deal_priority": 5 + } + } + }, + "type": "banner" + } + ] + } + ] +} From 495870361fa1371bc80c9a274fb6830265b181df Mon Sep 17 00:00:00 2001 From: Viral Vala Date: Fri, 8 Jan 2021 10:33:11 +0530 Subject: [PATCH 333/381] OTT-54: Removing Bidder Config --- ...{bidder_config.go => bidder_config.go.bak} | 5 --- adapters/tagbidder/bidder_macro.go | 14 ++++++--- adapters/tagbidder/ibidder_macro.go | 7 ++++- adapters/tagbidder/macro_processor.go | 4 +-- adapters/tagbidder/tagbidder.go | 31 +++++++++---------- openrtb_ext/bidders.go | 2 ++ router/router.go | 13 ++++---- 7 files changed, 39 insertions(+), 37 deletions(-) rename adapters/tagbidder/{bidder_config.go => bidder_config.go.bak} (95%) diff --git a/adapters/tagbidder/bidder_config.go b/adapters/tagbidder/bidder_config.go.bak similarity index 95% rename from adapters/tagbidder/bidder_config.go rename to adapters/tagbidder/bidder_config.go.bak index 6ee9997e3fb..e82ba2209fd 100644 --- a/adapters/tagbidder/bidder_config.go +++ b/adapters/tagbidder/bidder_config.go.bak @@ -11,11 +11,6 @@ import ( "github.com/golang/glog" ) -//Flags of each tag bidder -type Flags struct { - RemoveEmptyParam bool `json:"remove_empty,omitempty"` -} - //BidderConfig mapper json type BidderConfig struct { URL string `json:"url,omitempty"` diff --git a/adapters/tagbidder/bidder_macro.go b/adapters/tagbidder/bidder_macro.go index 8271ff34ed6..68f883eb243 100644 --- a/adapters/tagbidder/bidder_macro.go +++ b/adapters/tagbidder/bidder_macro.go @@ -20,8 +20,8 @@ type BidderMacro struct { IBidderMacro //Configuration Parameters - Conf *config.Adapter - BidderConf *BidderConfig + Conf *config.Adapter + //BidderConf *BidderConfig //OpenRTB Specific Parameters Request *openrtb.BidRequest @@ -101,19 +101,23 @@ func (tag *BidderMacro) SetAdapterConfig(conf *config.Adapter) { tag.Conf = conf } +/* //SetBidderConfig will set Bidder config func (tag *BidderMacro) SetBidderConfig(conf *BidderConfig) { tag.BidderConf = conf } +*/ //GetURI get URL func (tag *BidderMacro) GetURI() string { //1. check for impression level URL //2. check for bidder config level URL //3. check for adapter config level URL - if len(tag.BidderConf.URL) > 0 { - return tag.BidderConf.URL - } + /* + if nil != tag.BidderConf && len(tag.BidderConf.URL) > 0 { + return tag.BidderConf.URL + } + */ return tag.Conf.Endpoint } diff --git a/adapters/tagbidder/ibidder_macro.go b/adapters/tagbidder/ibidder_macro.go index 27081391181..baf83cdb77a 100644 --- a/adapters/tagbidder/ibidder_macro.go +++ b/adapters/tagbidder/ibidder_macro.go @@ -8,6 +8,11 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/config" ) +//Flags of each tag bidder +type Flags struct { + RemoveEmptyParam bool `json:"remove_empty,omitempty"` +} + //IBidderMacro interface will capture all macro definition type IBidderMacro interface { //Helper Function @@ -15,7 +20,7 @@ type IBidderMacro interface { LoadImpression(imp *openrtb.Imp) error GetBidderKeys() map[string]string SetAdapterConfig(*config.Adapter) - SetBidderConfig(*BidderConfig) + //SetBidderConfig(*BidderConfig) GetURI() string GetHeaders() http.Header diff --git a/adapters/tagbidder/macro_processor.go b/adapters/tagbidder/macro_processor.go index 7576868c694..f3ee715cd5c 100644 --- a/adapters/tagbidder/macro_processor.go +++ b/adapters/tagbidder/macro_processor.go @@ -9,8 +9,8 @@ import ( ) const ( - macroPrefix string = `{{` //macro prefix can not be empty - macroSuffix string = `}}` //macro suffix can not be empty + macroPrefix string = `{` //macro prefix can not be empty + macroSuffix string = `}` //macro suffix can not be empty macroEscapeSuffix string = `_ESC` macroPrefixLen int = len(macroPrefix) macroSuffixLen int = len(macroSuffix) diff --git a/adapters/tagbidder/tagbidder.go b/adapters/tagbidder/tagbidder.go index 26bf9b45687..c12037ab4cd 100644 --- a/adapters/tagbidder/tagbidder.go +++ b/adapters/tagbidder/tagbidder.go @@ -13,8 +13,8 @@ import ( //TagBidder is default implementation of ITagBidder type TagBidder struct { adapters.Bidder - bidderName string - bidderConfig *BidderConfig + bidderName string + //bidderConfig *BidderConfig adapterConfig *config.Adapter } @@ -23,24 +23,19 @@ func NewTagBidder(bidderName openrtb_ext.BidderName, config config.Adapter) (*Ta obj := &TagBidder{ bidderName: string(bidderName), adapterConfig: &config, - bidderConfig: GetBidderConfig(string(bidderName)), - } - if nil == obj.bidderConfig { - return nil, errors.New(`missing bidder config`) + //bidderConfig: GetBidderConfig(string(bidderName)), } + /* + if nil == obj.bidderConfig { + return nil, errors.New(`missing bidder config`) + } + */ return obj, nil } //NewTestTagBidder is an constructor for TagBidder func NewTestTagBidder(bidderName openrtb_ext.BidderName, config config.Adapter) *TagBidder { - obj := &TagBidder{ - bidderName: string(bidderName), - adapterConfig: &config, - bidderConfig: GetBidderConfig(string(bidderName)), - } - if nil == obj.bidderConfig { - return nil - } + obj, _ := NewTagBidder(bidderName, config) return obj } @@ -60,8 +55,8 @@ func (a *TagBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters. macroProcessor := NewMacroProcessor(bidderMacro, bidderMapper) //Setting config parameters + //bidderMacro.SetBidderConfig(a.bidderConfig) bidderMacro.SetAdapterConfig(a.adapterConfig) - bidderMacro.SetBidderConfig(a.bidderConfig) bidderMacro.InitBidRequest(request) requestData := []*adapters.RequestData{} @@ -75,7 +70,8 @@ func (a *TagBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters. macroProcessor.SetBidderKeys(bidderKeys) fmt.Printf("\n[V2] Bidder Keys:%v", bidderKeys) - uri := macroProcessor.ProcessURL(bidderMacro.GetURI(), a.bidderConfig.Flags) + //uri := macroProcessor.ProcessURL(bidderMacro.GetURI(), a.bidderConfig.Flags) + uri := macroProcessor.ProcessURL(bidderMacro.GetURI(), Flags{RemoveEmptyParam: true}) requestData = append(requestData, &adapters.RequestData{ ImpIndex: i, @@ -91,7 +87,8 @@ func (a *TagBidder) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters. //MakeBids makes bids func (a *TagBidder) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { //response validation can be done here independently - handler, err := GetResponseHandler(a.bidderConfig.ResponseType) + //handler, err := GetResponseHandler(a.bidderConfig.ResponseType) + handler, err := GetResponseHandler(VASTTagResponseHandlerType) if nil != err { return nil, []error{err} } diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 36391701839..1e2d1d12b22 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -216,10 +216,12 @@ var BidderMap = map[string]BidderName{ "zeroclickfraud": BidderZeroClickFraud, } +/* //TagBidderMap contains list of tag based bidders var TagBidderMap = map[string]BidderName{ "spotx": BidderSpotX, } +*/ // BidderList returns the values of the BidderMap func BidderList() []BidderName { diff --git a/router/router.go b/router/router.go index 9549818dd0f..c1a16767b83 100644 --- a/router/router.go +++ b/router/router.go @@ -29,7 +29,6 @@ import ( "github.com/PubMatic-OpenWrap/prebid-server/adapters/pulsepoint" "github.com/PubMatic-OpenWrap/prebid-server/adapters/rubicon" "github.com/PubMatic-OpenWrap/prebid-server/adapters/sovrn" - "github.com/PubMatic-OpenWrap/prebid-server/adapters/tagbidder" "github.com/PubMatic-OpenWrap/prebid-server/analytics" analyticsConf "github.com/PubMatic-OpenWrap/prebid-server/analytics/config" "github.com/PubMatic-OpenWrap/prebid-server/cache" @@ -249,12 +248,12 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r if err != nil { glog.Fatalf("Failed to create the bidder params validator. %v", err) } - - err = tagbidder.InitTagBidderConfig(tagBidderSchemaDirectory, openrtb_ext.TagBidderMap) - if err != nil { - glog.Fatalf("Failed to create the tag bidder config. %v", err) - } - + /* + err = tagbidder.InitTagBidderConfig(tagBidderSchemaDirectory, openrtb_ext.TagBidderMap) + if err != nil { + glog.Fatalf("Failed to create the tag bidder config. %v", err) + } + */ p, _ := filepath.Abs(infoDirectory) bidderInfos := adapters.ParseBidderInfos(cfg.Adapters, p, openrtb_ext.BidderList()) From be4aa7312e2b59b722199fd616de2a1052253863 Mon Sep 17 00:00:00 2001 From: Giudici-a <34242194+Giudici-a@users.noreply.github.com> Date: Mon, 11 Jan 2021 16:25:55 +0100 Subject: [PATCH 334/381] Add Adot adapter (#1605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aurélien Giudici --- adapters/adot/adot.go | 117 +++++++++++++++++ adapters/adot/adot_test.go | 76 +++++++++++ .../adottest/exemplary/simple-banner.json | 94 ++++++++++++++ .../exemplary/simple-interstitial.json | 101 +++++++++++++++ .../adottest/exemplary/simple-native.json | 88 +++++++++++++ .../adot/adottest/exemplary/simple-video.json | 120 ++++++++++++++++++ .../adot/adottest/params/race/banner.json | 3 + adapters/adot/adottest/params/race/video.json | 3 + .../adottest/supplemental/simple-audio.json | 66 ++++++++++ .../supplemental/simple-parallax.json | 103 +++++++++++++++ .../adottest/supplemental/status_204.json | 39 ++++++ .../adottest/supplemental/status_400.json | 62 +++++++++ .../adottest/supplemental/status_500.json | 62 +++++++++ .../supplemental/unmarshal_error.json | 62 +++++++++ adapters/adot/params_test.go | 53 ++++++++ config/config.go | 1 + exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adot.go | 6 + static/bidder-info/adot.yaml | 14 ++ static/bidder-params/adot.json | 17 +++ usersync/usersyncers/syncer_test.go | 1 + 22 files changed, 1092 insertions(+) create mode 100644 adapters/adot/adot.go create mode 100644 adapters/adot/adot_test.go create mode 100644 adapters/adot/adottest/exemplary/simple-banner.json create mode 100644 adapters/adot/adottest/exemplary/simple-interstitial.json create mode 100644 adapters/adot/adottest/exemplary/simple-native.json create mode 100644 adapters/adot/adottest/exemplary/simple-video.json create mode 100644 adapters/adot/adottest/params/race/banner.json create mode 100644 adapters/adot/adottest/params/race/video.json create mode 100644 adapters/adot/adottest/supplemental/simple-audio.json create mode 100644 adapters/adot/adottest/supplemental/simple-parallax.json create mode 100644 adapters/adot/adottest/supplemental/status_204.json create mode 100644 adapters/adot/adottest/supplemental/status_400.json create mode 100644 adapters/adot/adottest/supplemental/status_500.json create mode 100644 adapters/adot/adottest/supplemental/unmarshal_error.json create mode 100644 adapters/adot/params_test.go create mode 100644 openrtb_ext/imp_adot.go create mode 100644 static/bidder-info/adot.yaml create mode 100644 static/bidder-params/adot.json diff --git a/adapters/adot/adot.go b/adapters/adot/adot.go new file mode 100644 index 00000000000..c58c4a1ee7a --- /dev/null +++ b/adapters/adot/adot.go @@ -0,0 +1,117 @@ +package adot + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" +) + +type adapter struct { + endpoint string +} + +type adotBidExt struct { + Adot bidExt `json:"adot"` +} + +type bidExt struct { + MediaType string `json:"media_type"` +} + +// Builder builds a new instance of the Adot adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +// MakeRequests makes the HTTP requests which should be made to fetch bids. +func (a *adapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var reqJSON []byte + var err error + + if reqJSON, err = json.Marshal(request); err != nil { + return nil, []error{fmt.Errorf("unable to marshal openrtb request (%v)", err)} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + return []*adapters.RequestData{{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + }}, nil +} + +// MakeBids unpacks the server's response into Bids. +// The bidder return a status code 204 when it cannot delivery an ad. +func (a *adapter) 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} + } + + bidsCapacity := 1 + if len(bidResp.SeatBid) > 0 { + bidsCapacity = len(bidResp.SeatBid[0].Bid) + } + bidResponse := adapters.NewBidderResponseWithBidsCapacity(bidsCapacity) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + if bidType, err := getMediaTypeForBid(&sb.Bid[i]); err == nil { + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + }) + } + } + } + + return bidResponse, nil +} + +// getMediaTypeForBid determines which type of bid. +func getMediaTypeForBid(bid *openrtb.Bid) (openrtb_ext.BidType, error) { + if bid == nil { + return "", fmt.Errorf("the bid request object is nil") + } + + var impExt adotBidExt + if err := json.Unmarshal(bid.Ext, &impExt); err == nil { + switch impExt.Adot.MediaType { + case string(openrtb_ext.BidTypeBanner): + return openrtb_ext.BidTypeBanner, nil + case string(openrtb_ext.BidTypeVideo): + return openrtb_ext.BidTypeVideo, nil + case string(openrtb_ext.BidTypeNative): + return openrtb_ext.BidTypeNative, nil + } + } + + return "", fmt.Errorf("unrecognized bid type in response from adot") +} diff --git a/adapters/adot/adot_test.go b/adapters/adot/adot_test.go new file mode 100644 index 00000000000..fca6b303626 --- /dev/null +++ b/adapters/adot/adot_test.go @@ -0,0 +1,76 @@ +package adot + +import ( + "encoding/json" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/adapters/adapterstest" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/openrtb_ext" + "github.com/stretchr/testify/assert" + "testing" +) + +const testsBidderEndpoint = "https://dsp.adotmob.com/headerbidding/bidrequest" + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAdot, config.Adapter{ + Endpoint: testsBidderEndpoint}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "adottest", bidder) +} + +//Test the media type error +func TestMediaTypeError(t *testing.T) { + _, err := getMediaTypeForBid(nil) + + assert.Error(t, err) + + byteInvalid, _ := json.Marshal(&adotBidExt{Adot: bidExt{"invalid"}}) + _, err = getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteInvalid)}) + + assert.Error(t, err) +} + +//Test the bid response when the bidder return a status code 204 +func TestBidResponseNoContent(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAdot, config.Adapter{ + Endpoint: "https://dsp.adotmob.com/headerbidding/bidrequest"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + bidResponse, err := bidder.MakeBids(nil, nil, &adapters.ResponseData{StatusCode: 204}) + if bidResponse != nil { + t.Fatalf("the bid response should be nil since the bidder status is No Content") + } else if err != nil { + t.Fatalf("the error should be nil since the bidder status is 204 : No Content") + } +} + +//Test the media type for a bid response +func TestMediaTypeForBid(t *testing.T) { + byteBanner, _ := json.Marshal(&adotBidExt{Adot: bidExt{"banner"}}) + byteVideo, _ := json.Marshal(&adotBidExt{Adot: bidExt{"video"}}) + byteNative, _ := json.Marshal(&adotBidExt{Adot: bidExt{"native"}}) + + bidTypeBanner, _ := getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteBanner)}) + if bidTypeBanner != openrtb_ext.BidTypeBanner { + t.Errorf("the type is not the valid one. actual: %v, expected: %v", bidTypeBanner, openrtb_ext.BidTypeBanner) + } + + bidTypeVideo, _ := getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteVideo)}) + if bidTypeVideo != openrtb_ext.BidTypeVideo { + t.Errorf("the type is not the valid one. actual: %v, expected: %v", bidTypeVideo, openrtb_ext.BidTypeVideo) + } + + bidTypeNative, _ := getMediaTypeForBid(&openrtb.Bid{Ext: json.RawMessage(byteNative)}) + if bidTypeNative != openrtb_ext.BidTypeNative { + t.Errorf("the type is not the valid one. actual: %v, expected: %v", bidTypeNative, openrtb_ext.BidTypeVideo) + } +} diff --git a/adapters/adot/adottest/exemplary/simple-banner.json b/adapters/adot/adottest/exemplary/simple-banner.json new file mode 100644 index 00000000000..1b8cb9867f6 --- /dev/null +++ b/adapters/adot/adottest/exemplary/simple-banner.json @@ -0,0 +1,94 @@ +{ + "mockBidRequest": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-banner-id", + "seatbid": [{ + "seat": "adot", + "bid": [{ + "id": "test-request-banner-id", + "impid": "test-imp-banner-id", + "price": 1.16346, + "adm": "some-test-ad", + "w": 320, + "h": 50, + "ext": { + "adot": { + "media_type": "banner" + } + } + }] + } + ], + "cur": "USD" + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "test-request-banner-id", + "impid": "test-imp-banner-id", + "price": 1.16346, + "adm": "some-test-ad", + "w": 320, + "h": 50, + "ext": { + "adot": { + "media_type": "banner" + } + } + }, + "type": "banner" + }] + }] +} + diff --git a/adapters/adot/adottest/exemplary/simple-interstitial.json b/adapters/adot/adottest/exemplary/simple-interstitial.json new file mode 100644 index 00000000000..0e9b573a530 --- /dev/null +++ b/adapters/adot/adottest/exemplary/simple-interstitial.json @@ -0,0 +1,101 @@ +{ + "mockBidRequest": { + "id": "test-request-inter-id", + "imp": [ + { + "id": "test-imp-inter-id", + "banner": { + "format": [ + { + "w": 320, + "h": 480 + } + ], + "w": 320, + "h": 480 + }, + "instl":1, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-inter-id", + "imp": [ + { + "id": "test-imp-inter-id", + "banner": { + "format": [ + { + "w": 320, + "h": 480 + } + ], + "w": 320, + "h": 480 + }, + "instl":1, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "adot", + "bid": [{ + "id": "test-request-inter-id", + "impid": "test-imp-inter-id", + "adm": "some-test-ad", + "price": 1.16346, + "w": 320, + "h": 480, + "ext": { + "adot": { + "media_type": "banner" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "test-request-inter-id", + "impid": "test-imp-inter-id", + "price": 1.16346, + "adm": "some-test-ad", + "w": 320, + "h": 480, + "ext": { + "adot": { + "media_type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} + diff --git a/adapters/adot/adottest/exemplary/simple-native.json b/adapters/adot/adottest/exemplary/simple-native.json new file mode 100644 index 00000000000..5fa3c70fd73 --- /dev/null +++ b/adapters/adot/adottest/exemplary/simple-native.json @@ -0,0 +1,88 @@ +{ + "mockBidRequest": { + "id": "test-request-native-id", + "imp": [ + { + "id": "test-imp-native-id", + "native": { + "request": "test-native", + "ver": "1.1" + }, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-native-id", + "imp": [ + { + "id": "test-imp-native-id", + "native": { + "request": "test-native", + "ver": "1.1" + }, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "adot", + "bid": [{ + "id": "test-request-native-id", + "impid": "test-imp-native-id", + "price": 1.16346, + "adm" : "native-ad", + "w": 300, + "h": 250, + "ext": { + "adot": { + "media_type": "native" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test-request-native-id", + "impid": "test-imp-native-id", + "price": 1.16346, + "adm" : "native-ad", + "w": 300, + "h": 250, + "ext": { + "adot": { + "media_type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} + diff --git a/adapters/adot/adottest/exemplary/simple-video.json b/adapters/adot/adottest/exemplary/simple-video.json new file mode 100644 index 00000000000..a453c4b9e18 --- /dev/null +++ b/adapters/adot/adottest/exemplary/simple-video.json @@ -0,0 +1,120 @@ +{ + "mockBidRequest": { + "id": "test-request-video-id", + "imp": [ + { + "id": "test-imp-video-id", + "video": { + "w": 300, + "h": 250, + "maxduration": 60, + "minduration": 1, + "api": [ + 1, + 2, + 5, + 6, + 7 + ], + "mimes": [ + "video\/mp4" + ], + "placement": 4, + "protocols": [ + 2 + ] + }, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-video-id", + "imp": [ + { + "id": "test-imp-video-id", + "video": { + "w": 300, + "h": 250, + "maxduration": 60, + "minduration": 1, + "api": [ + 1, + 2, + 5, + 6, + 7 + ], + "mimes": [ + "video\/mp4" + ], + "placement": 4, + "protocols": [ + 2 + ] + }, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "adot", + "bid": [{ + "id": "test-request-video-id", + "impid": "test-imp-video-id", + "price": 1.16346, + "adm": "some-test-ad", + "w": 300, + "h": 250, + "ext": { + "adot": { + "media_type": "video" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test-request-video-id", + "impid": "test-imp-video-id", + "price": 1.16346, + "adm": "some-test-ad", + "w": 300, + "h": 250, + "ext": { + "adot": { + "media_type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} + diff --git a/adapters/adot/adottest/params/race/banner.json b/adapters/adot/adottest/params/race/banner.json new file mode 100644 index 00000000000..ada77aa4440 --- /dev/null +++ b/adapters/adot/adottest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "placementId": "ee234aac-113" +} \ No newline at end of file diff --git a/adapters/adot/adottest/params/race/video.json b/adapters/adot/adottest/params/race/video.json new file mode 100644 index 00000000000..37808cd2ddc --- /dev/null +++ b/adapters/adot/adottest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "placementId": "ee234aac-112" +} \ No newline at end of file diff --git a/adapters/adot/adottest/supplemental/simple-audio.json b/adapters/adot/adottest/supplemental/simple-audio.json new file mode 100644 index 00000000000..9be53c4cee9 --- /dev/null +++ b/adapters/adot/adottest/supplemental/simple-audio.json @@ -0,0 +1,66 @@ +{ + "mockBidRequest": { + "id": "unsupported-audio-request", + "imp": [ + { + "id": "unsupported-audio-imp", + "audio": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "unsupported-audio-request", + "imp": [ + { + "id": "unsupported-audio-imp", + "audio": { + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "adot", + "bid": [{ + "id": "test-request-audio-id", + "impid": "test-imp-audio-id", + "price": 1.16346, + "adm": "some-audio-ad", + "ext": { + "adot": { + "media_type": "audio" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [] +} diff --git a/adapters/adot/adottest/supplemental/simple-parallax.json b/adapters/adot/adottest/supplemental/simple-parallax.json new file mode 100644 index 00000000000..4ee2ebc22d0 --- /dev/null +++ b/adapters/adot/adottest/supplemental/simple-parallax.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "test-request-parallax-id", + "imp": [ + { + "id": "test-imp-parallax-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 600 + }, + "ext": { + "adot": { + "parallax": true + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-parallax-id", + "imp": [ + { + "id": "test-imp-parallax-id", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 600 + }, + "ext": { + "adot": { + "parallax": true + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "adot", + "bid": [{ + "id": "test-request-parallax-id", + "impid": "test-imp-parallax-id", + "price": 0.5, + "adm": "some-test-ad", + "w": 300, + "h": 600, + "ext": { + "adot": { + "media_type": "banner" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test-request-parallax-id", + "impid": "test-imp-parallax-id", + "price": 0.5, + "adm": "some-test-ad", + "w": 300, + "h": 600, + "ext": { + "adot": { + "media_type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/adot/adottest/supplemental/status_204.json b/adapters/adot/adottest/supplemental/status_204.json new file mode 100644 index 00000000000..44a895b5c24 --- /dev/null +++ b/adapters/adot/adottest/supplemental/status_204.json @@ -0,0 +1,39 @@ +{ + "mockBidRequest": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": {}, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": {}, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} + diff --git a/adapters/adot/adottest/supplemental/status_400.json b/adapters/adot/adottest/supplemental/status_400.json new file mode 100644 index 00000000000..22328fd9908 --- /dev/null +++ b/adapters/adot/adottest/supplemental/status_400.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 400, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} + diff --git a/adapters/adot/adottest/supplemental/status_500.json b/adapters/adot/adottest/supplemental/status_500.json new file mode 100644 index 00000000000..879bb8c5581 --- /dev/null +++ b/adapters/adot/adottest/supplemental/status_500.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 500, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} + diff --git a/adapters/adot/adottest/supplemental/unmarshal_error.json b/adapters/adot/adottest/supplemental/unmarshal_error.json new file mode 100644 index 00000000000..a87e1189a62 --- /dev/null +++ b/adapters/adot/adottest/supplemental/unmarshal_error.json @@ -0,0 +1,62 @@ +{ + "mockBidRequest": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://dsp.adotmob.com/headerbidding/bidrequest", + "body": { + "id": "test-request-banner-id", + "imp": [ + { + "id": "test-imp-banner-id", + "banner": { + "format": [ + { + "w": 320, + "h": 250 + } + ], + "w": 320, + "h": 250 + }, + "ext": { + "adot": {} + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": "fail for unmarshal" + } + }], + + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse", + "comparison": "literal" + } + ] +} + diff --git a/adapters/adot/params_test.go b/adapters/adot/params_test.go new file mode 100644 index 00000000000..2f7b4b9af4e --- /dev/null +++ b/adapters/adot/params_test.go @@ -0,0 +1,53 @@ +package adot + +import ( + "encoding/json" + "github.com/prebid/prebid-server/openrtb_ext" + "testing" +) + +// This file actually intends to test static/bidder-params/adot.json +// +// These also validate the format of the external API: request.imp[i].ext.adot + +// TestValidParams makes sure that the adot 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.BidderAdot, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected adot 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.BidderAdot, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{}`, + `{"placementId": "test-114"}`, + `{"placementId": "test-113", "parallax": true}`, + `{"placementId": "test-113", "parallax": false}`, +} + +var invalidParams = []string{ + `{"parallax": 1}`, + `{"placementId": 135123}`, + `{"placementId": 113, "parallax": 1}`, + `{"placementId": 142, "parallax": true}`, + `{"placementId": "test-114", "parallax": 1}`, +} diff --git a/config/config.go b/config/config.go index f720d84106a..4d8f6ab025b 100755 --- a/config/config.go +++ b/config/config.go @@ -799,6 +799,7 @@ func SetupViper(v *viper.Viper, filename string) { 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://{{.AccountID}}.trustedmarketplace.io/ads/processHeaderBid/{{.AdUnit}}") + v.SetDefault("adapters.adot.endpoint", "https://dsp.adotmob.com/headerbidding/bidrequest") v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server") v.SetDefault("adapters.adprime.endpoint", "http://delta.adprime.com/?c=o&m=ortb") v.SetDefault("adapters.adtarget.endpoint", "http://ghb.console.adtarget.com.tr/pbs/ortb") diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 9095ec1e76e..97f8adfeb97 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -13,6 +13,7 @@ import ( "github.com/prebid/prebid-server/adapters/admixer" "github.com/prebid/prebid-server/adapters/adocean" "github.com/prebid/prebid-server/adapters/adoppler" + "github.com/prebid/prebid-server/adapters/adot" "github.com/prebid/prebid-server/adapters/adpone" "github.com/prebid/prebid-server/adapters/adprime" "github.com/prebid/prebid-server/adapters/adtarget" @@ -112,6 +113,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdOcean: adocean.Builder, openrtb_ext.BidderAdoppler: adoppler.Builder, openrtb_ext.BidderAdpone: adpone.Builder, + openrtb_ext.BidderAdot: adot.Builder, openrtb_ext.BidderAdprime: adprime.Builder, openrtb_ext.BidderAdtarget: adtarget.Builder, openrtb_ext.BidderAdtelligent: adtelligent.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index f7f8312508f..80a4510867a 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -51,6 +51,7 @@ const ( BidderAdmixer BidderName = "admixer" BidderAdOcean BidderName = "adocean" BidderAdoppler BidderName = "adoppler" + BidderAdot BidderName = "adot" BidderAdpone BidderName = "adpone" BidderAdprime BidderName = "adprime" BidderAdtarget BidderName = "adtarget" @@ -148,6 +149,7 @@ func CoreBidderNames() []BidderName { BidderAdmixer, BidderAdOcean, BidderAdoppler, + BidderAdot, BidderAdpone, BidderAdprime, BidderAdtarget, diff --git a/openrtb_ext/imp_adot.go b/openrtb_ext/imp_adot.go new file mode 100644 index 00000000000..5d741589896 --- /dev/null +++ b/openrtb_ext/imp_adot.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpAdot struct { + Parallax bool `json:"parallax,omitempty"` + PlacementId string `json:"placementId,omitempty"` +} diff --git a/static/bidder-info/adot.yaml b/static/bidder-info/adot.yaml new file mode 100644 index 00000000000..529ff3ae4cb --- /dev/null +++ b/static/bidder-info/adot.yaml @@ -0,0 +1,14 @@ +maintainer: + email: "admin@we-are-adot.com" +modifyingVastXmlAllowed: false +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native diff --git a/static/bidder-params/adot.json b/static/bidder-params/adot.json new file mode 100644 index 00000000000..fa2b85333e2 --- /dev/null +++ b/static/bidder-params/adot.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "The Adot Adapter Params", + "description": "A schema which validates params accepted by Adot adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "An ID which identifies this placement of the impression" + }, + "parallax": { + "type": "boolean", + "description": "It determines if the wanted advertising format is a parallax." + } + }, + "required": [] +} \ No newline at end of file diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 60ab2478a93..ec8920ee80c 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -97,6 +97,7 @@ func TestNewSyncerMap(t *testing.T) { openrtb_ext.BidderAdgeneration: true, openrtb_ext.BidderAdhese: true, openrtb_ext.BidderAdoppler: true, + openrtb_ext.BidderAdot: true, openrtb_ext.BidderApplogy: true, openrtb_ext.BidderInMobi: true, openrtb_ext.BidderKidoz: true, From f3fbc8c0d3de7ac7f53566a8ba236c7511471035 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 11 Jan 2021 21:37:16 -0500 Subject: [PATCH 335/381] Refactor AMP Param Parsing (#1627) * Refactor AMP Param Parsing * Added Tests --- amp/parse.go | 110 ++++++++++++++++ amp/parse_test.go | 168 +++++++++++++++++++++++++ endpoints/openrtb2/amp_auction.go | 150 +++++++--------------- endpoints/openrtb2/amp_auction_test.go | 18 ++- 4 files changed, 331 insertions(+), 115 deletions(-) create mode 100644 amp/parse.go create mode 100644 amp/parse_test.go diff --git a/amp/parse.go b/amp/parse.go new file mode 100644 index 00000000000..9e0e019f953 --- /dev/null +++ b/amp/parse.go @@ -0,0 +1,110 @@ +package amp + +import ( + "errors" + "net/http" + "strconv" + "strings" + + "github.com/mxmCherry/openrtb" +) + +// Params defines the paramters of an AMP request. +type Params struct { + Account string + CanonicalURL string + Consent string + Debug bool + Origin string + Size Size + Slot string + StoredRequestID string + Timeout *uint64 +} + +// Size defines size information of an AMP request. +type Size struct { + Height uint64 + Multisize []openrtb.Format + OverrideHeight uint64 + OverrideWidth uint64 + Width uint64 +} + +// ParseParams parses the AMP paramters from a HTTP request. +func ParseParams(httpRequest *http.Request) (Params, error) { + query := httpRequest.URL.Query() + + tagID := query.Get("tag_id") + if len(tagID) == 0 { + return Params{}, errors.New("AMP requests require an AMP tag_id") + } + + params := Params{ + Account: query.Get("account"), + CanonicalURL: query.Get("curl"), + Consent: chooseConsent(query.Get("consent_string"), query.Get("gdpr_consent")), + Debug: query.Get("debug") == "1", + Origin: query.Get("__amp_source_origin"), + Size: Size{ + Height: parseInt(query.Get("h")), + Multisize: parseMultisize(query.Get("ms")), + OverrideHeight: parseInt(query.Get("oh")), + OverrideWidth: parseInt(query.Get("ow")), + Width: parseInt(query.Get("w")), + }, + Slot: query.Get("slot"), + StoredRequestID: tagID, + Timeout: parseIntPtr(query.Get("timeout")), + } + return params, nil +} + +func parseIntPtr(value string) *uint64 { + if parsed, err := strconv.ParseUint(value, 10, 64); err == nil { + return &parsed + } + return nil +} + +func parseInt(value string) uint64 { + if parsed, err := strconv.ParseUint(value, 10, 64); err == nil { + return parsed + } + return 0 +} + +func parseMultisize(multisize string) []openrtb.Format { + if multisize == "" { + return nil + } + + sizeStrings := strings.Split(multisize, ",") + sizes := make([]openrtb.Format, 0, len(sizeStrings)) + for _, sizeString := range sizeStrings { + wh := strings.Split(sizeString, "x") + if len(wh) != 2 { + return nil + } + f := openrtb.Format{ + W: parseInt(wh[0]), + H: parseInt(wh[1]), + } + if f.W == 0 && f.H == 0 { + return nil + } + + sizes = append(sizes, f) + } + return sizes +} + +func chooseConsent(consent, gdprConsent string) string { + if len(consent) > 0 { + return consent + } + + // Fallback to 'gdpr_consent' for compatability until it's no longer used. This was our original + // implementation before the same AMP macro was reused for CCPA. + return gdprConsent +} diff --git a/amp/parse_test.go b/amp/parse_test.go new file mode 100644 index 00000000000..e2c82d71030 --- /dev/null +++ b/amp/parse_test.go @@ -0,0 +1,168 @@ +package amp + +import ( + "net/http" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/stretchr/testify/assert" +) + +func TestParseParams(t *testing.T) { + var expectedTimeout uint64 = 42 + + testCases := []struct { + description string + query string + expectedParams Params + expectedError string + }{ + { + description: "Empty", + query: "", + expectedError: "AMP requests require an AMP tag_id", + }, + { + description: "All Fields", + query: "tag_id=anyTagID&account=anyAccount&curl=anyCurl&consent_string=anyConsent&debug=1&__amp_source_origin=anyOrigin" + + "&slot=anySlot&timeout=42&h=1&w=2&oh=3&ow=4&ms=10x11,12x13", + expectedParams: Params{ + Account: "anyAccount", + CanonicalURL: "anyCurl", + Consent: "anyConsent", + Debug: true, + Origin: "anyOrigin", + Slot: "anySlot", + StoredRequestID: "anyTagID", + Timeout: &expectedTimeout, + Size: Size{ + Height: 1, + OverrideHeight: 3, + OverrideWidth: 4, + Width: 2, + Multisize: []openrtb.Format{ + {W: 10, H: 11}, {W: 12, H: 13}, + }, + }, + }, + }, + { + description: "Integer Values Ignored If Invalid", + query: "tag_id=anyTagID&h=invalid&w=invalid&oh=invalid&ow=invalid&ms=invalid", + expectedParams: Params{StoredRequestID: "anyTagID"}, + }, + { + description: "consent_string Preferred Over gdpr_consent", + query: "tag_id=anyTagID&consent_string=consent1&gdpr_consent=consent2", + expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent1"}, + }, + { + description: "consent_string Preferred Over gdpr_consent - Order Doesn't Matter", + query: "tag_id=anyTagID&gdpr_consent=consent2&consent_string=consent1", + expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent1"}, + }, + { + description: "Just gdpr_consent", + query: "tag_id=anyTagID&gdpr_consent=consent2", + expectedParams: Params{StoredRequestID: "anyTagID", Consent: "consent2"}, + }, + { + description: "Debug 0", + query: "tag_id=anyTagID&debug=0", + expectedParams: Params{StoredRequestID: "anyTagID", Debug: false}, + }, + { + description: "Debug Ignored If Invalid", + query: "tag_id=anyTagID&debug=invalid", + expectedParams: Params{StoredRequestID: "anyTagID", Debug: false}, + }, + } + + for _, test := range testCases { + httpRequest, err := http.NewRequest("GET", "http://any.url/anypage?"+test.query, nil) + assert.NoError(t, err, test.description+":request") + + params, err := ParseParams(httpRequest) + assert.Equal(t, test.expectedParams, params, test.description+":params") + if test.expectedError == "" { + assert.NoError(t, err, test.description+":err") + } else { + assert.EqualError(t, err, test.expectedError) + } + } +} + +func TestParseMultisize(t *testing.T) { + testCases := []struct { + description string + multisize string + expectedFormats []openrtb.Format + }{ + { + description: "Empty", + multisize: "", + expectedFormats: nil, + }, + { + description: "One", + multisize: "1x2", + expectedFormats: []openrtb.Format{{W: 1, H: 2}}, + }, + { + description: "Many", + multisize: "1x2,3x4", + expectedFormats: []openrtb.Format{{W: 1, H: 2}, {W: 3, H: 4}}, + }, + { + // Existing Behavior: The " 3" token in the second size is parsed as 0. + description: "Many With Space - Quirky Result", + multisize: "1x2, 3x4", + expectedFormats: []openrtb.Format{{W: 1, H: 2}, {W: 0, H: 4}}, + }, + { + description: "One - Zero Size - Ignored", + multisize: "0x0", + expectedFormats: nil, + }, + { + description: "Many - Zero Size - All Ignored", + multisize: "0x0,3x4", + expectedFormats: nil, + }, + { + description: "One - Extra Dimension - Ignored", + multisize: "1x2x3", + expectedFormats: nil, + }, + { + description: "Many - Extra Dimension - All Ignored", + multisize: "1x2x3,4x5", + expectedFormats: nil, + }, + { + description: "One - Invalid Values - Ignored", + multisize: "INVALIDxINVALID", + expectedFormats: nil, + }, + { + description: "Many - Invalid Values - All Ignored", + multisize: "1x2,INVALIDxINVALID", + expectedFormats: nil, + }, + { + description: "One - No Pair - Ignored", + multisize: "INVALID", + expectedFormats: nil, + }, + { + description: "Many - No Pair - All Ignored", + multisize: "1x2,INVALID", + expectedFormats: nil, + }, + } + + for _, test := range testCases { + result := parseMultisize(test.multisize) + assert.ElementsMatch(t, test.expectedFormats, result, test.description) + } +} diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index afc32e5ea2b..dc66163a699 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "net/url" - "strconv" "strings" "time" @@ -17,6 +16,7 @@ import ( "github.com/julienschmidt/httprouter" "github.com/mxmCherry/openrtb" accountService "github.com/prebid/prebid-server/account" + "github.com/prebid/prebid-server/amp" "github.com/prebid/prebid-server/analytics" "github.com/prebid/prebid-server/config" "github.com/prebid/prebid-server/errortypes" @@ -314,43 +314,41 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req req = &openrtb.BidRequest{} errs = nil - ampID := httpRequest.FormValue("tag_id") - if ampID == "" { - errs = []error{errors.New("AMP requests require an AMP tag_id")} - return + ampParams, err := amp.ParseParams(httpRequest) + if err != nil { + return nil, []error{err} } ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond) defer cancel() - storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampID}, nil) + storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampParams.StoredRequestID}, nil) if len(errs) > 0 { return nil, errs } if len(storedRequests) == 0 { - errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampID)} + errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampParams.StoredRequestID)} return } // The fetched config becomes the entire OpenRTB request - requestJSON := storedRequests[ampID] + requestJSON := storedRequests[ampParams.StoredRequestID] if err := json.Unmarshal(requestJSON, req); err != nil { errs = []error{err} return } - debugParam := httpRequest.FormValue("debug") - if debugParam == "1" { + if ampParams.Debug { req.Test = 1 } // Two checks so users know which way the Imp check failed. if len(req.Imp) == 0 { - errs = []error{fmt.Errorf("data for tag_id='%s' does not define the required imp array", ampID)} + errs = []error{fmt.Errorf("data for tag_id='%s' does not define the required imp array", ampParams.StoredRequestID)} return } if len(req.Imp) > 1 { - errs = []error{fmt.Errorf("data for tag_id '%s' includes %d imp elements. Only one is allowed", ampID, len(req.Imp))} + errs = []error{fmt.Errorf("data for tag_id '%s' includes %d imp elements. Only one is allowed", ampParams.StoredRequestID, len(req.Imp))} return } @@ -367,35 +365,30 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req *req.Imp[0].Secure = 1 } - errs = deps.overrideWithParams(httpRequest, req) + errs = deps.overrideWithParams(ampParams, req) return } -func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *openrtb.BidRequest) []error { +func (deps *endpointDeps) overrideWithParams(ampParams amp.Params, 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) - height := parseFormInt(httpRequest, "h", 0) - overrideWidth := parseFormInt(httpRequest, "ow", 0) - overrideHeight := parseFormInt(httpRequest, "oh", 0) - if format := makeFormatReplacement(overrideWidth, overrideHeight, width, height, httpRequest.FormValue("ms")); len(format) != 0 { + if format := makeFormatReplacement(ampParams.Size); len(format) != 0 { req.Imp[0].Banner.Format = format - } else if width != 0 { - setWidths(req.Imp[0].Banner.Format, width) - } else if height != 0 { - setHeights(req.Imp[0].Banner.Format, height) + } else if ampParams.Size.Width != 0 { + setWidths(req.Imp[0].Banner.Format, ampParams.Size.Width) + } else if ampParams.Size.Height != 0 { + setHeights(req.Imp[0].Banner.Format, ampParams.Size.Height) } } - canonicalURL := httpRequest.FormValue("curl") - if canonicalURL != "" { - req.Site.Page = canonicalURL + if ampParams.CanonicalURL != "" { + req.Site.Page = ampParams.CanonicalURL // Fixes #683 - if parsedURL, err := url.Parse(canonicalURL); err == nil { + if parsedURL, err := url.Parse(ampParams.CanonicalURL); err == nil { domain := parsedURL.Host if colonIndex := strings.LastIndex(domain, ":"); colonIndex != -1 { domain = domain[:colonIndex] @@ -406,14 +399,13 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope setAmpExt(req.Site, "1") - setEffectiveAmpPubID(req, httpRequest.URL.Query()) + setEffectiveAmpPubID(req, ampParams.Account) - slot := httpRequest.FormValue("slot") - if slot != "" { - req.Imp[0].TagID = slot + if ampParams.Slot != "" { + req.Imp[0].TagID = ampParams.Slot } - policyWriter, policyWriterErr := readPolicyFromUrl(httpRequest.URL) + policyWriter, policyWriterErr := readPolicy(ampParams.Consent) if policyWriterErr != nil { return []error{policyWriterErr} } @@ -421,42 +413,38 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope return []error{err} } - if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil { - req.TMax = timeout - deps.cfg.AMPTimeoutAdjustment + if ampParams.Timeout != nil { + req.TMax = int64(*ampParams.Timeout) - deps.cfg.AMPTimeoutAdjustment } return nil } -func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format { +func makeFormatReplacement(size amp.Size) []openrtb.Format { var formats []openrtb.Format - if overrideWidth != 0 && overrideHeight != 0 { + if size.OverrideWidth != 0 && size.OverrideHeight != 0 { formats = []openrtb.Format{{ - W: overrideWidth, - H: overrideHeight, + W: size.OverrideWidth, + H: size.OverrideHeight, }} - } else if overrideWidth != 0 && height != 0 { + } else if size.OverrideWidth != 0 && size.Height != 0 { formats = []openrtb.Format{{ - W: overrideWidth, - H: height, + W: size.OverrideWidth, + H: size.Height, }} - } else if width != 0 && overrideHeight != 0 { + } else if size.Width != 0 && size.OverrideHeight != 0 { formats = []openrtb.Format{{ - W: width, - H: overrideHeight, + W: size.Width, + H: size.OverrideHeight, }} - } else if width != 0 && height != 0 { + } else if size.Width != 0 && size.Height != 0 { formats = []openrtb.Format{{ - W: width, - H: height, + W: size.Width, + H: size.Height, }} } - if parsedSizes := parseMultisize(multisize); len(parsedSizes) != 0 { - formats = append(formats, parsedSizes...) - } - - return formats + return append(formats, size.Multisize...) } func setWidths(formats []openrtb.Format, width uint64) { @@ -471,42 +459,6 @@ func setHeights(formats []openrtb.Format, height uint64) { } } -func parseMultisize(multisize string) []openrtb.Format { - if multisize == "" { - return nil - } - - sizeStrings := strings.Split(multisize, ",") - sizes := make([]openrtb.Format, 0, len(sizeStrings)) - for _, sizeString := range sizeStrings { - wh := strings.Split(sizeString, "x") - if len(wh) != 2 { - return nil - } - f := openrtb.Format{ - W: parseIntErrorless(wh[0], 0), - H: parseIntErrorless(wh[1], 0), - } - if f.W == 0 && f.H == 0 { - return nil - } - - sizes = append(sizes, f) - } - return sizes -} - -func parseFormInt(req *http.Request, value string, defaultTo uint64) uint64 { - return parseIntErrorless(req.FormValue(value), defaultTo) -} - -func parseIntErrorless(value string, defaultTo uint64) uint64 { - if parsed, err := strconv.ParseUint(value, 10, 64); err == nil { - return parsed - } - return defaultTo -} - // AMP won't function unless ext.prebid.targeting and ext.prebid.cache.bids are defined. // If the user didn't include them, default those here. func defaultRequestExt(req *openrtb.BidRequest) (errs []error) { @@ -563,9 +515,7 @@ func setAmpExt(site *openrtb.Site, value string) { } } -func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) { - consent := readConsentFromURL(url) - +func readPolicy(consent string) (privacy.PolicyWriter, error) { if len(consent) == 0 { return privacy.NilPolicyWriter{}, nil } @@ -583,17 +533,8 @@ func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) { } } -func readConsentFromURL(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") -} - // Sets the effective publisher ID for amp request -func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) { +func setEffectiveAmpPubID(req *openrtb.BidRequest, account string) { var pub *openrtb.Publisher if req.App != nil { if req.App.Publisher == nil { @@ -608,10 +549,9 @@ func setEffectiveAmpPubID(req *openrtb.BidRequest, urlQueryParams url.Values) { } if pub.ID == "" { - // For amp requests, the publisher ID could be sent via the account - // query string - if acc := urlQueryParams.Get("account"); acc != "" && acc != "ACCOUNT_ID" { - pub.ID = acc + // ACCOUNT_ID is the unresolved macro name and should be ignored. + if account != "" && account != "ACCOUNT_ID" { + pub.ID = account } } } diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 746d436d711..97e99c6bd01 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -1031,14 +1031,12 @@ func getTestBidRequest(nilUser bool, userExt *openrtb_ext.ExtUser, nilRegs bool, func TestSetEffectiveAmpPubID(t *testing.T) { testPubID := "test-pub" - testURLQueryParams := url.Values{} - testURLQueryParams.Add("account", testPubID) testCases := []struct { - description string - req *openrtb.BidRequest - urlQueryParams url.Values - expectedPubID string + description string + req *openrtb.BidRequest + account string + expectedPubID string }{ { description: "No publisher ID provided", @@ -1072,7 +1070,7 @@ func TestSetEffectiveAmpPubID(t *testing.T) { expectedPubID: testPubID, }, { - description: "Publisher ID present in account query parameter", + description: "Publisher ID present in account parameter", req: &openrtb.BidRequest{ App: &openrtb.App{ Publisher: &openrtb.Publisher{ @@ -1080,8 +1078,8 @@ func TestSetEffectiveAmpPubID(t *testing.T) { }, }, }, - urlQueryParams: testURLQueryParams, - expectedPubID: testPubID, + account: testPubID, + expectedPubID: testPubID, }, { description: "req.Site.Publisher present but ID set to empty string", @@ -1097,7 +1095,7 @@ func TestSetEffectiveAmpPubID(t *testing.T) { } for _, test := range testCases { - setEffectiveAmpPubID(test.req, test.urlQueryParams) + setEffectiveAmpPubID(test.req, test.account) if test.req.Site != nil { assert.Equal(t, test.expectedPubID, test.req.Site.Publisher.ID, "should return the expected Publisher ID for test case: %s", test.description) From b111b6d4c2e41a8fc98a77e56fd79e0a4f2ce1ea Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 12 Jan 2021 10:19:17 -0500 Subject: [PATCH 336/381] Enforce GDPR privacy if there's an error parsing consent (#1593) * Enforce GDPR privacy if there's an error parsing consent * Update test with consent string variables to improve readability * Fix test typo * Update test variable names to follow go conventions --- exchange/utils.go | 12 +++++++----- exchange/utils_test.go | 41 +++++++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/exchange/utils.go b/exchange/utils.go index 21c31a290b4..d574c2a6452 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -118,11 +118,13 @@ func cleanOpenRTBRequests(ctx context.Context, if gdprEnforced { var publisherID = req.LegacyLabels.PubID _, geo, id, err := gDPR.PersonalInfoAllowed(ctx, bidderRequest.BidderCoreName, publisherID, gdprSignal, consent) - privacyEnforcement.GDPRGeo = !geo && err == nil - privacyEnforcement.GDPRID = !id && err == nil - } else { - privacyEnforcement.GDPRGeo = false - privacyEnforcement.GDPRID = false + if err == nil { + privacyEnforcement.GDPRGeo = !geo + privacyEnforcement.GDPRID = !id + } else { + privacyEnforcement.GDPRGeo = true + privacyEnforcement.GDPRID = true + } } privacyEnforcement.Apply(bidderRequest.BidRequest) diff --git a/exchange/utils_test.go b/exchange/utils_test.go index f9a04f25e40..e13f956b46d 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -20,7 +20,8 @@ import ( // // It only allows appnexus for GDPR consent type permissionsMock struct { - personalInfoAllowed bool + personalInfoAllowed bool + personalInfoAllowedError error } func (p *permissionsMock) HostCookiesAllowed(ctx context.Context, consent string) (bool, error) { @@ -32,7 +33,7 @@ func (p *permissionsMock) BidderSyncAllowed(ctx context.Context, bidder openrtb_ } func (p *permissionsMock) PersonalInfoAllowed(ctx context.Context, bidder openrtb_ext.BidderName, PublisherID string, gdpr gdpr.Signal, consent string) (bool, bool, bool, error) { - return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, nil + return p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowed, p.personalInfoAllowedError } func assertReq(t *testing.T, bidderRequests []BidderRequest, @@ -1045,6 +1046,8 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { } func TestCleanOpenRTBRequestsGDPR(t *testing.T) { + tcf1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY" + tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA" trueValue, falseValue := true, false testCases := []struct { @@ -1054,6 +1057,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr string gdprConsent string gdprScrub bool + permissionsError error userSyncIfAmbiguous bool expectPrivacyLabels metrics.PrivacyLabels }{ @@ -1074,7 +1078,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: &trueValue, gdprHostEnabled: true, gdpr: "1", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, @@ -1086,7 +1090,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: &trueValue, gdprHostEnabled: true, gdpr: "1", - gdprConsent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", + gdprConsent: tcf2Consent, gdprScrub: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, @@ -1098,7 +1102,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: &trueValue, gdprHostEnabled: true, gdpr: "0", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, @@ -1110,7 +1114,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: &trueValue, gdprHostEnabled: false, gdpr: "1", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, @@ -1122,7 +1126,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: &falseValue, gdprHostEnabled: true, gdpr: "1", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, @@ -1134,7 +1138,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: nil, gdprHostEnabled: true, gdpr: "1", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, @@ -1146,7 +1150,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: nil, gdprHostEnabled: false, gdpr: "1", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, @@ -1158,7 +1162,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: nil, gdprHostEnabled: true, gdpr: "null", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: true, userSyncIfAmbiguous: false, expectPrivacyLabels: metrics.PrivacyLabels{ @@ -1171,7 +1175,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprAccountEnabled: nil, gdprHostEnabled: true, gdpr: "null", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf1Consent, gdprScrub: false, userSyncIfAmbiguous: true, expectPrivacyLabels: metrics.PrivacyLabels{ @@ -1179,6 +1183,19 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { GDPRTCFVersion: "", }, }, + { + description: "Enforce - error while checking if personal info is allowed", + gdprAccountEnabled: nil, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: tcf1Consent, + gdprScrub: true, + permissionsError: errors.New("Some error"), + expectPrivacyLabels: metrics.PrivacyLabels{ + GDPREnforced: true, + GDPRTCFVersion: metrics.TCFVersionV1, + }, + }, } for _, test := range testCases { @@ -1214,7 +1231,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { context.Background(), auctionReq, nil, - &permissionsMock{personalInfoAllowed: !test.gdprScrub}, + &permissionsMock{personalInfoAllowed: !test.gdprScrub, personalInfoAllowedError: test.permissionsError}, test.userSyncIfAmbiguous, privacyConfig) result := results[0] From 2336b64f13bd84e7adbb01e462d8a7599facff75 Mon Sep 17 00:00:00 2001 From: Gena Date: Tue, 12 Jan 2021 20:22:04 +0200 Subject: [PATCH 337/381] MediaFuse adapter (#1635) * MediaFuse alias * Syncer and tests * gvlid * gvlid * new mail --- adapters/mediafuse/usersync.go | 12 ++++++++++++ adapters/mediafuse/usersync_test.go | 30 +++++++++++++++++++++++++++++ config/config.go | 2 ++ exchange/adapter_builders.go | 1 + openrtb_ext/bidders.go | 2 ++ static/bidder-info/mediafuse.yaml | 11 +++++++++++ static/bidder-params/mediafuse.json | 26 +++++++++++++++++++++++++ usersync/usersyncers/syncer.go | 2 ++ usersync/usersyncers/syncer_test.go | 1 + 9 files changed, 87 insertions(+) create mode 100644 adapters/mediafuse/usersync.go create mode 100644 adapters/mediafuse/usersync_test.go create mode 100644 static/bidder-info/mediafuse.yaml create mode 100644 static/bidder-params/mediafuse.json diff --git a/adapters/mediafuse/usersync.go b/adapters/mediafuse/usersync.go new file mode 100644 index 00000000000..5381bf3606d --- /dev/null +++ b/adapters/mediafuse/usersync.go @@ -0,0 +1,12 @@ +package mediafuse + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewMediafuseSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("mediafuse", 411, temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/mediafuse/usersync_test.go b/adapters/mediafuse/usersync_test.go new file mode 100644 index 00000000000..30b5b535b12 --- /dev/null +++ b/adapters/mediafuse/usersync_test.go @@ -0,0 +1,30 @@ +package mediafuse + +import ( + "testing" + "text/template" + + "github.com/prebid/prebid-server/privacy" + "github.com/prebid/prebid-server/privacy/gdpr" + "github.com/stretchr/testify/assert" +) + +func TestMediafuseSyncer(t *testing.T) { + syncURL := "//sync.hbmp.mediafuse.com/csync?t=p&ep=0&redir=localhost%2Fsetuid%3Fbidder%3Dmediafuse%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + + syncer := NewMediafuseSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "//sync.hbmp.mediafuse.com/csync?t=p&ep=0&redir=localhost%2Fsetuid%3Fbidder%3Dmediafuse%26gdpr%3D0%26gdpr_consent%3D%26uid%3D%7Buid%7D", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.EqualValues(t, 411, syncer.GDPRVendorID()) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 4d8f6ab025b..ed3c6e96622 100755 --- a/config/config.go +++ b/config/config.go @@ -606,6 +606,7 @@ func (cfg *Configuration) setDerivedDefaults() { setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLogicad, "https://cr-p31.ladsp.jp/cookiesender/31?r=true&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ru="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLunaMedia, "https://api.lunamedia.io/xp/user-sync?redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmarsmedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D") + setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMediafuse, "https://sync.hbmp.mediafuse.com/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmediafuse%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMgid, "https://cm.mgid.com/m?cdsp=363893&adu="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D") setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNanoInteractive, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%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="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dninthdecimal%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID") @@ -845,6 +846,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.logicad.endpoint", "https://pbs.ladsp.com/adrequest/prebidserver") 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.mediafuse.endpoint", "http://ghb.hbmp.mediafuse.com/pbs/ortb") v.SetDefault("adapters.mgid.endpoint", "https://prebid.mgid.com/prebid/") v.SetDefault("adapters.mobilefuse.endpoint", "http://mfx.mobilefuse.com/openrtb?pub_id={{.PublisherID}}") v.SetDefault("adapters.mobfoxpb.endpoint", "http://bes.mobfox.com/?c=o&m=ortb") diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 97f8adfeb97..5e95038151b 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -154,6 +154,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderLogicad: logicad.Builder, openrtb_ext.BidderLunaMedia: lunamedia.Builder, openrtb_ext.BidderMarsmedia: marsmedia.Builder, + openrtb_ext.BidderMediafuse: adtelligent.Builder, openrtb_ext.BidderMgid: mgid.Builder, openrtb_ext.BidderMobfoxpb: mobfoxpb.Builder, openrtb_ext.BidderMobileFuse: mobilefuse.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 80a4510867a..e412a6f940b 100755 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -94,6 +94,7 @@ const ( BidderLogicad BidderName = "logicad" BidderLunaMedia BidderName = "lunamedia" BidderMarsmedia BidderName = "marsmedia" + BidderMediafuse BidderName = "mediafuse" BidderMgid BidderName = "mgid" BidderMobfoxpb BidderName = "mobfoxpb" BidderMobileFuse BidderName = "mobilefuse" @@ -192,6 +193,7 @@ func CoreBidderNames() []BidderName { BidderLogicad, BidderLunaMedia, BidderMarsmedia, + BidderMediafuse, BidderMgid, BidderMobfoxpb, BidderMobileFuse, diff --git a/static/bidder-info/mediafuse.yaml b/static/bidder-info/mediafuse.yaml new file mode 100644 index 00000000000..112f67fe556 --- /dev/null +++ b/static/bidder-info/mediafuse.yaml @@ -0,0 +1,11 @@ +maintainer: + email: "support@mediafuse.com" +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video diff --git a/static/bidder-params/mediafuse.json b/static/bidder-params/mediafuse.json new file mode 100644 index 00000000000..4f38f89d299 --- /dev/null +++ b/static/bidder-params/mediafuse.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Mediafuse Adapter Params", + "description": "A schema which validates params accepted by the Mediafuse 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/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 84044e85089..c41f7c6c746 100755 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -50,6 +50,7 @@ import ( "github.com/prebid/prebid-server/adapters/logicad" "github.com/prebid/prebid-server/adapters/lunamedia" "github.com/prebid/prebid-server/adapters/marsmedia" + "github.com/prebid/prebid-server/adapters/mediafuse" "github.com/prebid/prebid-server/adapters/mgid" "github.com/prebid/prebid-server/adapters/nanointeractive" "github.com/prebid/prebid-server/adapters/ninthdecimal" @@ -136,6 +137,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderLogicad, logicad.NewLogicadSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderLunaMedia, lunamedia.NewLunaMediaSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderMarsmedia, marsmedia.NewMarsmediaSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderMediafuse, mediafuse.NewMediafuseSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderMgid, mgid.NewMgidSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderNanoInteractive, nanointeractive.NewNanoInteractiveSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderNinthDecimal, ninthdecimal.NewNinthDecimalSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index ec8920ee80c..b6acbb4d23c 100755 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -59,6 +59,7 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderLogicad): syncConfig, string(openrtb_ext.BidderLunaMedia): syncConfig, string(openrtb_ext.BidderMarsmedia): syncConfig, + string(openrtb_ext.BidderMediafuse): syncConfig, string(openrtb_ext.BidderMgid): syncConfig, string(openrtb_ext.BidderNanoInteractive): syncConfig, string(openrtb_ext.BidderNinthDecimal): syncConfig, From b6b64e7de1a416aab7840cf3e97dde2235fa12c1 Mon Sep 17 00:00:00 2001 From: jcamp-revc <68560678+jcamp-revc@users.noreply.github.com> Date: Tue, 12 Jan 2021 16:11:52 -0500 Subject: [PATCH 338/381] New Adapter: Revcontent (#1622) --- adapters/revcontent/revcontent.go | 105 ++++++++++++++++++ adapters/revcontent/revcontent_test.go | 21 ++++ .../revcontenttest/exemplary/no-bid.json | 45 ++++++++ .../exemplary/simple-banner.json | 85 ++++++++++++++ .../exemplary/simple-native.json | 76 +++++++++++++ .../supplemental/bad_response.json | 49 ++++++++ .../supplemental/missing_app_name.json | 23 ++++ .../supplemental/missing_domain.json | 23 ++++ .../supplemental/status_400.json | 49 ++++++++ .../supplemental/status_500.json | 49 ++++++++ config/config.go | 2 + exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + static/bidder-info/revcontent.yaml | 11 ++ static/bidder-params/revcontent.json | 9 ++ usersync/usersyncers/syncer_test.go | 3 +- 16 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 adapters/revcontent/revcontent.go create mode 100644 adapters/revcontent/revcontent_test.go create mode 100644 adapters/revcontent/revcontenttest/exemplary/no-bid.json create mode 100644 adapters/revcontent/revcontenttest/exemplary/simple-banner.json create mode 100644 adapters/revcontent/revcontenttest/exemplary/simple-native.json create mode 100644 adapters/revcontent/revcontenttest/supplemental/bad_response.json create mode 100644 adapters/revcontent/revcontenttest/supplemental/missing_app_name.json create mode 100644 adapters/revcontent/revcontenttest/supplemental/missing_domain.json create mode 100644 adapters/revcontent/revcontenttest/supplemental/status_400.json create mode 100644 adapters/revcontent/revcontenttest/supplemental/status_500.json create mode 100644 static/bidder-info/revcontent.yaml create mode 100644 static/bidder-params/revcontent.json diff --git a/adapters/revcontent/revcontent.go b/adapters/revcontent/revcontent.go new file mode 100644 index 00000000000..5a34f3ef199 --- /dev/null +++ b/adapters/revcontent/revcontent.go @@ -0,0 +1,105 @@ +package revcontent + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" + "net/http" +) + +type adapter struct { + endpoint string +} + +// Builder builds a new instance of the Revcontent adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + reqBody, err := json.Marshal(request) + + if err != nil { + return nil, []error{err} + } + + if err := checkRequest(request); err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + + req := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqBody, + Headers: headers, + } + return []*adapters.RequestData{req}, nil +} + +func checkRequest(request *openrtb.BidRequest) error { + if (request.App == nil || len(request.App.Name) == 0) && (request.Site == nil || len(request.Site.Domain) == 0) { + return &errortypes.BadInput{ + Message: "Impression is missing app name or site domain, and must contain one.", + } + } + + return nil +} + +// MakeBids make the bids for the bid response. +func (a *adapter) 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.", response.StatusCode), + }} + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("unexpected status code: %d.", response.StatusCode), + }} + } + + var bidResp openrtb.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + var mediaType = getBidType(sb.Bid[i].AdM) + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: mediaType, + }) + } + } + return bidResponse, nil + +} + +func getBidType(bidAdm string) openrtb_ext.BidType { + // native: {"ver":"1.1","assets":... + // banner: