From 6a011ed23d02a181d9ed487c105967ed91b98edf Mon Sep 17 00:00:00 2001 From: ccorbo Date: Wed, 11 Sep 2024 09:53:54 -0400 Subject: [PATCH 01/47] Update github.com/rs/cors to v1.11.0 (#3884) Co-authored-by: Chris Corbo --- go.mod | 2 +- go.sum | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c16acc331df..81f94ad1ccf 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_model v0.2.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 - github.com/rs/cors v1.8.2 + github.com/rs/cors v1.11.0 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.8.1 github.com/vrischmann/go-metrics-influxdb v0.1.1 diff --git a/go.sum b/go.sum index 00827d6fb6c..8ada75c73cb 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,7 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= @@ -316,9 +317,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= @@ -436,8 +439,9 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= -github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= From b920cca0c426eed97e44ea17d28dfbf4dd8f868d Mon Sep 17 00:00:00 2001 From: Yanivplaydigo <165155195+Yanivplaydigo@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:19:12 +0300 Subject: [PATCH 02/47] Playdigo: Add tcfeu support (#3890) --- static/bidder-info/playdigo.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/static/bidder-info/playdigo.yaml b/static/bidder-info/playdigo.yaml index 1ca35cfd0a2..b90b680f183 100644 --- a/static/bidder-info/playdigo.yaml +++ b/static/bidder-info/playdigo.yaml @@ -3,6 +3,7 @@ geoscope: - USA maintainer: email: "yr@playdigo.com" +gvlVendorID: 1302 capabilities: site: mediaTypes: From e0a21d0c355ccc6cc903259a3435c7939151e66c Mon Sep 17 00:00:00 2001 From: qt-io <104574052+qt-io@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:31:58 +0300 Subject: [PATCH 03/47] QT: Add tcfeu support (#3892) Co-authored-by: qt-io --- static/bidder-info/qt.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/static/bidder-info/qt.yaml b/static/bidder-info/qt.yaml index 8c040144bc8..a8d16e574bd 100644 --- a/static/bidder-info/qt.yaml +++ b/static/bidder-info/qt.yaml @@ -1,6 +1,7 @@ endpoint: "https://endpoint1.qt.io/pserver" maintainer: email: "qtssp-support@qt.io" +gvlVendorID: 1331 capabilities: site: mediaTypes: From 6cbedf0319f6b82946bfde02f8cdc4875f518a69 Mon Sep 17 00:00:00 2001 From: schubert-sc <144821265+schubert-sc@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:41:53 +0300 Subject: [PATCH 04/47] Smartx: Declare OpenRTB 2.6 support (#3896) --- static/bidder-info/smartx.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/bidder-info/smartx.yaml b/static/bidder-info/smartx.yaml index 9a387ecfbd2..a3c96a57528 100644 --- a/static/bidder-info/smartx.yaml +++ b/static/bidder-info/smartx.yaml @@ -1,4 +1,6 @@ endpoint: "https://bid.smartclip.net/bid/1005" +openrtb: + version: 2.6 maintainer: email: "bidding@smartclip.tv" gvlVendorID: 115 From 7613ff5a2aa304522854ac1b61dafd780caea8d7 Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Thu, 12 Sep 2024 12:27:07 -0700 Subject: [PATCH 05/47] Update adapter json test framework to validate BidVideo (#3835) --- adapters/adapterstest/test_json.go | 19 ++++++++++++++++--- .../freewheelssptest/exemplary/multi-imp.json | 3 ++- .../pubmatictest/exemplary/video.json | 3 ++- .../yeahmobitest/exemplary/simple-video.json | 3 ++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go index 1c10513919b..78f08421b3d 100644 --- a/adapters/adapterstest/test_json.go +++ b/adapters/adapterstest/test_json.go @@ -226,9 +226,10 @@ type expectedBidResponse struct { } type expectedBid struct { - Bid json.RawMessage `json:"bid"` - Type string `json:"type"` - Seat string `json:"seat"` + Bid json.RawMessage `json:"bid"` + Type string `json:"type"` + Seat string `json:"seat"` + Video json.RawMessage `json:"video,omitempty"` } // --------------------------------------- @@ -342,6 +343,9 @@ func diffBids(t *testing.T, description string, actual *adapters.TypedBid, expec assert.Equal(t, string(expected.Seat), string(actual.Seat), fmt.Sprintf(`%s.seat "%s" does not match expected "%s."`, description, string(actual.Seat), string(expected.Seat))) assert.Equal(t, string(expected.Type), string(actual.BidType), fmt.Sprintf(`%s.type "%s" does not match expected "%s."`, description, string(actual.BidType), string(expected.Type))) assert.NoError(t, diffOrtbBids(fmt.Sprintf("%s.bid", description), actual.Bid, expected.Bid)) + if expected.Video != nil { + assert.NoError(t, diffBidVideo(fmt.Sprintf("%s.video", description), actual.BidVideo, expected.Video)) + } } // diffOrtbBids compares the actual Bid made by the adapter to the expectation from the JSON file. @@ -358,6 +362,15 @@ func diffOrtbBids(description string, actual *openrtb2.Bid, expected json.RawMes return diffJson(description, actualJson, expected) } +func diffBidVideo(description string, actual *openrtb_ext.ExtBidPrebidVideo, expected json.RawMessage) error { + actualJson, err := json.Marshal(actual) + if err != nil { + return fmt.Errorf("%s failed to marshal actual Bid Video into JSON. %v", description, err) + } + + return diffJson(description, actualJson, []byte(expected)) +} + // diffJson compares two JSON byte arrays for structural equality. It will produce an error if either // byte array is not actually JSON. func diffJson(description string, actual []byte, expected []byte) error { diff --git a/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json b/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json index 2562a812edc..18a1dbdbdef 100644 --- a/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json +++ b/adapters/freewheelssp/freewheelssptest/exemplary/multi-imp.json @@ -153,7 +153,8 @@ }, "type": "video", "video" : { - "duration" : 10 + "duration" : 10, + "primary_category": "" } } ] diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json index be24d849cca..f8bdaee6e8c 100644 --- a/adapters/pubmatic/pubmatictest/exemplary/video.json +++ b/adapters/pubmatic/pubmatictest/exemplary/video.json @@ -177,7 +177,8 @@ }, "type": "video", "video" :{ - "duration" : 5 + "duration" : 5, + "primary_category": "" } } ] diff --git a/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json b/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json index af07de8f8bb..5538150f450 100644 --- a/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json +++ b/adapters/yeahmobi/yeahmobitest/exemplary/simple-video.json @@ -96,7 +96,8 @@ }, "type": "video", "video": { - "duration": 300 + "duration": 300, + "primary_category": "" } } ] From c02ee8c13280f72e173fee2adae441b92fb218ae Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Thu, 12 Sep 2024 12:34:46 -0700 Subject: [PATCH 06/47] Update exchange json tests with correct hb_pb_cat_dur (#3836) --- exchange/exchange_test.go | 8 +++++--- exchange/exchangetest/append-bidder-names.json | 8 ++++++-- exchange/exchangetest/debuglog_disabled.json | 8 ++++++-- exchange/exchangetest/debuglog_enabled.json | 8 ++++++-- exchange/exchangetest/generate-bid-id-error.json | 8 ++++++-- exchange/exchangetest/generate-bid-id-many.json | 15 ++++++++++++--- exchange/exchangetest/generate-bid-id-one.json | 9 +++++++-- exchange/exchangetest/include-brand-category.json | 8 ++++++-- 8 files changed, 54 insertions(+), 18 deletions(-) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index bab8dded1c5..aec062b70c6 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -5568,9 +5568,10 @@ type bidderSeatBid struct { // bidderBid is basically a subset of entities.PbsOrtbBid from exchange/bidder.go. // See the comment on bidderSeatBid for more info. type bidderBid struct { - Bid *openrtb2.Bid `json:"ortbBid,omitempty"` - Type string `json:"bidType,omitempty"` - Meta *openrtb_ext.ExtBidPrebidMeta `json:"bidMeta,omitempty"` + Bid *openrtb2.Bid `json:"ortbBid,omitempty"` + Type string `json:"bidType,omitempty"` + BidVideo *openrtb_ext.ExtBidPrebidVideo `json:"bidVideo,omitempty"` + Meta *openrtb_ext.ExtBidPrebidMeta `json:"bidMeta,omitempty"` } type mockIdFetcher map[string]string @@ -5617,6 +5618,7 @@ func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderR bids[i] = &entities.PbsOrtbBid{ OriginalBidCPM: mockSeatBid.Bids[i].Bid.Price, Bid: mockSeatBid.Bids[i].Bid, + BidVideo: mockSeatBid.Bids[i].BidVideo, BidType: openrtb_ext.BidType(mockSeatBid.Bids[i].Type), BidMeta: mockSeatBid.Bids[i].Meta, } diff --git a/exchange/exchangetest/append-bidder-names.json b/exchange/exchangetest/append-bidder-names.json index bc45321303f..0be1fdca522 100644 --- a/exchange/exchangetest/append-bidder-names.json +++ b/exchange/exchangetest/append-bidder-names.json @@ -148,10 +148,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", + "hb_pb_cat_dur": "0.20_VideoGames_30s_appnexus", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s_appnexus", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/debuglog_disabled.json b/exchange/exchangetest/debuglog_disabled.json index ce0669cf6fb..4b1ad744be4 100644 --- a/exchange/exchangetest/debuglog_disabled.json +++ b/exchange/exchangetest/debuglog_disabled.json @@ -157,10 +157,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_pb_cat_dur": "0.20_VideoGames_30s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/debuglog_enabled.json b/exchange/exchangetest/debuglog_enabled.json index 71a0bc06e96..faf3834a655 100644 --- a/exchange/exchangetest/debuglog_enabled.json +++ b/exchange/exchangetest/debuglog_enabled.json @@ -159,10 +159,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_pb_cat_dur": "0.20_VideoGames_30s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/generate-bid-id-error.json b/exchange/exchangetest/generate-bid-id-error.json index c38d0c97e3b..3536f38b3d1 100644 --- a/exchange/exchangetest/generate-bid-id-error.json +++ b/exchange/exchangetest/generate-bid-id-error.json @@ -121,10 +121,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", + "hb_pb_cat_dur": "0.20_VideoGames_30s_appnexus", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s_appnexus", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } diff --git a/exchange/exchangetest/generate-bid-id-many.json b/exchange/exchangetest/generate-bid-id-many.json index 8a55d1976d4..ecdefbaa71b 100644 --- a/exchange/exchangetest/generate-bid-id-many.json +++ b/exchange/exchangetest/generate-bid-id-many.json @@ -67,7 +67,8 @@ }, "bidType": "video", "bidVideo": { - "duration": 30 + "duration": 30, + "PrimaryCategory": "" } } ], @@ -100,7 +101,11 @@ "meta": { }, "bidid": "bid-appnexus-1", - "type": "video" + "type": "video", + "video": { + "duration": 30, + "primary_category": "" + } } } }, @@ -120,7 +125,11 @@ "meta": { }, "bidid": "bid-appnexus-2", - "type": "video" + "type": "video", + "video": { + "duration": 30, + "primary_category": "" + } } } } diff --git a/exchange/exchangetest/generate-bid-id-one.json b/exchange/exchangetest/generate-bid-id-one.json index e14e5db22b9..ccd6abd05f2 100644 --- a/exchange/exchangetest/generate-bid-id-one.json +++ b/exchange/exchangetest/generate-bid-id-one.json @@ -50,7 +50,8 @@ }, "bidType": "video", "bidVideo": { - "duration": 30 + "duration": 30, + "primary_category": "" } } ], @@ -83,7 +84,11 @@ "meta": { }, "bidid": "bid-appnexus-1", - "type": "video" + "type": "video", + "video": { + "duration": 30, + "primary_category": "" + } } } } diff --git a/exchange/exchangetest/include-brand-category.json b/exchange/exchangetest/include-brand-category.json index 74950598ec2..7334cf1a6c1 100644 --- a/exchange/exchangetest/include-brand-category.json +++ b/exchange/exchangetest/include-brand-category.json @@ -146,10 +146,14 @@ "hb_cache_path_appnex": "/pbcache/endpoint", "hb_pb": "0.20", "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s", + "hb_pb_cat_dur": "0.20_VideoGames_30s", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_30s", "hb_size": "200x250", "hb_size_appnexus": "200x250" + }, + "video": { + "duration": 30, + "primary_category": "" } } } From ffdd75f6fe1f24cf8ed7891b6af7b65afbda9761 Mon Sep 17 00:00:00 2001 From: Robert Kawecki Date: Thu, 12 Sep 2024 21:52:51 +0200 Subject: [PATCH 07/47] New Adapter: AdTonos (#3853) --- adapters/adtonos/adtonos.go | 142 ++++++++++++++++++ adapters/adtonos/adtonos_test.go | 30 ++++ .../exemplary/simple-audio-with-mtype.json | 115 ++++++++++++++ .../adtonostest/exemplary/simple-audio.json | 113 ++++++++++++++ .../adtonostest/exemplary/simple-video.json | 113 ++++++++++++++ .../wrong-impression-mapping.json | 103 +++++++++++++ adapters/adtonos/params_test.go | 43 ++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adtonos.go | 5 + static/bidder-info/adtonos.yaml | 24 +++ static/bidder-params/adtonos.json | 14 ++ 12 files changed, 706 insertions(+) create mode 100644 adapters/adtonos/adtonos.go create mode 100644 adapters/adtonos/adtonos_test.go create mode 100644 adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json create mode 100644 adapters/adtonos/adtonostest/exemplary/simple-audio.json create mode 100644 adapters/adtonos/adtonostest/exemplary/simple-video.json create mode 100644 adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json create mode 100644 adapters/adtonos/params_test.go create mode 100644 openrtb_ext/imp_adtonos.go create mode 100644 static/bidder-info/adtonos.yaml create mode 100644 static/bidder-params/adtonos.json diff --git a/adapters/adtonos/adtonos.go b/adapters/adtonos/adtonos.go new file mode 100644 index 00000000000..dff60733955 --- /dev/null +++ b/adapters/adtonos/adtonos.go @@ -0,0 +1,142 @@ +package adtonos + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpointTemplate *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpointTemplate: template, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Invalid imp.ext for impression index %d. Error Infomation: %s", 0, err.Error()), + }} + } + var impExt openrtb_ext.ImpExtAdTonos + if err := json.Unmarshal(bidderExt.Bidder, &impExt); err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Invalid imp.ext.bidder for impression index %d. Error Infomation: %s", 0, err.Error()), + }} + } + + endpoint, err := a.buildEndpointURL(&impExt) + if err != nil { + return nil, []error{err} + } + + requestJson, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: endpoint, + Body: requestJson, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + } + + return []*adapters.RequestData{requestData}, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ImpExtAdTonos) (string, error) { + endpointParams := macros.EndpointTemplateParams{PublisherID: params.SupplierID} + return macros.ResolveMacros(a.endpointTemplate, endpointParams) +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + var errors []error + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getMediaTypeForBid(seatBid.Bid[i], request.Imp) + if err != nil { + errors = append(errors, err) + continue + } + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, errors +} + +func getMediaTypeForBid(bid openrtb2.Bid, requestImps []openrtb2.Imp) (openrtb_ext.BidType, error) { + if bid.MType != 0 { + // If present, use explicit markup type annotation from the bidder: + switch bid.MType { + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + } + // As a fallback, guess markup type based on requested type - AdTonos is an audio company so we prioritize that. + for _, requestImp := range requestImps { + if requestImp.ID == bid.ImpID { + if requestImp.Audio != nil { + return openrtb_ext.BidTypeAudio, nil + } else if requestImp.Video != nil { + return openrtb_ext.BidTypeVideo, nil + } else { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Unsupported bidtype for bid: \"%s\"", bid.ImpID), + } + } + } + } + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to find impression: \"%s\"", bid.ImpID), + } +} diff --git a/adapters/adtonos/adtonos_test.go b/adapters/adtonos/adtonos_test.go new file mode 100644 index 00000000000..612e71783be --- /dev/null +++ b/adapters/adtonos/adtonos_test.go @@ -0,0 +1,30 @@ +package adtonos + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAdTonos, config.Adapter{ + Endpoint: "http://exchange.example.com/bid/{{.PublisherID}}"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "adtonostest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderAdTonos, config.Adapter{ + Endpoint: "{{Malformed}}"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json new file mode 100644 index 00000000000..c9103e37ee4 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json @@ -0,0 +1,115 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5, + "mtype": 3 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5, + "mtype": 3 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio.json b/adapters/adtonos/adtonostest/exemplary/simple-audio.json new file mode 100644 index 00000000000..61cac002660 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-audio.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/exemplary/simple-video.json b/adapters/adtonos/adtonostest/exemplary/simple-video.json new file mode 100644 index 00000000000..f00089b4008 --- /dev/null +++ b/adapters/adtonos/adtonostest/exemplary/simple-video.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "video-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "video-request-id", + "tmax": 1000, + "imp": [ + { + "id": "some-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "video-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..6fa20e3d3c3 --- /dev/null +++ b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "correct-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "test": 1, + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {} + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://exchange.example.com/bid/777XYZ123", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "some-request-id", + "tmax": 1000, + "imp": [ + { + "id": "correct-impression-id", + "bidfloor": 4.2, + "ext": { + "bidder": { + "supplierId": "777XYZ123" + } + }, + "audio": { + "mimes": [ + "audio/mpeg" + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "http://www.example.com", + "domain": "www.example.com" + }, + "device": {}, + "test": 1 + }, + "impIDs":["correct-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "unexpected-impression-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 6.5 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "Failed to find impression: \"unexpected-impression-id\"", + "comparison": "literal" + } + ] +} diff --git a/adapters/adtonos/params_test.go b/adapters/adtonos/params_test.go new file mode 100644 index 00000000000..98fa85f7b0d --- /dev/null +++ b/adapters/adtonos/params_test.go @@ -0,0 +1,43 @@ +package adtonos + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderAdTonos, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAdTonos, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"supplierId": ""}`, + `{"supplierId": "7YZxxxdJMSXWv7SwY"}`, +} + +var invalidParams = []string{ + `{"supplierId": 42}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 755f968c4a5..b05c048ec7c 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -26,6 +26,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/adsinteractive" "github.com/prebid/prebid-server/v2/adapters/adtarget" "github.com/prebid/prebid-server/v2/adapters/adtelligent" + "github.com/prebid/prebid-server/v2/adapters/adtonos" "github.com/prebid/prebid-server/v2/adapters/adtrgtme" "github.com/prebid/prebid-server/v2/adapters/advangelists" "github.com/prebid/prebid-server/v2/adapters/adview" @@ -253,6 +254,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderAdtarget: adtarget.Builder, openrtb_ext.BidderAdtrgtme: adtrgtme.Builder, openrtb_ext.BidderAdtelligent: adtelligent.Builder, + openrtb_ext.BidderAdTonos: adtonos.Builder, openrtb_ext.BidderAdvangelists: advangelists.Builder, openrtb_ext.BidderAdView: adview.Builder, openrtb_ext.BidderAdxcg: adxcg.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 67a513bdc1f..0570d8a468d 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -43,6 +43,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderAdtarget, BidderAdtrgtme, BidderAdtelligent, + BidderAdTonos, BidderAdvangelists, BidderAdView, BidderAdxcg, @@ -368,6 +369,7 @@ const ( BidderAdsinteractive BidderName = "adsinteractive" BidderAdtarget BidderName = "adtarget" BidderAdtrgtme BidderName = "adtrgtme" + BidderAdTonos BidderName = "adtonos" BidderAdtelligent BidderName = "adtelligent" BidderAdvangelists BidderName = "advangelists" BidderAdView BidderName = "adview" diff --git a/openrtb_ext/imp_adtonos.go b/openrtb_ext/imp_adtonos.go new file mode 100644 index 00000000000..f59ee35b329 --- /dev/null +++ b/openrtb_ext/imp_adtonos.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ImpExtAdTonos struct { + SupplierID string `json:"supplierId"` +} diff --git a/static/bidder-info/adtonos.yaml b/static/bidder-info/adtonos.yaml new file mode 100644 index 00000000000..37a81372710 --- /dev/null +++ b/static/bidder-info/adtonos.yaml @@ -0,0 +1,24 @@ +endpoint: https://exchange.adtonos.com/bid/{{.PublisherID}} +maintainer: + email: support@adtonos.com +gvlVendorID: 682 +geoscope: + - global +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - audio + # NOTE: This is purely an audio ad exchange - video ads are synthesized from audio ads for compatibility + # with mobile games that only understand video mimetypes. The visual layer is just a placeholder. + - video + site: + mediaTypes: + - audio + dooh: + mediaTypes: + - audio +userSync: + redirect: + url: https://play.adtonos.com/redir?to={{.RedirectURL}} + userMacro: '@UUID@' diff --git a/static/bidder-params/adtonos.json b/static/bidder-params/adtonos.json new file mode 100644 index 00000000000..e43d23e2b4b --- /dev/null +++ b/static/bidder-params/adtonos.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdTonos Adapter Params", + "description": "A schema which validates params accepted by the AdTonos adapter", + + "type": "object", + "properties": { + "supplierId": { + "type": "string", + "description": "ID of the supplier account in AdTonos platform" + } + }, + "required": ["supplierId"] +} \ No newline at end of file From 3e24be7950bea48b960a3d68206083e4995cd97b Mon Sep 17 00:00:00 2001 From: Alexander Pykhteyev Date: Sat, 14 Sep 2024 04:04:09 +0700 Subject: [PATCH 08/47] New Adapter: TGM (#3848) Co-authored-by: apykhteyev --- static/bidder-info/tgm.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 static/bidder-info/tgm.yaml diff --git a/static/bidder-info/tgm.yaml b/static/bidder-info/tgm.yaml new file mode 100644 index 00000000000..29d2039ee3f --- /dev/null +++ b/static/bidder-info/tgm.yaml @@ -0,0 +1 @@ +aliasOf: "limelightDigital" From 640b97c1e0c2286e98252794762926ad4fc8ce38 Mon Sep 17 00:00:00 2001 From: lm-ved <105272141+lm-ved@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:17:54 +0530 Subject: [PATCH 09/47] LemmaDigital: change endpoint (#3862) --- adapters/lemmadigital/lemmadigital_test.go | 2 +- .../lemmadigital/lemmadigitaltest/exemplary/banner.json | 2 +- .../lemmadigitaltest/exemplary/multi-imp.json | 2 +- .../lemmadigital/lemmadigitaltest/exemplary/video.json | 2 +- .../supplemental/empty-seatbid-array.json | 2 +- .../lemmadigitaltest/supplemental/invalid-response.json | 2 +- .../supplemental/status-code-bad-request.json | 2 +- .../supplemental/status-code-no-content.json | 2 +- .../supplemental/status-code-other-error.json | 2 +- static/bidder-info/lemmadigital.yaml | 8 ++++++-- 10 files changed, 15 insertions(+), 11 deletions(-) diff --git a/adapters/lemmadigital/lemmadigital_test.go b/adapters/lemmadigital/lemmadigital_test.go index e0062c0b565..cf507f34504 100644 --- a/adapters/lemmadigital/lemmadigital_test.go +++ b/adapters/lemmadigital/lemmadigital_test.go @@ -10,7 +10,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderLemmadigital, config.Adapter{ - Endpoint: "https://sg.ads.lemmatechnologies.com/lemma/servad?pid={{.PublisherID}}&aid={{.AdUnit}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + Endpoint: "https://test.lemmaurl.com/lemma/servad?src=prebid&pid={{.PublisherID}}&aid={{.AdUnit}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) if buildErr != nil { t.Fatalf("Builder returned unexpected error %v", buildErr) diff --git a/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json b/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json index 69bdb266b96..3b4d3a0a1c3 100644 --- a/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json +++ b/adapters/lemmadigital/lemmadigitaltest/exemplary/banner.json @@ -32,7 +32,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json b/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json index d71beda8d6d..1b29ab259f7 100644 --- a/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json +++ b/adapters/lemmadigital/lemmadigitaltest/exemplary/multi-imp.json @@ -49,7 +49,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json b/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json index b6d0e33e25b..af0e5561212 100644 --- a/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json +++ b/adapters/lemmadigital/lemmadigitaltest/exemplary/video.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json index e24c937fbd7..2e7ddc84f60 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/empty-seatbid-array.json @@ -42,7 +42,7 @@ }, "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "app": { "bundle": "com.ld.test", diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json index dcf23479bbf..7b703721881 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/invalid-response.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json index bee034cebfa..3243a12ce38 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-bad-request.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json index d7d62c091fa..2c70967922d 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-no-content.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json index 65dea99a36c..4213da80a23 100644 --- a/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json +++ b/adapters/lemmadigital/lemmadigitaltest/supplemental/status-code-other-error.json @@ -25,7 +25,7 @@ "httpCalls": [{ "expectedRequest": { - "uri": "https://sg.ads.lemmatechnologies.com/lemma/servad?pid=1&aid=1", + "uri": "https://test.lemmaurl.com/lemma/servad?src=prebid&pid=1&aid=1", "body": { "id": "test-request-id-video", "imp": [{ diff --git a/static/bidder-info/lemmadigital.yaml b/static/bidder-info/lemmadigital.yaml index 535c91ffa77..03cabc8f710 100644 --- a/static/bidder-info/lemmadigital.yaml +++ b/static/bidder-info/lemmadigital.yaml @@ -1,4 +1,4 @@ -endpoint: "https://sg.ads.lemmatechnologies.com/lemma/servad?pid={{.PublisherID}}&aid={{.AdUnit}}" +endpoint: "https://pbid.lemmamedia.com/lemma/servad?src=prebid&pid={{.PublisherID}}&aid={{.AdUnit}}" maintainer: email: support@lemmatechnologies.com endpointCompression: gzip @@ -11,4 +11,8 @@ capabilities: site: mediaTypes: - banner - - video \ No newline at end of file + - video +userSync: + iframe: + url: "https://sync.lemmadigital.com/setuid?publisher=850&redirect={{.RedirectURL}}" + userMacro: "${UUID}" \ No newline at end of file From 3c4527e9790e19647f12237f59875a5bbe61cfa3 Mon Sep 17 00:00:00 2001 From: Mikael Lundin Date: Wed, 18 Sep 2024 13:38:49 +0200 Subject: [PATCH 10/47] Adnuntius: Return DSA in bid response (#3777) --- adapters/adnuntius/adnuntius.go | 48 +++++++ ...heck-dsa-advertiser-legalName-omitted.json | 133 +++++++++++++++++ .../check-dsa-advertiser-legalName.json | 134 ++++++++++++++++++ .../check-dsa-advertiser-omitted.json | 123 ++++++++++++++++ .../supplemental/invalid-regs-ext.json | 46 ++++++ 5 files changed, 484 insertions(+) create mode 100644 adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName-omitted.json create mode 100644 adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName.json create mode 100644 adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-omitted.json create mode 100644 adapters/adnuntius/adnuntiustest/supplemental/invalid-regs-ext.json diff --git a/adapters/adnuntius/adnuntius.go b/adapters/adnuntius/adnuntius.go index 829b6e823e4..a28ac669795 100644 --- a/adapters/adnuntius/adnuntius.go +++ b/adapters/adnuntius/adnuntius.go @@ -37,6 +37,11 @@ type siteExt struct { Data interface{} `json:"data"` } +type adnAdvertiser struct { + LegalName string `json:"legalName,omitempty"` + Name string `json:"name,omitempty"` +} + type Ad struct { Bid struct { Amount float64 @@ -56,6 +61,7 @@ type Ad struct { LineItemId string Html string DestinationUrls map[string]string + Advertiser adnAdvertiser `json:"advertiser,omitempty"` } type AdUnit struct { @@ -369,6 +375,40 @@ func getGDPR(request *openrtb2.BidRequest) (string, string, error) { return gdpr, consent, nil } +func generateReturnExt(ad Ad, request *openrtb2.BidRequest) (json.RawMessage, error) { + // We always force the publisher to render + var adRender int8 = 0 + + var requestRegsExt *openrtb_ext.ExtRegs + if request.Regs != nil && request.Regs.Ext != nil { + if err := json.Unmarshal(request.Regs.Ext, &requestRegsExt); err != nil { + + return nil, fmt.Errorf("Failed to parse Ext information in Adnuntius: %v", err) + } + } + + if ad.Advertiser.Name != "" && requestRegsExt != nil && requestRegsExt.DSA != nil { + legalName := ad.Advertiser.Name + if ad.Advertiser.LegalName != "" { + legalName = ad.Advertiser.LegalName + } + ext := &openrtb_ext.ExtBid{ + DSA: &openrtb_ext.ExtBidDSA{ + AdRender: &adRender, + Paid: legalName, + Behalf: legalName, + }, + } + returnExt, err := json.Marshal(ext) + if err != nil { + return nil, fmt.Errorf("Failed to parse Ext information in Adnuntius: %v", err) + } + + return returnExt, nil + } + return nil, nil +} + func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2.BidRequest) (*openrtb2.Bid, []error) { creativeWidth, widthErr := strconv.ParseInt(ad.CreativeWidth, 10, 64) @@ -410,6 +450,13 @@ func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2. } } + extJson, err := generateReturnExt(ad, request) + if err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Error extracting Ext: %s", err.Error()), + }} + } + adDomain := []string{} for _, url := range ad.DestinationUrls { domainArray := strings.Split(url, "/") @@ -429,6 +476,7 @@ func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2. Price: price * 1000, AdM: html, ADomain: adDomain, + Ext: extJson, } return &bid, nil diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName-omitted.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName-omitted.json new file mode 100644 index 00000000000..0b44aa16dca --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName-omitted.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "datatopub": 1 + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv", + "advertiser": { + "name": "Name" + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240, + "ext": { + "dsa": { + "paid": "Name", + "behalf": "Name", + "adrender": 0 + } + } + }, + "type": "banner" + + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName.json new file mode 100644 index 00000000000..7999bd476aa --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-legalName.json @@ -0,0 +1,134 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "datatopub": 1 + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv", + "advertiser": { + "name": "Name", + "legalName": "LegalName" + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240, + "ext": { + "dsa": { + "paid": "LegalName", + "behalf": "LegalName", + "adrender": 0 + } + } + }, + "type": "banner" + + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-omitted.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-omitted.json new file mode 100644 index 00000000000..11cea9bcf66 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dsa-advertiser-omitted.json @@ -0,0 +1,123 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "datatopub": 1 + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=prebid&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240 + }, + "type": "banner" + + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file diff --git a/adapters/adnuntius/adnuntiustest/supplemental/invalid-regs-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/invalid-regs-ext.json new file mode 100644 index 00000000000..bf0365f60a9 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/invalid-regs-ext.json @@ -0,0 +1,46 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "regs": { + "ext": "" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "failed to parse URL: [failed to parse Adnuntius endpoint: failed to parse ExtRegs in Adnuntius GDPR check: json: cannot unmarshal string into Go value of type openrtb_ext.ExtRegs]", + "comparison": "literal" + } + ] +} From 905b3a5a29682ed74e9dc5e4c5e2404778ff9f08 Mon Sep 17 00:00:00 2001 From: ShriprasadM Date: Thu, 19 Sep 2024 18:30:20 +0530 Subject: [PATCH 11/47] Log non bid reasons in bidder framework (#2891) Co-authored-by: Shriprasad Marathe Co-authored-by: ashish.shinde Co-authored-by: dhruv.sonone --- analytics/pubstack/pubstack_module_test.go | 2 +- exchange/bidder.go | 7 + exchange/bidder_test.go | 146 +++++ exchange/exchange.go | 51 +- exchange/exchange_test.go | 101 ++-- exchange/non_bid_reason.go | 50 +- exchange/non_bid_reason_test.go | 65 +++ exchange/seat_non_bids.go | 56 +- exchange/seat_non_bids_test.go | 529 ++++++++++++++++-- openrtb_ext/response.go | 8 +- stored_requests/data/by_id/accounts/test.json | 2 +- 11 files changed, 859 insertions(+), 158 deletions(-) create mode 100644 exchange/non_bid_reason_test.go diff --git a/analytics/pubstack/pubstack_module_test.go b/analytics/pubstack/pubstack_module_test.go index 911de4c6959..6b16d0b7e92 100644 --- a/analytics/pubstack/pubstack_module_test.go +++ b/analytics/pubstack/pubstack_module_test.go @@ -99,7 +99,7 @@ func TestNewModuleSuccess(t *testing.T) { { ImpId: "123", StatusCode: 34, - Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + Ext: &openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, }, }, }, diff --git a/exchange/bidder.go b/exchange/bidder.go index fb2c73be5c2..cfaea36c0bb 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -76,12 +76,14 @@ type bidRequestOptions struct { type extraBidderRespInfo struct { respProcessingStartTime time.Time + seatNonBidBuilder SeatNonBidBuilder } type extraAuctionResponseInfo struct { fledge *openrtb_ext.Fledge bidsFound bool bidderResponseStartTime time.Time + seatNonBidBuilder SeatNonBidBuilder } const ImpIdReqBody = "Stored bid response for impression id: " @@ -135,6 +137,7 @@ type bidderAdapterConfig struct { func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) { request := openrtb_ext.RequestWrapper{BidRequest: bidderRequest.BidRequest} reject := hookExecutor.ExecuteBidderRequestStage(&request, string(bidderRequest.BidderName)) + seatNonBidBuilder := SeatNonBidBuilder{} if reject != nil { return nil, extraBidderRespInfo{}, []error{reject} } @@ -398,13 +401,17 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde } } else { errs = append(errs, httpInfo.err) + nonBidReason := httpInfoToNonBidReason(httpInfo) + seatNonBidBuilder.rejectImps(httpInfo.request.ImpIDs, nonBidReason, string(bidderRequest.BidderName)) } } + seatBids := make([]*entities.PbsOrtbSeatBid, 0, len(seatBidMap)) for _, seatBid := range seatBidMap { seatBids = append(seatBids, seatBid) } + extraRespInfo.seatNonBidBuilder = seatNonBidBuilder return seatBids, extraRespInfo, errs } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index 31c497d6f1c..9d5faf47549 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -9,11 +9,15 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/http/httptest" "net/http/httptrace" + "net/url" + "os" "sort" "strings" + "syscall" "testing" "time" @@ -3096,6 +3100,148 @@ func TestGetBidType(t *testing.T) { } } +func TestSeatNonBid(t *testing.T) { + type args struct { + BidRequest *openrtb2.BidRequest + Seat string + SeatRequests []*adapters.RequestData + BidderResponse func() (*http.Response, error) + client *http.Client + } + type expect struct { + seatBids []*entities.PbsOrtbSeatBid + seatNonBids SeatNonBidBuilder + errors []error + } + testCases := []struct { + name string + args args + expect expect + }{ + { + name: "NBR_101_timeout_for_context_deadline_exceeded", + args: args{ + Seat: "pubmatic", + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{ID: "1234"}}, + }, + SeatRequests: []*adapters.RequestData{{ImpIDs: []string{"1234"}}}, + BidderResponse: func() (*http.Response, error) { return nil, context.DeadlineExceeded }, + client: &http.Client{Timeout: time.Nanosecond}, // for timeout + }, + expect: expect{ + seatNonBids: SeatNonBidBuilder{ + "pubmatic": {{ + ImpId: "1234", + StatusCode: int(ErrorTimeout), + }}, + }, + errors: []error{&errortypes.Timeout{Message: context.DeadlineExceeded.Error()}}, + seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", Seat: "pubmatic", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, + }, + }, { + name: "NBR_103_Bidder_Unreachable_Connection_Refused", + args: args{ + Seat: "appnexus", + SeatRequests: []*adapters.RequestData{{ImpIDs: []string{"1234", "4567"}}}, + BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1234"}, {ID: "4567"}}}, + BidderResponse: func() (*http.Response, error) { + return nil, &net.OpError{Err: os.NewSyscallError(syscall.ECONNREFUSED.Error(), syscall.ECONNREFUSED)} + }, + }, + expect: expect{ + seatNonBids: SeatNonBidBuilder{ + "appnexus": { + {ImpId: "1234", StatusCode: int(ErrorBidderUnreachable)}, + {ImpId: "4567", StatusCode: int(ErrorBidderUnreachable)}, + }, + }, + seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", Seat: "appnexus", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, + errors: []error{&url.Error{Op: "Get", URL: "", Err: &net.OpError{Err: os.NewSyscallError(syscall.ECONNREFUSED.Error(), syscall.ECONNREFUSED)}}}, + }, + }, { + name: "no_impids_populated_in_request_data", + args: args{ + SeatRequests: []*adapters.RequestData{{ + ImpIDs: nil, // no imp ids + }}, + BidRequest: &openrtb2.BidRequest{Imp: []openrtb2.Imp{{ID: "1234"}}}, + BidderResponse: func() (*http.Response, error) { + return nil, errors.New("some_error") + }, + }, + expect: expect{ + seatNonBids: SeatNonBidBuilder{}, + seatBids: []*entities.PbsOrtbSeatBid{{Bids: []*entities.PbsOrtbBid{}, Currency: "USD", HttpCalls: []*openrtb_ext.ExtHttpCall{}}}, + errors: []error{&url.Error{Op: "Get", URL: "", Err: errors.New("some_error")}}, + }, + }, + } + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + mockBidder := &mockBidder{} + mockBidder.On("MakeRequests", mock.Anything, mock.Anything).Return(test.args.SeatRequests, []error(nil)) + mockMetricsEngine := &metrics.MetricsEngineMock{} + mockMetricsEngine.On("RecordOverheadTime", mock.Anything, mock.Anything).Return(nil) + mockMetricsEngine.On("RecordBidderServerResponseTime", mock.Anything).Return(nil) + roundTrip := &mockRoundTripper{} + roundTrip.On("RoundTrip", mock.Anything).Return(test.args.BidderResponse()) + client := &http.Client{ + Transport: roundTrip, + Timeout: 0, + } + if test.args.client != nil { + client.Timeout = test.args.client.Timeout + } + bidder := AdaptBidder(mockBidder, client, &config.Configuration{}, mockMetricsEngine, openrtb_ext.BidderAppnexus, &config.DebugInfo{}, test.args.Seat) + + ctx := context.Background() + if client.Timeout > 0 { + ctxTimeout, cancel := context.WithTimeout(ctx, client.Timeout) + ctx = ctxTimeout + defer cancel() + } + seatBids, responseExtra, errors := bidder.requestBid(ctx, BidderRequest{ + BidRequest: test.args.BidRequest, + BidderName: openrtb_ext.BidderName(test.args.Seat), + }, nil, &adapters.ExtraRequestInfo{}, &MockSigner{}, bidRequestOptions{}, openrtb_ext.ExtAlternateBidderCodes{}, hookexecution.EmptyHookExecutor{}, nil) + assert.Equal(t, test.expect.seatBids, seatBids) + assert.Equal(t, test.expect.seatNonBids, responseExtra.seatNonBidBuilder) + assert.Equal(t, test.expect.errors, errors) + for _, nonBids := range responseExtra.seatNonBidBuilder { + for _, nonBid := range nonBids { + for _, seatBid := range seatBids { + for _, bid := range seatBid.Bids { + // ensure non bids are not present in seat bids + if nonBid.ImpId == bid.Bid.ImpID { + assert.Fail(t, "imp id [%s] present in both seat bid and non seat bid", nonBid.ImpId) + } + } + } + } + } + }) + } +} + +type mockRoundTripper struct { + mock.Mock +} + +func (rt *mockRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + args := rt.Called(request) + var response *http.Response + if args.Get(0) != nil { + response = args.Get(0).(*http.Response) + } + var err error + if args.Get(1) != nil { + err = args.Get(1).(error) + } + + return response, err +} + type mockBidderTmaxCtx struct { startTime, deadline, now time.Time ok bool diff --git a/exchange/exchange.go b/exchange/exchange.go index 5c27b0d3c5a..a8787ac3db2 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -95,6 +95,8 @@ type seatResponseExtra struct { // httpCalls is the list of debugging info. It should only be populated if the request.test == 1. // This will become response.ext.debug.httpcalls.{bidder} on the final Response. HttpCalls []*openrtb_ext.ExtHttpCall + // NonBid contains non bid reason information + NonBid *openrtb_ext.NonBid } type bidResponseWrapper struct { @@ -103,6 +105,7 @@ type bidResponseWrapper struct { bidder openrtb_ext.BidderName adapter openrtb_ext.BidderName bidderResponseStartTime time.Time + seatNonBidBuilder SeatNonBidBuilder } type BidIDGenerator interface { @@ -373,7 +376,8 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog fledge *openrtb_ext.Fledge anyBidsReturned bool // List of bidders we have requests for. - liveAdapters []openrtb_ext.BidderName + liveAdapters []openrtb_ext.BidderName + seatNonBidBuilder SeatNonBidBuilder = SeatNonBidBuilder{} ) if len(r.StoredAuctionResponses) > 0 { @@ -399,13 +403,15 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog fledge = extraRespInfo.fledge anyBidsReturned = extraRespInfo.bidsFound r.BidderResponseStartTime = extraRespInfo.bidderResponseStartTime + if extraRespInfo.seatNonBidBuilder != nil { + seatNonBidBuilder = extraRespInfo.seatNonBidBuilder + } } var ( auc *auction cacheErrs []error bidResponseExt *openrtb_ext.ExtBidResponse - seatNonBids = nonBids{} ) if anyBidsReturned { @@ -423,7 +429,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog if rejectedBid.Bids[0].Bid.DealID != "" { rejectionReason = ResponseRejectedBelowDealFloor } - seatNonBids.addBid(rejectedBid.Bids[0], int(rejectionReason), rejectedBid.Seat) + seatNonBidBuilder.rejectBid(rejectedBid.Bids[0], int(rejectionReason), rejectedBid.Seat) } } @@ -431,7 +437,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog //If includebrandcategory is present in ext then CE feature is on. if requestExtPrebid.Targeting != nil && requestExtPrebid.Targeting.IncludeBrandCategory != nil { var rejections []string - bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, *requestExtPrebid.Targeting, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &seatNonBids) + bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, *requestExtPrebid.Targeting, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &seatNonBidBuilder) if err != nil { return nil, fmt.Errorf("Error in category mapping : %s", err.Error()) } @@ -526,14 +532,14 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog e.bidValidationEnforcement.SetBannerCreativeMaxSize(r.Account.Validations) // Build the response - bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBids) + bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBidBuilder) bidResponse = adservertargeting.Apply(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute) bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt) if err != nil { return nil, err } - bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBids) + bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBidBuilder) return &AuctionResponse{ BidResponse: bidResponse, @@ -713,7 +719,7 @@ func (e *exchange) getAllBids( adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, len(bidderRequests)) adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests)) chBids := make(chan *bidResponseWrapper, len(bidderRequests)) - extraRespInfo := extraAuctionResponseInfo{} + extraRespInfo := extraAuctionResponseInfo{seatNonBidBuilder: SeatNonBidBuilder{}} e.me.RecordOverheadTime(metrics.MakeBidderRequests, time.Since(pbsRequestStartTime)) @@ -753,6 +759,7 @@ func (e *exchange) getAllBids( // Add in time reporting elapsed := time.Since(start) brw.adapterSeatBids = seatBids + brw.seatNonBidBuilder = extraBidderRespInfo.seatNonBidBuilder // Structure to record extra tracking data generated during bidding ae := new(seatResponseExtra) ae.ResponseTimeMillis = int(elapsed / time.Millisecond) @@ -805,6 +812,10 @@ func (e *exchange) getAllBids( } //but we need to add all bidders data to adapterExtra to have metrics and other metadata adapterExtra[brw.bidder] = brw.adapterExtra + + // collect adapter non bids + extraRespInfo.seatNonBidBuilder.append(brw.seatNonBidBuilder) + } return adapterBids, adapterExtra, extraRespInfo @@ -922,7 +933,7 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { } // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester -func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBids *nonBids) *openrtb2.BidResponse { +func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBidBuilder *SeatNonBidBuilder) *openrtb2.BidResponse { bidResponse := new(openrtb2.BidResponse) bidResponse.ID = bidRequest.ID @@ -937,7 +948,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ for a, adapterSeatBids := range adapterSeatBids { //while processing every single bib, do we need to handle categories here? if adapterSeatBids != nil && len(adapterSeatBids.Bids) > 0 { - sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, pubID, seatNonBids) + sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, pubID, seatNonBidBuilder) seatBids = append(seatBids, *sb) bidResponse.Cur = adapterSeatBids.Currency } @@ -957,7 +968,7 @@ func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, e return buffer.Bytes(), err } -func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBids *nonBids) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { +func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBidBuilder *SeatNonBidBuilder) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { res := make(map[string]string) type bidDedupe struct { @@ -1019,7 +1030,7 @@ func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestT //on receiving bids from adapters if no unique IAB category is returned or if no ad server category is returned discard the bid bidsToRemove = append(bidsToRemove, bidInd) rejections = updateRejections(rejections, bidID, "Bid did not contain a category") - seatNonBids.addBid(bid, int(ResponseRejectedCategoryMappingInvalid), string(bidderName)) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCategoryMappingInvalid), string(bidderName)) continue } if translateCategories { @@ -1256,14 +1267,14 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*en // Return an openrtb seatBid for a bidder // buildBidResponse is responsible for ensuring nil bid seatbids are not included -func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBids *nonBids) *openrtb2.SeatBid { +func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBidBuilder *SeatNonBidBuilder) *openrtb2.SeatBid { seatBid := &openrtb2.SeatBid{ Seat: adapter.String(), Group: 0, // Prebid cannot support roadblocking } var errList []error - seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, adapter, pubID, seatNonBids) + seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, adapter, pubID, seatNonBidBuilder) if len(errList) > 0 { adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...) } @@ -1271,7 +1282,7 @@ func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter open return seatBid } -func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBids *nonBids) ([]openrtb2.Bid, []error) { +func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBidBuilder *SeatNonBidBuilder) ([]openrtb2.Bid, []error) { result := make([]openrtb2.Bid, 0, len(bids)) errs := make([]error, 0, 1) @@ -1283,12 +1294,12 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea } bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], dsaMessage) - seatNonBids.addBid(bid, int(ResponseRejectedGeneral), adapter.String()) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedGeneral), adapter.String()) continue // Don't add bid to result } if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner { if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) { - seatNonBids.addBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String()) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String()) continue // Don't add bid to result } } else if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationWarn && bid.BidType == openrtb_ext.BidTypeBanner { @@ -1297,7 +1308,7 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea if _, ok := impExtInfoMap[bid.Bid.ImpID]; ok { if e.bidValidationEnforcement.SecureMarkup == config.ValidationEnforce && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { if !e.validateBidAdM(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.SecureMarkup) { - seatNonBids.addBid(bid, int(ResponseRejectedCreativeNotSecure), adapter.String()) + seatNonBidBuilder.rejectBid(bid, int(ResponseRejectedCreativeNotSecure), adapter.String()) continue // Don't add bid to result } } else if e.bidValidationEnforcement.SecureMarkup == config.ValidationWarn && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { @@ -1597,8 +1608,8 @@ func setErrorMessageSecureMarkup(validationType string) string { } // setSeatNonBid adds SeatNonBids within bidResponse.Ext.Prebid.SeatNonBid -func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBids nonBids) *openrtb_ext.ExtBidResponse { - if len(seatNonBids.seatNonBidsMap) == 0 { +func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBidBuilder SeatNonBidBuilder) *openrtb_ext.ExtBidResponse { + if len(seatNonBidBuilder) == 0 { return bidResponseExt } if bidResponseExt == nil { @@ -1608,6 +1619,6 @@ func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBids nonBi bidResponseExt.Prebid = &openrtb_ext.ExtResponsePrebid{} } - bidResponseExt.Prebid.SeatNonBid = seatNonBids.get() + bidResponseExt.Prebid.SeatNonBid = seatNonBidBuilder.Slice() return bidResponseExt } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index aec062b70c6..df272a1f5bf 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -172,7 +172,7 @@ func TestCharacterEscape(t *testing.T) { var errList []error // 4) Build bid response - bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList, &nonBids{}) + bidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, "", errList, &SeatNonBidBuilder{}) // 5) Assert we have no errors and one '&' character as we are supposed to if len(errList) > 0 { @@ -1343,7 +1343,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { var errList []error // 4) Build bid response - bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList, &nonBids{}) + bid_resp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, "", errList, &SeatNonBidBuilder{}) expectedBidResponse := &openrtb2.BidResponse{ SeatBid: []openrtb2.SeatBid{ @@ -1433,7 +1433,7 @@ func TestBidReturnsCreative(t *testing.T) { //Run tests for _, test := range testCases { - resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &nonBids{}) + resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &SeatNonBidBuilder{}) assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description) assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description) @@ -1718,7 +1718,7 @@ func TestBidResponseCurrency(t *testing.T) { } // Run tests for i := range testCases { - actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList, &nonBids{}) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, "", errList, &SeatNonBidBuilder{}) assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext)) } } @@ -1786,7 +1786,7 @@ func TestBidResponseImpExtInfo(t *testing.T) { expectedBidResponseExt := `{"origbidcpm":0,"prebid":{"meta":{"adaptercode":"appnexus"},"type":"video","passthrough":{"imp_passthrough_val":1}},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}` - actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList, &nonBids{}) + actualBidResp := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, "", errList, &SeatNonBidBuilder{}) resBidExt := string(actualBidResp.SeatBid[0].Bid[0].Ext) assert.Equalf(t, expectedBidResponseExt, resBidExt, "Expected bid response extension is incorrect") @@ -2607,7 +2607,7 @@ func TestCategoryMapping(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2662,7 +2662,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2714,7 +2714,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message") @@ -2796,7 +2796,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be no bid rejection messages") @@ -2874,7 +2874,7 @@ func TestCategoryDedupe(t *testing.T) { }, } deduplicateGenerator := fakeBooleanGenerator{value: tt.dedupeGeneratorValue} - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &SeatNonBidBuilder{}) assert.Nil(t, err) assert.Equal(t, 3, len(rejections)) @@ -2943,7 +2943,7 @@ func TestNoCategoryDedupe(t *testing.T) { adapterBids[bidderName1] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.Equal(t, nil, err, "Category mapping error should be empty") assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages") @@ -3008,7 +3008,7 @@ func TestCategoryMappingBidderName(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3062,7 +3062,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { adapterBids[bidderName1] = &seatBid1 adapterBids[bidderName2] = &seatBid2 - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Empty(t, rejections, "There should be 0 bid rejection messages") @@ -3163,7 +3163,7 @@ func TestBidRejectionErrors(t *testing.T) { adapterBids[bidderName] = &seatBid - bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *test.reqExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) if len(test.expectedCatDur) > 0 { // Bid deduplication case @@ -3226,7 +3226,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - bidCategory, _, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &nonBids{}) + bidCategory, _, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") assert.Len(t, rejections, 1, "There should be 1 bid rejection message") @@ -3310,7 +3310,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - _, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeBooleanGenerator{value: true}, &nonBids{}) + _, adapterBids, rejections, err := applyCategoryMapping(context.TODO(), *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeBooleanGenerator{value: true}, &SeatNonBidBuilder{}) assert.NoError(t, err, "Category mapping error should be empty") @@ -4774,7 +4774,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids []*entities.PbsOrtbBid givenSeat openrtb_ext.BidderName expectedNumOfBids int - expectedNonBids *nonBids + expectedNonBids *SeatNonBidBuilder expectedNumDebugErrors int expectedNumDebugWarnings int }{ @@ -4785,15 +4785,13 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {"adrender":1}}`)}}, {Bid: &openrtb2.Bid{}}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &nonBids{ - seatNonBidsMap: map[string][]openrtb_ext.NonBid{ - "pubmatic": { - { - StatusCode: 300, - Ext: openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{}, - }, + expectedNonBids: &SeatNonBidBuilder{ + "pubmatic": { + { + StatusCode: 300, + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{}, }, }, }, @@ -4807,17 +4805,15 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &nonBids{ - seatNonBidsMap: map[string][]openrtb_ext.NonBid{ - "pubmatic": { - { - StatusCode: 351, - Ext: openrtb_ext.NonBidExt{ - Prebid: openrtb_ext.ExtResponseNonBidPrebid{ - Bid: openrtb_ext.NonBidObject{ - W: 200, - H: 200, - }, + expectedNonBids: &SeatNonBidBuilder{ + "pubmatic": { + { + StatusCode: 351, + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + W: 200, + H: 200, }, }, }, @@ -4832,7 +4828,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, expectedNumDebugErrors: 1, }, { @@ -4841,12 +4837,15 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 1, - expectedNonBids: &nonBids{ - seatNonBidsMap: map[string][]openrtb_ext.NonBid{ - "pubmatic": { - { - ImpId: "1", - StatusCode: 352, + expectedNonBids: &SeatNonBidBuilder{ + "pubmatic": { + { + ImpId: "1", + StatusCode: 352, + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{}, + }, }, }, }, @@ -4859,7 +4858,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, expectedNumDebugErrors: 1, }, { @@ -4868,7 +4867,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, }, { name: "Creative_size_validation_skipped,_Adm_Validation_enforced,_one_of_two_bids_has_invalid_dimensions", @@ -4876,7 +4875,7 @@ func TestMakeBidWithValidation(t *testing.T) { givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + expectedNonBids: &SeatNonBidBuilder{}, }, } @@ -4925,7 +4924,7 @@ func TestMakeBidWithValidation(t *testing.T) { } e.bidValidationEnforcement = test.givenValidations sampleBids := test.givenBids - nonBids := &nonBids{} + nonBids := &SeatNonBidBuilder{} resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidRequest, bidExtResponse, test.givenSeat, "", nonBids) assert.Equal(t, 0, len(resultingErrs)) @@ -6060,7 +6059,7 @@ func TestSelectNewDuration(t *testing.T) { func TestSetSeatNonBid(t *testing.T) { type args struct { bidResponseExt *openrtb_ext.ExtBidResponse - seatNonBids nonBids + seatNonBids SeatNonBidBuilder } tests := []struct { name string @@ -6069,12 +6068,12 @@ func TestSetSeatNonBid(t *testing.T) { }{ { name: "empty-seatNonBidsMap", - args: args{seatNonBids: nonBids{}, bidResponseExt: nil}, + args: args{seatNonBids: SeatNonBidBuilder{}, bidResponseExt: nil}, want: nil, }, { name: "nil-bidResponseExt", - args: args{seatNonBids: nonBids{seatNonBidsMap: map[string][]openrtb_ext.NonBid{"key": nil}}, bidResponseExt: nil}, + args: args{seatNonBids: SeatNonBidBuilder{"key": nil}, bidResponseExt: nil}, want: &openrtb_ext.ExtBidResponse{ Prebid: &openrtb_ext.ExtResponsePrebid{ SeatNonBid: []openrtb_ext.SeatNonBid{{ diff --git a/exchange/non_bid_reason.go b/exchange/non_bid_reason.go index fe9a8e26c48..edfd3bc1e3d 100644 --- a/exchange/non_bid_reason.go +++ b/exchange/non_bid_reason.go @@ -1,12 +1,22 @@ package exchange +import ( + "errors" + "net" + "syscall" + + "github.com/prebid/prebid-server/v2/errortypes" +) + // SeatNonBid list the reasons why bid was not resulted in positive bid // reason could be either No bid, Error, Request rejection or Response rejection -// Reference: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/seat-non-bid.md -type NonBidReason int +// Reference: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/seat-non-bid.md#list-non-bid-status-codes +type NonBidReason int64 const ( - NoBidUnknownError NonBidReason = 0 // No Bid - General + ErrorGeneral NonBidReason = 100 // Error - General + ErrorTimeout NonBidReason = 101 // Error - Timeout + ErrorBidderUnreachable NonBidReason = 103 // Error - Bidder Unreachable ResponseRejectedGeneral NonBidReason = 300 ResponseRejectedBelowFloor NonBidReason = 301 // Response Rejected - Below Floor ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid @@ -15,15 +25,33 @@ const ( ResponseRejectedCreativeNotSecure NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure) ) -// Ptr returns pointer to own value. -func (n NonBidReason) Ptr() *NonBidReason { - return &n +func errorToNonBidReason(err error) NonBidReason { + switch errortypes.ReadCode(err) { + case errortypes.TimeoutErrorCode: + return ErrorTimeout + default: + return ErrorGeneral + } } -// Val safely dereferences pointer, returning default value (NoBidUnknownError) for nil. -func (n *NonBidReason) Val() NonBidReason { - if n == nil { - return NoBidUnknownError +// httpInfoToNonBidReason determines NoBidReason code (NBR) +// It will first try to resolve the NBR based on prebid's proprietary error code. +// If proprietary error code not found then it will try to determine NBR using +// system call level error code +func httpInfoToNonBidReason(httpInfo *httpCallInfo) NonBidReason { + nonBidReason := errorToNonBidReason(httpInfo.err) + if nonBidReason != ErrorGeneral { + return nonBidReason } - return *n + if isBidderUnreachableError(httpInfo) { + return ErrorBidderUnreachable + } + return ErrorGeneral +} + +// isBidderUnreachableError checks if the error is due to connection refused or no such host +func isBidderUnreachableError(httpInfo *httpCallInfo) bool { + var dnsErr *net.DNSError + isNoSuchHost := errors.As(httpInfo.err, &dnsErr) && dnsErr.IsNotFound + return errors.Is(httpInfo.err, syscall.ECONNREFUSED) || isNoSuchHost } diff --git a/exchange/non_bid_reason_test.go b/exchange/non_bid_reason_test.go new file mode 100644 index 00000000000..ab5c9b4f957 --- /dev/null +++ b/exchange/non_bid_reason_test.go @@ -0,0 +1,65 @@ +package exchange + +import ( + "errors" + "net" + "syscall" + "testing" + + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/stretchr/testify/assert" +) + +func Test_httpInfoToNonBidReason(t *testing.T) { + type args struct { + httpInfo *httpCallInfo + } + tests := []struct { + name string + args args + want NonBidReason + }{ + { + name: "error-timeout", + args: args{ + httpInfo: &httpCallInfo{ + err: &errortypes.Timeout{}, + }, + }, + want: ErrorTimeout, + }, + { + name: "error-general", + args: args{ + httpInfo: &httpCallInfo{ + err: errors.New("some_error"), + }, + }, + want: ErrorGeneral, + }, + { + name: "error-bidderUnreachable", + args: args{ + httpInfo: &httpCallInfo{ + err: syscall.ECONNREFUSED, + }, + }, + want: ErrorBidderUnreachable, + }, + { + name: "error-biddersUnreachable-no-such-host", + args: args{ + httpInfo: &httpCallInfo{ + err: &net.DNSError{IsNotFound: true}, + }, + }, + want: ErrorBidderUnreachable, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := httpInfoToNonBidReason(tt.args.httpInfo) + assert.Equal(t, tt.want, actual) + }) + } +} diff --git a/exchange/seat_non_bids.go b/exchange/seat_non_bids.go index 78c1b23e3f3..760431ef44f 100644 --- a/exchange/seat_non_bids.go +++ b/exchange/seat_non_bids.go @@ -5,22 +5,18 @@ import ( "github.com/prebid/prebid-server/v2/openrtb_ext" ) -type nonBids struct { - seatNonBidsMap map[string][]openrtb_ext.NonBid -} +type SeatNonBidBuilder map[string][]openrtb_ext.NonBid -// addBid is not thread safe as we are initializing and writing to map -func (snb *nonBids) addBid(bid *entities.PbsOrtbBid, nonBidReason int, seat string) { - if bid == nil || bid.Bid == nil { +// rejectBid appends a non bid object to the builder based on a bid +func (b SeatNonBidBuilder) rejectBid(bid *entities.PbsOrtbBid, nonBidReason int, seat string) { + if b == nil || bid == nil || bid.Bid == nil { return } - if snb.seatNonBidsMap == nil { - snb.seatNonBidsMap = make(map[string][]openrtb_ext.NonBid) - } + nonBid := openrtb_ext.NonBid{ ImpId: bid.Bid.ImpID, StatusCode: nonBidReason, - Ext: openrtb_ext.NonBidExt{ + Ext: &openrtb_ext.NonBidExt{ Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{ Price: bid.Bid.Price, ADomain: bid.Bid.ADomain, @@ -36,16 +32,29 @@ func (snb *nonBids) addBid(bid *entities.PbsOrtbBid, nonBidReason int, seat stri }}, }, } - - snb.seatNonBidsMap[seat] = append(snb.seatNonBidsMap[seat], nonBid) + b[seat] = append(b[seat], nonBid) } -func (snb *nonBids) get() []openrtb_ext.SeatNonBid { - if snb == nil { - return nil +// rejectImps appends a non bid object to the builder for every specified imp +func (b SeatNonBidBuilder) rejectImps(impIds []string, nonBidReason NonBidReason, seat string) { + nonBids := []openrtb_ext.NonBid{} + for _, impId := range impIds { + nonBid := openrtb_ext.NonBid{ + ImpId: impId, + StatusCode: int(nonBidReason), + } + nonBids = append(nonBids, nonBid) } - var seatNonBid []openrtb_ext.SeatNonBid - for seat, nonBids := range snb.seatNonBidsMap { + + if len(nonBids) > 0 { + b[seat] = append(b[seat], nonBids...) + } +} + +// slice transforms the seat non bid map into a slice of SeatNonBid objects representing the non-bids for each seat +func (b SeatNonBidBuilder) Slice() []openrtb_ext.SeatNonBid { + seatNonBid := make([]openrtb_ext.SeatNonBid, 0) + for seat, nonBids := range b { seatNonBid = append(seatNonBid, openrtb_ext.SeatNonBid{ Seat: seat, NonBid: nonBids, @@ -53,3 +62,16 @@ func (snb *nonBids) get() []openrtb_ext.SeatNonBid { } return seatNonBid } + +// append adds the nonBids from the input nonBids to the current nonBids. +// This method is not thread safe as we are initializing and writing to map +func (b SeatNonBidBuilder) append(nonBids ...SeatNonBidBuilder) { + if b == nil { + return + } + for _, nonBid := range nonBids { + for seat, nonBids := range nonBid { + b[seat] = append(b[seat], nonBids...) + } + } +} diff --git a/exchange/seat_non_bids_test.go b/exchange/seat_non_bids_test.go index 103c0939496..b754f885965 100644 --- a/exchange/seat_non_bids_test.go +++ b/exchange/seat_non_bids_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSeatNonBidsAdd(t *testing.T) { +func TestRejectBid(t *testing.T) { type fields struct { - seatNonBidsMap map[string][]openrtb_ext.NonBid + builder SeatNonBidBuilder } type args struct { bid *entities.PbsOrtbBid @@ -22,89 +22,512 @@ func TestSeatNonBidsAdd(t *testing.T) { name string fields fields args args - want map[string][]openrtb_ext.NonBid + want SeatNonBidBuilder }{ { - name: "nil-seatNonBidsMap", - fields: fields{seatNonBidsMap: nil}, - args: args{}, - want: nil, + name: "nil_builder", + fields: fields{ + builder: nil, + }, + args: args{}, + want: nil, }, { - name: "nil-seatNonBidsMap-with-bid-object", - fields: fields{seatNonBidsMap: nil}, - args: args{bid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{}}, seat: "bidder1"}, - want: sampleSeatNonBidMap("bidder1", 1), + name: "nil_pbsortbid", + fields: fields{ + builder: SeatNonBidBuilder{}, + }, + args: args{ + bid: nil, + }, + want: SeatNonBidBuilder{}, }, { - name: "multiple-nonbids-for-same-seat", - fields: fields{seatNonBidsMap: sampleSeatNonBidMap("bidder2", 1)}, - args: args{bid: &entities.PbsOrtbBid{Bid: &openrtb2.Bid{}}, seat: "bidder2"}, - want: sampleSeatNonBidMap("bidder2", 2), + name: "nil_bid", + fields: fields{ + builder: SeatNonBidBuilder{}, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: nil, + }, + }, + want: SeatNonBidBuilder{}, + }, + { + name: "append_nonbids_new_seat", + fields: fields{ + builder: SeatNonBidBuilder{}, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ImpID: "Imp1", + Price: 10, + }, + }, + nonBidReason: int(ErrorGeneral), + seat: "seat1", + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + Price: 10, + }, + }, + }, + }, + }, + }, + }, + { + name: "append_nonbids_for_different_seat", + fields: fields{ + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + }, + }, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ImpID: "Imp2", + Price: 10, + }, + }, + nonBidReason: int(ErrorGeneral), + seat: "seat2", + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + }, + "seat2": []openrtb_ext.NonBid{ + { + ImpId: "Imp2", + StatusCode: int(ErrorGeneral), + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + Price: 10, + }, + }, + }, + }, + }, + }, + }, + { + name: "append_nonbids_for_existing_seat", + fields: fields{ + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + }, + }, + }, + args: args{ + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ImpID: "Imp2", + Price: 10, + }, + }, + nonBidReason: int(ErrorGeneral), + seat: "seat1", + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "Imp1", + StatusCode: int(ErrorGeneral), + }, + { + ImpId: "Imp2", + StatusCode: int(ErrorGeneral), + Ext: &openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{ + Price: 10, + }, + }, + }, + }, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - snb := &nonBids{ - seatNonBidsMap: tt.fields.seatNonBidsMap, - } - snb.addBid(tt.args.bid, tt.args.nonBidReason, tt.args.seat) - assert.Equalf(t, tt.want, snb.seatNonBidsMap, "expected seatNonBidsMap not nil") + snb := tt.fields.builder + snb.rejectBid(tt.args.bid, tt.args.nonBidReason, tt.args.seat) + assert.Equal(t, tt.want, snb) }) } } -func TestSeatNonBidsGet(t *testing.T) { - type fields struct { - snb *nonBids - } +func TestAppend(t *testing.T) { tests := []struct { - name string - fields fields - want []openrtb_ext.SeatNonBid + name string + builder SeatNonBidBuilder + toAppend []SeatNonBidBuilder + expected SeatNonBidBuilder }{ { - name: "get-seat-nonbids", - fields: fields{&nonBids{sampleSeatNonBidMap("bidder1", 2)}}, - want: sampleSeatBids("bidder1", 2), + name: "nil_buider", + builder: nil, + toAppend: []SeatNonBidBuilder{{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}}, + expected: nil, + }, + { + name: "empty_builder", + builder: SeatNonBidBuilder{}, + toAppend: []SeatNonBidBuilder{{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + }, + { + name: "append_one_different_seat", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: []SeatNonBidBuilder{{"seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}, "seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}, + }, + { + name: "append_multiple_different_seats", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: []SeatNonBidBuilder{{"seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}}, {"seat3": []openrtb_ext.NonBid{{ImpId: "imp3"}}}}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}, "seat2": []openrtb_ext.NonBid{{ImpId: "imp2"}}, "seat3": []openrtb_ext.NonBid{{ImpId: "imp3"}}}, + }, + { + name: "nil_append", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: nil, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + }, + { + name: "empty_append", + builder: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, + toAppend: []SeatNonBidBuilder{}, + expected: SeatNonBidBuilder{"seat1": []openrtb_ext.NonBid{{ImpId: "imp1"}}}, }, { - name: "nil-seat-nonbids", - fields: fields{nil}, + name: "append_multiple_same_seat", + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + {ImpId: "imp1"}, + }, + }, + toAppend: []SeatNonBidBuilder{ + { + "seat1": []openrtb_ext.NonBid{ + {ImpId: "imp2"}, + }, + }, + }, + expected: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + {ImpId: "imp1"}, + {ImpId: "imp2"}, + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.fields.snb.get(); !assert.Equal(t, tt.want, got) { - t.Errorf("seatNonBids.get() = %v, want %v", got, tt.want) - } + tt.builder.append(tt.toAppend...) + assert.Equal(t, tt.expected, tt.builder) }) } } -var sampleSeatNonBidMap = func(seat string, nonBidCount int) map[string][]openrtb_ext.NonBid { - nonBids := make([]openrtb_ext.NonBid, 0) - for i := 0; i < nonBidCount; i++ { - nonBids = append(nonBids, openrtb_ext.NonBid{ - Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, - }) +func TestRejectImps(t *testing.T) { + tests := []struct { + name string + impIDs []string + builder SeatNonBidBuilder + want SeatNonBidBuilder + }{ + { + name: "nil_imps", + impIDs: nil, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{}, + }, + { + name: "empty_imps", + impIDs: []string{}, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{}, + }, + { + name: "one_imp", + impIDs: []string{"imp1"}, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 300, + }, + }, + }, + }, + { + name: "many_imps", + impIDs: []string{"imp1", "imp2"}, + builder: SeatNonBidBuilder{}, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 300, + }, + { + ImpId: "imp2", + StatusCode: 300, + }, + }, + }, + }, + { + name: "many_imps_appended_to_prepopulated_list", + impIDs: []string{"imp1", "imp2"}, + builder: SeatNonBidBuilder{ + "seat0": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 0, + }, + }, + }, + want: SeatNonBidBuilder{ + "seat0": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 0, + }, + }, + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 300, + }, + { + ImpId: "imp2", + StatusCode: 300, + }, + }, + }, + }, + { + name: "many_imps_appended_to_prepopulated_list_same_seat", + impIDs: []string{"imp1", "imp2"}, + builder: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 300, + }, + }, + }, + want: SeatNonBidBuilder{ + "seat1": []openrtb_ext.NonBid{ + { + ImpId: "imp0", + StatusCode: 300, + }, + { + ImpId: "imp1", + StatusCode: 300, + }, + { + ImpId: "imp2", + StatusCode: 300, + }, + }, + }, + }, } - return map[string][]openrtb_ext.NonBid{ - seat: nonBids, + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.builder.rejectImps(test.impIDs, 300, "seat1") + + assert.Equal(t, len(test.builder), len(test.want)) + for seat := range test.want { + assert.ElementsMatch(t, test.want[seat], test.builder[seat]) + } + }) } } -var sampleSeatBids = func(seat string, nonBidCount int) []openrtb_ext.SeatNonBid { - seatNonBids := make([]openrtb_ext.SeatNonBid, 0) - seatNonBid := openrtb_ext.SeatNonBid{ - Seat: seat, - NonBid: make([]openrtb_ext.NonBid, 0), +func TestSlice(t *testing.T) { + tests := []struct { + name string + builder SeatNonBidBuilder + want []openrtb_ext.SeatNonBid + }{ + { + name: "nil", + builder: nil, + want: []openrtb_ext.SeatNonBid{}, + }, + { + name: "empty", + builder: SeatNonBidBuilder{}, + want: []openrtb_ext.SeatNonBid{}, + }, + { + name: "one_no_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{}, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "a", + }, + }, + }, + { + name: "one_with_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + Seat: "a", + }, + }, + }, + { + name: "many_no_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{}, + "b": []openrtb_ext.NonBid{}, + "c": []openrtb_ext.NonBid{}, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "a", + }, + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "b", + }, + { + NonBid: []openrtb_ext.NonBid{}, + Seat: "c", + }, + }, + }, + { + name: "many_with_nonbids", + builder: SeatNonBidBuilder{ + "a": []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + "b": []openrtb_ext.NonBid{ + { + ImpId: "imp3", + StatusCode: 300, + }, + }, + "c": []openrtb_ext.NonBid{ + { + ImpId: "imp4", + StatusCode: 400, + }, + { + ImpId: "imp5", + StatusCode: 500, + }, + }, + }, + want: []openrtb_ext.SeatNonBid{ + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp1", + StatusCode: 100, + }, + { + ImpId: "imp2", + StatusCode: 200, + }, + }, + Seat: "a", + }, + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp3", + StatusCode: 300, + }, + }, + Seat: "b", + }, + { + NonBid: []openrtb_ext.NonBid{ + { + ImpId: "imp4", + StatusCode: 400, + }, + { + ImpId: "imp5", + StatusCode: 500, + }, + }, + Seat: "c", + }, + }, + }, } - for i := 0; i < nonBidCount; i++ { - seatNonBid.NonBid = append(seatNonBid.NonBid, openrtb_ext.NonBid{ - Ext: openrtb_ext.NonBidExt{Prebid: openrtb_ext.ExtResponseNonBidPrebid{Bid: openrtb_ext.NonBidObject{}}}, + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := test.builder.Slice() + assert.ElementsMatch(t, test.want, result) }) } - seatNonBids = append(seatNonBids, seatNonBid) - return seatNonBids } diff --git a/openrtb_ext/response.go b/openrtb_ext/response.go index d9baea3f4da..449ff939bf5 100644 --- a/openrtb_ext/response.go +++ b/openrtb_ext/response.go @@ -132,14 +132,14 @@ type NonBidExt struct { // NonBid represnts the Non Bid Reason (statusCode) for given impression ID type NonBid struct { - ImpId string `json:"impid"` - StatusCode int `json:"statuscode"` - Ext NonBidExt `json:"ext"` + ImpId string `json:"impid"` + StatusCode int `json:"statuscode"` + Ext *NonBidExt `json:"ext,omitempty"` } // SeatNonBid is collection of NonBid objects with seat information type SeatNonBid struct { NonBid []NonBid `json:"nonbid"` Seat string `json:"seat"` - Ext json.RawMessage `json:"ext"` + Ext json.RawMessage `json:"ext,omitempty"` } diff --git a/stored_requests/data/by_id/accounts/test.json b/stored_requests/data/by_id/accounts/test.json index 699f6bd1e57..a53f8997f37 100644 --- a/stored_requests/data/by_id/accounts/test.json +++ b/stored_requests/data/by_id/accounts/test.json @@ -49,4 +49,4 @@ } } } -} +} \ No newline at end of file From 93368cc9c4327be60622dae321e06bbfdb1e971f Mon Sep 17 00:00:00 2001 From: rtuschkany <35923908+rtuschkany@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:45:32 +0200 Subject: [PATCH 12/47] ConnectAd: String/int fix (#3925) --- adapters/connectad/connectad.go | 2 +- openrtb_ext/imp_connectad.go | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/adapters/connectad/connectad.go b/adapters/connectad/connectad.go index 83023195c54..083d6830935 100644 --- a/adapters/connectad/connectad.go +++ b/adapters/connectad/connectad.go @@ -139,7 +139,7 @@ func preprocess(request *openrtb2.BidRequest) []error { } func addImpInfo(imp *openrtb2.Imp, secure *int8, cadExt *openrtb_ext.ExtImpConnectAd) { - imp.TagID = strconv.Itoa(cadExt.SiteID) + imp.TagID = strconv.Itoa(int(cadExt.SiteID)) imp.Secure = secure if cadExt.Bidfloor != 0 { diff --git a/openrtb_ext/imp_connectad.go b/openrtb_ext/imp_connectad.go index c4c7ab696f2..d530534cf4f 100644 --- a/openrtb_ext/imp_connectad.go +++ b/openrtb_ext/imp_connectad.go @@ -1,7 +1,9 @@ package openrtb_ext +import "github.com/prebid/prebid-server/v2/util/jsonutil" + type ExtImpConnectAd struct { - NetworkID int `json:"networkId"` - SiteID int `json:"siteId"` - Bidfloor float64 `json:"bidfloor,omitempty"` + NetworkID jsonutil.StringInt `json:"networkId"` + SiteID jsonutil.StringInt `json:"siteId"` + Bidfloor float64 `json:"bidfloor,omitempty"` } From 11b6546f5bffe2737cdf0eb6c9a98d346e138504 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Mon, 23 Sep 2024 16:54:18 +0300 Subject: [PATCH 13/47] Rubicon: Pass PBS host info to XAPI (#3903) --- adapters/rubicon/rubicon.go | 12 +++- adapters/rubicon/rubicon_test.go | 69 ++++++++++++++++++- .../exemplary/25-26-transition-period.json | 5 +- .../rubicontest/exemplary/app-imp-fpd.json | 5 +- .../exemplary/bidonmultiformat.json | 10 ++- .../exemplary/flexible-schema.json | 5 +- .../exemplary/hardcode-secure.json | 5 +- .../exemplary/non-bidonmultiformat.json | 5 +- .../rubicontest/exemplary/simple-banner.json | 5 +- .../rubicontest/exemplary/simple-native.json | 5 +- .../rubicontest/exemplary/simple-video.json | 5 +- .../rubicontest/exemplary/site-imp-fpd.json | 5 +- .../rubicontest/exemplary/user-fpd.json | 5 +- .../supplemental/no-site-content-data.json | 5 +- .../supplemental/no-site-content.json | 5 +- 15 files changed, 134 insertions(+), 17 deletions(-) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 1cb2ab49465..9cf8eb6ff20 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -3,6 +3,7 @@ package rubicon import ( "encoding/json" "fmt" + "github.com/prebid/prebid-server/v2/version" "net/http" "net/url" "strconv" @@ -25,6 +26,7 @@ var bannerExtContent = []byte(`{"rp":{"mime":"text/html"}}`) type RubiconAdapter struct { URI string + externalURI string XAPIUsername string XAPIPassword string } @@ -219,6 +221,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co bidder := &RubiconAdapter{ URI: uri, + externalURI: server.ExternalUrl, XAPIUsername: config.XAPI.Username, XAPIPassword: config.XAPI.Password, } @@ -271,7 +274,7 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada rubiconRequest := *request for imp, bidderExt := range impsToExtMap { rubiconExt := bidderExt.Bidder - target, err := updateImpRpTargetWithFpdAttributes(bidderExt, rubiconExt, *imp, request.Site, request.App) + target, err := a.updateImpRpTarget(bidderExt, rubiconExt, *imp, request.Site, request.App) if err != nil { errs = append(errs, err) continue @@ -650,7 +653,7 @@ func resolveBidFloor(bidFloor float64, bidFloorCur string, reqInfo *adapters.Ext return bidFloor, nil } -func updateImpRpTargetWithFpdAttributes(extImp rubiconExtImpBidder, extImpRubicon openrtb_ext.ExtImpRubicon, +func (a *RubiconAdapter) updateImpRpTarget(extImp rubiconExtImpBidder, extImpRubicon openrtb_ext.ExtImpRubicon, imp openrtb2.Imp, site *openrtb2.Site, app *openrtb2.App) (json.RawMessage, error) { existingTarget, _, _, err := jsonparser.Get(imp.Ext, "rp", "target") @@ -743,6 +746,11 @@ func updateImpRpTargetWithFpdAttributes(extImp rubiconExtImpBidder, extImpRubico if len(extImpRubicon.Keywords) > 0 { addStringArrayAttribute(extImpRubicon.Keywords, target, "keywords") } + + target["pbs_login"] = a.XAPIUsername + target["pbs_version"] = version.Ver + target["pbs_url"] = a.externalURI + updatedTarget, err := json.Marshal(target) if err != nil { return nil, err diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index 18850d96f9f..ba72727c41c 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -608,6 +608,73 @@ func TestOpenRTBFirstPartyDataPopulating(t *testing.T) { } } +func TestPbsHostInfoPopulating(t *testing.T) { + bidder := RubiconAdapter{ + URI: "url", + externalURI: "externalUrl", + XAPIUsername: "username", + XAPIPassword: "password", + } + + request := &openrtb2.BidRequest{ + ID: "test-request-id", + Imp: []openrtb2.Imp{{ + ID: "test-imp-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + {W: 300, H: 250}, + }, + }, + Ext: json.RawMessage(`{ + "bidder": { + "zoneId": 8394, + "siteId": 283282, + "accountId": 7891, + "inventory": {"key1" : "val1"}, + "visitor": {"key2" : "val2"} + } + }`), + }}, + App: &openrtb2.App{ + ID: "com.test", + Name: "testApp", + }, + } + + reqs, _ := bidder.MakeRequests(request, &adapters.ExtraRequestInfo{}) + + rubiconReq := &openrtb2.BidRequest{} + if err := json.Unmarshal(reqs[0].Body, rubiconReq); err != nil { + t.Fatalf("Unexpected error while decoding request: %s", err) + } + + var rpImpExt rubiconImpExt + if err := json.Unmarshal(rubiconReq.Imp[0].Ext, &rpImpExt); err != nil { + t.Fatalf("Error unmarshalling imp.ext: %s", err) + } + + var pbsLogin string + pbsLogin, err := jsonparser.GetString(rpImpExt.RP.Target, "pbs_login") + if err != nil { + t.Fatal("Error extracting pbs_login") + } + assert.Equal(t, pbsLogin, "username", "Unexpected pbs_login value") + + var pbsVersion string + pbsVersion, err = jsonparser.GetString(rpImpExt.RP.Target, "pbs_version") + if err != nil { + t.Fatal("Error extracting pbs_version") + } + assert.Equal(t, pbsVersion, "", "Unexpected pbs_version value") + + var pbsUrl string + pbsUrl, err = jsonparser.GetString(rpImpExt.RP.Target, "pbs_url") + if err != nil { + t.Fatal("Error extracting pbs_url") + } + assert.Equal(t, pbsUrl, "externalUrl", "Unexpected pbs_url value") +} + func TestOpenRTBRequestWithBadvOverflowed(t *testing.T) { bidder := new(RubiconAdapter) @@ -990,7 +1057,7 @@ func TestOpenRTBResponseOverridePriceFromCorrespondingImp(t *testing.T) { "siteId": 68780, "zoneId": 327642, "debug": { - "cpmoverride" : 20 + "cpmoverride" : 20 } }}`), }}, diff --git a/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json b/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json index d64f2361521..0196c93049a 100644 --- a/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json +++ b/adapters/rubicon/rubicontest/exemplary/25-26-transition-period.json @@ -364,7 +364,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json b/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json index 914cfce03de..4e7c2a92148 100644 --- a/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json +++ b/adapters/rubicon/rubicontest/exemplary/app-imp-fpd.json @@ -349,7 +349,10 @@ "sectioncat": [ "sectionCat1", "sectionCat2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json b/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json index 33757cd889d..c47cd3f9c36 100644 --- a/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json +++ b/adapters/rubicon/rubicontest/exemplary/bidonmultiformat.json @@ -106,7 +106,10 @@ ], "search": [ "someSearch" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", @@ -213,7 +216,10 @@ ], "search": [ "someSearch" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/flexible-schema.json b/adapters/rubicon/rubicontest/exemplary/flexible-schema.json index 0434f6bab83..f4dcebc2280 100644 --- a/adapters/rubicon/rubicontest/exemplary/flexible-schema.json +++ b/adapters/rubicon/rubicontest/exemplary/flexible-schema.json @@ -349,7 +349,10 @@ "sectioncat": [ "sectionCat1", "sectionCat2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json b/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json index c349b86da91..9df133385c6 100644 --- a/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json +++ b/adapters/rubicon/rubicontest/exemplary/hardcode-secure.json @@ -323,7 +323,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json b/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json index 1e1485861fd..dae54aca730 100644 --- a/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json +++ b/adapters/rubicon/rubicontest/exemplary/non-bidonmultiformat.json @@ -105,7 +105,10 @@ ], "search": [ "someSearch" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/simple-banner.json b/adapters/rubicon/rubicontest/exemplary/simple-banner.json index d2da4ce8a8d..21ec12990da 100644 --- a/adapters/rubicon/rubicontest/exemplary/simple-banner.json +++ b/adapters/rubicon/rubicontest/exemplary/simple-banner.json @@ -323,7 +323,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/simple-native.json b/adapters/rubicon/rubicontest/exemplary/simple-native.json index f5e43ff9211..98bfdb4f845 100644 --- a/adapters/rubicon/rubicontest/exemplary/simple-native.json +++ b/adapters/rubicon/rubicontest/exemplary/simple-native.json @@ -307,7 +307,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/simple-video.json b/adapters/rubicon/rubicontest/exemplary/simple-video.json index 5552dd71816..3bceef2cf38 100644 --- a/adapters/rubicon/rubicontest/exemplary/simple-video.json +++ b/adapters/rubicon/rubicontest/exemplary/simple-video.json @@ -322,7 +322,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json b/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json index 87c20e227b1..b8519dabdb3 100644 --- a/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json +++ b/adapters/rubicon/rubicontest/exemplary/site-imp-fpd.json @@ -473,7 +473,10 @@ "sectionCat1", "sectionCat2" ], - "dfp_ad_unit_code": "adSlotFromData" + "dfp_ad_unit_code": "adSlotFromData", + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/exemplary/user-fpd.json b/adapters/rubicon/rubicontest/exemplary/user-fpd.json index 42ab0f0babe..c989c6a1e19 100644 --- a/adapters/rubicon/rubicontest/exemplary/user-fpd.json +++ b/adapters/rubicon/rubicontest/exemplary/user-fpd.json @@ -275,7 +275,10 @@ "search": [ "someSearch" ], - "dfp_ad_unit_code": "someAdSlot" + "dfp_ad_unit_code": "someAdSlot", + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json b/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json index b2f463a95dc..b121d75a0f6 100644 --- a/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json +++ b/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json @@ -234,7 +234,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", diff --git a/adapters/rubicon/rubicontest/supplemental/no-site-content.json b/adapters/rubicon/rubicontest/supplemental/no-site-content.json index 5744b2c9e47..65c3cacd858 100644 --- a/adapters/rubicon/rubicontest/supplemental/no-site-content.json +++ b/adapters/rubicon/rubicontest/supplemental/no-site-content.json @@ -230,7 +230,10 @@ "pagecat": [ "val1", "val2" - ] + ], + "pbs_login": "xuser", + "pbs_url": "http://hosturl.com", + "pbs_version": "" }, "track": { "mint": "", From 4462fcef3555e5d37cabecbbc12e8c98bd56ec30 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 25 Sep 2024 16:11:45 +0200 Subject: [PATCH 14/47] 51degrees module (#3893) --- Dockerfile | 12 +- README.md | 5 + go.mod | 5 + go.sum | 11 + modules/builder.go | 4 + .../fiftyonedegrees/devicedetection/README.md | 255 +++++++ .../devicedetection/account_info_extractor.go | 37 + .../account_info_extractor_test.go | 74 ++ .../devicedetection/account_validator.go | 28 + .../devicedetection/account_validator_test.go | 71 ++ .../fiftyonedegrees/devicedetection/config.go | 80 ++ .../devicedetection/config_test.go | 119 +++ .../devicedetection/context.go | 8 + .../devicedetection/device_detector.go | 157 ++++ .../devicedetection/device_detector_test.go | 190 +++++ .../devicedetection/device_info_extractor.go | 121 +++ .../device_info_extractor_test.go | 130 ++++ .../devicedetection/evidence_extractor.go | 118 +++ .../evidence_extractor_test.go | 256 +++++++ .../devicedetection/fiftyone_device_types.go | 77 ++ .../fiftyone_device_types_test.go | 90 +++ .../hook_auction_entrypoint.go | 27 + .../hook_raw_auction_request.go | 173 +++++ .../fiftyonedegrees/devicedetection/models.go | 66 ++ .../devicedetection/models_test.go | 63 ++ .../fiftyonedegrees/devicedetection/module.go | 107 +++ .../devicedetection/module_test.go | 703 ++++++++++++++++++ .../request_headers_extractor.go | 47 ++ .../request_headers_extractor_test.go | 118 +++ .../devicedetection/sample/pbs.json | 84 +++ .../devicedetection/sample/request_data.json | 114 +++ .../devicedetection/sua_payload_extractor.go | 144 ++++ 32 files changed, 3491 insertions(+), 3 deletions(-) create mode 100644 modules/fiftyonedegrees/devicedetection/README.md create mode 100644 modules/fiftyonedegrees/devicedetection/account_info_extractor.go create mode 100644 modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/account_validator.go create mode 100644 modules/fiftyonedegrees/devicedetection/account_validator_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/config.go create mode 100644 modules/fiftyonedegrees/devicedetection/config_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/context.go create mode 100644 modules/fiftyonedegrees/devicedetection/device_detector.go create mode 100644 modules/fiftyonedegrees/devicedetection/device_detector_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/device_info_extractor.go create mode 100644 modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/evidence_extractor.go create mode 100644 modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go create mode 100644 modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go create mode 100644 modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go create mode 100644 modules/fiftyonedegrees/devicedetection/models.go create mode 100644 modules/fiftyonedegrees/devicedetection/models_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/module.go create mode 100644 modules/fiftyonedegrees/devicedetection/module_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/request_headers_extractor.go create mode 100644 modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go create mode 100644 modules/fiftyonedegrees/devicedetection/sample/pbs.json create mode 100644 modules/fiftyonedegrees/devicedetection/sample/request_data.json create mode 100644 modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go diff --git a/Dockerfile b/Dockerfile index 17b6ce20387..2838ba17e3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,10 +11,14 @@ WORKDIR /app/prebid-server/ ENV GOROOT=/usr/local/go ENV PATH=$GOROOT/bin:$PATH ENV GOPROXY="https://proxy.golang.org" + +# Installing gcc as cgo uses it to build native code of some modules RUN apt-get update && \ - apt-get install -y git && \ + apt-get install -y git gcc && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -ENV CGO_ENABLED 0 + +# CGO must be enabled because some modules depend on native C code +ENV CGO_ENABLED 1 COPY ./ ./ RUN go mod tidy RUN go mod vendor @@ -30,8 +34,10 @@ RUN chmod a+xr prebid-server COPY static static/ COPY stored_requests/data stored_requests/data RUN chmod -R a+r static/ stored_requests/data + +# Installing libatomic1 as it is a runtime dependency for some modules RUN apt-get update && \ - apt-get install -y ca-certificates mtr && \ + apt-get install -y ca-certificates mtr libatomic1 && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN adduser prebid_user USER prebid_user diff --git a/README.md b/README.md index 4a44ecd7c6a..b616594a68a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,11 @@ or compile a standalone binary using the command: ``` bash go build . ``` +**Note:** if building from source there are a couple dependencies to be aware of: +1. *Compile-time*. Some modules ship native code that requires `cgo` (comes with the `go` compiler) being enabled - by default it is and environment variable `CGO_ENABLED=1` do NOT set it to `0`. +2. *Compile-time*. `cgo` depends on the C-compiler, which usually is `gcc`, but can be changed by setting the value of `CC` env var, f.e. `CC=clang`. On ubuntu `gcc` can be installed via `sudo apt-get install gcc`. +3. *Runtime*. Some modules require `libatomic`. On ubuntu it is installed by running `sudo apt-get install libatomic1`. `libatomic1` is a dependency of `gcc`, so if you are building with `gcc` and running on the same machine, it is likely that `libatomic1` is already installed. + Ensure that you deploy the `/static` directory, as Prebid Server requires those files at startup. ## Developing diff --git a/go.mod b/go.mod index 81f94ad1ccf..b27d55e803d 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( ) require ( + github.com/51Degrees/device-detection-go/v4 v4.4.35 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -67,6 +68,10 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect diff --git a/go.sum b/go.sum index 8ada75c73cb..a6a6226c616 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/51Degrees/device-detection-go/v4 v4.4.35 h1:qhP2tzoXhGE1aYY3NftMJ+ccxz0+2kM8aF4SH7fTyuA= +github.com/51Degrees/device-detection-go/v4 v4.4.35/go.mod h1:dbdG1fySqdY+a5pUnZ0/G0eD03G6H3Vh8kRC+1f9qSc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= @@ -487,6 +489,15 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/vrischmann/go-metrics-influxdb v0.1.1 h1:xneKFRjsS4BiVYvAKaM/rOlXYd1pGHksnES0ECCJLgo= github.com/vrischmann/go-metrics-influxdb v0.1.1/go.mod h1:q7YC8bFETCYopXRMtUvQQdLaoVhpsEwvQS2zZEYCqg8= diff --git a/modules/builder.go b/modules/builder.go index e5d04e149af..36ac5589add 100644 --- a/modules/builder.go +++ b/modules/builder.go @@ -1,6 +1,7 @@ package modules import ( + fiftyonedegreesDevicedetection "github.com/prebid/prebid-server/v2/modules/fiftyonedegrees/devicedetection" prebidOrtb2blocking "github.com/prebid/prebid-server/v2/modules/prebid/ortb2blocking" ) @@ -8,6 +9,9 @@ import ( // vendor and module names are chosen based on the module directory name func builders() ModuleBuilders { return ModuleBuilders{ + "fiftyonedegrees": { + "devicedetection": fiftyonedegreesDevicedetection.Builder, + }, "prebid": { "ortb2blocking": prebidOrtb2blocking.Builder, }, diff --git a/modules/fiftyonedegrees/devicedetection/README.md b/modules/fiftyonedegrees/devicedetection/README.md new file mode 100644 index 00000000000..645fb407fe5 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/README.md @@ -0,0 +1,255 @@ +## Overview + +The 51Degrees module enriches an incoming OpenRTB request with [51Degrees Device Data](https://51degrees.com/documentation/_device_detection__overview.html). + +The module sets the following fields of the device object: `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pxratio` - interested bidder adapters may use these fields as needed. In addition the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID which can be rapidly looked up in on premise data exposing over 250 properties including the device age, chip set, codec support, and price, operating system and app/browser versions, age, and embedded features. + +## Operation Details + +### Evidence + +The module uses `device.ua` (User Agent) and `device.sua` (Structured User Agent) provided in the oRTB request payload as input (or 'evidence' in 51Degrees terminology). There is a fallback to the corresponding HTTP request headers if any of these are not present in the oRTB payload - in particular: `User-Agent` and `Sec-CH-UA-*` (aka User-Agent Client Hints). To make sure Prebid.js sends Structured User Agent in the oRTB payload - we strongly advice publishers to enable [First Party Data Enrichment module](dev-docs/modules/enrichmentFpdModule.html) for their wrappers and specify + +```js +pbjs.setConfig({ + firstPartyData: { + uaHints: [ + 'architecture', + 'model', + 'platform', + 'platformVersion', + 'fullVersionList', + ] + } +}) +``` + +### Data File Updates + +The module operates **fully autonomously and does not make any requests to any cloud services in real time to do device detection**. This is an [on-premise data](https://51degrees.com/developers/deployment-options/on-premise-data) deployment in 51Degrees terminology. The module operates using a local data file that is loaded into memory fully or partially during operation. The data file is occasionally updated to accomodate new devices, so it is recommended to enable automatic data updates in the module configuration. Alternatively `watch_file_system` option can be used and the file may be downloaded and replaced on disk manually. See the configuration options below. + +## Setup + +The 51Degrees module operates using a data file. You can get started with a free Lite data file that can be downloaded here: [51Degrees-LiteV4.1.hash](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash). The Lite file is capable of detecting limited device information, so if you need in-depth device data, please contact 51Degrees to obtain a license: [https://51degrees.com/contact-us](https://51degrees.com/contact-us?ContactReason=Free%20Trial). + +Put the data file in a file system location writable by the system account that is running the Prebid Server module and specify that directory location in the configuration parameters. The location needs to be writable if you would like to enable [automatic data file updates](https://51degrees.com/documentation/_features__automatic_datafile_updates.html). + +### Execution Plan + +This module supports running at two stages: + +* entrypoint: this is where incoming requests are parsed and device detection evidences are extracted. +* raw-auction-request: this is where outgoing auction requests to each bidder are enriched with the device detection data + +We recommend defining the execution plan right in the account config +so the module is only invoked for specific accounts. See below for an example. + +### Global Config + +There is no host-company level config for this module. + +### Account-Level Config + +To start using current module in PBS-Go you have to enable module and add `fiftyone-devicedetection-entrypoint-hook` and `fiftyone-devicedetection-raw-auction-request-hook` into hooks execution plan inside your config file: +Here's a general template for the account config used in PBS-Go: + +```json +{ + "hooks": { + "enabled":true, + "modules": { + "fiftyonedegrees": { + "devicedetection": { + "enabled": true, + "make_temp_copy": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash", + "update": { + "auto": true, + "url": "", + "polling_interval": 1800, + "license_key": "", + "product": "V4Enterprise", + "watch_file_system": "true", + "on_startup": true + } + } + } + }, + "host_execution_plan": { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-raw-auction-request-hook" + } + ] + } + ] + } + } + } + } + } + } + } +} +``` + +The same config in YAML format: +```yaml +hooks: + enabled: true + modules: + fiftyonedegrees: + devicedetection: + enabled: true + make_temp_copy: true + data_file: + path: path/to/51Degrees-LiteV4.1.hash + update: + auto: true + url: "" + polling_interval: 1800 + license_key: "" + product: V4Enterprise + watch_file_system: 'true' + host_execution_plan: + endpoints: + "/openrtb2/auction": + stages: + entrypoint: + groups: + - timeout: 10 + hook_sequence: + - module_code: fiftyonedegrees.devicedetection + hook_impl_code: fiftyone-devicedetection-entrypoint-hook + raw_auction_request: + groups: + - timeout: 10 + hook_sequence: + - module_code: fiftyonedegrees.devicedetection + hook_impl_code: fiftyone-devicedetection-raw-auction-request-hook +``` + +Note that at a minimum (besides adding to the host_execution_plan) you need to enable the module and specify a path to the data file in the configuration. +Sample module enablement configuration in JSON and YAML formats: + +```json +{ + "modules": { + "fiftyonedegrees": { + "devicedetection": { + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash" + } + } + } + } +} +``` + +```yaml + modules: + fiftyonedegrees: + devicedetection: + enabled: true + data_file: + path: "/path/to/51Degrees-LiteV4.1.hash" +``` + +## Module Configuration Parameters + +The parameter names are specified with full path using dot-notation. F.e. `section_name` .`sub_section` .`param_name` would result in this nesting in the JSON configuration: + +```json +{ + "section_name": { + "sub_section": { + "param_name": "param-value" + } + } +} +``` + +| Param Name | Required| Type | Default value | Description | +|:-------|:------|:------|:------|:---------------------------------------| +| `account_filter` .`allow_list` | No | list of strings | [] (empty list) | A list of account IDs that are allowed to use this module - only relevant if enabled globally for the host. If empty, all accounts are allowed. Full-string match is performed (whitespaces and capitalization matter). | +| `data_file` .`path` | **Yes** | string | null |The full path to the device detection data file. Sample file can be downloaded from [data repo on GitHub](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash), or get an Enterprise data file [here](https://51degrees.com/pricing). | +| `data_file` .`make_temp_copy` | No | boolean | true | If true, the engine will create a temporary copy of the data file rather than using the data file directly. | +| `data_file` .`update` .`auto` | No | boolean | true | If enabled, the engine will periodically (at predefined time intervals - see `polling-interval` parameter) check if new data file is available. When the new data file is available engine downloads it and switches to it for device detection. If custom `url` is not specified `license_key` param is required. | +| `data_file` .`update` .`on_startup` | No | boolean | false | If enabled, engine will check for the updated data file right away without waiting for the defined time interval. | +| `data_file` .`update` .`url` | No | string | null | Configure the engine to check the specified URL for the availability of the updated data file. If not specified the [51Degrees distributor service](https://51degrees.com/documentation/4.4/_info__distributor.html) URL will be used, which requires a License Key. | +| `data_file` .`update` .`license_key` | No | string | null | Required if `auto` is true and custom `url` is not specified. Allows to download the data file from the [51Degrees distributor service](https://51degrees.com/documentation/4.4/_info__distributor.html). | +| `data_file` .`update` .`watch_file_system` | No | boolean | true | If enabled the engine will watch the data file path for any changes, and automatically reload the data file from disk once it is updated. | +| `data_file` .`update` .`polling_interval` | No | int | 1800 | The time interval in seconds between consequent attempts to download an updated data file. Default = 1800 seconds = 30 minutes. | +| `data_file` .`update` .`product`| No | string | `V4Enterprise` | Set the Product used when checking for new device detection data files. A Product is exclusive to the 51Degrees paid service. Please see options [here](https://51degrees.com/documentation/_info__distributor.html). | +| `performance` .`profile` | No | string | `Balanced` | `performance.*` parameters are related to the tradeoffs between speed of device detection and RAM consumption or accuracy. `profile` dictates the proportion between the use of the RAM (the more RAM used - the faster is the device detection) and reads from disk (less RAM but slower device detection). Must be one of: `LowMemory`, `MaxPerformance`, `HighPerformance`, `Balanced`, `BalancedTemp`, `InMemory`. Defaults to `Balanced`. | +| `performance` .`concurrency` | No | int | 10 | Specify the expected number of concurrent operations that engine does. This sets the concurrency of the internal caches to avoid excessive locking. Default: 10. | +| `performance` .`difference` | No | int | 0 | Set the maximum difference to allow when processing evidence (HTTP headers). The meaning is the difference in hash value between the hash that was found, and the hash that is being searched for. By default this is 0. For more information see [51Degrees documentation](https://51degrees.com/documentation/_device_detection__hash.html). | +| `performance` .`drift` | No | int | 0 | Set the maximum drift to allow when matching hashes. If the drift is exceeded, the result is considered invalid and values will not be returned. By default this is 0. For more information see [51Degrees documentation](https://51degrees.com/documentation/_device_detection__hash.html). | +| `performance` .`allow_unmatched` | No | boolean | false | If set to false, a non-matching evidence will result in properties with no values set. If set to true, a non-matching evidence will cause the 'default profiles' to be returned. This means that properties will always have values (i.e. no need to check .hasValue) but some may be inaccurate. By default, this is false. | + +## Running the demo + +1. Download dependencies: +```bash +go mod download +``` + +2. Replace the original config file `pbs.json` (placed in the repository root or in `/etc/config`) with the sample [config file](sample/pbs.json): +``` +cp modules/fiftyonedegrees/devicedetection/sample/pbs.json pbs.json +``` + +3. Download `51Degrees-LiteV4.1.hash` from [[GitHub](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash)] and put it in the project root directory. + +```bash +curl -o 51Degrees-LiteV4.1.hash -L https://github.com/51Degrees/device-detection-data/raw/main/51Degrees-LiteV4.1.hash +``` + +4. Create a directory for sample stored requests (needed for the server to run): +```bash +mkdir -p sample/stored +``` + +5. Start the server: +```bash +go run main.go +``` + +6. Run sample request: +```bash +curl \ +--header "Content-Type: application/json" \ +http://localhost:8000/openrtb2/auction \ +--data @modules/fiftyonedegrees/devicedetection/sample/request_data.json +``` + +7. Observe the `device` object get enriched with `devicetype`, `os`, `osv`, `w`, `h` and `ext.fiftyonedegrees_deviceId`. + +## Maintainer contacts + +Any suggestions or questions can be directed to [support@51degrees.com](support@51degrees.com) e-mail. + +Or just open new [issue](https://github.com/prebid/prebid-server/issues/new) or [pull request](https://github.com/prebid/prebid-server/pulls) in this repository. diff --git a/modules/fiftyonedegrees/devicedetection/account_info_extractor.go b/modules/fiftyonedegrees/devicedetection/account_info_extractor.go new file mode 100644 index 00000000000..2a5168cfe0c --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_info_extractor.go @@ -0,0 +1,37 @@ +package devicedetection + +import ( + "github.com/tidwall/gjson" +) + +type accountInfo struct { + Id string +} + +type accountInfoExtractor struct{} + +func newAccountInfoExtractor() accountInfoExtractor { + return accountInfoExtractor{} +} + +// extract extracts the account information from the payload +// The account information is extracted from the publisher id or site publisher id +func (x accountInfoExtractor) extract(payload []byte) *accountInfo { + if payload == nil { + return nil + } + + publisherResult := gjson.GetBytes(payload, "app.publisher.id") + if publisherResult.Exists() { + return &accountInfo{ + Id: publisherResult.String(), + } + } + publisherResult = gjson.GetBytes(payload, "site.publisher.id") + if publisherResult.Exists() { + return &accountInfo{ + Id: publisherResult.String(), + } + } + return nil +} diff --git a/modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go b/modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go new file mode 100644 index 00000000000..2d32f7915b5 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go @@ -0,0 +1,74 @@ +package devicedetection + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + siteRequestPayload = []byte(` + { + "site": { + "publisher": { + "id": "p-bid-config-test-005" + } + } + } + `) + + mobileRequestPayload = []byte(` + { + "app": { + "publisher": { + "id": "p-bid-config-test-005" + } + } + } + `) + + emptyPayload = []byte(`{}`) +) + +func TestPublisherIdExtraction(t *testing.T) { + tests := []struct { + name string + payload []byte + expected string + expectNil bool + }{ + { + name: "SiteRequest", + payload: siteRequestPayload, + expected: "p-bid-config-test-005", + }, + { + name: "MobileRequest", + payload: mobileRequestPayload, + expected: "p-bid-config-test-005", + }, + { + name: "EmptyPublisherId", + payload: emptyPayload, + expectNil: true, + }, + { + name: "EmptyPayload", + payload: nil, + expectNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + extractor := newAccountInfoExtractor() + accountInfo := extractor.extract(tt.payload) + + if tt.expectNil { + assert.Nil(t, accountInfo) + } else { + assert.Equal(t, tt.expected, accountInfo.Id) + } + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/account_validator.go b/modules/fiftyonedegrees/devicedetection/account_validator.go new file mode 100644 index 00000000000..fdff92531a7 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_validator.go @@ -0,0 +1,28 @@ +package devicedetection + +import "slices" + +// defaultAccountValidator is a struct that contains an accountInfoExtractor +// and is used to validate if an account is allowed +type defaultAccountValidator struct { + AccountExtractor accountInfoExtractor +} + +func newAccountValidator() *defaultAccountValidator { + return &defaultAccountValidator{ + AccountExtractor: newAccountInfoExtractor(), + } +} + +func (x defaultAccountValidator) isAllowed(cfg config, req []byte) bool { + if len(cfg.AccountFilter.AllowList) == 0 { + return true + } + + accountInfo := x.AccountExtractor.extract(req) + if accountInfo != nil && slices.Contains(cfg.AccountFilter.AllowList, accountInfo.Id) { + return true + } + + return false +} diff --git a/modules/fiftyonedegrees/devicedetection/account_validator_test.go b/modules/fiftyonedegrees/devicedetection/account_validator_test.go new file mode 100644 index 00000000000..25f99e3b796 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/account_validator_test.go @@ -0,0 +1,71 @@ +package devicedetection + +import ( + "encoding/json" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + + "github.com/stretchr/testify/assert" +) + +func TestIsAllowed(t *testing.T) { + tests := []struct { + name string + allowList []string + expectedResult bool + }{ + { + name: "allowed", + allowList: []string{"1001"}, + expectedResult: true, + }, + { + name: "empty", + allowList: []string{}, + expectedResult: true, + }, + { + name: "disallowed", + allowList: []string{"1002"}, + expectedResult: false, + }, + { + name: "allow_list_is_nil", + allowList: nil, + expectedResult: true, + }, + { + name: "allow_list_contains_multiple", + allowList: []string{"1000", "1001", "1002"}, + expectedResult: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + validator := newAccountValidator() + cfg := config{ + AccountFilter: accountFilter{AllowList: test.allowList}, + } + + res := validator.isAllowed( + cfg, toBytes( + &openrtb2.BidRequest{ + App: &openrtb2.App{ + Publisher: &openrtb2.Publisher{ + ID: "1001", + }, + }, + }, + ), + ) + assert.Equal(t, test.expectedResult, res) + }) + } +} + +func toBytes(v interface{}) []byte { + res, _ := json.Marshal(v) + return res +} diff --git a/modules/fiftyonedegrees/devicedetection/config.go b/modules/fiftyonedegrees/devicedetection/config.go new file mode 100644 index 00000000000..a5519026791 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/config.go @@ -0,0 +1,80 @@ +package devicedetection + +import ( + "encoding/json" + "os" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/pkg/errors" + + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +type config struct { + DataFile dataFile `json:"data_file"` + AccountFilter accountFilter `json:"account_filter"` + Performance performance `json:"performance"` +} + +type dataFile struct { + Path string `json:"path"` + Update dataFileUpdate `json:"update"` + MakeTempCopy *bool `json:"make_temp_copy"` +} + +type dataFileUpdate struct { + Auto bool `json:"auto"` + Url string `json:"url"` + License string `json:"license_key"` + PollingInterval int `json:"polling_interval"` + Product string `json:"product"` + WatchFileSystem *bool `json:"watch_file_system"` + OnStartup bool `json:"on_startup"` +} + +type accountFilter struct { + AllowList []string `json:"allow_list"` +} + +type performance struct { + Profile string `json:"profile"` + Concurrency *int `json:"concurrency"` + Difference *int `json:"difference"` + AllowUnmatched *bool `json:"allow_unmatched"` + Drift *int `json:"drift"` +} + +var performanceProfileMap = map[string]dd.PerformanceProfile{ + "Default": dd.Default, + "LowMemory": dd.LowMemory, + "BalancedTemp": dd.BalancedTemp, + "Balanced": dd.Balanced, + "HighPerformance": dd.HighPerformance, + "InMemory": dd.InMemory, +} + +func (c *config) getPerformanceProfile() dd.PerformanceProfile { + mappedResult, ok := performanceProfileMap[c.Performance.Profile] + if !ok { + return dd.Default + } + + return mappedResult +} + +func parseConfig(data json.RawMessage) (config, error) { + var cfg config + if err := jsonutil.UnmarshalValid(data, &cfg); err != nil { + return cfg, errors.Wrap(err, "failed to parse config") + } + return cfg, nil +} + +func validateConfig(cfg config) error { + _, err := os.Stat(cfg.DataFile.Path) + if err != nil { + return errors.Wrap(err, "error opening hash file path") + } + + return nil +} diff --git a/modules/fiftyonedegrees/devicedetection/config_test.go b/modules/fiftyonedegrees/devicedetection/config_test.go new file mode 100644 index 00000000000..e2478d82b7d --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/config_test.go @@ -0,0 +1,119 @@ +package devicedetection + +import ( + "os" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/stretchr/testify/assert" +) + +func TestParseConfig(t *testing.T) { + cfgRaw := []byte(`{ + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "license_key": "your_license_key", + "product": "V4Enterprise", + "on_startup": true + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`) + + cfg, err := parseConfig(cfgRaw) + + assert.NoError(t, err) + + assert.Equal(t, cfg.DataFile.Path, "path/to/51Degrees-LiteV4.1.hash") + assert.True(t, cfg.DataFile.Update.Auto) + assert.Equal(t, cfg.DataFile.Update.Url, "https://my.datafile.com/datafile.gz") + assert.Equal(t, cfg.DataFile.Update.PollingInterval, 3600) + assert.Equal(t, cfg.DataFile.Update.License, "your_license_key") + assert.Equal(t, cfg.DataFile.Update.Product, "V4Enterprise") + assert.True(t, cfg.DataFile.Update.OnStartup) + assert.Equal(t, cfg.AccountFilter.AllowList, []string{"123"}) + assert.Equal(t, cfg.Performance.Profile, "default") + assert.Equal(t, *cfg.Performance.Concurrency, 1) + assert.Equal(t, *cfg.Performance.Difference, 1) + assert.True(t, *cfg.Performance.AllowUnmatched) + assert.Equal(t, *cfg.Performance.Drift, 1) + assert.Equal(t, cfg.getPerformanceProfile(), dd.Default) +} + +func TestValidateConfig(t *testing.T) { + file, err := os.Create("test-validate-config.hash") + if err != nil { + t.Errorf("Failed to create file: %v", err) + } + defer file.Close() + defer os.Remove("test-validate-config.hash") + + cfgRaw := []byte(`{ + "enabled": true, + "data_file": { + "path": "test-validate-config.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "licence_key": "your_licence_key", + "product": "V4Enterprise" + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`) + + cfg, err := parseConfig(cfgRaw) + assert.NoError(t, err) + + err = validateConfig(cfg) + assert.NoError(t, err) + +} + +func TestInvalidPerformanceProfile(t *testing.T) { + cfgRaw := []byte(`{ + "enabled": true, + "data_file": { + "path": "test-validate-config.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "licence_key": "your_licence_key", + "product": "V4Enterprise" + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "123", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`) + cfg, err := parseConfig(cfgRaw) + assert.NoError(t, err) + + assert.Equal(t, cfg.getPerformanceProfile(), dd.Default) +} diff --git a/modules/fiftyonedegrees/devicedetection/context.go b/modules/fiftyonedegrees/devicedetection/context.go new file mode 100644 index 00000000000..3c10dd2f393 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/context.go @@ -0,0 +1,8 @@ +package devicedetection + +// Context keys for device detection +const ( + evidenceFromHeadersCtxKey = "evidence_from_headers" + evidenceFromSuaCtxKey = "evidence_from_sua" + ddEnabledCtxKey = "dd_enabled" +) diff --git a/modules/fiftyonedegrees/devicedetection/device_detector.go b/modules/fiftyonedegrees/devicedetection/device_detector.go new file mode 100644 index 00000000000..8369d343d34 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_detector.go @@ -0,0 +1,157 @@ +package devicedetection + +import ( + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/pkg/errors" +) + +type engine interface { + Process(evidences []onpremise.Evidence) (*dd.ResultsHash, error) + GetHttpHeaderKeys() []dd.EvidenceKey +} + +type extractor interface { + extract(results Results, ua string) (*deviceInfo, error) +} + +type defaultDeviceDetector struct { + cfg *dd.ConfigHash + deviceInfoExtractor extractor + engine engine +} + +func newDeviceDetector(cfg *dd.ConfigHash, moduleConfig *config) (*defaultDeviceDetector, error) { + engineOptions := buildEngineOptions(moduleConfig, cfg) + + ddEngine, err := onpremise.New( + engineOptions..., + ) + if err != nil { + return nil, errors.Wrap(err, "Failed to create onpremise engine.") + } + + deviceDetector := &defaultDeviceDetector{ + engine: ddEngine, + cfg: cfg, + deviceInfoExtractor: newDeviceInfoExtractor(), + } + + return deviceDetector, nil +} + +func buildEngineOptions(moduleConfig *config, configHash *dd.ConfigHash) []onpremise.EngineOptions { + options := []onpremise.EngineOptions{ + onpremise.WithDataFile(moduleConfig.DataFile.Path), + } + + options = append( + options, + onpremise.WithProperties([]string{ + "HardwareVendor", + "HardwareName", + "DeviceType", + "PlatformVendor", + "PlatformName", + "PlatformVersion", + "BrowserVendor", + "BrowserName", + "BrowserVersion", + "ScreenPixelsWidth", + "ScreenPixelsHeight", + "PixelRatio", + "Javascript", + "GeoLocation", + "HardwareModel", + "HardwareFamily", + "HardwareModelVariants", + "ScreenInchesHeight", + "IsCrawler", + }), + ) + + options = append( + options, + onpremise.WithConfigHash(configHash), + ) + + if moduleConfig.DataFile.MakeTempCopy != nil { + options = append( + options, + onpremise.WithTempDataCopy(*moduleConfig.DataFile.MakeTempCopy), + ) + } + + dataUpdateOptions := []onpremise.EngineOptions{ + onpremise.WithAutoUpdate(moduleConfig.DataFile.Update.Auto), + } + + if moduleConfig.DataFile.Update.Url != "" { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithDataUpdateUrl( + moduleConfig.DataFile.Update.Url, + ), + ) + } + + if moduleConfig.DataFile.Update.PollingInterval > 0 { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithPollingInterval( + moduleConfig.DataFile.Update.PollingInterval, + ), + ) + } + + if moduleConfig.DataFile.Update.License != "" { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithLicenseKey(moduleConfig.DataFile.Update.License), + ) + } + + if moduleConfig.DataFile.Update.Product != "" { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithProduct(moduleConfig.DataFile.Update.Product), + ) + } + + if moduleConfig.DataFile.Update.WatchFileSystem != nil { + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithFileWatch( + *moduleConfig.DataFile.Update.WatchFileSystem, + ), + ) + } + + dataUpdateOptions = append( + dataUpdateOptions, + onpremise.WithUpdateOnStart(moduleConfig.DataFile.Update.OnStartup), + ) + + options = append( + options, + dataUpdateOptions..., + ) + + return options +} + +func (x defaultDeviceDetector) getSupportedHeaders() []dd.EvidenceKey { + return x.engine.GetHttpHeaderKeys() +} + +func (x defaultDeviceDetector) getDeviceInfo(evidence []onpremise.Evidence, ua string) (*deviceInfo, error) { + results, err := x.engine.Process(evidence) + if err != nil { + return nil, errors.Wrap(err, "Failed to process evidence") + } + defer results.Free() + + deviceInfo, err := x.deviceInfoExtractor.extract(results, ua) + + return deviceInfo, err +} diff --git a/modules/fiftyonedegrees/devicedetection/device_detector_test.go b/modules/fiftyonedegrees/devicedetection/device_detector_test.go new file mode 100644 index 00000000000..84d6ab28cc0 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_detector_test.go @@ -0,0 +1,190 @@ +package devicedetection + +import ( + "fmt" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestBuildEngineOptions(t *testing.T) { + cases := []struct { + cfgRaw []byte + length int + }{ + { + cfgRaw: []byte(`{ + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "license_key": "your_license_key", + "product": "V4Enterprise", + "watch_file_system": true, + "on_startup": true + }, + "make_temp_copy": true + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`), + length: 11, + // data_file.path, data_file.update.auto:true, url, polling_interval, license_key, product, confighash, properties + // data_file.update.on_startup:true, data_file.update.watch_file_system:true, data_file.make_temp_copy:true + }, + { + cfgRaw: []byte(`{ + "enabled": true, + "data_file": { + "path": "path/to/51Degrees-LiteV4.1.hash" + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "default", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`), + length: 5, // data_file.update.auto:false, data_file.path, confighash, properties, data_file.update.on_startup:false + }, + } + + for _, c := range cases { + cfg, err := parseConfig(c.cfgRaw) + assert.NoError(t, err) + configHash := configHashFromConfig(&cfg) + options := buildEngineOptions(&cfg, configHash) + assert.Equal(t, c.length, len(options)) + } +} + +type engineMock struct { + mock.Mock +} + +func (e *engineMock) Process(evidences []onpremise.Evidence) (*dd.ResultsHash, error) { + args := e.Called(evidences) + res := args.Get(0) + if res == nil { + return nil, args.Error(1) + } + + return res.(*dd.ResultsHash), args.Error(1) +} + +func (e *engineMock) GetHttpHeaderKeys() []dd.EvidenceKey { + args := e.Called() + return args.Get(0).([]dd.EvidenceKey) +} + +type extractorMock struct { + mock.Mock +} + +func (e *extractorMock) extract(results Results, ua string) (*deviceInfo, error) { + args := e.Called(results, ua) + return args.Get(0).(*deviceInfo), args.Error(1) +} + +func TestGetDeviceInfo(t *testing.T) { + tests := []struct { + name string + engineResponse *dd.ResultsHash + engineError error + expectedResult *deviceInfo + expectedError string + }{ + { + name: "Success_path", + engineResponse: &dd.ResultsHash{}, + engineError: nil, + expectedResult: &deviceInfo{ + DeviceId: "123", + }, + expectedError: "", + }, + { + name: "Error_path", + engineResponse: nil, + engineError: fmt.Errorf("error"), + expectedResult: nil, + expectedError: "Failed to process evidence: error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + extractorM := &extractorMock{} + extractorM.On("extract", mock.Anything, mock.Anything).Return( + &deviceInfo{ + DeviceId: "123", + }, nil, + ) + + engineM := &engineMock{} + engineM.On("Process", mock.Anything).Return( + tt.engineResponse, tt.engineError, + ) + + deviceDetector := defaultDeviceDetector{ + cfg: nil, + deviceInfoExtractor: extractorM, + engine: engineM, + } + + result, err := deviceDetector.getDeviceInfo( + []onpremise.Evidence{{ + Prefix: dd.HttpEvidenceQuery, + Key: "key", + Value: "val", + }}, "ua", + ) + + if tt.expectedError == "" { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tt.expectedResult.DeviceId, result.DeviceId) + } else { + assert.Errorf(t, err, tt.expectedError) + assert.Nil(t, result) + } + }) + } +} + +func TestGetSupportedHeaders(t *testing.T) { + engineM := &engineMock{} + + engineM.On("GetHttpHeaderKeys").Return( + []dd.EvidenceKey{{ + Key: "key", + Prefix: dd.HttpEvidenceQuery, + }}, + ) + + deviceDetector := defaultDeviceDetector{ + cfg: nil, + deviceInfoExtractor: nil, + engine: engineM, + } + + result := deviceDetector.getSupportedHeaders() + assert.NotNil(t, result) + assert.Equal(t, len(result), 1) + assert.Equal(t, result[0].Key, "key") + +} diff --git a/modules/fiftyonedegrees/devicedetection/device_info_extractor.go b/modules/fiftyonedegrees/devicedetection/device_info_extractor.go new file mode 100644 index 00000000000..1c913e21696 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_info_extractor.go @@ -0,0 +1,121 @@ +package devicedetection + +import ( + "strconv" + + "github.com/golang/glog" + "github.com/pkg/errors" +) + +// deviceInfoExtractor is a struct that contains the methods to extract device information +// from the results of the device detection +type deviceInfoExtractor struct{} + +func newDeviceInfoExtractor() deviceInfoExtractor { + return deviceInfoExtractor{} +} + +type Results interface { + ValuesString(string, string) (string, error) + HasValues(string) (bool, error) + DeviceId() (string, error) +} + +type deviceInfoProperty string + +const ( + deviceInfoHardwareVendor deviceInfoProperty = "HardwareVendor" + deviceInfoHardwareName deviceInfoProperty = "HardwareName" + deviceInfoDeviceType deviceInfoProperty = "DeviceType" + deviceInfoPlatformVendor deviceInfoProperty = "PlatformVendor" + deviceInfoPlatformName deviceInfoProperty = "PlatformName" + deviceInfoPlatformVersion deviceInfoProperty = "PlatformVersion" + deviceInfoBrowserVendor deviceInfoProperty = "BrowserVendor" + deviceInfoBrowserName deviceInfoProperty = "BrowserName" + deviceInfoBrowserVersion deviceInfoProperty = "BrowserVersion" + deviceInfoScreenPixelsWidth deviceInfoProperty = "ScreenPixelsWidth" + deviceInfoScreenPixelsHeight deviceInfoProperty = "ScreenPixelsHeight" + deviceInfoPixelRatio deviceInfoProperty = "PixelRatio" + deviceInfoJavascript deviceInfoProperty = "Javascript" + deviceInfoGeoLocation deviceInfoProperty = "GeoLocation" + deviceInfoHardwareModel deviceInfoProperty = "HardwareModel" + deviceInfoHardwareFamily deviceInfoProperty = "HardwareFamily" + deviceInfoHardwareModelVariants deviceInfoProperty = "HardwareModelVariants" + deviceInfoScreenInchesHeight deviceInfoProperty = "ScreenInchesHeight" +) + +func (x deviceInfoExtractor) extract(results Results, ua string) (*deviceInfo, error) { + hardwareVendor := x.getValue(results, deviceInfoHardwareVendor) + hardwareName := x.getValue(results, deviceInfoHardwareName) + deviceType := x.getValue(results, deviceInfoDeviceType) + platformVendor := x.getValue(results, deviceInfoPlatformVendor) + platformName := x.getValue(results, deviceInfoPlatformName) + platformVersion := x.getValue(results, deviceInfoPlatformVersion) + browserVendor := x.getValue(results, deviceInfoBrowserVendor) + browserName := x.getValue(results, deviceInfoBrowserName) + browserVersion := x.getValue(results, deviceInfoBrowserVersion) + screenPixelsWidth, _ := strconv.ParseInt(x.getValue(results, deviceInfoScreenPixelsWidth), 10, 64) + screenPixelsHeight, _ := strconv.ParseInt(x.getValue(results, deviceInfoScreenPixelsHeight), 10, 64) + pixelRatio, _ := strconv.ParseFloat(x.getValue(results, deviceInfoPixelRatio), 10) + javascript, _ := strconv.ParseBool(x.getValue(results, deviceInfoJavascript)) + geoLocation, _ := strconv.ParseBool(x.getValue(results, deviceInfoGeoLocation)) + deviceId, err := results.DeviceId() + if err != nil { + return nil, errors.Wrap(err, "Failed to get device id.") + } + hardwareModel := x.getValue(results, deviceInfoHardwareModel) + hardwareFamily := x.getValue(results, deviceInfoHardwareFamily) + hardwareModelVariants := x.getValue(results, deviceInfoHardwareModelVariants) + screenInchedHeight, _ := strconv.ParseFloat(x.getValue(results, deviceInfoScreenInchesHeight), 10) + + p := &deviceInfo{ + HardwareVendor: hardwareVendor, + HardwareName: hardwareName, + DeviceType: deviceType, + PlatformVendor: platformVendor, + PlatformName: platformName, + PlatformVersion: platformVersion, + BrowserVendor: browserVendor, + BrowserName: browserName, + BrowserVersion: browserVersion, + ScreenPixelsWidth: screenPixelsWidth, + ScreenPixelsHeight: screenPixelsHeight, + PixelRatio: pixelRatio, + Javascript: javascript, + GeoLocation: geoLocation, + UserAgent: ua, + DeviceId: deviceId, + HardwareModel: hardwareModel, + HardwareFamily: hardwareFamily, + HardwareModelVariants: hardwareModelVariants, + ScreenInchesHeight: screenInchedHeight, + } + + return p, nil +} + +// function getValue return a value results for a property +func (x deviceInfoExtractor) getValue(results Results, propertyName deviceInfoProperty) string { + // Get the values in string + value, err := results.ValuesString( + string(propertyName), + ",", + ) + if err != nil { + glog.Errorf("Failed to get results values string.") + return "" + } + + hasValues, err := results.HasValues(string(propertyName)) + if err != nil { + glog.Errorf("Failed to check if a matched value exists for property %s.\n", propertyName) + return "" + } + + if !hasValues { + glog.Warningf("Property %s does not have a matched value.\n", propertyName) + return "Unknown" + } + + return value +} diff --git a/modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go b/modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go new file mode 100644 index 00000000000..197e3928602 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go @@ -0,0 +1,130 @@ +package devicedetection + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type ResultsHashMock struct { + mock.Mock +} + +func (m *ResultsHashMock) DeviceId() (string, error) { + return "", nil +} + +func (m *ResultsHashMock) ValuesString(prop1 string, prop2 string) (string, error) { + args := m.Called(prop1, prop2) + return args.String(0), args.Error(1) +} + +func (m *ResultsHashMock) HasValues(prop1 string) (bool, error) { + args := m.Called(prop1) + return args.Bool(0), args.Error(1) +} + +func TestDeviceInfoExtraction(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + mockValue(results, "HardwareName", "Macbook") + mockValues(results) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + + assert.Equal(t, deviceInfo.HardwareName, "Macbook") + assertDeviceInfo(t, deviceInfo) +} + +func TestDeviceInfoExtractionNoProperty(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + results.Mock.On("ValuesString", "HardwareName", ",").Return("", errors.New("Error")) + results.Mock.On("HasValues", "HardwareName").Return(true, nil) + mockValues(results) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + + assertDeviceInfo(t, deviceInfo) + assert.Equal(t, deviceInfo.HardwareName, "") +} + +func TestDeviceInfoExtractionNoValue(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + mockValues(results) + mockValue(results, "HardwareVendor", "Apple") + + results.Mock.On("ValuesString", "HardwareName", ",").Return("Macbook", nil) + results.Mock.On("HasValues", "HardwareName").Return(false, nil) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + assertDeviceInfo(t, deviceInfo) + assert.Equal(t, deviceInfo.HardwareName, "Unknown") +} + +func TestDeviceInfoExtractionHasValueError(t *testing.T) { + results := &ResultsHashMock{} + + extractor := newDeviceInfoExtractor() + mockValue(results, "HardwareVendor", "Apple") + + results.Mock.On("ValuesString", "HardwareName", ",").Return("Macbook", nil) + results.Mock.On("HasValues", "HardwareName").Return(true, errors.New("error")) + + mockValues(results) + + deviceInfo, _ := extractor.extract(results, "ua") + assert.NotNil(t, deviceInfo) + assertDeviceInfo(t, deviceInfo) + assert.Equal(t, deviceInfo.HardwareName, "") +} + +func mockValues(results *ResultsHashMock) { + mockValue(results, "HardwareVendor", "Apple") + mockValue(results, "DeviceType", "Desctop") + mockValue(results, "PlatformVendor", "Apple") + mockValue(results, "PlatformName", "MacOs") + mockValue(results, "PlatformVersion", "14") + mockValue(results, "BrowserVendor", "Google") + mockValue(results, "BrowserName", "Crome") + mockValue(results, "BrowserVersion", "12") + mockValue(results, "ScreenPixelsWidth", "1024") + mockValue(results, "ScreenPixelsHeight", "1080") + mockValue(results, "PixelRatio", "223") + mockValue(results, "Javascript", "true") + mockValue(results, "GeoLocation", "true") + mockValue(results, "HardwareModel", "Macbook") + mockValue(results, "HardwareFamily", "Macbook") + mockValue(results, "HardwareModelVariants", "Macbook") + mockValue(results, "ScreenInchesHeight", "12") +} + +func assertDeviceInfo(t *testing.T, deviceInfo *deviceInfo) { + assert.Equal(t, deviceInfo.HardwareVendor, "Apple") + assert.Equal(t, deviceInfo.DeviceType, "Desctop") + assert.Equal(t, deviceInfo.PlatformVendor, "Apple") + assert.Equal(t, deviceInfo.PlatformName, "MacOs") + assert.Equal(t, deviceInfo.PlatformVersion, "14") + assert.Equal(t, deviceInfo.BrowserVendor, "Google") + assert.Equal(t, deviceInfo.BrowserName, "Crome") + assert.Equal(t, deviceInfo.BrowserVersion, "12") + assert.Equal(t, deviceInfo.ScreenPixelsWidth, int64(1024)) + assert.Equal(t, deviceInfo.ScreenPixelsHeight, int64(1080)) + assert.Equal(t, deviceInfo.PixelRatio, float64(223)) + assert.Equal(t, deviceInfo.Javascript, true) + assert.Equal(t, deviceInfo.GeoLocation, true) +} + +func mockValue(results *ResultsHashMock, name string, value string) { + results.Mock.On("ValuesString", name, ",").Return(value, nil) + results.Mock.On("HasValues", name).Return(true, nil) +} diff --git a/modules/fiftyonedegrees/devicedetection/evidence_extractor.go b/modules/fiftyonedegrees/devicedetection/evidence_extractor.go new file mode 100644 index 00000000000..1d67e1cdeed --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/evidence_extractor.go @@ -0,0 +1,118 @@ +package devicedetection + +import ( + "net/http" + + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/pkg/errors" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/prebid/prebid-server/v2/hooks/hookstage" +) + +type defaultEvidenceExtractor struct { + valFromHeaders evidenceFromRequestHeadersExtractor + valFromSUA evidenceFromSUAPayloadExtractor +} + +func newEvidenceExtractor() *defaultEvidenceExtractor { + evidenceExtractor := &defaultEvidenceExtractor{ + valFromHeaders: newEvidenceFromRequestHeadersExtractor(), + valFromSUA: newEvidenceFromSUAPayloadExtractor(), + } + + return evidenceExtractor +} + +func (x *defaultEvidenceExtractor) fromHeaders(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence { + return x.valFromHeaders.extract(request, httpHeaderKeys) +} + +func (x *defaultEvidenceExtractor) fromSuaPayload(payload []byte) []stringEvidence { + return x.valFromSUA.extract(payload) +} + +// merge merges two slices of stringEvidence into one slice of stringEvidence +func merge(val1, val2 []stringEvidence) []stringEvidence { + evidenceMap := make(map[string]stringEvidence) + for _, e := range val1 { + evidenceMap[e.Key] = e + } + + for _, e := range val2 { + _, exists := evidenceMap[e.Key] + if !exists { + evidenceMap[e.Key] = e + } + } + + evidence := make([]stringEvidence, 0) + + for _, e := range evidenceMap { + evidence = append(evidence, e) + } + + return evidence +} + +func (x *defaultEvidenceExtractor) extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { + if ctx == nil { + return nil, "", errors.New("context is nil") + } + + suaStrings, err := x.getEvidenceStrings(ctx[evidenceFromSuaCtxKey]) + if err != nil { + return nil, "", errors.Wrap(err, "error extracting sua evidence") + } + headerString, err := x.getEvidenceStrings(ctx[evidenceFromHeadersCtxKey]) + if err != nil { + return nil, "", errors.Wrap(err, "error extracting header evidence") + } + + // Merge evidence from headers and SUA, sua has higher priority + evidenceStrings := merge(suaStrings, headerString) + + if len(evidenceStrings) > 0 { + userAgentE, exists := getEvidenceByKey(evidenceStrings, userAgentHeader) + if !exists { + return nil, "", errors.New("User-Agent not found") + } + + evidence := x.extractEvidenceFromStrings(evidenceStrings) + + return evidence, userAgentE.Value, nil + } + + return nil, "", nil +} + +func (x *defaultEvidenceExtractor) getEvidenceStrings(source interface{}) ([]stringEvidence, error) { + if source == nil { + return []stringEvidence{}, nil + } + + evidenceStrings, ok := source.([]stringEvidence) + if !ok { + return nil, errors.New("bad cast to []stringEvidence") + } + + return evidenceStrings, nil +} + +func (x *defaultEvidenceExtractor) extractEvidenceFromStrings(strEvidence []stringEvidence) []onpremise.Evidence { + evidenceResult := make([]onpremise.Evidence, len(strEvidence)) + for i, e := range strEvidence { + prefix := dd.HttpHeaderString + if e.Prefix == queryPrefix { + prefix = dd.HttpEvidenceQuery + } + + evidenceResult[i] = onpremise.Evidence{ + Prefix: prefix, + Key: e.Key, + Value: e.Value, + } + } + + return evidenceResult +} diff --git a/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go b/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go new file mode 100644 index 00000000000..9abdf799643 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go @@ -0,0 +1,256 @@ +package devicedetection + +import ( + "net/http" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/stretchr/testify/assert" +) + +func TestFromHeaders(t *testing.T) { + extractor := newEvidenceExtractor() + + req := http.Request{ + Header: make(map[string][]string), + } + req.Header.Add("header", "Value") + req.Header.Add("Sec-CH-UA-Full-Version-List", "Chrome;12") + evidenceKeys := []dd.EvidenceKey{ + { + Prefix: dd.EvidencePrefix(10), + Key: "header", + }, + { + Prefix: dd.EvidencePrefix(10), + Key: "Sec-CH-UA-Full-Version-List", + }, + } + + evidence := extractor.fromHeaders(&req, evidenceKeys) + + assert.NotNil(t, evidence) + assert.NotEmpty(t, evidence) + assert.Equal(t, evidence[0].Value, "Value") + assert.Equal(t, evidence[0].Key, "header") + assert.Equal(t, evidence[1].Value, "Chrome;12") + assert.Equal(t, evidence[1].Key, "Sec-CH-UA-Full-Version-List") +} + +func TestFromSuaPayload(t *testing.T) { + tests := []struct { + name string + payload []byte + evidenceSize int + evidenceKeyOrder int + expectedKey string + expectedValue string + }{ + { + name: "from_SUA_tag", + payload: []byte(`{ + "device": { + "sua": { + "browsers": [ + { + "brand": "Google Chrome", + "version": ["121", "0", "6167", "184"] + } + ], + "platform": { + "brand": "macOS", + "version": ["14", "0", "0"] + }, + "architecture": "arm" + } + } + }`), + evidenceSize: 4, + evidenceKeyOrder: 0, + expectedKey: "Sec-Ch-Ua-Arch", + expectedValue: "arm", + }, + { + name: "from_UA_headers", + payload: []byte(`{ + "device": { + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "sua": { + "architecture": "arm" + } + } + }`), + evidenceSize: 2, + evidenceKeyOrder: 1, + expectedKey: "Sec-Ch-Ua-Arch", + expectedValue: "arm", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + extractor := newEvidenceExtractor() + + evidence := extractor.fromSuaPayload(tt.payload) + + assert.NotNil(t, evidence) + assert.NotEmpty(t, evidence) + assert.Equal(t, len(evidence), tt.evidenceSize) + assert.Equal(t, evidence[tt.evidenceKeyOrder].Key, tt.expectedKey) + assert.Equal(t, evidence[tt.evidenceKeyOrder].Value, tt.expectedValue) + }) + } +} + +func TestExtract(t *testing.T) { + uaEvidence1 := stringEvidence{ + Prefix: "ua1", + Key: userAgentHeader, + Value: "uav1", + } + uaEvidence2 := stringEvidence{ + Prefix: "ua2", + Key: userAgentHeader, + Value: "uav2", + } + evidence1 := stringEvidence{ + Prefix: "e1", + Key: "k1", + Value: "v1", + } + emptyEvidence := stringEvidence{ + Prefix: "empty", + Key: "e1", + Value: "", + } + + tests := []struct { + name string + ctx hookstage.ModuleContext + wantEvidenceCount int + wantUserAgent string + wantError bool + }{ + { + name: "nil", + ctx: nil, + wantError: true, + }, + { + name: "empty", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{}, + evidenceFromHeadersCtxKey: []stringEvidence{}, + }, + wantEvidenceCount: 0, + wantUserAgent: "", + }, + { + name: "from_headers", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + }, + wantEvidenceCount: 1, + wantUserAgent: "uav1", + }, + { + name: "from_headers_no_user_agent", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{evidence1}, + }, + wantError: true, + }, + { + name: "from_sua", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{uaEvidence1}, + }, + wantEvidenceCount: 1, + wantUserAgent: "uav1", + }, + { + name: "from_sua_no_user_agent", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{evidence1}, + }, + wantError: true, + }, + { + name: "from_headers_error", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: "bad value", + }, + wantError: true, + }, + { + name: "from_sua_error", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{}, + evidenceFromSuaCtxKey: "bad value", + }, + wantError: true, + }, + { + name: "from_sua_and_headers", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + evidenceFromSuaCtxKey: []stringEvidence{evidence1}, + }, + wantEvidenceCount: 2, + wantUserAgent: "uav1", + }, + { + name: "from_sua_and_headers_sua_can_overwrite_if_ua_present", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + evidenceFromSuaCtxKey: []stringEvidence{uaEvidence2}, + }, + wantEvidenceCount: 1, + wantUserAgent: "uav2", + }, + { + name: "empty_string_values", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{emptyEvidence}, + }, + wantError: true, + }, + { + name: "empty_sua_values", + ctx: hookstage.ModuleContext{ + evidenceFromSuaCtxKey: []stringEvidence{emptyEvidence}, + }, + wantError: true, + }, + { + name: "mixed_valid_and_invalid", + ctx: hookstage.ModuleContext{ + evidenceFromHeadersCtxKey: []stringEvidence{uaEvidence1}, + evidenceFromSuaCtxKey: "bad value", + }, + wantError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + extractor := newEvidenceExtractor() + evidence, userAgent, err := extractor.extract(test.ctx) + + if test.wantError { + assert.Error(t, err) + assert.Nil(t, evidence) + assert.Equal(t, userAgent, "") + } else if test.wantEvidenceCount == 0 { + assert.NoError(t, err) + assert.Nil(t, evidence) + assert.Equal(t, userAgent, "") + } else { + assert.NoError(t, err) + assert.Equal(t, len(evidence), test.wantEvidenceCount) + assert.Equal(t, userAgent, test.wantUserAgent) + } + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go new file mode 100644 index 00000000000..7237698117d --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go @@ -0,0 +1,77 @@ +package devicedetection + +import ( + "github.com/prebid/openrtb/v20/adcom1" +) + +type deviceTypeMap = map[deviceType]adcom1.DeviceType + +var mobileOrTabletDeviceTypes = []deviceType{ + deviceTypeMobile, + deviceTypeSmartPhone, +} + +var personalComputerDeviceTypes = []deviceType{ + deviceTypeDesktop, + deviceTypeEReader, + deviceTypeVehicleDisplay, +} + +var tvDeviceTypes = []deviceType{ + deviceTypeTv, +} + +var phoneDeviceTypes = []deviceType{ + deviceTypePhone, +} + +var tabletDeviceTypes = []deviceType{ + deviceTypeTablet, +} + +var connectedDeviceTypes = []deviceType{ + deviceTypeIoT, + deviceTypeRouter, + deviceTypeSmallScreen, + deviceTypeSmartSpeaker, + deviceTypeSmartWatch, +} + +var setTopBoxDeviceTypes = []deviceType{ + deviceTypeMediaHub, + deviceTypeConsole, +} + +var oohDeviceTypes = []deviceType{ + deviceTypeKiosk, +} + +func applyCollection(items []deviceType, value adcom1.DeviceType, mappedCollection deviceTypeMap) { + for _, item := range items { + mappedCollection[item] = value + } +} + +var deviceTypeMapCollection = deviceTypeMap{} + +func init() { + applyCollection(mobileOrTabletDeviceTypes, adcom1.DeviceMobile, deviceTypeMapCollection) + applyCollection(personalComputerDeviceTypes, adcom1.DevicePC, deviceTypeMapCollection) + applyCollection(tvDeviceTypes, adcom1.DeviceTV, deviceTypeMapCollection) + applyCollection(phoneDeviceTypes, adcom1.DevicePhone, deviceTypeMapCollection) + applyCollection(tabletDeviceTypes, adcom1.DeviceTablet, deviceTypeMapCollection) + applyCollection(connectedDeviceTypes, adcom1.DeviceConnected, deviceTypeMapCollection) + applyCollection(setTopBoxDeviceTypes, adcom1.DeviceSetTopBox, deviceTypeMapCollection) + applyCollection(oohDeviceTypes, adcom1.DeviceOOH, deviceTypeMapCollection) +} + +// fiftyOneDtToRTB converts a 51Degrees device type to an OpenRTB device type. +// If the device type is not recognized, it defaults to PC. +func fiftyOneDtToRTB(val string) adcom1.DeviceType { + id, ok := deviceTypeMapCollection[deviceType(val)] + if ok { + return id + } + + return adcom1.DevicePC +} diff --git a/modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go new file mode 100644 index 00000000000..5fd0203bac8 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go @@ -0,0 +1,90 @@ +package devicedetection + +import ( + "testing" + + "github.com/prebid/openrtb/v20/adcom1" + "github.com/stretchr/testify/assert" +) + +func TestFiftyOneDtToRTB(t *testing.T) { + cases := []struct { + fiftyOneDt string + rtbDt adcom1.DeviceType + }{ + { + fiftyOneDt: "Phone", + rtbDt: adcom1.DevicePhone, + }, + { + fiftyOneDt: "Console", + rtbDt: adcom1.DeviceSetTopBox, + }, + { + fiftyOneDt: "Desktop", + rtbDt: adcom1.DevicePC, + }, + { + fiftyOneDt: "EReader", + rtbDt: adcom1.DevicePC, + }, + { + fiftyOneDt: "IoT", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "Kiosk", + rtbDt: adcom1.DeviceOOH, + }, + { + fiftyOneDt: "MediaHub", + rtbDt: adcom1.DeviceSetTopBox, + }, + { + fiftyOneDt: "Mobile", + rtbDt: adcom1.DeviceMobile, + }, + { + fiftyOneDt: "Router", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "SmallScreen", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "SmartPhone", + rtbDt: adcom1.DeviceMobile, + }, + { + fiftyOneDt: "SmartSpeaker", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "SmartWatch", + rtbDt: adcom1.DeviceConnected, + }, + { + fiftyOneDt: "Tablet", + rtbDt: adcom1.DeviceTablet, + }, + { + fiftyOneDt: "Tv", + rtbDt: adcom1.DeviceTV, + }, + { + fiftyOneDt: "Vehicle Display", + rtbDt: adcom1.DevicePC, + }, + { + fiftyOneDt: "Unknown", + rtbDt: adcom1.DevicePC, + }, + } + + for _, c := range cases { + t.Run(c.fiftyOneDt, func(t *testing.T) { + assert.Equal(t, c.rtbDt, fiftyOneDtToRTB(c.fiftyOneDt)) + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go b/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go new file mode 100644 index 00000000000..911f20e1840 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go @@ -0,0 +1,27 @@ +package devicedetection + +import ( + "github.com/prebid/prebid-server/v2/hooks/hookexecution" + "github.com/prebid/prebid-server/v2/hooks/hookstage" +) + +// handleAuctionEntryPointRequestHook is a hookstage.HookFunc that is used to handle the auction entrypoint request hook. +func handleAuctionEntryPointRequestHook(cfg config, payload hookstage.EntrypointPayload, deviceDetector deviceDetector, evidenceExtractor evidenceExtractor, accountValidator accountValidator) (result hookstage.HookResult[hookstage.EntrypointPayload], err error) { + // if account/domain is not allowed, return failure + if !accountValidator.isAllowed(cfg, payload.Body) { + return hookstage.HookResult[hookstage.EntrypointPayload]{}, hookexecution.NewFailure("account not allowed") + } + // fetch evidence from headers and sua + evidenceFromHeaders := evidenceExtractor.fromHeaders(payload.Request, deviceDetector.getSupportedHeaders()) + evidenceFromSua := evidenceExtractor.fromSuaPayload(payload.Body) + + // create a Module context and set the evidence from headers, evidence from sua and dd enabled flag + moduleContext := make(hookstage.ModuleContext) + moduleContext[evidenceFromHeadersCtxKey] = evidenceFromHeaders + moduleContext[evidenceFromSuaCtxKey] = evidenceFromSua + moduleContext[ddEnabledCtxKey] = true + + return hookstage.HookResult[hookstage.EntrypointPayload]{ + ModuleContext: moduleContext, + }, nil +} diff --git a/modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go b/modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go new file mode 100644 index 00000000000..1146c3cc639 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go @@ -0,0 +1,173 @@ +package devicedetection + +import ( + "fmt" + "math" + + "github.com/prebid/prebid-server/v2/hooks/hookexecution" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +func handleAuctionRequestHook(ctx hookstage.ModuleInvocationContext, deviceDetector deviceDetector, evidenceExtractor evidenceExtractor) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) { + var result hookstage.HookResult[hookstage.RawAuctionRequestPayload] + + // If the entrypoint hook was not configured, return the result without any changes + if ctx.ModuleContext == nil { + return result, hookexecution.NewFailure("entrypoint hook was not configured") + } + + result.ChangeSet.AddMutation( + func(rawPayload hookstage.RawAuctionRequestPayload) (hookstage.RawAuctionRequestPayload, error) { + evidence, ua, err := evidenceExtractor.extract(ctx.ModuleContext) + if err != nil { + return rawPayload, hookexecution.NewFailure("error extracting evidence %s", err) + } + if evidence == nil { + return rawPayload, hookexecution.NewFailure("error extracting evidence") + } + + deviceInfo, err := deviceDetector.getDeviceInfo(evidence, ua) + if err != nil { + return rawPayload, hookexecution.NewFailure("error getting device info %s", err) + } + + result, err := hydrateFields(deviceInfo, rawPayload) + if err != nil { + return rawPayload, hookexecution.NewFailure(fmt.Sprintf("error hydrating fields %s", err)) + } + + return result, nil + }, hookstage.MutationUpdate, + ) + + return result, nil +} + +// hydrateFields hydrates the fields in the raw auction request payload with the device information +func hydrateFields(fiftyOneDd *deviceInfo, payload hookstage.RawAuctionRequestPayload) (hookstage.RawAuctionRequestPayload, error) { + devicePayload := gjson.GetBytes(payload, "device") + dPV := devicePayload.Value() + if dPV == nil { + return payload, nil + } + + deviceObject := dPV.(map[string]any) + deviceObject = setMissingFields(deviceObject, fiftyOneDd) + deviceObject = signDeviceData(deviceObject, fiftyOneDd) + + return mergeDeviceIntoPayload(payload, deviceObject) +} + +// setMissingFields sets fields such as ["devicetype", "ua", "make", "os", "osv", "h", "w", "pxratio", "js", "geoFetch", "model", "ppi"] +// if they are not already present in the device object +func setMissingFields(deviceObj map[string]any, fiftyOneDd *deviceInfo) map[string]any { + optionalFields := map[string]func() any{ + "devicetype": func() any { + return fiftyOneDtToRTB(fiftyOneDd.DeviceType) + }, + "ua": func() any { + if fiftyOneDd.UserAgent != ddUnknown { + return fiftyOneDd.UserAgent + } + return nil + }, + "make": func() any { + if fiftyOneDd.HardwareVendor != ddUnknown { + return fiftyOneDd.HardwareVendor + } + return nil + }, + "os": func() any { + if fiftyOneDd.PlatformName != ddUnknown { + return fiftyOneDd.PlatformName + } + return nil + }, + "osv": func() any { + if fiftyOneDd.PlatformVersion != ddUnknown { + return fiftyOneDd.PlatformVersion + } + return nil + }, + "h": func() any { + return fiftyOneDd.ScreenPixelsHeight + }, + "w": func() any { + return fiftyOneDd.ScreenPixelsWidth + }, + "pxratio": func() any { + return fiftyOneDd.PixelRatio + }, + "js": func() any { + val := 0 + if fiftyOneDd.Javascript { + val = 1 + } + return val + }, + "geoFetch": func() any { + val := 0 + if fiftyOneDd.GeoLocation { + val = 1 + } + return val + }, + "model": func() any { + newVal := fiftyOneDd.HardwareModel + if newVal == ddUnknown { + newVal = fiftyOneDd.HardwareName + } + if newVal != ddUnknown { + return newVal + } + return nil + }, + "ppi": func() any { + if fiftyOneDd.ScreenPixelsHeight > 0 && fiftyOneDd.ScreenInchesHeight > 0 { + ppi := float64(fiftyOneDd.ScreenPixelsHeight) / fiftyOneDd.ScreenInchesHeight + return int(math.Round(ppi)) + } + return nil + }, + } + + for field, valFunc := range optionalFields { + _, ok := deviceObj[field] + if !ok { + val := valFunc() + if val != nil { + deviceObj[field] = val + } + } + } + + return deviceObj +} + +// signDeviceData signs the device data with the device information in the ext map of the device object +func signDeviceData(deviceObj map[string]any, fiftyOneDd *deviceInfo) map[string]any { + extObj, ok := deviceObj["ext"] + var ext map[string]any + if ok { + ext = extObj.(map[string]any) + } else { + ext = make(map[string]any) + } + + ext["fiftyonedegrees_deviceId"] = fiftyOneDd.DeviceId + deviceObj["ext"] = ext + + return deviceObj +} + +// mergeDeviceIntoPayload merges the modified device object back into the RawAuctionRequestPayload +func mergeDeviceIntoPayload(payload hookstage.RawAuctionRequestPayload, deviceObject map[string]any) (hookstage.RawAuctionRequestPayload, error) { + newPayload, err := sjson.SetBytes(payload, "device", deviceObject) + if err != nil { + return payload, err + } + + return newPayload, nil +} diff --git a/modules/fiftyonedegrees/devicedetection/models.go b/modules/fiftyonedegrees/devicedetection/models.go new file mode 100644 index 00000000000..c58daa211fd --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/models.go @@ -0,0 +1,66 @@ +package devicedetection + +// Prefixes in literal format +const queryPrefix = "query." +const headerPrefix = "header." +const ddUnknown = "Unknown" + +// Evidence where all fields are in string format +type stringEvidence struct { + Prefix string + Key string + Value string +} + +func getEvidenceByKey(e []stringEvidence, key string) (stringEvidence, bool) { + for _, evidence := range e { + if evidence.Key == key { + return evidence, true + } + } + return stringEvidence{}, false +} + +type deviceType string + +const ( + deviceTypePhone = "Phone" + deviceTypeConsole = "Console" + deviceTypeDesktop = "Desktop" + deviceTypeEReader = "EReader" + deviceTypeIoT = "IoT" + deviceTypeKiosk = "Kiosk" + deviceTypeMediaHub = "MediaHub" + deviceTypeMobile = "Mobile" + deviceTypeRouter = "Router" + deviceTypeSmallScreen = "SmallScreen" + deviceTypeSmartPhone = "SmartPhone" + deviceTypeSmartSpeaker = "SmartSpeaker" + deviceTypeSmartWatch = "SmartWatch" + deviceTypeTablet = "Tablet" + deviceTypeTv = "Tv" + deviceTypeVehicleDisplay = "Vehicle Display" +) + +type deviceInfo struct { + HardwareVendor string + HardwareName string + DeviceType string + PlatformVendor string + PlatformName string + PlatformVersion string + BrowserVendor string + BrowserName string + BrowserVersion string + ScreenPixelsWidth int64 + ScreenPixelsHeight int64 + PixelRatio float64 + Javascript bool + GeoLocation bool + HardwareFamily string + HardwareModel string + HardwareModelVariants string + UserAgent string + DeviceId string + ScreenInchesHeight float64 +} diff --git a/modules/fiftyonedegrees/devicedetection/models_test.go b/modules/fiftyonedegrees/devicedetection/models_test.go new file mode 100644 index 00000000000..898f25f4144 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/models_test.go @@ -0,0 +1,63 @@ +package devicedetection + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetEvidenceByKey(t *testing.T) { + populatedEvidence := []stringEvidence{ + {Key: "key1", Value: "value1"}, + {Key: "key2", Value: "value2"}, + {Key: "key3", Value: "value3"}, + } + + tests := []struct { + name string + evidence []stringEvidence + key string + expectEvidence stringEvidence + expectFound bool + }{ + { + name: "nil_evidence", + evidence: nil, + key: "key2", + expectEvidence: stringEvidence{}, + expectFound: false, + }, + { + name: "empty_evidence", + evidence: []stringEvidence{}, + key: "key2", + expectEvidence: stringEvidence{}, + expectFound: false, + }, + { + name: "key_found", + evidence: populatedEvidence, + key: "key2", + expectEvidence: stringEvidence{ + Key: "key2", + Value: "value2", + }, + expectFound: true, + }, + { + name: "key_not_found", + evidence: populatedEvidence, + key: "key4", + expectEvidence: stringEvidence{}, + expectFound: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, exists := getEvidenceByKey(test.evidence, test.key) + assert.Equal(t, test.expectFound, exists) + assert.Equal(t, test.expectEvidence, result) + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/module.go b/modules/fiftyonedegrees/devicedetection/module.go new file mode 100644 index 00000000000..df72e6338a5 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/module.go @@ -0,0 +1,107 @@ +package devicedetection + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/pkg/errors" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/prebid/prebid-server/v2/modules/moduledeps" +) + +func configHashFromConfig(cfg *config) *dd.ConfigHash { + configHash := dd.NewConfigHash(cfg.getPerformanceProfile()) + if cfg.Performance.Concurrency != nil { + configHash.SetConcurrency(uint16(*cfg.Performance.Concurrency)) + } + + if cfg.Performance.Difference != nil { + configHash.SetDifference(int32(*cfg.Performance.Difference)) + } + + if cfg.Performance.AllowUnmatched != nil { + configHash.SetAllowUnmatched(*cfg.Performance.AllowUnmatched) + } + + if cfg.Performance.Drift != nil { + configHash.SetDrift(int32(*cfg.Performance.Drift)) + } + return configHash +} + +func Builder(rawConfig json.RawMessage, _ moduledeps.ModuleDeps) (interface{}, error) { + cfg, err := parseConfig(rawConfig) + if err != nil { + return Module{}, errors.Wrap(err, "failed to parse config") + } + + err = validateConfig(cfg) + if err != nil { + return nil, errors.Wrap(err, "invalid config") + } + + configHash := configHashFromConfig(&cfg) + + deviceDetectorImpl, err := newDeviceDetector( + configHash, + &cfg, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create device detector") + } + + return Module{ + cfg, + deviceDetectorImpl, + newEvidenceExtractor(), + newAccountValidator(), + }, + nil +} + +type Module struct { + config config + deviceDetector deviceDetector + evidenceExtractor evidenceExtractor + accountValidator accountValidator +} + +type deviceDetector interface { + getSupportedHeaders() []dd.EvidenceKey + getDeviceInfo(evidence []onpremise.Evidence, ua string) (*deviceInfo, error) +} + +type accountValidator interface { + isAllowed(cfg config, req []byte) bool +} + +type evidenceExtractor interface { + fromHeaders(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence + fromSuaPayload(payload []byte) []stringEvidence + extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) +} + +func (m Module) HandleEntrypointHook( + _ context.Context, + _ hookstage.ModuleInvocationContext, + payload hookstage.EntrypointPayload, +) (hookstage.HookResult[hookstage.EntrypointPayload], error) { + return handleAuctionEntryPointRequestHook( + m.config, + payload, + m.deviceDetector, + m.evidenceExtractor, + m.accountValidator, + ) +} + +func (m Module) HandleRawAuctionHook( + _ context.Context, + mCtx hookstage.ModuleInvocationContext, + _ hookstage.RawAuctionRequestPayload, +) (hookstage.HookResult[hookstage.RawAuctionRequestPayload], error) { + return handleAuctionRequestHook(mCtx, m.deviceDetector, m.evidenceExtractor) +} diff --git a/modules/fiftyonedegrees/devicedetection/module_test.go b/modules/fiftyonedegrees/devicedetection/module_test.go new file mode 100644 index 00000000000..7b8095ac431 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/module_test.go @@ -0,0 +1,703 @@ +package devicedetection + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "os" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/51Degrees/device-detection-go/v4/onpremise" + "github.com/prebid/prebid-server/v2/hooks/hookstage" + "github.com/prebid/prebid-server/v2/modules/moduledeps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type mockAccValidator struct { + mock.Mock +} + +func (m *mockAccValidator) isAllowed(cfg config, req []byte) bool { + args := m.Called(cfg, req) + return args.Bool(0) +} + +type mockEvidenceExtractor struct { + mock.Mock +} + +func (m *mockEvidenceExtractor) fromHeaders(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence { + args := m.Called(request, httpHeaderKeys) + + return args.Get(0).([]stringEvidence) +} + +func (m *mockEvidenceExtractor) fromSuaPayload(payload []byte) []stringEvidence { + args := m.Called(payload) + + return args.Get(0).([]stringEvidence) +} + +func (m *mockEvidenceExtractor) extract(ctx hookstage.ModuleContext) ([]onpremise.Evidence, string, error) { + args := m.Called(ctx) + + res := args.Get(0) + if res == nil { + return nil, args.String(1), args.Error(2) + } + + return res.([]onpremise.Evidence), args.String(1), args.Error(2) +} + +type mockDeviceDetector struct { + mock.Mock +} + +func (m *mockDeviceDetector) getSupportedHeaders() []dd.EvidenceKey { + args := m.Called() + return args.Get(0).([]dd.EvidenceKey) +} + +func (m *mockDeviceDetector) getDeviceInfo(evidence []onpremise.Evidence, ua string) (*deviceInfo, error) { + + args := m.Called(evidence, ua) + + res := args.Get(0) + + if res == nil { + return nil, args.Error(1) + } + + return res.(*deviceInfo), args.Error(1) +} + +func TestHandleEntrypointHookAccountNotAllowed(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(false) + + module := Module{ + accountValidator: &mockValidator, + } + + _, err := module.HandleEntrypointHook(nil, hookstage.ModuleInvocationContext{}, hookstage.EntrypointPayload{}) + assert.Error(t, err) + assert.Equal(t, "hook execution failed: account not allowed", err.Error()) +} + +func TestHandleEntrypointHookAccountAllowed(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var mockEvidenceExtractor mockEvidenceExtractor + mockEvidenceExtractor.On("fromHeaders", mock.Anything, mock.Anything).Return( + []stringEvidence{{ + Prefix: "123", + Key: "key", + Value: "val", + }}, + ) + + mockEvidenceExtractor.On("fromSuaPayload", mock.Anything, mock.Anything).Return( + []stringEvidence{{ + Prefix: "123", + Key: "User-Agent", + Value: "ua", + }}, + ) + + var mockDeviceDetector mockDeviceDetector + + mockDeviceDetector.On("getSupportedHeaders").Return( + []dd.EvidenceKey{{ + Prefix: dd.HttpEvidenceQuery, + Key: "key", + }}, + ) + + module := Module{ + deviceDetector: &mockDeviceDetector, + evidenceExtractor: &mockEvidenceExtractor, + accountValidator: &mockValidator, + } + + result, err := module.HandleEntrypointHook(nil, hookstage.ModuleInvocationContext{}, hookstage.EntrypointPayload{}) + assert.NoError(t, err) + + assert.Equal( + t, result.ModuleContext[evidenceFromHeadersCtxKey], []stringEvidence{{ + Prefix: "123", + Key: "key", + Value: "val", + }}, + ) + + assert.Equal( + t, result.ModuleContext[evidenceFromSuaCtxKey], []stringEvidence{{ + Prefix: "123", + Key: "User-Agent", + Value: "ua", + }}, + ) +} + +func TestHandleRawAuctionHookNoCtx(t *testing.T) { + module := Module{} + + _, err := module.HandleRawAuctionHook( + nil, + hookstage.ModuleInvocationContext{}, + hookstage.RawAuctionRequestPayload{}, + ) + assert.Errorf(t, err, "entrypoint hook was not configured") +} + +func TestHandleRawAuctionHookExtractError(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var evidenceExtractorM mockEvidenceExtractor + evidenceExtractorM.On("extract", mock.Anything).Return( + nil, + "ua", + nil, + ) + + var mockDeviceDetector mockDeviceDetector + + module := Module{ + deviceDetector: &mockDeviceDetector, + evidenceExtractor: &evidenceExtractorM, + accountValidator: &mockValidator, + } + + mctx := make(hookstage.ModuleContext) + + mctx[ddEnabledCtxKey] = true + + result, err := module.HandleRawAuctionHook( + context.TODO(), hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + hookstage.RawAuctionRequestPayload{}, + ) + + assert.NoError(t, err) + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation := result.ChangeSet.Mutations()[0] + + body := []byte(`{}`) + + _, err = mutation.Apply(body) + assert.Errorf(t, err, "error extracting evidence") + + var mockEvidenceErrExtractor mockEvidenceExtractor + mockEvidenceErrExtractor.On("extract", mock.Anything).Return( + nil, + "", + errors.New("error"), + ) + + module.evidenceExtractor = &mockEvidenceErrExtractor + + result, err = module.HandleRawAuctionHook( + context.TODO(), hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + hookstage.RawAuctionRequestPayload{}, + ) + + assert.NoError(t, err) + + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation = result.ChangeSet.Mutations()[0] + + _, err = mutation.Apply(body) + assert.Errorf(t, err, "error extracting evidence error") + +} + +func TestHandleRawAuctionHookEnrichment(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var mockEvidenceExtractor mockEvidenceExtractor + mockEvidenceExtractor.On("extract", mock.Anything).Return( + []onpremise.Evidence{ + { + Key: "key", + Value: "val", + }, + }, + "ua", + nil, + ) + + var deviceDetectorM mockDeviceDetector + + deviceDetectorM.On("getDeviceInfo", mock.Anything, mock.Anything).Return( + &deviceInfo{ + HardwareVendor: "Apple", + HardwareName: "Macbook", + DeviceType: "device", + PlatformVendor: "Apple", + PlatformName: "MacOs", + PlatformVersion: "14", + BrowserVendor: "Google", + BrowserName: "Crome", + BrowserVersion: "12", + ScreenPixelsWidth: 1024, + ScreenPixelsHeight: 1080, + PixelRatio: 223, + Javascript: true, + GeoLocation: true, + HardwareFamily: "Macbook", + HardwareModel: "Macbook", + HardwareModelVariants: "Macbook", + UserAgent: "ua", + DeviceId: "", + }, + nil, + ) + + module := Module{ + deviceDetector: &deviceDetectorM, + evidenceExtractor: &mockEvidenceExtractor, + accountValidator: &mockValidator, + } + + mctx := make(hookstage.ModuleContext) + mctx[ddEnabledCtxKey] = true + + result, err := module.HandleRawAuctionHook( + nil, hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + []byte{}, + ) + assert.NoError(t, err) + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation := result.ChangeSet.Mutations()[0] + + body := []byte(`{ + "device": { + "connectiontype": 2, + "ext": { + "atts": 0, + "ifv": "1B8EFA09-FF8F-4123-B07F-7283B50B3870" + }, + "sua": { + "source": 2, + "browsers": [ + { + "brand": "Not A(Brand", + "version": [ + "99", + "0", + "0", + "0" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "121", + "0", + "6167", + "184" + ] + }, + { + "brand": "Chromium", + "version": [ + "121", + "0", + "6167", + "184" + ] + } + ], + "platform": { + "brand": "macOS", + "version": [ + "14", + "0", + "0" + ] + }, + "mobile": 0, + "architecture": "arm", + "model": "" + } + } + }`) + + mutationResult, err := mutation.Apply(body) + + require.JSONEq(t, string(mutationResult), `{ + "device": { + "connectiontype": 2, + "ext": { + "atts": 0, + "ifv": "1B8EFA09-FF8F-4123-B07F-7283B50B3870", + "fiftyonedegrees_deviceId":"" + }, + "sua": { + "source": 2, + "browsers": [ + { + "brand": "Not A(Brand", + "version": [ + "99", + "0", + "0", + "0" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "121", + "0", + "6167", + "184" + ] + }, + { + "brand": "Chromium", + "version": [ + "121", + "0", + "6167", + "184" + ] + } + ], + "platform": { + "brand": "macOS", + "version": [ + "14", + "0", + "0" + ] + }, + "mobile": 0, + "architecture": "arm", + "model": "" + } + ,"devicetype":2,"ua":"ua","make":"Apple","model":"Macbook","os":"MacOs","osv":"14","h":1080,"w":1024,"pxratio":223,"js":1,"geoFetch":1} + }`) + + var deviceDetectorErrM mockDeviceDetector + + deviceDetectorErrM.On("getDeviceInfo", mock.Anything, mock.Anything).Return( + nil, + errors.New("error"), + ) + + module.deviceDetector = &deviceDetectorErrM + + result, err = module.HandleRawAuctionHook( + nil, hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + []byte{}, + ) + + assert.NoError(t, err) + + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation = result.ChangeSet.Mutations()[0] + + _, err = mutation.Apply(body) + assert.Errorf(t, err, "error getting device info") +} + +func TestHandleRawAuctionHookEnrichmentWithErrors(t *testing.T) { + var mockValidator mockAccValidator + + mockValidator.On("isAllowed", mock.Anything, mock.Anything).Return(true) + + var mockEvidenceExtractor mockEvidenceExtractor + mockEvidenceExtractor.On("extract", mock.Anything).Return( + []onpremise.Evidence{ + { + Key: "key", + Value: "val", + }, + }, + "ua", + nil, + ) + + var mockDeviceDetector mockDeviceDetector + + mockDeviceDetector.On("getDeviceInfo", mock.Anything, mock.Anything).Return( + &deviceInfo{ + HardwareVendor: "Apple", + HardwareName: "Macbook", + DeviceType: "device", + PlatformVendor: "Apple", + PlatformName: "MacOs", + PlatformVersion: "14", + BrowserVendor: "Google", + BrowserName: "Crome", + BrowserVersion: "12", + ScreenPixelsWidth: 1024, + ScreenPixelsHeight: 1080, + PixelRatio: 223, + Javascript: true, + GeoLocation: true, + HardwareFamily: "Macbook", + HardwareModel: "Macbook", + HardwareModelVariants: "Macbook", + UserAgent: "ua", + DeviceId: "", + ScreenInchesHeight: 7, + }, + nil, + ) + + module := Module{ + deviceDetector: &mockDeviceDetector, + evidenceExtractor: &mockEvidenceExtractor, + accountValidator: &mockValidator, + } + + mctx := make(hookstage.ModuleContext) + mctx[ddEnabledCtxKey] = true + + result, err := module.HandleRawAuctionHook( + nil, hookstage.ModuleInvocationContext{ + ModuleContext: mctx, + }, + []byte{}, + ) + assert.NoError(t, err) + assert.Equal(t, len(result.ChangeSet.Mutations()), 1) + assert.Equal(t, result.ChangeSet.Mutations()[0].Type(), hookstage.MutationUpdate) + + mutation := result.ChangeSet.Mutations()[0] + + mutationResult, err := mutation.Apply(hookstage.RawAuctionRequestPayload(`{"device":{}}`)) + assert.NoError(t, err) + require.JSONEq(t, string(mutationResult), `{"device":{"devicetype":2,"ua":"ua","make":"Apple","model":"Macbook","os":"MacOs","osv":"14","h":1080,"w":1024,"pxratio":223,"js":1,"geoFetch":1,"ppi":154,"ext":{"fiftyonedegrees_deviceId":""}}}`) +} + +func TestConfigHashFromConfig(t *testing.T) { + cfg := config{ + Performance: performance{ + Profile: "", + Concurrency: nil, + Difference: nil, + AllowUnmatched: nil, + Drift: nil, + }, + } + + result := configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.Default) + assert.Equal(t, result.Concurrency(), uint16(0xa)) + assert.Equal(t, result.Difference(), int32(0)) + assert.Equal(t, result.AllowUnmatched(), false) + assert.Equal(t, result.Drift(), int32(0)) + + concurrency := 1 + difference := 1 + allowUnmatched := true + drift := 1 + + cfg = config{ + Performance: performance{ + Profile: "Balanced", + Concurrency: &concurrency, + Difference: &difference, + AllowUnmatched: &allowUnmatched, + Drift: &drift, + }, + } + + result = configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.Balanced) + assert.Equal(t, result.Concurrency(), uint16(1)) + assert.Equal(t, result.Difference(), int32(1)) + assert.Equal(t, result.AllowUnmatched(), true) + assert.Equal(t, result.Drift(), int32(1)) + + cfg = config{ + Performance: performance{ + Profile: "InMemory", + }, + } + result = configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.InMemory) + + cfg = config{ + Performance: performance{ + Profile: "HighPerformance", + }, + } + result = configHashFromConfig(&cfg) + assert.Equal(t, result.PerformanceProfile(), dd.HighPerformance) +} + +func TestSignDeviceData(t *testing.T) { + devicePld := map[string]any{ + "ext": map[string]any{ + "my-key": "my-value", + }, + } + + deviceInfo := deviceInfo{ + DeviceId: "test-device-id", + } + + result := signDeviceData(devicePld, &deviceInfo) + r, err := json.Marshal(result) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + require.JSONEq( + t, + `{"ext":{"fiftyonedegrees_deviceId":"test-device-id","my-key":"my-value"}}`, + string(r), + ) +} + +func TestBuilderWithInvalidJson(t *testing.T) { + _, err := Builder([]byte(`{`), moduledeps.ModuleDeps{}) + assert.Error(t, err) + assert.Errorf(t, err, "failed to parse config") +} + +func TestBuilderWithInvalidConfig(t *testing.T) { + _, err := Builder([]byte(`{"data_file":{}}`), moduledeps.ModuleDeps{}) + assert.Error(t, err) + assert.Errorf(t, err, "invalid config") +} + +func TestBuilderHandleDeviceDetectorError(t *testing.T) { + var mockConfig config + mockConfig.Performance.Profile = "default" + testFile, _ := os.Create("test-builder-config.hash") + defer testFile.Close() + defer os.Remove("test-builder-config.hash") + + _, err := Builder( + []byte(`{ + "enabled": true, + "data_file": { + "path": "test-builder-config.hash", + "update": { + "auto": true, + "url": "https://my.datafile.com/datafile.gz", + "polling_interval": 3600, + "licence_key": "your_licence_key", + "product": "V4Enterprise" + } + }, + "account_filter": {"allow_list": ["123"]}, + "performance": { + "profile": "123", + "concurrency": 1, + "difference": 1, + "allow_unmatched": true, + "drift": 1 + } + }`), moduledeps.ModuleDeps{}, + ) + assert.Error(t, err) + assert.Errorf(t, err, "failed to create device detector") +} + +func TestHydrateFields(t *testing.T) { + deviceInfo := &deviceInfo{ + HardwareVendor: "Apple", + HardwareName: "Macbook", + DeviceType: "device", + PlatformVendor: "Apple", + PlatformName: "MacOs", + PlatformVersion: "14", + BrowserVendor: "Google", + BrowserName: "Crome", + BrowserVersion: "12", + ScreenPixelsWidth: 1024, + ScreenPixelsHeight: 1080, + PixelRatio: 223, + Javascript: true, + GeoLocation: true, + HardwareFamily: "Macbook", + HardwareModel: "Macbook", + HardwareModelVariants: "Macbook", + UserAgent: "ua", + DeviceId: "dev-ide", + } + + rawPld := `{ + "imp": [{ + "id": "", + "banner": { + "topframe": 1, + "format": [ + { + "w": 728, + "h": 90 + } + ], + "pos": 1 + }, + "bidfloor": 0.01, + "bidfloorcur": "USD" + }], + "device": { + "model": "Macintosh", + "w": 843, + "h": 901, + "dnt": 0, + "ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A037U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36", + "language": "en", + "sua": {"browsers":[{"brand":"Not/A)Brand","version":["99","0","0","0"]},{"brand":"Samsung Internet","version":["23","0","1","1"]},{"brand":"Chromium","version":["115","0","5790","168"]}],"platform":{"brand":"Android","version":["13","0","0"]},"mobile":1,"model":"SM-A037U","source":2}, + "ext": {"h":"901","w":843} + }, + "cur": [ + "USD" + ], + "tmax": 1700 + }` + + payload, err := hydrateFields(deviceInfo, []byte(rawPld)) + assert.NoError(t, err) + + var deviceHolder struct { + Device json.RawMessage `json:"device"` + } + + err = json.Unmarshal(payload, &deviceHolder) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + require.JSONEq( + t, + `{"devicetype":2,"dnt":0,"ext":{"fiftyonedegrees_deviceId":"dev-ide","h":"901","w":843},"geoFetch":1,"h":901,"js":1,"language":"en","make":"Apple","model":"Macintosh","os":"MacOs","osv":"14","pxratio":223,"sua":{"browsers":[{"brand":"Not/A)Brand","version":["99","0","0","0"]},{"brand":"Samsung Internet","version":["23","0","1","1"]},{"brand":"Chromium","version":["115","0","5790","168"]}],"mobile":1,"model":"SM-A037U","platform":{"brand":"Android","version":["13","0","0"]},"source":2},"ua":"Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A037U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36","w":843}`, + string(deviceHolder.Device), + ) +} diff --git a/modules/fiftyonedegrees/devicedetection/request_headers_extractor.go b/modules/fiftyonedegrees/devicedetection/request_headers_extractor.go new file mode 100644 index 00000000000..8440886b353 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/request_headers_extractor.go @@ -0,0 +1,47 @@ +package devicedetection + +import ( + "net/http" + "strings" + + "github.com/51Degrees/device-detection-go/v4/dd" +) + +// evidenceFromRequestHeadersExtractor is a struct that extracts evidence from http request headers +type evidenceFromRequestHeadersExtractor struct{} + +func newEvidenceFromRequestHeadersExtractor() evidenceFromRequestHeadersExtractor { + return evidenceFromRequestHeadersExtractor{} +} + +func (x evidenceFromRequestHeadersExtractor) extract(request *http.Request, httpHeaderKeys []dd.EvidenceKey) []stringEvidence { + return x.extractEvidenceStrings(request, httpHeaderKeys) +} + +func (x evidenceFromRequestHeadersExtractor) extractEvidenceStrings(r *http.Request, keys []dd.EvidenceKey) []stringEvidence { + evidence := make([]stringEvidence, 0) + for _, e := range keys { + if e.Prefix == dd.HttpEvidenceQuery { + continue + } + + // Get evidence from headers + headerVal := r.Header.Get(e.Key) + if headerVal == "" { + continue + } + + if e.Key != secUaFullVersionList && e.Key != secChUa { + headerVal = strings.Replace(headerVal, "\"", "", -1) + } + + if headerVal != "" { + evidence = append(evidence, stringEvidence{ + Prefix: headerPrefix, + Key: e.Key, + Value: headerVal, + }) + } + } + return evidence +} diff --git a/modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go b/modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go new file mode 100644 index 00000000000..77fbed3a42f --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go @@ -0,0 +1,118 @@ +package devicedetection + +import ( + "net/http" + "testing" + + "github.com/51Degrees/device-detection-go/v4/dd" + "github.com/stretchr/testify/assert" +) + +func TestExtractEvidenceStrings(t *testing.T) { + tests := []struct { + name string + headers map[string]string + keys []dd.EvidenceKey + expectedEvidence []stringEvidence + }{ + { + name: "Ignored_query_evidence", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpEvidenceQuery, Key: "User-Agent"}, + }, + expectedEvidence: []stringEvidence{}, + }, + { + name: "Empty_headers", + headers: map[string]string{}, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "User-Agent"}, + }, + expectedEvidence: []stringEvidence{}, + }, + { + name: "Single_header", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "User-Agent"}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: "User-Agent", Value: "Mozilla/5.0"}, + }, + }, + { + name: "Multiple_headers", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0", + "Accept": "text/html", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "User-Agent"}, + {Prefix: dd.HttpEvidenceQuery, Key: "Query"}, + {Prefix: dd.HttpHeaderString, Key: "Accept"}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: "User-Agent", Value: "Mozilla/5.0"}, + {Prefix: headerPrefix, Key: "Accept", Value: "text/html"}, + }, + }, + { + name: "Header_with_quotes_removed", + headers: map[string]string{ + "IP-List": "\"92.0.4515.159\"", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: "IP-List"}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: "IP-List", Value: "92.0.4515.159"}, + }, + }, + { + name: "Sec-CH-UA_headers_with_quotes_left", + headers: map[string]string{ + "Sec-CH-UA": "\"Chromium\";v=\"92\", \"Google Chrome\";v=\"92\"", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: secChUa}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: secChUa, Value: "\"Chromium\";v=\"92\", \"Google Chrome\";v=\"92\""}, + }, + }, + { + name: "Sec-CH-UA-Full-Version-List_headers_with_quotes_left", + headers: map[string]string{ + "Sec-CH-UA-Full-Version-List": "\"92.0.4515.159\"", + }, + keys: []dd.EvidenceKey{ + {Prefix: dd.HttpHeaderString, Key: secUaFullVersionList}, + }, + expectedEvidence: []stringEvidence{ + {Prefix: headerPrefix, Key: secUaFullVersionList, Value: "\"92.0.4515.159\""}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := http.Request{ + Header: make(map[string][]string), + } + + for key, value := range test.headers { + req.Header.Set(key, value) + } + + extractor := newEvidenceFromRequestHeadersExtractor() + evidence := extractor.extractEvidenceStrings(&req, test.keys) + + assert.Equal(t, test.expectedEvidence, evidence) + }) + } +} diff --git a/modules/fiftyonedegrees/devicedetection/sample/pbs.json b/modules/fiftyonedegrees/devicedetection/sample/pbs.json new file mode 100644 index 00000000000..43fd28610f1 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/sample/pbs.json @@ -0,0 +1,84 @@ +{ + "adapters": [ + { + "appnexus": { + "enabled": true + } + } + ], + "gdpr": { + "enabled": true, + "default_value": 0, + "timeouts_ms": { + "active_vendorlist_fetch": 900000 + } + }, + "stored_requests": { + "filesystem": { + "enabled": true, + "directorypath": "sample/stored" + } + }, + "stored_responses": { + "filesystem": { + "enabled": true, + "directorypath": "sample/stored" + } + }, + "hooks": { + "enabled": true, + "modules": { + "fiftyonedegrees": { + "devicedetection": { + "enabled": true, + "data_file": { + "path": "TAC-HashV41.hash", + "update": { + "auto": false, + "polling_interval": 3600, + "license_key": "YOUR_LICENSE_KEY", + "product": "V4Enterprise" + } + }, + "performance": { + "profile": "InMemory" + } + } + } + }, + "host_execution_plan": { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "fiftyonedegrees.devicedetection", + "hook_impl_code": "fiftyone-devicedetection-raw-auction-request-hook" + } + ] + } + ] + } + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/fiftyonedegrees/devicedetection/sample/request_data.json b/modules/fiftyonedegrees/devicedetection/sample/request_data.json new file mode 100644 index 00000000000..1f6bc8900f8 --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/sample/request_data.json @@ -0,0 +1,114 @@ +{ + "imp": [{ + "ext": { + "data": { + "adserver": { + "name": "gam", + "adslot": "test" + }, + "pbadslot": "test", + "gpid": "test" + }, + "gpid": "test", + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 1, + "use_pmt_rule": false + } + }, + "adunitcode": "25e8ad9f-13a4-4404-ba74-f9eebff0e86c", + "floors": { + "floorMin": 0.01 + } + } + }, + "id": "2529eeea-813e-4da6-838f-f91c28d64867", + "banner": { + "topframe": 1, + "format": [ + { + "w": 728, + "h": 90 + } + ], + "pos": 1 + }, + "bidfloor": 0.01, + "bidfloorcur": "USD" + }], + "site": { + "domain": "test.com", + "publisher": { + "domain": "test.com", + "id": "1" + }, + "page": "https://www.test.com/" + }, + "device": { + "ua": "Mozilla/5.0 (Linux; Android 11; SM-G998W) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36" + }, + "id": "fc4670ce-4985-4316-a245-b43c885dc37a", + "test": 1, + "cur": [ + "USD" + ], + "source": { + "ext": { + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + }, + "ext": { + "prebid": { + "cache": { + "bids": { + "returnCreative": true + }, + "vastxml": { + "returnCreative": true + } + }, + "auctiontimestamp": 1698390609882, + "targeting": { + "includewinners": true, + "includebidderkeys": false + }, + "schains": [ + { + "bidders": [ + "appnexus" + ], + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + ], + "floors": { + "enabled": false, + "floorMin": 0.01, + "floorMinCur": "USD" + }, + "createtids": false + } + }, + "user": {}, + "tmax": 1700 +} \ No newline at end of file diff --git a/modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go b/modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go new file mode 100644 index 00000000000..ab69210449f --- /dev/null +++ b/modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go @@ -0,0 +1,144 @@ +package devicedetection + +import ( + "fmt" + "strings" + + "github.com/spf13/cast" + "github.com/tidwall/gjson" +) + +const ( + secChUaArch = "Sec-Ch-Ua-Arch" + secChUaMobile = "Sec-Ch-Ua-Mobile" + secChUaModel = "Sec-Ch-Ua-Model" + secChUaPlatform = "Sec-Ch-Ua-Platform" + secUaFullVersionList = "Sec-Ch-Ua-Full-Version-List" + secChUaPlatformVersion = "Sec-Ch-Ua-Platform-Version" + secChUa = "Sec-Ch-Ua" + + userAgentHeader = "User-Agent" +) + +// evidenceFromSUAPayloadExtractor extracts evidence from the SUA payload of device +type evidenceFromSUAPayloadExtractor struct{} + +func newEvidenceFromSUAPayloadExtractor() evidenceFromSUAPayloadExtractor { + return evidenceFromSUAPayloadExtractor{} +} + +// Extract extracts evidence from the SUA payload +func (x evidenceFromSUAPayloadExtractor) extract(payload []byte) []stringEvidence { + if payload != nil { + return x.extractEvidenceStrings(payload) + } + + return nil +} + +var ( + uaPath = "device.ua" + archPath = "device.sua.architecture" + mobilePath = "device.sua.mobile" + modelPath = "device.sua.model" + platformBrandPath = "device.sua.platform.brand" + platformVersionPath = "device.sua.platform.version" + browsersPath = "device.sua.browsers" +) + +// extractEvidenceStrings extracts evidence from the SUA payload +func (x evidenceFromSUAPayloadExtractor) extractEvidenceStrings(payload []byte) []stringEvidence { + res := make([]stringEvidence, 0, 10) + + uaResult := gjson.GetBytes(payload, uaPath) + if uaResult.Exists() { + res = append( + res, + stringEvidence{Prefix: headerPrefix, Key: userAgentHeader, Value: uaResult.String()}, + ) + } + + archResult := gjson.GetBytes(payload, archPath) + if archResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaArch, archResult.String()) + } + + mobileResult := gjson.GetBytes(payload, mobilePath) + if mobileResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaMobile, mobileResult.String()) + } + + modelResult := gjson.GetBytes(payload, modelPath) + if modelResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaModel, modelResult.String()) + } + + platformBrandResult := gjson.GetBytes(payload, platformBrandPath) + if platformBrandResult.Exists() { + res = x.appendEvidenceIfExists(res, secChUaPlatform, platformBrandResult.String()) + } + + platformVersionResult := gjson.GetBytes(payload, platformVersionPath) + if platformVersionResult.Exists() { + res = x.appendEvidenceIfExists( + res, + secChUaPlatformVersion, + strings.Join(resultToStringArray(platformVersionResult.Array()), "."), + ) + } + + browsersResult := gjson.GetBytes(payload, browsersPath) + if browsersResult.Exists() { + res = x.appendEvidenceIfExists(res, secUaFullVersionList, x.extractBrowsers(browsersResult)) + + } + + return res +} + +func resultToStringArray(array []gjson.Result) []string { + strArray := make([]string, len(array)) + for i, result := range array { + strArray[i] = result.String() + } + + return strArray +} + +// appendEvidenceIfExists appends evidence to the destination if the value is not nil +func (x evidenceFromSUAPayloadExtractor) appendEvidenceIfExists(destination []stringEvidence, name string, value interface{}) []stringEvidence { + if value != nil { + valStr := cast.ToString(value) + if len(valStr) == 0 { + return destination + } + + return append( + destination, + stringEvidence{Prefix: headerPrefix, Key: name, Value: valStr}, + ) + } + + return destination +} + +// extractBrowsers extracts browsers from the SUA payload +func (x evidenceFromSUAPayloadExtractor) extractBrowsers(browsers gjson.Result) string { + if !browsers.IsArray() { + return "" + } + + browsersRaw := make([]string, len(browsers.Array())) + + for i, result := range browsers.Array() { + brand := result.Get("brand").String() + versionsRaw := result.Get("version").Array() + versions := resultToStringArray(versionsRaw) + + browsersRaw[i] = fmt.Sprintf(`"%s";v="%s"`, brand, strings.Join(versions, ".")) + } + + res := strings.Join(browsersRaw, ",") + + return res +} From 8b1b96e59003f7743d0e86301a87d9455e8592c1 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:22:18 +0000 Subject: [PATCH 15/47] Add docs build readme (#3930) --- docs/build/README.md | 110 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/build/README.md diff --git a/docs/build/README.md b/docs/build/README.md new file mode 100644 index 00000000000..0b52671f216 --- /dev/null +++ b/docs/build/README.md @@ -0,0 +1,110 @@ +## Overview + +As of v2.31.0, Prebid Server contains a module that requires CGo which introduces both build and runtime dependencies. To build, you need a C compiler, preferably gcc. To run, you may require one or more runtime dependencies, most notably libatomic. + +## Examples +For a containerized example, see the Dockerfile. +For manual build examples, including some cross-compilation use cases, see below. + +### From darwin amd64 + +#### To darwin amd64 +`GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build` + +Running the built binary on mac amd64: +`./prebid-server --stderrthreshold=WARNING -v=2` + +#### To darwin arm64 +`GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build` + +Running the built binary on mac arm64: +`./prebid-server --stderrthreshold=WARNING -v=2` + +#### To windows amd64 +Build +Install mingw-w64 which consists of a gcc compiler port you can use to generate windows binaries: +`brew install mingw-w64` + +`GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC="x86_64-w64-mingw32-gcc" go build` + +Run +Running the built binary on windows: +`.\prebid-server.exe --sderrthreshold=WARNING =v=2` + +You may receive the following errors or something similar: +``` +"The code execution cannot proceed because libatomic-1.dll was not found." +"The code execution cannot proceed because libwinpthread-1.dll was not found." +``` + +To resolve these errors, copy the following files from mingw-64 on your mac to `C:/windows/System32` and re-run: +`/usr/local/Cellar/mingw-w64/12.0.0_1/toolchain-x86_64/x86_64-w64-mingw32/lib/libatomic-1.dll` +`/usr/local/Cellar/mingw-w64/12.0.0_1/toolchain-x86_64/x86_64-w64-mingw32/bin/libwinpthread-1.dll` + +### From windows amd64 +#### To windows amd64 +Build +`set CGO_ENABLED=1` +`set GOOS=windows` +`set GOARCH=amd64` +`go build . && .\prebid-server.exe --stderrthreshold=WARNING -v=2` + +You may receive the following error or something similar: +``` +# runtime/cgo +cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in %PATH% +``` + +To resolve the error, install MSYS2: +1) Download the installer (https://www.msys2.org/) +2) Run the installer and follow the steps of the installation wizard +3) Run MSYS2 which will open an MSYS2 terminal for you +4) In the MSYS2 terminal, install windows/amd64 gcc toolchain: `pacman -S --needed base-devel mingw-w64-x86_64-gcc` +5) Enter `Y` when prompted whether to proceed with the installation +6) Add the path of your MinGW-w64 `bin` folder to the Windows `PATH` environment variable by using the following steps: + - In the Windows search bar, type Settings to open your Windows Settings. + - Search for Edit environment variables for your account. + - In your User variables, select the `Path` variable and then select Edit. + - Select New and add the MinGW-w64 destination folder you recorded during the installation process to the list. If you used the default settings above, then this will be the path: `C:\msys64\ucrt64\bin`. + - Select OK, and then select OK again in the Environment Variables window to update the `PATH` environment variable. You have to reopen any console windows for the updated `PATH` environment variable to be available. +7) Confirm gcc installed: `gcc --version` + +Run +Running the built binary on windows: +`go build . && .\prebid-server.exe --stderrthreshold=WARNING -v=2` + +You may receive the following errors or something similar: +``` +"The code execution cannot proceed because libatomic-1.dll was not found." +"The code execution cannot proceed because libwinpthread-1.dll was not found." +``` +To resolve these errors, copy the following files from MSYS2 installation to `C:/windows/System32` and re-run: +`C:\mysys64\mingw64\bin\libatomic-1.dll` +`C:\mysys64\mingw64\bin\libwinpthread-1.dll` + +### From linux amd64 +#### To linux amd64 +Note +These instructions are for building and running on Debian-based distributions + +Build +`GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build` + +You may receive the following error or something similar: +``` +# runtime/cgo +cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in $PATH +``` +To resolve the error, install gcc and re-build: +`sudo apt-get install -y gcc` + +Run +Running the built binary on Linux: +`./prebid-server --stderrthreshold=WARNING -v=2` + +You may receive the following error or something similar: +``` +... error while loading shared libraries: libatomic.so.1: cannot open shared object file ... +``` +To resolve the error, install libatomic1 and re-run: +`sudo apt-get install -y libatomic1` \ No newline at end of file From c42fe53a22b85dc0d2d651963158e92e8ce288ce Mon Sep 17 00:00:00 2001 From: bkaneyama Date: Wed, 25 Sep 2024 07:46:01 -0700 Subject: [PATCH 16/47] InMobi: mtype support (#3921) --- adapters/inmobi/inmobi.go | 28 +++--- .../exemplary/simple-app-banner.json | 4 +- .../exemplary/simple-app-native.json | 4 +- .../exemplary/simple-app-video.json | 4 +- .../exemplary/simple-web-banner.json | 4 +- .../exemplary/simple-web-native.json | 4 +- .../exemplary/simple-web-video.json | 4 +- .../supplemental/banner-format-coersion.json | 4 +- .../supplemental/invalid-mtype.json | 97 +++++++++++++++++++ 9 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 adapters/inmobi/inmobitest/supplemental/invalid-mtype.json diff --git a/adapters/inmobi/inmobi.go b/adapters/inmobi/inmobi.go index 9ceac3ec01a..eb69bdf3ec2 100644 --- a/adapters/inmobi/inmobi.go +++ b/adapters/inmobi/inmobi.go @@ -77,7 +77,10 @@ func (a *InMobiAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR for _, sb := range serverBidResponse.SeatBid { for i := range sb.Bid { - mediaType := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp) + mediaType, err := getMediaTypeForImp(sb.Bid[i]) + if err != nil { + return nil, []error{err} + } bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ Bid: &sb.Bid[i], BidType: mediaType, @@ -118,18 +121,17 @@ func preprocess(imp *openrtb2.Imp) error { return nil } -func getMediaTypeForImp(impId string, imps []openrtb2.Imp) openrtb_ext.BidType { - mediaType := openrtb_ext.BidTypeBanner - for _, imp := range imps { - if imp.ID == impId { - if imp.Video != nil { - mediaType = openrtb_ext.BidTypeVideo - } - if imp.Native != nil { - mediaType = openrtb_ext.BidTypeNative - } - break +func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unsupported mtype %d for bid %s", bid.MType, bid.ID), } } - return mediaType } diff --git a/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json b/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json index 563c8b9103e..01ccd98596f 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-app-banner.json @@ -75,7 +75,8 @@ "price": 2.0, "id": "1234", "adm": "bannerhtml", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 1 } ] } @@ -94,6 +95,7 @@ "adm": "bannerhtml", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 1, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-app-native.json b/adapters/inmobi/inmobitest/exemplary/simple-app-native.json index 7b823c13e11..0e956b6e586 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-app-native.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-app-native.json @@ -73,7 +73,8 @@ "price": 2.0, "id": "1234", "adm": "native-json", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 4 } ] } @@ -92,6 +93,7 @@ "adm": "native-json", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 4, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-app-video.json b/adapters/inmobi/inmobitest/exemplary/simple-app-video.json index 69356fd4de7..644b42d573a 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-app-video.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-app-video.json @@ -77,7 +77,8 @@ "price": 2.0, "id": "1234", "adm": " ", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 2 } ] } @@ -96,6 +97,7 @@ "adm": " ", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 2, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json b/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json index 0aac1b1571d..3359906e436 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-web-banner.json @@ -73,7 +73,8 @@ "price": 2.0, "id": "1234", "adm": "bannerhtml", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 1 } ] } @@ -92,6 +93,7 @@ "adm": "bannerhtml", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 1, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-web-native.json b/adapters/inmobi/inmobitest/exemplary/simple-web-native.json index 2d5a2a8c07c..25313573ea8 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-web-native.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-web-native.json @@ -71,7 +71,8 @@ "price": 2.0, "id": "1234", "adm": "native-json", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 4 } ] } @@ -90,6 +91,7 @@ "adm": "native-json", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 4, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/exemplary/simple-web-video.json b/adapters/inmobi/inmobitest/exemplary/simple-web-video.json index 7ea5dd268ef..582f9044fc9 100644 --- a/adapters/inmobi/inmobitest/exemplary/simple-web-video.json +++ b/adapters/inmobi/inmobitest/exemplary/simple-web-video.json @@ -75,7 +75,8 @@ "price": 2.0, "id": "1234", "adm": " ", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 2 } ] } @@ -94,6 +95,7 @@ "adm": " ", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 2, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json b/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json index 211348de3f3..ee6aae529ee 100644 --- a/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json +++ b/adapters/inmobi/inmobitest/supplemental/banner-format-coersion.json @@ -81,7 +81,8 @@ "price": 2.0, "id": "1234", "adm": "bannerhtml", - "impid": "imp-id" + "impid": "imp-id", + "mtype": 1 } ] } @@ -100,6 +101,7 @@ "adm": "bannerhtml", "crid": "123456789", "nurl": "https://some.event.url/params", + "mtype": 1, "ext": { "prebid": { "meta": { diff --git a/adapters/inmobi/inmobitest/supplemental/invalid-mtype.json b/adapters/inmobi/inmobitest/supplemental/invalid-mtype.json new file mode 100644 index 00000000000..af2192836b0 --- /dev/null +++ b/adapters/inmobi/inmobitest/supplemental/invalid-mtype.json @@ -0,0 +1,97 @@ +{ + "mockBidRequest": { + "site": { + "page": "https://www.inmobi.com" + }, + "id": "req-id", + "device": { + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1621323101291" + } + }, + "video": { + "w": 640, + "h": 360, + "mimes": ["video/mp4"] + }, + "id": "imp-id" + } + ] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid", + "body": { + "site": { + "page": "https://www.inmobi.com" + }, + "id": "req-id", + "device": { + "ip": "1.1.1.1", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36" + }, + "imp": [ + { + "ext": { + "bidder": { + "plc": "1621323101291" + } + }, + "video": { + "w": 640, + "h": 360, + "mimes": ["video/mp4"] + }, + "id": "imp-id" + } + ] + }, + "impIDs":["imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "req-id", + "seatbid": [ + { + "bid": [ + { + "ext": { + "prebid": { + "meta": { + "networkName": "inmobi" + } + } + }, + "nurl": "https://some.event.url/params", + "crid": "123456789", + "adomain": [], + "price": 2.0, + "id": "1234", + "adm": " ", + "impid": "imp-id", + "mtype": 0 + } + ] + } + ] + } + } + }], + + "expectedBidResponses":[], + "expectedMakeBidsErrors":[ + { + "value":"Unsupported mtype 0 for bid 1234", + "comparison":"literal" + } + ] +} + + From 53f51a6668ba7c1309915e5bd3feaabcac4cd771 Mon Sep 17 00:00:00 2001 From: przemkaczmarek <167743744+przemkaczmarek@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:53:07 +0200 Subject: [PATCH 17/47] GPC: Set extension based on header (#3895) --- endpoints/openrtb2/auction.go | 28 +++++ endpoints/openrtb2/auction_test.go | 169 ++++++++++++++++++++++++++++ openrtb_ext/request_wrapper.go | 38 ++++++- openrtb_ext/request_wrapper_test.go | 55 +++++++++ 4 files changed, 289 insertions(+), 1 deletion(-) diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 57d9e73a27d..1b5a9a4b5b1 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -64,6 +64,7 @@ const observeBrowsingTopicsValue = "?1" var ( dntKey string = http.CanonicalHeaderKey("DNT") + secGPCKey string = http.CanonicalHeaderKey("Sec-GPC") dntDisabled int8 = 0 dntEnabled int8 = 1 notAmp int8 = 0 @@ -1497,6 +1498,11 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ setAuctionTypeImplicitly(r) + err := setGPCImplicitly(httpReq, r) + if err != nil { + return []error{err} + } + errs := setSecBrowsingTopicsImplicitly(httpReq, r, account) return errs } @@ -1516,6 +1522,28 @@ func setAuctionTypeImplicitly(r *openrtb_ext.RequestWrapper) { } } +func setGPCImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) error { + secGPC := httpReq.Header.Get(secGPCKey) + + if secGPC != "1" { + return nil + } + + regExt, err := r.GetRegExt() + if err != nil { + return err + } + + if regExt.GetGPC() != nil { + return nil + } + + gpc := "1" + regExt.SetGPC(&gpc) + + return nil +} + // setSecBrowsingTopicsImplicitly updates user.data with data from request header 'Sec-Browsing-Topics' func setSecBrowsingTopicsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error { secBrowsingTopics := httpReq.Header.Get(secBrowsingTopics) diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 717609e23d9..3f8cdc668f9 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -5618,6 +5618,175 @@ func TestValidateOrFillCookieDeprecation(t *testing.T) { } } +func TestSetGPCImplicitly(t *testing.T) { + testCases := []struct { + description string + header string + regs *openrtb2.Regs + expectError bool + expectedRegs *openrtb2.Regs + }{ + { + description: "regs_ext_gpc_not_set_and_header_is_1", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "sec_gpc_header_not_set_gpc_should_not_be_modified", + header: "", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "sec_gpc_header_set_to_2_gpc_should_not_be_modified", + header: "2", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "sec_gpc_header_set_to_1_and_regs_ext_contains_other_data", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{"some_other_field":"some_value"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"some_other_field":"some_value","gpc":"1"}`), + }, + }, + { + description: "regs_ext_gpc_not_set_and_header_not_set", + header: "", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "regs_ext_gpc_not_set_and_header_not_1", + header: "0", + regs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{}`), + }, + }, + { + description: "regs_ext_gpc_is_1_and_header_is_1", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "regs_ext_gpc_is_1_and_header_not_1", + header: "0", + regs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "regs_ext_other_data_and_header_is_1", + header: "1", + regs: &openrtb2.Regs{ + Ext: []byte(`{"other":"value"}`), + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"other":"value","gpc":"1"}`), + }, + }, + { + description: "regs_nil_and_header_is_1", + header: "1", + regs: nil, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: []byte(`{"gpc":"1"}`), + }, + }, + { + description: "regs_nil_and_header_not_set", + header: "", + regs: nil, + expectError: false, + expectedRegs: nil, + }, + { + description: "regs_ext_is_nil_and_header_not_set", + header: "", + regs: &openrtb2.Regs{ + Ext: nil, + }, + expectError: false, + expectedRegs: &openrtb2.Regs{ + Ext: nil, + }, + }, + } + + for _, test := range testCases { + t.Run(test.description, func(t *testing.T) { + httpReq := &http.Request{ + Header: http.Header{ + http.CanonicalHeaderKey("Sec-GPC"): []string{test.header}, + }, + } + + r := &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: test.regs, + }, + } + + err := setGPCImplicitly(httpReq, r) + + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.NoError(t, r.RebuildRequest()) + if test.expectedRegs == nil { + assert.Nil(t, r.BidRequest.Regs) + } else if test.expectedRegs.Ext == nil { + assert.Nil(t, r.BidRequest.Regs.Ext) + } else { + assert.JSONEq(t, string(test.expectedRegs.Ext), string(r.BidRequest.Regs.Ext)) + } + }) + } +} + func TestValidateRequestCookieDeprecation(t *testing.T) { testCases := []struct { diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index 27c20e98f35..a73cfc9241c 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -61,6 +61,7 @@ const ( schainKey = "schain" us_privacyKey = "us_privacy" cdepKey = "cdep" + gpcKey = "gpc" ) // LenImp returns the number of impressions without causing the creation of ImpWrapper objects. @@ -1201,6 +1202,8 @@ type RegExt struct { dsaDirty bool gdpr *int8 gdprDirty bool + gpc *string + gpcDirty bool usPrivacy string usPrivacyDirty bool } @@ -1244,6 +1247,13 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error { } } + gpcJson, hasGPC := re.ext[gpcKey] + if hasGPC && gpcJson != nil { + if err := jsonutil.Unmarshal(gpcJson, &re.gpc); err != nil { + return err + } + } + return nil } @@ -1287,6 +1297,19 @@ func (re *RegExt) marshal() (json.RawMessage, error) { re.usPrivacyDirty = false } + if re.gpcDirty { + if re.gpc != nil { + rawjson, err := jsonutil.Marshal(re.gpc) + if err != nil { + return nil, err + } + re.ext[gpcKey] = rawjson + } else { + delete(re.ext, gpcKey) + } + re.gpcDirty = false + } + re.extDirty = false if len(re.ext) == 0 { return nil, nil @@ -1295,7 +1318,7 @@ func (re *RegExt) marshal() (json.RawMessage, error) { } func (re *RegExt) Dirty() bool { - return re.extDirty || re.dsaDirty || re.gdprDirty || re.usPrivacyDirty + return re.extDirty || re.dsaDirty || re.gdprDirty || re.usPrivacyDirty || re.gpcDirty } func (re *RegExt) GetExt() map[string]json.RawMessage { @@ -1337,6 +1360,19 @@ func (re *RegExt) SetGDPR(gdpr *int8) { re.gdprDirty = true } +func (re *RegExt) GetGPC() *string { + if re.gpc == nil { + return nil + } + gpc := *re.gpc + return &gpc +} + +func (re *RegExt) SetGPC(gpc *string) { + re.gpc = gpc + re.gpcDirty = true +} + func (re *RegExt) GetUSPrivacy() string { uSPrivacy := re.usPrivacy return uSPrivacy diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index 7b21531e02e..c7892b964af 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -2174,6 +2174,30 @@ func TestRebuildRegExt(t *testing.T) { regExt: RegExt{usPrivacy: "", usPrivacyDirty: true}, expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, }, + { + name: "req_regs_gpc_populated_-_not_dirty_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + }, + { + name: "req_regs_gpc_populated_-_dirty_and_different-_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{gpc: &strB, gpcDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"b"}`)}}, + }, + { + name: "req_regs_gpc_populated_-_dirty_and_same_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{gpc: &strA, gpcDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + }, + { + name: "req_regs_gpc_populated_-_dirty_and_nil_-_cleared", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gpc":"a"}`)}}, + regExt: RegExt{gpc: nil, gpcDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + }, } for _, tt := range tests { @@ -2194,6 +2218,7 @@ func TestRegExtUnmarshal(t *testing.T) { extJson json.RawMessage expectDSA *ExtRegsDSA expectGDPR *int8 + expectGPC *string expectUSPrivacy string expectError bool }{ @@ -2253,6 +2278,21 @@ func TestRegExtUnmarshal(t *testing.T) { expectGDPR: ptrutil.ToPtr[int8](0), expectError: true, }, + // GPC + { + name: "valid_gpc_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"gpc":"some_value"}`), + expectGPC: ptrutil.ToPtr("some_value"), + expectError: false, + }, + { + name: "malformed_gpc_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"gpc":nill}`), + expectGPC: nil, + expectError: true, + }, // us_privacy { name: "valid_usprivacy_json", @@ -2348,3 +2388,18 @@ func TestRegExtGetGDPRSetGDPR(t *testing.T) { assert.Equal(t, regExtGDPR, gdpr) assert.NotSame(t, regExtGDPR, gdpr) } + +func TestRegExtGetGPCSetGPC(t *testing.T) { + regExt := &RegExt{} + regExtGPC := regExt.GetGPC() + assert.Nil(t, regExtGPC) + assert.False(t, regExt.Dirty()) + + gpc := ptrutil.ToPtr("Gpc") + regExt.SetGPC(gpc) + assert.True(t, regExt.Dirty()) + + regExtGPC = regExt.GetGPC() + assert.Equal(t, regExtGPC, gpc) + assert.NotSame(t, regExtGPC, gpc) +} From 6c154e0f7f86d63227026506cf38872f6fe61cb1 Mon Sep 17 00:00:00 2001 From: Alexander Pykhteyev Date: Wed, 25 Sep 2024 23:08:23 +0700 Subject: [PATCH 18/47] New Adapter: Streamlyn (#3900) Co-authored-by: apykhteyev --- static/bidder-info/streamlyn.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 static/bidder-info/streamlyn.yaml diff --git a/static/bidder-info/streamlyn.yaml b/static/bidder-info/streamlyn.yaml new file mode 100644 index 00000000000..0cf1444ef29 --- /dev/null +++ b/static/bidder-info/streamlyn.yaml @@ -0,0 +1,2 @@ +endpoint: "http://rtba.bidsxchange.com/openrtb/{{.PublisherID}}?host={{.Host}}" +aliasOf: "limelightDigital" From f27bcefd0ffa351a1b844f26cc5d6250fd071c64 Mon Sep 17 00:00:00 2001 From: dkornet-ad <169174147+dkornet-ad@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:33:54 +0300 Subject: [PATCH 19/47] New Adapter: Bidmatic (#3731) authored by @dkornet-ad --- adapters/bidmatic/bidmatic.go | 206 ++++++++++++++++++ adapters/bidmatic/bidmatic_test.go | 23 ++ .../exemplary/media-type-mapping.json | 91 ++++++++ .../bidmatictest/exemplary/simple-banner.json | 98 +++++++++ .../bidmatictest/exemplary/simple-video.json | 57 +++++ .../supplemental/explicit-dimensions.json | 60 +++++ .../supplemental/imp-ext-empty.json | 21 ++ .../supplemental/wrong-impression-ext.json | 26 +++ .../wrong-impression-mapping.json | 79 +++++++ .../supplemental/wrong-response.json | 65 ++++++ adapters/bidmatic/params_test.go | 64 ++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_bidmatic.go | 11 + static/bidder-info/bidmatic.yaml | 18 ++ static/bidder-params/bidmatic.json | 29 +++ 16 files changed, 852 insertions(+) create mode 100644 adapters/bidmatic/bidmatic.go create mode 100644 adapters/bidmatic/bidmatic_test.go create mode 100644 adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json create mode 100644 adapters/bidmatic/bidmatictest/exemplary/simple-banner.json create mode 100644 adapters/bidmatic/bidmatictest/exemplary/simple-video.json create mode 100644 adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json create mode 100644 adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json create mode 100644 adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json create mode 100644 adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json create mode 100644 adapters/bidmatic/bidmatictest/supplemental/wrong-response.json create mode 100644 adapters/bidmatic/params_test.go create mode 100644 openrtb_ext/imp_bidmatic.go create mode 100644 static/bidder-info/bidmatic.yaml create mode 100644 static/bidder-params/bidmatic.json diff --git a/adapters/bidmatic/bidmatic.go b/adapters/bidmatic/bidmatic.go new file mode 100644 index 00000000000..950107ea0e0 --- /dev/null +++ b/adapters/bidmatic/bidmatic.go @@ -0,0 +1,206 @@ +package bidmatic + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type bidmaticImpExt struct { + Bidmatic openrtb_ext.ExtImpBidmatic `json:"bidmatic"` +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + totalImps := len(request.Imp) + errors := make([]error, 0, totalImps) + imp2source := make(map[int][]int) + + for i := 0; i < totalImps; i++ { + sourceId, err := validateImpression(&request.Imp[i]) + if err != nil { + errors = append(errors, err) + continue + } + + if _, ok := imp2source[sourceId]; !ok { + imp2source[sourceId] = make([]int, 0, totalImps-i) + } + + imp2source[sourceId] = append(imp2source[sourceId], i) + } + + totalReqs := len(imp2source) + if totalReqs == 0 { + return nil, errors + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + reqs := make([]*adapters.RequestData, 0, totalReqs) + + imps := request.Imp + request.Imp = make([]openrtb2.Imp, 0, len(imps)) + for sourceId, impIds := range imp2source { + request.Imp = request.Imp[:0] + + for i := 0; i < len(impIds); i++ { + request.Imp = append(request.Imp, imps[impIds[i]]) + } + + body, err := json.Marshal(request) + if err != nil { + errors = append(errors, fmt.Errorf("error while encoding bidRequest, err: %s", err)) + return nil, errors + } + + reqs = append(reqs, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint + fmt.Sprintf("?source=%d", sourceId), + Body: body, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }) + } + + if len(reqs) == 0 { + return nil, errors + } + + return reqs, errors +} + +func (a *adapter) MakeBids(bidReq *openrtb2.BidRequest, unused *adapters.RequestData, httpRes *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(httpRes) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(httpRes); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(httpRes.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("error while decoding response, err: %s", err), + }} + } + + bidResponse := adapters.NewBidderResponse() + var errors []error + + var impOK bool + for _, sb := range bidResp.SeatBid { + for i := 0; i < len(sb.Bid); i++ { + + bid := sb.Bid[i] + + impOK = false + mediaType := openrtb_ext.BidTypeBanner + bid.MType = openrtb2.MarkupBanner + loop: + for _, imp := range bidReq.Imp { + if imp.ID == bid.ImpID { + + impOK = true + + switch { + case imp.Video != nil: + mediaType = openrtb_ext.BidTypeVideo + bid.MType = openrtb2.MarkupVideo + break loop + case imp.Banner != nil: + mediaType = openrtb_ext.BidTypeBanner + bid.MType = openrtb2.MarkupBanner + break loop + case imp.Audio != nil: + mediaType = openrtb_ext.BidTypeAudio + bid.MType = openrtb2.MarkupAudio + break loop + case imp.Native != nil: + mediaType = openrtb_ext.BidTypeNative + bid.MType = openrtb2.MarkupNative + break loop + } + } + } + + if !impOK { + errors = append(errors, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("ignoring bid id=%s, request doesn't contain any impression with id=%s", bid.ID, bid.ImpID), + }) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: mediaType, + }) + } + } + + return bidResponse, errors +} + +func validateImpression(imp *openrtb2.Imp) (int, error) { + if len(imp.Ext) == 0 { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, extImpBidder is empty", imp.ID), + } + } + + var bidderExt adapters.ExtImpBidder + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), + } + } + + impExt := openrtb_ext.ExtImpBidmatic{} + err := json.Unmarshal(bidderExt.Bidder, &impExt) + if err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), + } + } + + // common extension for all impressions + var impExtBuffer []byte + + impExtBuffer, err = json.Marshal(&bidmaticImpExt{ + Bidmatic: impExt, + }) + if err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while marshaling impExt, err: %s", imp.ID, err), + } + } + + if impExt.BidFloor > 0 { + imp.BidFloor = impExt.BidFloor + } + + imp.Ext = impExtBuffer + + source, err := impExt.SourceId.Int64() // json.Unmarshal returns err if it isn't valid + if err != nil { + return 0, err + } + return int(source), nil +} + +// Builder builds a new instance of the bidmatic adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + return &adapter{endpoint: config.Endpoint}, nil +} diff --git a/adapters/bidmatic/bidmatic_test.go b/adapters/bidmatic/bidmatic_test.go new file mode 100644 index 00000000000..c6a31823223 --- /dev/null +++ b/adapters/bidmatic/bidmatic_test.go @@ -0,0 +1,23 @@ +package bidmatic + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderBidmatic, + config.Adapter{Endpoint: "http://adapter.bidmatic.io/pbs/ortb"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, + ) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "bidmatictest", bidder) +} diff --git a/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json b/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json new file mode 100644 index 00000000000..57f1215af43 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json @@ -0,0 +1,91 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json b/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json new file mode 100644 index 00000000000..d3c41278231 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json @@ -0,0 +1,98 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "source": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [ + {"w":300,"h":250}, + {"w":300,"h":600} + ] + }, + "bidfloor": 20, + "ext": { + "bidmatic": { + "source": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 1, + "price": 3.5, + "w": 900, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/exemplary/simple-video.json b/adapters/bidmatic/bidmatictest/exemplary/simple-video.json new file mode 100644 index 00000000000..a9bcb6a141e --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/simple-video.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json b/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json new file mode 100644 index 00000000000..b1f2f6ea510 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json b/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json new file mode 100644 index 00000000000..0607da05fb9 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json @@ -0,0 +1,21 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "video": { + "w": 100, + "h": 200 + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, extImpBidder is empty", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json new file mode 100644 index 00000000000..8154afed75f --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json @@ -0,0 +1,26 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "video": { + "w": 100, + "h": 200 + }, + "ext": { + "bidder": { + "source": "some string instead of int" + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, error while decoding impExt, err: json: invalid number literal, trying to unmarshal \"\\\"some string instead of int\\\"\" into Number", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..05679082aa3 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "SOME-WRONG-IMP-ID", + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json new file mode 100644 index 00000000000..ad09b32cd1a --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "error while decoding response, err: unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/params_test.go b/adapters/bidmatic/params_test.go new file mode 100644 index 00000000000..6bdc5f4339d --- /dev/null +++ b/adapters/bidmatic/params_test.go @@ -0,0 +1,64 @@ +package bidmatic + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/bidmatic.json +// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.bidmatic +// TestValidParams makes sure that the bidmatic schema accepts all imp.ext fields which we intend to support. + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderBidmatic, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected bidmatic params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the bidmatic schema rejects all the imp.ext fields we don't support. +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderBidmatic, json.RawMessage(invalidParam)); err == nil { + ext := openrtb_ext.ExtImpBidmatic{} + err = json.Unmarshal([]byte(invalidParam), &ext) + if err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } + } +} + +var validParams = []string{ + `{"source":123}`, + `{"source":"123"}`, + `{"source":123,"placementId":1234}`, + `{"source":123,"siteId":4321}`, + `{"source":"123","siteId":0,"bidFloor":0}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"source":"qwerty"}`, + `{"source":"123","placementId":"123"}`, + `{"source":123, "placementId":"123", "siteId":"321"}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index b05c048ec7c..88948460aff 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -52,6 +52,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/between" "github.com/prebid/prebid-server/v2/adapters/beyondmedia" "github.com/prebid/prebid-server/v2/adapters/bidmachine" + "github.com/prebid/prebid-server/v2/adapters/bidmatic" "github.com/prebid/prebid-server/v2/adapters/bidmyadz" "github.com/prebid/prebid-server/v2/adapters/bidscube" "github.com/prebid/prebid-server/v2/adapters/bidstack" @@ -279,6 +280,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderBetween: between.Builder, openrtb_ext.BidderBeyondMedia: beyondmedia.Builder, openrtb_ext.BidderBidmachine: bidmachine.Builder, + openrtb_ext.BidderBidmatic: bidmatic.Builder, openrtb_ext.BidderBidmyadz: bidmyadz.Builder, openrtb_ext.BidderBidsCube: bidscube.Builder, openrtb_ext.BidderBidstack: bidstack.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 0570d8a468d..522e2490da1 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -68,6 +68,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderBetween, BidderBeyondMedia, BidderBidmachine, + BidderBidmatic, BidderBidmyadz, BidderBidsCube, BidderBidstack, @@ -395,6 +396,7 @@ const ( BidderBetween BidderName = "between" BidderBeyondMedia BidderName = "beyondmedia" BidderBidmachine BidderName = "bidmachine" + BidderBidmatic BidderName = "bidmatic" BidderBidmyadz BidderName = "bidmyadz" BidderBidsCube BidderName = "bidscube" BidderBidstack BidderName = "bidstack" diff --git a/openrtb_ext/imp_bidmatic.go b/openrtb_ext/imp_bidmatic.go new file mode 100644 index 00000000000..935c977e7ac --- /dev/null +++ b/openrtb_ext/imp_bidmatic.go @@ -0,0 +1,11 @@ +package openrtb_ext + +import "encoding/json" + +// ExtImpBidmatic defines the contract for bidrequest.imp[i].ext.prebid.bidder.bidmatic +type ExtImpBidmatic struct { + SourceId json.Number `json:"source"` + PlacementId int `json:"placementId,omitempty"` + SiteId int `json:"siteId,omitempty"` + BidFloor float64 `json:"bidFloor,omitempty"` +} diff --git a/static/bidder-info/bidmatic.yaml b/static/bidder-info/bidmatic.yaml new file mode 100644 index 00000000000..19211190033 --- /dev/null +++ b/static/bidder-info/bidmatic.yaml @@ -0,0 +1,18 @@ +endpoint: "http://adapter.bidmatic.io/pbs/ortb" +maintainer: + email: "advertising@bidmatic.io" +gvlVendorID: 1134 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + # bidmatic supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - iframe diff --git a/static/bidder-params/bidmatic.json b/static/bidder-params/bidmatic.json new file mode 100644 index 00000000000..b3002a55ac5 --- /dev/null +++ b/static/bidder-params/bidmatic.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidmatic Adapter Params", + "description": "A schema which validates params accepted by the Bidmatic adapter", + + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "siteId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "source": { + "type": [ + "integer", + "string" + ], + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": ["source"] +} From 8689e0df95b9449a22d28eb5c56bbe9fc53f719c Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 7 Oct 2024 10:39:59 -0400 Subject: [PATCH 20/47] Sonobi: native and currency conversion support (#3889) Co-authored-by: bansawbanchee --- adapters/sonobi/sonobi.go | 39 +++- .../sonobi/sonobitest/exemplary/banner.json | 2 + .../sonobi/sonobitest/exemplary/native.json | 143 +++++++++++++++ .../sonobi/sonobitest/exemplary/no-bid.json | 2 + .../supplemental/currency-conversion.json | 172 ++++++++++++++++++ static/bidder-info/sonobi.yaml | 5 + 6 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 adapters/sonobi/sonobitest/exemplary/native.json create mode 100644 adapters/sonobi/sonobitest/supplemental/currency-conversion.json diff --git a/adapters/sonobi/sonobi.go b/adapters/sonobi/sonobi.go index 65ea0d33e7c..afc1e5793d9 100644 --- a/adapters/sonobi/sonobi.go +++ b/adapters/sonobi/sonobi.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/adapters" @@ -53,6 +54,25 @@ func (a *SonobiAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adap reqCopy.Imp[0].TagID = sonobiExt.TagID + // If the bid floor currency is not USD, do the conversion to USD + if reqCopy.Imp[0].BidFloor > 0 && reqCopy.Imp[0].BidFloorCur != "" && strings.ToUpper(reqCopy.Imp[0].BidFloorCur) != "USD" { + + // Convert to US dollars + convertedValue, err := reqInfo.ConvertCurrency(reqCopy.Imp[0].BidFloor, reqCopy.Imp[0].BidFloorCur, "USD") + if err != nil { + errs = append(errs, err) + continue + } + + // Update after conversion. All imp elements inside request.Imp are shallow copies + // therefore, their non-pointer values are not shared memory and are safe to modify. + reqCopy.Imp[0].BidFloorCur = "USD" + reqCopy.Imp[0].BidFloor = convertedValue + } + + // Sonobi only bids in USD + reqCopy.Cur = append(make([]string, 0, 1), "USD") + adapterReq, errors := a.makeRequest(&reqCopy) if adapterReq != nil { adapterRequests = append(adapterRequests, adapterReq) @@ -115,19 +135,19 @@ func (a *SonobiAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR } bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) + bidResponse.Currency = "USD" // Sonobi only bids in USD for _, sb := range bidResp.SeatBid { for i := range sb.Bid { - bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp) + bid := sb.Bid[i] + bidType, err := getMediaTypeForImp(bid.ImpID, internalRequest.Imp) if err != nil { - errs = append(errs, err) - } else { - b := &adapters.TypedBid{ - Bid: &sb.Bid[i], - BidType: bidType, - } - bidResponse.Bids = append(bidResponse.Bids, b) + return nil, []error{err} } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bidType, + }) } } return bidResponse, errs @@ -140,6 +160,9 @@ func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, if imp.Banner == nil && imp.Video != nil { mediaType = openrtb_ext.BidTypeVideo } + if imp.Banner == nil && imp.Video == nil && imp.Native != nil { + mediaType = openrtb_ext.BidTypeNative + } return mediaType, nil } } diff --git a/adapters/sonobi/sonobitest/exemplary/banner.json b/adapters/sonobi/sonobitest/exemplary/banner.json index d60ddb44a8b..f3a39f74a27 100644 --- a/adapters/sonobi/sonobitest/exemplary/banner.json +++ b/adapters/sonobi/sonobitest/exemplary/banner.json @@ -1,5 +1,6 @@ { "mockBidRequest": { + "cur": ["GBP"], "id": "some-request-id", "site": { "page": "http://tester.go.sonobi.com", @@ -48,6 +49,7 @@ }, "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", "body": { + "cur": ["USD"], "id": "some-request-id", "imp": [ { diff --git a/adapters/sonobi/sonobitest/exemplary/native.json b/adapters/sonobi/sonobitest/exemplary/native.json new file mode 100644 index 00000000000..ff9eb4c4693 --- /dev/null +++ b/adapters/sonobi/sonobitest/exemplary/native.json @@ -0,0 +1,143 @@ +{ + "mockBidRequest": { + "cur": ["USD"], + "id": "some-request-id", + "site": { + "page": "http://tester.go.sonobi.com", + "domain": "sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":2,\"plcmtcnt\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":1000}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":600,\"hmin\":600}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":3000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":60}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}},{\"id\":10,\"required\":0,\"data\":{\"type\":12,\"len\":15}}],\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"privacy\":1}", + "ver": "1.2", + "battr": [ + 1, + 2, + 6, + 7, + 8, + 9, + 10, + 14 + ] + }, + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", + "body": { + "cur": ["USD"], + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "native": { + "request": "{\"ver\":\"1.2\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":2,\"plcmtcnt\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":1000}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":600,\"hmin\":600}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":3000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":60}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}},{\"id\":10,\"required\":0,\"data\":{\"type\":12,\"len\":15}}],\"eventtrackers\":[{\"event\":1,\"methods\":[1,2]},{\"event\":2,\"methods\":[1]}],\"privacy\":1}", + "ver": "1.2", + "battr": [ + 1, + 2, + 6, + 7, + 8, + 9, + 10, + 14 + ] + }, + "tagid": "/7780971/apex_3pdm_integration", + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "site": { + "domain": "sonobi.com", + "page": "http://tester.go.sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + }, + "impIDs": [ + "some-impression-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "test-markup", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox" + + } + ], + "seat": "sonobi" + } + ], + "bidid": "sandbox_642305097", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "test-markup", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox" + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/sonobi/sonobitest/exemplary/no-bid.json b/adapters/sonobi/sonobitest/exemplary/no-bid.json index ef31c9f2ba4..a43d1f670a0 100644 --- a/adapters/sonobi/sonobitest/exemplary/no-bid.json +++ b/adapters/sonobi/sonobitest/exemplary/no-bid.json @@ -1,5 +1,6 @@ { "mockBidRequest": { + "cur": ["USD"], "id": "some-request-id", "site": { "page": "http://tester.go.sonobi.com", @@ -48,6 +49,7 @@ }, "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", "body": { + "cur": ["USD"], "id": "some-request-id", "imp": [ { diff --git a/adapters/sonobi/sonobitest/supplemental/currency-conversion.json b/adapters/sonobi/sonobitest/supplemental/currency-conversion.json new file mode 100644 index 00000000000..522e1bd7326 --- /dev/null +++ b/adapters/sonobi/sonobitest/supplemental/currency-conversion.json @@ -0,0 +1,172 @@ +{ + "mockBidRequest": { + "cur": ["GBP"], + "id": "some-request-id", + "site": { + "page": "http://tester.go.sonobi.com", + "domain": "sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 300, + "h": 250, + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "bidfloor": 1.00, + "bidfloorcur": "GBP", + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "GBP": { + "USD": 0.05 + } + }, + "usepbsrates": false + } + } + }, + "test": 1, + "tmax": 500 + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af", + "body": { + "cur": ["USD"], + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ], + "w": 300, + "h": 250 + }, + "bidfloor": 0.05, + "bidfloorcur": "USD", + "tagid": "/7780971/apex_3pdm_integration", + "ext": { + "bidder": { + "TagID": "/7780971/apex_3pdm_integration" + } + } + } + ], + "ext": { + "prebid": { + "currency": { + "rates": { + "GBP": { + "USD": 0.05 + } + }, + "usepbsrates": false + } + } + }, + "site": { + "domain": "sonobi.com", + "page": "http://tester.go.sonobi.com" + }, + "device": { + "ip": "123.123.123.123" + }, + "test": 1, + "tmax": 500 + }, + "impIDs":["some-impression-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox", + "h": 1, + "w": 1 + } + ], + "seat": "sonobi" + } + ], + "bidid": "sandbox_642305097", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "some-impression-id", + "price": 2.8649999999999998, + "adm": "", + "adomain": [ + "sonobi.com" + ], + "cid": "house", + "crid": "sandbox", + "h": 1, + "w": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/static/bidder-info/sonobi.yaml b/static/bidder-info/sonobi.yaml index 135bdacfe93..422ba610a6f 100644 --- a/static/bidder-info/sonobi.yaml +++ b/static/bidder-info/sonobi.yaml @@ -7,11 +7,16 @@ capabilities: mediaTypes: - banner - video + - native app: mediaTypes: - banner - video + - native userSync: + iframe: + url: "https://sync.go.sonobi.com/uc.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&loc={{.RedirectURL}}" + userMacro: "[UID]" redirect: url: "https://sync.go.sonobi.com/us.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&loc={{.RedirectURL}}" userMacro: "[UID]" From d54c3ed22d437a590092870e611903208f840969 Mon Sep 17 00:00:00 2001 From: ysfbsf Date: Tue, 8 Oct 2024 15:42:40 +0200 Subject: [PATCH 21/47] New Adapter: Missena (#3761) --- adapters/missena/missena.go | 215 ++++++++++++++++++ adapters/missena/missena_test.go | 21 ++ .../missenatest/exemplary/multiple-imps.json | 129 +++++++++++ .../exemplary/simple-banner-ipv6.json | 105 +++++++++ .../missenatest/exemplary/simple-banner.json | 105 +++++++++ .../exemplary/valid-imp-error-imp.json | 129 +++++++++++ .../supplemental/error-ext-bidder.json | 25 ++ .../supplemental/error-imp-ext.json | 23 ++ .../missenatest/supplemental/status-204.json | 83 +++++++ .../missenatest/supplemental/status-400.json | 89 ++++++++ .../supplemental/status-not-200.json | 89 ++++++++ adapters/missena/params_test.go | 50 ++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_missena.go | 7 + static/bidder-info/missena.yaml | 16 ++ static/bidder-params/missena.json | 24 ++ 17 files changed, 1114 insertions(+) create mode 100644 adapters/missena/missena.go create mode 100644 adapters/missena/missena_test.go create mode 100644 adapters/missena/missenatest/exemplary/multiple-imps.json create mode 100644 adapters/missena/missenatest/exemplary/simple-banner-ipv6.json create mode 100644 adapters/missena/missenatest/exemplary/simple-banner.json create mode 100644 adapters/missena/missenatest/exemplary/valid-imp-error-imp.json create mode 100644 adapters/missena/missenatest/supplemental/error-ext-bidder.json create mode 100644 adapters/missena/missenatest/supplemental/error-imp-ext.json create mode 100644 adapters/missena/missenatest/supplemental/status-204.json create mode 100644 adapters/missena/missenatest/supplemental/status-400.json create mode 100644 adapters/missena/missenatest/supplemental/status-not-200.json create mode 100644 adapters/missena/params_test.go create mode 100644 openrtb_ext/imp_missena.go create mode 100644 static/bidder-info/missena.yaml create mode 100644 static/bidder-params/missena.json diff --git a/adapters/missena/missena.go b/adapters/missena/missena.go new file mode 100644 index 00000000000..93d4c2ba1cb --- /dev/null +++ b/adapters/missena/missena.go @@ -0,0 +1,215 @@ +package missena + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type MissenaAdRequest struct { + RequestId string `json:"request_id"` + Timeout int `json:"timeout"` + Referer string `json:"referer"` + RefererCanonical string `json:"referer_canonical"` + GDPRConsent string `json:"consent_string"` + GDPR bool `json:"consent_required"` + Placement string `json:"placement"` + TestMode string `json:"test"` +} + +type MissenaBidServerResponse struct { + Ad string `json:"ad"` + Cpm float64 `json:"cpm"` + Currency string `json:"currency"` + RequestId string `json:"requestId"` +} + +type MissenaInternalParams struct { + ApiKey string + RequestId string + Timeout int + Referer string + RefererCanonical string + GDPRConsent string + GDPR bool + Placement string + TestMode string +} + +type MissenaAdapter struct { + EndpointTemplate *template.Template +} + +// Builder builds a new instance of the Foo adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) makeRequest(missenaParams MissenaInternalParams, reqInfo *adapters.ExtraRequestInfo, impID string, request *openrtb2.BidRequest) (*adapters.RequestData, error) { + url := a.endpoint + "?t=" + missenaParams.ApiKey + + missenaRequest := MissenaAdRequest{ + RequestId: request.ID, + Timeout: 2000, + Referer: request.Site.Page, + RefererCanonical: request.Site.Domain, + GDPRConsent: missenaParams.GDPRConsent, + GDPR: missenaParams.GDPR, + Placement: missenaParams.Placement, + TestMode: missenaParams.TestMode, + } + + body, errm := json.Marshal(missenaRequest) + if errm != nil { + return nil, errm + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + if request.Device != nil { + headers.Add("User-Agent", request.Device.UA) + if request.Device.IP != "" { + headers.Add("X-Forwarded-For", request.Device.IP) + } else if request.Device.IPv6 != "" { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + } + if request.Site != nil { + headers.Add("Referer", request.Site.Page) + } + + return &adapters.RequestData{ + Method: "POST", + Uri: url, + Headers: headers, + Body: body, + ImpIDs: []string{impID}, + }, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + var httpRequests []*adapters.RequestData + var errors []error + gdprApplies, consentString := readGDPR(request) + + missenaInternalParams := MissenaInternalParams{ + GDPR: gdprApplies, + GDPRConsent: consentString, + } + + for _, imp := range request.Imp { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: "Error parsing bidderExt object", + }) + continue + } + + var missenaExt openrtb_ext.ExtImpMissena + if err := json.Unmarshal(bidderExt.Bidder, &missenaExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: "Error parsing missenaExt parameters", + }) + continue + } + + missenaInternalParams.ApiKey = missenaExt.ApiKey + missenaInternalParams.Placement = missenaExt.Placement + missenaInternalParams.TestMode = missenaExt.TestMode + + newHttpRequest, err := a.makeRequest(missenaInternalParams, requestInfo, imp.ID, request) + if err != nil { + errors = append(errors, err) + continue + } + + httpRequests = append(httpRequests, newHttpRequest) + + break + } + + return httpRequests, errors +} + +func readGDPR(request *openrtb2.BidRequest) (bool, string) { + consentString := "" + if request.User != nil { + var extUser openrtb_ext.ExtUser + if err := json.Unmarshal(request.User.Ext, &extUser); err == nil { + consentString = extUser.Consent + } + } + gdprApplies := false + var extRegs openrtb_ext.ExtRegs + if request.Regs != nil { + if err := json.Unmarshal(request.Regs.Ext, &extRegs); err == nil { + if extRegs.GDPR != nil { + gdprApplies = (*extRegs.GDPR == 1) + } + } + } + return gdprApplies, consentString +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if responseData.StatusCode == http.StatusNoContent { + return nil, nil + } + + if responseData.StatusCode == http.StatusBadRequest { + err := &errortypes.BadInput{ + Message: "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.", + } + return nil, []error{err} + } + + if responseData.StatusCode != http.StatusOK { + err := &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode), + } + return nil, []error{err} + } + + var missenaResponse MissenaBidServerResponse + if err := json.Unmarshal(responseData.Body, &missenaResponse); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + bidResponse.Currency = missenaResponse.Currency + + responseBid := &openrtb2.Bid{ + ID: request.ID, + Price: float64(missenaResponse.Cpm), + ImpID: request.Imp[0].ID, + AdM: missenaResponse.Ad, + CrID: missenaResponse.RequestId, + } + + b := &adapters.TypedBid{ + Bid: responseBid, + BidType: openrtb_ext.BidTypeBanner, + } + + bidResponse.Bids = append(bidResponse.Bids, b) + + return bidResponse, nil +} diff --git a/adapters/missena/missena_test.go b/adapters/missena/missena_test.go new file mode 100644 index 00000000000..2b13bf085db --- /dev/null +++ b/adapters/missena/missena_test.go @@ -0,0 +1,21 @@ +package missena + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderMissena, config.Adapter{ + Endpoint: "http://example.com/"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "missenatest", bidder) +} diff --git a/adapters/missena/missenatest/exemplary/multiple-imps.json b/adapters/missena/missenatest/exemplary/multiple-imps.json new file mode 100644 index 00000000000..5b83f19ccd0 --- /dev/null +++ b/adapters/missena/missenatest/exemplary/multiple-imps.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-1", + "test": "1" + } + } + }, + { + "id": "test-imp-id-2", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-2", + "test": "1" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": "abc" + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement-1", + "test": "1" + }, + "impIDs":["test-imp-id-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id-1", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/exemplary/simple-banner-ipv6.json b/adapters/missena/missenatest/exemplary/simple-banner-ipv6.json new file mode 100644 index 00000000000..ea240f82e09 --- /dev/null +++ b/adapters/missena/missenatest/exemplary/simple-banner-ipv6.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ipv6": "2001:0000:130F:0000:0000:09C0:876A:130B", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "2001:0000:130F:0000:0000:09C0:876A:130B" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/exemplary/simple-banner.json b/adapters/missena/missenatest/exemplary/simple-banner.json new file mode 100644 index 00000000000..74ff3abfd57 --- /dev/null +++ b/adapters/missena/missenatest/exemplary/simple-banner.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/exemplary/valid-imp-error-imp.json b/adapters/missena/missenatest/exemplary/valid-imp-error-imp.json new file mode 100644 index 00000000000..61be3f78c4c --- /dev/null +++ b/adapters/missena/missenatest/exemplary/valid-imp-error-imp.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id-1", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-1", + "test": "1" + } + } + }, + { + "id": "test-imp-id-2", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement-2", + "test": "1" + } + } + }, + { + "id": "test-imp-id-3", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": "abc" + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement-1", + "test": "1" + }, + "impIDs": ["test-imp-id-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "ad": "
test ad
", + "cpm": 1.5, + "currency": "EUR", + "requestId": "test-request-id" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "test-request-id", + "impid": "test-imp-id-1", + "price": 1.5, + "adm": "
test ad
", + "crid": "test-request-id" + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/error-ext-bidder.json b/adapters/missena/missenatest/supplemental/error-ext-bidder.json new file mode 100644 index 00000000000..fdc08f4704b --- /dev/null +++ b/adapters/missena/missenatest/supplemental/error-ext-bidder.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://publisher.com/url" + }, + "user": { + "buyeruid": "1" + }, + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": "abc" + } + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing missenaExt parameters", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/error-imp-ext.json b/adapters/missena/missenatest/supplemental/error-imp-ext.json new file mode 100644 index 00000000000..3905efa6bab --- /dev/null +++ b/adapters/missena/missenatest/supplemental/error-imp-ext.json @@ -0,0 +1,23 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://publisher.com/url" + }, + "user": { + "buyeruid": "1" + }, + "imp": [ + { + "id": "test-imp-id", + "ext": "error" + } + ] + }, + "expectedMakeRequestsErrors": [ + { + "value": "Error parsing bidderExt object", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/status-204.json b/adapters/missena/missenatest/supplemental/status-204.json new file mode 100644 index 00000000000..59070ab4ecb --- /dev/null +++ b/adapters/missena/missenatest/supplemental/status-204.json @@ -0,0 +1,83 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/status-400.json b/adapters/missena/missenatest/supplemental/status-400.json new file mode 100644 index 00000000000..23a153208e3 --- /dev/null +++ b/adapters/missena/missenatest/supplemental/status-400.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 400, + "body": "Bad request from publisher." + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.", + "comparison": "literal" + } + ] + } \ No newline at end of file diff --git a/adapters/missena/missenatest/supplemental/status-not-200.json b/adapters/missena/missenatest/supplemental/status-not-200.json new file mode 100644 index 00000000000..8c913791fc3 --- /dev/null +++ b/adapters/missena/missenatest/supplemental/status-not-200.json @@ -0,0 +1,89 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "tmax": 500, + "at": 1, + "cur": [ + "EUR" + ], + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "CO-X2XiO_eyUoAsAxBFRBECsA" + } + }, + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "https://example.com/page", + "domain": "example.com" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "h": 50, + "w": 320 + }, + "ext": { + "bidder": { + "apiKey": "test-api-key", + "placement": "test-placement", + "test": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://example.com/?t=test-api-key", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ], + "User-Agent": [ + "test-user-agent" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "https://example.com/page" + ] + }, + "body": { + "request_id": "test-request-id", + "timeout": 2000, + "referer": "https://example.com/page", + "referer_canonical": "example.com", + "consent_string": "CO-X2XiO_eyUoAsAxBFRBECsA", + "consent_required": true, + "placement": "test-placement", + "test": "1" + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info.", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/missena/params_test.go b/adapters/missena/params_test.go new file mode 100644 index 00000000000..e76b80b694f --- /dev/null +++ b/adapters/missena/params_test.go @@ -0,0 +1,50 @@ +package missena + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderMissena, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderMissena, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"apiKey": "PA-123456"}`, + `{"apiKey": "PA-123456", "placement": "sticky"}`, + `{"apiKey": "PA-123456", "test": "native"}`, +} + +var invalidParams = []string{ + `{"apiKey": ""}`, + `{"apiKey": 42}`, + `{"placement": 111}`, + `{"placement": "sticky"}`, + `{"apiKey": "PA-123456", "placement": 111}`, + `{"test": "native"}`, + `{"apiKey": "PA-123456", "test": 111}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 88948460aff..7f1496810e2 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -138,6 +138,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/mgid" "github.com/prebid/prebid-server/v2/adapters/mgidX" "github.com/prebid/prebid-server/v2/adapters/minutemedia" + "github.com/prebid/prebid-server/v2/adapters/missena" "github.com/prebid/prebid-server/v2/adapters/mobfoxpb" "github.com/prebid/prebid-server/v2/adapters/mobilefuse" "github.com/prebid/prebid-server/v2/adapters/motorik" @@ -369,6 +370,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderMgid: mgid.Builder, openrtb_ext.BidderMgidX: mgidX.Builder, openrtb_ext.BidderMinuteMedia: minutemedia.Builder, + openrtb_ext.BidderMissena: missena.Builder, openrtb_ext.BidderMobfoxpb: mobfoxpb.Builder, openrtb_ext.BidderMobileFuse: mobilefuse.Builder, openrtb_ext.BidderMotorik: motorik.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 522e2490da1..f7706ce5c25 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -156,6 +156,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderMgid, BidderMgidX, BidderMinuteMedia, + BidderMissena, BidderMobfoxpb, BidderMobileFuse, BidderMotorik, @@ -484,6 +485,7 @@ const ( BidderMgid BidderName = "mgid" BidderMgidX BidderName = "mgidX" BidderMinuteMedia BidderName = "minutemedia" + BidderMissena BidderName = "missena" BidderMobfoxpb BidderName = "mobfoxpb" BidderMobileFuse BidderName = "mobilefuse" BidderMotorik BidderName = "motorik" diff --git a/openrtb_ext/imp_missena.go b/openrtb_ext/imp_missena.go new file mode 100644 index 00000000000..3e341957123 --- /dev/null +++ b/openrtb_ext/imp_missena.go @@ -0,0 +1,7 @@ +package openrtb_ext + +type ExtImpMissena struct { + ApiKey string `json:"apiKey"` + Placement string `json:"placement"` + TestMode string `json:"test"` +} diff --git a/static/bidder-info/missena.yaml b/static/bidder-info/missena.yaml new file mode 100644 index 00000000000..47f089b9c5a --- /dev/null +++ b/static/bidder-info/missena.yaml @@ -0,0 +1,16 @@ +endpoint: https://bid.missena.io/ +maintainer: + email: prebid@missena.com +gvlVendorID: 687 +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner +userSync: + iframe: + url: https://sync.missena.io/iframe?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: $UID \ No newline at end of file diff --git a/static/bidder-params/missena.json b/static/bidder-params/missena.json new file mode 100644 index 00000000000..c9e20e5a828 --- /dev/null +++ b/static/bidder-params/missena.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Missena Adapter Params", + "description": "A schema which validates params accepted by the Missena adapter", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "API Key", + "minLength": 1 + }, + "placement": { + "type": "string", + "description": "Placement Type (Sticky, Header, ...)" + }, + "test": { + "type": "string", + "description": "Test Mode" + } + }, + "required": [ + "apiKey" + ] +} \ No newline at end of file From 64584f60f0922ef9b285089469d667424e33d395 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:52:25 +0000 Subject: [PATCH 22/47] Refactor: Remove getAuctionBidderRequests (#3957) --- exchange/utils.go | 147 ++++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 78 deletions(-) diff --git a/exchange/utils.go b/exchange/utils.go index f56a88151d9..0ba1b3e831d 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -81,9 +81,76 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, return } - var allBidderRequests []BidderRequest + allBidderRequests := make([]BidderRequest, 0, len(impsByBidder)) var allBidderRequestErrs []error - allBidderRequests, allBidderRequestErrs = getAuctionBidderRequests(auctionReq, requestExt, rs.bidderToSyncerKey, impsByBidder, requestAliases, rs.hostSChainNode) + explicitBuyerUIDs, err := extractBuyerUIDs(req.BidRequest.User) + if err != nil { + errs = []error{err} + return + } + + bidderParamsInReqExt, err := ExtractReqExtBidderParamsMap(req.BidRequest) + if err != nil { + errs = []error{err} + return + } + + sChainWriter, err := schain.NewSChainWriter(requestExt, rs.hostSChainNode) + if err != nil { + errs = []error{err} + return + } + + lowerCaseExplicitBuyerUIDs := make(map[string]string) + for bidder, uid := range explicitBuyerUIDs { + lowerKey := strings.ToLower(bidder) + lowerCaseExplicitBuyerUIDs[lowerKey] = uid + } + + for bidder, imps := range impsByBidder { + coreBidder, isRequestAlias := resolveBidder(bidder, requestAliases) + + reqCopy := *req.BidRequest + reqCopy.Imp = imps + + sChainWriter.Write(&reqCopy, bidder) + + reqCopy.Ext, err = buildRequestExtForBidder(bidder, req.BidRequest.Ext, requestExt, bidderParamsInReqExt, auctionReq.Account.AlternateBidderCodes) + if err != nil { + errs = append(errs, err) + continue + } + + if err := removeUnpermissionedEids(&reqCopy, bidder, requestExt); err != nil { + errs = append(errs, fmt.Errorf("unable to enforce request.ext.prebid.data.eidpermissions because %v", err)) + continue + } + + bidderRequest := BidderRequest{ + BidderName: openrtb_ext.BidderName(bidder), + BidderCoreName: coreBidder, + IsRequestAlias: isRequestAlias, + BidRequest: &reqCopy, + BidderLabels: metrics.AdapterLabels{ + Source: auctionReq.LegacyLabels.Source, + RType: auctionReq.LegacyLabels.RType, + Adapter: coreBidder, + PubID: auctionReq.LegacyLabels.PubID, + CookieFlag: auctionReq.LegacyLabels.CookieFlag, + AdapterBids: metrics.AdapterBidPresent, + }, + } + + syncerKey := rs.bidderToSyncerKey[string(coreBidder)] + if hadSync := prepareUser(&reqCopy, bidder, syncerKey, lowerCaseExplicitBuyerUIDs, auctionReq.UserSyncs); !hadSync && req.BidRequest.App == nil { + bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagNo + } else { + bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagYes + } + + allBidderRequests = append(allBidderRequests, bidderRequest) + } + if allBidderRequestErrs != nil { errs = append(errs, allBidderRequestErrs...) } @@ -322,82 +389,6 @@ func ExtractReqExtBidderParamsMap(bidRequest *openrtb2.BidRequest) (map[string]j return bidderParams, nil } -func getAuctionBidderRequests(auctionRequest AuctionRequest, - requestExt *openrtb_ext.ExtRequest, - bidderToSyncerKey map[string]string, - impsByBidder map[string][]openrtb2.Imp, - requestAliases map[string]string, - hostSChainNode *openrtb2.SupplyChainNode) ([]BidderRequest, []error) { - - bidderRequests := make([]BidderRequest, 0, len(impsByBidder)) - req := auctionRequest.BidRequestWrapper - explicitBuyerUIDs, err := extractBuyerUIDs(req.BidRequest.User) - if err != nil { - return nil, []error{err} - } - - bidderParamsInReqExt, err := ExtractReqExtBidderParamsMap(req.BidRequest) - if err != nil { - return nil, []error{err} - } - - sChainWriter, err := schain.NewSChainWriter(requestExt, hostSChainNode) - if err != nil { - return nil, []error{err} - } - - lowerCaseExplicitBuyerUIDs := make(map[string]string) - for bidder, uid := range explicitBuyerUIDs { - lowerKey := strings.ToLower(bidder) - lowerCaseExplicitBuyerUIDs[lowerKey] = uid - } - - var errs []error - for bidder, imps := range impsByBidder { - coreBidder, isRequestAlias := resolveBidder(bidder, requestAliases) - - reqCopy := *req.BidRequest - reqCopy.Imp = imps - - sChainWriter.Write(&reqCopy, bidder) - - reqCopy.Ext, err = buildRequestExtForBidder(bidder, req.BidRequest.Ext, requestExt, bidderParamsInReqExt, auctionRequest.Account.AlternateBidderCodes) - if err != nil { - return nil, []error{err} - } - - if err := removeUnpermissionedEids(&reqCopy, bidder, requestExt); err != nil { - errs = append(errs, fmt.Errorf("unable to enforce request.ext.prebid.data.eidpermissions because %v", err)) - continue - } - - bidderRequest := BidderRequest{ - BidderName: openrtb_ext.BidderName(bidder), - BidderCoreName: coreBidder, - IsRequestAlias: isRequestAlias, - BidRequest: &reqCopy, - BidderLabels: metrics.AdapterLabels{ - Source: auctionRequest.LegacyLabels.Source, - RType: auctionRequest.LegacyLabels.RType, - Adapter: coreBidder, - PubID: auctionRequest.LegacyLabels.PubID, - CookieFlag: auctionRequest.LegacyLabels.CookieFlag, - AdapterBids: metrics.AdapterBidPresent, - }, - } - - syncerKey := bidderToSyncerKey[string(coreBidder)] - if hadSync := prepareUser(&reqCopy, bidder, syncerKey, lowerCaseExplicitBuyerUIDs, auctionRequest.UserSyncs); !hadSync && req.BidRequest.App == nil { - bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagNo - } else { - bidderRequest.BidderLabels.CookieFlag = metrics.CookieFlagYes - } - - bidderRequests = append(bidderRequests, bidderRequest) - } - return bidderRequests, errs -} - func buildRequestExtForBidder(bidder string, requestExt json.RawMessage, requestExtParsed *openrtb_ext.ExtRequest, bidderParamsInReqExt map[string]json.RawMessage, cfgABC *openrtb_ext.ExtAlternateBidderCodes) (json.RawMessage, error) { // Resolve alternatebiddercode for current bidder var reqABC *openrtb_ext.ExtAlternateBidderCodes From 87d4412b6c8e01a011c8b258274a87c37c7760bc Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:08:14 +0000 Subject: [PATCH 23/47] Refactor: Move privacy init up (#3958) --- exchange/utils.go | 110 +++++++++++++++++++++++----------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/exchange/utils.go b/exchange/utils.go index 0ba1b3e831d..2805f32a08a 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -81,13 +81,16 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, return } - allBidderRequests := make([]BidderRequest, 0, len(impsByBidder)) - var allBidderRequestErrs []error explicitBuyerUIDs, err := extractBuyerUIDs(req.BidRequest.User) if err != nil { errs = []error{err} return } + lowerCaseExplicitBuyerUIDs := make(map[string]string) + for bidder, uid := range explicitBuyerUIDs { + lowerKey := strings.ToLower(bidder) + lowerCaseExplicitBuyerUIDs[lowerKey] = uid + } bidderParamsInReqExt, err := ExtractReqExtBidderParamsMap(req.BidRequest) if err != nil { @@ -101,12 +104,58 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, return } - lowerCaseExplicitBuyerUIDs := make(map[string]string) - for bidder, uid := range explicitBuyerUIDs { - lowerKey := strings.ToLower(bidder) - lowerCaseExplicitBuyerUIDs[lowerKey] = uid + var gpp gpplib.GppContainer + if req.BidRequest.Regs != nil && len(req.BidRequest.Regs.GPP) > 0 { + var gppErrs []error + gpp, gppErrs = gpplib.Parse(req.BidRequest.Regs.GPP) + if len(gppErrs) > 0 { + errs = append(errs, gppErrs[0]) + } + } + + consent, err := getConsent(req, gpp) + if err != nil { + errs = append(errs, err) + } + + ccpaEnforcer, err := extractCCPA(req.BidRequest, rs.privacyConfig, &auctionReq.Account, requestAliases, channelTypeMap[auctionReq.LegacyLabels.RType], gpp) + if err != nil { + errs = append(errs, err) + } + + lmtEnforcer := extractLMT(req.BidRequest, rs.privacyConfig) + + // request level privacy policies + coppa := req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1 + lmt := lmtEnforcer.ShouldEnforce(unknownBidder) + + privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce() + privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder) + privacyLabels.COPPAEnforced = coppa + privacyLabels.LMTEnforced = lmt + + var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{} + + if gdprEnforced { + privacyLabels.GDPREnforced = true + parsedConsent, err := vendorconsent.ParseString(consent) + if err == nil { + version := int(parsedConsent.Version()) + privacyLabels.GDPRTCFVersion = metrics.TCFVersionToValue(version) + } + + gdprRequestInfo := gdpr.RequestInfo{ + AliasGVLIDs: requestAliasesGVLIDs, + Consent: consent, + GDPRSignal: gdprSignal, + PublisherID: auctionReq.LegacyLabels.PubID, + } + gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo) } + allBidderRequests := make([]BidderRequest, 0, len(impsByBidder)) + var allBidderRequestErrs []error + for bidder, imps := range impsByBidder { coreBidder, isRequestAlias := resolveBidder(bidder, requestAliases) @@ -159,60 +208,11 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, //this function should be executed after getAuctionBidderRequests allBidderRequests = mergeBidderRequests(allBidderRequests, bidderNameToBidderReq) - var gpp gpplib.GppContainer - if req.BidRequest.Regs != nil && len(req.BidRequest.Regs.GPP) > 0 { - var gppErrs []error - gpp, gppErrs = gpplib.Parse(req.BidRequest.Regs.GPP) - if len(gppErrs) > 0 { - errs = append(errs, gppErrs[0]) - } - } - if auctionReq.Account.PriceFloors.IsAdjustForBidAdjustmentEnabled() { //Apply BidAdjustmentFactor to imp.BidFloor applyBidAdjustmentToFloor(allBidderRequests, bidAdjustmentFactors) } - consent, err := getConsent(req, gpp) - if err != nil { - errs = append(errs, err) - } - - ccpaEnforcer, err := extractCCPA(req.BidRequest, rs.privacyConfig, &auctionReq.Account, requestAliases, channelTypeMap[auctionReq.LegacyLabels.RType], gpp) - if err != nil { - errs = append(errs, err) - } - - lmtEnforcer := extractLMT(req.BidRequest, rs.privacyConfig) - - // request level privacy policies - coppa := req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1 - lmt := lmtEnforcer.ShouldEnforce(unknownBidder) - - privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce() - privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder) - privacyLabels.COPPAEnforced = coppa - privacyLabels.LMTEnforced = lmt - - var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{} - - if gdprEnforced { - privacyLabels.GDPREnforced = true - parsedConsent, err := vendorconsent.ParseString(consent) - if err == nil { - version := int(parsedConsent.Version()) - privacyLabels.GDPRTCFVersion = metrics.TCFVersionToValue(version) - } - - gdprRequestInfo := gdpr.RequestInfo{ - AliasGVLIDs: requestAliasesGVLIDs, - Consent: consent, - GDPRSignal: gdprSignal, - PublisherID: auctionReq.LegacyLabels.PubID, - } - gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo) - } - allowedBidderRequests = make([]BidderRequest, 0, len(allBidderRequests)) for _, bidderRequest := range allBidderRequests { From 9bb9b3db2135b0085e8b1f3608eb1da73871e6b7 Mon Sep 17 00:00:00 2001 From: bhainesnexxen <146981622+bhainesnexxen@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:09:33 -0700 Subject: [PATCH 24/47] Unruly: Indicate Support for OpenRTB 2.6 (#3984) --- static/bidder-info/unruly.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/bidder-info/unruly.yaml b/static/bidder-info/unruly.yaml index 6389f831a64..62f03dec84d 100644 --- a/static/bidder-info/unruly.yaml +++ b/static/bidder-info/unruly.yaml @@ -1,7 +1,13 @@ endpoint: "https://targeting.unrulymedia.com/unruly_prebid_server" +endpointCompression: gzip +geoscope: + - global maintainer: email: "prebidsupport@unrulygroup.com" gvlVendorID: 36 +openrtb: + version: 2.6 + gpp-supported: true capabilities: app: mediaTypes: @@ -14,4 +20,4 @@ capabilities: userSync: redirect: url: "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}" - userMacro: "[RX_UUID]" \ No newline at end of file + userMacro: "[RX_UUID]" From c37951a3be2494623bffbed352137eed98bf7b1b Mon Sep 17 00:00:00 2001 From: ym-winston <46379634+ym-winston@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:18:54 -0400 Subject: [PATCH 25/47] update yieldmo.yaml to indicate support for ortb 2.6 (#3968) --- static/bidder-info/yieldmo.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/bidder-info/yieldmo.yaml b/static/bidder-info/yieldmo.yaml index 02ab8721a16..17b25ecbbf1 100644 --- a/static/bidder-info/yieldmo.yaml +++ b/static/bidder-info/yieldmo.yaml @@ -2,6 +2,8 @@ endpoint: "https://ads.yieldmo.com/exchange/prebid-server" maintainer: email: "prebid@yieldmo.com" gvlVendorID: 173 +openrtb: + version: 2.6 capabilities: app: mediaTypes: From 5fcbbbfb17d4029bf60f03c7919490dfcdd98874 Mon Sep 17 00:00:00 2001 From: Patrick Loughrey Date: Tue, 15 Oct 2024 15:19:47 -0400 Subject: [PATCH 26/47] Triplelift: Adding flag for 2.6 support (#3966) --- static/bidder-info/triplelift.yaml | 8 +++++--- static/bidder-info/triplelift_native.yaml | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/static/bidder-info/triplelift.yaml b/static/bidder-info/triplelift.yaml index 605bcc71e6e..79a8951680f 100644 --- a/static/bidder-info/triplelift.yaml +++ b/static/bidder-info/triplelift.yaml @@ -12,9 +12,10 @@ capabilities: - banner - video userSync: - # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. - # Contact this bidder directly at the email address above to ask about enabling user sync. - # + # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. + # If you are a publisher hosting your own Prebid Server, contact this bidder directly at the email address above to ask about enabling user sync. + # If you are a Prebid Server Host, please have your publisher contact the bidder. + # iframe: url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: $UID @@ -23,4 +24,5 @@ userSync: userMacro: "$UID" endpointCompression: "GZIP" openrtb: + version: 2.6 gpp-supported: true \ No newline at end of file diff --git a/static/bidder-info/triplelift_native.yaml b/static/bidder-info/triplelift_native.yaml index ff93b544c4c..c5e08152692 100644 --- a/static/bidder-info/triplelift_native.yaml +++ b/static/bidder-info/triplelift_native.yaml @@ -12,8 +12,9 @@ capabilities: mediaTypes: - native userSync: - # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. - # Contact this bidder directly at the email address above to ask about enabling user sync. + # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. + # If you are a publisher hosting your own Prebid Server, contact this bidder directly at the email address above to ask about enabling user sync. + # If you are a Prebid Server Host, please have your publisher contact the bidder. # iframe: url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" @@ -22,4 +23,5 @@ userSync: url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" userMacro: "$UID" openrtb: + version: 2.6 gpp-supported: true \ No newline at end of file From cbe987664b8b36a63620ac9fd88ef3a4731c1892 Mon Sep 17 00:00:00 2001 From: Irakli Gotsiridze Date: Tue, 15 Oct 2024 23:30:17 +0400 Subject: [PATCH 27/47] enable gzip (#3946) --- static/bidder-info/sovrn.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/static/bidder-info/sovrn.yaml b/static/bidder-info/sovrn.yaml index 62d74152b0b..fad1850d4b3 100644 --- a/static/bidder-info/sovrn.yaml +++ b/static/bidder-info/sovrn.yaml @@ -1,6 +1,7 @@ endpoint: "http://pbs.lijit.com/rtb/bid?src=prebid_server" maintainer: email: "sovrnoss@sovrn.com" +endpointCompression: gzip gvlVendorID: 13 modifyingVastXmlAllowed: true capabilities: From b56923c28d20f05cc0b8d4d0a340233b4ed03840 Mon Sep 17 00:00:00 2001 From: Piotr Jaworski <109736938+piotrj-rtbh@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:49:39 +0200 Subject: [PATCH 28/47] RTB House: Resolve AUCTION_PRICE macro (#3901) --- adapters/rtbhouse/rtbhouse.go | 12 ++- .../exemplary/banner-resolve-macros.json | 87 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 adapters/rtbhouse/rtbhousetest/exemplary/banner-resolve-macros.json diff --git a/adapters/rtbhouse/rtbhouse.go b/adapters/rtbhouse/rtbhouse.go index bd0d892b996..daca87bc961 100644 --- a/adapters/rtbhouse/rtbhouse.go +++ b/adapters/rtbhouse/rtbhouse.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "github.com/buger/jsonparser" @@ -161,8 +162,9 @@ func (adapter *RTBHouseAdapter) MakeBids( var typedBid *adapters.TypedBid for _, seatBid := range openRTBBidderResponse.SeatBid { for _, bid := range seatBid.Bid { - bid := bid // pin! -> https://github.com/kyoh86/scopelint#whats-this + bid := bid bidType, err := getMediaTypeForBid(bid) + resolveMacros(&bid) if err != nil { errs = append(errs, err) continue @@ -222,3 +224,11 @@ func getNativeAdm(adm string) (string, error) { return adm, nil } + +func resolveMacros(bid *openrtb2.Bid) { + if bid != nil { + price := strconv.FormatFloat(bid.Price, 'f', -1, 64) + bid.NURL = strings.Replace(bid.NURL, "${AUCTION_PRICE}", price, -1) + bid.AdM = strings.Replace(bid.AdM, "${AUCTION_PRICE}", price, -1) + } +} diff --git a/adapters/rtbhouse/rtbhousetest/exemplary/banner-resolve-macros.json b/adapters/rtbhouse/rtbhousetest/exemplary/banner-resolve-macros.json new file mode 100644 index 00000000000..eaa3965040e --- /dev/null +++ b/adapters/rtbhouse/rtbhousetest/exemplary/banner-resolve-macros.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": {} + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://localhost/prebid_server", + "body": { + "id": "test-request-id", + "cur": ["USD"], + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": {} + } + }] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "seat": "rtbhouse", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300, + "mtype": 1 + }] + }], + "cur": "USD" + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + }] + }] +} From 8134328c03de3b8d4618c027dd6caf1c063a00a6 Mon Sep 17 00:00:00 2001 From: Boris Yu Date: Wed, 16 Oct 2024 21:08:50 +0300 Subject: [PATCH 29/47] Displayio: Make imp.bidfloor optional (#3959) --- adapters/displayio/displayio.go | 15 ++------ .../supplemental/missing-bidfloor.json | 35 ------------------- 2 files changed, 3 insertions(+), 47 deletions(-) delete mode 100644 adapters/displayio/displayiotest/supplemental/missing-bidfloor.json diff --git a/adapters/displayio/displayio.go b/adapters/displayio/displayio.go index a3af7c9614f..b54998553b3 100644 --- a/adapters/displayio/displayio.go +++ b/adapters/displayio/displayio.go @@ -37,18 +37,9 @@ func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo * for _, impression := range request.Imp { var requestExt map[string]interface{} - if impression.BidFloor == 0 { - errs = append(errs, &errortypes.BadInput{ - Message: "BidFloor should be defined", - }) - continue - } - - if impression.BidFloorCur == "" { + if impression.BidFloorCur == "" || impression.BidFloor == 0 { impression.BidFloorCur = "USD" - } - - if impression.BidFloorCur != "USD" { + } else if impression.BidFloorCur != "USD" { convertedValue, err := requestInfo.ConvertCurrency(impression.BidFloor, impression.BidFloorCur, "USD") if err != nil { @@ -56,8 +47,8 @@ func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo * continue } - impression.BidFloorCur = "USD" impression.BidFloor = convertedValue + impression.BidFloorCur = "USD" } if len(impression.Ext) == 0 { diff --git a/adapters/displayio/displayiotest/supplemental/missing-bidfloor.json b/adapters/displayio/displayiotest/supplemental/missing-bidfloor.json deleted file mode 100644 index d3fe15778ff..00000000000 --- a/adapters/displayio/displayiotest/supplemental/missing-bidfloor.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "mockBidRequest": { - "id": "testid", - "imp": [ - { - "id": "testimpid", - "banner": { - "format": [ - { - "w": 300, - "h": 250 - }, - { - "w": 320, - "h": 50 - } - ] - }, - "ext": { - "bidder": { - "placementId": "1101", - "inventoryId": "1101", - "publisherId": "101" - } - } - } - ] - }, - "expectedMakeRequestsErrors": [ - { - "value": "BidFloor should be defined", - "comparison": "literal" - } - ] -} \ No newline at end of file From 451bc449e09be5aa5310e865bc3904fa92ad3474 Mon Sep 17 00:00:00 2001 From: Bluesea <129151981+blueseasx@users.noreply.github.com> Date: Thu, 17 Oct 2024 03:12:50 +0800 Subject: [PATCH 30/47] BlueSea: Add site capability (#3910) Co-authored-by: prebid-bluesea --- .../blueseatest/exemplary/site-banner.json | 133 ++++++++++++++ .../blueseatest/exemplary/site-native.json | 133 ++++++++++++++ .../blueseatest/exemplary/site-video.json | 163 ++++++++++++++++++ static/bidder-info/bluesea.yaml | 6 + 4 files changed, 435 insertions(+) create mode 100644 adapters/bluesea/blueseatest/exemplary/site-banner.json create mode 100644 adapters/bluesea/blueseatest/exemplary/site-native.json create mode 100644 adapters/bluesea/blueseatest/exemplary/site-video.json diff --git a/adapters/bluesea/blueseatest/exemplary/site-banner.json b/adapters/bluesea/blueseatest/exemplary/site-banner.json new file mode 100644 index 00000000000..b3ca13f8d53 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/site-banner.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id": "100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id":"100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "banner":{ + "w":300, + "h":250 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "impIDs":["1"] + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":300, + "ext":{ + "mediatype":"banner" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":300, + "ext":{ + "mediatype":"banner" + } + }, + "type":"banner" + } + ] + } + ] +} diff --git a/adapters/bluesea/blueseatest/exemplary/site-native.json b/adapters/bluesea/blueseatest/exemplary/site-native.json new file mode 100644 index 00000000000..b907ebdbea4 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/site-native.json @@ -0,0 +1,133 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id": "100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "native":{ + "request":"{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":150}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":100,\"hmin\":100}},{\"id\":3,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver":"1.2" + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id":"100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "native":{ + "request":"{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":150}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":100,\"hmin\":100}},{\"id\":3,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}", + "ver":"1.2" + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "impIDs":["1"] + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-native-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-native-crid", + "w":300, + "ext":{ + "mediatype":"native" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-native-adm", + "impid":"1", + "id":"test-bid-id", + "h":250, + "adomain":[ + "adv.com" + ], + "crid":"test-site-native-crid", + "w":300, + "ext":{ + "mediatype":"native" + } + }, + "type":"native" + } + ] + } + ] +} diff --git a/adapters/bluesea/blueseatest/exemplary/site-video.json b/adapters/bluesea/blueseatest/exemplary/site-video.json new file mode 100644 index 00000000000..dd191105cd3 --- /dev/null +++ b/adapters/bluesea/blueseatest/exemplary/site-video.json @@ -0,0 +1,163 @@ +{ + "mockBidRequest":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id": "100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "video":{ + "mimes":[ + "video/mp4", + "application/javascript", + "video/webm" + ], + "minduration":5, + "maxduration":120, + "protocols":[ + 2, + 3, + 5, + 6 + ], + "pos":7, + "w":320, + "h":480, + "linearity":1 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "httpCalls":[ + { + "expectedRequest":{ + "uri":"https://test.prebid.bluesea?pubid=test-pubid&token=test-pub-token", + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "site":{ + "id":"100", + "domain": "test.domain", + "page": "https://test.domain?target=_blank", + "keywords": "fashion" + }, + "imp":[ + { + "id":"1", + "video":{ + "mimes":[ + "video/mp4", + "application/javascript", + "video/webm" + ], + "minduration":5, + "maxduration":120, + "protocols":[ + 2, + 3, + 5, + 6 + ], + "pos":7, + "w":320, + "h":480, + "linearity":1 + }, + "secure":1, + "ext":{ + "bidder":{ + "pubid":"test-pubid", + "token":"test-pub-token" + } + } + } + ], + "device":{ + "os":"android", + "ua":"Mozilla/5.0 (Linux; Android 8.0.0; SC-04J Build/R16NW; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/113.0.5672.162 Mobile Safari/537.36", + "ip":"101.101.101.101", + "h":1280, + "w":720 + }, + "at":1, + "tmax":1200, + "test":1 + }, + "impIDs":["1"] + }, + "mockResponse":{ + "status":200, + "body":{ + "id":"b655d86c-fdf6-4e68-a1e9-abc223f84a65", + "seatbid":[ + { + "bid":[ + { + "price":0.01, + "adm":"test-vast", + "impid":"1", + "id":"test-bid-id", + "h":480, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":320, + "ext":{ + "mediatype":"video" + } + } + ], + "seat":"test-seat" + } + ], + "cur":"USD" + } + } + } + ], + "expectedBidResponses":[ + { + "bids":[ + { + "bid":{ + "price":0.01, + "adm":"test-vast", + "impid":"1", + "id":"test-bid-id", + "h":480, + "adomain":[ + "adv.com" + ], + "crid":"test-site-crid", + "w":320, + "ext":{ + "mediatype":"video" + } + }, + "type":"video" + } + ] + } + ] +} diff --git a/static/bidder-info/bluesea.yaml b/static/bidder-info/bluesea.yaml index 14667cafd6e..a9a5ca203d6 100644 --- a/static/bidder-info/bluesea.yaml +++ b/static/bidder-info/bluesea.yaml @@ -3,9 +3,15 @@ maintainer: email: prebid@blueseasx.com endpointCompression: gzip modifyingVastXmlAllowed: true +gvlVendorID: 1294 capabilities: app: mediaTypes: - banner - native - video + site: + mediaTypes: + - banner + - native + - video From bcf6491f94c685fefc22f222e96c65a5cdd38344 Mon Sep 17 00:00:00 2001 From: sindhuja-sridharan <148382298+sindhuja-sridharan@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:20:45 -0600 Subject: [PATCH 31/47] GumGum: Declare ORTB 2.6 support (#3985) --- static/bidder-info/gumgum.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/bidder-info/gumgum.yaml b/static/bidder-info/gumgum.yaml index f7e782e40df..945acf7ca5c 100644 --- a/static/bidder-info/gumgum.yaml +++ b/static/bidder-info/gumgum.yaml @@ -12,3 +12,6 @@ userSync: url: "https://rtb.gumgum.com/usync/prbds2s?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}" userMacro: "" # gumgum appends the user id to end of the redirect url and does not utilize a macro +openrtb: + version: 2.6 + gpp-supported: true \ No newline at end of file From 18f679834a60cdb243afd3b7a17fd10dc020a53f Mon Sep 17 00:00:00 2001 From: Sebastien Boisvert Date: Tue, 22 Oct 2024 15:15:07 -0400 Subject: [PATCH 32/47] Bump Go version to 1.22 in dev containers config (#3983) --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ebf6f614df3..f9bb82598ff 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "dockerfile": "Dockerfile", "args": { // Update the VARIANT arg to pick a version of Go - "VARIANT": "1.20", + "VARIANT": "1.22", // Options "INSTALL_NODE": "false", "NODE_VERSION": "lts/*" From 03a4abdf04b3176fcb394ca173fedd1b6beba0ff Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Tue, 22 Oct 2024 22:26:45 +0300 Subject: [PATCH 33/47] Sample: Fix prebid js loading bug on sample html page (#3792) --- sample/001_banner/pbjs.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sample/001_banner/pbjs.html b/sample/001_banner/pbjs.html index b02ba95b995..af6ad643eec 100644 --- a/sample/001_banner/pbjs.html +++ b/sample/001_banner/pbjs.html @@ -12,8 +12,8 @@ border-style: solid; } - - + -