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 1 commit
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
4 changes: 3 additions & 1 deletion endpoints/openrtb2/amp_auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
}

// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
deps.setFieldsImplicitly(r, reqWrapper, account)
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)
Expand Down
30 changes: 18 additions & 12 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ func setBrowsingTopicsHeader(w http.ResponseWriter, r *http.Request) {
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 @@ -544,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, account)
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 @@ -559,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 @@ -2049,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, account *config.Account) {
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 @@ -2062,7 +2066,8 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_

setAuctionTypeImplicitly(r)

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

// setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device
Expand All @@ -2081,27 +2086,28 @@ 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) {
secBrowsingTopics := httpReq.Header.Get("Sec-Browsing-Topics")
func setSecBrowsingTopicsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error {
secBrowsingTopics := httpReq.Header.Get(secBrowsingTopics)
if secBrowsingTopics == "" {
return
return nil
}

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

topics := privacysandbox.ParseTopicsFromHeader(secBrowsingTopics)
topics, errs := privacysandbox.ParseTopicsFromHeader(secBrowsingTopics)
if len(topics) == 0 {
return
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) {
Expand Down
10 changes: 8 additions & 2 deletions endpoints/openrtb2/video_auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,16 +299,21 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
}

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

errL = deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false)
errs := deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false)
errL = append(errL, errs...)
if errortypes.ContainsFatalError(errL) {
handleError(&labels, w, errL, &vo, &debugLog)
return
}

activityControl = privacy.NewActivityControl(&account.Privacy)

warnings := errortypes.DebugWarningOnly(errL)
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved

secGPC := r.Header.Get("Sec-GPC")
auctionRequest := &exchange.AuctionRequest{
BidRequestWrapper: bidReqWrapper,
Expand All @@ -317,6 +322,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
RequestType: labels.RType,
StartTime: start,
LegacyLabels: labels,
Warnings: warnings,
GlobalPrivacyControlHeader: secGPC,
PubID: labels.PubID,
HookExecutor: hookexecution.EmptyHookExecutor{},
Expand Down
1 change: 1 addition & 0 deletions errortypes/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
FloorBidRejectionWarningCode
InvalidBidResponseDSAWarningCode
SecCookieDeprecationLenWarningCode
SecBrowsingTopicsWarningCode
)

// Coder provides an error or warning code with severity.
Expand Down
19 changes: 19 additions & 0 deletions errortypes/errortypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,22 @@ func (err *FailedToMarshal) Code() int {
func (err *FailedToMarshal) Severity() Severity {
return SeverityFatal
}

// DebugWarning is a generic non-fatal error used in debug mode. Throughout the codebase, an error can
// only be a warning if it's of the type defined below
type DebugWarning struct {
Message string
WarningCode int
}

func (err *DebugWarning) Error() string {
return err.Message
}

func (err *DebugWarning) Code() int {
return err.WarningCode
}

func (err *DebugWarning) Severity() Severity {
return SeverityWarning
}
25 changes: 25 additions & 0 deletions errortypes/severity.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const (
// SeverityWarning represents a non-fatal bid processing error where invalid or ambiguous
// data in the bid request was ignored.
SeverityWarning

// SeverityDebugWarning represents a non-fatal bid processing error where invalid or ambiguous
// data in the bid request was ignored and to be logged in debug mode.
SeverityDebugWarning
)

func isFatal(err error) bool {
Expand All @@ -28,6 +32,14 @@ func IsWarning(err error) bool {
return ok && s.Severity() == SeverityWarning
}

// IsDebugWarning returns true if an error is labeled with a Severity of SeverityDebugWarning
// Throughout the codebase, errors with SeverityDebugWarning are of the type Warning
// defined in this package
func IsDebugWarning(err error) bool {
_, ok := err.(*DebugWarning)
return ok
}

// ContainsFatalError checks if the error list contains a fatal error.
func ContainsFatalError(errors []error) bool {
for _, err := range errors {
Expand Down Expand Up @@ -64,3 +76,16 @@ func WarningOnly(errs []error) []error {

return errsWarning
}

// DebugWarningOnly returns a new error list with only the debug warning severity errors.
func DebugWarningOnly(errs []error) []error {
errsDebugWarning := make([]error, 0, len(errs))

for _, err := range errs {
if _, ok := err.(*DebugWarning); ok {
errsDebugWarning = append(errsDebugWarning, err)
}
}

return errsDebugWarning
}
3 changes: 3 additions & 0 deletions exchange/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
}

for _, warning := range r.Warnings {
if errortypes.IsDebugWarning(warning) && !responseDebugAllow {
continue
}
generalWarning := openrtb_ext.ExtBidderMessage{
Code: errortypes.ReadCode(warning),
Message: warning.Error(),
Expand Down
58 changes: 39 additions & 19 deletions privacysandbox/topics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package privacysandbox

import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v2/errortypes"
"github.com/prebid/prebid-server/v2/util/jsonutil"
)

Expand All @@ -16,30 +19,33 @@ type Topic struct {
}

// ParseTopicsFromHeader parses the Sec-Browsing-Topics header data into Topics object
func ParseTopicsFromHeader(secBrowsingTopics string) []Topic {
func ParseTopicsFromHeader(secBrowsingTopics string) ([]Topic, []error) {
topics := make([]Topic, 0, 10)
warnings := make([]error, 0)
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved

for _, seg := range strings.Split(secBrowsingTopics, ",") {
if topic, ok := parseTopicSegment(seg); ok {
topics = append(topics, topic)
for _, field := range strings.Split(secBrowsingTopics, ",") {
field = strings.TrimSpace(field)
if field == "" || strings.HasPrefix(field, "();p=") {
continue
}

if len(topics) == 10 {
break
if len(topics) < 10 {
if topic, ok := parseTopicSegment(field); ok {
topics = append(topics, topic)
} else {
warnings = addWarning(warnings, field)
pm-nilesh-chate marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
warnings = addWarning(warnings, field+" discarded due to limit reached.")
}
}

return topics
return topics, warnings
}

// parseTopicSegment parses a single topic segment from the header into Topics object
func parseTopicSegment(seg string) (Topic, bool) {
seg = strings.TrimSpace(seg)
if seg == "" || strings.HasPrefix(seg, "();p=") {
return Topic{}, false
}

segment := strings.Split(seg, ";")
func parseTopicSegment(field string) (Topic, bool) {
segment := strings.Split(field, ";")
if len(segment) != 2 {
return Topic{}, false
}
Expand All @@ -54,10 +60,15 @@ func parseTopicSegment(seg string) (Topic, bool) {
return Topic{}, false
}

segIDs, err := parseSegmentIDs(segmentsIDs[1 : len(segmentsIDs)-1])
if err != nil {
return Topic{}, false
}

return Topic{
SegTax: segtax,
SegClass: segclass,
SegIDs: parseSegmentIDs(segmentsIDs[1 : len(segmentsIDs)-1]),
SegIDs: segIDs,
}, true
}

Expand All @@ -80,16 +91,18 @@ func parseSegTaxSegClass(seg string) (int, string) {
}

// parseSegmentIDs parses the segment ids from the header string into int array
func parseSegmentIDs(segmentsIDs string) []int {
func parseSegmentIDs(segmentsIDs string) ([]int, error) {
var selectedSegmentIDs []int
for _, segmentID := range strings.Fields(segmentsIDs) {
segmentID = strings.TrimSpace(segmentID)
if selectedSegmentID, err := strconv.Atoi(segmentID); err == nil && selectedSegmentID > 0 {
selectedSegmentIDs = append(selectedSegmentIDs, selectedSegmentID)
selectedSegmentID, err := strconv.Atoi(segmentID)
if err != nil || selectedSegmentID <= 0 {
return selectedSegmentIDs, errors.New("invalid segment id")
}
selectedSegmentIDs = append(selectedSegmentIDs, selectedSegmentID)
}

return selectedSegmentIDs
return selectedSegmentIDs, nil
}

func UpdateUserDataWithTopics(userData []openrtb2.Data, headerData []Topic, topicsDomain string) []openrtb2.Data {
Expand Down Expand Up @@ -206,3 +219,10 @@ func findNewSegIDs(dataName, topicsDomain string, userData Topic, userDataSegmen

return segIDs
}

func addWarning(warnings []error, msg string) []error {
return append(warnings, &errortypes.DebugWarning{
WarningCode: errortypes.SecBrowsingTopicsWarningCode,
Message: fmt.Sprintf("Invalid field in Sec-Browsing-Topics header: %s", msg),
})
}