From 4f177cae3b1ca96a9e9ed5145610eb6ab9ada42e Mon Sep 17 00:00:00 2001 From: benben2001 <145416009+benben2001@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:07:38 +0800 Subject: [PATCH 01/15] New Adapter: MeloZen (#3784) --- adapters/melozen/melozen.go | 185 ++++++++++++++ adapters/melozen/melozen_test.go | 30 +++ .../melozentest/exemplary/app-banner.json | 132 ++++++++++ .../melozentest/exemplary/app-native.json | 100 ++++++++ .../melozentest/exemplary/app-video.json | 137 ++++++++++ .../melozentest/exemplary/multi-imps.json | 239 ++++++++++++++++++ .../melozentest/exemplary/web-banner.json | 138 ++++++++++ .../melozentest/exemplary/web-video.json | 129 ++++++++++ .../supplemental/bad-media-type-request.json | 28 ++ .../melozentest/supplemental/no-fill.json | 90 +++++++ .../supplemental/response-status-400.json | 95 +++++++ .../supplemental/response-status-not-200.json | 84 ++++++ .../supplemental/wrong-bid-ext.json | 85 +++++++ adapters/melozen/params_test.go | 50 ++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_melozen.go | 5 + static/bidder-info/melozen.yaml | 19 ++ static/bidder-params/melozen.json | 14 + 19 files changed, 1564 insertions(+) create mode 100644 adapters/melozen/melozen.go create mode 100644 adapters/melozen/melozen_test.go create mode 100644 adapters/melozen/melozentest/exemplary/app-banner.json create mode 100644 adapters/melozen/melozentest/exemplary/app-native.json create mode 100644 adapters/melozen/melozentest/exemplary/app-video.json create mode 100644 adapters/melozen/melozentest/exemplary/multi-imps.json create mode 100644 adapters/melozen/melozentest/exemplary/web-banner.json create mode 100644 adapters/melozen/melozentest/exemplary/web-video.json create mode 100644 adapters/melozen/melozentest/supplemental/bad-media-type-request.json create mode 100644 adapters/melozen/melozentest/supplemental/no-fill.json create mode 100644 adapters/melozen/melozentest/supplemental/response-status-400.json create mode 100644 adapters/melozen/melozentest/supplemental/response-status-not-200.json create mode 100644 adapters/melozen/melozentest/supplemental/wrong-bid-ext.json create mode 100644 adapters/melozen/params_test.go create mode 100644 openrtb_ext/imp_melozen.go create mode 100644 static/bidder-info/melozen.yaml create mode 100644 static/bidder-params/melozen.json diff --git a/adapters/melozen/melozen.go b/adapters/melozen/melozen.go new file mode 100644 index 00000000000..cb76274865b --- /dev/null +++ b/adapters/melozen/melozen.go @@ -0,0 +1,185 @@ +package melozen + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "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 +} + +// Builder builds a new instance of the MeloZen adapter for the given bidder with the given config. +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, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errors []error + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestCopy := *request + for _, imp := range request.Imp { + // Extract Melozen Params + var strImpExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &strImpExt); err != nil { + errors = append(errors, err) + continue + } + var strImpParams openrtb_ext.ImpExtMeloZen + if err := json.Unmarshal(strImpExt.Bidder, &strImpParams); err != nil { + errors = append(errors, err) + continue + } + + url, err := macros.ResolveMacros(a.endpointTemplate, macros.EndpointTemplateParams{PublisherID: strImpParams.PubId}) + if err != nil { + errors = append(errors, err) + continue + } + // Convert Floor into USD + if imp.BidFloor > 0 && imp.BidFloorCur != "" && !strings.EqualFold(imp.BidFloorCur, "USD") { + convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD") + if err != nil { + errors = append(errors, err) + continue + } + imp.BidFloorCur = "USD" + imp.BidFloor = convertedValue + } + + impressionsByMediaType, err := splitImpressionsByMediaType(&imp) + if err != nil { + errors = append(errors, err) + continue + } + + for _, impression := range impressionsByMediaType { + requestCopy.Imp = []openrtb2.Imp{impression} + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errors = append(errors, err) + continue + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: requestJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(requestCopy.Imp), + } + requests = append(requests, requestData) + } + } + + return requests, errors +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(response) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { + return nil, []error{err} + } + + var bidReq openrtb2.BidRequest + if err := json.Unmarshal(requestData.Body, &bidReq); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{err} + } + + bidderResponse := adapters.NewBidderResponse() + var errors []error + for _, seatBid := range bidResp.SeatBid { + for i := range seatBid.Bid { + bid := &seatBid.Bid[i] + bidType, err := getMediaTypeForBid(*bid) + if err != nil { + errors = append(errors, err) + continue + } + + bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ + BidType: bidType, + Bid: bid, + }) + } + } + return bidderResponse, errors +} + +func splitImpressionsByMediaType(impression *openrtb2.Imp) ([]openrtb2.Imp, error) { + if impression.Banner == nil && impression.Native == nil && impression.Video == nil { + return nil, &errortypes.BadInput{Message: "Invalid MediaType. MeloZen only supports Banner, Video and Native."} + } + + impressions := make([]openrtb2.Imp, 0, 2) + + if impression.Banner != nil { + impCopy := *impression + impCopy.Video = nil + impCopy.Native = nil + impressions = append(impressions, impCopy) + } + + if impression.Video != nil { + impCopy := *impression + impCopy.Banner = nil + impCopy.Native = nil + impressions = append(impressions, impCopy) + } + + if impression.Native != nil { + impCopy := *impression + impCopy.Banner = nil + impCopy.Video = nil + impressions = append(impressions, impCopy) + } + + return impressions, nil +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse bid mediatype for impression \"%s\"", bid.ImpID), + } +} diff --git a/adapters/melozen/melozen_test.go b/adapters/melozen/melozen_test.go new file mode 100644 index 00000000000..0191ab73182 --- /dev/null +++ b/adapters/melozen/melozen_test.go @@ -0,0 +1,30 @@ +package melozen + +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.BidderMeloZen, config.Adapter{ + Endpoint: "https://example.com/rtb/v2/bid?publisher_id={{.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, "melozentest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderMeloZen, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/melozen/melozentest/exemplary/app-banner.json b/adapters/melozen/melozentest/exemplary/app-banner.json new file mode 100644 index 00000000000..6cdb82bf5ad --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/app-banner.json @@ -0,0 +1,132 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "device": { + "w": 1200, + "h": 900 + }, + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + } + }, + "impIDs":["banner-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "banner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "banner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/app-native.json b/adapters/melozen/melozentest/exemplary/app-native.json new file mode 100644 index 00000000000..f93abd44bea --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/app-native.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "web-native", + "imp": [ + { + "id": "native-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-native", + "imp": [ + { + "id": "native-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "native": { + "ver": "1.2", + "request": "placeholder request" + } + } + ] + }, + "impIDs":["native-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-native", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-native", + "impid": "native-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-native", + "impid": "native-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/app-video.json b/adapters/melozen/melozentest/exemplary/app-video.json new file mode 100644 index 00000000000..3d913c43e44 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/app-video.json @@ -0,0 +1,137 @@ +{ + "mockBidRequest": { + "id": "app-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "app-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs": [ + "video-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "app-video", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "app-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "app-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/multi-imps.json b/adapters/melozen/melozentest/exemplary/multi-imps.json new file mode 100644 index 00000000000..916c74cb685 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/multi-imps.json @@ -0,0 +1,239 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id-1", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + }, + { + "id": "banner-imp-id-2", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id-1", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs":["banner-imp-id-1"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "banner-imp-id-1", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id-2", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs":["banner-imp-id-2"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "banner-imp-id-2", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "banner-imp-id-1", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "banner-imp-id-2", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 600, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/melozen/melozentest/exemplary/web-banner.json b/adapters/melozen/melozentest/exemplary/web-banner.json new file mode 100644 index 00000000000..0439baa1033 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/web-banner.json @@ -0,0 +1,138 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "baner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "test": 0, + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "baner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs": [ + "baner-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-banner", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-banner", + "impid": "baner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-banner", + "impid": "baner-imp-id", + "crid": "some-creative-id", + "adm": "
Ad
", + "price": 20, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/exemplary/web-video.json b/adapters/melozen/melozentest/exemplary/web-video.json new file mode 100644 index 00000000000..b4f179bdc55 --- /dev/null +++ b/adapters/melozen/melozentest/exemplary/web-video.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "web-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": ["video/mp4"], + "placement": 1 + } + } + ], + "test": 0, + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "web-video", + "tmax": 3000, + "imp": [ + { + "id": "video-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "video": { + "w": 640, + "h": 480, + "mimes": ["video/mp4"], + "placement": 1 + } + } + ], + "site": { + "publisher": { + "id": "1" + }, + "page": "https://some-site.com", + "ref": "https://some-site.com" + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "impIDs":["video-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "web-video", + "cur": "USD", + "seatbid": [ + { + "bid": [ + { + "id": "web-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "web-video", + "impid": "video-imp-id", + "crid": "some-creative-id", + "adm": "TAG", + "price": 20, + "w": 640, + "h": 480, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/bad-media-type-request.json b/adapters/melozen/melozentest/supplemental/bad-media-type-request.json new file mode 100644 index 00000000000..f6c17a70b8f --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/bad-media-type-request.json @@ -0,0 +1,28 @@ +{ + "mockBidRequest": { + "id": "unsupported-request", + "imp": [ + { + "id": "unsupported-imp", + "unupported": { + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ], + "site": { + "id": "siteID" + } + }, + + "expectedMakeRequestsErrors": [ + { + "value": "Invalid MediaType. MeloZen only supports Banner, Video and Native.", + "comparison": "literal" + } + ] + } + \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/no-fill.json b/adapters/melozen/melozentest/supplemental/no-fill.json new file mode 100644 index 00000000000..7dd600a72b6 --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/no-fill.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "device": { + "w": 1200, + "h": 900 + }, + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + } + }, + "impIDs": [ + "banner-imp-id" + ] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/response-status-400.json b/adapters/melozen/melozentest/supplemental/response-status-400.json new file mode 100644 index 00000000000..969875b86ec --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/response-status-400.json @@ -0,0 +1,95 @@ +{ + "mockBidRequest": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + }, + "device": { + "w": 1200, + "h": 900 + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": [ + "application/json;charset=utf-8" + ], + "Accept": [ + "application/json" + ] + }, + "body": { + "id": "web-banner", + "tmax": 3000, + "imp": [ + { + "id": "banner-imp-id", + "ext": { + "bidder": { + "pubId": "386276e072" + } + }, + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + } + } + ], + "device": { + "w": 1200, + "h": 900 + }, + "app": { + "bundle": "com.fake.app", + "publisher": { + "id": "42", + "name": "whatever.pub" + } + } + }, + "impIDs": [ + "banner-imp-id" + ] + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/melozentest/supplemental/response-status-not-200.json b/adapters/melozen/melozentest/supplemental/response-status-not-200.json new file mode 100644 index 00000000000..9b26ee58091 --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/response-status-not-200.json @@ -0,0 +1,84 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.test.testapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/melozen/melozentest/supplemental/wrong-bid-ext.json b/adapters/melozen/melozentest/supplemental/wrong-bid-ext.json new file mode 100644 index 00000000000..b6a1c1f7268 --- /dev/null +++ b/adapters/melozen/melozentest/supplemental/wrong-bid-ext.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://example.com/rtb/v2/bid?publisher_id=386276e072", + "headers": { + "Content-Type": ["application/json;charset=utf-8"], + "Accept": ["application/json"] + }, + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "pubId": "386276e072" + } + } + } + ] + }, + "impIDs": [ + "test-imp-id" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "another-imp-id", + "price": 3.5, + "w": 900, + "h": 250, + "ext": {} + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "Failed to parse bid mediatype for impression \"another-imp-id\"", + "comparison": "regex" + } + ] +} \ No newline at end of file diff --git a/adapters/melozen/params_test.go b/adapters/melozen/params_test.go new file mode 100644 index 00000000000..7e1be7f0db0 --- /dev/null +++ b/adapters/melozen/params_test.go @@ -0,0 +1,50 @@ +package melozen + +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.BidderMeloZen, 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.BidderMeloZen, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"pubId": "12345"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"pubId": ""}`, + `{"pubId": 12345}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index b79d03a8afd..6e5c30652c4 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -129,6 +129,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/marsmedia" "github.com/prebid/prebid-server/v2/adapters/mediago" "github.com/prebid/prebid-server/v2/adapters/medianet" + "github.com/prebid/prebid-server/v2/adapters/melozen" "github.com/prebid/prebid-server/v2/adapters/metax" "github.com/prebid/prebid-server/v2/adapters/mgid" "github.com/prebid/prebid-server/v2/adapters/mgidX" @@ -353,6 +354,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderMediafuse: appnexus.Builder, openrtb_ext.BidderMediaGo: mediago.Builder, openrtb_ext.BidderMedianet: medianet.Builder, + openrtb_ext.BidderMeloZen: melozen.Builder, openrtb_ext.BidderMetaX: metax.Builder, openrtb_ext.BidderMgid: mgid.Builder, openrtb_ext.BidderMgidX: mgidX.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 46d70c7f998..286361b6df7 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -147,6 +147,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderMediafuse, BidderMediaGo, BidderMedianet, + BidderMeloZen, BidderMetaX, BidderMgid, BidderMgidX, @@ -468,6 +469,7 @@ const ( BidderMediafuse BidderName = "mediafuse" BidderMediaGo BidderName = "mediago" BidderMedianet BidderName = "medianet" + BidderMeloZen BidderName = "melozen" BidderMetaX BidderName = "metax" BidderMgid BidderName = "mgid" BidderMgidX BidderName = "mgidX" diff --git a/openrtb_ext/imp_melozen.go b/openrtb_ext/imp_melozen.go new file mode 100644 index 00000000000..598df6a28e9 --- /dev/null +++ b/openrtb_ext/imp_melozen.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ImpExtMeloZen struct { + PubId string `json:"pubId"` +} diff --git a/static/bidder-info/melozen.yaml b/static/bidder-info/melozen.yaml new file mode 100644 index 00000000000..391e0a8d43b --- /dev/null +++ b/static/bidder-info/melozen.yaml @@ -0,0 +1,19 @@ +# We have the following regional endpoint domains: us-east and us-west +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +endpoint: "https://prebid.melozen.com/rtb/v2/bid?publisher_id={{.PublisherID}}" +endpointCompression: gzip +geoscope: + - global +maintainer: + email: DSP@melodong.com +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native \ No newline at end of file diff --git a/static/bidder-params/melozen.json b/static/bidder-params/melozen.json new file mode 100644 index 00000000000..6b5cef5b3fd --- /dev/null +++ b/static/bidder-params/melozen.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MeloZen Adapter Params", + "description": "A schema which validates params accepted by the MeloZen adapter", + "type": "object", + "properties": { + "pubId": { + "type": "string", + "minLength": 1, + "description": "The unique identifier for the publisher." + } + }, + "required": ["pubId"] +} From 2a19924a0076a6a501acb47ec42f80ba5bc19abc Mon Sep 17 00:00:00 2001 From: ahmadlob <109217988+ahmadlob@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:09:23 +0300 Subject: [PATCH 02/15] taboola-support-app-in-prebid-server (#3795) --- adapters/taboola/taboola.go | 27 +++++++++++-------- .../bannerAppRequest.json} | 15 +++++------ 2 files changed, 23 insertions(+), 19 deletions(-) rename adapters/taboola/taboolatest/{supplemental/emptySiteInRequest.json => exemplary/bannerAppRequest.json} (93%) diff --git a/adapters/taboola/taboola.go b/adapters/taboola/taboola.go index 9d950bef2cb..6621c78f6eb 100644 --- a/adapters/taboola/taboola.go +++ b/adapters/taboola/taboola.go @@ -131,7 +131,14 @@ func (a *adapter) buildRequest(request *openrtb2.BidRequest) (*adapters.RequestD return nil, fmt.Errorf("unsupported media type for imp: %v", request.Imp[0]) } - url, err := a.buildEndpointURL(request.Site.ID, mediaType) + var taboolaPublisherId string + if request.Site != nil && request.Site.ID != "" { + taboolaPublisherId = request.Site.ID + } else if request.App != nil && request.App.ID != "" { + taboolaPublisherId = request.App.ID + } + + url, err := a.buildEndpointURL(taboolaPublisherId, mediaType) if err != nil { return nil, err } @@ -207,22 +214,20 @@ func createTaboolaRequests(request *openrtb2.BidRequest) (taboolaRequests []*ope ID: taboolaExt.PublisherId, } - if modifiedRequest.Site == nil { - newSite := &openrtb2.Site{ - ID: taboolaExt.PublisherId, - Name: taboolaExt.PublisherId, - Domain: evaluateDomain(taboolaExt.PublisherDomain, request), - Publisher: publisher, - } - modifiedRequest.Site = newSite - } else { + if modifiedRequest.Site != nil { modifiedSite := *modifiedRequest.Site - modifiedSite.Publisher = publisher modifiedSite.ID = taboolaExt.PublisherId modifiedSite.Name = taboolaExt.PublisherId modifiedSite.Domain = evaluateDomain(taboolaExt.PublisherDomain, request) + modifiedSite.Publisher = publisher modifiedRequest.Site = &modifiedSite } + if modifiedRequest.App != nil { + modifiedApp := *modifiedRequest.App + modifiedApp.ID = taboolaExt.PublisherId + modifiedApp.Publisher = publisher + modifiedRequest.App = &modifiedApp + } if taboolaExt.BCat != nil { modifiedRequest.BCat = taboolaExt.BCat diff --git a/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json b/adapters/taboola/taboolatest/exemplary/bannerAppRequest.json similarity index 93% rename from adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json rename to adapters/taboola/taboolatest/exemplary/bannerAppRequest.json index 3245290f756..34322399a00 100644 --- a/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json +++ b/adapters/taboola/taboolatest/exemplary/bannerAppRequest.json @@ -23,13 +23,14 @@ "ext": { "bidder": { "publisherId": "publisher-id", - "tagid": "tag-id" + "tagid": "tag-id", + "tagId": "tag-Id" } } } ], "app": { - "domain": "http://domain.com" + "bundle": "com.app.my" }, "device": { "ua": "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.62 Mobile Safari/537.36", @@ -62,21 +63,19 @@ } ] }, - "tagid" : "tag-id", + "tagid" : "tag-Id", "ext": { "bidder": { "publisherId": "publisher-id", - "tagid": "tag-id" + "tagid": "tag-id", + "tagId": "tag-Id" } } } ], "app": { - "domain": "http://domain.com" - }, - "site": { "id": "publisher-id", - "name": "publisher-id", + "bundle": "com.app.my", "publisher": { "id": "publisher-id" } From 4d64623dd680bfd9add95fbf2c65b5b224f2b64a Mon Sep 17 00:00:00 2001 From: ownAdx <135326256+ownAdx-prebid@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:51:53 +0530 Subject: [PATCH 03/15] OwnAdx: Bidder param and URL updates (#3813) Co-authored-by: Hina Yadav --- adapters/ownadx/ownadx.go | 9 +++++---- adapters/ownadx/ownadx_test.go | 2 +- macros/macros.go | 3 +++ static/bidder-info/ownadx.yaml | 2 +- static/bidder-params/ownadx.json | 7 +------ 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/adapters/ownadx/ownadx.go b/adapters/ownadx/ownadx.go index 5c54b395ddc..59bc0b68fab 100644 --- a/adapters/ownadx/ownadx.go +++ b/adapters/ownadx/ownadx.go @@ -56,9 +56,9 @@ func createBidRequest(rtbBidRequest *openrtb2.BidRequest, imps []openrtb2.Imp) * } func (adapter *adapter) buildEndpointURL(params *openrtb_ext.ExtImpOwnAdx) (string, error) { endpointParams := macros.EndpointTemplateParams{ - ZoneID: params.SspId, - AccountID: params.SeatId, - SourceId: params.TokenId, + SspID: params.SspId, // Macro + SeatID: params.SeatId, + TokenID: params.TokenId, } return macros.ResolveMacros(adapter.endpoint, endpointParams) } @@ -124,6 +124,7 @@ func groupImpsByExt(imps []openrtb2.Imp) (map[openrtb_ext.ExtImpOwnAdx][]openrtb } func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if response.StatusCode == http.StatusNoContent { return nil, nil } @@ -159,6 +160,7 @@ func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR seatBid := bidResp.SeatBid[0] bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) + if len(seatBid.Bid) == 0 { return nil, []error{ &errortypes.BadServerResponse{ @@ -169,7 +171,6 @@ func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalR for i := 0; i < len(seatBid.Bid); i++ { var bidType openrtb_ext.BidType bid := seatBid.Bid[i] - bidType, err := getMediaType(bid) if err != nil { return nil, []error{&errortypes.BadServerResponse{ diff --git a/adapters/ownadx/ownadx_test.go b/adapters/ownadx/ownadx_test.go index 07dc928b9b0..5995cdd10a7 100644 --- a/adapters/ownadx/ownadx_test.go +++ b/adapters/ownadx/ownadx_test.go @@ -11,7 +11,7 @@ import ( func TestJsonSamples(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderOwnAdx, config.Adapter{ - Endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.AccountID}}/{{.ZoneID}}?token={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + Endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.SeatID}}/{{.SspID}}?token={{.TokenID}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) assert.NoError(t, buildErr) adapterstest.RunJSONBidderTest(t, "ownadxtest", bidder) diff --git a/macros/macros.go b/macros/macros.go index 0c8d1428c67..2b0e29d6238 100644 --- a/macros/macros.go +++ b/macros/macros.go @@ -18,6 +18,9 @@ type EndpointTemplateParams struct { PageID string SupplyId string SspId string + SspID string + SeatID string + TokenID string } // UserSyncPrivacy specifies privacy policy macros, represented as strings, for user sync urls. diff --git a/static/bidder-info/ownadx.yaml b/static/bidder-info/ownadx.yaml index ee98a6c9517..073d75f0278 100644 --- a/static/bidder-info/ownadx.yaml +++ b/static/bidder-info/ownadx.yaml @@ -1,4 +1,4 @@ -endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.AccountID}}/{{.ZoneID}}?token={{.SourceId}}" +endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{.SeatID}}/{{.SspID}}?token={{.TokenID}}" maintainer: email: prebid-team@techbravo.com capabilities: diff --git a/static/bidder-params/ownadx.json b/static/bidder-params/ownadx.json index f529e74cb01..e0e09a7e9f7 100644 --- a/static/bidder-params/ownadx.json +++ b/static/bidder-params/ownadx.json @@ -18,10 +18,5 @@ "description": "Token ID" } }, - - "oneOf": [ - { "required": ["sspId"] }, - { "required": ["feedId"] }, - { "required": ["token"] } - ] + "required": ["sspId","seatId","tokenId"] } From 0e9b234a8b6faa7a3a105ab5662aa047c8798574 Mon Sep 17 00:00:00 2001 From: Antonios Sarhanis Date: Tue, 13 Aug 2024 18:28:44 +1000 Subject: [PATCH 04/15] Use format=prebid on adserver requests. (#3846) --- adapters/adnuntius/adnuntius.go | 2 +- .../adnuntiustest/exemplary/simple-banner.json | 2 +- .../adnuntiustest/supplemental/check-dealId.json | 2 +- .../adnuntiustest/supplemental/check-gdpr.json | 2 +- .../adnuntiustest/supplemental/check-gross-bids.json | 2 +- .../adnuntiustest/supplemental/check-net-bids.json | 2 +- .../supplemental/check-noCookies-parameter.json | 2 +- .../adnuntiustest/supplemental/check-noCookies.json | 2 +- .../supplemental/check-order-multi-imp.json | 6 +++--- .../adnuntiustest/supplemental/check-userId.json | 2 +- .../adnuntiustest/supplemental/empty-regs-ext.json | 2 +- .../adnuntiustest/supplemental/empty-regs.json | 2 +- .../adnuntiustest/supplemental/height-error.json | 2 +- .../adnuntiustest/supplemental/max-deals-test.json | 4 ++-- .../supplemental/send-header-information.json | 2 +- .../adnuntius/adnuntiustest/supplemental/site-ext.json | 4 ++-- .../adnuntiustest/supplemental/size-check.json | 2 +- .../adnuntiustest/supplemental/status-400.json | 2 +- .../adnuntiustest/supplemental/test-networks.json | 2 +- .../adnuntius/adnuntiustest/supplemental/user-ext.json | 10 +++++----- .../adnuntiustest/supplemental/width-error.json | 2 +- 21 files changed, 29 insertions(+), 29 deletions(-) diff --git a/adapters/adnuntius/adnuntius.go b/adapters/adnuntius/adnuntius.go index 823c1783144..829b6e823e4 100644 --- a/adapters/adnuntius/adnuntius.go +++ b/adapters/adnuntius/adnuntius.go @@ -162,7 +162,7 @@ func makeEndpointUrl(ortbRequest openrtb2.BidRequest, a *adapter, noCookies bool } q.Set("tzo", fmt.Sprint(tzo)) - q.Set("format", "json") + q.Set("format", "prebid") url := endpointUrl + "?" + q.Encode() return url, nil diff --git a/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json b/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json index 881c02e340d..49c74ad6683 100644 --- a/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json +++ b/adapters/adnuntius/adnuntiustest/exemplary/simple-banner.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json b/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json index 06dbeff4113..7869fa87f94 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-dealId.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json b/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json index 44e59316a7d..296aa4894a4 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-gdpr.json @@ -38,7 +38,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://gdpr.url?consentString=CONSENT_STRING&format=json&gdpr=1&tzo=0", + "uri": "http://gdpr.url?consentString=CONSENT_STRING&format=prebid&gdpr=1&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json b/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json index 7e533098fb5..95c77e464a9 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-gross-bids.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json b/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json index 301a1f60fec..236d9f6d489 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-net-bids.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json index 5ac10a86ce5..7104b6b0580 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies-parameter.json @@ -31,7 +31,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&noCookies=true&tzo=0", + "uri": "http://whatever.url?format=prebid&noCookies=true&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json index 7080ef678d2..f60f7a636a9 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-noCookies.json @@ -35,7 +35,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&noCookies=true&tzo=0", + "uri": "http://whatever.url?format=prebid&noCookies=true&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json b/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json index 75b08f73716..2269297d9cd 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-order-multi-imp.json @@ -47,11 +47,11 @@ } ] }, - + "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { @@ -159,6 +159,6 @@ ], "currency": "NOK" } - + ] } diff --git a/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json b/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json index eb2f71111a7..2263798d3e5 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/check-userId.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json index d0b7b1450ea..211416294ce 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs-ext.json @@ -33,7 +33,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json index 79571562c7f..2d3d0c861d0 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/empty-regs.json @@ -32,7 +32,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/height-error.json b/adapters/adnuntius/adnuntiustest/supplemental/height-error.json index c58d27eba3e..770f1db032f 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/height-error.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/height-error.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json b/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json index 7f480d8cd08..9a2f24bdc45 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/max-deals-test.json @@ -28,11 +28,11 @@ } ] }, - + "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json b/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json index 8d2b60da238..2383cd4e3e7 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/send-header-information.json @@ -50,7 +50,7 @@ "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Mobile Safari/537.36" ] }, - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json index 8213ac1787f..ddf37962054 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json @@ -37,7 +37,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { @@ -111,4 +111,4 @@ "currency": "NOK" } ] -} \ No newline at end of file +} diff --git a/adapters/adnuntius/adnuntiustest/supplemental/size-check.json b/adapters/adnuntius/adnuntiustest/supplemental/size-check.json index da17af1497d..f39586d9b9f 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/size-check.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/size-check.json @@ -28,7 +28,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&noCookies=true&tzo=0", + "uri": "http://whatever.url?format=prebid&noCookies=true&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/status-400.json b/adapters/adnuntius/adnuntiustest/supplemental/status-400.json index ca54a2057e8..695aeb36e0d 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/status-400.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/status-400.json @@ -27,7 +27,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json b/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json index 6e608b28a39..b81e51cd3d9 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/test-networks.json @@ -32,7 +32,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { diff --git a/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json index 0d852386b9e..2c2dcac1575 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json @@ -4,11 +4,11 @@ "user": { "ext":{ "eids" : [ - { - "source": "idProvider", + { + "source": "idProvider", "uids": [ { "id": "userId", "atype": 1, "ext": { "stype": "ppuid" } } - ] + ] } ] } @@ -39,7 +39,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { @@ -110,4 +110,4 @@ "currency": "NOK" } ] -} \ No newline at end of file +} diff --git a/adapters/adnuntius/adnuntiustest/supplemental/width-error.json b/adapters/adnuntius/adnuntiustest/supplemental/width-error.json index 870cdbc3f87..63d52302376 100644 --- a/adapters/adnuntius/adnuntiustest/supplemental/width-error.json +++ b/adapters/adnuntius/adnuntiustest/supplemental/width-error.json @@ -30,7 +30,7 @@ "httpCalls": [ { "expectedRequest": { - "uri": "http://whatever.url?format=json&tzo=0", + "uri": "http://whatever.url?format=prebid&tzo=0", "body": { "adUnits": [ { From 2606e7529f36aca8e645fc38814ba9924baba8a9 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Fri, 16 Aug 2024 14:20:22 +0100 Subject: [PATCH 05/15] New Module: 51Degrees (#3650) Co-authored-by: James Rosewell Co-authored-by: Marin Miletic Co-authored-by: Sarana-Anna Co-authored-by: Eugene Dorfman Co-authored-by: Krasilchuk Yaroslav --- 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 ++++ 30 files changed, 3477 insertions(+) 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/go.mod b/go.mod index c16acc331df..423abe2438e 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 00827d6fb6c..d62ceefeefb 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= @@ -483,6 +485,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 e4bd6d3675111a973c100694af84f6764fc21f1e Mon Sep 17 00:00:00 2001 From: gg-natalia <148577437+gg-natalia@users.noreply.github.com> Date: Sun, 18 Aug 2024 06:52:28 -0300 Subject: [PATCH 06/15] ADTS-455 remove video validations (#3842) authored by @gg-natalia --- adapters/gumgum/gumgum.go | 15 +--- .../supplemental/missing-video-params.json | 36 --------- .../supplemental/video-missing-size.json | 35 --------- .../supplemental/video-partial-size.json | 77 +++++++++++++++++- .../supplemental/video-zero-size.json | 78 ++++++++++++++++++- 5 files changed, 148 insertions(+), 93 deletions(-) delete mode 100644 adapters/gumgum/gumgumtest/supplemental/missing-video-params.json delete mode 100644 adapters/gumgum/gumgumtest/supplemental/video-missing-size.json diff --git a/adapters/gumgum/gumgum.go b/adapters/gumgum/gumgum.go index 9e25e291825..4e44aacb82e 100644 --- a/adapters/gumgum/gumgum.go +++ b/adapters/gumgum/gumgum.go @@ -161,12 +161,8 @@ func preprocess(imp *openrtb2.Imp) (*openrtb_ext.ExtImpGumGum, error) { } if imp.Video != nil { - err := validateVideoParams(imp.Video) - if err != nil { - return nil, err - } - if gumgumExt.IrisID != "" { + var err error videoCopy := *imp.Video videoExt := openrtb_ext.ExtImpGumGumVideo{IrisID: gumgumExt.IrisID} videoCopy.Ext, err = json.Marshal(&videoExt) @@ -221,15 +217,6 @@ func getMediaTypeForImpID(impID string, imps []openrtb2.Imp) openrtb_ext.BidType return openrtb_ext.BidTypeVideo } -func validateVideoParams(video *openrtb2.Video) (err error) { - if video.W == nil || *video.W == 0 || video.H == nil || *video.H == 0 || video.MinDuration == 0 || video.MaxDuration == 0 || video.Placement == 0 || video.Linearity == 0 { - return &errortypes.BadInput{ - Message: "Invalid or missing video field(s)", - } - } - return nil -} - // Builder builds a new instance of the GumGum 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{ diff --git a/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json b/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json deleted file mode 100644 index b2475cd7bb4..00000000000 --- a/adapters/gumgum/gumgumtest/supplemental/missing-video-params.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": [ - "video/mp4" - ], - "protocols": [ - 1, - 2 - ], - "w": 640, - "h": 480, - "startdelay": 1, - "placement": 1, - "linearity": 1 - }, - "ext": { - "bidder": { - "zone": "ggumtest" - } - } - } - ] - }, - "expectedMakeRequestsErrors": [ - { - "value": "Invalid or missing video field(s)", - "comparison": "literal" - } - ] - } - \ No newline at end of file diff --git a/adapters/gumgum/gumgumtest/supplemental/video-missing-size.json b/adapters/gumgum/gumgumtest/supplemental/video-missing-size.json deleted file mode 100644 index 1e4afe167ea..00000000000 --- a/adapters/gumgum/gumgumtest/supplemental/video-missing-size.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "mockBidRequest": { - "id": "test-request-id", - "imp": [ - { - "id": "test-imp-id", - "video": { - "mimes": [ - "video/mp4" - ], - "minduration": 1, - "maxduration": 2, - "protocols": [ - 1, - 2 - ], - "startdelay": 1, - "placement": 1, - "linearity": 1 - }, - "ext": { - "bidder": { - "zone": "ggumtest" - } - } - } - ] - }, - "expectedMakeRequestsErrors": [ - { - "value": "Invalid or missing video field(s)", - "comparison": "literal" - } - ] -} \ No newline at end of file diff --git a/adapters/gumgum/gumgumtest/supplemental/video-partial-size.json b/adapters/gumgum/gumgumtest/supplemental/video-partial-size.json index 3c9727a1a9c..ce43a93b145 100644 --- a/adapters/gumgum/gumgumtest/supplemental/video-partial-size.json +++ b/adapters/gumgum/gumgumtest/supplemental/video-partial-size.json @@ -27,10 +27,79 @@ } ] }, - "expectedMakeRequestsErrors": [ + "httpCalls": [ { - "value": "Invalid or missing video field(s)", - "comparison": "literal" + "expectedRequest": { + "uri": "https://g2.gumgum.com/providers/prbds2s/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2 + ], + "w": 640, + "startdelay": 1, + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "zone": "ggumtest" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + } + ] + } + ] + } + } + } +], +"expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + }, + "type": "video" + } + ] } - ] +] } \ No newline at end of file diff --git a/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json b/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json index d3d4b427120..4fc7dc3ce77 100644 --- a/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json +++ b/adapters/gumgum/gumgumtest/supplemental/video-zero-size.json @@ -28,10 +28,80 @@ } ] }, - "expectedMakeRequestsErrors": [ + "httpCalls": [ { - "value": "Invalid or missing video field(s)", - "comparison": "literal" + "expectedRequest": { + "uri": "https://g2.gumgum.com/providers/prbds2s/bid", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [ + 1, + 2 + ], + "w": 0, + "h": 0, + "startdelay": 1, + "placement": 1, + "linearity": 1 + }, + "ext": { + "bidder": { + "zone": "ggumtest" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "seatbid": [ + { + "bid": [ + { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + } + ] + } + ] + } + } + } +], +"expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "15da721e-940a-4db6-8621-a1f93140b21b", + "impid": "video1", + "price": 15, + "adid": "59082", + "adm": "\n \n \n GumGum Video\n \n \n \n \n \n \n \n \n \n 00:00:15\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n", + "cid": "3579", + "crid": "59082" + }, + "type": "video" + } + ] } - ] +] } \ No newline at end of file From 6be724459440c9e8fdc2f94eeff5df8280c8750e Mon Sep 17 00:00:00 2001 From: Saar Amrani Date: Wed, 21 Aug 2024 12:37:59 +0300 Subject: [PATCH 07/15] Update Vidazoo bidder info for GPP support (#3869) --- static/bidder-info/vidazoo.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/bidder-info/vidazoo.yaml b/static/bidder-info/vidazoo.yaml index 7abef2e3518..a58f6849501 100644 --- a/static/bidder-info/vidazoo.yaml +++ b/static/bidder-info/vidazoo.yaml @@ -14,5 +14,7 @@ capabilities: - video userSync: iframe: - url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} - userMacro: ${userId} \ No newline at end of file + url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}} + userMacro: ${userId} +openrtb: + gpp_supported: true \ No newline at end of file From e8509e659034f406b1aa0c728742b63ea565a04e Mon Sep 17 00:00:00 2001 From: bretg Date: Wed, 21 Aug 2024 05:40:26 -0400 Subject: [PATCH 08/15] declare support for ORTB 2.6 (#3872) authored by @bretg --- static/bidder-info/rubicon.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/bidder-info/rubicon.yaml b/static/bidder-info/rubicon.yaml index c3943058511..b4c2cfce6d2 100644 --- a/static/bidder-info/rubicon.yaml +++ b/static/bidder-info/rubicon.yaml @@ -13,6 +13,9 @@ xapi: maintainer: email: "header-bidding@rubiconproject.com" gvlVendorID: 52 +openrtb: + version: 2.6 + gpp-supported: true capabilities: app: mediaTypes: From d16171226aacf9d3aeaf4426c10e499fcd57d9eb Mon Sep 17 00:00:00 2001 From: Pubrise Date: Wed, 21 Aug 2024 12:43:39 +0300 Subject: [PATCH 09/15] new adapter (#3833) authored by @Pubrise --- adapters/pubrise/params_test.go | 47 ++++ adapters/pubrise/pubrise.go | 159 +++++++++++ adapters/pubrise/pubrise_test.go | 20 ++ .../pubrisetest/exemplary/endpointId.json | 136 ++++++++++ .../pubrisetest/exemplary/multi-format.json | 105 ++++++++ .../pubrisetest/exemplary/multi-imp.json | 253 ++++++++++++++++++ .../pubrisetest/exemplary/simple-banner.json | 136 ++++++++++ .../pubrisetest/exemplary/simple-native.json | 120 +++++++++ .../pubrisetest/exemplary/simple-video.json | 131 +++++++++ .../exemplary/simple-web-banner.json | 136 ++++++++++ .../supplemental/bad_media_type.json | 83 ++++++ .../supplemental/bad_response.json | 85 ++++++ .../supplemental/no-valid-impressions.json | 20 ++ .../pubrisetest/supplemental/status-204.json | 80 ++++++ .../supplemental/status-not-200.json | 85 ++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_pubrise.go | 6 + static/bidder-info/pubrise.yaml | 21 ++ static/bidder-params/pubrise.json | 22 ++ 20 files changed, 1649 insertions(+) create mode 100644 adapters/pubrise/params_test.go create mode 100644 adapters/pubrise/pubrise.go create mode 100644 adapters/pubrise/pubrise_test.go create mode 100644 adapters/pubrise/pubrisetest/exemplary/endpointId.json create mode 100644 adapters/pubrise/pubrisetest/exemplary/multi-format.json create mode 100644 adapters/pubrise/pubrisetest/exemplary/multi-imp.json create mode 100644 adapters/pubrise/pubrisetest/exemplary/simple-banner.json create mode 100644 adapters/pubrise/pubrisetest/exemplary/simple-native.json create mode 100644 adapters/pubrise/pubrisetest/exemplary/simple-video.json create mode 100644 adapters/pubrise/pubrisetest/exemplary/simple-web-banner.json create mode 100644 adapters/pubrise/pubrisetest/supplemental/bad_media_type.json create mode 100644 adapters/pubrise/pubrisetest/supplemental/bad_response.json create mode 100644 adapters/pubrise/pubrisetest/supplemental/no-valid-impressions.json create mode 100644 adapters/pubrise/pubrisetest/supplemental/status-204.json create mode 100644 adapters/pubrise/pubrisetest/supplemental/status-not-200.json create mode 100644 openrtb_ext/imp_pubrise.go create mode 100644 static/bidder-info/pubrise.yaml create mode 100644 static/bidder-params/pubrise.json diff --git a/adapters/pubrise/params_test.go b/adapters/pubrise/params_test.go new file mode 100644 index 00000000000..df5d38fd02e --- /dev/null +++ b/adapters/pubrise/params_test.go @@ -0,0 +1,47 @@ +package pubrise + +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.BidderPubrise, 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.BidderPubrise, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"placementId": "test"}`, + `{"placementId": "1"}`, + `{"endpointId": "test"}`, + `{"endpointId": "1"}`, +} + +var invalidParams = []string{ + `{"placementId": 42}`, + `{"endpointId": 42}`, + `{"placementId": "1", "endpointId": "1"}`, +} diff --git a/adapters/pubrise/pubrise.go b/adapters/pubrise/pubrise.go new file mode 100644 index 00000000000..9d71f2e1439 --- /dev/null +++ b/adapters/pubrise/pubrise.go @@ -0,0 +1,159 @@ +package pubrise + +import ( + "encoding/json" + "errors" + "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/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type reqBodyExt struct { + PubriseBidderExt reqBodyExtBidder `json:"bidder"` +} + +type reqBodyExtBidder struct { + Type string `json:"type"` + PlacementID string `json:"placementId,omitempty"` + EndpointID string `json:"endpointId,omitempty"` +} + +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) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errs []error + var adapterRequests []*adapters.RequestData + + reqCopy := *request + for _, imp := range request.Imp { + reqCopy.Imp = []openrtb2.Imp{imp} + + var bidderExt adapters.ExtImpBidder + var pubriseExt openrtb_ext.ImpExtPubrise + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errs = append(errs, err) + continue + } + if err := json.Unmarshal(bidderExt.Bidder, &pubriseExt); err != nil { + errs = append(errs, err) + continue + } + + impExt := reqBodyExt{PubriseBidderExt: reqBodyExtBidder{}} + + if pubriseExt.PlacementID != "" { + impExt.PubriseBidderExt.PlacementID = pubriseExt.PlacementID + impExt.PubriseBidderExt.Type = "publisher" + } else if pubriseExt.EndpointID != "" { + impExt.PubriseBidderExt.EndpointID = pubriseExt.EndpointID + impExt.PubriseBidderExt.Type = "network" + } + + finalyImpExt, err := json.Marshal(impExt) + if err != nil { + errs = append(errs, err) + continue + } + + reqCopy.Imp[0].Ext = finalyImpExt + + adapterReq, err := a.makeRequest(&reqCopy) + if err != nil { + errs = append(errs, err) + continue + } + + if adapterReq != nil { + adapterRequests = append(adapterRequests, adapterReq) + } + } + + if len(adapterRequests) == 0 { + errs = append(errs, errors.New("found no valid impressions")) + return nil, errs + } + + return adapterRequests, nil +} + +func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { + reqJSON, err := json.Marshal(request) + if err != nil { + return nil, err + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + return &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: reqJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }, nil +} + +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)) + if len(response.Cur) != 0 { + bidResponse.Currency = response.Cur + } + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getBidType(seatBid.Bid[i]) + if err != nil { + return nil, []error{err} + } + + b := &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + } + bidResponse.Bids = append(bidResponse.Bids, b) + } + } + return bidResponse, nil +} + +func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + // determinate media type by bid response field mtype + 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 + } + + return "", fmt.Errorf("could not define media type for impression: %s", bid.ImpID) +} diff --git a/adapters/pubrise/pubrise_test.go b/adapters/pubrise/pubrise_test.go new file mode 100644 index 00000000000..a50878c339e --- /dev/null +++ b/adapters/pubrise/pubrise_test.go @@ -0,0 +1,20 @@ +package pubrise + +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.BidderEmtv, config.Adapter{ + Endpoint: "https://backend.pubrise.ai/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "pubrisetest", bidder) +} diff --git a/adapters/pubrise/pubrisetest/exemplary/endpointId.json b/adapters/pubrise/pubrisetest/exemplary/endpointId.json new file mode 100644 index 00000000000..3766c7a3ef4 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/endpointId.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/multi-format.json b/adapters/pubrise/pubrisetest/exemplary/multi-format.json new file mode 100644 index 00000000000..9e5c2ef2c57 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/multi-format.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/multi-imp.json b/adapters/pubrise/pubrisetest/exemplary/multi-imp.json new file mode 100644 index 00000000000..d922113a512 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/multi-imp.json @@ -0,0 +1,253 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + }, + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "endpointId": "test", + "type": "network" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-banner.json b/adapters/pubrise/pubrisetest/exemplary/simple-banner.json new file mode 100644 index 00000000000..6669c460ea1 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-native.json b/adapters/pubrise/pubrisetest/exemplary/simple-native.json new file mode 100644 index 00000000000..e535c9ce5f9 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-native.json @@ -0,0 +1,120 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 4, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-video.json b/adapters/pubrise/pubrisetest/exemplary/simple-video.json new file mode 100644 index 00000000000..047b2bc7d99 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-video.json @@ -0,0 +1,131 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + }, + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 5 + ], + "w": 1024, + "h": 576 + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "00:01:00", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/exemplary/simple-web-banner.json b/adapters/pubrise/pubrisetest/exemplary/simple-web-banner.json new file mode 100644 index 00000000000..ef9b8080ae7 --- /dev/null +++ b/adapters/pubrise/pubrisetest/exemplary/simple-web-banner.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "tagid": "test", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "site": { + "id": "1", + "domain": "test.com" + }, + "device": { + "ip": "123.123.123.123", + "ua": "Ubuntu" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "mtype": 1, + "w": 468, + "h": 60, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/pubrise/pubrisetest/supplemental/bad_media_type.json b/adapters/pubrise/pubrisetest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..2b6165345ac --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/bad_media_type.json @@ -0,0 +1,83 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test_bid_id", + "impid": "test-imp-id", + "price": 0.27543, + "adm": "", + "cid": "test_cid", + "crid": "test_crid", + "dealid": "test_dealid", + "w": 300, + "h": 250, + "ext": {} + } + ], + "seat": "pubrise" + } + ], + "cur": "USD" + } + } + }], + "expectedMakeBidsErrors": [ + { + "value": "could not define media type for impression: test-imp-id", + "comparison": "literal" + } + ] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/bad_response.json b/adapters/pubrise/pubrisetest/supplemental/bad_response.json new file mode 100644 index 00000000000..08b58d888ed --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/bad_response.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": "" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/no-valid-impressions.json b/adapters/pubrise/pubrisetest/supplemental/no-valid-impressions.json new file mode 100644 index 00000000000..cc1edd685f9 --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/no-valid-impressions.json @@ -0,0 +1,20 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "found no valid impressions", + "comparison": "literal" + } + ] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/status-204.json b/adapters/pubrise/pubrisetest/supplemental/status-204.json new file mode 100644 index 00000000000..1ed98ff0c72 --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/status-204.json @@ -0,0 +1,80 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204, + "body": {} + } + }], + "expectedBidResponses": [] +} diff --git a/adapters/pubrise/pubrisetest/supplemental/status-not-200.json b/adapters/pubrise/pubrisetest/supplemental/status-not-200.json new file mode 100644 index 00000000000..c4b3cdc6f57 --- /dev/null +++ b/adapters/pubrise/pubrisetest/supplemental/status-not-200.json @@ -0,0 +1,85 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://backend.pubrise.ai/pserver", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "placementId": "test", + "type": "publisher" + } + } + } + ], + "app": { + "id": "1", + "bundle": "com.wls.testwlsapplication" + }, + "device": { + "ip": "123.123.123.123", + "ifa": "sdjfksdf-dfsds-dsdg-dsgg" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 404, + "body": {} + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 404. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 6e5c30652c4..d6b7ddd092c 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -152,6 +152,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/playdigo" "github.com/prebid/prebid-server/v2/adapters/pubmatic" "github.com/prebid/prebid-server/v2/adapters/pubnative" + "github.com/prebid/prebid-server/v2/adapters/pubrise" "github.com/prebid/prebid-server/v2/adapters/pulsepoint" "github.com/prebid/prebid-server/v2/adapters/pwbid" "github.com/prebid/prebid-server/v2/adapters/qt" @@ -377,6 +378,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderPlaydigo: playdigo.Builder, openrtb_ext.BidderPubmatic: pubmatic.Builder, openrtb_ext.BidderPubnative: pubnative.Builder, + openrtb_ext.BidderPubrise: pubrise.Builder, openrtb_ext.BidderPulsepoint: pulsepoint.Builder, openrtb_ext.BidderPWBid: pwbid.Builder, openrtb_ext.BidderQT: qt.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 286361b6df7..38a1bca3203 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -169,6 +169,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderPGAMSsp, BidderPlaydigo, BidderPubmatic, + BidderPubrise, BidderPubnative, BidderPulsepoint, BidderPWBid, @@ -491,6 +492,7 @@ const ( BidderPGAMSsp BidderName = "pgamssp" BidderPlaydigo BidderName = "playdigo" BidderPubmatic BidderName = "pubmatic" + BidderPubrise BidderName = "pubrise" BidderPubnative BidderName = "pubnative" BidderPulsepoint BidderName = "pulsepoint" BidderPWBid BidderName = "pwbid" diff --git a/openrtb_ext/imp_pubrise.go b/openrtb_ext/imp_pubrise.go new file mode 100644 index 00000000000..c2b30391748 --- /dev/null +++ b/openrtb_ext/imp_pubrise.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ImpExtPubrise struct { + PlacementID string `json:"placementId"` + EndpointID string `json:"endpointId"` +} diff --git a/static/bidder-info/pubrise.yaml b/static/bidder-info/pubrise.yaml new file mode 100644 index 00000000000..fe5e6cd6d40 --- /dev/null +++ b/static/bidder-info/pubrise.yaml @@ -0,0 +1,21 @@ +endpoint: "https://backend.pubrise.ai/" +maintainer: + email: "prebid@pubrise.ai" +capabilities: + site: + mediaTypes: + - banner + - video + - native + app: + mediaTypes: + - banner + - video + - native +userSync: + redirect: + url: "https://sync.pubrise.ai/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}" + userMacro: "[UID]" + iframe: + url: "https://sync.pubrise.ai/pbserverIframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&pbserverUrl={{.RedirectURL}}" + userMacro: "[UID]" diff --git a/static/bidder-params/pubrise.json b/static/bidder-params/pubrise.json new file mode 100644 index 00000000000..0d972da45e9 --- /dev/null +++ b/static/bidder-params/pubrise.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Pubrise Adapter Params", + "description": "A schema which validates params accepted by the Pubrise adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { "required": ["placementId"] }, + { "required": ["endpointId"] } + ] +} \ No newline at end of file From a556e2d3479d53509dffd21bd0e23689332a87a8 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:23:39 +0300 Subject: [PATCH 10/15] Fix currency conversion bug. (#3867) Co-authored-by: ddubyk --- adapters/rubicon/rubicon.go | 6 ++++-- adapters/rubicon/rubicon_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 000850752de..1cb2ab49465 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -317,9 +317,11 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada continue } - if resolvedBidFloor > 0 { - imp.BidFloorCur = "USD" + if resolvedBidFloor >= 0 { imp.BidFloor = resolvedBidFloor + if imp.BidFloorCur != "" { + imp.BidFloorCur = "USD" + } } if request.User != nil { diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go index 7a420a10eca..18850d96f9f 100644 --- a/adapters/rubicon/rubicon_test.go +++ b/adapters/rubicon/rubicon_test.go @@ -232,6 +232,14 @@ func TestOpenRTBRequestWithDifferentBidFloorAttributes(t *testing.T) { expectedBidCur: "", expectedErrors: nil, }, + { + bidFloor: 0, + bidFloorCur: "EUR", + setMock: func(m *mock.Mock) {}, + expectedBidFloor: 0, + expectedBidCur: "USD", + expectedErrors: nil, + }, { bidFloor: -1, bidFloorCur: "CZK", From 59a5b07ed07f91fec6e51ee21e57e1de6099d4b5 Mon Sep 17 00:00:00 2001 From: mwang-sticky Date: Wed, 21 Aug 2024 20:26:15 +0800 Subject: [PATCH 11/15] freewheel-adapter: support 2.6 (#3873) --- static/bidder-info/freewheelssp.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/bidder-info/freewheelssp.yaml b/static/bidder-info/freewheelssp.yaml index 0c0a11edfce..cd18c2d8172 100644 --- a/static/bidder-info/freewheelssp.yaml +++ b/static/bidder-info/freewheelssp.yaml @@ -14,3 +14,6 @@ userSync: iframe: url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}" userMacro: "{viewerid}" +openrtb: + version: 2.6 + gpp-supported: true \ No newline at end of file From 54f875981f54964410b9e5370a421f7d25af116f Mon Sep 17 00:00:00 2001 From: dtbarne <7635750+dtbarne@users.noreply.github.com> Date: Wed, 21 Aug 2024 07:26:30 -0500 Subject: [PATCH 12/15] Update mobilefuse.yaml to indicate support for OpenRTB 2.6 and GPP (#3871) --- static/bidder-info/mobilefuse.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/bidder-info/mobilefuse.yaml b/static/bidder-info/mobilefuse.yaml index 1d6b323c3a6..62714f15124 100644 --- a/static/bidder-info/mobilefuse.yaml +++ b/static/bidder-info/mobilefuse.yaml @@ -13,3 +13,6 @@ capabilities: - video - native endpointCompression: "GZIP" +openrtb: + version: 2.6 + gpp-supported: true From bd85ba414df00ba971015773ef67ccecb6f7792c Mon Sep 17 00:00:00 2001 From: Nick Llerandi Date: Thu, 22 Aug 2024 02:15:12 -0400 Subject: [PATCH 13/15] specifies ortb 2.6 support (#3) (#3876) --- static/bidder-info/kargo.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/static/bidder-info/kargo.yaml b/static/bidder-info/kargo.yaml index 1a7a77eb8bb..6acd679ebc0 100644 --- a/static/bidder-info/kargo.yaml +++ b/static/bidder-info/kargo.yaml @@ -15,4 +15,5 @@ userSync: userMacro: "$UID" endpointCompression: "GZIP" openrtb: + version: 2.6 gpp-supported: true From 84a8162205b1aacaec18ceedd9cb0b99796818b7 Mon Sep 17 00:00:00 2001 From: Anand Venkatraman Date: Thu, 22 Aug 2024 16:14:32 +0530 Subject: [PATCH 14/15] PulsePoint: ortb 2.6 version and gpp support (#3874) authored by @anand-venkatraman --- static/bidder-info/pulsepoint.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/bidder-info/pulsepoint.yaml b/static/bidder-info/pulsepoint.yaml index 762dbbb0c73..87aff0b5f04 100644 --- a/static/bidder-info/pulsepoint.yaml +++ b/static/bidder-info/pulsepoint.yaml @@ -19,3 +19,6 @@ userSync: redirect: url: "https://bh.contextweb.com/rtset?pid=561205&ev=1&rurl={{.RedirectURL}}" userMacro: "%%VGUID%%" +openrtb: + version: 2.6 + gpp-supported: true From 8d7117d948879d0cf44b2513a9beec3bf6404012 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:23:34 -0400 Subject: [PATCH 15/15] Revert "New Module: 51Degrees (#3650)" (#3888) This reverts commit 2606e7529f36aca8e645fc38814ba9924baba8a9. --- 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 ---- 30 files changed, 3477 deletions(-) delete mode 100644 modules/fiftyonedegrees/devicedetection/README.md delete mode 100644 modules/fiftyonedegrees/devicedetection/account_info_extractor.go delete mode 100644 modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/account_validator.go delete mode 100644 modules/fiftyonedegrees/devicedetection/account_validator_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/config.go delete mode 100644 modules/fiftyonedegrees/devicedetection/config_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/context.go delete mode 100644 modules/fiftyonedegrees/devicedetection/device_detector.go delete mode 100644 modules/fiftyonedegrees/devicedetection/device_detector_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/device_info_extractor.go delete mode 100644 modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/evidence_extractor.go delete mode 100644 modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go delete mode 100644 modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go delete mode 100644 modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go delete mode 100644 modules/fiftyonedegrees/devicedetection/models.go delete mode 100644 modules/fiftyonedegrees/devicedetection/models_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/module.go delete mode 100644 modules/fiftyonedegrees/devicedetection/module_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/request_headers_extractor.go delete mode 100644 modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go delete mode 100644 modules/fiftyonedegrees/devicedetection/sample/pbs.json delete mode 100644 modules/fiftyonedegrees/devicedetection/sample/request_data.json delete mode 100644 modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go diff --git a/go.mod b/go.mod index 423abe2438e..c16acc331df 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,6 @@ 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 @@ -68,10 +67,6 @@ 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 d62ceefeefb..00827d6fb6c 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,6 @@ 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= @@ -485,15 +483,6 @@ 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 36ac5589add..e5d04e149af 100644 --- a/modules/builder.go +++ b/modules/builder.go @@ -1,7 +1,6 @@ package modules import ( - fiftyonedegreesDevicedetection "github.com/prebid/prebid-server/v2/modules/fiftyonedegrees/devicedetection" prebidOrtb2blocking "github.com/prebid/prebid-server/v2/modules/prebid/ortb2blocking" ) @@ -9,9 +8,6 @@ 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 deleted file mode 100644 index 645fb407fe5..00000000000 --- a/modules/fiftyonedegrees/devicedetection/README.md +++ /dev/null @@ -1,255 +0,0 @@ -## 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 deleted file mode 100644 index 2a5168cfe0c..00000000000 --- a/modules/fiftyonedegrees/devicedetection/account_info_extractor.go +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 2d32f7915b5..00000000000 --- a/modules/fiftyonedegrees/devicedetection/account_info_extractor_test.go +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index fdff92531a7..00000000000 --- a/modules/fiftyonedegrees/devicedetection/account_validator.go +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 25f99e3b796..00000000000 --- a/modules/fiftyonedegrees/devicedetection/account_validator_test.go +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index a5519026791..00000000000 --- a/modules/fiftyonedegrees/devicedetection/config.go +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index e2478d82b7d..00000000000 --- a/modules/fiftyonedegrees/devicedetection/config_test.go +++ /dev/null @@ -1,119 +0,0 @@ -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 deleted file mode 100644 index 3c10dd2f393..00000000000 --- a/modules/fiftyonedegrees/devicedetection/context.go +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 8369d343d34..00000000000 --- a/modules/fiftyonedegrees/devicedetection/device_detector.go +++ /dev/null @@ -1,157 +0,0 @@ -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 deleted file mode 100644 index 84d6ab28cc0..00000000000 --- a/modules/fiftyonedegrees/devicedetection/device_detector_test.go +++ /dev/null @@ -1,190 +0,0 @@ -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 deleted file mode 100644 index 1c913e21696..00000000000 --- a/modules/fiftyonedegrees/devicedetection/device_info_extractor.go +++ /dev/null @@ -1,121 +0,0 @@ -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 deleted file mode 100644 index 197e3928602..00000000000 --- a/modules/fiftyonedegrees/devicedetection/device_info_extractor_test.go +++ /dev/null @@ -1,130 +0,0 @@ -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 deleted file mode 100644 index 1d67e1cdeed..00000000000 --- a/modules/fiftyonedegrees/devicedetection/evidence_extractor.go +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 9abdf799643..00000000000 --- a/modules/fiftyonedegrees/devicedetection/evidence_extractor_test.go +++ /dev/null @@ -1,256 +0,0 @@ -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 deleted file mode 100644 index 7237698117d..00000000000 --- a/modules/fiftyonedegrees/devicedetection/fiftyone_device_types.go +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 5fd0203bac8..00000000000 --- a/modules/fiftyonedegrees/devicedetection/fiftyone_device_types_test.go +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index 911f20e1840..00000000000 --- a/modules/fiftyonedegrees/devicedetection/hook_auction_entrypoint.go +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 1146c3cc639..00000000000 --- a/modules/fiftyonedegrees/devicedetection/hook_raw_auction_request.go +++ /dev/null @@ -1,173 +0,0 @@ -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 deleted file mode 100644 index c58daa211fd..00000000000 --- a/modules/fiftyonedegrees/devicedetection/models.go +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 898f25f4144..00000000000 --- a/modules/fiftyonedegrees/devicedetection/models_test.go +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index df72e6338a5..00000000000 --- a/modules/fiftyonedegrees/devicedetection/module.go +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index 7b8095ac431..00000000000 --- a/modules/fiftyonedegrees/devicedetection/module_test.go +++ /dev/null @@ -1,703 +0,0 @@ -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 deleted file mode 100644 index 8440886b353..00000000000 --- a/modules/fiftyonedegrees/devicedetection/request_headers_extractor.go +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 77fbed3a42f..00000000000 --- a/modules/fiftyonedegrees/devicedetection/request_headers_extractor_test.go +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 43fd28610f1..00000000000 --- a/modules/fiftyonedegrees/devicedetection/sample/pbs.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "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 deleted file mode 100644 index 1f6bc8900f8..00000000000 --- a/modules/fiftyonedegrees/devicedetection/sample/request_data.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "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 deleted file mode 100644 index ab69210449f..00000000000 --- a/modules/fiftyonedegrees/devicedetection/sua_payload_extractor.go +++ /dev/null @@ -1,144 +0,0 @@ -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 -}