From 11adffe40f9542a4325e3910c7f55750d9098196 Mon Sep 17 00:00:00 2001 From: egsk Date: Mon, 7 Jun 2021 20:46:11 +0300 Subject: [PATCH 01/16] Between: Fix for bid floor issue#1787 (#1870) Co-authored-by: Egor Skorokhodov --- adapters/between/between.go | 12 ------------ .../between/betweentest/exemplary/multi-request.json | 2 -- .../between/betweentest/exemplary/secure-detect.json | 1 - .../betweentest/exemplary/simple-site-banner.json | 10 ++-------- .../supplemental/bad-dsp-request-example.json | 1 - .../betweentest/supplemental/bad-response-body.json | 1 - .../dsp-server-internal-error-example.json | 1 - .../between/betweentest/supplemental/no-bids.json | 1 - adapters/between/params_test.go | 9 ++++----- openrtb_ext/imp_between.go | 6 ++---- static/bidder-params/between.json | 9 --------- 11 files changed, 8 insertions(+), 45 deletions(-) diff --git a/adapters/between/between.go b/adapters/between/between.go index 9d77a0413fd..f710855a4cd 100644 --- a/adapters/between/between.go +++ b/adapters/between/between.go @@ -20,10 +20,6 @@ type BetweenAdapter struct { EndpointTemplate template.Template } -// BetweenSSP requires bidfloor > 0. -// If BidFloor of openrtb_ext.ExtImpBetween is zero, set it to defaultBidFloor value, see addImpProps -const defaultBidfloor = 0.00001 - func (a *BetweenAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { var errors []error if len(request.Imp) == 0 { @@ -129,14 +125,6 @@ func buildImpBanner(imp *openrtb2.Imp) error { // Add Between required properties to Imp object func addImpProps(imp *openrtb2.Imp, secure *int8, betweenExt *openrtb_ext.ExtImpBetween) { imp.Secure = secure - if betweenExt.BidFloor <= 0 { - imp.BidFloor = defaultBidfloor - } else { - imp.BidFloor = betweenExt.BidFloor - } - if betweenExt.BidFloorCur != "" { - imp.BidFloorCur = betweenExt.BidFloorCur - } } // Adding header fields to request header diff --git a/adapters/between/betweentest/exemplary/multi-request.json b/adapters/between/betweentest/exemplary/multi-request.json index 0a68f2046ca..345e99dd527 100644 --- a/adapters/between/betweentest/exemplary/multi-request.json +++ b/adapters/between/betweentest/exemplary/multi-request.json @@ -71,7 +71,6 @@ "publisher_id": "1" } }, - "bidfloor": 1e-05, "secure": 0 }, { @@ -92,7 +91,6 @@ "publisher_id": "1" } }, - "bidfloor": 1e-05, "secure": 0 } ], diff --git a/adapters/between/betweentest/exemplary/secure-detect.json b/adapters/between/betweentest/exemplary/secure-detect.json index ab636ecb567..9df77f0d3cc 100644 --- a/adapters/between/betweentest/exemplary/secure-detect.json +++ b/adapters/between/betweentest/exemplary/secure-detect.json @@ -46,7 +46,6 @@ "w": 728, "h": 90 }, - "bidfloor": 1e-05, "secure": 1, "ext": { "bidder": { diff --git a/adapters/between/betweentest/exemplary/simple-site-banner.json b/adapters/between/betweentest/exemplary/simple-site-banner.json index d11f203319d..bcb7b30eea0 100644 --- a/adapters/between/betweentest/exemplary/simple-site-banner.json +++ b/adapters/between/betweentest/exemplary/simple-site-banner.json @@ -21,9 +21,7 @@ "ext": { "bidder": { "host": "127.0.0.1", - "publisher_id": "1", - "bid_floor_cur": "RUB", - "bid_floor": 1000 + "publisher_id": "1" } } } @@ -48,15 +46,11 @@ "w": 728, "h": 90 }, - "bidfloor": 1000, - "bidfloorcur": "RUB", "secure": 0, "ext": { "bidder": { "host": "127.0.0.1", - "publisher_id": "1", - "bid_floor_cur": "RUB", - "bid_floor": 1000 + "publisher_id": "1" } } } diff --git a/adapters/between/betweentest/supplemental/bad-dsp-request-example.json b/adapters/between/betweentest/supplemental/bad-dsp-request-example.json index 4e9bbac2928..0e52313502f 100644 --- a/adapters/between/betweentest/supplemental/bad-dsp-request-example.json +++ b/adapters/between/betweentest/supplemental/bad-dsp-request-example.json @@ -46,7 +46,6 @@ "w": 728, "h": 90 }, - "bidfloor": 1e-05, "secure": 0, "ext": { "bidder": { diff --git a/adapters/between/betweentest/supplemental/bad-response-body.json b/adapters/between/betweentest/supplemental/bad-response-body.json index fbd08206bd8..a916136c54e 100644 --- a/adapters/between/betweentest/supplemental/bad-response-body.json +++ b/adapters/between/betweentest/supplemental/bad-response-body.json @@ -42,7 +42,6 @@ "w": 300, "h": 250 }, - "bidfloor": 1e-05, "secure": 0, "ext": { "bidder": { diff --git a/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json b/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json index 67e6979891f..f17644bd1a1 100644 --- a/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json +++ b/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json @@ -46,7 +46,6 @@ "w": 728, "h": 90 }, - "bidfloor": 1e-05, "secure": 0, "ext": { "bidder": { diff --git a/adapters/between/betweentest/supplemental/no-bids.json b/adapters/between/betweentest/supplemental/no-bids.json index d73c3e6207c..d82828a9f1a 100644 --- a/adapters/between/betweentest/supplemental/no-bids.json +++ b/adapters/between/betweentest/supplemental/no-bids.json @@ -46,7 +46,6 @@ "w": 728, "h": 90 }, - "bidfloor": 1e-05, "secure": 0, "ext": { "bidder": { diff --git a/adapters/between/params_test.go b/adapters/between/params_test.go index 857718690d3..f0594c87f1d 100644 --- a/adapters/between/params_test.go +++ b/adapters/between/params_test.go @@ -41,16 +41,15 @@ func TestInvalidParams(t *testing.T) { var validParams = []string{ `{"host":"lbs-eu1.ads", "publisher_id": "1"}`, - `{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": 5.5}`, - `{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": 1, "bid_floor_cur": "RUB"}`, + `{"host":"lbs-ru1.ads", "publisher_id": "2"}`, + `{"host":"lbs-us-east1.ads", "publisher_id": "3"}`, + `{"host":"lbs-asia1.ads", "publisher_id": "4"}`, } var invalidParams = []string{ `{"host":"badhost.ads", "publisher_id": "1"}`, `{"host":"lbs-eu1.ads", "publisher_id": 1}`, - `{"host":"lbs-eu1.ads", "publisher_id": "1"", "bid_floor": 5.5}`, - `{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": "5.5"}`, - `{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": 5.5, "bid_floor_cur": "cur"}`, + `{"host":"lbs-eu1.ads", "publisher_id": "1""}`, ``, `null`, `true`, diff --git a/openrtb_ext/imp_between.go b/openrtb_ext/imp_between.go index c868baeb9da..9fe912a8618 100644 --- a/openrtb_ext/imp_between.go +++ b/openrtb_ext/imp_between.go @@ -1,8 +1,6 @@ package openrtb_ext type ExtImpBetween struct { - Host string `json:"host"` - PublisherID string `json:"publisher_id"` - BidFloor float64 `json:"bid_floor"` - BidFloorCur string `json:"bid_floor_cur"` + Host string `json:"host"` + PublisherID string `json:"publisher_id"` } diff --git a/static/bidder-params/between.json b/static/bidder-params/between.json index 032d38fec4b..64863462697 100644 --- a/static/bidder-params/between.json +++ b/static/bidder-params/between.json @@ -12,15 +12,6 @@ "publisher_id": { "type": "string", "description": "Publisher ID from Between Exchange control panel" - }, - "bid_floor": { - "type": "number", - "description": "The minimum price acceptable for a bid" - }, - "bid_floor_cur": { - "type": "string", - "description": "Currency of bid floor", - "enum": ["USD", "EUR", "RUB"] } }, "required": ["host", "publisher_id"] From fc9f9546ac2a0f782c892d48a558f05ce2c0a984 Mon Sep 17 00:00:00 2001 From: Jim Naumann Date: Tue, 8 Jun 2021 23:43:41 -0400 Subject: [PATCH 02/16] Beachfront: Fix for bid floor issue#1787 (#1878) Co-authored-by: jim naumann --- adapters/beachfront/beachfront.go | 33 +- .../supplemental/banner-bad-request-400.json | 2 +- .../supplemental/bidfloor-below-min.json | 102 ----- ...-four-variations-on-nothing-adm-video.json | 428 ++++++++++++++++++ .../bidfloor-test-ext-wins-adm-video.json | 124 +++++ .../bidfloor-test-imp-wins-adm-video.json | 125 +++++ adapters/beachfront/params_test.go | 36 +- static/bidder-params/beachfront.json | 21 +- 8 files changed, 747 insertions(+), 124 deletions(-) delete mode 100644 adapters/beachfront/beachfronttest/supplemental/bidfloor-below-min.json create mode 100644 adapters/beachfront/beachfronttest/supplemental/bidfloor-four-variations-on-nothing-adm-video.json create mode 100644 adapters/beachfront/beachfronttest/supplemental/bidfloor-test-ext-wins-adm-video.json create mode 100644 adapters/beachfront/beachfronttest/supplemental/bidfloor-test-imp-wins-adm-video.json diff --git a/adapters/beachfront/beachfront.go b/adapters/beachfront/beachfront.go index c5f6c4d5b1e..6eba9923e64 100644 --- a/adapters/beachfront/beachfront.go +++ b/adapters/beachfront/beachfront.go @@ -294,14 +294,12 @@ func getBannerRequest(request *openrtb2.BidRequest) (beachfrontBannerRequest, [] continue } + setBidFloor(&beachfrontExt, &request.Imp[i]) + slot := beachfrontSlot{ Id: appid, Slot: request.Imp[i].ID, - Bidfloor: beachfrontExt.BidFloor, - } - - if beachfrontExt.BidFloor <= minBidFloor { - slot.Bidfloor = 0 + Bidfloor: request.Imp[i].BidFloor, } for j := 0; j < len(request.Imp[i].Banner.Format); j++ { @@ -468,12 +466,7 @@ func getVideoRequests(request *openrtb2.BidRequest) ([]beachfrontVideoRequest, [ imp.Banner = nil imp.Ext = nil imp.Secure = &secure - - if beachfrontExt.BidFloor <= minBidFloor { - imp.BidFloor = 0 - } else { - imp.BidFloor = beachfrontExt.BidFloor - } + setBidFloor(&beachfrontExt, &imp) if imp.Video.H == 0 && imp.Video.W == 0 { imp.Video.W = defaultVideoWidth @@ -564,6 +557,24 @@ func (a *BeachfrontAdapter) MakeBids(internalRequest *openrtb2.BidRequest, exter return bidResponse, errs } +func setBidFloor(ext *openrtb_ext.ExtImpBeachfront, imp *openrtb2.Imp) { + var floor float64 + + if imp.BidFloor > 0 { + floor = imp.BidFloor + } else if ext.BidFloor > 0 { + floor = ext.BidFloor + } else { + floor = minBidFloor + } + + if floor <= minBidFloor { + floor = 0 + } + + imp.BidFloor = floor +} + func (a *BeachfrontAdapter) getBidType(externalRequest *adapters.RequestData) openrtb_ext.BidType { t := strings.Split(externalRequest.Uri, "=")[0] if t == a.extraInfo.VideoEndpoint { diff --git a/adapters/beachfront/beachfronttest/supplemental/banner-bad-request-400.json b/adapters/beachfront/beachfronttest/supplemental/banner-bad-request-400.json index 426499586f5..7463e2bf374 100644 --- a/adapters/beachfront/beachfronttest/supplemental/banner-bad-request-400.json +++ b/adapters/beachfront/beachfronttest/supplemental/banner-bad-request-400.json @@ -34,7 +34,7 @@ { "slot": "", "id": "dudAppId", - "bidfloor": 5.02, + "bidfloor": 0.02, "sizes": [ { "w": 300, diff --git a/adapters/beachfront/beachfronttest/supplemental/bidfloor-below-min.json b/adapters/beachfront/beachfronttest/supplemental/bidfloor-below-min.json deleted file mode 100644 index 70b89e5db1d..00000000000 --- a/adapters/beachfront/beachfronttest/supplemental/bidfloor-below-min.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "mockBidRequest": { - "id": "some_test_ad", - "site": { - "page": "https://some.domain.us/some/page.html" - }, - "imp": [ - { - "banner": { - "format": [ - { - "w": 300, - "h": 250 - } - ] - }, - "ext": { - "bidder": { - "bidfloor": 0.002, - "appId": "bannerAppId1" - } - } - } - ] - }, - - "httpCalls": [ - { - "expectedRequest": { - "uri": "https://qa.beachrtb.com/prebid_display", - "body": { - "slots": [ - { - "slot": "", - "id": "bannerAppId1", - "bidfloor": 0, - "sizes": [ - { - "w": 300, - "h": 250 - } - ] - } - ], - "domain": "some.domain.us", - "page": "https://some.domain.us/some/page.html", - "real204": true, - "referrer": "", - "search": "", - "secure": 1, - "requestId": "some_test_ad", - "isMobile": 0, - "ip": "", - "deviceModel": "", - "deviceOs": "", - "dnt": 0, - "ua": "", - "adapterName": "BF_PREBID_S2S", - "adapterVersion": "0.9.2", - "user": {}, - "schain": { - "complete": 0, - "nodes": null, - "ver": "" - } - } - }, - "mockResponse": { - "status": 200, - "body": [ - { - "crid": "crid_1", - "price": 2.942808, - "w": 300, - "h": 250, - "slot": "div-gpt-ad-1460505748561-0", - "adm": "
\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\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\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\n\n\n\n\n\n\n\n\n\"\"\n\"\"\n\n", - NURL: "http://test.com/syspixel?__ads=ip8070-3PJ4q4QyZxnHE6woGe1sQ3&__adt=4122105428549383603&__ade=2&type=tracking&rqc=0w23qR-q7O7MsGkWlR9wOBm8qL7msKBtSKRJV3Pw0a0tZ47xJTnT2JwzqvXgrzPZOLZfI__68S9kCKELawQtZcO6kMyvlPM55uCaRZWng_j5btuPaEuXyA&pab=true", - Width: 300, - Height: 250, - }, - } - cobj[1] = &CacheObject{ IsVideo: false, Value: &BidCache{ Adm: "{\"type\":\"ID\",\"bid_id\":\"8255649814109237089\",\"placement_id\":\"1995257847363113_1997038003851764\",\"resolved_placement_id\":\"1995257847363113_1997038003851764\",\"sdk_version\":\"4.25.0-appnexus.bidding\",\"device_id\":\"87ECBA49-908A-428F-9DE7-4B9CED4F486C\",\"template\":7,\"payload\":\"null\"}", @@ -103,7 +94,7 @@ func TestPrebidClient(t *testing.T) { Height: 250, }, } - cobj[2] = &CacheObject{ + cobj[1] = &CacheObject{ IsVideo: false, Value: &BidCache{ Adm: "", @@ -111,14 +102,10 @@ func TestPrebidClient(t *testing.T) { Height: 250, }, } - cobj[3] = &CacheObject{ + cobj[2] = &CacheObject{ IsVideo: true, Value: "", } - cobj[4] = &CacheObject{ - IsVideo: true, - Value: "\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\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\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\n\n\n\n\n\n\n\n\n\n\n\n\n\"\"\n\"\"\n\n", - } InitPrebidCache(server.URL) ctx := context.TODO() @@ -136,12 +123,6 @@ func TestPrebidClient(t *testing.T) { if cobj[2].UUID != "UUID-3" { t.Errorf("Third object UUID was '%s', should have been 'UUID-3'", cobj[2].UUID) } - if cobj[3].UUID != "UUID-4" { - t.Errorf("Fourth object UUID was '%s', should have been 'UUID-4'", cobj[3].UUID) - } - if cobj[4].UUID != "UUID-5" { - t.Errorf("Fifth object UUID was '%s', should have been 'UUID-5'", cobj[4].UUID) - } delay = 5 * time.Millisecond ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) diff --git a/router/router.go b/router/router.go index 3b51d0730c1..e1c42699225 100644 --- a/router/router.go +++ b/router/router.go @@ -23,7 +23,6 @@ import ( "github.com/prebid/prebid-server/adapters/appnexus" "github.com/prebid/prebid-server/adapters/conversant" "github.com/prebid/prebid-server/adapters/ix" - "github.com/prebid/prebid-server/adapters/lifestreet" "github.com/prebid/prebid-server/adapters/pubmatic" "github.com/prebid/prebid-server/adapters/pulsepoint" "github.com/prebid/prebid-server/adapters/rubicon" @@ -160,7 +159,6 @@ func newExchangeMap(cfg *config.Configuration) map[string]adapters.Adapter { "pulsepoint": pulsepoint.NewPulsePointLegacyAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderPulsepoint)].Endpoint), "rubicon": rubicon.NewRubiconLegacyAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderRubicon)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Username, cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Password, cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Tracker), - "lifestreet": lifestreet.NewLifestreetLegacyAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderLifestreet)].Endpoint), "conversant": conversant.NewConversantLegacyAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderConversant)].Endpoint), "adform": adform.NewAdformLegacyAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderAdform)].Endpoint), "sovrn": sovrn.NewSovrnLegacyAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderSovrn)].Endpoint), diff --git a/static/bidder-info/lifestreet.yaml b/static/bidder-info/lifestreet.yaml deleted file mode 100644 index 34dc4eca2d9..00000000000 --- a/static/bidder-info/lifestreet.yaml +++ /dev/null @@ -1,11 +0,0 @@ -maintainer: - email: "mobile.tech@lifestreet.com" -gvlVendorID: 67 -capabilities: - app: - mediaTypes: - - banner - site: - mediaTypes: - - banner - - video diff --git a/static/bidder-params/lifestreet.json b/static/bidder-params/lifestreet.json deleted file mode 100644 index 2190d761e69..00000000000 --- a/static/bidder-params/lifestreet.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Lifestreet Adapter Params", - "description": "A schema which validates params accepted by the Lifestreet adapter", - "type": "object", - "properties": { - "slot_tag": { - "type": "string", - "description": "A tag which identifies the ad slot" - } - }, - "required": ["slot_tag"] -} diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index 1c4e809e72b..a3f32320796 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -51,7 +51,6 @@ import ( "github.com/prebid/prebid-server/adapters/ix" "github.com/prebid/prebid-server/adapters/jixie" "github.com/prebid/prebid-server/adapters/krushmedia" - "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" @@ -149,7 +148,6 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderIx, ix.NewIxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderJixie, jixie.NewJixieSyncer) 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) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 10aaafd5985..39da3955fd9 100644 --- a/usersync/usersyncers/syncer_test.go +++ b/usersync/usersyncers/syncer_test.go @@ -60,7 +60,6 @@ func TestNewSyncerMap(t *testing.T) { string(openrtb_ext.BidderIx): syncConfig, string(openrtb_ext.BidderJixie): 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, From 7267f6e855f9f38eda179ea3c4f1629a22b62766 Mon Sep 17 00:00:00 2001 From: e-volution-tech <61746103+e-volution-tech@users.noreply.github.com> Date: Thu, 10 Jun 2021 18:53:26 +0300 Subject: [PATCH 09/16] New Adapter: E-Volution (#1868) --- adapters/e_volution/evolution.go | 112 ++++++++++ adapters/e_volution/evolution_test.go | 18 ++ .../exemplary/banner-without-mediatype.json | 168 +++++++++++++++ .../evolutiontest/exemplary/banner.json | 174 +++++++++++++++ .../evolutiontest/exemplary/native.json | 181 ++++++++++++++++ .../evolutiontest/exemplary/video.json | 200 ++++++++++++++++++ .../evolutiontest/params/race/banner.json | 3 + .../evolutiontest/params/race/native.json | 3 + .../evolutiontest/params/race/video.json | 3 + .../supplemental/bad-response.json | 158 ++++++++++++++ .../supplemental/empty-seatbid.json | 149 +++++++++++++ .../supplemental/status-204.json | 126 +++++++++++ .../supplemental/status-400.json | 133 ++++++++++++ .../supplemental/status-503.json | 125 +++++++++++ .../supplemental/unexpected-status.json | 132 ++++++++++++ adapters/e_volution/params_test.go | 49 +++++ adapters/e_volution/usersync.go | 12 ++ adapters/e_volution/usersync_test.go | 33 +++ config/config.go | 2 + exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + static/bidder-info/e_volution.yaml | 14 ++ static/bidder-params/e_volution.json | 13 ++ usersync/usersyncers/syncer.go | 2 + usersync/usersyncers/syncer_test.go | 1 + 25 files changed, 1815 insertions(+) create mode 100644 adapters/e_volution/evolution.go create mode 100644 adapters/e_volution/evolution_test.go create mode 100644 adapters/e_volution/evolutiontest/exemplary/banner-without-mediatype.json create mode 100644 adapters/e_volution/evolutiontest/exemplary/banner.json create mode 100644 adapters/e_volution/evolutiontest/exemplary/native.json create mode 100644 adapters/e_volution/evolutiontest/exemplary/video.json create mode 100644 adapters/e_volution/evolutiontest/params/race/banner.json create mode 100644 adapters/e_volution/evolutiontest/params/race/native.json create mode 100644 adapters/e_volution/evolutiontest/params/race/video.json create mode 100644 adapters/e_volution/evolutiontest/supplemental/bad-response.json create mode 100644 adapters/e_volution/evolutiontest/supplemental/empty-seatbid.json create mode 100644 adapters/e_volution/evolutiontest/supplemental/status-204.json create mode 100644 adapters/e_volution/evolutiontest/supplemental/status-400.json create mode 100644 adapters/e_volution/evolutiontest/supplemental/status-503.json create mode 100644 adapters/e_volution/evolutiontest/supplemental/unexpected-status.json create mode 100644 adapters/e_volution/params_test.go create mode 100644 adapters/e_volution/usersync.go create mode 100644 adapters/e_volution/usersync_test.go create mode 100644 static/bidder-info/e_volution.yaml create mode 100644 static/bidder-params/e_volution.json diff --git a/adapters/e_volution/evolution.go b/adapters/e_volution/evolution.go new file mode 100644 index 00000000000..26df301cdb7 --- /dev/null +++ b/adapters/e_volution/evolution.go @@ -0,0 +1,112 @@ +package evolution + +import ( + "encoding/json" + "fmt" + "github.com/mxmCherry/openrtb/v15/openrtb2" + "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 { + URI string +} + +type bidExt struct { + MediaType openrtb_ext.BidType `json:"mediaType"` +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) { + bidder := &adapter{ + URI: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests( + openRTBRequest *openrtb2.BidRequest, + reqInfo *adapters.ExtraRequestInfo, +) ( + requestsToBidder []*adapters.RequestData, + errs []error, +) { + + reqJSON, err := json.Marshal(openRTBRequest) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + return []*adapters.RequestData{{ + Method: http.MethodPost, + Body: reqJSON, + Uri: a.URI, + Headers: headers, + }}, nil +} + +func (a *adapter) MakeBids( + openRTBRequest *openrtb2.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. %s", string(bidderRawResponse.Body)), + }} + } + + if bidderRawResponse.StatusCode == http.StatusServiceUnavailable { + return nil, nil + } + + if bidderRawResponse.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Something went wrong, please contact your Account Manager. Status Code: [ %d ] ", bidderRawResponse.StatusCode), + }} + } + + responseBody := bidderRawResponse.Body + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(responseBody, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Bad response, %s", err), + }} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Empty seatbid"), + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + sb := bidResp.SeatBid[0] + for i := range sb.Bid { + var bidType openrtb_ext.BidType + var bidExt bidExt + if err := json.Unmarshal(sb.Bid[i].Ext, &bidExt); err != nil { + bidType = openrtb_ext.BidTypeBanner + } else { + bidType = bidExt.MediaType + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: bidType, + }) + } + return bidResponse, nil +} diff --git a/adapters/e_volution/evolution_test.go b/adapters/e_volution/evolution_test.go new file mode 100644 index 00000000000..1d2ee7ef9a6 --- /dev/null +++ b/adapters/e_volution/evolution_test.go @@ -0,0 +1,18 @@ +package evolution + +import ( + "testing" + + "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" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderEVolution, config.Adapter{ + Endpoint: "http://service.e-volution.ai/pbserver"}) + + assert.NoError(t, buildErr) + adapterstest.RunJSONBidderTest(t, "evolutiontest", bidder) +} diff --git a/adapters/e_volution/evolutiontest/exemplary/banner-without-mediatype.json b/adapters/e_volution/evolutiontest/exemplary/banner-without-mediatype.json new file mode 100644 index 00000000000..251fe8c6f87 --- /dev/null +++ b/adapters/e_volution/evolutiontest/exemplary/banner-without-mediatype.json @@ -0,0 +1,168 @@ +{ + "mockBidRequest": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "secure": 1, + "bidfloor": 0.20, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "key": "test_banner" + } + } + }], + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "country": "MEX", + "region": "CMX", + "city": "Mexico City", + "lat": 19.42, + "lon": -99.1663 + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "test_banner" + } + } + }], + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "lat": 19.42, + "lon": -99.1663, + "country": "MEX", + "region": "CMX", + "city": "Mexico City" + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "bidid": "880184da7ec02b1f42802acb46b63b3c", + "seatbid": [{ + "bid": [{ + "id": "6ca51ef38eb42820d27503ee96b634b5", + "impid": "126365", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "adm": "\"\"\"\"\"\"", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13", + "w": 0, + "h": 0 + }], + "seat": "1" + }], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "6ca51ef38eb42820d27503ee96b634b5", + "impid": "126365", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "adm": "\"\"\"\"\"\"", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/exemplary/banner.json b/adapters/e_volution/evolutiontest/exemplary/banner.json new file mode 100644 index 00000000000..68fda4907e2 --- /dev/null +++ b/adapters/e_volution/evolutiontest/exemplary/banner.json @@ -0,0 +1,174 @@ +{ + "mockBidRequest": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "secure": 1, + "bidfloor": 0.20, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "key": "test_banner" + } + } + }], + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "country": "MEX", + "region": "CMX", + "city": "Mexico City", + "lat": 19.42, + "lon": -99.1663 + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "test_banner" + } + } + }], + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "lat": 19.42, + "lon": -99.1663, + "country": "MEX", + "region": "CMX", + "city": "Mexico City" + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "bidid": "880184da7ec02b1f42802acb46b63b3c", + "seatbid": [{ + "bid": [{ + "id": "6ca51ef38eb42820d27503ee96b634b5", + "impid": "126365", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "adm": "\"\"\"\"\"\"", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13", + "w": 0, + "h": 0, + "ext": { + "mediaType": "banner" + } + }], + "seat": "1" + }], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "6ca51ef38eb42820d27503ee96b634b5", + "impid": "126365", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=880184da7ec02b1f42802acb46b63b3c&price=${AUCTION_PRICE}", + "adm": "\"\"\"\"\"\"", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13", + "ext": { + "mediaType": "banner" + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/exemplary/native.json b/adapters/e_volution/evolutiontest/exemplary/native.json new file mode 100644 index 00000000000..724f55f6b8b --- /dev/null +++ b/adapters/e_volution/evolutiontest/exemplary/native.json @@ -0,0 +1,181 @@ +{ + "mockBidRequest": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "tmax": 500, + "at": 1, + "device": { + "dnt": 0, + "devicetype": 2, + "js": 1, + "ip": "79.26.58.249", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", + "os": "Mac OS", + "language": "en", + "geo": { + "country": "ITA" + } + }, + "user": { + "id": "l1eHS1lZE41a", + "ext": { + "consent": "CPCfkEbPCfkEbERACAENBbCsAP_AAH_AAAAAHetf_X_fb39j-_59_9t0eY1f9_7_v-wzjhfds-8NyPvX_L8X42M7PF36pq4KuR4Eu3LBIQFlHOHUTUmw6okVrTPsak2Mr7NKJ7LEinMbe2dYGHtfn91TuZKYr_7s_9fz__-v_v__79f3r-3_3_vp9X---_e_V399xKB3QBJhqXwAWYljgSTRpVCiBCFYSHQCgAooBhaJrCAhYFOyuAj1BAwAQGoCMCIEGIKMWAQAAAQBIREBIAeCARAEQCAAEAKkBCAAiYBBYAWBgEAAoBoWIEUAQgSEGRwVHKYEBEi0UE8lYAlF3sYYQhlFgBQKP6KjARKEAAAA.YAAAAAAAAAAA" + } + }, + "site": { + "id": "57078628", + "domain": "www.affaritaliani.it", + "page": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html", + "ref": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html?refresh_ce", + "name": "www.affaritaliani.it", + "publisher": { + "id": "439696", + "name": "www.affaritaliani.it" + }, + "cat": [ + "IAB12" + ] + }, + "publisher": {}, + "cur": [ + "USD" + ], + "bcat": [ + "IAB12-2", + "IAB9-7" + ], + "imp": [{ + "id": "1", + "bidfloor": 0.1, + "bidfloorcur": "USD", + "instl": 0, + "secure": 1, + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "key": "test_native" + } + } + }], + "regs": { + "ext": { + "gdpr": 1 + } + } + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "imp": [{ + "id": "1", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "bidfloor": 0.1, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "test_native" + } + } + }], + "site": { + "id": "57078628", + "name": "www.affaritaliani.it", + "domain": "www.affaritaliani.it", + "cat": ["IAB12"], + "page": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html", + "ref": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html?refresh_ce", + "publisher": { + "id": "439696", + "name": "www.affaritaliani.it" + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", + "geo": { + "country": "ITA" + }, + "dnt": 0, + "ip": "79.26.58.249", + "devicetype": 2, + "os": "Mac OS", + "js": 1, + "language": "en" + }, + "user": { + "id": "l1eHS1lZE41a", + "ext": { + "consent": "CPCfkEbPCfkEbERACAENBbCsAP_AAH_AAAAAHetf_X_fb39j-_59_9t0eY1f9_7_v-wzjhfds-8NyPvX_L8X42M7PF36pq4KuR4Eu3LBIQFlHOHUTUmw6okVrTPsak2Mr7NKJ7LEinMbe2dYGHtfn91TuZKYr_7s_9fz__-v_v__79f3r-3_3_vp9X---_e_V399xKB3QBJhqXwAWYljgSTRpVCiBCFYSHQCgAooBhaJrCAhYFOyuAj1BAwAQGoCMCIEGIKMWAQAAAQBIREBIAeCARAEQCAAEAKkBCAAiYBBYAWBgEAAoBoWIEUAQgSEGRwVHKYEBEi0UE8lYAlF3sYYQhlFgBQKP6KjARKEAAAA.YAAAAAAAAAAA" + } + }, + "at": 1, + "tmax": 500, + "cur": ["USD"], + "bcat": ["IAB12-2", "IAB9-7"], + "regs": { + "ext": { + "gdpr": 1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "bidid": "359da97d0384d8a14767029c18fd840d", + "seatbid": [{ + "bid": [{ + "id": "1af875cae46410c18e4d8b1fcc909e6c", + "impid": "1", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=359da97d0384d8a14767029c18fd840d&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=359da97d0384d8a14767029c18fd840d&price=${AUCTION_PRICE}", + "adm": "{\"link\":{\"url\":\"https://e-volution.ai\"},\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"text\":\"Viralize test\"}},{\"id\":2,\"required\":1,\"img\":{}},{\"id\":4,\"data\":{\"value\":\"3$\"}},{\"id\":5,\"data\":{\"value\":\"2$\"}},{\"id\":6,\"data\":{\"value\":\"viralize\"}}],\"ver\":\"1.1\",\"imptrackers\":[\"https://us-east-edge1.e-volution.ai/?c=rtb&m=i&key=359da97d0384d8a14767029c18fd840d&cp=${AUCTION_PRICE}\",\"https://us-east-edge1.e-volution.ai/?c=rtb&m=sync&gdpr=1&gdpr_consent=CPCfkEbPCfkEbERACAENBbCsAP_AAH_AAAAAHetf_X_fb39j-_59_9t0eY1f9_7_v-wzjhfds-8NyPvX_L8X42M7PF36pq4KuR4Eu3LBIQFlHOHUTUmw6okVrTPsak2Mr7NKJ7LEinMbe2dYGHtfn91TuZKYr_7s_9fz__-v_v__79f3r-3_3_vp9X---_e_V399xKB3QBJhqXwAWYljgSTRpVCiBCFYSHQCgAooBhaJrCAhYFOyuAj1BAwAQGoCMCIEGIKMWAQAAAQBIREBIAeCARAEQCAAEAKkBCAAiYBBYAWBgEAAoBoWIEUAQgSEGRwVHKYEBEi0UE8lYAlF3sYYQhlFgBQKP6KjARKEAAAA.YAAAAAAAAAAA&ccpa_consent=\"]}", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13", + "w": 0, + "h": 0, + "ext": { + "mediaType": "native" + } + }], + "seat": "1" + }], + "cur": "USD" + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "1af875cae46410c18e4d8b1fcc909e6c", + "impid": "1", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=359da97d0384d8a14767029c18fd840d&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=359da97d0384d8a14767029c18fd840d&price=${AUCTION_PRICE}", + "adm": "{\"link\":{\"url\":\"https://e-volution.ai\"},\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"text\":\"Viralize test\"}},{\"id\":2,\"required\":1,\"img\":{}},{\"id\":4,\"data\":{\"value\":\"3$\"}},{\"id\":5,\"data\":{\"value\":\"2$\"}},{\"id\":6,\"data\":{\"value\":\"viralize\"}}],\"ver\":\"1.1\",\"imptrackers\":[\"https://us-east-edge1.e-volution.ai/?c=rtb&m=i&key=359da97d0384d8a14767029c18fd840d&cp=${AUCTION_PRICE}\",\"https://us-east-edge1.e-volution.ai/?c=rtb&m=sync&gdpr=1&gdpr_consent=CPCfkEbPCfkEbERACAENBbCsAP_AAH_AAAAAHetf_X_fb39j-_59_9t0eY1f9_7_v-wzjhfds-8NyPvX_L8X42M7PF36pq4KuR4Eu3LBIQFlHOHUTUmw6okVrTPsak2Mr7NKJ7LEinMbe2dYGHtfn91TuZKYr_7s_9fz__-v_v__79f3r-3_3_vp9X---_e_V399xKB3QBJhqXwAWYljgSTRpVCiBCFYSHQCgAooBhaJrCAhYFOyuAj1BAwAQGoCMCIEGIKMWAQAAAQBIREBIAeCARAEQCAAEAKkBCAAiYBBYAWBgEAAoBoWIEUAQgSEGRwVHKYEBEi0UE8lYAlF3sYYQhlFgBQKP6KjARKEAAAA.YAAAAAAAAAAA&ccpa_consent=\"]}", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13", + "ext": { + "mediaType": "native" + } + }, + "type": "native" + }] + }] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/exemplary/video.json b/adapters/e_volution/evolutiontest/exemplary/video.json new file mode 100644 index 00000000000..f7a03146918 --- /dev/null +++ b/adapters/e_volution/evolutiontest/exemplary/video.json @@ -0,0 +1,200 @@ +{ + "mockBidRequest": { + "id": "6d773e60ab245243a04217d5f7e9e786", + "imp": [{ + "id": "bfb2dd46a5e63a0a84711c20fd8ce9e1", + "video": { + "mimes": ["video/mp4", "video/ogg", "video/webm"], + "minduration": 3, + "maxduration": 3000, + "protocols": [2, 3, 5, 6, 7, 8], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [2], + "pos": 0 + }, + "bidfloor": 0.40404, + "bidfloorcur": "USD", + "secure": 1, + "exp": 3600, + "ext": { + "bidder": { + "key": "test_video" + } + } + }], + "site": { + "id": "1c94d85ea1a55ebf3325a82935240614", + "domain": "kiwilimon.com", + "cat": ["IAB1", "IAB2", "IAB3", "IAB4", "IAB5", "IAB6", "IAB7", "IAB8", "IAB9", "IAB10", "IAB11", "IAB13", "IAB14", "IAB15", "IAB17", "IAB18"], + "page": "https://kiwilimon.com/", + "publisher": { + "id": "866d6b3a4a483ed4a454a1acaa588852" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36", + "geo": { + "lat": 20.6668, + "lon": -103.3918, + "accuracy": 5, + "country": "MEX", + "region": "JAL", + "zip": "45186" + }, + "dnt": 0, + "lmt": 0, + "ip": "187.152.119.145", + "devicetype": 2, + "os": "Windows", + "osv": "10", + "carrier": "Telmex" + }, + "test": 1, + "at": 1, + "tmax": 500, + "cur": ["USD"], + "source": { + "tid": "42da74bc-88c5-40ea-8187-9070a34e8e10" + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "6d773e60ab245243a04217d5f7e9e786", + "imp": [{ + "id": "bfb2dd46a5e63a0a84711c20fd8ce9e1", + "video": { + "mimes": ["video/mp4", "video/ogg", "video/webm"], + "minduration": 3, + "maxduration": 3000, + "protocols": [2, 3, 5, 6, 7, 8], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [2], + "pos": 0 + }, + "bidfloor": 0.40404, + "bidfloorcur": "USD", + "secure": 1, + "exp": 3600, + "ext": { + "bidder": { + "key": "test_video" + } + } + }], + "site": { + "id": "1c94d85ea1a55ebf3325a82935240614", + "domain": "kiwilimon.com", + "cat": ["IAB1", "IAB2", "IAB3", "IAB4", "IAB5", "IAB6", "IAB7", "IAB8", "IAB9", "IAB10", "IAB11", "IAB13", "IAB14", "IAB15", "IAB17", "IAB18"], + "page": "https://kiwilimon.com/", + "publisher": { + "id": "866d6b3a4a483ed4a454a1acaa588852" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36", + "geo": { + "lat": 20.6668, + "lon": -103.3918, + "accuracy": 5, + "country": "MEX", + "region": "JAL", + "zip": "45186" + }, + "dnt": 0, + "lmt": 0, + "ip": "187.152.119.145", + "devicetype": 2, + "os": "Windows", + "osv": "10", + "carrier": "Telmex" + }, + "test": 1, + "at": 1, + "tmax": 500, + "cur": ["USD"], + "source": { + "tid": "42da74bc-88c5-40ea-8187-9070a34e8e10" + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "6d773e60ab245243a04217d5f7e9e786", + "bidid": "ccbd63285c0e7b69602d90319bda6be4", + "seatbid": [{ + "bid": [{ + "id": "7f6c6d6ba432059a5305815a8d740283", + "impid": "bfb2dd46a5e63a0a84711c20fd8ce9e1", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=ccbd63285c0e7b69602d90319bda6be4&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=ccbd63285c0e7b69602d90319bda6be4&price=${AUCTION_PRICE}", + "adm": "846500:00:16", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13", + "w": 0, + "h": 0, + "ext": { + "mediaType": "video" + } + }], + "seat": "1" + }], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7f6c6d6ba432059a5305815a8d740283", + "impid": "bfb2dd46a5e63a0a84711c20fd8ce9e1", + "price": 0.1, + "nurl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=nurl&auctionId=ccbd63285c0e7b69602d90319bda6be4&price=${AUCTION_PRICE}", + "burl": "http://us-east-edge1.e-volution.ai/?c=rtb&m=burl&auctionId=ccbd63285c0e7b69602d90319bda6be4&price=${AUCTION_PRICE}", + "adm": "846500:00:16", + "adomain": ["test.com"], + "cat": ["IAB24"], + "cid": "1IP31", + "crid": "1KS13", + "ext": { + "mediaType": "video" + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/params/race/banner.json b/adapters/e_volution/evolutiontest/params/race/banner.json new file mode 100644 index 00000000000..0a04c95b072 --- /dev/null +++ b/adapters/e_volution/evolutiontest/params/race/banner.json @@ -0,0 +1,3 @@ +{ + "key": "test_banner" +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/params/race/native.json b/adapters/e_volution/evolutiontest/params/race/native.json new file mode 100644 index 00000000000..032b9dd56d8 --- /dev/null +++ b/adapters/e_volution/evolutiontest/params/race/native.json @@ -0,0 +1,3 @@ +{ + "key": "test_native" +} diff --git a/adapters/e_volution/evolutiontest/params/race/video.json b/adapters/e_volution/evolutiontest/params/race/video.json new file mode 100644 index 00000000000..87071003920 --- /dev/null +++ b/adapters/e_volution/evolutiontest/params/race/video.json @@ -0,0 +1,3 @@ +{ + "key": "test_video" +} diff --git a/adapters/e_volution/evolutiontest/supplemental/bad-response.json b/adapters/e_volution/evolutiontest/supplemental/bad-response.json new file mode 100644 index 00000000000..75f3cb455af --- /dev/null +++ b/adapters/e_volution/evolutiontest/supplemental/bad-response.json @@ -0,0 +1,158 @@ +{ + "mockBidRequest": { + "id": "6d773e60ab245243a04217d5f7e9e786", + "imp": [{ + "id": "bfb2dd46a5e63a0a84711c20fd8ce9e1", + "video": { + "mimes": ["video/mp4", "video/ogg", "video/webm"], + "minduration": 3, + "maxduration": 3000, + "protocols": [2, 3, 5, 6, 7, 8], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [2], + "pos": 0 + }, + "bidfloor": 0.40404, + "bidfloorcur": "USD", + "secure": 1, + "exp": 3600, + "ext": { + "bidder": { + "key": "test_video" + } + } + }], + "site": { + "id": "1c94d85ea1a55ebf3325a82935240614", + "domain": "kiwilimon.com", + "cat": ["IAB1", "IAB2", "IAB3", "IAB4", "IAB5", "IAB6", "IAB7", "IAB8", "IAB9", "IAB10", "IAB11", "IAB13", "IAB14", "IAB15", "IAB17", "IAB18"], + "page": "https://kiwilimon.com/", + "publisher": { + "id": "866d6b3a4a483ed4a454a1acaa588852" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36", + "geo": { + "lat": 20.6668, + "lon": -103.3918, + "accuracy": 5, + "country": "MEX", + "region": "JAL", + "zip": "45186" + }, + "dnt": 0, + "lmt": 0, + "ip": "187.152.119.145", + "devicetype": 2, + "os": "Windows", + "osv": "10", + "carrier": "Telmex" + }, + "test": 1, + "at": 1, + "tmax": 500, + "cur": ["USD"], + "source": { + "tid": "42da74bc-88c5-40ea-8187-9070a34e8e10" + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "6d773e60ab245243a04217d5f7e9e786", + "imp": [{ + "id": "bfb2dd46a5e63a0a84711c20fd8ce9e1", + "video": { + "mimes": ["video/mp4", "video/ogg", "video/webm"], + "minduration": 3, + "maxduration": 3000, + "protocols": [2, 3, 5, 6, 7, 8], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [2], + "pos": 0 + }, + "bidfloor": 0.40404, + "bidfloorcur": "USD", + "secure": 1, + "exp": 3600, + "ext": { + "bidder": { + "key": "test_video" + } + } + }], + "site": { + "id": "1c94d85ea1a55ebf3325a82935240614", + "domain": "kiwilimon.com", + "cat": ["IAB1", "IAB2", "IAB3", "IAB4", "IAB5", "IAB6", "IAB7", "IAB8", "IAB9", "IAB10", "IAB11", "IAB13", "IAB14", "IAB15", "IAB17", "IAB18"], + "page": "https://kiwilimon.com/", + "publisher": { + "id": "866d6b3a4a483ed4a454a1acaa588852" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36", + "geo": { + "lat": 20.6668, + "lon": -103.3918, + "accuracy": 5, + "country": "MEX", + "region": "JAL", + "zip": "45186" + }, + "dnt": 0, + "lmt": 0, + "ip": "187.152.119.145", + "devicetype": 2, + "os": "Windows", + "osv": "10", + "carrier": "Telmex" + }, + "test": 1, + "at": 1, + "tmax": 500, + "cur": ["USD"], + "source": { + "tid": "42da74bc-88c5-40ea-8187-9070a34e8e10" + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Bad response, json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/supplemental/empty-seatbid.json b/adapters/e_volution/evolutiontest/supplemental/empty-seatbid.json new file mode 100644 index 00000000000..c9a103aea39 --- /dev/null +++ b/adapters/e_volution/evolutiontest/supplemental/empty-seatbid.json @@ -0,0 +1,149 @@ +{ + "mockBidRequest": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "tmax": 500, + "at": 1, + "device": { + "dnt": 0, + "devicetype": 2, + "js": 1, + "ip": "79.26.58.249", + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", + "os": "Mac OS", + "language": "en", + "geo": { + "country": "ITA" + } + }, + "user": { + "id": "l1eHS1lZE41a", + "ext": { + "consent": "CPCfkEbPCfkEbERACAENBbCsAP_AAH_AAAAAHetf_X_fb39j-_59_9t0eY1f9_7_v-wzjhfds-8NyPvX_L8X42M7PF36pq4KuR4Eu3LBIQFlHOHUTUmw6okVrTPsak2Mr7NKJ7LEinMbe2dYGHtfn91TuZKYr_7s_9fz__-v_v__79f3r-3_3_vp9X---_e_V399xKB3QBJhqXwAWYljgSTRpVCiBCFYSHQCgAooBhaJrCAhYFOyuAj1BAwAQGoCMCIEGIKMWAQAAAQBIREBIAeCARAEQCAAEAKkBCAAiYBBYAWBgEAAoBoWIEUAQgSEGRwVHKYEBEi0UE8lYAlF3sYYQhlFgBQKP6KjARKEAAAA.YAAAAAAAAAAA" + } + }, + "site": { + "id": "57078628", + "domain": "www.affaritaliani.it", + "page": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html", + "ref": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html?refresh_ce", + "name": "www.affaritaliani.it", + "publisher": { + "id": "439696", + "name": "www.affaritaliani.it" + }, + "cat": [ + "IAB12" + ] + }, + "publisher": {}, + "cur": [ + "USD" + ], + "bcat": [ + "IAB12-2", + "IAB9-7" + ], + "imp": [{ + "id": "1", + "bidfloor": 0.1, + "bidfloorcur": "USD", + "instl": 0, + "secure": 1, + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "key": "test_native" + } + } + }], + "regs": { + "ext": { + "gdpr": 1 + } + } + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "imp": [{ + "id": "1", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "bidfloor": 0.1, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "test_native" + } + } + }], + "site": { + "id": "57078628", + "name": "www.affaritaliani.it", + "domain": "www.affaritaliani.it", + "cat": ["IAB12"], + "page": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html", + "ref": "https://www.affaritaliani.it/cronache/gestione-covid-proteste-di-imprenditori-sanitari-ma-ignorate-da-ogni-media-742138.html?refresh_ce", + "publisher": { + "id": "439696", + "name": "www.affaritaliani.it" + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", + "geo": { + "country": "ITA" + }, + "dnt": 0, + "ip": "79.26.58.249", + "devicetype": 2, + "os": "Mac OS", + "js": 1, + "language": "en" + }, + "user": { + "id": "l1eHS1lZE41a", + "ext": { + "consent": "CPCfkEbPCfkEbERACAENBbCsAP_AAH_AAAAAHetf_X_fb39j-_59_9t0eY1f9_7_v-wzjhfds-8NyPvX_L8X42M7PF36pq4KuR4Eu3LBIQFlHOHUTUmw6okVrTPsak2Mr7NKJ7LEinMbe2dYGHtfn91TuZKYr_7s_9fz__-v_v__79f3r-3_3_vp9X---_e_V399xKB3QBJhqXwAWYljgSTRpVCiBCFYSHQCgAooBhaJrCAhYFOyuAj1BAwAQGoCMCIEGIKMWAQAAAQBIREBIAeCARAEQCAAEAKkBCAAiYBBYAWBgEAAoBoWIEUAQgSEGRwVHKYEBEi0UE8lYAlF3sYYQhlFgBQKP6KjARKEAAAA.YAAAAAAAAAAA" + } + }, + "at": 1, + "tmax": 500, + "cur": ["USD"], + "bcat": ["IAB12-2", "IAB9-7"], + "regs": { + "ext": { + "gdpr": 1 + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "bidid": "359da97d0384d8a14767029c18fd840d", + "seatbid": [], + "cur": "USD" + } + } + }], + + "expectedMakeBidsErrors": [ + { + "value": "Empty seatbid", + "comparison": "literal" + } + ], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/supplemental/status-204.json b/adapters/e_volution/evolutiontest/supplemental/status-204.json new file mode 100644 index 00000000000..85e89873fd2 --- /dev/null +++ b/adapters/e_volution/evolutiontest/supplemental/status-204.json @@ -0,0 +1,126 @@ +{ + "mockBidRequest": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "secure": 1, + "bidfloor": 0.20, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "key": "test_banner" + } + } + }], + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "country": "MEX", + "region": "CMX", + "city": "Mexico City", + "lat": 19.42, + "lon": -99.1663 + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "test_banner" + } + } + }], + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "lat": 19.42, + "lon": -99.1663, + "country": "MEX", + "region": "CMX", + "city": "Mexico City" + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/supplemental/status-400.json b/adapters/e_volution/evolutiontest/supplemental/status-400.json new file mode 100644 index 00000000000..b26e827200e --- /dev/null +++ b/adapters/e_volution/evolutiontest/supplemental/status-400.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "secure": 1, + "bidfloor": 0.20, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "key": "45352" + } + } + }], + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "country": "MEX", + "region": "CMX", + "city": "Mexico City", + "lat": 19.42, + "lon": -99.1663 + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "45352" + } + } + }], + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "lat": 19.42, + "lon": -99.1663, + "country": "MEX", + "region": "CMX", + "city": "Mexico City" + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 400, + "body": "The Key has a different ad format" + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Bad Request. \"The Key has a different ad format\"", + "comparison": "literal" + } + ], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/supplemental/status-503.json b/adapters/e_volution/evolutiontest/supplemental/status-503.json new file mode 100644 index 00000000000..0f289ea8d3e --- /dev/null +++ b/adapters/e_volution/evolutiontest/supplemental/status-503.json @@ -0,0 +1,125 @@ +{ + "mockBidRequest": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "secure": 1, + "bidfloor": 0.20, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "key": "45352" + } + } + }], + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "country": "MEX", + "region": "CMX", + "city": "Mexico City", + "lat": 19.42, + "lon": -99.1663 + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "45352" + } + } + }], + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "lat": 19.42, + "lon": -99.1663, + "country": "MEX", + "region": "CMX", + "city": "Mexico City" + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 503 + } + } + ], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/e_volution/evolutiontest/supplemental/unexpected-status.json b/adapters/e_volution/evolutiontest/supplemental/unexpected-status.json new file mode 100644 index 00000000000..5d0df32383e --- /dev/null +++ b/adapters/e_volution/evolutiontest/supplemental/unexpected-status.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "secure": 1, + "bidfloor": 0.20, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "key": "45352" + } + } + }], + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "country": "MEX", + "region": "CMX", + "city": "Mexico City", + "lat": 19.42, + "lon": -99.1663 + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://service.e-volution.ai/pbserver", + "body": { + "id": "d910ceef496ff2d746176506e296f4b3_z53356_a126365", + "imp": [{ + "id": "126365", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "bidder": { + "key": "45352" + } + } + }], + "site": { + "id": "12", + "domain": "milano.repubblica.it", + "cat": ["IAB12"], + "page": "https://milano.repubblica.it", + "publisher": { + "id": "45672" + } + }, + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1", + "geo": { + "lat": 19.42, + "lon": -99.1663, + "country": "MEX", + "region": "CMX", + "city": "Mexico City" + }, + "ip": "189.146.118.247", + "devicetype": 4, + "language": "it" + }, + "user": { + "id": "460ecf1d41fa6656c4b892fd84e61ff8c2ab5ad5", + "ext": { + "consent": "" + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } + } + }, + "mockResponse": { + "status": 401 + } + } + ], + + "expectedMakeBidsErrors": [ + { + "value": "Something went wrong, please contact your Account Manager. Status Code: [ 401 ] ", + "comparison": "literal" + } + ], + + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/e_volution/params_test.go b/adapters/e_volution/params_test.go new file mode 100644 index 00000000000..2d3602fd72b --- /dev/null +++ b/adapters/e_volution/params_test.go @@ -0,0 +1,49 @@ +package evolution + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/openrtb_ext" +) + +var validParams = []string{ + `{ "key": "24" }`, +} + +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.BidderEVolution, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected evolution params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{ "anyparam": "anyvalue" }`, +} + +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.BidderEVolution, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/adapters/e_volution/usersync.go b/adapters/e_volution/usersync.go new file mode 100644 index 00000000000..f22784d018b --- /dev/null +++ b/adapters/e_volution/usersync.go @@ -0,0 +1,12 @@ +package evolution + +import ( + "text/template" + + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/usersync" +) + +func NewEvolutionSyncer(temp *template.Template) usersync.Usersyncer { + return adapters.NewSyncer("e_volution", temp, adapters.SyncTypeRedirect) +} diff --git a/adapters/e_volution/usersync_test.go b/adapters/e_volution/usersync_test.go new file mode 100644 index 00000000000..d7a3eba5f0a --- /dev/null +++ b/adapters/e_volution/usersync_test.go @@ -0,0 +1,33 @@ +package evolution + +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 TestNewEvolutionSyncer(t *testing.T) { + syncURL := "https://sync.test.com/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}" + syncURLTemplate := template.Must( + template.New("sync-template").Parse(syncURL), + ) + syncer := NewEvolutionSyncer(syncURLTemplate) + syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{ + GDPR: gdpr.Policy{ + Signal: "0", + Consent: "allGdpr", + }, + CCPA: ccpa.Policy{ + Consent: "1---", + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "https://sync.test.com/pbserver?gdpr=0&gdpr_consent=allGdpr&ccpa=1---", syncInfo.URL) + assert.Equal(t, "redirect", syncInfo.Type) + assert.Equal(t, false, syncInfo.SupportCORS) +} diff --git a/config/config.go b/config/config.go index 2d8fb129e42..a259a9aa4e1 100644 --- a/config/config.go +++ b/config/config.go @@ -604,6 +604,7 @@ func (cfg *Configuration) setDerivedDefaults() { // openrtb_ext.BidderDMX doesn't have a good default. setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderDeepintent, "https://match.deepintent.com/usersync/136?id=unk&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Ddeepintent%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D") 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.BidderEVolution, "https://sync.e-volution.ai/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3De_volution%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D") 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") // openrtb_ext.BidderEpom doesn't have a good default. @@ -862,6 +863,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("adapters.eplanning.endpoint", "http://rtb.e-planning.net/pbs/1") v.SetDefault("adapters.epom.endpoint", "https://an.epom.com/ortb") v.SetDefault("adapters.epom.disabled", true) + v.SetDefault("adapters.e_volution.endpoint", "http://service.e-volution.ai/pbserver") 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", "https://grid.bidswitch.net/sp_bid?sp=prebid") diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 4965f7f5019..a773d268604 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -46,6 +46,7 @@ import ( "github.com/prebid/prebid-server/adapters/decenterads" "github.com/prebid/prebid-server/adapters/deepintent" "github.com/prebid/prebid-server/adapters/dmx" + "github.com/prebid/prebid-server/adapters/e_volution" "github.com/prebid/prebid-server/adapters/emx_digital" "github.com/prebid/prebid-server/adapters/engagebdr" "github.com/prebid/prebid-server/adapters/eplanning" @@ -169,6 +170,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderEngageBDR: engagebdr.Builder, openrtb_ext.BidderEPlanning: eplanning.Builder, openrtb_ext.BidderEpom: epom.Builder, + openrtb_ext.BidderEVolution: evolution.Builder, openrtb_ext.BidderGamma: gamma.Builder, openrtb_ext.BidderGamoshi: gamoshi.Builder, openrtb_ext.BidderGrid: grid.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 082498d2893..ea6c809fd9b 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -121,6 +121,7 @@ const ( BidderEngageBDR BidderName = "engagebdr" BidderEPlanning BidderName = "eplanning" BidderEpom BidderName = "epom" + BidderEVolution BidderName = "e_volution" BidderGamma BidderName = "gamma" BidderGamoshi BidderName = "gamoshi" BidderGrid BidderName = "grid" @@ -240,6 +241,7 @@ func CoreBidderNames() []BidderName { BidderEngageBDR, BidderEPlanning, BidderEpom, + BidderEVolution, BidderGamma, BidderGamoshi, BidderGrid, diff --git a/static/bidder-info/e_volution.yaml b/static/bidder-info/e_volution.yaml new file mode 100644 index 00000000000..6ea9dc7bac2 --- /dev/null +++ b/static/bidder-info/e_volution.yaml @@ -0,0 +1,14 @@ +maintainer: + email: "admin@e-volution.ai" +gvlVendorID: 957 +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-params/e_volution.json b/static/bidder-params/e_volution.json new file mode 100644 index 00000000000..18de2a6062d --- /dev/null +++ b/static/bidder-params/e_volution.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "E-volution Adapter Params", + "description": "A schema which validates params accepted by the E-volution adapter", + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "network or placement key" + } + }, + "required": ["key"] + } \ No newline at end of file diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go index a3f32320796..88752f4d7d7 100644 --- a/usersync/usersyncers/syncer.go +++ b/usersync/usersyncers/syncer.go @@ -39,6 +39,7 @@ import ( "github.com/prebid/prebid-server/adapters/datablocks" "github.com/prebid/prebid-server/adapters/deepintent" "github.com/prebid/prebid-server/adapters/dmx" + "github.com/prebid/prebid-server/adapters/e_volution" "github.com/prebid/prebid-server/adapters/emx_digital" "github.com/prebid/prebid-server/adapters/engagebdr" "github.com/prebid/prebid-server/adapters/eplanning" @@ -136,6 +137,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync insertIntoMap(cfg, syncers, openrtb_ext.BidderDeepintent, deepintent.NewDeepintentSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderDmx, dmx.NewDmxSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEmxDigital, emx_digital.NewEMXDigitalSyncer) + insertIntoMap(cfg, syncers, openrtb_ext.BidderEVolution, evolution.NewEvolutionSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEngageBDR, engagebdr.NewEngageBDRSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderEPlanning, eplanning.NewEPlanningSyncer) insertIntoMap(cfg, syncers, openrtb_ext.BidderAudienceNetwork, audienceNetwork.NewFacebookSyncer) diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go index 39da3955fd9..2ebd541d015 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.BidderEmxDigital): syncConfig, string(openrtb_ext.BidderEngageBDR): syncConfig, string(openrtb_ext.BidderEPlanning): syncConfig, + string(openrtb_ext.BidderEVolution): syncConfig, string(openrtb_ext.BidderGamma): syncConfig, string(openrtb_ext.BidderGamoshi): syncConfig, string(openrtb_ext.BidderGrid): syncConfig, From 08ad3eef8fba678f4b82461184651b46f49b6d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9onard=20Labat?= Date: Thu, 10 Jun 2021 18:55:20 +0200 Subject: [PATCH 10/16] [criteo] accept zoneId and networkId alternate case (#1869) --- .../supplemental/multislots-alt-case.json | 232 ++++++++++++++++++ adapters/criteo/params_test.go | 7 + static/bidder-params/criteo.json | 78 +++--- 3 files changed, 288 insertions(+), 29 deletions(-) create mode 100644 adapters/criteo/criteotest/supplemental/multislots-alt-case.json diff --git a/adapters/criteo/criteotest/supplemental/multislots-alt-case.json b/adapters/criteo/criteotest/supplemental/multislots-alt-case.json new file mode 100644 index 00000000000..beb855e3f2b --- /dev/null +++ b/adapters/criteo/criteotest/supplemental/multislots-alt-case.json @@ -0,0 +1,232 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "id": "site-id", + "page": "criteo.com" + }, + "device": { + "os": "android", + "ip": "91.199.242.236", + "ua": "random user agent" + }, + "user": { + "ext": { + "eids": [{ + "source": "criteo.com", + "uids": [{ + "id": "criteo-eid" + }] + }] + } + }, + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "zoneId": 123456, + "networkId": 78910 + } + } + }, + { + "id": "test-imp-id-2", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "zoneId": 7891011, + "networkId": 78910 + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "zoneId": 121314, + "networkId": 78910 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bidder.criteo.com/cdb?profileId=230", + "headers": { + "X-Forwarded-For": [ + "91.199.242.236" + ], + "User-Agent": [ + "random user agent" + ] + }, + "body": { + "id": "test-request-id", + "publisher": { + "siteid": "site-id", + "url": "criteo.com", + "networkid": 78910 + }, + "user": { + "ip": "91.199.242.236", + "ua": "random user agent", + "deviceos": "android", + "deviceidtype": "gaid" + }, + "gdprconsent": {}, + "slots": [ + { + "slotid": "00000000-0000-0000-00000000", + "impid": "test-imp-id-1", + "zoneid": 123456, + "networkid": 78910, + "sizes": [ + "300x250" + ] + }, + { + "slotid": "00000000-0000-0000-00000000", + "impid": "test-imp-id-2", + "zoneid": 7891011, + "networkid": 78910, + "sizes": [ + "300x250" + ] + }, + { + "slotid": "00000000-0000-0000-00000000", + "impid": "test-imp-id-3", + "zoneid": 121314, + "networkid": 78910, + "sizes": [ + "300x250" + ] + } + ], + "eids": [ + { + "source": "criteo.com", + "uids": [ + { + "id": "criteo-eid" + } + ] + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "slots": [ + { + "id": "test-slot-id-1", + "impid": "test-imp-id-1", + "zoneid": 123456, + "networkid": 78910, + "cpm": 0.1, + "currency": "USD", + "width": 300, + "height": 250, + "creativeid": "creative-123", + "creative": "" + }, + { + "id": "test-slot-id-2", + "impid": "test-imp-id-2", + "zoneid": 7891011, + "networkid": 78910, + "cpm": 0.2, + "currency": "USD", + "width": 320, + "height": 50, + "creativeid": "creative-123", + "creative": "" + }, + { + "id": "test-slot-id-3", + "impid": "test-imp-id-3", + "zoneid": 121314, + "networkid": 78910, + "cpm": 0.3, + "currency": "USD", + "width": 300, + "height": 600, + "creativeid": "creative-123", + "creative": "" + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-slot-id-1", + "impid": "test-imp-id-1", + "price": 0.1, + "crid": "creative-123", + "adm": "", + "w": 300, + "h": 250 + }, + "type": "banner" + }, + { + "bid": { + "id": "test-slot-id-2", + "impid": "test-imp-id-2", + "price": 0.2, + "crid": "creative-123", + "adm": "", + "w": 320, + "h": 50 + }, + "type": "banner" + }, + { + "bid": { + "id": "test-slot-id-3", + "impid": "test-imp-id-3", + "price": 0.3, + "crid": "creative-123", + "adm": "", + "w": 300, + "h": 600 + }, + "type": "banner" + } + ] + } + ] + } diff --git a/adapters/criteo/params_test.go b/adapters/criteo/params_test.go index 73ace617b2d..9c836769aca 100644 --- a/adapters/criteo/params_test.go +++ b/adapters/criteo/params_test.go @@ -41,9 +41,13 @@ func TestInvalidParams(t *testing.T) { var validParams = []string{ `{"zoneid": 123456}`, + `{"zoneId": 123456}`, `{"networkid": 78910}`, + `{"networkId": 78910}`, `{"zoneid": 123456, "networkid": 78910}`, + `{"zoneId": 123456, "networkId": 78910}`, `{"zoneid": 0, "networkid": 0}`, + `{"zoneId": 0, "networkId": 0}`, } var invalidParams = []string{ @@ -55,8 +59,11 @@ var invalidParams = []string{ `[]`, `{}`, `{"zoneid": -123}`, + `{"zoneId": -123}`, `{"networkid": -321}`, + `{"networkId": -321}`, `{"zoneid": -123, "networkid": -321}`, + `{"zoneId": -123, "networkId": -321}`, `{"zoneid": -1}`, `{"networkid": -1}`, `{"zoneid": -1, "networkid": -1}`, diff --git a/static/bidder-params/criteo.json b/static/bidder-params/criteo.json index 9d348a7eded..88c6fba5d3a 100644 --- a/static/bidder-params/criteo.json +++ b/static/bidder-params/criteo.json @@ -1,30 +1,50 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Criteo adapter params", - "description": "The schema to validate Criteo specific params accepted by Criteo adapter", - "type": "object", - "properties": { - "zoneid": { - "type": "number", - "description": "Impression's zone ID.", - "minimum": 0 - }, - "networkid": { - "type": "number", - "description": "Impression's network ID.", - "minimum": 0 - } - }, - "anyOf": [ - { - "required": [ - "zoneid" - ] - }, - { - "required": [ - "networkid" - ] - } - ] +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Criteo adapter params", + "description": "The schema to validate Criteo specific params accepted by Criteo adapter", + "type": "object", + "properties": { + "zoneid": { + "type": "integer", + "description": "Impression's zone ID.", + "minimum": 0 + }, + "zoneId": { + "type": "integer", + "description": "Impression's zone ID, preferred.", + "minimum": 0 + }, + "networkid": { + "type": "integer", + "description": "Impression's network ID.", + "minimum": 0 + }, + "networkId": { + "type": "integer", + "description": "Impression's network ID, preferred.", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "zoneid" + ] + }, + { + "required": [ + "zoneId" + ] + }, + { + "required": [ + "networkid" + ] + }, + { + "required": [ + "networkId" + ] + } + ] } \ No newline at end of file From 486926600eeb669f69a1b753e583cca311e873d0 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Thu, 10 Jun 2021 10:17:18 -0700 Subject: [PATCH 11/16] Unit test random map order fix (#1887) Co-authored-by: Veronika Solovei --- exchange/exchange_test.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index a03c4786b79..d3bcf082cf2 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -2623,26 +2623,42 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) totalNumberOfbids := 0 + //due to random map order we need to identify what bidder was first + firstBidderIndicator := true + if bidsFromFirstBidder.bids != nil { totalNumberOfbids += len(bidsFromFirstBidder.bids) } if bidsFromSecondBidder.bids != nil { + firstBidderIndicator = false totalNumberOfbids += len(bidsFromSecondBidder.bids) } assert.Equal(t, 2, totalNumberOfbids, "2 bids total should be returned") + assert.Len(t, rejections, 2, "2 bids should be de-duplicated") - assert.Len(t, adapterBids[bidderNameApn1].bids, 0) - assert.Len(t, adapterBids[bidderNameApn2].bids, 2) + if firstBidderIndicator { + assert.Len(t, adapterBids[bidderNameApn1].bids, 2) + assert.Len(t, adapterBids[bidderNameApn2].bids, 0) - assert.Equal(t, "bid_idApn2_1", adapterBids[bidderNameApn2].bids[0].bid.ID, "Incorrect expected bid 1 id") - assert.Equal(t, "bid_idApn2_2", adapterBids[bidderNameApn2].bids[1].bid.ID, "Incorrect expected bid 2 id") + assert.Equal(t, "bid_idApn1_1", adapterBids[bidderNameApn1].bids[0].bid.ID, "Incorrect expected bid 1 id") + assert.Equal(t, "bid_idApn1_2", adapterBids[bidderNameApn1].bids[1].bid.ID, "Incorrect expected bid 2 id") - assert.Len(t, rejections, 2, "2 bids should be de-duplicated") - assert.Equal(t, "bid rejected [bid ID: bid_idApn1_1] reason: Bid was deduplicated", rejections[0], "Incorrect rejected bid 1") - assert.Equal(t, "bid rejected [bid ID: bid_idApn1_2] reason: Bid was deduplicated", rejections[1], "Incorrect rejected bid 2") + assert.Equal(t, "bid rejected [bid ID: bid_idApn2_1] reason: Bid was deduplicated", rejections[0], "Incorrect rejected bid 1") + assert.Equal(t, "bid rejected [bid ID: bid_idApn2_2] reason: Bid was deduplicated", rejections[1], "Incorrect rejected bid 2") + + } else { + assert.Len(t, adapterBids[bidderNameApn1].bids, 0) + assert.Len(t, adapterBids[bidderNameApn2].bids, 2) + + assert.Equal(t, "bid_idApn2_1", adapterBids[bidderNameApn2].bids[0].bid.ID, "Incorrect expected bid 1 id") + assert.Equal(t, "bid_idApn2_2", adapterBids[bidderNameApn2].bids[1].bid.ID, "Incorrect expected bid 2 id") + assert.Equal(t, "bid rejected [bid ID: bid_idApn1_1] reason: Bid was deduplicated", rejections[0], "Incorrect rejected bid 1") + assert.Equal(t, "bid rejected [bid ID: bid_idApn1_2] reason: Bid was deduplicated", rejections[1], "Incorrect rejected bid 2") + + } } func TestRemoveBidById(t *testing.T) { From 1993de4cb8db5537d83ede873b50902e24799808 Mon Sep 17 00:00:00 2001 From: guscarreon Date: Thu, 10 Jun 2021 13:57:51 -0400 Subject: [PATCH 12/16] Request Provided Currency Rates (#1753) --- currency/aggregate_conversions.go | 41 ++ currency/aggregate_conversions_test.go | 89 ++++ currency/constant_rates.go | 4 +- currency/errors.go | 13 + currency/rates.go | 14 +- endpoints/openrtb2/auction.go | 29 ++ endpoints/openrtb2/auction_test.go | 245 ++++++++++- .../no-account/not-required-no-acct.json | 1 + .../with-account/required-with-acct.json | 1 + .../aliased/multiple-alias.json | 1 + .../sample-requests/aliased/simple.json | 25 +- .../errors/conversion-disabled.json | 46 ++ ...rates-currency-missing-usepbs-default.json | 52 +++ ...m-rates-currency-missing-usepbs-false.json | 53 +++ .../custom-rates-empty-usepbs-false.json | 49 +++ .../custom-rates-invalid-usepbs-false.json | 53 +++ .../valid/conversion-disabled.json | 62 +++ ...om-rate-not-found-usepbsrates-default.json | 70 ++++ ...om-rates-override-usepbsrates-default.json | 69 +++ ...stom-rates-override-usepbsrates-false.json | 70 ++++ ...ustom-rates-override-usepbsrates-true.json | 70 ++++ .../valid/reverse-currency-conversion.json | 66 +++ .../valid/server-rates-usepbsrates-true.json | 70 ++++ .../errors/no-conversion-found.json | 38 ++ .../server-rates/valid/simple-conversion.json | 55 +++ .../disabled/good/partial.json | 1 + .../valid-fpd-allowed-with-ext-bidder.json | 3 +- .../valid-fpd-allowed-with-prebid-bidder.json | 3 +- .../valid-native/asset-img-no-hmin.json | 25 +- .../valid-native/asset-img-no-wmin.json | 25 +- .../valid-native/asset-with-id.json | 1 + .../valid-native/asset-with-no-id.json | 1 + .../valid-native/assets-with-unique-ids.json | 1 + .../context-product-compatible-subtype.json | 25 +- .../context-social-compatible-subtype.json | 25 +- .../eventtracker-exchange-specific.json | 3 +- .../valid-native/request-no-context.json | 1 + .../valid-native/request-plcmttype-empty.json | 1 + .../video-asset-event-tracker.json | 1 + .../valid-native/with-video-asset.json | 1 + .../valid-whole/exemplary/all-ext.json | 1 + .../valid-whole/exemplary/prebid-test-ad.json | 1 + .../valid-whole/exemplary/skadn.json | 3 +- errortypes/code.go | 2 + exchange/bidder_test.go | 20 +- exchange/exchange.go | 30 +- exchange/exchange_test.go | 395 +++++++++++++++++- go.mod | 2 +- go.sum | 3 +- openrtb_ext/request.go | 7 + 50 files changed, 1761 insertions(+), 106 deletions(-) create mode 100644 currency/aggregate_conversions.go create mode 100644 currency/aggregate_conversions_test.go create mode 100644 currency/errors.go create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/conversion-disabled.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-default.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-false.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-empty-usepbs-false.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-invalid-usepbs-false.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/conversion-disabled.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rate-not-found-usepbsrates-default.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-default.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-false.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-true.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/reverse-currency-conversion.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/server-rates-usepbsrates-true.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/server-rates/errors/no-conversion-found.json create mode 100644 endpoints/openrtb2/sample-requests/currency-conversion/server-rates/valid/simple-conversion.json diff --git a/currency/aggregate_conversions.go b/currency/aggregate_conversions.go new file mode 100644 index 00000000000..53c5ebff4b6 --- /dev/null +++ b/currency/aggregate_conversions.go @@ -0,0 +1,41 @@ +package currency + +// AggregateConversions contains both the request-defined currency rate +// map found in request.ext.prebid.currency and the currencies conversion +// rates fetched with the RateConverter object defined in rate_converter.go +// It implements the Conversions interface. +type AggregateConversions struct { + customRates, serverRates Conversions +} + +// NewAggregateConversions expects both customRates and pbsRates to not be nil +func NewAggregateConversions(customRates, pbsRates Conversions) *AggregateConversions { + return &AggregateConversions{ + customRates: customRates, + serverRates: pbsRates, + } +} + +// GetRate returns the conversion rate between two currencies prioritizing +// the customRates currency rate over that of the PBS currency rate service +// returns an error if both Conversions objects return error. +func (re *AggregateConversions) GetRate(from string, to string) (float64, error) { + rate, err := re.customRates.GetRate(from, to) + if err == nil { + return rate, nil + } else if _, isMissingRateErr := err.(ConversionRateNotFound); !isMissingRateErr { + // other error, return the error + return 0, err + } + + // because the custom rates' GetRate() call returned an error other than "conversion + // rate not found", there's nothing wrong with the 3 letter currency code so let's + // try the PBS rates instead + return re.serverRates.GetRate(from, to) +} + +// GetRates is not implemented for AggregateConversions . There is no need to call +// this function for this scenario. +func (r *AggregateConversions) GetRates() *map[string]map[string]float64 { + return nil +} diff --git a/currency/aggregate_conversions_test.go b/currency/aggregate_conversions_test.go new file mode 100644 index 00000000000..35ca51a1fe7 --- /dev/null +++ b/currency/aggregate_conversions_test.go @@ -0,0 +1,89 @@ +package currency + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGroupedGetRate(t *testing.T) { + + // Setup: + customRates := NewRates(time.Now(), map[string]map[string]float64{ + "USD": { + "GBP": 3.00, + "EUR": 2.00, + }, + }) + + pbsRates := NewRates(time.Now(), map[string]map[string]float64{ + "USD": { + "GBP": 4.00, + "MXN": 10.00, + }, + }) + aggregateConversions := NewAggregateConversions(customRates, pbsRates) + + // Test cases: + type aTest struct { + desc string + from string + to string + expectedRate float64 + } + + testGroups := []struct { + expectedError error + testCases []aTest + }{ + { + expectedError: nil, + testCases: []aTest{ + {"Found in both, return custom rate", "USD", "GBP", 3.00}, + {"Found in both, return inverse custom rate", "GBP", "USD", 1 / 3.00}, + {"Found in custom rates only", "USD", "EUR", 2.00}, + {"Found in PBS rates only", "USD", "MXN", 10.00}, + {"Found in PBS rates only, return inverse", "MXN", "USD", 1 / 10.00}, + {"Same currency, return unitary rate", "USD", "USD", 1}, + }, + }, + { + expectedError: errors.New("currency: tag is not well-formed"), + testCases: []aTest{ + {"From-currency three-digit code malformed", "XX", "EUR", 0}, + {"To-currency three-digit code malformed", "GBP", "", 0}, + {"Both currencies malformed", "", "", 0}, + }, + }, + { + expectedError: errors.New("currency: tag is not a recognized currency"), + testCases: []aTest{ + {"From-currency three-digit code not found", "FOO", "EUR", 0}, + {"To-currency three-digit code not found", "GBP", "BAR", 0}, + }, + }, + { + expectedError: ConversionRateNotFound{"GBP", "EUR"}, + testCases: []aTest{ + {"Valid three-digit currency codes, but conversion rate not found", "GBP", "EUR", 0}, + }, + }, + } + + for _, group := range testGroups { + for _, tc := range group.testCases { + // Execute: + rate, err := aggregateConversions.GetRate(tc.from, tc.to) + + // Verify: + assert.Equal(t, tc.expectedRate, rate, "conversion rate doesn't match the expected rate: %s\n", tc.desc) + if group.expectedError != nil { + assert.Error(t, err, "error doesn't match expected: %s\n", tc.desc) + } else { + assert.NoError(t, err, "err should be nil: %s\n", tc.desc) + } + } + } +} diff --git a/currency/constant_rates.go b/currency/constant_rates.go index 26471a966a5..dde317d809e 100644 --- a/currency/constant_rates.go +++ b/currency/constant_rates.go @@ -1,8 +1,6 @@ package currency import ( - "fmt" - "golang.org/x/text/currency" ) @@ -29,7 +27,7 @@ func (r *ConstantRates) GetRate(from string, to string) (float64, error) { } if fromUnit.String() != toUnit.String() { - return 0, fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert '%s' => '%s'", fromUnit.String(), toUnit.String()) + return 0, ConversionRateNotFound{fromUnit.String(), toUnit.String()} } return 1, nil diff --git a/currency/errors.go b/currency/errors.go new file mode 100644 index 00000000000..d764c15b984 --- /dev/null +++ b/currency/errors.go @@ -0,0 +1,13 @@ +package currency + +import "fmt" + +// ConversionRateNotFound is thrown by the currency.Conversions GetRate(from string, to string) method +// when the conversion rate between the two currencies, nor its reciprocal, can be found. +type ConversionRateNotFound struct { + FromCur, ToCur string +} + +func (err ConversionRateNotFound) Error() string { + return fmt.Sprintf("Currency conversion rate not found: '%s' => '%s'", err.FromCur, err.ToCur) +} diff --git a/currency/rates.go b/currency/rates.go index a3ae5f30fd5..62914c4b2e2 100644 --- a/currency/rates.go +++ b/currency/rates.go @@ -3,7 +3,6 @@ package currency import ( "encoding/json" "errors" - "fmt" "time" "golang.org/x/text/currency" @@ -45,8 +44,11 @@ func (r *Rates) UnmarshalJSON(b []byte) error { return nil } -// GetRate returns the conversion rate between two currencies -// returns an error in case the conversion rate between the two given currencies is not in the currencies rates map +// GetRate returns the conversion rate between two currencies or: +// - An error if one of the currency strings is not well-formed +// - An error if any of the currency strings is not a recognized currency code. +// - A MissingConversionRate error in case the conversion rate between the two +// given currencies is not in the currencies rates map func (r *Rates) GetRate(from string, to string) (float64, error) { var err error fromUnit, err := currency.ParseISO(from) @@ -63,12 +65,12 @@ func (r *Rates) GetRate(from string, to string) (float64, error) { if r.Conversions != nil { if conversion, present := r.Conversions[fromUnit.String()][toUnit.String()]; present { // In case we have an entry FROM -> TO - return conversion, err + return conversion, nil } else if conversion, present := r.Conversions[toUnit.String()][fromUnit.String()]; present { // In case we have an entry TO -> FROM - return 1 / conversion, err + return 1 / conversion, nil } - return 0, fmt.Errorf("Currency conversion rate not found: '%s' => '%s'", fromUnit.String(), toUnit.String()) + return 0, ConversionRateNotFound{fromUnit.String(), toUnit.String()} } return 0, errors.New("rates are nil") } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 8913e90791d..d8a7fa689b9 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -37,6 +37,7 @@ import ( "github.com/prebid/prebid-server/util/httputil" "github.com/prebid/prebid-server/util/iputil" "golang.org/x/net/publicsuffix" + "golang.org/x/text/currency" ) const storedRequestTimeoutMillis = 50 @@ -343,6 +344,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error { if err := deps.validateEidPermissions(bidExt, aliases); err != nil { return []error{err} } + + if err := validateCustomRates(bidExt.Prebid.CurrencyConversions); err != nil { + return []error{err} + } } if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) { @@ -437,6 +442,30 @@ func validateSChains(req *openrtb_ext.ExtRequest) error { return err } +// validateCustomRates throws a bad input error if any of the 3-digit currency codes found in +// the bidRequest.ext.prebid.currency field is invalid, malfomed or does not represent any actual +// currency. No error is thrown if bidRequest.ext.prebid.currency is invalid or empty. +func validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) error { + if bidReqCurrencyRates == nil { + return nil + } + + for fromCurrency, rates := range bidReqCurrencyRates.ConversionRates { + // Check if fromCurrency is a valid 3-letter currency code + if _, err := currency.ParseISO(fromCurrency); err != nil { + return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", fromCurrency)} + } + + // Check if currencies mapped to fromCurrency are valid 3-letter currency codes + for toCurrency := range rates { + if _, err := currency.ParseISO(toCurrency); err != nil { + return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", toCurrency)} + } + } + } + return nil +} + func (deps *endpointDeps) validateEidPermissions(req *openrtb_ext.ExtRequest, aliases map[string]string) error { if req == nil || req.Prebid.Data == nil { return nil diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 3d40d35b068..bcdac13dc06 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -24,6 +24,7 @@ import ( "github.com/mxmCherry/openrtb/v15/openrtb2" analyticsConf "github.com/prebid/prebid-server/analytics/config" "github.com/prebid/prebid-server/config" + "github.com/prebid/prebid-server/currency" "github.com/prebid/prebid-server/errortypes" "github.com/prebid/prebid-server/exchange" "github.com/prebid/prebid-server/metrics" @@ -45,11 +46,13 @@ type testCase struct { } type testConfigValues struct { - AccountRequired bool `json:"accountRequired"` - AliasJSON string `json:"aliases"` - BlacklistedAccounts []string `json:"blacklistedAccts"` - BlacklistedApps []string `json:"blacklistedApps"` - DisabledAdapters []string `json:"disabledAdapters"` + AccountRequired bool `json:"accountRequired"` + AliasJSON string `json:"aliases"` + BlacklistedAccounts []string `json:"blacklistedAccts"` + BlacklistedApps []string `json:"blacklistedApps"` + DisabledAdapters []string `json:"disabledAdapters"` + CurrencyRates map[string]map[string]float64 `json:"currencyRates"` + MockBidder mockBidExchangeBidder `json:"mockBidder"` } func TestJsonSampleRequests(t *testing.T) { @@ -105,6 +108,22 @@ func TestJsonSampleRequests(t *testing.T) { "Requests with first party data context info found in imp[i].ext.prebid.bidder,context", "first-party-data", }, + { + "Assert we correctly use the server conversion rates when needed", + "currency-conversion/server-rates/valid", + }, + { + "Assert we correctly throw an error when no conversion rate was found in the server conversions map", + "currency-conversion/server-rates/errors", + }, + { + "Assert we correctly use request-defined custom currency rates when present in root.ext", + "currency-conversion/custom-rates/valid", + }, + { + "Assert we correctly validate request-defined custom currency rates when present in root.ext", + "currency-conversion/custom-rates/errors", + }, } for _, test := range testSuites { testCaseFiles, err := getTestFiles(filepath.Join("sample-requests", test.sampleRequestsSubDir)) @@ -248,6 +267,7 @@ func assertBidResponseEqual(t *testing.T, testFile string, expectedBidResponse o 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.Equalf(t, expectedBidResponse.Cur, actualBidResponse.Cur, "BidResponse.Cur 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) @@ -441,8 +461,10 @@ func doRequest(t *testing.T, test testCase) (int, string) { bidderMap := exchange.GetActiveBidders(bidderInfos) disabledBidders := exchange.GetDisabledBiddersErrorMessages(bidderInfos) + mockExchange := newMockBidExchange(test.Config.MockBidder, test.Config.CurrencyRates) + endpoint, _ := NewEndpoint( - &mockBidExchange{}, + mockExchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, @@ -1184,6 +1206,113 @@ func TestContentType(t *testing.T) { } } +func TestValidateCustomRates(t *testing.T) { + boolTrue := true + boolFalse := false + + testCases := []struct { + desc string + inBidReqCurrencies *openrtb_ext.ExtRequestCurrency + outCurrencyError error + }{ + { + desc: "nil input, no errors expected", + inBidReqCurrencies: nil, + outCurrencyError: nil, + }, + { + desc: "empty custom currency rates but UsePBSRates is set to false, we don't return error nor warning", + inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{}, + UsePBSRates: &boolFalse, + }, + outCurrencyError: nil, + }, + { + desc: "empty custom currency rates but UsePBSRates is set to true, no need to return error because we can use PBS rates", + inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{}, + UsePBSRates: &boolTrue, + }, + outCurrencyError: nil, + }, + { + desc: "UsePBSRates is nil and defaults to true, bidExt fromCurrency is invalid, expect bad input error", + inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{ + "FOO": { + "GBP": 1.2, + "MXN": 0.05, + "JPY": 0.95, + }, + }, + }, + outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"}, + }, + { + desc: "UsePBSRates set to false, bidExt fromCurrency is invalid, expect bad input error", + inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{ + "FOO": { + "GBP": 1.2, + "MXN": 0.05, + "JPY": 0.95, + }, + }, + UsePBSRates: &boolFalse, + }, + outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"}, + }, + { + desc: "UsePBSRates set to false, some of the bidExt 'to' Currencies are invalid, expect bad input error when parsing the first invalid currency code", + inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{ + "USD": { + "FOO": 10.0, + "MXN": 0.05, + }, + }, + UsePBSRates: &boolFalse, + }, + outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"}, + }, + { + desc: "UsePBSRates set to false, some of the bidExt 'from' and 'to' currencies are invalid, expect bad input error when parsing the first invalid currency code", + inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{ + "FOO": { + "MXN": 0.05, + "CAD": 0.95, + }, + }, + UsePBSRates: &boolFalse, + }, + outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"}, + }, + { + desc: "All 3-digit currency codes exist, expect no error", + inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: map[string]map[string]float64{ + "USD": { + "MXN": 0.05, + }, + "MXN": { + "JPY": 10.0, + "EUR": 10.95, + }, + }, + UsePBSRates: &boolFalse, + }, + }, + } + + for _, tc := range testCases { + actualErr := validateCustomRates(tc.inBidReqCurrencies) + + assert.Equal(t, tc.outCurrencyError, actualErr, tc.desc) + } +} + func TestValidateImpExt(t *testing.T) { type testCase struct { description string @@ -2590,7 +2719,49 @@ func (e *nobidExchange) HoldAuction(ctx context.Context, r exchange.AuctionReque } type mockBidExchange struct { - gotRequest *openrtb2.BidRequest + mockBidder mockBidExchangeBidder + pbsRates map[string]map[string]float64 +} + +func newMockBidExchange(bidder mockBidExchangeBidder, mockCurrencyConversionRates map[string]map[string]float64) *mockBidExchange { + if bidder.BidCurrency == "" { + bidder.BidCurrency = "USD" + } + + return &mockBidExchange{ + mockBidder: bidder, + pbsRates: mockCurrencyConversionRates, + } +} + +// getAuctionCurrencyRates copies the logic of the exchange package for testing purposes +func (e *mockBidExchange) getAuctionCurrencyRates(customRates *openrtb_ext.ExtRequestCurrency) currency.Conversions { + if customRates == nil { + // The timestamp is required for the function signature, but is not used and its + // value has no significance in the tests + return currency.NewRates(time.Now(), e.pbsRates) + } + + usePbsRates := true + if customRates.UsePBSRates != nil { + usePbsRates = *customRates.UsePBSRates + } + + if !usePbsRates { + // The timestamp is required for the function signature, but is not used and its + // value has no significance in the tests + return currency.NewRates(time.Now(), customRates.ConversionRates) + } + + // Both PBS and custom rates can be used, check if ConversionRates is not empty + if len(customRates.ConversionRates) == 0 { + // Custom rates map is empty, use PBS rates only + return currency.NewRates(time.Now(), e.pbsRates) + } + + // Return an AggregateConversions object that includes both custom and PBS currency rates but will + // prioritize custom rates over PBS rates whenever a currency rate is found in both + return currency.NewAggregateConversions(currency.NewRates(time.Time{}, customRates.ConversionRates), currency.NewRates(time.Now(), e.pbsRates)) } // mockBidExchange is a well-behaved exchange that lists the bidders found in every bidRequest.Imp[i].Ext @@ -2601,6 +2772,36 @@ func (e *mockBidExchange) HoldAuction(ctx context.Context, r exchange.AuctionReq BidID: "test bid id", NBR: openrtb2.NoBidReasonCodeUnknownError.Ptr(), } + + // Use currencies inside r.BidRequest.Cur, if any, and convert currencies if needed + if len(r.BidRequest.Cur) == 0 { + r.BidRequest.Cur = []string{"USD"} + } + + var currencyFrom string = e.mockBidder.getBidCurrency() + var conversionRate float64 = 0.00 + var err error + + var requestExt openrtb_ext.ExtRequest + if len(r.BidRequest.Ext) > 0 { + if err := json.Unmarshal(r.BidRequest.Ext, &requestExt); err != nil { + return nil, fmt.Errorf("request.ext is invalid: %v", err) + } + } + + conversions := e.getAuctionCurrencyRates(requestExt.Prebid.CurrencyConversions) + for _, bidReqCur := range r.BidRequest.Cur { + if conversionRate, err = conversions.GetRate(currencyFrom, bidReqCur); err == nil { + bidResponse.Cur = bidReqCur + break + } + } + + if conversionRate == 0 { + // Can't have bids if there's not even a 1 USD to 1 USD conversion rate + return nil, errors.New("Can't produce bid with no valid currency to use or currency conversion to convert to.") + } + if len(r.BidRequest.Imp) > 0 { var SeatBidMap = make(map[string]openrtb2.SeatBid, 0) for _, imp := range r.BidRequest.Imp { @@ -2625,9 +2826,17 @@ func (e *mockBidExchange) HoldAuction(ctx context.Context, r exchange.AuctionReq for bidderNameOrAlias := range bidderExts { if isBidderToValidate(bidderNameOrAlias) { if val, ok := SeatBidMap[bidderNameOrAlias]; ok { - val.Bid = append(val.Bid, openrtb2.Bid{ID: fmt.Sprintf("%s-bid", bidderNameOrAlias)}) + val.Bid = append(val.Bid, openrtb2.Bid{ID: e.mockBidder.getBidId(bidderNameOrAlias)}) } else { - SeatBidMap[bidderNameOrAlias] = openrtb2.SeatBid{Seat: fmt.Sprintf("%s-bids", bidderNameOrAlias), Bid: []openrtb2.Bid{{ID: fmt.Sprintf("%s-bid", bidderNameOrAlias)}}} + SeatBidMap[bidderNameOrAlias] = openrtb2.SeatBid{ + Seat: e.mockBidder.getSeatName(bidderNameOrAlias), + Bid: []openrtb2.Bid{ + { + ID: e.mockBidder.getBidId(bidderNameOrAlias), + Price: e.mockBidder.getBidPrice() * conversionRate, + }, + }, + } } } } @@ -2640,6 +2849,24 @@ func (e *mockBidExchange) HoldAuction(ctx context.Context, r exchange.AuctionReq return bidResponse, nil } +type mockBidExchangeBidder struct { + BidCurrency string `json:"currency"` + BidPrice float64 `json:"price"` +} + +func (bidder mockBidExchangeBidder) getBidCurrency() string { + return bidder.BidCurrency +} +func (bidder mockBidExchangeBidder) getBidPrice() float64 { + return bidder.BidPrice +} +func (bidder mockBidExchangeBidder) getSeatName(bidderNameOrAlias string) string { + return fmt.Sprintf("%s-bids", bidderNameOrAlias) +} +func (bidder mockBidExchangeBidder) getBidId(bidderNameOrAlias string) string { + return fmt.Sprintf("%s-bid", bidderNameOrAlias) +} + type brokenExchange struct{} func (e *brokenExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) { 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 index c3ab09d4883..75c859d212b 100644 --- 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 @@ -66,6 +66,7 @@ "expectedBidResponse": { "id":"some-request-id", "bidid":"test bid id", + "cur": "USD", "nbr":0, "seatbid": [ { 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 index a72d184c81c..ae930384499 100644 --- 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 @@ -68,6 +68,7 @@ "expectedBidResponse": { "id":"some-request-id", "bidid":"test bid id", + "cur": "USD", "nbr":0, "seatbid": [ { diff --git a/endpoints/openrtb2/sample-requests/aliased/multiple-alias.json b/endpoints/openrtb2/sample-requests/aliased/multiple-alias.json index 55e45041e6e..00906c89772 100644 --- a/endpoints/openrtb2/sample-requests/aliased/multiple-alias.json +++ b/endpoints/openrtb2/sample-requests/aliased/multiple-alias.json @@ -87,6 +87,7 @@ } ], "bidid": "test bid id", + "cur": "USD", "nbr": 0 }, "expectedReturnCode": 200 diff --git a/endpoints/openrtb2/sample-requests/aliased/simple.json b/endpoints/openrtb2/sample-requests/aliased/simple.json index a99907ab370..677d3d8cf53 100644 --- a/endpoints/openrtb2/sample-requests/aliased/simple.json +++ b/endpoints/openrtb2/sample-requests/aliased/simple.json @@ -27,19 +27,20 @@ "expectedBidResponse": { "id":"some-request-id", "bidid":"test bid id", + "cur": "USD", "nbr":0, - "seatbid": [ - { - "bid": [ - { - "id": "alias1-bid", - "impid": "", - "price": 0 - } - ], - "seat": "alias1-bids" - } - ] + "seatbid": [ + { + "bid": [ + { + "id": "alias1-bid", + "impid": "", + "price": 0 + } + ], + "seat": "alias1-bids" + } + ] }, "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/conversion-disabled.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/conversion-disabled.json new file mode 100644 index 00000000000..03877031294 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/conversion-disabled.json @@ -0,0 +1,46 @@ +{ + "description": "request.ext.prebid.currency.rates empty, usepbsrates is false, a conversion is needed but conversions are disabled", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "currency": { + "rates": {}, + "usepbsrates": false + } + } + } + }, + "expectedReturnCode": 500, + "expectedErrorMessage": "Critical error while running the auction: Can't produce bid with no valid currency to use or currency conversion to convert to." +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-default.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-default.json new file mode 100644 index 00000000000..6a727e9615c --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-default.json @@ -0,0 +1,52 @@ +{ + "description": "currency in request.cur cannot be converted because conversion rate not found in either custom currency rates nor server rates. usepbsrates defaults to true", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["GBP"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "MXN": 5.00 + } + } + } + } + } + }, + "expectedReturnCode": 500, + "expectedErrorMessage": "Critical error while running the auction: Can't produce bid with no valid currency to use or currency conversion to convert to." +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-false.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-false.json new file mode 100644 index 00000000000..5549fa9b688 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-currency-missing-usepbs-false.json @@ -0,0 +1,53 @@ +{ + "description": "currency in request.cur cannot be converted because usepbsrates set to false not allowing for PBS to use its rates. Default to price of 0", + "config": { + "currencyRates":{ + "USD": { + "MXN": 5.09 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "JPY": 2.00 + } + }, + "usepbsrates": false + } + } + } + }, + "expectedReturnCode": 500, + "expectedErrorMessage": "Critical error while running the auction: Can't produce bid with no valid currency to use or currency conversion to convert to." +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-empty-usepbs-false.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-empty-usepbs-false.json new file mode 100644 index 00000000000..f4e19f3a4c5 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-empty-usepbs-false.json @@ -0,0 +1,49 @@ +{ + "description": "usepbsrates set to false forces BidRequest to use custom currency rates but bidRequest.ext.prebid.currency.rates field is empty", + "config": { + "currencyRates":{ + "USD": { + "MXN": 5.09 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": {}, + "usepbsrates": false + } + } + } + }, + "expectedReturnCode": 500, + "expectedErrorMessage": "Critical error while running the auction: Can't produce bid with no valid currency to use or currency conversion to convert to." +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-invalid-usepbs-false.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-invalid-usepbs-false.json new file mode 100644 index 00000000000..39857650f12 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/errors/custom-rates-invalid-usepbs-false.json @@ -0,0 +1,53 @@ +{ + "description": "False usepbsrates forces BidRequest use custom currency rates but bidRequest.ext.prebid.currency.rates field comes with invalid currency codes", + "config": { + "currencyRates":{ + "USD": { + "MXN": 5.09 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "FOO": 10.0 + } + }, + "usepbsrates": false + } + } + } + }, + "expectedReturnCode": 400, + "expectedErrorMessage": "Invalid request: currency code " +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/conversion-disabled.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/conversion-disabled.json new file mode 100644 index 00000000000..0741ea4d315 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/conversion-disabled.json @@ -0,0 +1,62 @@ +{ + "description": "request.ext.prebid.currency.rates empty, usepbsrates set to false, request succeeded because no conversion was needed", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": {}, + "usepbsrates": false + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "USD", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 1.00 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rate-not-found-usepbsrates-default.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rate-not-found-usepbsrates-default.json new file mode 100644 index 00000000000..fb65a852355 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rate-not-found-usepbsrates-default.json @@ -0,0 +1,70 @@ +{ + "description": "request comes with custom rates but request.cur currency is only found in the server rates. Error wasn't thrown because usepbsrates defaults to true", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "JPY": 15.00, + "EUR": 0.85 + } + } + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "MXN", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 2.00 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-default.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-default.json new file mode 100644 index 00000000000..80790a52543 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-default.json @@ -0,0 +1,69 @@ +{ + "description": "Despite being more expensive, custom conversion rate overrides that of the currency conversion service. usepbsrates defaults to true", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "MXN": 5.00 + } + } + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "MXN", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 5.00 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-false.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-false.json new file mode 100644 index 00000000000..ef372c1cf66 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-false.json @@ -0,0 +1,70 @@ +{ + "description": "request.ext.prebid.currency substitutes those of the currency conversion server because usepbsrates is false", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "MXN": 5.00 + } + }, + "usepbsrates": false + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "MXN", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 5.00 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-true.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-true.json new file mode 100644 index 00000000000..276e8da43c2 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/custom-rates-override-usepbsrates-true.json @@ -0,0 +1,70 @@ +{ + "description": "Despite being more expensive, custom conversion rate overrides that of the currency conversion service. usepbsrates was true", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "MXN": 5.00 + } + }, + "usepbsrates": true + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "MXN", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 5.00 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/reverse-currency-conversion.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/reverse-currency-conversion.json new file mode 100644 index 00000000000..624f0784dac --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/reverse-currency-conversion.json @@ -0,0 +1,66 @@ +{ + "description": "USD BidRequest gets converted because mockbidder bids in foreign currency, custom conversion rate is used", + "config": { + "currencyRates":{ + "USD": { + "MXN": 8.00 + } + }, + "mockBidder": { + "currency": "MXN", + "price": 20.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "USD": { + "MXN": 10.00 + } + }, + "usepbsrates": true + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "nbr":0, + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 2 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/server-rates-usepbsrates-true.json b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/server-rates-usepbsrates-true.json new file mode 100644 index 00000000000..929c2e0cbd5 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/custom-rates/valid/server-rates-usepbsrates-true.json @@ -0,0 +1,70 @@ +{ + "description": "Despite being more expensive, custom conversion rate overrides that of the currency conversion service. usepbsrates was true", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "cur": ["MXN"], + "ext": { + "prebid": { + "aliases": { + "unknown": "appnexus" + }, + "currency": { + "rates": { + "USD": { + "CAD": 5.00 + } + }, + "usepbsrates": true + } + } + } + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "MXN", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 2.00 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/server-rates/errors/no-conversion-found.json b/endpoints/openrtb2/sample-requests/currency-conversion/server-rates/errors/no-conversion-found.json new file mode 100644 index 00000000000..dc0d7ce6042 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/server-rates/errors/no-conversion-found.json @@ -0,0 +1,38 @@ +{ + "description": "bid request calls for a bid in foreign currency MXN but conversion rate is not found in the currency conversion service.", + "config": { + "currencyRates":{ + "USD": { + "GBP": 0.80 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "cur": ["MXN"], + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedReturnCode": 500, + "expectedErrorMessage": "Critical error while running the auction: Can't produce bid with no valid currency to use or currency conversion to convert to." +} diff --git a/endpoints/openrtb2/sample-requests/currency-conversion/server-rates/valid/simple-conversion.json b/endpoints/openrtb2/sample-requests/currency-conversion/server-rates/valid/simple-conversion.json new file mode 100644 index 00000000000..84788d5ada1 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/currency-conversion/server-rates/valid/simple-conversion.json @@ -0,0 +1,55 @@ +{ + "description": "bid request calls for a bid in foreign currency but mockbidder bids in USD. Conversion rate is applied", + "config": { + "currencyRates":{ + "USD": { + "MXN": 2.00 + } + }, + "mockBidder": { + "currency": "USD", + "price": 1.00 + } + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "cur": ["MXN"], + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ] + }, + "expectedBidResponse": { + "id":"some-request-id", + "bidid":"test bid id", + "cur": "MXN", + "nbr":0, + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 2.00 + } + ], + "seat": "appnexus-bids" + } + ] + }, + "expectedReturnCode": 200 +} diff --git a/endpoints/openrtb2/sample-requests/disabled/good/partial.json b/endpoints/openrtb2/sample-requests/disabled/good/partial.json index 3549abaa934..735e7c5ede1 100644 --- a/endpoints/openrtb2/sample-requests/disabled/good/partial.json +++ b/endpoints/openrtb2/sample-requests/disabled/good/partial.json @@ -58,6 +58,7 @@ "expectedBidResponse": { "id":"some-request-id", "bidid":"test bid id", + "cur": "USD", "nbr":0, "seatbid": [ { diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-ext-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-ext-bidder.json index c36ae0cd41d..a4b716b2040 100644 --- a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-ext-bidder.json +++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-ext-bidder.json @@ -43,7 +43,8 @@ "seat": "appnexus-bids" }], "bidid": "test bid id", + "cur": "USD", "nbr": 0 }, "expectedReturnCode": 200 -} \ No newline at end of file +} diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-prebid-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-prebid-bidder.json index ad6298db39a..27e8c46d9d7 100644 --- a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-prebid-bidder.json +++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-prebid-bidder.json @@ -47,7 +47,8 @@ "seat": "appnexus-bids" }], "bidid": "test bid id", + "cur": "USD", "nbr": 0 }, "expectedReturnCode": 200 -} \ No newline at end of file +} 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 15af8551da6..e556b15d4f2 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 @@ -23,19 +23,20 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur":"USD", "nbr": 0, - "seatbid": [ - { - "bid": [ - { - "id": "appnexus-bid", - "impid": "", - "price": 0 - } - ], - "seat": "appnexus-bids" - } - ] + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] }, "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 5d986bcf755..06673bcdf32 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 @@ -23,19 +23,20 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur":"USD", "nbr": 0, - "seatbid": [ - { - "bid": [ - { - "id": "appnexus-bid", - "impid": "", - "price": 0 - } - ], - "seat": "appnexus-bids" - } - ] + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] }, "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 1e55cdda63f..9b8763491a3 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json +++ b/endpoints/openrtb2/sample-requests/valid-native/asset-with-id.json @@ -23,6 +23,7 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, "seatbid": [ { 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 36a1745cb19..22ffc7f50d8 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 @@ -23,6 +23,7 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, "seatbid": [ { 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 98cdeedadbe..e60e2028637 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 @@ -23,6 +23,7 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, "seatbid": [ { diff --git a/endpoints/openrtb2/sample-requests/valid-native/context-product-compatible-subtype.json b/endpoints/openrtb2/sample-requests/valid-native/context-product-compatible-subtype.json index dbf7b9c5e0d..a3b7101d8d5 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/context-product-compatible-subtype.json +++ b/endpoints/openrtb2/sample-requests/valid-native/context-product-compatible-subtype.json @@ -23,19 +23,20 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, - "seatbid": [ - { - "bid": [ - { - "id": "appnexus-bid", - "impid": "", - "price": 0 - } - ], - "seat": "appnexus-bids" - } - ] + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] }, "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-native/context-social-compatible-subtype.json b/endpoints/openrtb2/sample-requests/valid-native/context-social-compatible-subtype.json index 41fb833d770..77e8ce10a41 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/context-social-compatible-subtype.json +++ b/endpoints/openrtb2/sample-requests/valid-native/context-social-compatible-subtype.json @@ -23,19 +23,20 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, - "seatbid": [ - { - "bid": [ - { - "id": "appnexus-bid", - "impid": "", - "price": 0 - } - ], - "seat": "appnexus-bids" - } - ] + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "", + "price": 0 + } + ], + "seat": "appnexus-bids" + } + ] }, "expectedReturnCode": 200 } diff --git a/endpoints/openrtb2/sample-requests/valid-native/eventtracker-exchange-specific.json b/endpoints/openrtb2/sample-requests/valid-native/eventtracker-exchange-specific.json index 501e7ef5016..214031177ca 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/eventtracker-exchange-specific.json +++ b/endpoints/openrtb2/sample-requests/valid-native/eventtracker-exchange-specific.json @@ -22,6 +22,7 @@ "id": "req-id", "bidid": "test bid id", "nbr": 0, + "cur": "USD", "seatbid": [{ "bid": [{ "id": "appnexus-bid", @@ -32,4 +33,4 @@ }] }, "expectedReturnCode": 200 -} \ No newline at end of file +} 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 1ad97c8ff8f..5ebc4e697e4 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json +++ b/endpoints/openrtb2/sample-requests/valid-native/request-no-context.json @@ -23,6 +23,7 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, "seatbid": [ { 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 88af803684d..5518b7a06bc 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json +++ b/endpoints/openrtb2/sample-requests/valid-native/request-plcmttype-empty.json @@ -23,6 +23,7 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, "seatbid": [ { 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 index ab192e14881..fcc7b72d62a 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/video-asset-event-tracker.json +++ b/endpoints/openrtb2/sample-requests/valid-native/video-asset-event-tracker.json @@ -23,6 +23,7 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, "seatbid": [ { diff --git a/endpoints/openrtb2/sample-requests/valid-native/with-video-asset.json b/endpoints/openrtb2/sample-requests/valid-native/with-video-asset.json index 0ec3c993251..f920c52a591 100644 --- a/endpoints/openrtb2/sample-requests/valid-native/with-video-asset.json +++ b/endpoints/openrtb2/sample-requests/valid-native/with-video-asset.json @@ -23,6 +23,7 @@ "expectedBidResponse": { "id": "req-id", "bidid": "test bid id", + "cur": "USD", "nbr": 0, "seatbid": [ { 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 f875fa880bc..46af51635f9 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json @@ -115,6 +115,7 @@ } ], "bidid":"test bid id", + "cur":"USD", "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 2c6a34f569e..d592cb66fcb 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 @@ -44,6 +44,7 @@ } ], "bidid": "test bid id", + "cur":"USD", "nbr": 0 }, "expectedReturnCode": 200 diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/skadn.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/skadn.json index e238f3c07c7..cb2cec992fe 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/skadn.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/skadn.json @@ -42,7 +42,8 @@ "seat": "appnexus-bids" }], "bidid": "test bid id", + "cur":"USD", "nbr": 0 }, "expectedReturnCode": 200 -} \ No newline at end of file +} diff --git a/errortypes/code.go b/errortypes/code.go index 2749b978006..869e7d541a4 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -11,6 +11,7 @@ const ( BidderTemporarilyDisabledErrorCode BlacklistedAcctErrorCode AcctRequiredErrorCode + NoConversionRateErrorCode ) // Defines numeric codes for well-known warnings. @@ -19,6 +20,7 @@ const ( InvalidPrivacyConsentWarningCode = iota + 10000 AccountLevelDebugDisabledWarningCode BidderLevelDebugDisabledWarningCode + DisabledCurrencyConversionWarningCode ) // Coder provides an error or warning code with severity. diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 3140930d8e6..5fdfe445206 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -564,7 +564,7 @@ func TestMultiCurrencies(t *testing.T) { {currency: "USD", price: 1.3 * 1.3050530256}, }, expectedBadCurrencyErrors: []error{ - errors.New("Currency conversion rate not found: 'JPY' => 'USD'"), + currency.ConversionRateNotFound{"JPY", "USD"}, }, description: "Case 6 - Bidder respond with a mix of currencies and one unknown on all HTTP responses", }, @@ -587,9 +587,9 @@ func TestMultiCurrencies(t *testing.T) { }, expectedBids: []bid{}, expectedBadCurrencyErrors: []error{ - errors.New("Currency conversion rate not found: 'JPY' => 'USD'"), - errors.New("Currency conversion rate not found: 'BZD' => 'USD'"), - errors.New("Currency conversion rate not found: 'DKK' => 'USD'"), + currency.ConversionRateNotFound{"JPY", "USD"}, + currency.ConversionRateNotFound{"BZD", "USD"}, + currency.ConversionRateNotFound{"DKK", "USD"}, }, description: "Case 7 - Bidder respond with currencies not having any rate on all HTTP responses", }, @@ -719,9 +719,9 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { bidCurrency: []string{"EUR", "EUR", "EUR"}, expectedBidsCount: 0, expectedBadCurrencyErrors: []error{ - fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert 'EUR' => 'USD'"), - fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert 'EUR' => 'USD'"), - fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert 'EUR' => 'USD'"), + currency.ConversionRateNotFound{"EUR", "USD"}, + currency.ConversionRateNotFound{"EUR", "USD"}, + currency.ConversionRateNotFound{"EUR", "USD"}, }, description: "Case 2 - Bidder respond with the same currency (not default one) on all HTTP responses", }, @@ -753,7 +753,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { bidCurrency: []string{"EUR", "", "USD"}, expectedBidsCount: 2, expectedBadCurrencyErrors: []error{ - fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert 'EUR' => 'USD'"), + currency.ConversionRateNotFound{"EUR", "USD"}, }, description: "Case 7 - Bidder responds with a mix of not set, non default currency and default currency in HTTP responses", }, @@ -761,7 +761,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { bidCurrency: []string{"GBP", "", "USD"}, expectedBidsCount: 2, expectedBadCurrencyErrors: []error{ - fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert 'GBP' => 'USD'"), + currency.ConversionRateNotFound{"GBP", "USD"}, }, description: "Case 8 - Bidder responds with a mix of not set, non default currency and default currency in HTTP responses", }, @@ -769,7 +769,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { bidCurrency: []string{"GBP", "", ""}, expectedBidsCount: 2, expectedBadCurrencyErrors: []error{ - fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert 'GBP' => 'USD'"), + currency.ConversionRateNotFound{"GBP", "USD"}, }, description: "Case 9 - Bidder responds with a mix of not set and empty currencies (default currency) in HTTP responses", }, diff --git a/exchange/exchange.go b/exchange/exchange.go index ba70305f660..c1602aadfcb 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -202,7 +202,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * defer cancel() // Get currency rates conversions for the auction - conversions := e.currencyConverter.Rates() + conversions := e.getAuctionCurrencyRates(requestExt.Prebid.CurrencyConversions) adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, r.Account.DebugAllow, r.GlobalPrivacyControlHeader) @@ -972,6 +972,34 @@ func (e *exchange) getBidCacheInfo(bid *pbsOrtbBid, auction *auction) (cacheInfo return } +func (e *exchange) getAuctionCurrencyRates(requestRates *openrtb_ext.ExtRequestCurrency) currency.Conversions { + if requestRates == nil { + // No bidRequest.ext.currency field was found, use PBS rates as usual + return e.currencyConverter.Rates() + } + + // If bidRequest.ext.currency.usepbsrates is nil, we understand its value as true. It will be false + // only if it's explicitly set to false + usePbsRates := requestRates.UsePBSRates == nil || *requestRates.UsePBSRates + + if !usePbsRates { + // At this point, we can safely assume the ConversionRates map is not empty because + // validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) would have + // thrown an error under such conditions. + return currency.NewRates(time.Time{}, requestRates.ConversionRates) + } + + // Both PBS and custom rates can be used, check if ConversionRates is not empty + if len(requestRates.ConversionRates) == 0 { + // Custom rates map is empty, use PBS rates only + return e.currencyConverter.Rates() + } + + // Return an AggregateConversions object that includes both custom and PBS currency rates but will + // prioritize custom rates over PBS rates whenever a currency rate is found in both + return currency.NewAggregateConversions(currency.NewRates(time.Time{}, requestRates.ConversionRates), e.currencyConverter.Rates()) +} + func findCacheID(bid *pbsOrtbBid, auction *auction) (string, bool) { if bid != nil && bid.bid != nil && auction != nil { if id, found := auction.cacheIds[bid.bid]; found { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index d3bcf082cf2..f778e3ba411 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -86,8 +86,8 @@ func TestNewExchange(t *testing.T) { // 4) Build a BidResponse struct using exchange.buildBidResponse(ctx.Background(), liveA... ) // 5) Assert we have no '&' characters in the response that exchange.buildBidResponse returns func TestCharacterEscape(t *testing.T) { - /* 1) Adapter with a '& char in its endpoint property */ - /* https://github.com/prebid/prebid-server/issues/465 */ + // 1) Adapter with a '& char in its endpoint property + // https://github.com/prebid/prebid-server/issues/465 cfg := &config.Configuration{ Adapters: make(map[string]config.Adapter, 1), } @@ -95,7 +95,7 @@ func TestCharacterEscape(t *testing.T) { Endpoint: "http://ib.adnxs.com/openrtb2?query1&query2", //Note the '&' character in there } - /* 2) Init new exchange with said configuration */ + // 2) Init new exchange with said configuration //Other parameters also needed to create exchange handlerNoBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) @@ -114,7 +114,7 @@ func TestCharacterEscape(t *testing.T) { currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e := NewExchange(adapters, nil, cfg, &metricsConf.DummyMetricsEngine{}, biddersInfo, gdpr.AlwaysAllow{}, currencyConverter, nilCategoryFetcher{}).(*exchange) - /* 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */ + // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs //liveAdapters []openrtb_ext.BidderName, liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" @@ -147,10 +147,10 @@ func TestCharacterEscape(t *testing.T) { var errList []error - /* 4) Build bid response */ + // 4) Build bid response 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 */ + // 5) Assert we have no errors and one '&' character as we are supposed to if err != nil { t.Errorf("exchange.buildBidResponse returned unexpected error: %v", err) } @@ -507,6 +507,366 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) { } +func TestOverrideWithCustomCurrency(t *testing.T) { + + mockCurrencyClient := &mockCurrencyRatesClient{ + responseBody: `{"dataAsOf":"2018-09-12","conversions":{"USD":{"MXN":10.00}}}`, + } + mockCurrencyConverter := currency.NewRateConverter( + mockCurrencyClient, + "currency.fake.com", + 24*time.Hour, + ) + + type testIn struct { + customCurrencyRates json.RawMessage + bidRequestCurrency string + } + type testResults struct { + numBids int + bidRespPrice float64 + bidRespCurrency string + } + + testCases := []struct { + desc string + in testIn + expected testResults + }{ + { + desc: "Blank currency field in ext. bidRequest comes with a valid currency but conversion rate was not found in PBS. Return no bids", + in: testIn{ + customCurrencyRates: json.RawMessage(`{ "prebid": { "currency": {} } } `), + bidRequestCurrency: "GBP", + }, + expected: testResults{}, + }, + { + desc: "valid request.ext.prebid.currency, expect custom rates to override those of the currency rate server", + in: testIn{ + customCurrencyRates: json.RawMessage(`{ + "prebid": { + "currency": { + "rates": { + "USD": { + "MXN": 20.00, + "EUR": 10.95 + } + } + } + } + }`), + bidRequestCurrency: "MXN", + }, + expected: testResults{ + numBids: 1, + bidRespPrice: 20.00, + bidRespCurrency: "MXN", + }, + }, + } + + // Init mock currency conversion service + mockCurrencyConverter.Run() + + // Init an exchange to run an auction from + noBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } + mockAppnexusBidService := httptest.NewServer(http.HandlerFunc(noBidServer)) + defer mockAppnexusBidService.Close() + + categoriesFetcher, error := newCategoryFetcher("./test/category-mapping") + if error != nil { + t.Errorf("Failed to create a category Fetcher: %v", error) + } + + oneDollarBidBidder := &goodSingleBidder{ + httpRequest: &adapters.RequestData{ + Method: "POST", + Uri: mockAppnexusBidService.URL, + Body: []byte("{\"key\":\"val\"}"), + Headers: http.Header{}, + }, + } + + e := new(exchange) + e.cache = &wellBehavedCache{} + e.me = &metricsConf.DummyMetricsEngine{} + e.gDPR = gdpr.AlwaysAllow{} + e.currencyConverter = mockCurrencyConverter + e.categoriesFetcher = categoriesFetcher + e.bidIDGenerator = &mockBidIDGenerator{false, false} + + // Define mock incoming bid requeset + mockBidRequest := &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + }}, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + } + + // Run tests + for _, test := range testCases { + + oneDollarBidBidder.bidResponse = &adapters.BidderResponse{ + Bids: []*adapters.TypedBid{ + { + Bid: &openrtb2.Bid{Price: 1.00}, + }, + }, + Currency: "USD", + } + + e.adapterMap = map[openrtb_ext.BidderName]adaptedBidder{ + openrtb_ext.BidderAppnexus: adaptBidder(oneDollarBidBidder, mockAppnexusBidService.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, nil), + } + + // Set custom rates in extension + mockBidRequest.Ext = test.in.customCurrencyRates + + // Set bidRequest currency list + mockBidRequest.Cur = []string{test.in.bidRequestCurrency} + + auctionRequest := AuctionRequest{ + BidRequest: mockBidRequest, + Account: config.Account{}, + UserSyncs: &emptyUsersync{}, + } + + // Run test + outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{}) + + // Assertions + assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) + + if test.expected.numBids > 0 { + // Assert out currency + assert.Equal(t, test.expected.bidRespCurrency, outBidResponse.Cur, "Bid response currency is wrong: %s \n", test.desc) + + // Assert returned bid + if !assert.NotNil(t, outBidResponse, "outBidResponse is nil: %s \n", test.desc) { + return + } + if !assert.NotEmpty(t, outBidResponse.SeatBid, "outBidResponse.SeatBid is empty: %s", test.desc) { + return + } + if !assert.NotEmpty(t, outBidResponse.SeatBid[0].Bid, "outBidResponse.SeatBid[0].Bid is empty: %s", test.desc) { + return + } + + // Assert returned bid price matches the currency conversion + assert.Equal(t, test.expected.bidRespPrice, outBidResponse.SeatBid[0].Bid[0].Price, "Bid response seatBid price is wrong: %s", test.desc) + } else { + assert.Len(t, outBidResponse.SeatBid, 0, "outBidResponse.SeatBid should be empty: %s", test.desc) + } + } +} + +func TestGetAuctionCurrencyRates(t *testing.T) { + + pbsRates := map[string]map[string]float64{ + "MXN": { + "USD": 20.13, + "EUR": 27.82, + "JPY": 5.09, // "MXN" to "JPY" rate not found in customRates + }, + } + + customRates := map[string]map[string]float64{ + "MXN": { + "USD": 25.00, // different rate than in pbsRates + "EUR": 27.82, // same as in pbsRates + "GBP": 31.12, // not found in pbsRates at all + }, + } + + expectedRateEngineRates := map[string]map[string]float64{ + "MXN": { + "USD": 25.00, // rates engine will prioritize the value found in custom rates + "EUR": 27.82, // same value in both the engine reads the custom entry first + "JPY": 5.09, // the engine will find it in the pbsRates conversions + "GBP": 31.12, // the engine will find it in the custom conversions + }, + } + + boolTrue := true + boolFalse := false + + type testInput struct { + pbsRates map[string]map[string]float64 + bidExtCurrency *openrtb_ext.ExtRequestCurrency + } + type testOutput struct { + constantRates bool + resultingRates map[string]map[string]float64 + } + testCases := []struct { + desc string + given testInput + expected testOutput + }{ + { + "valid pbsRates, valid ConversionRates, false UsePBSRates. Resulting rates identical to customRates", + testInput{ + pbsRates: pbsRates, + bidExtCurrency: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: customRates, + UsePBSRates: &boolFalse, + }, + }, + testOutput{ + resultingRates: customRates, + }, + }, + { + "valid pbsRates, valid ConversionRates, true UsePBSRates. Resulting rates are a mix but customRates gets priority", + testInput{ + pbsRates: pbsRates, + bidExtCurrency: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: customRates, + UsePBSRates: &boolTrue, + }, + }, + testOutput{ + resultingRates: expectedRateEngineRates, + }, + }, + { + "nil pbsRates, valid ConversionRates, false UsePBSRates. Resulting rates identical to customRates", + testInput{ + pbsRates: nil, + bidExtCurrency: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: customRates, + UsePBSRates: &boolFalse, + }, + }, + testOutput{ + resultingRates: customRates, + }, + }, + { + "nil pbsRates, valid ConversionRates, true UsePBSRates. Resulting rates identical to customRates", + testInput{ + pbsRates: nil, + bidExtCurrency: &openrtb_ext.ExtRequestCurrency{ + ConversionRates: customRates, + UsePBSRates: &boolTrue, + }, + }, + testOutput{ + resultingRates: customRates, + }, + }, + { + "valid pbsRates, empty ConversionRates, false UsePBSRates. Because pbsRates cannot be used, default to constant rates", + testInput{ + pbsRates: pbsRates, + bidExtCurrency: &openrtb_ext.ExtRequestCurrency{ + // ConversionRates inCustomRates not initialized makes for a zero-length map + UsePBSRates: &boolFalse, + }, + }, + testOutput{ + constantRates: true, + }, + }, + { + "valid pbsRates, nil ConversionRates, UsePBSRates defaults to true. Resulting rates will be identical to pbsRates", + testInput{ + pbsRates: pbsRates, + bidExtCurrency: nil, + }, + testOutput{ + resultingRates: pbsRates, + }, + }, + { + "nil pbsRates, empty ConversionRates, false UsePBSRates. Default to constant rates", + testInput{ + pbsRates: nil, + bidExtCurrency: &openrtb_ext.ExtRequestCurrency{ + // ConversionRates inCustomRates not initialized makes for a zero-length map + UsePBSRates: &boolFalse, + }, + }, + testOutput{ + constantRates: true, + }, + }, + { + "customRates empty, UsePBSRates set to true, pbsRates are nil. Return default constant rates converter", + testInput{ + pbsRates: nil, + bidExtCurrency: &openrtb_ext.ExtRequestCurrency{ + // ConversionRates inCustomRates not initialized makes for a zero-length map + UsePBSRates: &boolTrue, + }, + }, + testOutput{ + constantRates: true, + }, + }, + { + "nil customRates, nil pbsRates, UsePBSRates defaults to true. Return default constant rates converter", + testInput{ + pbsRates: nil, + bidExtCurrency: nil, + }, + testOutput{ + constantRates: true, + }, + }, + } + + for _, tc := range testCases { + + // Test setup: + jsonPbsRates, err := json.Marshal(tc.given.pbsRates) + if err != nil { + t.Fatalf("Failed to marshal PBS rates: %v", err) + } + + // Init mock currency conversion service + mockCurrencyClient := &mockCurrencyRatesClient{ + responseBody: `{"dataAsOf":"2018-09-12","conversions":` + string(jsonPbsRates) + `}`, + } + mockCurrencyConverter := currency.NewRateConverter( + mockCurrencyClient, + "currency.fake.com", + 24*time.Hour, + ) + mockCurrencyConverter.Run() + + e := new(exchange) + e.currencyConverter = mockCurrencyConverter + + // Run test + auctionRates := e.getAuctionCurrencyRates(tc.given.bidExtCurrency) + + // When fromCurrency and toCurrency are the same, a rate of 1.00 is always expected + rate, err := auctionRates.GetRate("USD", "USD") + assert.NoError(t, err, tc.desc) + assert.Equal(t, float64(1), rate, tc.desc) + + // If we expect an error, assert we have one along with a conversion rate of zero + if tc.expected.constantRates { + rate, err := auctionRates.GetRate("USD", "MXN") + assert.Error(t, err, tc.desc) + assert.Equal(t, float64(0), rate, tc.desc) + } else { + for fromCurrency, rates := range tc.expected.resultingRates { + for toCurrency, expectedRate := range rates { + actualRate, err := auctionRates.GetRate(fromCurrency, toCurrency) + assert.NoError(t, err, tc.desc) + assert.Equal(t, expectedRate, actualRate, tc.desc) + } + } + } + } +} + func TestReturnCreativeEndToEnd(t *testing.T) { sampleAd := "" @@ -709,7 +1069,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { testExternalCacheHost := "www.externalprebidcache.net" testExternalCachePath := "endpoints/cache" - /* 1) An adapter */ + // 1) An adapter bidderName := openrtb_ext.BidderName("appnexus") cfg := &config.Configuration{ @@ -730,7 +1090,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { adapterList := make([]openrtb_ext.BidderName, 0, 2) testEngine := metricsConf.NewMetricsEngine(cfg, adapterList) - /* 2) Init new exchange with said configuration */ + // 2) Init new exchange with said configuration handlerNoBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) } server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer)) defer server.Close() @@ -747,7 +1107,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) pbc := pbc.NewClient(&http.Client{}, &cfg.CacheURL, &cfg.ExtCacheURL, testEngine) e := NewExchange(adapters, pbc, cfg, &metricsConf.DummyMetricsEngine{}, biddersInfo, gdpr.AlwaysAllow{}, currencyConverter, nilCategoryFetcher{}).(*exchange) - /* 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs */ + // 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs liveAdapters := []openrtb_ext.BidderName{bidderName} //adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, @@ -849,10 +1209,10 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { var errList []error - /* 4) Build bid response */ + // 4) Build bid response 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*/ + // 5) Assert we have no errors and the bid response we expected assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error") expectedBidResponse := &openrtb2.BidResponse{ @@ -3251,3 +3611,16 @@ type nilCategoryFetcher struct{} func (nilCategoryFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { return "", nil } + +// mockCurrencyRatesClient is a simple http client mock returning a constant response body +type mockCurrencyRatesClient struct { + responseBody string +} + +func (m *mockCurrencyRatesClient) Do(req *http.Request) (*http.Response, error) { + return &http.Response{ + Status: "200 OK", + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(m.responseBody)), + }, nil +} diff --git a/go.mod b/go.mod index e3b9ff556d5..6d1fe334390 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,6 @@ require ( 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-20201202161906-c7110b5ffcbb - golang.org/x/text v0.3.3 + golang.org/x/text v0.3.6 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 215f5e68e28..78b21ae139c 100644 --- a/go.sum +++ b/go.sum @@ -169,8 +169,9 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index f14cf196366..606874f196a 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -41,6 +41,13 @@ type ExtRequestPrebid struct { // 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"` + + CurrencyConversions *ExtRequestCurrency `json:"currency,omitempty"` +} + +type ExtRequestCurrency struct { + ConversionRates map[string]map[string]float64 `json:"rates"` + UsePBSRates *bool `json:"usepbsrates"` } // ExtRequestPrebid defines the contract for bidrequest.ext.prebid.schains From 613de7e052dcf0933e42fd59183defc4ccb3d2d5 Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Thu, 10 Jun 2021 12:27:37 -0700 Subject: [PATCH 13/16] Debug override header (#1853) --- config/config.go | 2 + config/config_test.go | 1 + endpoints/openrtb2/video_auction.go | 18 ++--- endpoints/openrtb2/video_auction_test.go | 6 +- exchange/auction.go | 27 +++++--- exchange/auction_test.go | 50 ++++++++++++++ exchange/bidder.go | 28 +++++--- exchange/bidder_test.go | 18 +++-- exchange/bidder_validate_bids.go | 4 +- exchange/bidder_validate_bids_test.go | 10 +-- exchange/cachetest/debuglog_enabled.json | 2 + exchange/exchange.go | 19 +++--- exchange/exchange_test.go | 68 +++++++++++++------ exchange/exchangetest/debuglog_enabled.json | 2 + .../debuglog_enabled_no_bids.json | 2 + 15 files changed, 186 insertions(+), 71 deletions(-) diff --git a/config/config.go b/config/config.go index a259a9aa4e1..e8dd94a00a5 100644 --- a/config/config.go +++ b/config/config.go @@ -449,6 +449,7 @@ type DefReqFiles struct { type Debug struct { TimeoutNotification TimeoutNotification `mapstructure:"timeout_notification"` + OverrideToken string `mapstructure:"override_token"` } func (cfg *Debug) validate(errs []error) []error { @@ -988,6 +989,7 @@ func SetupViper(v *viper.Viper, filename string) { v.SetDefault("debug.timeout_notification.log", false) v.SetDefault("debug.timeout_notification.sampling_rate", 0.0) v.SetDefault("debug.timeout_notification.fail_only", false) + v.SetDefault("debug.override_token", "") /* IPv4 /* Site Local: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 diff --git a/config/config_test.go b/config/config_test.go index 1d4c00a5cd1..84d3b4794a9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -421,6 +421,7 @@ func TestFullConfig(t *testing.T) { 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") cmpBools(t, "generate_bid_id", cfg.GenerateBidID, true) + cmpStrings(t, "debug.override_token", cfg.Debug.OverrideToken, "") } func TestUnmarshalAdapterExtraInfo(t *testing.T) { diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 84f0699e011..0af3ba512bb 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -128,11 +128,13 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re cacheTTL = int64(deps.cfg.CacheURL.DefaultTTLs.Video) } debugLog := exchange.DebugLog{ - Enabled: strings.EqualFold(debugQuery, "true"), - CacheType: prebid_cache_client.TypeXML, - TTL: cacheTTL, - Regexp: deps.debugLogRegexp, + Enabled: strings.EqualFold(debugQuery, "true"), + CacheType: prebid_cache_client.TypeXML, + TTL: cacheTTL, + Regexp: deps.debugLogRegexp, + DebugOverride: exchange.IsDebugOverrideEnabled(r.Header.Get(exchange.DebugOverrideHeader), deps.cfg.Debug.OverrideToken), } + debugLog.DebugEnabledOrOverridden = debugLog.Enabled || debugLog.DebugOverride defer func() { if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil { @@ -157,7 +159,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re } resolvedRequest := requestJson - if debugLog.Enabled { + if debugLog.DebugEnabledOrOverridden { debugLog.Data.Request = string(requestJson) if headerBytes, err := json.Marshal(r.Header); err == nil { debugLog.Data.Headers = string(headerBytes) @@ -209,7 +211,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re //create full open rtb req from full video request mergeData(videoBidReq, bidReq) // If debug query param is set, force the response to enable test flag - if debugLog.Enabled { + if debugLog.DebugEnabledOrOverridden { bidReq.Test = 1 } @@ -306,7 +308,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re bidResp.Ext = response.Ext } - if len(bidResp.AdPods) == 0 && debugLog.Enabled { + if len(bidResp.AdPods) == 0 && debugLog.DebugEnabledOrOverridden { err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors) if err != nil { vo.Errors = append(vo.Errors, err) @@ -344,7 +346,7 @@ func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []P } func handleError(labels *metrics.Labels, w http.ResponseWriter, errL []error, vo *analytics.VideoObject, debugLog *exchange.DebugLog) { - if debugLog != nil && debugLog.Enabled { + if debugLog != nil && debugLog.DebugEnabledOrOverridden { if rawUUID, err := uuid.NewV4(); err == nil { debugLog.CacheKey = rawUUID.String() } diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 9ede7147686..9f0859a32cd 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1041,8 +1041,10 @@ func TestHandleErrorDebugLog(t *testing.T) { Headers: "test headers string", Response: "test response string", }, - TTL: int64(3600), - Regexp: regexp.MustCompile(`[<>]`), + TTL: int64(3600), + Regexp: regexp.MustCompile(`[<>]`), + DebugOverride: false, + DebugEnabledOrOverridden: true, } handleError(&labels, recorder, []error{err1, err2}, &vo, &debugLog) diff --git a/exchange/auction.go b/exchange/auction.go index 3d733daaff8..94e808801d9 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -17,14 +17,21 @@ import ( "github.com/prebid/prebid-server/prebid_cache_client" ) +const ( + DebugOverrideHeader string = "x-pbs-debug-override" +) + type DebugLog struct { - Enabled bool - CacheType prebid_cache_client.PayloadType - Data DebugData - TTL int64 - CacheKey string - CacheString string - Regexp *regexp.Regexp + Enabled bool + CacheType prebid_cache_client.PayloadType + Data DebugData + TTL int64 + CacheKey string + CacheString string + Regexp *regexp.Regexp + DebugOverride bool + //little optimization, it stores value of debugLog.Enabled || debugLog.DebugOverride + DebugEnabledOrOverridden bool } type DebugData struct { @@ -47,6 +54,10 @@ func (d *DebugLog) BuildCacheString() { d.CacheString = fmt.Sprintf("%s%s%s%s", xml.Header, d.Data.Request, d.Data.Headers, d.Data.Response) } +func IsDebugOverrideEnabled(debugHeader, configOverrideToken string) bool { + return configOverrideToken != "" && debugHeader == configOverrideToken +} + 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" @@ -238,7 +249,7 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, } } - if len(toCache) > 0 && debugLog != nil && debugLog.Enabled { + if len(toCache) > 0 && debugLog != nil && debugLog.DebugEnabledOrOverridden { debugLog.CacheKey = hbCacheID debugLog.BuildCacheString() if jsonBytes, err := json.Marshal(debugLog.CacheString); err == nil { diff --git a/exchange/auction_test.go b/exchange/auction_test.go index 54f67eb8177..ee064fcb6f1 100644 --- a/exchange/auction_test.go +++ b/exchange/auction_test.go @@ -118,6 +118,56 @@ func TestCacheJSON(t *testing.T) { } } +func TestIsDebugOverrideEnabled(t *testing.T) { + type inTest struct { + debugHeader string + configToken string + } + type aTest struct { + desc string + in inTest + result bool + } + testCases := []aTest{ + { + desc: "test debug header is empty, config token is empty", + in: inTest{debugHeader: "", configToken: ""}, + result: false, + }, + { + desc: "test debug header is present, config token is empty", + in: inTest{debugHeader: "TestToken", configToken: ""}, + result: false, + }, + { + desc: "test debug header is empty, config token is present", + in: inTest{debugHeader: "", configToken: "TestToken"}, + result: false, + }, + { + desc: "test debug header is present, config token is present, not equal", + in: inTest{debugHeader: "TestToken123", configToken: "TestToken"}, + result: false, + }, + { + desc: "test debug header is present, config token is present, equal", + in: inTest{debugHeader: "TestToken", configToken: "TestToken"}, + result: true, + }, + { + desc: "test debug header is present, config token is present, not case equal", + in: inTest{debugHeader: "TestTokeN", configToken: "TestToken"}, + result: false, + }, + } + + for _, test := range testCases { + result := IsDebugOverrideEnabled(test.in.debugHeader, test.in.configToken) + assert.Equal(t, test.result, result, test.desc) + } + +} + // LoadCacheSpec reads and parses a file as a test case. If something goes wrong, it returns an error. func loadCacheSpec(filename string) (*cacheSpec, error) { specData, err := ioutil.ReadFile(filename) diff --git a/exchange/bidder.go b/exchange/bidder.go index c6e2587452a..83466c7d3b0 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -49,7 +49,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 *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed bool) (*pbsOrtbSeatBid, []error) + requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed, headerDebugAllowed bool) (*pbsOrtbSeatBid, []error) } // pbsOrtbBid is a Bid returned by an adaptedBidder. @@ -126,7 +126,7 @@ type bidderAdapterConfig struct { DebugInfo config.DebugInfo } -func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed bool) (*pbsOrtbSeatBid, []error) { +func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed, headerDebugAllowed bool) (*pbsOrtbSeatBid, []error) { reqData, errs := bidder.Bidder.MakeRequests(request, reqInfo) if len(reqData) == 0 { @@ -176,19 +176,25 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb2.B httpInfo := <-responseChannel // If this is a test bid, capture debugging info from the requests. // Write debug data to ext in case if: + // - headerDebugAllowed (debug override header specified correct) - it overrides all other debug restrictions // - debugContextKey (url param) in true // - account debug is allowed // - bidder debug is allowed - if debugInfo := ctx.Value(DebugContextKey); debugInfo != nil && debugInfo.(bool) { - if accountDebugAllowed { - if bidder.config.DebugInfo.Allow { - seatBid.httpCalls = append(seatBid.httpCalls, makeExt(httpInfo)) - } else { - debugDisabledWarning := errortypes.Warning{ - WarningCode: errortypes.BidderLevelDebugDisabledWarningCode, - Message: "debug turned off for bidder", + if headerDebugAllowed { + seatBid.httpCalls = append(seatBid.httpCalls, makeExt(httpInfo)) + } else { + debugInfo := ctx.Value(DebugContextKey) + if debugInfo != nil && debugInfo.(bool) { + if accountDebugAllowed { + if bidder.config.DebugInfo.Allow { + seatBid.httpCalls = append(seatBid.httpCalls, makeExt(httpInfo)) + } else { + debugDisabledWarning := errortypes.Warning{ + WarningCode: errortypes.BidderLevelDebugDisabledWarningCode, + Message: "debug turned off for bidder", + } + errs = append(errs, &debugDisabledWarning) } - errs = append(errs, &debugDisabledWarning) } } } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 5fdfe445206..deff066200a 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -94,7 +94,7 @@ func TestSingleBidder(t *testing.T) { bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, test.debugInfo) currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true) + seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, false) // Make sure the goodSingleBidder was called with the expected arguments. if bidderImpl.httpResponse == nil { @@ -167,7 +167,7 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) { bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo) currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true) + seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, false) expectedHttpCalls := []*openrtb_ext.ExtHttpCall{ { @@ -208,7 +208,7 @@ func TestSetGPCHeader(t *testing.T) { bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo) currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, true) + seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, true, false) expectedHttpCall := []*openrtb_ext.ExtHttpCall{ { @@ -246,7 +246,7 @@ func TestSetGPCHeaderNil(t *testing.T) { bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, debugInfo) currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, true) + seatBid, errs := bidder.requestBid(ctx, &openrtb2.BidRequest{}, "test", 1, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, true, false) expectedHttpCall := []*openrtb_ext.ExtHttpCall{ { @@ -304,7 +304,7 @@ func TestMultiBidder(t *testing.T) { } bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, nil) currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - seatBid, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, true) if seatBid == nil { t.Fatalf("SeatBid should exist, because bids exist.") @@ -681,6 +681,7 @@ func TestMultiCurrencies(t *testing.T) { currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, + true, ) // Verify: @@ -826,6 +827,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) { currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, + true, ) // Verify: @@ -999,6 +1001,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) { currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, + false, ) // Verify: @@ -1303,6 +1306,7 @@ func TestMobileNativeTypes(t *testing.T) { currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, + true, ) var actualValue string @@ -1316,7 +1320,7 @@ func TestMobileNativeTypes(t *testing.T) { func TestErrorReporting(t *testing.T) { bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{}, openrtb_ext.BidderAppnexus, nil) currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - bids, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true) + bids, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, false) if bids != nil { t.Errorf("There should be no seatbid if no http requests are returned.") } @@ -1537,7 +1541,7 @@ 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, nil) currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - _, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true) + _, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, true, true) // Assert no errors assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs) diff --git a/exchange/bidder_validate_bids.go b/exchange/bidder_validate_bids.go index 3d2eb0b8e42..aec0948ddde 100644 --- a/exchange/bidder_validate_bids.go +++ b/exchange/bidder_validate_bids.go @@ -28,8 +28,8 @@ type validatedBidder struct { bidder adaptedBidder } -func (v *validatedBidder) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed bool) (*pbsOrtbSeatBid, []error) { - seatBid, errs := v.bidder.requestBid(ctx, request, name, bidAdjustment, conversions, reqInfo, accountDebugAllowed) +func (v *validatedBidder) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed, headerDebugAllowed bool) (*pbsOrtbSeatBid, []error) { + seatBid, errs := v.bidder.requestBid(ctx, request, name, bidAdjustment, conversions, reqInfo, accountDebugAllowed, headerDebugAllowed) 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 3bb43559856..06973b837c2 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(), &openrtb2.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true, false) 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(), &openrtb2.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true, false) 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(), &openrtb2.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true) + seatBid, errs := bidder.requestBid(context.Background(), &openrtb2.BidRequest{}, openrtb_ext.BidderAppnexus, 1.0, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true, false) 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, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true) + seatBid, errs := bidder.requestBid(context.Background(), request, openrtb_ext.BidderAppnexus, 1.0, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, true, false) 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 *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed bool) (*pbsOrtbSeatBid, []error) { +func (b *mockAdaptedBidder) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed, headerDebugAllowed bool) (*pbsOrtbSeatBid, []error) { return b.bidResponse, b.errorResponse } diff --git a/exchange/cachetest/debuglog_enabled.json b/exchange/cachetest/debuglog_enabled.json index e6c85c57055..faba3ed690d 100644 --- a/exchange/cachetest/debuglog_enabled.json +++ b/exchange/cachetest/debuglog_enabled.json @@ -1,6 +1,8 @@ { "debugLog": { "Enabled": true, + "DebugEnabledOrOverridden": true, + "DebugOverride": false, "CacheType": "xml", "TTL": 3600, "Data": { diff --git a/exchange/exchange.go b/exchange/exchange.go index c1602aadfcb..6f0c610a958 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -169,13 +169,13 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * } if debugLog == nil { - debugLog = &DebugLog{Enabled: false} + debugLog = &DebugLog{Enabled: false, DebugEnabledOrOverridden: false} } requestDebugInfo := getDebugInfo(r.BidRequest, requestExt) - debugInfo := requestDebugInfo && r.Account.DebugAllow - debugLog.Enabled = debugLog.Enabled && r.Account.DebugAllow + debugInfo := debugLog.DebugEnabledOrOverridden || (requestDebugInfo && r.Account.DebugAllow) + debugLog.Enabled = debugLog.DebugEnabledOrOverridden || r.Account.DebugAllow if debugInfo { ctx = e.makeDebugContext(ctx, debugInfo) @@ -204,7 +204,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * // Get currency rates conversions for the auction conversions := e.getAuctionCurrencyRates(requestExt.Prebid.CurrencyConversions) - adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, r.Account.DebugAllow, r.GlobalPrivacyControlHeader) + adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, r.Account.DebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride) var auc *auction var cacheErrs []error @@ -249,7 +249,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * } bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, debugInfo, errs) - if debugLog.Enabled { + if debugLog.DebugEnabledOrOverridden { if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil { debugLog.Data.Response = string(bidRespExtBytes) } else { @@ -270,7 +270,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * } else { bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, debugInfo, errs) - if debugLog.Enabled { + if debugLog.DebugEnabledOrOverridden { if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil { debugLog.Data.Response = string(bidRespExtBytes) @@ -281,7 +281,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * } } - if !r.Account.DebugAllow && requestDebugInfo { + if !r.Account.DebugAllow && requestDebugInfo && !debugLog.DebugOverride { accountDebugDisabledWarning := openrtb_ext.ExtBidderMessage{ Code: errortypes.AccountLevelDebugDisabledWarningCode, Message: "debug turned off for account", @@ -414,7 +414,8 @@ func (e *exchange) getAllBids( bidAdjustments map[string]float64, conversions currency.Conversions, accountDebugAllowed bool, - globalPrivacyControlHeader string) ( + globalPrivacyControlHeader string, + headerDebugAllowed bool) ( map[openrtb_ext.BidderName]*pbsOrtbSeatBid, map[openrtb_ext.BidderName]*seatResponseExtra, bool) { // Set up pointers to the bid results @@ -446,7 +447,7 @@ func (e *exchange) getAllBids( var reqInfo adapters.ExtraRequestInfo reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader - bids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest.BidRequest, bidderRequest.BidderName, adjustmentFactor, conversions, &reqInfo, accountDebugAllowed) + bids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest.BidRequest, bidderRequest.BidderName, adjustmentFactor, conversions, &reqInfo, accountDebugAllowed, headerDebugAllowed) // Add in time reporting elapsed := time.Since(start) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index f778e3ba411..cd36ad94b22 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -176,8 +176,9 @@ func TestDebugBehaviour(t *testing.T) { } type debugData struct { - bidderLevelDebugAllowed bool - accountLevelDebugAllowed bool + bidderLevelDebugAllowed bool + accountLevelDebugAllowed bool + headerOverrideDebugAllowed bool } type aTest struct { @@ -192,57 +193,78 @@ func TestDebugBehaviour(t *testing.T) { desc: "test flag equals zero, ext debug flag false, no debug info expected", in: inTest{test: 0, debug: false}, out: outTest{debugInfoIncluded: false}, - debugData: debugData{true, true}, + debugData: debugData{true, true, false}, generateWarnings: false, }, { desc: "test flag equals zero, ext debug flag true, debug info expected", in: inTest{test: 0, debug: true}, out: outTest{debugInfoIncluded: true}, - debugData: debugData{true, true}, + debugData: debugData{true, true, false}, generateWarnings: false, }, { desc: "test flag equals 1, ext debug flag false, debug info expected", in: inTest{test: 1, debug: false}, out: outTest{debugInfoIncluded: true}, - debugData: debugData{true, true}, + debugData: debugData{true, true, false}, generateWarnings: false, }, { desc: "test flag equals 1, ext debug flag true, debug info expected", in: inTest{test: 1, debug: true}, out: outTest{debugInfoIncluded: true}, - debugData: debugData{true, true}, + debugData: debugData{true, true, false}, generateWarnings: false, }, { 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}, - debugData: debugData{true, true}, + debugData: debugData{true, true, false}, generateWarnings: 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}, - debugData: debugData{true, true}, + debugData: debugData{true, true, false}, generateWarnings: true, }, { desc: "test account level debug disabled", in: inTest{test: -1, debug: true}, out: outTest{debugInfoIncluded: false}, - debugData: debugData{true, false}, + debugData: debugData{true, false, false}, generateWarnings: true, }, { - desc: "test bidder level debug disabled", + desc: "test header override enabled when all other debug options are disabled", + in: inTest{test: -1, debug: false}, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{false, false, true}, + generateWarnings: false, + }, + { + desc: "test header override and url debug options are enabled when all other debug options are disabled", in: inTest{test: -1, debug: true}, - out: outTest{debugInfoIncluded: false}, - debugData: debugData{false, true}, - generateWarnings: true, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{false, false, true}, + generateWarnings: false, + }, + { + desc: "test header override and url and bidder debug options are enabled when account debug option is disabled", + in: inTest{test: -1, debug: true}, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{true, false, true}, + generateWarnings: false, + }, + { + desc: "test all debug options are enabled", + in: inTest{test: -1, debug: true}, + out: outTest{debugInfoIncluded: true}, + debugData: debugData{true, true, true}, + generateWarnings: false, }, } @@ -322,9 +344,12 @@ func TestDebugBehaviour(t *testing.T) { WarningCode: errortypes.InvalidPrivacyConsentWarningCode}) auctionRequest.Warnings = errL } - + debugLog := &DebugLog{} + if test.debugData.headerOverrideDebugAllowed { + debugLog = &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true} + } // Run test - outBidResponse, err := e.HoldAuction(ctx, auctionRequest, nil) + outBidResponse, err := e.HoldAuction(ctx, auctionRequest, debugLog) // Assert no HoldAuction error assert.NoErrorf(t, err, "%s. ex.HoldAuction returned an error: %v \n", test.desc, err) @@ -338,6 +363,11 @@ func TestDebugBehaviour(t *testing.T) { assert.NotEmpty(t, actualExt.Prebid.AuctionTimestamp, "%s. ext.prebid.auctiontimestamp should not be empty when AuctionRequest.StartTime is set") assert.Equal(t, auctionRequest.StartTime.UnixNano()/1e+6, actualExt.Prebid.AuctionTimestamp, "%s. ext.prebid.auctiontimestamp has incorrect value") + if test.debugData.headerOverrideDebugAllowed { + assert.Empty(t, actualExt.Warnings, "warnings should be empty") + assert.Empty(t, actualExt.Errors, "errors should be empty") + } + 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) @@ -357,13 +387,13 @@ func TestDebugBehaviour(t *testing.T) { assert.Nil(t, actualExt.Debug, "%s. ext.debug.httpcalls array should not be empty", "With bidder level debug disable option http calls should be empty") } - if test.out.debugInfoIncluded && !test.debugData.accountLevelDebugAllowed { + if test.out.debugInfoIncluded && !test.debugData.accountLevelDebugAllowed && !test.debugData.headerOverrideDebugAllowed { assert.Len(t, actualExt.Warnings, 1, "warnings should have one warning") assert.NotNil(t, actualExt.Warnings["general"], "general warning should be present") assert.Equal(t, "debug turned off for account", actualExt.Warnings["general"][0].Message, "account debug disabled message should be present") } - if !test.out.debugInfoIncluded && test.in.debug && test.debugData.accountLevelDebugAllowed { + if !test.out.debugInfoIncluded && test.in.debug && test.debugData.accountLevelDebugAllowed && !test.debugData.headerOverrideDebugAllowed { if test.generateWarnings { assert.Len(t, actualExt.Warnings, 2, "warnings should have one warning") } else { @@ -3411,7 +3441,7 @@ type validatingBidder struct { mockResponses map[string]bidderResponse } -func (b *validatingBidder) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed bool) (seatBid *pbsOrtbSeatBid, errs []error) { +func (b *validatingBidder) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed, headerDebugAllowed bool) (seatBid *pbsOrtbSeatBid, errs []error) { if expectedRequest, ok := b.expectations[string(name)]; ok { if expectedRequest != nil { if expectedRequest.BidAdjustment != bidAdjustment { @@ -3590,7 +3620,7 @@ func (e *mockUsersync) LiveSyncCount() int { type panicingAdapter struct{} -func (panicingAdapter) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed bool) (posb *pbsOrtbSeatBid, errs []error) { +func (panicingAdapter) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed, headerDebugAllowed bool) (posb *pbsOrtbSeatBid, errs []error) { panic("Panic! Panic! The world is ending!") } diff --git a/exchange/exchangetest/debuglog_enabled.json b/exchange/exchangetest/debuglog_enabled.json index 851bda69097..8475482f35b 100644 --- a/exchange/exchangetest/debuglog_enabled.json +++ b/exchange/exchangetest/debuglog_enabled.json @@ -1,6 +1,8 @@ { "debugLog": { "Enabled": true, + "DebugEnabledOrOverridden": true, + "DebugOverride": false, "CacheType": "xml", "TTL": 3600, "Data": { diff --git a/exchange/exchangetest/debuglog_enabled_no_bids.json b/exchange/exchangetest/debuglog_enabled_no_bids.json index 4823acf8f16..b9bb15df7fb 100644 --- a/exchange/exchangetest/debuglog_enabled_no_bids.json +++ b/exchange/exchangetest/debuglog_enabled_no_bids.json @@ -1,6 +1,8 @@ { "debugLog": { "Enabled": true, + "DebugEnabledOrOverridden": true, + "DebugOverride": false, "CacheType": "xml", "TTL": 3600, "Data": { From ccb56efe1f23ee101aef3fc604db799a1282f7f9 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Fri, 11 Jun 2021 10:58:47 -0400 Subject: [PATCH 14/16] Remove GDPR TCF1 (#1854) --- config/config.go | 44 +++--- config/config_test.go | 81 +++++++++- exchange/utils_test.go | 57 +++---- gdpr/gdpr.go | 4 +- gdpr/impl.go | 57 +++---- gdpr/impl_test.go | 215 +++++++++++++++----------- gdpr/vendorlist-fetching.go | 28 +--- gdpr/vendorlist-fetching_test.go | 173 ++++----------------- metrics/go_metrics_test.go | 10 -- metrics/metrics.go | 4 - metrics/prometheus/prometheus_test.go | 15 -- privacy/gdpr/policy_test.go | 5 - static/tcf1/fallback_gvl.json | 1 - 13 files changed, 296 insertions(+), 398 deletions(-) delete mode 100644 static/tcf1/fallback_gvl.json diff --git a/config/config.go b/config/config.go index e8dd94a00a5..7e24a00d370 100644 --- a/config/config.go +++ b/config/config.go @@ -197,7 +197,6 @@ type GDPR struct { Timeouts GDPRTimeouts `mapstructure:"timeouts_ms"` NonStandardPublishers []string `mapstructure:"non_standard_publishers,flow"` NonStandardPublisherMap map[string]struct{} - TCF1 TCF1 `mapstructure:"tcf1"` TCF2 TCF2 `mapstructure:"tcf2"` AMPException bool `mapstructure:"amp_exception"` // Deprecated: Use account-level GDPR settings (gdpr.integration_enabled.amp) instead // EEACountries (EEA = European Economic Area) are a list of countries where we should assume GDPR applies. @@ -218,9 +217,6 @@ func (cfg *GDPR) validate(errs []error) []error { if cfg.AMPException == true { 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)")) } - if cfg.TCF1.FetchGVL == true { - errs = append(errs, fmt.Errorf("gdpr.tcf1.fetch_gvl has been discontinued and must be removed from your config. TCF1 will always use the fallback GVL going forward")) - } return errs } @@ -237,20 +233,14 @@ 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"` // Deprecated: In a future version TCF1 will always use the fallback GVL - FallbackGVLPath string `mapstructure:"fallback_gvl_path"` -} - // 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"` + Enabled bool `mapstructure:"enabled"` + Purpose1 PurposeDetail `mapstructure:"purpose1"` + Purpose2 PurposeDetail `mapstructure:"purpose2"` + Purpose7 PurposeDetail `mapstructure:"purpose7"` + SpecialPurpose1 PurposeDetail `mapstructure:"special_purpose1"` + PurposeOneTreatment PurposeOneTreatment `mapstructure:"purpose_one_treatment"` } // Making a purpose struct so purpose specific details can be added later. @@ -258,7 +248,7 @@ type PurposeDetail struct { Enabled bool `mapstructure:"enabled"` } -type PurposeOneTreatement struct { +type PurposeOneTreatment struct { Enabled bool `mapstructure:"enabled"` AccessAllowed bool `mapstructure:"access_allowed"` } @@ -951,16 +941,12 @@ 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", false) - 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) 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("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", @@ -1015,6 +1001,10 @@ func SetupViper(v *viper.Viper, filename string) { // Migrate config settings to maintain compatibility with old configs migrateConfig(v) + migrateConfigPurposeOneTreatment(v) + + v.SetDefault("gdpr.tcf2.purpose_one_treatment.enabled", true) + v.SetDefault("gdpr.tcf2.purpose_one_treatment.access_allowed", true) } func migrateConfig(v *viper.Viper) { @@ -1030,6 +1020,18 @@ func migrateConfig(v *viper.Viper) { } } +func migrateConfigPurposeOneTreatment(v *viper.Viper) { + if oldConfig, ok := v.Get("gdpr.tcf2.purpose_one_treatement").(map[string]interface{}); ok { + if v.IsSet("gdpr.tcf2.purpose_one_treatment") { + glog.Warning("using gdpr.tcf2.purpose_one_treatment and ignoring deprecated gdpr.tcf2.purpose_one_treatement") + } else { + glog.Warning("gdpr.tcf2.purpose_one_treatement.enabled should be changed to gdpr.tcf2.purpose_one_treatment.enabled") + glog.Warning("gdpr.tcf2.purpose_one_treatement.access_allowed should be changed to gdpr.tcf2.purpose_one_treatment.access_allowed") + v.Set("gdpr.tcf2.purpose_one_treatment", oldConfig) + } + } +} + func setBidderDefaults(v *viper.Viper, bidder string) { adapterCfgPrefix := "adapters." v.SetDefault(adapterCfgPrefix+bidder+".endpoint", "") diff --git a/config/config_test.go b/config/config_test.go index 84d3b4794a9..f34bd5fa189 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -141,6 +141,8 @@ func TestDefaults(t *testing.T) { cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path) cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, true) cmpBools(t, "generate_bid_id", cfg.GenerateBidID, false) + cmpBools(t, "gdpr.tcf2.purpose_one_treatment.enabled", true, cfg.GDPR.TCF2.PurposeOneTreatment.Enabled) + cmpBools(t, "gdpr.tcf2.purpose_one_treatment.access_allowed", true, cfg.GDPR.TCF2.PurposeOneTreatment.AccessAllowed) } var fullConfig = []byte(` @@ -510,6 +512,79 @@ func TestMigrateConfigFromEnv(t *testing.T) { cmpBools(t, "stored_requests.filesystem.enabled", true, cfg.StoredRequests.Files.Enabled) } +func TestMigrateConfigPurposeOneTreatment(t *testing.T) { + oldPurposeOneTreatmentConfig := []byte(` + gdpr: + tcf2: + purpose_one_treatement: + enabled: true + access_allowed: true + `) + newPurposeOneTreatmentConfig := []byte(` + gdpr: + tcf2: + purpose_one_treatment: + enabled: true + access_allowed: true + `) + oldAndNewPurposeOneTreatmentConfig := []byte(` + gdpr: + tcf2: + purpose_one_treatement: + enabled: false + access_allowed: true + purpose_one_treatment: + enabled: true + access_allowed: false + `) + + tests := []struct { + description string + config []byte + wantPurpose1TreatmentEnabled bool + wantPurpose1TreatmentAccessAllowed bool + }{ + { + description: "New config and old config not set", + config: []byte{}, + }, + { + description: "New config not set, old config set", + config: oldPurposeOneTreatmentConfig, + wantPurpose1TreatmentEnabled: true, + wantPurpose1TreatmentAccessAllowed: true, + }, + { + description: "New config set, old config not set", + config: newPurposeOneTreatmentConfig, + wantPurpose1TreatmentEnabled: true, + wantPurpose1TreatmentAccessAllowed: true, + }, + { + description: "New config and old config set", + config: oldAndNewPurposeOneTreatmentConfig, + wantPurpose1TreatmentEnabled: true, + wantPurpose1TreatmentAccessAllowed: false, + }, + } + + for _, tt := range tests { + v := viper.New() + v.SetConfigType("yaml") + v.ReadConfig(bytes.NewBuffer(tt.config)) + + migrateConfigPurposeOneTreatment(v) + + if len(tt.config) > 0 { + assert.Equal(t, tt.wantPurpose1TreatmentEnabled, v.Get("gdpr.tcf2.purpose_one_treatment.enabled").(bool), tt.description) + assert.Equal(t, tt.wantPurpose1TreatmentAccessAllowed, v.Get("gdpr.tcf2.purpose_one_treatment.access_allowed").(bool), tt.description) + } else { + assert.Nil(t, v.Get("gdpr.tcf2.purpose_one_treatment.enabled"), tt.description) + assert.Nil(t, v.Get("gdpr.tcf2.purpose_one_treatment.access_allowed"), tt.description) + } + } +} + func TestInvalidAdapterEndpointConfig(t *testing.T) { v := viper.New() SetupViper(v, "") @@ -569,12 +644,6 @@ func TestInvalidHostVendorID(t *testing.T) { } } -func TestInvalidFetchGVL(t *testing.T) { - cfg := newDefaultConfig(t) - cfg.GDPR.TCF1.FetchGVL = true - assertOneError(t, cfg.validate(), "gdpr.tcf1.fetch_gvl has been discontinued and must be removed from your config. TCF1 will always use the fallback GVL going forward") -} - func TestInvalidAMPException(t *testing.T) { cfg := newDefaultConfig(t) cfg.GDPR.AMPException = true diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 50636d35ccd..1788d508c31 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -1447,8 +1447,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { } } -func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { - tcf1Consent := "BONV8oqONXwgmADACHENAO7pqzAAppY" +func TestCleanOpenRTBRequestsGDPR(t *testing.T) { tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA" trueValue, falseValue := true, false @@ -1477,19 +1476,7 @@ func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { }, }, { - description: "Enforce - TCF 1", - gdprAccountEnabled: &trueValue, - gdprHostEnabled: true, - gdpr: "1", - gdprConsent: tcf1Consent, - gdprScrub: true, - expectPrivacyLabels: metrics.PrivacyLabels{ - GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, - }, - }, - { - description: "Enforce - TCF 2", + description: "Enforce", gdprAccountEnabled: &trueValue, gdprHostEnabled: true, gdpr: "1", @@ -1501,11 +1488,11 @@ func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { }, }, { - description: "Not Enforce - TCF 1", + description: "Not Enforce", gdprAccountEnabled: &trueValue, gdprHostEnabled: true, gdpr: "0", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, @@ -1513,36 +1500,36 @@ func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { }, }, { - description: "Enforce - TCF 1; GDPR signal extraction error", + description: "Enforce; GDPR signal extraction error", gdprAccountEnabled: &trueValue, gdprHostEnabled: true, gdpr: "0{", - gdprConsent: "BONV8oqONXwgmADACHENAO7pqzAAppY", + gdprConsent: tcf2Consent, gdprScrub: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, + GDPRTCFVersion: metrics.TCFVersionV2, }, expectError: true, }, { - description: "Enforce - TCF 1; account GDPR enabled, host GDPR setting disregarded", + description: "Enforce; account GDPR enabled, host GDPR setting disregarded", gdprAccountEnabled: &trueValue, gdprHostEnabled: false, gdpr: "1", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, + GDPRTCFVersion: metrics.TCFVersionV2, }, }, { - description: "Not Enforce - TCF 1; account GDPR disabled, host GDPR setting disregarded", + description: "Not Enforce; account GDPR disabled, host GDPR setting disregarded", gdprAccountEnabled: &falseValue, gdprHostEnabled: true, gdpr: "1", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, @@ -1550,23 +1537,23 @@ func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { }, }, { - description: "Enforce - TCF 1; account GDPR not specified, host GDPR enabled", + description: "Enforce; account GDPR not specified, host GDPR enabled", gdprAccountEnabled: nil, gdprHostEnabled: true, gdpr: "1", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: true, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, + GDPRTCFVersion: metrics.TCFVersionV2, }, }, { - description: "Not Enforce - TCF 1; account GDPR not specified, host GDPR disabled", + description: "Not Enforce; account GDPR not specified, host GDPR disabled", gdprAccountEnabled: nil, gdprHostEnabled: false, gdpr: "1", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, @@ -1578,12 +1565,12 @@ func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { gdprAccountEnabled: nil, gdprHostEnabled: true, gdpr: "null", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: true, userSyncIfAmbiguous: false, expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, + GDPRTCFVersion: metrics.TCFVersionV2, }, }, { @@ -1591,7 +1578,7 @@ func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { gdprAccountEnabled: nil, gdprHostEnabled: true, gdpr: "null", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: false, userSyncIfAmbiguous: true, expectPrivacyLabels: metrics.PrivacyLabels{ @@ -1604,12 +1591,12 @@ func TestCleanOpenRTBRequestsGDPRScrub(t *testing.T) { gdprAccountEnabled: nil, gdprHostEnabled: true, gdpr: "1", - gdprConsent: tcf1Consent, + gdprConsent: tcf2Consent, gdprScrub: true, permissionsError: errors.New("Some error"), expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, + GDPRTCFVersion: metrics.TCFVersionV2, }, }, } diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go index ffd5ced462a..4179d8122ac 100644 --- a/gdpr/gdpr.go +++ b/gdpr/gdpr.go @@ -30,7 +30,6 @@ type Permissions interface { // Versions of the GDPR TCF technical specification. const ( - tcf1SpecVersion uint8 = 1 tcf2SpecVersion uint8 = 2 ) @@ -44,8 +43,7 @@ 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){ - tcf1SpecVersion: newVendorListFetcherTCF1(cfg), - tcf2SpecVersion: newVendorListFetcherTCF2(ctx, cfg, client, vendorListURLMaker)}, + tcf2SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker)}, } if cfg.HostVendorID == 0 { diff --git a/gdpr/impl.go b/gdpr/impl.go index a91a9308e24..aca07fd068d 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -5,8 +5,8 @@ import ( "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/consentconstants" + tcf2ConsentConstants "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" @@ -116,23 +116,15 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, consen return false, nil } - // InfoStorageAccess is the same across TCF 1 and TCF 2 - if parsedConsent.Version() == 2 { - if !p.cfg.TCF2.Purpose1.Enabled { - // We are not enforcing purpose 1 - return true, nil - } - consent, ok := parsedConsent.(tcf2.ConsentMetadata) - if !ok { - err := fmt.Errorf("Unable to access TCF2 parsed consent") - return false, err - } - return p.checkPurpose(consent, vendor, vendorID, consentconstants.InfoStorageAccess, false), nil - } - if vendor.Purpose(consentconstants.InfoStorageAccess) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && parsedConsent.VendorConsent(vendorID) { + if !p.cfg.TCF2.Purpose1.Enabled { return true, nil } - return false, nil + consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata) + if !ok { + err := fmt.Errorf("Unable to access TCF2 parsed consent") + return false, err + } + return p.checkPurpose(consentMeta, vendor, vendorID, tcf2ConsentConstants.InfoStorageAccess, false), nil } func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16, consent string, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) { @@ -141,44 +133,33 @@ func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16, return false, false, false, err } + // vendor will be nil if not a valid TCF2 consent string if vendor == nil { return false, false, false, nil } - if parsedConsent.Version() == 2 { - if p.cfg.TCF2.Enabled { - return p.allowActivitiesTCF2(parsedConsent, vendor, vendorID, weakVendorEnforcement) - } - if (vendor.Purpose(consentconstants.InfoStorageAccess) || vendor.LegitimateInterest(consentconstants.InfoStorageAccess) || weakVendorEnforcement) && parsedConsent.PurposeAllowed(consentconstants.InfoStorageAccess) && (vendor.Purpose(consentconstants.PersonalizationProfile) || vendor.LegitimateInterest(consentconstants.PersonalizationProfile) || weakVendorEnforcement) && parsedConsent.PurposeAllowed(consentconstants.PersonalizationProfile) && (parsedConsent.VendorConsent(vendorID) || weakVendorEnforcement) { - 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, true, nil - } + if !p.cfg.TCF2.Enabled { + return true, false, false, nil } - return true, false, false, nil -} -func (p *permissionsImpl) allowActivitiesTCF2(parsedConsent api.VendorConsents, vendor api.Vendor, vendorID uint16, weakVendorEnforcement bool) (allowBidRequest bool, passGeo bool, passID bool, err error) { - consent, ok := parsedConsent.(tcf2.ConsentMetadata) + consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata) if !ok { err = fmt.Errorf("Unable to access TCF2 parsed consent") return } if p.cfg.TCF2.SpecialPurpose1.Enabled { - passGeo = consent.SpecialFeatureOptIn(1) && (vendor.SpecialPurpose(1) || weakVendorEnforcement) + passGeo = consentMeta.SpecialFeatureOptIn(1) && (vendor.SpecialPurpose(1) || weakVendorEnforcement) } else { passGeo = true } if p.cfg.TCF2.Purpose2.Enabled { - allowBidRequest = p.checkPurpose(consent, vendor, vendorID, tcf1constants.Purpose(2), weakVendorEnforcement) + allowBidRequest = p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(2), weakVendorEnforcement) } else { allowBidRequest = true } for i := 2; i <= 10; i++ { - if p.checkPurpose(consent, vendor, vendorID, tcf1constants.Purpose(i), weakVendorEnforcement) { + if p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(i), weakVendorEnforcement) { passID = true break } @@ -191,8 +172,8 @@ const pubRestrictNotAllowed = 0 const pubRestrictRequireConsent = 1 const pubRestrictRequireLegitInterest = 2 -func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose tcf1constants.Purpose, weakVendorEnforcement bool) bool { - if purpose == consentconstants.InfoStorageAccess && p.cfg.TCF2.PurposeOneTreatment.Enabled && consent.PurposeOneTreatment() { +func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, weakVendorEnforcement bool) bool { + if purpose == tcf2ConsentConstants.InfoStorageAccess && p.cfg.TCF2.PurposeOneTreatment.Enabled && consent.PurposeOneTreatment() { return p.cfg.TCF2.PurposeOneTreatment.AccessAllowed } if consent.CheckPubRestriction(uint8(purpose), pubRestrictNotAllowed, vendorID) { @@ -224,7 +205,7 @@ func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, cons } version := parsedConsent.Version() - if version < 1 || version > 2 { + if version != 2 { return } vendorList, err := p.fetchVendorList[version](ctx, parsedConsent.VendorListVersion()) diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index 3b974ffa3bb..d26c0c57231 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -23,7 +23,6 @@ func TestDisallowOnEmptyConsent(t *testing.T) { }, vendorIDs: nil, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tcf1SpecVersion: failedListFetcher, tcf2SpecVersion: failedListFetcher, }, } @@ -49,106 +48,120 @@ func TestAllowOnSignalNo(t *testing.T) { } func TestAllowedSyncs(t *testing.T) { - vendorListData := tcf1MarshalVendorList(tcf1VendorList{ - VendorListVersion: 1, - Vendors: []tcf1Vendor{ - {ID: 2, Purposes: []int{1}}, - {ID: 3, Purposes: []int{1}}, + vendor2AndPurpose1Consent := "CPGWbY_PGWbY_GYAAAENABCAAIAAAAAAAAAAACEAAAAA" + vendorListData := MarshalVendorList(vendorList{ + VendorListVersion: 2, + Vendors: map[string]*vendor{ + "2": { + ID: 2, + Purposes: []int{1}, + }, }, }) + perms := permissionsImpl{ cfg: config.GDPR{ HostVendorID: 2, + TCF2: config.TCF2{ + Purpose1: config.PurposeDetail{ + Enabled: true, + }, + }, }, 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{ - 1: parseVendorListData(t, vendorListData), - }), tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), + 1: parseVendorListDataV2(t, vendorListData), }), }, } - allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "BON3PCUON3PCUABABBAAABoAAAAAMw") + allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, vendor2AndPurpose1Consent) assertNilErr(t, err) assertBoolsEqual(t, true, allowSync) - allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderPubmatic, SignalYes, "BON3PCUON3PCUABABBAAABoAAAAAMw") + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, SignalYes, vendor2AndPurpose1Consent) assertNilErr(t, err) assertBoolsEqual(t, true, allowSync) } func TestProhibitedPurposes(t *testing.T) { - vendorListData := tcf1MarshalVendorList(tcf1VendorList{ - VendorListVersion: 1, - Vendors: []tcf1Vendor{ - {ID: 2, Purposes: []int{1}}, // cookie reads/writes - {ID: 3, Purposes: []int{3}}, // ad personalization + vendor2NoPurpose1Consent := "CPGWkCaPGWkCaApAAAENABCAAAAAAAAAAAAAABEAAAAA" + vendorListData := MarshalVendorList(vendorList{ + VendorListVersion: 2, + Vendors: map[string]*vendor{ + "2": { + ID: 2, + Purposes: []int{1}, + }, }, }) + perms := permissionsImpl{ cfg: config.GDPR{ HostVendorID: 2, + TCF2: config.TCF2{ + Purpose1: config.PurposeDetail{ + Enabled: true, + }, + }, }, 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{ - 1: parseVendorListData(t, vendorListData), - }), tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), + 1: parseVendorListDataV2(t, vendorListData), }), }, } - allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "BON3PCUON3PCUABABBAAABAAAAAAMw") + allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, vendor2NoPurpose1Consent) assertNilErr(t, err) assertBoolsEqual(t, false, allowSync) - allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderPubmatic, SignalYes, "BON3PCUON3PCUABABBAAABAAAAAAMw") + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, SignalYes, vendor2NoPurpose1Consent) assertNilErr(t, err) assertBoolsEqual(t, false, allowSync) } func TestProhibitedVendors(t *testing.T) { - vendorListData := tcf1MarshalVendorList(tcf1VendorList{ - VendorListVersion: 1, - Vendors: []tcf1Vendor{ - {ID: 2, Purposes: []int{1}}, // cookie reads/writes - {ID: 3, Purposes: []int{3}}, // ad personalization + purpose1NoVendorConsent := "CPGWkCaPGWkCaApAAAENABCAAIAAAAAAAAAAABAAAAAA" + vendorListData := MarshalVendorList(vendorList{ + VendorListVersion: 2, + Vendors: map[string]*vendor{ + "2": { + ID: 2, + Purposes: []int{1}, + }, }, }) perms := permissionsImpl{ cfg: config.GDPR{ HostVendorID: 2, + TCF2: config.TCF2{ + Purpose1: config.PurposeDetail{ + Enabled: true, + }, + }, }, 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{ - 1: parseVendorListData(t, vendorListData), - }), tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), + 1: parseVendorListDataV2(t, vendorListData), }), }, } - allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "BOS2bx5OS2bx5ABABBAAABoAAAAAFA") + allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, purpose1NoVendorConsent) assertNilErr(t, err) assertBoolsEqual(t, false, allowSync) - allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderPubmatic, SignalYes, "BOS2bx5OS2bx5ABABBAAABoAAAAAFA") + allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderPubmatic, SignalYes, purpose1NoVendorConsent) assertNilErr(t, err) assertBoolsEqual(t, false, allowSync) } @@ -159,7 +172,6 @@ func TestMalformedConsent(t *testing.T) { HostVendorID: 2, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tcf1SpecVersion: listFetcher(nil), tcf2SpecVersion: listFetcher(nil), }, } @@ -172,7 +184,7 @@ func TestMalformedConsent(t *testing.T) { func TestAllowActivities(t *testing.T) { bidderAllowedByConsent := openrtb_ext.BidderAppnexus bidderBlockedByConsent := openrtb_ext.BidderRubicon - consent := "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA" + vendor2AndPurpose2Consent := "CPGWbY_PGWbY_GYAAAENABCAAEAAAAAAAAAAACEAAAAA" tests := []struct { description string @@ -190,7 +202,7 @@ func TestAllowActivities(t *testing.T) { publisherID: "appNexusAppID", userSyncIfAmbiguous: false, gdpr: SignalYes, - consent: consent, + consent: vendor2AndPurpose2Consent, passID: true, }, { @@ -198,7 +210,7 @@ func TestAllowActivities(t *testing.T) { bidderName: bidderBlockedByConsent, userSyncIfAmbiguous: false, gdpr: SignalNo, - consent: consent, + consent: vendor2AndPurpose2Consent, passID: true, }, { @@ -206,7 +218,7 @@ func TestAllowActivities(t *testing.T) { bidderName: bidderAllowedByConsent, userSyncIfAmbiguous: false, gdpr: SignalYes, - consent: consent, + consent: vendor2AndPurpose2Consent, passID: true, }, { @@ -222,7 +234,7 @@ func TestAllowActivities(t *testing.T) { bidderName: bidderAllowedByConsent, userSyncIfAmbiguous: true, gdpr: SignalAmbiguous, - consent: consent, + consent: vendor2AndPurpose2Consent, passID: true, }, { @@ -238,7 +250,7 @@ func TestAllowActivities(t *testing.T) { bidderName: bidderAllowedByConsent, userSyncIfAmbiguous: false, gdpr: SignalAmbiguous, - consent: consent, + consent: vendor2AndPurpose2Consent, passID: true, }, { @@ -254,31 +266,37 @@ func TestAllowActivities(t *testing.T) { bidderName: bidderBlockedByConsent, userSyncIfAmbiguous: false, gdpr: SignalYes, - consent: consent, + consent: vendor2AndPurpose2Consent, passID: false, }, } - - vendorListData := tcf1MarshalVendorList(tcf1VendorList{ + vendorListData := MarshalVendorList(vendorList{ VendorListVersion: 1, - Vendors: []tcf1Vendor{ - {ID: 2, Purposes: []int{1, 3}}, + Vendors: map[string]*vendor{ + "2": { + ID: 2, + Purposes: []int{2}, + }, }, }) + perms := permissionsImpl{ cfg: config.GDPR{ HostVendorID: 2, NonStandardPublisherMap: map[string]struct{}{"appNexusAppID": {}}, + TCF2: config.TCF2{ + Enabled: true, + Purpose2: config.PurposeDetail{ + Enabled: true, + }, + }, }, vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), - }), tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ - 1: parseVendorListData(t, vendorListData), + 1: parseVendorListDataV2(t, vendorListData), }), }, } @@ -293,10 +311,10 @@ func TestAllowActivities(t *testing.T) { } } -func buildTCF2VendorList34() tcf2VendorList { - return tcf2VendorList{ +func buildVendorList34() vendorList { + return vendorList{ VendorListVersion: 2, - Vendors: map[string]*tcf2Vendor{ + Vendors: map[string]*vendor{ "2": { ID: 2, Purposes: []int{1}, @@ -332,7 +350,7 @@ func buildTCF2VendorList34() tcf2VendorList { } } -var tcf2Config = config.GDPR{ +var gdprConfig = config.GDPR{ HostVendorID: 2, TCF2: config.TCF2{ Enabled: true, @@ -343,7 +361,7 @@ var tcf2Config = config.GDPR{ }, } -type tcf2TestDef struct { +type testDef struct { description string bidder openrtb_ext.BidderName consent string @@ -353,10 +371,10 @@ type tcf2TestDef struct { weakVendorEnforcement bool } -func TestAllowActivitiesTCF2(t *testing.T) { - vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) +func TestAllowActivitiesGeoAndID(t *testing.T) { + vendorListData := MarshalVendorList(buildVendorList34()) perms := permissionsImpl{ - cfg: tcf2Config, + cfg: gdprConfig, vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, openrtb_ext.BidderPubmatic: 6, @@ -364,7 +382,6 @@ func TestAllowActivitiesTCF2(t *testing.T) { openrtb_ext.BidderOpenx: 20, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tcf1SpecVersion: nil, tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), 74: parseVendorListDataV2(t, vendorListData), @@ -372,8 +389,8 @@ func TestAllowActivitiesTCF2(t *testing.T) { }, } - // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes and vendors 2, 6, 8 - testDefs := []tcf2TestDef{ + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consents to purposes and vendors 2, 6, 8 + testDefs := []testDef{ { description: "Appnexus vendor test, insufficient purposes claimed", bidder: openrtb_ext.BidderAppnexus, @@ -429,17 +446,16 @@ func TestAllowActivitiesTCF2(t *testing.T) { } } -func TestAllowActivitiesWhitelistTCF2(t *testing.T) { - vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) +func TestAllowActivitiesWhitelist(t *testing.T) { + vendorListData := MarshalVendorList(buildVendorList34()) perms := permissionsImpl{ - cfg: tcf2Config, + cfg: gdprConfig, 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){ - tcf1SpecVersion: nil, tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), @@ -453,17 +469,16 @@ func TestAllowActivitiesWhitelistTCF2(t *testing.T) { assert.EqualValuesf(t, true, passID, "PassID failure") } -func TestAllowActivitiesTCF2PubRestrict(t *testing.T) { - vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) +func TestAllowActivitiesPubRestrict(t *testing.T) { + vendorListData := MarshalVendorList(buildVendorList34()) perms := permissionsImpl{ - cfg: tcf2Config, + cfg: gdprConfig, 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){ - tcf1SpecVersion: nil, tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 15: parseVendorListDataV2(t, vendorListData), }), @@ -472,7 +487,7 @@ func TestAllowActivitiesTCF2PubRestrict(t *testing.T) { // 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{ + testDefs := []testDef{ { description: "Appnexus vendor test, insufficient purposes claimed", bidder: openrtb_ext.BidderAppnexus, @@ -504,24 +519,23 @@ func TestAllowActivitiesTCF2PubRestrict(t *testing.T) { } } -func TestAllowSyncTCF2(t *testing.T) { - vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) +func TestAllowSync(t *testing.T) { + vendorListData := MarshalVendorList(buildVendorList34()) perms := permissionsImpl{ - cfg: tcf2Config, + cfg: gdprConfig, 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){ - 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 : full consensts to purposes and vendors 2, 6, 8 allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") assert.EqualValuesf(t, true, allowSync, "HostCookiesAllowed failure") @@ -531,19 +545,18 @@ func TestAllowSyncTCF2(t *testing.T) { assert.EqualValuesf(t, true, allowSync, "BidderSyncAllowed failure") } -func TestProhibitedPurposeSyncTCF2(t *testing.T) { - tcf2VendorList34 := buildTCF2VendorList34() - tcf2VendorList34.Vendors["8"].Purposes = []int{7} - vendorListData := tcf2MarshalVendorList(tcf2VendorList34) +func TestProhibitedPurposeSync(t *testing.T) { + vendorList34 := buildVendorList34() + vendorList34.Vendors["8"].Purposes = []int{7} + vendorListData := MarshalVendorList(vendorList34) perms := permissionsImpl{ - cfg: tcf2Config, + cfg: gdprConfig, 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){ - tcf1SpecVersion: nil, tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), @@ -551,7 +564,7 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) { } perms.cfg.HostVendorID = 8 - // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for vendors 2, 6, 8 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consents to purposes for vendors 2, 6, 8 allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure") @@ -561,10 +574,10 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) { assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure") } -func TestProhibitedVendorSyncTCF2(t *testing.T) { - vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) +func TestProhibitedVendorSync(t *testing.T) { + vendorListData := MarshalVendorList(buildVendorList34()) perms := permissionsImpl{ - cfg: tcf2Config, + cfg: gdprConfig, vendorIDs: map[openrtb_ext.BidderName]uint16{ openrtb_ext.BidderAppnexus: 2, openrtb_ext.BidderPubmatic: 6, @@ -572,7 +585,6 @@ func TestProhibitedVendorSyncTCF2(t *testing.T) { openrtb_ext.BidderOpenx: 10, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tcf1SpecVersion: nil, tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), @@ -580,7 +592,7 @@ func TestProhibitedVendorSyncTCF2(t *testing.T) { } perms.cfg.HostVendorID = 10 - // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for vendors 2, 6, 8 + // COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consents to purposes for vendors 2, 6, 8 allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA") assert.NoErrorf(t, err, "Error processing HostCookiesAllowed") assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure") @@ -713,7 +725,7 @@ func TestNormalizeGDPR(t *testing.T) { } } -func TestAllowActivitiesTCF2BidRequests(t *testing.T) { +func TestAllowActivitiesBidRequests(t *testing.T) { purpose2AndVendorConsent := "CPF_61ePF_61eFxAAAENAiCAAEAAAAAAAAAAADAQAAAAAA" purpose2ConsentWithoutVendorConsent := "CPF_61ePF_61eFxAAAENAiCAAEAAAAAAAAAAABIAAAAA" @@ -757,7 +769,7 @@ func TestAllowActivitiesTCF2BidRequests(t *testing.T) { } for _, td := range testDefs { - vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34()) + vendorListData := MarshalVendorList(buildVendorList34()) perms := permissionsImpl{ cfg: config.GDPR{ HostVendorID: 2, @@ -773,7 +785,6 @@ func TestAllowActivitiesTCF2BidRequests(t *testing.T) { openrtb_ext.BidderPubmatic: 6, }, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ - tcf1SpecVersion: nil, tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{ 34: parseVendorListDataV2(t, vendorListData), }), @@ -787,3 +798,21 @@ func TestAllowActivitiesTCF2BidRequests(t *testing.T) { assert.EqualValuesf(t, td.passID, passID, "PassID failure on %s", td.description) } } + +func TestTCF1Consent(t *testing.T) { + bidderAllowedByConsent := openrtb_ext.BidderAppnexus + tcf1Consent := "BOS2bx5OS2bx5ABABBAAABoAAAABBwAA" + + perms := permissionsImpl{ + vendorIDs: map[openrtb_ext.BidderName]uint16{ + openrtb_ext.BidderAppnexus: 2, + }, + } + + bidReq, passGeo, passID, err := perms.AuctionActivitiesAllowed(context.Background(), bidderAllowedByConsent, "", SignalYes, tcf1Consent, false) + + assert.Nil(t, err, "TCF1 consent - no error returned") + assert.Equal(t, false, bidReq, "TCF1 consent - bid request not allowed") + assert.Equal(t, false, passGeo, "TCF1 consent - passing geo not allowed") + assert.Equal(t, false, passID, "TCF1 consent - passing id not allowed") +} diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go index bc7eab40647..24489e73265 100644 --- a/gdpr/vendorlist-fetching.go +++ b/gdpr/vendorlist-fetching.go @@ -26,33 +26,7 @@ type saveVendors func(uint16, api.VendorList) // // Nothing in this file is exported. Public APIs can be found in gdpr.go -func newVendorListFetcherTCF1(cfg config.GDPR) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { - if len(cfg.TCF1.FallbackGVLPath) == 0 { - return func(_ context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) { - return nil, makeVendorListNotFoundError(vendorListVersion) - } - } - - fallback := loadFallbackGVLForTCF1(cfg.TCF1.FallbackGVLPath) - return func(_ context.Context, _ uint16) (vendorlist.VendorList, error) { - return fallback, nil - } -} - -func loadFallbackGVLForTCF1(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 -} - -func newVendorListFetcherTCF2(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) string) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) { cacheSave, cacheLoad := newVendorListCache() preloadContext, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout()) diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go index 27f1bc3b996..95529e4e334 100644 --- a/gdpr/vendorlist-fetching_test.go +++ b/gdpr/vendorlist-fetching_test.go @@ -14,71 +14,15 @@ import ( "github.com/prebid/prebid-server/config" ) -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: "Fallback - Vendor List 1", - setup: testSetup{ - enableTCF1Fallback: true, - vendorListVersion: 1, - }, - expected: vendorListFallbackExpected, - }, - { - description: "Fallback - Vendor List 2", - setup: testSetup{ - enableTCF1Fallback: true, - vendorListVersion: 2, - }, - expected: vendorListFallbackExpected, - }, - { - description: "No Fallback - Vendor List 1", - setup: testSetup{ - 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 Fallback - Vendor List 2", - setup: testSetup{ - 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 { - runTestTCF1(t, test, server) - } -} - -func TestTCF2FetcherDynamicLoadListExists(t *testing.T) { +func TestFetcherDynamicLoadListExists(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: tcf2VendorList1, - 2: tcf2VendorList2, + 1: vendorList1, + 2: vendorList2, }, }))) defer server.Close() @@ -91,17 +35,17 @@ func TestTCF2FetcherDynamicLoadListExists(t *testing.T) { expected: vendorList2Expected, } - runTestTCF2(t, test, server) + runTest(t, test, server) } -func TestTCF2FetcherDynamicLoadListDoesntExist(t *testing.T) { +func TestFetcherDynamicLoadListDoesntExist(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: tcf2VendorList1, + 1: vendorList1, }, }))) defer server.Close() @@ -116,30 +60,30 @@ func TestTCF2FetcherDynamicLoadListDoesntExist(t *testing.T) { }, } - runTestTCF2(t, test, server) + runTest(t, test, server) } -func TestTCF2FetcherThrottling(t *testing.T) { +func TestFetcherThrottling(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ vendorListLatestVersion: 1, vendorLists: map[int]string{ - 1: tcf2MarshalVendorList(tcf2VendorList{ + 1: MarshalVendorList(vendorList{ VendorListVersion: 1, - Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1}}}, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1}}}, }), - 2: tcf2MarshalVendorList(tcf2VendorList{ + 2: MarshalVendorList(vendorList{ VendorListVersion: 2, - Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2}}}, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2}}}, }), - 3: tcf2MarshalVendorList(tcf2VendorList{ + 3: MarshalVendorList(vendorList{ VendorListVersion: 3, - Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}}, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}}, }), }, }))) defer server.Close() - fetcher := newVendorListFetcherTCF2(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) // Dynamically Load List 2 Successfully _, errList1 := fetcher(context.Background(), 2) @@ -151,7 +95,7 @@ func TestTCF2FetcherThrottling(t *testing.T) { 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 TestTCF2MalformedVendorlist(t *testing.T) { +func TestMalformedVendorlist(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{ vendorListLatestVersion: 1, vendorLists: map[int]string{ @@ -160,30 +104,30 @@ func TestTCF2MalformedVendorlist(t *testing.T) { }))) defer server.Close() - fetcher := newVendorListFetcherTCF2(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) _, err := fetcher(context.Background(), 1) // Fetching should fail since vendor list could not be unmarshalled. assert.Error(t, err) } -func TestTCF2ServerUrlInvalid(t *testing.T) { +func TestServerUrlInvalid(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() invalidURLGenerator := func(uint16) string { return " http://invalid-url-has-leading-whitespace" } - fetcher := newVendorListFetcherTCF2(context.Background(), testConfig(), server.Client(), invalidURLGenerator) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator) _, 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) { +func TestServerUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - fetcher := newVendorListFetcherTCF2(context.Background(), testConfig(), server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server)) _, 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") @@ -213,24 +157,14 @@ func TestVendorListURLMaker(t *testing.T) { } } -var tcf1VendorList1 = tcf1MarshalVendorList(tcf1VendorList{ +var vendorList1 = MarshalVendorList(vendorList{ VendorListVersion: 1, - Vendors: []tcf1Vendor{{ID: 12, Purposes: []int{2}}}, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{2}}}, }) -var tcf2VendorList1 = tcf2MarshalVendorList(tcf2VendorList{ - VendorListVersion: 1, - Vendors: map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{2}}}, -}) - -var tcf1VendorList2 = tcf1MarshalVendorList(tcf1VendorList{ +var vendorList2 = MarshalVendorList(vendorList{ 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}}}, + Vendors: map[string]*vendor{"12": {ID: 12, Purposes: []int{2, 3}}}, }) var vendorList2Expected = testExpected{ @@ -245,27 +179,12 @@ var vendorListFallbackExpected = testExpected{ vendorPurposes: map[int]bool{1: true, 2: false, 3: true}, } -type tcf1VendorList struct { - VendorListVersion uint16 `json:"vendorListVersion"` - Vendors []tcf1Vendor `json:"vendors"` +type vendorList struct { + VendorListVersion uint16 `json:"vendorListVersion"` + Vendors map[string]*vendor `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 { +type vendor struct { ID uint16 `json:"id"` Purposes []int `json:"purposes"` LegIntPurposes []int `json:"legIntPurposes"` @@ -273,7 +192,7 @@ type tcf2Vendor struct { SpecialPurposes []int `json:"specialPurposes"` } -func tcf2MarshalVendorList(vendorList tcf2VendorList) string { +func MarshalVendorList(vendorList vendorList) string { json, _ := json.Marshal(vendorList) return string(json) } @@ -323,8 +242,7 @@ type test struct { } type testSetup struct { - enableTCF1Fallback bool - vendorListVersion uint16 + vendorListVersion uint16 } type testExpected struct { @@ -334,31 +252,9 @@ type testExpected struct { vendorPurposes map[int]bool } -func runTestTCF1(t *testing.T, test test, server *httptest.Server) { +func runTest(t *testing.T, test test, server *httptest.Server) { config := testConfig() - if test.setup.enableTCF1Fallback { - config.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json" - } - - fetcher := newVendorListFetcherTCF1(config) - 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) - } - } -} - -func runTestTCF2(t *testing.T, test test, server *httptest.Server) { - config := testConfig() - fetcher := newVendorListFetcherTCF2(context.Background(), config, server.Client(), testURLMaker(server)) + fetcher := newVendorListFetcher(context.Background(), config, server.Client(), testURLMaker(server)) vendorList, err := fetcher(context.Background(), test.setup.vendorListVersion) if test.expected.errorMessage != "" { @@ -387,8 +283,5 @@ func testConfig() config.GDPR { InitVendorlistFetch: 60 * 1000, ActiveVendorlistFetch: 1000 * 5, }, - TCF1: config.TCF1{ - FetchGVL: true, - }, } } diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go index 7c0a3f377e2..61930bf54f0 100644 --- a/metrics/go_metrics_test.go +++ b/metrics/go_metrics_test.go @@ -60,7 +60,6 @@ func TestNewMetrics(t *testing.T) { 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]) } @@ -548,25 +547,16 @@ func TestRecordRequestPrivacy(t *testing.T) { 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") } diff --git a/metrics/metrics.go b/metrics/metrics.go index 7cb5f0f1d88..5912a151bca 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -299,7 +299,6 @@ type TCFVersionValue string const ( TCFVersionErr TCFVersionValue = "err" - TCFVersionV1 TCFVersionValue = "v1" TCFVersionV2 TCFVersionValue = "v2" ) @@ -307,7 +306,6 @@ const ( func TCFVersions() []TCFVersionValue { return []TCFVersionValue{ TCFVersionErr, - TCFVersionV1, TCFVersionV2, } } @@ -315,8 +313,6 @@ func TCFVersions() []TCFVersionValue { // 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 } diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go index 72ddd105152..087ee570551 100644 --- a/metrics/prometheus/prometheus_test.go +++ b/metrics/prometheus/prometheus_test.go @@ -1390,18 +1390,10 @@ func TestRecordRequestPrivacy(t *testing.T) { GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionErr, }) - m.RecordRequestPrivacy(metrics.PrivacyLabels{ - GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, - }) m.RecordRequestPrivacy(metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, }) - m.RecordRequestPrivacy(metrics.PrivacyLabels{ - GDPREnforced: true, - GDPRTCFVersion: metrics.TCFVersionV1, - }) assertCounterVecValue(t, "", "privacy_ccpa", m.privacyCCPA, float64(1), @@ -1436,13 +1428,6 @@ func TestRecordRequestPrivacy(t *testing.T) { 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{ diff --git a/privacy/gdpr/policy_test.go b/privacy/gdpr/policy_test.go index dc8f56425c5..a0fa6241d72 100644 --- a/privacy/gdpr/policy_test.go +++ b/privacy/gdpr/policy_test.go @@ -17,11 +17,6 @@ func TestValidateConsent(t *testing.T) { consent: "", expected: false, }, - { - description: "TCF1 Valid", - consent: "BONV8oqONXwgmADACHENAO7pqzAAppY", - expected: true, - }, { description: "TCF2 Valid", consent: "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", diff --git a/static/tcf1/fallback_gvl.json b/static/tcf1/fallback_gvl.json deleted file mode 100644 index 9f1c8506b32..00000000000 --- a/static/tcf1/fallback_gvl.json +++ /dev/null @@ -1 +0,0 @@ -{"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 a9ee429449797f9a7fdf48a47c295fd5377cbf85 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:26:55 -0400 Subject: [PATCH 15/16] Rename GDPR UserSyncIfAmbiguous to DefaultValue (#1858) --- config/config.go | 9 +- config/config_test.go | 13 ++- endpoints/cookie_sync.go | 6 +- endpoints/cookie_sync_test.go | 4 +- exchange/exchange.go | 58 +++++------ exchange/exchange_test.go | 37 ++++--- exchange/targeting_test.go | 18 ++-- exchange/utils.go | 4 +- exchange/utils_test.go | 63 ++++++------ gdpr/impl.go | 2 +- gdpr/impl_test.go | 176 +++++++++++++++++----------------- 11 files changed, 209 insertions(+), 181 deletions(-) diff --git a/config/config.go b/config/config.go index 7e24a00d370..1260d28a779 100644 --- a/config/config.go +++ b/config/config.go @@ -193,7 +193,7 @@ type Privacy struct { type GDPR struct { Enabled bool `mapstructure:"enabled"` HostVendorID int `mapstructure:"host_vendor_id"` - UsersyncIfAmbiguous bool `mapstructure:"usersync_if_ambiguous"` + DefaultValue string `mapstructure:"default_value"` Timeouts GDPRTimeouts `mapstructure:"timeouts_ms"` NonStandardPublishers []string `mapstructure:"non_standard_publishers,flow"` NonStandardPublisherMap map[string]struct{} @@ -202,12 +202,15 @@ type GDPR struct { // 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 + // to DefaultValue EEACountries []string `mapstructure:"eea_countries"` EEACountriesMap map[string]struct{} } func (cfg *GDPR) validate(errs []error) []error { + if cfg.DefaultValue != "0" && cfg.DefaultValue != "1" { + errs = append(errs, fmt.Errorf("gdpr.default_value must be 0 or 1")) + } if cfg.HostVendorID < 0 || cfg.HostVendorID > 0xffff { errs = append(errs, fmt.Errorf("gdpr.host_vendor_id must be in the range [0, %d]. Got %d", 0xffff, cfg.HostVendorID)) } @@ -937,7 +940,7 @@ func SetupViper(v *viper.Viper, filename string) { 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.default_value", "1") 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{""}) diff --git a/config/config_test.go b/config/config_test.go index f34bd5fa189..a2b28d026d7 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -148,7 +148,7 @@ func TestDefaults(t *testing.T) { var fullConfig = []byte(` gdpr: host_vendor_id: 15 - usersync_if_ambiguous: true + default_value: "0" non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"] ccpa: enforce: true @@ -352,7 +352,7 @@ func TestFullConfig(t *testing.T) { 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) + cmpStrings(t, "gdpr.default_value", cfg.GDPR.DefaultValue, "0") //Assert the NonStandardPublishers was correctly unmarshalled cmpStrings(t, "gdpr.non_standard_publishers", cfg.GDPR.NonStandardPublishers[0], "siteID") @@ -460,6 +460,9 @@ func TestUnmarshalAdapterExtraInfo(t *testing.T) { func TestValidConfig(t *testing.T) { cfg := Configuration{ + GDPR: GDPR{ + DefaultValue: "1", + }, StoredRequests: StoredRequests{ Files: FileFetcherConfig{Enabled: true}, InMemoryCache: InMemoryCache{ @@ -650,6 +653,12 @@ func TestInvalidAMPException(t *testing.T) { assertOneError(t, cfg.validate(), "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)") } +func TestInvalidGDPRDefaultValue(t *testing.T) { + cfg := newDefaultConfig(t) + cfg.GDPR.DefaultValue = "2" + assertOneError(t, cfg.validate(), "gdpr.default_value must be 0 or 1") +} + func TestNegativeCurrencyConverterFetchInterval(t *testing.T) { cfg := Configuration{ CurrencyConverter: CurrencyConverter{ diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index bf3935f0535..3c1354c86bd 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -97,7 +97,7 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h } parsedReq := &cookieSyncRequest{} - if err := parseRequest(parsedReq, bodyBytes, deps.gDPR.UsersyncIfAmbiguous); err != nil { + if err := parseRequest(parsedReq, bodyBytes, deps.gDPR.DefaultValue); err != nil { co.Status = http.StatusBadRequest co.Errors = append(co.Errors, err) http.Error(w, co.Errors[len(co.Errors)-1].Error(), co.Status) @@ -181,7 +181,7 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h enc.Encode(csResp) } -func parseRequest(parsedReq *cookieSyncRequest, bodyBytes []byte, usersyncIfAmbiguous bool) error { +func parseRequest(parsedReq *cookieSyncRequest, bodyBytes []byte, gdprDefaultValue string) error { if err := json.Unmarshal(bodyBytes, parsedReq); err != nil { return fmt.Errorf("JSON parsing failed: %s", err.Error()) } @@ -193,7 +193,7 @@ func parseRequest(parsedReq *cookieSyncRequest, bodyBytes []byte, usersyncIfAmbi if parsedReq.GDPR == nil { var gdpr = new(int) *gdpr = 1 - if usersyncIfAmbiguous { + if gdprDefaultValue == "0" { *gdpr = 0 } parsedReq.GDPR = gdpr diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index 7632894baf6..2f56c262979 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -110,7 +110,7 @@ func TestCCPA(t *testing.T) { } for _, test := range testCases { - gdpr := config.GDPR{UsersyncIfAmbiguous: true} + gdpr := config.GDPR{DefaultValue: "0"} ccpa := config.CCPA{Enforce: test.enforceCCPA} rr := doConfigurablePost(test.requestBody, nil, true, syncersForTest(), gdpr, ccpa) assert.Equal(t, http.StatusOK, rr.Code, test.description+":httpResponseCode") @@ -149,7 +149,7 @@ func TestCookieSyncNoBidders(t *testing.T) { } func TestCookieSyncNoCookiesBrokenGDPR(t *testing.T) { - rr := doConfigurablePost(`{"bidders":["appnexus", "audienceNetwork", "random"],"gdpr_consent":"GLKHGKGKKGK"}`, nil, true, map[openrtb_ext.BidderName]usersync.Usersyncer{}, config.GDPR{UsersyncIfAmbiguous: true}, config.CCPA{}) + rr := doConfigurablePost(`{"bidders":["appnexus", "audienceNetwork", "random"],"gdpr_consent":"GLKHGKGKKGK"}`, nil, true, map[openrtb_ext.BidderName]usersync.Usersyncer{}, config.GDPR{DefaultValue: "0"}, config.CCPA{}) assert.Equal(t, rr.Header().Get("Content-Type"), "application/json; charset=utf-8") assert.Equal(t, http.StatusOK, rr.Code) assert.ElementsMatch(t, []string{"appnexus", "audienceNetwork"}, parseSyncs(t, rr.Body.Bytes())) diff --git a/exchange/exchange.go b/exchange/exchange.go index 6f0c610a958..7b156b5dee2 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -51,18 +51,18 @@ type IdFetcher interface { } type exchange struct { - adapterMap map[openrtb_ext.BidderName]adaptedBidder - bidderInfo config.BidderInfos - me metrics.MetricsEngine - cache prebid_cache_client.Client - cacheTime time.Duration - gDPR gdpr.Permissions - currencyConverter *currency.RateConverter - externalURL string - UsersyncIfAmbiguous bool - privacyConfig config.Privacy - categoriesFetcher stored_requests.CategoryFetcher - bidIDGenerator BidIDGenerator + adapterMap map[openrtb_ext.BidderName]adaptedBidder + bidderInfo config.BidderInfos + me metrics.MetricsEngine + cache prebid_cache_client.Client + cacheTime time.Duration + gDPR gdpr.Permissions + currencyConverter *currency.RateConverter + externalURL string + gdprDefaultValue string + privacyConfig config.Privacy + categoriesFetcher stored_requests.CategoryFetcher + bidIDGenerator BidIDGenerator } // Container to pass out response ext data from the GetAllBids goroutines back into the main thread @@ -111,16 +111,16 @@ func (randomDeduplicateBidBooleanGenerator) Generate() bool { func NewExchange(adapters map[openrtb_ext.BidderName]adaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gDPR gdpr.Permissions, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher) Exchange { return &exchange{ - adapterMap: adapters, - bidderInfo: infos, - cache: cache, - cacheTime: time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond, - categoriesFetcher: categoriesFetcher, - currencyConverter: currencyConverter, - externalURL: cfg.ExternalURL, - gDPR: gDPR, - me: metricsEngine, - UsersyncIfAmbiguous: cfg.GDPR.UsersyncIfAmbiguous, + adapterMap: adapters, + bidderInfo: infos, + cache: cache, + cacheTime: time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond, + categoriesFetcher: categoriesFetcher, + currencyConverter: currencyConverter, + externalURL: cfg.ExternalURL, + gDPR: gDPR, + me: metricsEngine, + gdprDefaultValue: cfg.GDPR.DefaultValue, privacyConfig: config.Privacy{ CCPA: cfg.CCPA, GDPR: cfg.GDPR, @@ -186,10 +186,10 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * recordImpMetrics(r.BidRequest, e.me) // Make our best guess if GDPR applies - usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(r.BidRequest) + gdprDefaultValue := e.parseGDPRDefaultValue(r.BidRequest) // Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder - bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(ctx, r, requestExt, e.gDPR, e.me, usersyncIfAmbiguous, e.privacyConfig, &r.Account) + bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(ctx, r, requestExt, e.gDPR, e.me, gdprDefaultValue, e.privacyConfig, &r.Account) e.me.RecordRequestPrivacy(privacyLabels) @@ -301,8 +301,8 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, errs) } -func (e *exchange) parseUsersyncIfAmbiguous(bidRequest *openrtb2.BidRequest) bool { - usersyncIfAmbiguous := e.UsersyncIfAmbiguous +func (e *exchange) parseGDPRDefaultValue(bidRequest *openrtb2.BidRequest) string { + gdprDefaultValue := e.gdprDefaultValue var geo *openrtb2.Geo = nil if bidRequest.User != nil && bidRequest.User.Geo != nil { @@ -314,14 +314,14 @@ func (e *exchange) parseUsersyncIfAmbiguous(bidRequest *openrtb2.BidRequest) boo // 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 + gdprDefaultValue = "1" } else if len(geo.Country) == 3 { // The country field is formatted properly as a three character country code - usersyncIfAmbiguous = true + gdprDefaultValue = "0" } } - return usersyncIfAmbiguous + return gdprDefaultValue } func recordImpMetrics(bidRequest *openrtb2.BidRequest, metricsEngine metrics.MetricsEngine) { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index cd36ad94b22..5a54b5e14d9 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -2004,6 +2004,13 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { eeac[c] = s } + var gdprDefaultValue string + if spec.AssumeGDPRApplies { + gdprDefaultValue = "1" + } else { + gdprDefaultValue = "0" + } + privacyConfig := config.Privacy{ CCPA: config.CCPA{ Enforce: spec.EnforceCCPA, @@ -2012,9 +2019,9 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { Enforce: spec.EnforceLMT, }, GDPR: config.GDPR{ - Enabled: spec.GDPREnabled, - UsersyncIfAmbiguous: !spec.AssumeGDPRApplies, - EEACountriesMap: eeac, + Enabled: spec.GDPREnabled, + DefaultValue: gdprDefaultValue, + EEACountriesMap: eeac, }, } bidIdGenerator := &mockBidIDGenerator{} @@ -2156,18 +2163,18 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] } return &exchange{ - adapterMap: bidderAdapters, - me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames()), - cache: &wellBehavedCache{}, - cacheTime: 0, - gDPR: &permissionsMock{allowAllBidders: true}, - currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), - UsersyncIfAmbiguous: privacyConfig.GDPR.UsersyncIfAmbiguous, - privacyConfig: privacyConfig, - categoriesFetcher: categoriesFetcher, - bidderInfo: bidderInfos, - externalURL: "http://localhost", - bidIDGenerator: bidIDGenerator, + adapterMap: bidderAdapters, + me: metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames()), + cache: &wellBehavedCache{}, + cacheTime: 0, + gDPR: &permissionsMock{allowAllBidders: true}, + currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), + gdprDefaultValue: privacyConfig.GDPR.DefaultValue, + privacyConfig: privacyConfig, + categoriesFetcher: categoriesFetcher, + bidderInfo: bidderInfos, + externalURL: "http://localhost", + bidIDGenerator: bidIDGenerator, } } diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index aa07ed0c77b..86646957091 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -87,15 +87,15 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op } ex := &exchange{ - adapterMap: buildAdapterMap(mockBids, server.URL, server.Client()), - me: &metricsConf.DummyMetricsEngine{}, - cache: &wellBehavedCache{}, - cacheTime: time.Duration(0), - gDPR: gdpr.AlwaysAllow{}, - currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), - UsersyncIfAmbiguous: false, - categoriesFetcher: categoriesFetcher, - bidIDGenerator: &mockBidIDGenerator{false, false}, + adapterMap: buildAdapterMap(mockBids, server.URL, server.Client()), + me: &metricsConf.DummyMetricsEngine{}, + cache: &wellBehavedCache{}, + cacheTime: time.Duration(0), + gDPR: gdpr.AlwaysAllow{}, + currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), + gdprDefaultValue: "1", + categoriesFetcher: categoriesFetcher, + bidIDGenerator: &mockBidIDGenerator{false, false}, } imps := buildImps(t, mockBids) diff --git a/exchange/utils.go b/exchange/utils.go index 3d5e2374008..c5cf673250d 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -57,7 +57,7 @@ func cleanOpenRTBRequests(ctx context.Context, requestExt *openrtb_ext.ExtRequest, gDPR gdpr.Permissions, metricsEngine metrics.MetricsEngine, - usersyncIfAmbiguous bool, + gdprDefaultValue string, privacyConfig config.Privacy, account *config.Account) (allowedBidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) { @@ -87,7 +87,7 @@ func cleanOpenRTBRequests(ctx context.Context, if err != nil { errs = append(errs, err) } - gdprEnforced := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && !usersyncIfAmbiguous) + gdprEnforced := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && gdprDefaultValue == "1") ccpaEnforcer, err := extractCCPA(req.BidRequest, privacyConfig, &req.Account, aliases, integrationTypeMap[req.LegacyLabels.RType]) if err != nil { diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 1788d508c31..dad0d69db15 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -479,7 +479,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { for _, test := range testCases { metricsMock := metrics.MetricsEngineMock{} permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true} - bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissions, &metricsMock, true, privacyConfig, nil) + bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, &permissions, &metricsMock, "0", privacyConfig, nil) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -636,7 +636,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { nil, &permissionsMock{allowAllBidders: true, passGeo: true, passID: true}, &metrics.MetricsEngineMock{}, - true, + "0", privacyConfig, nil) result := bidderRequests[0] @@ -698,7 +698,7 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { } permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true} metrics := metrics.MetricsEngineMock{} - _, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissions, &metrics, true, privacyConfig, nil) + _, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, &permissions, &metrics, "0", privacyConfig, nil) assert.ElementsMatch(t, []error{test.expectError}, errs, test.description) } @@ -740,7 +740,7 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true} metrics := metrics.MetricsEngineMock{} - bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, true, config.Privacy{}, nil) + bidderRequests, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, "0", config.Privacy{}, nil) result := bidderRequests[0] assert.Nil(t, errs) @@ -849,7 +849,7 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true} metrics := metrics.MetricsEngineMock{} - bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissions, &metrics, true, config.Privacy{}, nil) + bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, &permissions, &metrics, "0", config.Privacy{}, nil) if test.hasError == true { assert.NotNil(t, errs) assert.Len(t, bidderRequests, 0) @@ -1432,7 +1432,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true} metrics := metrics.MetricsEngineMock{} - results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, true, privacyConfig, nil) + results, privacyLabels, errs := cleanOpenRTBRequests(context.Background(), auctionReq, nil, &permissions, &metrics, "0", privacyConfig, nil) result := results[0] assert.Nil(t, errs) @@ -1459,7 +1459,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprConsent string gdprScrub bool permissionsError error - userSyncIfAmbiguous bool + gdprDefaultValue string expectPrivacyLabels metrics.PrivacyLabels expectError bool }{ @@ -1470,6 +1470,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "1", gdprConsent: "malformed", gdprScrub: false, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: "", @@ -1482,6 +1483,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "1", gdprConsent: tcf2Consent, gdprScrub: true, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, @@ -1494,6 +1496,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "0", gdprConsent: tcf2Consent, gdprScrub: false, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, GDPRTCFVersion: "", @@ -1506,6 +1509,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "0{", gdprConsent: tcf2Consent, gdprScrub: true, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, @@ -1519,6 +1523,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "1", gdprConsent: tcf2Consent, gdprScrub: true, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, @@ -1531,6 +1536,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "1", gdprConsent: tcf2Consent, gdprScrub: false, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, GDPRTCFVersion: "", @@ -1543,6 +1549,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "1", gdprConsent: tcf2Consent, gdprScrub: true, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, @@ -1555,32 +1562,33 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdpr: "1", gdprConsent: tcf2Consent, gdprScrub: false, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, GDPRTCFVersion: "", }, }, { - description: "Enforce - Ambiguous signal, don't sync user if ambiguous", - gdprAccountEnabled: nil, - gdprHostEnabled: true, - gdpr: "null", - gdprConsent: tcf2Consent, - gdprScrub: true, - userSyncIfAmbiguous: false, + description: "Enforce - Ambiguous signal, don't sync user if ambiguous", + gdprAccountEnabled: nil, + gdprHostEnabled: true, + gdpr: "null", + gdprConsent: tcf2Consent, + gdprScrub: true, + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, }, }, { - description: "Not Enforce - Ambiguous signal, sync user if ambiguous", - gdprAccountEnabled: nil, - gdprHostEnabled: true, - gdpr: "null", - gdprConsent: tcf2Consent, - gdprScrub: false, - userSyncIfAmbiguous: true, + description: "Not Enforce - Ambiguous signal, sync user if ambiguous", + gdprAccountEnabled: nil, + gdprHostEnabled: true, + gdpr: "null", + gdprConsent: tcf2Consent, + gdprScrub: false, + gdprDefaultValue: "0", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: false, GDPRTCFVersion: "", @@ -1594,6 +1602,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { gdprConsent: tcf2Consent, gdprScrub: true, permissionsError: errors.New("Some error"), + gdprDefaultValue: "1", expectPrivacyLabels: metrics.PrivacyLabels{ GDPREnforced: true, GDPRTCFVersion: metrics.TCFVersionV2, @@ -1610,8 +1619,8 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { privacyConfig := config.Privacy{ GDPR: config.GDPR{ - Enabled: test.gdprHostEnabled, - UsersyncIfAmbiguous: test.userSyncIfAmbiguous, + Enabled: test.gdprHostEnabled, + DefaultValue: test.gdprDefaultValue, TCF2: config.TCF2{ Enabled: true, }, @@ -1636,7 +1645,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { nil, &permissionsMock{allowAllBidders: true, passGeo: !test.gdprScrub, passID: !test.gdprScrub, activitiesError: test.permissionsError}, &metrics.MetricsEngineMock{}, - test.userSyncIfAmbiguous, + test.gdprDefaultValue, privacyConfig, nil) result := results[0] @@ -1698,8 +1707,8 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { privacyConfig := config.Privacy{ GDPR: config.GDPR{ - Enabled: test.gdprEnforced, - UsersyncIfAmbiguous: true, + Enabled: test.gdprEnforced, + DefaultValue: "0", TCF2: config.TCF2{ Enabled: true, }, @@ -1727,7 +1736,7 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { nil, &permissionsMock{allowedBidders: test.gdprAllowedBidders, passGeo: true, passID: true, activitiesError: nil}, &metricsMock, - true, + "0", privacyConfig, nil) diff --git a/gdpr/impl.go b/gdpr/impl.go index aca07fd068d..9c09e90b58e 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -94,7 +94,7 @@ func (p *permissionsImpl) normalizeGDPR(gdprSignal Signal) Signal { return gdprSignal } - if p.cfg.UsersyncIfAmbiguous { + if p.cfg.DefaultValue == "0" { return SignalNo } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index d26c0c57231..345dd52621d 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -18,8 +18,8 @@ import ( func TestDisallowOnEmptyConsent(t *testing.T) { perms := permissionsImpl{ cfg: config.GDPR{ - HostVendorID: 3, - UsersyncIfAmbiguous: true, + HostVendorID: 3, + DefaultValue: "0", }, vendorIDs: nil, fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){ @@ -190,84 +190,84 @@ func TestAllowActivities(t *testing.T) { description string bidderName openrtb_ext.BidderName publisherID string - userSyncIfAmbiguous bool + gdprDefaultValue string gdpr Signal consent string passID bool weakVendorEnforcement bool }{ { - description: "Allow PI - Non standard publisher", - bidderName: bidderBlockedByConsent, - publisherID: "appNexusAppID", - userSyncIfAmbiguous: false, - gdpr: SignalYes, - consent: vendor2AndPurpose2Consent, - passID: true, + description: "Allow PI - Non standard publisher", + bidderName: bidderBlockedByConsent, + publisherID: "appNexusAppID", + gdprDefaultValue: "1", + gdpr: SignalYes, + consent: vendor2AndPurpose2Consent, + passID: true, }, { - description: "Allow PI - known vendor with No GDPR", - bidderName: bidderBlockedByConsent, - userSyncIfAmbiguous: false, - gdpr: SignalNo, - consent: vendor2AndPurpose2Consent, - passID: true, + description: "Allow PI - known vendor with No GDPR", + bidderName: bidderBlockedByConsent, + gdprDefaultValue: "1", + gdpr: SignalNo, + consent: vendor2AndPurpose2Consent, + passID: true, }, { - description: "Allow PI - known vendor with Yes GDPR", - bidderName: bidderAllowedByConsent, - userSyncIfAmbiguous: false, - gdpr: SignalYes, - consent: vendor2AndPurpose2Consent, - passID: true, + description: "Allow PI - known vendor with Yes GDPR", + bidderName: bidderAllowedByConsent, + gdprDefaultValue: "1", + gdpr: SignalYes, + consent: vendor2AndPurpose2Consent, + passID: 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: "", - passID: true, + description: "PI allowed according to host setting gdprDefaultValue 0 - known vendor with ambiguous GDPR and empty consent", + bidderName: bidderAllowedByConsent, + gdprDefaultValue: "0", + gdpr: SignalAmbiguous, + consent: "", + passID: 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: vendor2AndPurpose2Consent, - passID: true, + description: "PI allowed according to host setting gdprDefaultValue 0 - known vendor with ambiguous GDPR and non-empty consent", + bidderName: bidderAllowedByConsent, + gdprDefaultValue: "0", + gdpr: SignalAmbiguous, + consent: vendor2AndPurpose2Consent, + passID: 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: "", - passID: false, + description: "PI allowed according to host setting gdprDefaultValue 1 - known vendor with ambiguous GDPR and empty consent", + bidderName: bidderAllowedByConsent, + gdprDefaultValue: "1", + gdpr: SignalAmbiguous, + consent: "", + passID: 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: vendor2AndPurpose2Consent, - passID: true, + description: "PI allowed according to host setting gdprDefaultValue 1 - known vendor with ambiguous GDPR and non-empty consent", + bidderName: bidderAllowedByConsent, + gdprDefaultValue: "1", + gdpr: SignalAmbiguous, + consent: vendor2AndPurpose2Consent, + passID: true, }, { - description: "Don't allow PI - known vendor with Yes GDPR and empty consent", - bidderName: bidderAllowedByConsent, - userSyncIfAmbiguous: false, - gdpr: SignalYes, - consent: "", - passID: false, + description: "Don't allow PI - known vendor with Yes GDPR and empty consent", + bidderName: bidderAllowedByConsent, + gdprDefaultValue: "1", + gdpr: SignalYes, + consent: "", + passID: false, }, { - description: "Don't allow PI - default vendor with Yes GDPR and non-empty consent", - bidderName: bidderBlockedByConsent, - userSyncIfAmbiguous: false, - gdpr: SignalYes, - consent: vendor2AndPurpose2Consent, - passID: false, + description: "Don't allow PI - default vendor with Yes GDPR and non-empty consent", + bidderName: bidderBlockedByConsent, + gdprDefaultValue: "1", + gdpr: SignalYes, + consent: vendor2AndPurpose2Consent, + passID: false, }, } vendorListData := MarshalVendorList(vendorList{ @@ -302,7 +302,7 @@ func TestAllowActivities(t *testing.T) { } for _, tt := range tests { - perms.cfg.UsersyncIfAmbiguous = tt.userSyncIfAmbiguous + perms.cfg.DefaultValue = tt.gdprDefaultValue _, _, passID, err := perms.AuctionActivitiesAllowed(context.Background(), tt.bidderName, tt.publisherID, tt.gdpr, tt.consent, tt.weakVendorEnforcement) @@ -669,53 +669,53 @@ func assertStringsEqual(t *testing.T, expected string, actual string) { func TestNormalizeGDPR(t *testing.T) { tests := []struct { - description string - userSyncIfAmbiguous bool - giveSignal Signal - wantSignal Signal + description string + gdprDefaultValue string + giveSignal Signal + wantSignal Signal }{ { - description: "Don't normalize - Signal No and userSyncIfAmbiguous false", - userSyncIfAmbiguous: false, - giveSignal: SignalNo, - wantSignal: SignalNo, + description: "Don't normalize - Signal No and gdprDefaultValue 1", + gdprDefaultValue: "1", + giveSignal: SignalNo, + wantSignal: SignalNo, }, { - description: "Don't normalize - Signal No and userSyncIfAmbiguous true", - userSyncIfAmbiguous: true, - giveSignal: SignalNo, - wantSignal: SignalNo, + description: "Don't normalize - Signal No and gdprDefaultValue 0", + gdprDefaultValue: "0", + giveSignal: SignalNo, + wantSignal: SignalNo, }, { - description: "Don't normalize - Signal Yes and userSyncIfAmbiguous false", - userSyncIfAmbiguous: false, - giveSignal: SignalYes, - wantSignal: SignalYes, + description: "Don't normalize - Signal Yes and gdprDefaultValue 1", + gdprDefaultValue: "1", + giveSignal: SignalYes, + wantSignal: SignalYes, }, { - description: "Don't normalize - Signal Yes and userSyncIfAmbiguous true", - userSyncIfAmbiguous: true, - giveSignal: SignalYes, - wantSignal: SignalYes, + description: "Don't normalize - Signal Yes and gdprDefaultValue 0", + gdprDefaultValue: "0", + giveSignal: SignalYes, + wantSignal: SignalYes, }, { - description: "Normalize - Signal Ambiguous and userSyncIfAmbiguous false", - userSyncIfAmbiguous: false, - giveSignal: SignalAmbiguous, - wantSignal: SignalYes, + description: "Normalize - Signal Ambiguous and gdprDefaultValue 1", + gdprDefaultValue: "1", + giveSignal: SignalAmbiguous, + wantSignal: SignalYes, }, { - description: "Normalize - Signal Ambiguous and userSyncIfAmbiguous true", - userSyncIfAmbiguous: true, - giveSignal: SignalAmbiguous, - wantSignal: SignalNo, + description: "Normalize - Signal Ambiguous and gdprDefaultValue 0", + gdprDefaultValue: "0", + giveSignal: SignalAmbiguous, + wantSignal: SignalNo, }, } for _, tt := range tests { perms := permissionsImpl{ cfg: config.GDPR{ - UsersyncIfAmbiguous: tt.userSyncIfAmbiguous, + DefaultValue: tt.gdprDefaultValue, }, } From 037bd7f19920e01ee2ee2504410b9b84f134bf92 Mon Sep 17 00:00:00 2001 From: Rachel Joyce Date: Tue, 15 Jun 2021 10:19:30 -0600 Subject: [PATCH 16/16] Accept bidfloor from impression to fix issue #1787 for sovrn adapter (#1886) --- adapters/sovrn/sovrn.go | 5 +- .../both-custom-default-bidfloor.json | 126 ++++++++++++++++++ .../sovrntest/supplemental/no-bidfloor.json | 122 +++++++++++++++++ .../supplemental/only-custom-bidfloor.json | 125 +++++++++++++++++ .../supplemental/only-default-bidfloor.json | 124 +++++++++++++++++ 5 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 adapters/sovrn/sovrntest/supplemental/both-custom-default-bidfloor.json create mode 100644 adapters/sovrn/sovrntest/supplemental/no-bidfloor.json create mode 100644 adapters/sovrn/sovrntest/supplemental/only-custom-bidfloor.json create mode 100644 adapters/sovrn/sovrntest/supplemental/only-default-bidfloor.json diff --git a/adapters/sovrn/sovrn.go b/adapters/sovrn/sovrn.go index be1c2221ae5..40969d3638e 100644 --- a/adapters/sovrn/sovrn.go +++ b/adapters/sovrn/sovrn.go @@ -287,7 +287,10 @@ func preprocess(imp *openrtb2.Imp) (string, error) { } imp.TagID = getTagid(sovrnExt) - imp.BidFloor = sovrnExt.BidFloor + + if imp.BidFloor == 0 && sovrnExt.BidFloor > 0 { + imp.BidFloor = sovrnExt.BidFloor + } return imp.TagID, nil } diff --git a/adapters/sovrn/sovrntest/supplemental/both-custom-default-bidfloor.json b/adapters/sovrn/sovrntest/supplemental/both-custom-default-bidfloor.json new file mode 100644 index 00000000000..4b997b68266 --- /dev/null +++ b/adapters/sovrn/sovrntest/supplemental/both-custom-default-bidfloor.json @@ -0,0 +1,126 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloor": 1.69, + "ext": { + "bidder": { + "tagid": "123456", + "bidfloor": 4.20 + } + } + } + ], + "device": { }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json"], + "Cookie": ["ljt_reader=test_reader_id"] + }, + "uri": "http://sovrn.com/test/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "123456", + "bidfloor": 1.69, + "ext": { + "bidder": { + "tagid": "123456", + "bidfloor": 4.20 + } + } + } + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + }, + "device": { } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "adm": "some-test-ad", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "adm": "some-test-ad", + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/sovrn/sovrntest/supplemental/no-bidfloor.json b/adapters/sovrn/sovrntest/supplemental/no-bidfloor.json new file mode 100644 index 00000000000..0aa3ad74e62 --- /dev/null +++ b/adapters/sovrn/sovrntest/supplemental/no-bidfloor.json @@ -0,0 +1,122 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "tagid": "123456" + } + } + } + ], + "device": { }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json"], + "Cookie": ["ljt_reader=test_reader_id"] + }, + "uri": "http://sovrn.com/test/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "123456", + "ext": { + "bidder": { + "tagid": "123456" + } + } + } + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + }, + "device": { } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "adm": "some-test-ad", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "adm": "some-test-ad", + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/sovrn/sovrntest/supplemental/only-custom-bidfloor.json b/adapters/sovrn/sovrntest/supplemental/only-custom-bidfloor.json new file mode 100644 index 00000000000..3cd6539f988 --- /dev/null +++ b/adapters/sovrn/sovrntest/supplemental/only-custom-bidfloor.json @@ -0,0 +1,125 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "tagid": "123456", + "bidfloor": 4.20 + } + } + } + ], + "device": { }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json"], + "Cookie": ["ljt_reader=test_reader_id"] + }, + "uri": "http://sovrn.com/test/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "123456", + "bidfloor": 4.20, + "ext": { + "bidder": { + "tagid": "123456", + "bidfloor": 4.20 + } + } + } + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + }, + "device": { } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "adm": "some-test-ad", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "adm": "some-test-ad", + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/sovrn/sovrntest/supplemental/only-default-bidfloor.json b/adapters/sovrn/sovrntest/supplemental/only-default-bidfloor.json new file mode 100644 index 00000000000..cb74e5643b6 --- /dev/null +++ b/adapters/sovrn/sovrntest/supplemental/only-default-bidfloor.json @@ -0,0 +1,124 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloor": 1.69, + "ext": { + "bidder": { + "tagid": "123456" + } + } + } + ], + "device": { }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + } + }, + + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Content-Type": ["application/json"], + "Cookie": ["ljt_reader=test_reader_id"] + }, + "uri": "http://sovrn.com/test/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "123456", + "bidfloor": 1.69, + "ext": { + "bidder": { + "tagid": "123456" + } + } + } + ], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site" + }, + "user": { + "buyeruid": "test_reader_id" + }, + "device": { } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "adm": "some-test-ad", + "w": 300, + "h": 250 + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "a_449642_554a13d3b9f348fba707cf83f0f63800", + "impid": "test-imp-id", + "price": 3.5, + "adm": "some-test-ad", + "nurl": "http://sovrn.com/rtb/impression?bannerid=138743&campaignid=3699&zoneid=449642&cb=69493397&tid=a_449642_554a13d3b9f348fba707cf83f0f63800", + "w": 300, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +}