diff --git a/config/account.go b/config/account.go index 34b025b0f42..6966261a992 100644 --- a/config/account.go +++ b/config/account.go @@ -364,10 +364,6 @@ func (ip *IPv4) Validate(errs []error) []error { return errs } -type AuctionPrivacy struct { - PrivacySandbox PrivacySandbox `mapstructure:"privacysandbox" json:"privacysandbox"` -} - type PrivacySandbox struct { CookieDeprecation bool `mapstructure:"cookiedeprecation" json:"cookiedeprecation"` CookieDeprecationExpirationSec int `mapstructure:"cookiedeprecationexpirationsec" json:"cookiedeprecationexpirationsec"` diff --git a/config/config.go b/config/config.go index 60a541e2ed0..3eb1d1ca0cb 100644 --- a/config/config.go +++ b/config/config.go @@ -1285,3 +1285,8 @@ type TmaxAdjustments struct { // PBS won't send a request to the bidder if the bidder tmax calculated is less than the BidderResponseDurationMin value BidderResponseDurationMin uint `mapstructure:"bidder_response_duration_min_ms"` } + +type AuctionPrivacy struct { + TopicsDomain string `mapstructure:"topicsdomain"` + PrivacySandbox PrivacySandbox `mapstructure:"privacysandbox" json:"privacysandbox"` +} diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 7ebbfae76c3..c136f25b23c 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -2013,6 +2013,8 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ } setAuctionTypeImplicitly(r) + + setSecBrowsingTopcisImplicitly(httpReq, r) } // setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device @@ -2031,6 +2033,184 @@ func setAuctionTypeImplicitly(r *openrtb_ext.RequestWrapper) { } } +// (100);v=chrome.1:1:20, (200);v=chrome.1:1:40, (300);v=chrome.1:1:60, ();p=P +func setSecBrowsingTopcisImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { + if r.User == nil { + r.User = &openrtb2.User{} + } + + secBrowsingTopics := httpReq.Header.Get("Sec-Browsing-Topics") + if secBrowsingTopics == "" { + return + } + + // segtax-segclass-name-segIds + userData := map[int]map[string]map[string]map[string]struct{}{} + secBrowsingTopicsArr := strings.Split(secBrowsingTopics, ",") + c := 0 + for _, seg := range secBrowsingTopicsArr { + if c > 10 { + break + } + + seg = strings.TrimSpace(seg) + if seg == "" { + continue + } + + if strings.HasPrefix(seg, "();p=") { + continue + } + + segment := strings.Split(seg, ";") + if len(segment) != 2 { + continue + } + + segmentsIds := strings.TrimSpace(segment[0]) + if len(segmentsIds) < 3 || segmentsIds[0] != '(' || segmentsIds[len(segmentsIds)-1] != ')' { + continue + } + segmentsIds = strings.TrimLeft(segmentsIds, "(") + segmentsIds = strings.TrimRight(segmentsIds, ")") + segmentsIdArr := strings.Fields(segmentsIds) + if len(segmentsIdArr) < 1 { + continue + } + + taxanomyModel := strings.Split(segment[1], ":") + if len(taxanomyModel) != 3 { + continue + } + + taxanomyVer := strings.TrimSpace(taxanomyModel[1]) + taxanomy, err := strconv.Atoi(taxanomyVer) + if err != nil || taxanomy < 1 || taxanomy > 10 { + continue + } + segtax := 600 + (taxanomy - 1) + segclass := strings.TrimSpace(taxanomyModel[2]) + // modelVer := strings.TrimSpace(taxanomyModel[2]) + // segclass, err := strconv.Atoi(modelVer) + // if err != nil { + // continue + // } + + // if _, ok := userData["TOPICS_DOMAIN"]; !ok { + // userData["TOPICS_DOMAIN"] = map[int]map[int][]string{} + // } + if _, ok := userData[segtax]; !ok { + userData[segtax] = map[string]map[string]map[string]struct{}{} + } + + if _, ok := userData[segtax][segclass]; !ok { + userData[segtax][segclass] = map[string]map[string]struct{}{} + } + + if _, ok := userData[segtax][segclass]["TOPICS_DOMAIN"]; !ok { + userData[segtax][segclass]["TOPICS_DOMAIN"] = map[string]struct{}{} + } + + for _, segId := range segmentsIdArr { + segId = strings.TrimSpace(segId) + if segid, err := strconv.Atoi(segId); err == nil && segid > 0 { + userData[segtax][segclass]["TOPICS_DOMAIN"][segId] = struct{}{} + } + } + + c++ + } + + type extData struct { + Segtax int + Segclass string + } + + requestUserData := map[int]map[string]map[string]map[string]struct{}{} + for _, data := range r.User.Data { + ext := &extData{} + if err := json.Unmarshal(data.Ext, ext); err != nil { + continue + } + + if ext.Segtax == 0 || ext.Segclass == "" { + continue + } + + if _, ok := requestUserData[ext.Segtax]; !ok { + requestUserData[ext.Segtax] = map[string]map[string]map[string]struct{}{} + } + + if _, ok := requestUserData[ext.Segtax][ext.Segclass]; !ok { + requestUserData[ext.Segtax][ext.Segclass] = map[string]map[string]struct{}{} + } + + if _, ok := requestUserData[ext.Segtax][ext.Segclass][data.Name]; !ok { + requestUserData[ext.Segtax][ext.Segclass][data.Name] = map[string]struct{}{} + } + + for _, segId := range data.Segment { + requestUserData[ext.Segtax][ext.Segclass][data.Name][segId.ID] = struct{}{} + } + + // merge segment ids if segtax, segclass and name are the same + merged := false + if _, ok := userData[ext.Segtax]; ok { + if _, ok := userData[ext.Segtax][ext.Segclass]; ok { + if _, ok := userData[ext.Segtax][ext.Segclass][data.Name]; ok { + for segId := range userData[ext.Segtax][ext.Segclass][data.Name] { + if _, ok := requestUserData[ext.Segtax][ext.Segclass][data.Name][segId]; !ok { + data.Segment = append(data.Segment, openrtb2.Segment{ + ID: segId, + }) + } + } + + delete(userData[ext.Segtax][ext.Segclass], data.Name) + } + } + } + + if !merged { + continue + } + + // if _, ok := userData[ext.Segtax]; !ok { + // userData[ext.Segtax] = map[string]map[string]map[string]struct{}{} + // } + + // if _, ok := userData[ext.Segtax][ext.Segclass]; !ok { + // userData[ext.Segtax][ext.Segclass] = map[string]map[string]struct{}{} + // } + + // if _, ok := userData[ext.Segtax][ext.Segclass][data.Name]; !ok { + // userData[ext.Segtax][ext.Segclass][data.Name] = map[string]struct{}{} + // } + + // for _, segId := range data.Segment { + // userData[ext.Segtax][ext.Segclass][data.Name][segId.ID] = struct{}{} + // } + } + + for segtax, SegclassSegName := range userData { + for segclass, segName := range SegclassSegName { + for segName, segIds := range segName { + r.User.Data = append(r.User.Data, openrtb2.Data{ + Name: segName, + Ext: json.RawMessage(fmt.Sprintf(`{"segtax": %d, "segclass": "%s"}`, segtax, segclass)), + }) + for segId := range segIds { + r.User.Data[len(r.User.Data)-1].Segment = append(r.User.Data[len(r.User.Data)-1].Segment, openrtb2.Segment{ + ID: segId, + }) + } + } + } + } + + // r.User.Ext = json.RawMessage(fmt.Sprintf(`{"consent": "%s"}`, secBrowsingTopic)) +} + func setSiteImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { if r.Site == nil { r.Site = &openrtb2.Site{} diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index c521d653cac..6c9d5376da6 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -13,6 +13,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "sort" "strings" "testing" "time" @@ -6108,3 +6109,529 @@ func TestValidateAliases(t *testing.T) { func fakeNormalizeBidderName(name string) (openrtb_ext.BidderName, bool) { return openrtb_ext.BidderName(strings.ToLower(name)), true } + +// func Test_secCookieDeprecation(t *testing.T) { +// type args struct { +// httpReq *http.Request +// r *openrtb_ext.RequestWrapper +// } +// tests := []struct { +// name string +// args args +// }{ +// { +// name: "Empty HTTP request", +// args: args{ +// httpReq: &http.Request{}, +// r: &openrtb_ext.RequestWrapper{}, +// }, +// }, +// { +// name: "HTTP request without sec-cookie header", +// args: args{ +// httpReq: &http.Request{ +// Header: http.Header{}, +// }, +// r: &openrtb_ext.RequestWrapper{}, +// }, +// }, +// { +// name: "HTTP request with valid sec-cookie header", +// args: args{ +// httpReq: &http.Request{ +// Header: http.Header{ +// "Sec-Cookie": []string{"some-sec-cookie-value"}, +// }, +// }, +// r: &openrtb_ext.RequestWrapper{ +// Device: &openrtb2.Device{ +// Ext: json.RawMessage(`{"key": "value"}`), +// }, +// }, +// }, +// }, +// { +// name: "HTTP request with multiple sec-cookie headers", +// args: args{ +// httpReq: &http.Request{ +// Header: http.Header{ +// "Sec-Cookie": []string{"cookie1", "cookie2", "cookie3"}, +// }, +// }, +// r: &openrtb_ext.RequestWrapper{}, +// }, +// }, +// { +// name: "HTTP request with invalid sec-cookie header", +// args: args{ +// httpReq: &http.Request{ +// Header: http.Header{ +// "Sec-Cookie": []string{"invalid-cookie"}, +// }, +// }, +// r: &openrtb_ext.RequestWrapper{}, +// }, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// secCookieDeprecation(tt.args.httpReq, tt.args.r) + +// // Assert r.Device.Ext +// if tt.args.r.Device.Ext != nil { +// t.Errorf("Expected r.Device.Ext to be nil, got %v", tt.args.r.Device.Ext) +// } +// }) +// } +// } + +func Test_setSecBrowsingTopcisImplicitly(t *testing.T) { + type args struct { + httpReq *http.Request + r *openrtb_ext.RequestWrapper + } + tests := []struct { + name string + args args + wantUser *openrtb2.User + }{ + { + name: "Empty HTTP request", + args: args{ + httpReq: &http.Request{}, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{}, + }, + { + name: "Sec-Browsing-Topics with empty value, request.user.data empty, no change in user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{""}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{}, + }, + { + name: "Sec-Browsing-Topics with invalid value, request.user.data empty, no change in user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"some-sec-cookie-value"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{}, + }, + { + name: "Sec-Browsing-Topics with finish padding, request.user.data empty, no change in user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"();p=P0000000000000000000000000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{}, + }, + { + name: "Sec-Browsing-Topics with one valid field, request.user.data empty, valid field data added to req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:1:2, ();p=P00000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with one valid field and one invalid field, request.user.data empty, valid field data added to req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:1:2, (4);v=chrome.1, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with one valid field having multiple segIds, request.user.data empty, valid field data added to req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1 2);v=chrome.1:1:2, ();p=P00000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + { + ID: "2", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with two valid field with different segclass, request.user.data empty, valid field data added to req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:1:2, (1);v=chrome.1:1:1, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "1"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with three fields one valid, one with invalid segtax > 10 and last with invalid segtax < 1, request.user.data empty, valid field data added to req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:11:2, (1);v=chrome.1:1:4, (1);v=chrome.1:0:2, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "4"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with three fields one invalid, two with same segtax and segclass, request.user.data empty, common valid fields merged in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(100);v=chrome.1:111111111111111111:20, (200);v=chrome.1:2:40, (200 300);v=chrome.1:2:40, ();p=P"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "200", + }, + { + ID: "300", + }, + }, + Ext: json.RawMessage(`{"segtax": 601, "segclass": "40"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with two fields both matching segtax, segclass and segIds, request.user.data empty, data merged and added in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:1:2, (1);v=chrome.1:1:2, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with two fields both matching segtax and segclass and different segIds, request.user.data empty, data merged and added in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:1:2, (2);v=chrome.1:1:2, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + { + ID: "2", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with two fields both matching segtax and segclass and different segIds, request.user.data empty, data merged and added in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:1:2, (2);v=chrome.1:1:2, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + { + ID: "2", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with valid fields having special characters (whitespaces, etc), request.user.data empty, valid data filtered and added in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1 2 4 6 7 4567 ) ; v=chrome.1: 1 : 2, (1);v=chrome.1, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + { + ID: "2", + }, + { + ID: "4", + }, + { + ID: "6", + }, + { + ID: "7", + }, + { + ID: "4567", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with valid fields having special characters (whitespaces, etc), request.user.data empty, valid data filtered and added in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1 -2 4 6 7 4567 ) ; v=chrome.1: 1 : 2, (1);v=chrome.1, ();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + { + ID: "4", + }, + { + ID: "6", + }, + { + ID: "7", + }, + { + ID: "4567", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "2"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with valid fields but finish padding in between, request.user.data empty, valid data filtered and added in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(1);v=chrome.1:1:4,();p=P0000000000,(2);v=chrome.1:1:4,();p=P000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + { + ID: "2", + }, + }, + Ext: json.RawMessage(`{"segtax": 600, "segclass": "4"}`), + }, + }, + }, + }, + { + name: "Sec-Browsing-Topics with valid fields but no finish padding, request.user.data empty, valid data added in req.user.data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + "Sec-Browsing-Topics": []string{"(100);v=chrome.1:2:20"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "TOPICS_DOMAIN", + Segment: []openrtb2.Segment{ + { + ID: "100", + }, + }, + Ext: json.RawMessage(`{"segtax": 601, "segclass": "20"}`), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setSecBrowsingTopcisImplicitly(tt.args.httpReq, tt.args.r) + + // sequence is not garunteed in request.user.data as we're using a map + if tt.wantUser.Data != nil { + sort.Slice(tt.wantUser.Data, func(i, j int) bool { + return string(tt.wantUser.Data[i].Ext) < string(tt.wantUser.Data[j].Ext) + }) + + for _, data := range tt.wantUser.Data { + sort.Slice(data.Segment, func(i, j int) bool { + return data.Segment[i].ID < data.Segment[j].ID + }) + } + } + if tt.args.r.User.Data != nil { + sort.Slice(tt.args.r.User.Data, func(i, j int) bool { + return string(tt.args.r.User.Data[i].Ext) < string(tt.args.r.User.Data[j].Ext) + }) + + for _, data := range tt.args.r.User.Data { + sort.Slice(data.Segment, func(i, j int) bool { + return data.Segment[i].ID < data.Segment[j].ID + }) + } + } + + assert.Equal(t, tt.wantUser, tt.args.r.User) + }) + } +}