Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Privacy Sandbox: Topics in headers #3393

Merged
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
eeba105
Privacy Sandbox: support testing label header #3297
pm-nilesh-chate Jan 3, 2024
d8da213
udpate device.ext.cdep
pm-nilesh-chate Jan 3, 2024
fdd72c6
support Topics in headers #3008
pm-nilesh-chate Jan 10, 2024
c73d388
add ut
pm-nilesh-chate Jan 11, 2024
898a1e8
update user object only when topics available
pm-nilesh-chate Jan 11, 2024
ef7179d
add ut
pm-nilesh-chate Jan 11, 2024
356c762
minor refactor
pm-nilesh-chate Jan 11, 2024
a16d110
add comments and minor refactoring
pm-nilesh-chate Jan 16, 2024
a7945f7
revert cookie deprecation changes
pm-nilesh-chate Jan 16, 2024
ebe802d
topicsdmain must be configured
pm-nilesh-chate Jan 19, 2024
5e3d1ff
set Sec-Browsing-Topics response header
pm-nilesh-chate Jan 23, 2024
6313d1c
refactor as per review comments
pm-nilesh-chate Jan 25, 2024
5a00ce6
move privacysandbox to account config
pm-nilesh-chate Jan 30, 2024
1ef804e
minor comment
pm-nilesh-chate Jan 30, 2024
e0979c7
Merge remote-tracking branch 'upstream-vanilla/master' into issue-300…
pm-nilesh-chate Jan 30, 2024
c8a9e22
merge upstream master
pm-nilesh-chate Jan 30, 2024
b566e82
fix typo
pm-nilesh-chate Feb 7, 2024
6a947af
add validation test for video endpoint
pm-nilesh-chate Feb 7, 2024
54f21b6
add validation test for amp endpoint
pm-nilesh-chate Feb 7, 2024
207c7a1
fix variable naming
pm-nilesh-chate Feb 8, 2024
96fbbcd
Merge remote-tracking branch 'upstream-vanilla/master' into issue-300…
pm-nilesh-chate Mar 6, 2024
d3fc19d
fix setFieldsImplicitly for video endpoint
pm-nilesh-chate Mar 6, 2024
365c09d
rename validateOrFillCookieDeprecation
pm-nilesh-chate Mar 7, 2024
9384936
Revert "add validation test for amp endpoint"
pm-nilesh-chate Mar 13, 2024
11e185e
rename file and minor refactor
pm-nilesh-chate Mar 13, 2024
c33bc1e
prc + minor test sort fix
pm-nilesh-chate Mar 13, 2024
78afa8f
prc
pm-nilesh-chate Mar 14, 2024
c4b9998
fix ut
pm-nilesh-chate Mar 14, 2024
997602d
add test and doc for user data with duplicate entries
pm-nilesh-chate Mar 14, 2024
0f99b76
fix typo
pm-nilesh-chate Mar 15, 2024
3381909
add debug logging
pm-nilesh-chate Mar 20, 2024
54dd5ec
implement logging via logging scope interface
pm-nilesh-chate Mar 21, 2024
54bf9e2
prc
pm-nilesh-chate Mar 27, 2024
ca89931
prc
pm-nilesh-chate Apr 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"`
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
CookieDeprecation CookieDeprecation `mapstructure:"cookiedeprecation"`
}

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...)
}
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved

hasStoredResponses := len(storedAuctionResponses) > 0
errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false)
errL = append(errL, errs...)
Expand Down Expand Up @@ -436,6 +442,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
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
}
bidderErr := openrtb_ext.ExtBidderMessage{
Code: errortypes.ReadCode(v),
Message: v.Error(),
Expand Down Expand Up @@ -496,9 +505,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
74 changes: 74 additions & 0 deletions endpoints/openrtb2/amp_auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/url"
"os"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -2404,3 +2405,76 @@ func TestSetSeatNonBid(t *testing.T) {
})
}
}

func TestAmpAuctionDebugWarningsOnly(t *testing.T) {
testCases := []struct {
description string
requestURLArguments string
addRequestHeaders func(r *http.Request)
expectedStatus int
expectedWarnings []string
}{
{
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: []string{"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)
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
test.addRequestHeaders(httpReq)
recorder := httptest.NewRecorder()

endpoint(recorder, httpReq, nil)

assert.Equal(t, test.expectedStatus, recorder.Result().StatusCode)
respBody := recorder.Body.String()
for _, warning := range test.expectedWarnings {
assert.True(t, strings.Contains(respBody, warning), respBody)
}
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
}
}
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...)
}
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -1927,7 +1943,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 @@ -2037,7 +2053,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 @@ -2049,14 +2065,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 @@ -2067,6 +2085,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