-
Notifications
You must be signed in to change notification settings - Fork 760
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
Changes from all commits
db4407f
916b092
a0da060
d5dbacc
e2d12b7
f8f3f44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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} | ||
} | ||
} | ||
|
||
if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) { | ||
|
@@ -362,6 +366,11 @@ func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases | |
return nil | ||
} | ||
|
||
func validateSChains(req *openrtb_ext.ExtRequest) error { | ||
_, err := exchange.BidderToPrebidSChains(req) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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":{}}`), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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"}}`), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
|
@@ -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) | ||
} |
There was a problem hiding this comment.
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 filesendpoints/openrtb2/amp_auction.go
andendpoints/openrtb2/video_auction.go
we could probably alsovalidateSChains(bidExt)
from there in order to prevent a defective "ext"JSON field from getting all the way tosplitBidRequests
There was a problem hiding this comment.
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
andopenrtb2/video_auction.go
endpoints both call thevalidateRequest
method inopenrtb2/auction.go
which callsvalidateSChains
so we're covered. Those endpoints forgo processing if the method returns an endpoint as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh I see