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

Add support for multiple root schain nodes #1374

Merged
merged 6 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error {
if err := validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil {
return []error{err}
}

if err := validateSChains(bidExt); err != nil {
return []error{err}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*openrtb_ext.ExtRequest also gets unmarshaled and validated in files endpoints/openrtb2/amp_auction.go and endpoints/openrtb2/video_auction.go we could probably also validateSChains(bidExt) from there in order to prevent a defective "ext"JSON field from getting all the way to splitBidRequests

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The openrtb2/amp_auction.go and openrtb2/video_auction.go endpoints both call the validateRequest method in openrtb2/auction.go which calls validateSChains so we're covered. Those endpoints forgo processing if the method returns an endpoint as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh I see

}

if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) {
Expand Down Expand Up @@ -362,6 +366,11 @@ func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases
return nil
}

func validateSChains(req *openrtb_ext.ExtRequest) error {
_, err := exchange.BidderToPrebidSChains(req)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a simplified version that just checks for duplicate bidders, or perhaps save the result from here for use later?

return err
}

func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]string, index int) []error {
if imp.ID == "" {
return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)}
Expand Down
47 changes: 47 additions & 0 deletions endpoints/openrtb2/auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,53 @@ func TestCCPAInvalid(t *testing.T) {
assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request")
}

func TestSChainInvalid(t *testing.T) {
deps := &endpointDeps{
&nobidExchange{},
newParamsValidator(t),
&mockStoredReqFetcher{},
empty_fetcher.EmptyFetcher{},
empty_fetcher.EmptyFetcher{},
&config.Configuration{},
pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}),
analyticsConf.NewPBSAnalytics(&config.Analytics{}),
map[string]string{},
false,
[]byte{},
openrtb_ext.BidderMap,
nil,
nil,
hardcodedResponseIPValidator{response: true},
}

ui := uint64(1)
req := openrtb.BidRequest{
ID: "someID",
Imp: []openrtb.Imp{
{
ID: "imp-ID",
Banner: &openrtb.Banner{
W: &ui,
H: &ui,
},
Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`),
},
},
Site: &openrtb.Site{
ID: "myID",
},
Regs: &openrtb.Regs{
Ext: json.RawMessage(`{"us_privacy":"abcd"}`),
},
Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
}

errL := deps.validateRequest(&req)

expectedError := fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.")
assert.ElementsMatch(t, errL, []error{expectedError})
}

func TestSanitizeRequest(t *testing.T) {
testCases := []struct {
description string
Expand Down
94 changes: 93 additions & 1 deletion exchange/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ type cleanMetrics struct {
gdprTcfVersion int
}

func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) {
bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)

if len(req.Prebid.SChains) == 0 {
return bidderToSChains, nil
}

for _, schainWrapper := range req.Prebid.SChains {
if schainWrapper != nil && len(schainWrapper.Bidders) > 0 {
for _, bidder := range schainWrapper.Bidders {
if _, present := bidderToSChains[bidder]; present {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems strange. If a bidder is in the list, and that bidder also has an entry in bidderToSChains, we delete that entry, otherwise we add an entry for the bidder into the map? This would mean if a bidder appears an even number of times, it will be absent from the map. If it appears an odd number of times, it will be in the map with the last SChain it was seen with. What is the logic supposed to be here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I don't see any info in the spec or in the applicable github issues/PRs about how to handle the case where multiple non-wildcard schains are defined for a bidder. The java version appears to take the same approach of ignoring all schains for a bidder if multiple are defined for a bidder, which is where I got the idea from. I'm not a java expert but it looks like the java version has the same bug. I'll confirm that this is the expected behavior and update the logic accordingly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The team decided that this is a data error that should be detected during request validation preventing further processing.

return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+
"it must contain no more than one per bidder.", bidder)
} else {
bidderToSChains[bidder] = &schainWrapper.SChain
}
}
}
}

return bidderToSChains, nil
}

// cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
//
// 1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder.
Expand Down Expand Up @@ -103,12 +126,35 @@ func cleanOpenRTBRequests(ctx context.Context,
return
}

func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.Imp, aliases map[string]string, usersyncs IdFetcher, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) {
func splitBidRequest(req *openrtb.BidRequest,
impsByBidder map[string][]openrtb.Imp,
aliases map[string]string,
usersyncs IdFetcher,
blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) {

requestsByBidder := make(map[openrtb_ext.BidderName]*openrtb.BidRequest, len(impsByBidder))
explicitBuyerUIDs, err := extractBuyerUIDs(req.User)
if err != nil {
return nil, []error{err}
}

var requestExt openrtb_ext.ExtRequest
var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain
if len(req.Ext) > 0 {
err := json.Unmarshal(req.Ext, &requestExt)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This data is also unmarshaled here: https://github.com/prebid/prebid-server/blob/master/exchange/exchange.go#L118-L122

I'd rather only unmarshal once but things might get a little convoluted without a refactor so I figured I would punt on that for now.

if err != nil {
return nil, []error{err}
}

sChainsByBidder, err = BidderToPrebidSChains(&requestExt)
if err != nil {
return nil, []error{err}
}
} else {
sChainsByBidder = make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)
}

for bidder, imps := range impsByBidder {
reqCopy := *req
coreBidder := resolveBidder(bidder, aliases)
Expand All @@ -128,11 +174,57 @@ func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.
blabels[coreBidder].CookieFlag = pbsmetrics.CookieFlagYes
}
reqCopy.Imp = imps

prepareSource(&reqCopy, bidder, sChainsByBidder)
prepareExt(&reqCopy, &requestExt)

requestsByBidder[openrtb_ext.BidderName(bidder)] = &reqCopy
}
return requestsByBidder, nil
}

func prepareExt(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) {
if len(req.Ext) == 0 {
return
}
extCopy := *unpackedExt
extCopy.Prebid.SChains = nil
reqExt, err := json.Marshal(extCopy)
if err == nil {
req.Ext = reqExt
}
}

func prepareSource(req *openrtb.BidRequest, bidder string, sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) {
const sChainWildCard = "*"
var selectedSChain *openrtb_ext.ExtRequestPrebidSChainSChain

wildCardSChain := sChainsByBidder[sChainWildCard]
bidderSChain := sChainsByBidder[bidder]

// source should not be modified
if bidderSChain == nil && wildCardSChain == nil {
return
}

if bidderSChain != nil {
selectedSChain = bidderSChain
} else {
selectedSChain = wildCardSChain
}

// set source
var source openrtb.Source
schain := openrtb_ext.ExtRequestPrebidSChain{
SChain: *selectedSChain,
}
sourceExt, err := json.Marshal(schain)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a performance optimization opportunity here that I punted on. We could cache each marshalled schain and copy the byte array to sourceExt instead of marshalling again. I didn't think this was worth it for a first pass but if others think it is worth exploring now, LMK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Application wide we are guilty of re-marshaling a ton. I agree this is fine for now and part of a much larger performance fix in the (near?) future.

if err == nil {
source.Ext = sourceExt
req.Source = &source
}
}

// extractBuyerUIDs parses the values from user.ext.prebid.buyeruids, and then deletes those values from the ext.
// This prevents a Bidder from using these values to figure out who else is involved in the Auction.
func extractBuyerUIDs(user *openrtb.User) (map[string]string, error) {
Expand Down
180 changes: 180 additions & 0 deletions exchange/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,92 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
}
}

func TestCleanOpenRTBRequestsSChain(t *testing.T) {
testCases := []struct {
description string
inSourceExt json.RawMessage
inExt json.RawMessage
outSourceExt json.RawMessage
outExt json.RawMessage
hasError bool
}{
{
description: "Empty root ext and source ext",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(``),
outSourceExt: json.RawMessage(``),
outExt: json.RawMessage(``),
hasError: false,
},
{
description: "No schains in root ext and empty source ext",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"schains":[]}}`),
outSourceExt: json.RawMessage(``),
outExt: json.RawMessage(`{"prebid":{}}`),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the expected behavior should be to return an empty prebid or if the key should be deleted. I think deleting the key makes more sense but I didn't go down that path because of performance concerns when performing a deep equal comparison using the reflect package to check for an empty struct prior to marshaling. I also don't think you can compare the struct that maps to prebid to a zero value struct since it has nested structs. I'm looking for some feedback here.

hasError: false,
},
{
description: "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains",
inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My hope is that we can eventually come up with a better way to set raw json inputs and validate raw json outputs since this is pretty hard to read.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could define the strings elsewhere, where you can include newlines and spacing for readability, and then json.RawMessage those variables at this line. It does move the JSON further away from the test, so there is a tradeoff.

inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{}}`),
hasError: false,
},
{
description: "Use schain for bidder in ext.prebid.schains",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{}}`),
hasError: false,
},
{
description: "Use wildcard schain in ext.prebid.schains",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{}}`),
hasError: false,
},
{
description: "Use schain for bidder in ext.prebid.schains instead of wildcard",
inSourceExt: json.RawMessage(``),
inExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`),
outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
outExt: json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`),
hasError: false,
},
{
description: "Use source schain -- multiple (two) bidder schains in ext.prebid.schains",
inSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
inExt: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
outSourceExt: nil,
outExt: nil,
hasError: true,
},
}

for _, test := range testCases {
req := newBidRequest(t)
req.Source.Ext = test.inSourceExt
req.Ext = test.inExt

results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{})
result := results["appnexus"]

if test.hasError == true {
assert.NotNil(t, errs)
assert.Nil(t, result)
} else {
assert.Nil(t, errs)
assert.Equal(t, test.outSourceExt, result.Source.Ext, test.description+":Source.Ext")
assert.Equal(t, test.outExt, result.Ext, test.description+":Ext")
}
}
}

func TestCleanOpenRTBRequestsLMT(t *testing.T) {
var (
enabled int8 = 1
Expand Down Expand Up @@ -302,5 +388,99 @@ func TestRandomizeList(t *testing.T) {
if len(adapters) != 1 {
t.Errorf("RandomizeList, expected a list of 1, found %d", len(adapters))
}
}

func TestBidderToPrebidChains(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: []*openrtb_ext.ExtRequestPrebidSChain{
{
Bidders: []string{"Bidder1", "Bidder2"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{
Complete: 1,
Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{
{
ASI: "asi1",
SID: "sid1",
Name: "name1",
RID: "rid1",
Domain: "domain1",
HP: 1,
},
{
ASI: "asi2",
SID: "sid2",
Name: "name2",
RID: "rid2",
Domain: "domain2",
HP: 2,
},
},
Ver: "version1",
},
},
{
Bidders: []string{"Bidder3", "Bidder4"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{},
},
},
},
}

output, err := BidderToPrebidSChains(&input)

assert.Nil(t, err)
assert.Equal(t, len(output), 4)
assert.Same(t, output["Bidder1"], &input.Prebid.SChains[0].SChain)
assert.Same(t, output["Bidder2"], &input.Prebid.SChains[0].SChain)
assert.Same(t, output["Bidder3"], &input.Prebid.SChains[1].SChain)
assert.Same(t, output["Bidder4"], &input.Prebid.SChains[1].SChain)
}

func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: []*openrtb_ext.ExtRequestPrebidSChain{
{
Bidders: []string{"Bidder1"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{},
},
{
Bidders: []string{"Bidder1", "Bidder2"},
SChain: openrtb_ext.ExtRequestPrebidSChainSChain{},
},
},
},
}

output, err := BidderToPrebidSChains(&input)

assert.NotNil(t, err)
assert.Nil(t, output)
}

func TestBidderToPrebidChainsNilSChains(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: nil,
},
}

output, err := BidderToPrebidSChains(&input)

assert.Nil(t, err)
assert.Equal(t, len(output), 0)
}

func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) {
input := openrtb_ext.ExtRequest{
Prebid: openrtb_ext.ExtRequestPrebid{
SChains: []*openrtb_ext.ExtRequestPrebidSChain{},
},
}

output, err := BidderToPrebidSChains(&input)

assert.Nil(t, err)
assert.Equal(t, len(output), 0)
}
Loading