Skip to content

Commit

Permalink
Privacy Sandbox: Topics in headers (#3393)
Browse files Browse the repository at this point in the history
  • Loading branch information
pm-nilesh-chate authored Apr 3, 2024
1 parent fb0384b commit e982bfe
Show file tree
Hide file tree
Showing 17 changed files with 1,522 additions and 24 deletions.
1 change: 1 addition & 0 deletions config/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ type AccountPrivacy struct {
}

type PrivacySandbox struct {
TopicsDomain string `mapstructure:"topicsdomain"`
CookieDeprecation CookieDeprecation `mapstructure:"cookiedeprecation"`
}

Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,7 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
v.SetDefault("account_defaults.price_floors.fetch.max_age_sec", 86400)
v.SetDefault("account_defaults.price_floors.fetch.period_sec", 3600)
v.SetDefault("account_defaults.price_floors.fetch.max_schema_dims", 0)
v.SetDefault("account_defaults.privacy.privacysandbox.topicsdomain", "")
v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false)
v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800)

Expand Down
3 changes: 3 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ func TestDefaults(t *testing.T) {
cmpInts(t, "account_defaults.price_floors.fetch.period_sec", 3600, cfg.AccountDefaults.PriceFloors.Fetcher.Period)
cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 86400, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge)
cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 0, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims)
cmpStrings(t, "account_defaults.privacy.topicsdomain", "", cfg.AccountDefaults.Privacy.PrivacySandbox.TopicsDomain)
cmpBools(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled)
cmpInts(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)

Expand Down Expand Up @@ -528,6 +529,7 @@ account_defaults:
ipv4:
anon_keep_bits: 20
privacysandbox:
topicsdomain: "test.com"
cookiedeprecation:
enabled: true
ttl_sec: 86400
Expand Down Expand Up @@ -665,6 +667,7 @@ func TestFullConfig(t *testing.T) {
cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits)
cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 20, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits)

cmpStrings(t, "account_defaults.privacy.topicsdomain", "test.com", cfg.AccountDefaults.Privacy.PrivacySandbox.TopicsDomain)
cmpBools(t, "account_defaults.privacy.cookiedeprecation.enabled", true, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled)
cmpInts(t, "account_defaults.privacy.cookiedeprecation.ttl_sec", 86400, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)

Expand Down
12 changes: 9 additions & 3 deletions endpoints/openrtb2/amp_auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
w.Header().Set("AMP-Access-Control-Allow-Source-Origin", origin)
w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin")
w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
setBrowsingTopicsHeader(w, r)

// There is no body for AMP requests, so we pass a nil body and ignore the return value.
_, rejectErr := hookExecutor.ExecuteEntrypointStage(r, nilBody)
Expand Down Expand Up @@ -230,6 +231,11 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
return
}

// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
if errs := deps.setFieldsImplicitly(r, reqWrapper, account); len(errs) > 0 {
errL = append(errL, errs...)
}

hasStoredResponses := len(storedAuctionResponses) > 0
errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false)
errL = append(errL, errs...)
Expand Down Expand Up @@ -441,6 +447,9 @@ func getExtBidResponse(
warnings = make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)
}
for _, v := range errortypes.WarningOnly(errs) {
if errortypes.ReadScope(v) == errortypes.ScopeDebug && !(reqWrapper != nil && reqWrapper.Test == 1) {
continue
}
bidderErr := openrtb_ext.ExtBidderMessage{
Code: errortypes.ReadCode(v),
Message: v.Error(),
Expand Down Expand Up @@ -501,9 +510,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
// move to using the request wrapper
req = &openrtb_ext.RequestWrapper{BidRequest: reqNormal}

// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
deps.setFieldsImplicitly(httpRequest, req)

// Need to ensure cache and targeting are turned on
e = initAmpTargetingAndCache(req)
if errs = append(errs, e...); errortypes.ContainsFatalError(errs) {
Expand Down
86 changes: 85 additions & 1 deletion endpoints/openrtb2/amp_auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2052,7 +2052,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) {
)

for _, test := range testCases {
httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil)
httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp"+test.requestURLArguments, nil)
recorder := httptest.NewRecorder()

endpoint(recorder, httpReq, nil)
Expand Down Expand Up @@ -2479,3 +2479,87 @@ func TestSetSeatNonBid(t *testing.T) {
})
}
}

func TestAmpAuctionDebugWarningsOnly(t *testing.T) {
testCases := []struct {
description string
requestURLArguments string
addRequestHeaders func(r *http.Request)
expectedStatus int
expectedWarnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage
}{
{
description: "debug_enabled_request_with_invalid_Sec-Browsing-Topics_header",
requestURLArguments: "?tag_id=1&debug=1",
addRequestHeaders: func(r *http.Request) {
r.Header.Add("Sec-Browsing-Topics", "foo")
},
expectedStatus: 200,
expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{
"general": {
{
Code: 10012,
Message: "Invalid field in Sec-Browsing-Topics header: foo",
},
},
},
},
{
description: "debug_disabled_request_with_invalid_Sec-Browsing-Topics_header",
requestURLArguments: "?tag_id=1",
addRequestHeaders: func(r *http.Request) {
r.Header.Add("Sec-Browsing-Topics", "foo")
},
expectedStatus: 200,
expectedWarnings: nil,
},
}

storedRequests := map[string]json.RawMessage{
"1": json.RawMessage(validRequest(t, "site.json")),
}
exchange := &nobidExchange{}
endpoint, _ := NewAmpEndpoint(
fakeUUIDGenerator{},
exchange,
newParamsValidator(t),
&mockAmpStoredReqFetcher{storedRequests},
empty_fetcher.EmptyFetcher{},
&config.Configuration{
MaxRequestSize: maxSize,
AccountDefaults: config.Account{
Privacy: config.AccountPrivacy{
PrivacySandbox: config.PrivacySandbox{
TopicsDomain: "abc",
},
},
},
},
&metricsConfig.NilMetricsEngine{},
analyticsBuild.New(&config.Analytics{}),
map[string]string{},
[]byte{},
openrtb_ext.BuildBidderMap(),
empty_fetcher.EmptyFetcher{},
hooks.EmptyPlanBuilder{},
nil,
)

for _, test := range testCases {
httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil)
test.addRequestHeaders(httpReq)
recorder := httptest.NewRecorder()

endpoint(recorder, httpReq, nil)

assert.Equal(t, test.expectedStatus, recorder.Result().StatusCode)

// Parse Response
var response AmpResponse
if err := jsonutil.UnmarshalValid(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("Error unmarshalling response: %s", err.Error())
}

assert.Equal(t, test.expectedWarnings, response.ORTB2.Ext.Warnings)
}
}
59 changes: 51 additions & 8 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/prebid/prebid-server/v2/hooks"
"github.com/prebid/prebid-server/v2/ortb"
"github.com/prebid/prebid-server/v2/privacy"
"github.com/prebid/prebid-server/v2/privacysandbox"
"golang.org/x/net/publicsuffix"
jsonpatch "gopkg.in/evanphx/json-patch.v4"

Expand Down Expand Up @@ -61,6 +62,9 @@ const storedRequestTimeoutMillis = 50
const ampChannel = "amp"
const appChannel = "app"
const secCookieDeprecation = "Sec-Cookie-Deprecation"
const secBrowsingTopics = "Sec-Browsing-Topics"
const observeBrowsingTopics = "Observe-Browsing-Topics"
const observeBrowsingTopicsValue = "?1"

var (
dntKey string = http.CanonicalHeaderKey("DNT")
Expand Down Expand Up @@ -190,6 +194,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
}()

w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
setBrowsingTopicsHeader(w, r)

req, impExtInfoMap, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, account, errL := deps.parseRequest(r, &labels, hookExecutor)
if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) {
Expand Down Expand Up @@ -393,6 +398,13 @@ func sendAuctionResponse(
return labels, ao
}

// setBrowsingTopicsHeader always set the Observe-Browsing-Topics header to a value of ?1 if the Sec-Browsing-Topics is present in request
func setBrowsingTopicsHeader(w http.ResponseWriter, r *http.Request) {
if value := r.Header.Get(secBrowsingTopics); value != "" {
w.Header().Set(observeBrowsingTopics, observeBrowsingTopicsValue)
}
}

// parseRequest turns the HTTP request into an OpenRTB request. This is guaranteed to return:
//
// - A context which times out appropriately, given the request.
Expand All @@ -406,6 +418,7 @@ func sendAuctionResponse(
func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metrics.Labels, hookExecutor hookexecution.HookStageExecutor) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, storedAuctionResponses stored_responses.ImpsWithBidResponses, storedBidResponses stored_responses.ImpBidderStoredResp, bidderImpReplaceImpId stored_responses.BidderImpReplaceImpID, account *config.Account, errs []error) {
errs = nil
var err error
var errL []error
var r io.ReadCloser = httpRequest.Body
reqContentEncoding := httputil.ContentEncoding(httpRequest.Header.Get("Content-Encoding"))
if reqContentEncoding != "" {
Expand Down Expand Up @@ -532,7 +545,9 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
}

// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
deps.setFieldsImplicitly(httpRequest, req)
if errsL := deps.setFieldsImplicitly(httpRequest, req, account); len(errsL) > 0 {
errs = append(errs, errsL...)
}

if err := ortb.SetDefaults(req); err != nil {
errs = []error{err}
Expand All @@ -547,13 +562,14 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
lmt.ModifyForIOS(req.BidRequest)

//Stored auction responses should be processed after stored requests due to possible impression modification
storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errs = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher)
if len(errs) > 0 {
storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errL = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher)
if len(errL) > 0 {
errs = append(errs, errL...)
return nil, nil, nil, nil, nil, nil, errs
}

hasStoredResponses := len(storedAuctionResponses) > 0
errL := deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest)
errL = deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest)
if len(errL) > 0 {
errs = append(errs, errL...)
}
Expand Down Expand Up @@ -876,7 +892,7 @@ func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http
return append(errL, err)
}

if err := validateOrFillCDep(httpReq, req, account); err != nil {
if err := validateOrFillCookieDeprecation(httpReq, req, account); err != nil {
errL = append(errL, err)
}

Expand Down Expand Up @@ -1919,7 +1935,7 @@ func validateDevice(device *openrtb2.Device) error {
return nil
}

func validateOrFillCDep(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error {
func validateOrFillCookieDeprecation(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error {
if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled {
return nil
}
Expand Down Expand Up @@ -2029,7 +2045,7 @@ func sanitizeRequest(r *openrtb_ext.RequestWrapper, ipValidator iputil.IPValidat
// OpenRTB properties from the headers and other implicit info.
//
// This function _should not_ override any fields which were defined explicitly by the caller in the request.
func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) {
func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error {
sanitizeRequest(r, deps.privateNetworkIPValidator)

setDeviceImplicitly(httpReq, r, deps.privateNetworkIPValidator)
Expand All @@ -2041,14 +2057,16 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_
}

setAuctionTypeImplicitly(r)

errs := setSecBrowsingTopicsImplicitly(httpReq, r, account)
return errs
}

// setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device
func setDeviceImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, ipValidtor iputil.IPValidator) {
setIPImplicitly(httpReq, r, ipValidtor)
setUAImplicitly(httpReq, r)
setDoNotTrackImplicitly(httpReq, r)

}

// setAuctionTypeImplicitly sets the auction type to 1 if it wasn't on the request,
Expand All @@ -2059,6 +2077,31 @@ func setAuctionTypeImplicitly(r *openrtb_ext.RequestWrapper) {
}
}

// setSecBrowsingTopicsImplicitly updates user.data with data from request header 'Sec-Browsing-Topics'
func setSecBrowsingTopicsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error {
secBrowsingTopics := httpReq.Header.Get(secBrowsingTopics)
if secBrowsingTopics == "" {
return nil
}

// host must configure privacy sandbox
if account == nil || account.Privacy.PrivacySandbox.TopicsDomain == "" {
return nil
}

topics, errs := privacysandbox.ParseTopicsFromHeader(secBrowsingTopics)
if len(topics) == 0 {
return errs
}

if r.User == nil {
r.User = &openrtb2.User{}
}

r.User.Data = privacysandbox.UpdateUserDataWithTopics(r.User.Data, topics, account.Privacy.PrivacySandbox.TopicsDomain)
return errs
}

func setSiteImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) {
if r.Site == nil {
r.Site = &openrtb2.Site{}
Expand Down
Loading

0 comments on commit e982bfe

Please sign in to comment.