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

Single stored auction response #2132

Merged
merged 10 commits into from
Mar 1, 2022
Merged

Conversation

VeronikaSolovei9
Copy link
Contributor

@VeronikaSolovei9 VeronikaSolovei9 commented Jan 13, 2022

Issue: #861
Implementation of Single Stored Auction Response

@VeronikaSolovei9 VeronikaSolovei9 force-pushed the single_stored_response branch 2 times, most recently from d17d316 to 68c1a5c Compare February 7, 2022 09:48
@VeronikaSolovei9 VeronikaSolovei9 marked this pull request as ready for review February 9, 2022 06:25
// - there is only stored request id in incoming request
// - there is at least one stored imp with stored auction response
// Need to update ImpExtPrebid after stored responses/impressions applied to incoming request
impInfo, errs := parseImpInfo(requestJson)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of calling parseImpInfo(requestJson) again here, can we pass the impInfo calculated in line 289 just as we do in the processStoredRequests call of line 295?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, this is how it was before I talked to Bret about we need to support stored responses in stored requests. Imp ext can be modified by applying store request and this stored request may have stored response in it. This is why we call processStoredRequests first and processStoredAuctionResponses then. I added a comment above with explanations why we need to call it here again (L1572-1576)

@@ -316,27 +318,27 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
}

// At this point, we should have a valid request that definitely has Targeting and Cache turned on
e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req}, true)
e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req}, true, false)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious. Why are we using false and not evaluating whether or not we have elements in the storedAuctionResponses array as we do in endpoints/openrtb2/auction.go?

321   -    e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req}, true, false)
321   +    e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req}, true, len(storedAuctionResponses) > 0)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, fixed, thank you!

@@ -249,7 +249,7 @@ 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, bidReq) // move after merge

errL = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: bidReq}, false)
errL = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: bidReq}, false, false)
Copy link
Contributor

Choose a reason for hiding this comment

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

I see we don't parse the storedAuctionResponses in this endpoint as compared with the others? Does that feature don't apply to video?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no way to pass custom imp.ext for video endpoint at this time. We don't support storedResponses for video requests.


} else {
adapterBids, adapterExtra, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Function call liveAdapters := listBiddersWithRequests(bidderRequests) would go to waste if check in line 220 evaluates to true. Given that liveAdapters will be used downstream, can we optimize a bit by calling listBiddersWithRequests(bidderRequests) only if needed?

205 -   // List of bidders we have requests for.
206 -   liveAdapters := listBiddersWithRequests(bidderRequests)
207
208     // If we need to cache bids, then it will take some time to call prebid cache.
209     // We should reduce the amount of time the bidders have, to compensate.
210     auctionCtx, cancel := e.makeAuctionContext(ctx, cacheInstructions.cacheBids)
211     defer cancel()
212
213     // Get currency rates conversions for the auction
214     conversions := e.getAuctionCurrencyRates(requestExt.Prebid.CurrencyConversions)
215
216     var adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid
217     var adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra
218     var anyBidsReturned bool
    +   var liveAdapters []openrtb_ext.BidderName
219
220     if len(r.StoredAuctionResponses) > 0 {
221         adapterBids, liveAdapters, err = buildStoreAuctionResponse(r.StoredAuctionResponses)
222         if err != nil {
223             return nil, err
224         }
225         anyBidsReturned = true
226
227     } else {
    +       liveAdapters = listBiddersWithRequests(bidderRequests)
228         adapterBids, adapterExtra, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride)
229     }
exchange/exchange.go

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, moved

@@ -3667,6 +3667,190 @@ func TestFPD(t *testing.T) {

}

func TestStoredAuctionResponses(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with TestStoredAuctionResponses because it calls HoldAuction but, can we also create a unit test to assert buildStoreAuctionResponse by its own? I don't have a problem if we keep both test cases. I'd like to hear @bsardo's take on this though.

Copy link
Contributor Author

@VeronikaSolovei9 VeronikaSolovei9 Feb 16, 2022

Choose a reason for hiding this comment

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

func buildStoreAuctionResponse has 100% coverage (except of unmarshal json error handler).
TestStoredAuctionResponses is mostly written to test buildStoreAuctionResponse, like you said separate test will be sort of copy of existing one.
Also this is a private function.
I'm ok to add extra test for it, will do what you think is better.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm it's kind of a matter of personal preference. The coverage is there. I agree private functions generally don't need their own dedicated unit test if the function is simplistic.
With that said, I would lean towards writing a unit test for this function given that there's a bit going on there and simplifying this test so it just focuses on exercising the conditional if len(r.StoredAuctionResponses) > 0 { in exchange.go but I understand if we don't want to do that at this point :)

Comment on lines +1540 to +1622
if len(storedAuctionResponseIds) > 0 {
storedAuctionResponses, errs := deps.storedRespFetcher.FetchResponses(ctx, storedAuctionResponseIds)
if len(errs) > 0 {
return nil, errs
}
for impId, respId := range impIdToRespId {
impIdToStoredResp[impId] = storedAuctionResponses[respId]
}
return impIdToStoredResp, nil
}
return nil, nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it make sense to fetch responses here? What if this function just produced a map of imp ids to stored auction response ids leaving the stored auction response fetching and response building near where bid response fetching and and response building typically happen in HoldAuction where we call getAllBids?
It seems a little less than ideal to me to fetch any kind of response before we've finished parsing and validating the request. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To fetch stored responses in getAllBids we will need to pass fetcher all the way down the stack. We don't have this option now.
Also processStoredAuctionResponses function is a part of parseRequest where previous function is processStoredRequests. To me it makes sense to have it all in one place and fetch extra data sooner (and error out in case of errors) than later.
Fetching bids is an http calls, not fetcher calls, it would be confusing to mix this logic. In this case we don't even go that deep, we don't call getAllBids if there were stored responses.

@@ -315,7 +322,7 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_

lmt.ModifyForIOS(req.BidRequest)

errL := deps.validateRequest(req, false)
errL := deps.validateRequest(req, false, len(storedAuctionResponses) > 0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick: can we put this conditional on its own line? It makes it a bit easier to read IMO and it will help us ensure test coverage when using the coverage tooling.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean to extract it to variable? Like hasStoredResponses := len(storedAuctionResponses) > 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If yes - done :)

Comment on lines 1580 to 1584
// Input request may have stored request or stored imps that will modify imps from incoming request
// Cases:
// - there is only stored request id in incoming request
// - there is at least one stored imp with stored auction response
// Need to update ImpExtPrebid after stored responses/impressions applied to incoming request
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we modify this comment slightly and perhaps move it just above the method signature?
Maybe something like:
processStoredAuctionResponses takes the incoming request as JSON with any stored requests/imps already merged into it, scans it to find any stored auction response ids in the request/imps and produces a map of imp IDs to stored auction responses. Note that processStoredAuctionResponses must be called after processStoredRequests because stored imps and stored requests can contain stored auction responses so the stored requests/imps have to be merged into the incoming request prior to processing stored auction responses.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +1602 to +1608
impId, err := jsonparser.GetString(impData.Imp, "id")
if err != nil {
return nil, []error{err}
}

impIdToRespId[impId] = impData.ImpExtPrebid.StoredAuctionResponse.ID
Copy link
Collaborator

Choose a reason for hiding this comment

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

This logic seems like it could result in some unexpected behavior if the imp IDs are not unique.
Suppose the request has three imps each mapping to the following stored auction responses:
imp-id-1 --> resp-1
imp-id-2 --> resp-2
imp-id-1 --> resp-3
It appears the two imps with id imp-id-1 will have resp-3.
I don't think we perform imp uniqueness validation, however, this scenario could produce unexpected results. Do we need to handle this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Imp ids must be unique per request, the situation you described is not possible.
It can be the situation where 2 or more imps have same stored response:
imp-id-1 --> resp-1
imp-id-2 --> resp-1
but in this case fetcher handles this properly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Gotcha. I see that imp IDs are guaranteed to be unique at this point because we ensured this during validation in endpoints/openrtb2/auction.go#validateRequest.

@@ -1076,3 +1093,39 @@ func listBiddersWithRequests(bidderRequests []BidderRequest) []openrtb_ext.Bidde

return liveAdapters
}

func buildStoreAuctionResponse(storedActionResponses map[string]json.RawMessage) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []openrtb_ext.BidderName, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick: typo - I think storedActionResponses should be storedAuctionResponses?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, renamed

Copy link
Collaborator

Choose a reason for hiding this comment

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

@VeronikaSolovei9 I see you modified the function name, which looks good, but I was referring to the first parameter storedActionResponses which should be storedAuctionResponses.


if impData.ImpExtPrebid.StoredAuctionResponse != nil {
if len(impData.ImpExtPrebid.StoredAuctionResponse.ID) == 0 {
return nil, []error{fmt.Errorf("request.imp[%d] has ext.prebid.storedauctionresponse specified, but \"id\" field is missing ", index)}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add test coverage for this error case, perhaps to TestParseRequestStoredResponses?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added TestProcessStoredAuctionResponsesErrors

storedAuctionResponseIds = append(storedAuctionResponseIds, impData.ImpExtPrebid.StoredAuctionResponse.ID)

impId, err := jsonparser.GetString(impData.Imp, "id")
if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add test coverage for this error case, perhaps to TestParseRequestStoredResponses?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added TestProcessStoredAuctionResponsesErrors


testCases := []struct {
description string
givenIsAmp bool
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can simplify this a bit by removing givenIsAmp and hasStoredResponses and just hardcoding them in the function call since the values are the same for all test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, modified.

@@ -3667,6 +3667,190 @@ func TestFPD(t *testing.T) {

}

func TestStoredAuctionResponses(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm it's kind of a matter of personal preference. The coverage is there. I agree private functions generally don't need their own dedicated unit test if the function is simplistic.
With that said, I would lean towards writing a unit test for this function given that there's a bit going on there and simplifying this test so it just focuses on exercising the conditional if len(r.StoredAuctionResponses) > 0 { in exchange.go but I understand if we don't want to do that at this point :)

@VeronikaSolovei9
Copy link
Contributor Author

VeronikaSolovei9 commented Feb 17, 2022

@guscarreon @bsardo guys, test for func buildStoredAuctionResponse turns into copy of TestStoredAuctionResponses, where instead of HoldAuction we call buildStoredAuctionResponse. I can push the duplicate, but idea is the same. Or should I modify it to have just one test for buildStoredAuctionResponse?

@bsardo
Copy link
Collaborator

bsardo commented Feb 22, 2022

@guscarreon @bsardo guys, test for func buildStoredAuctionResponse turns into copy of TestStoredAuctionResponses, where instead of HoldAuction we call buildStoredAuctionResponse. I can push the duplicate, but idea is the same. Or should I modify it to have just one test for buildStoredAuctionResponse?

@VeronikaSolovei9 I don't think you need to duplicate much. What you have is good. My suggestion though we be to do something like this:

  1. Take your existing TestStoredAuctionResponses test and make it a unit test for buildStoredAuctionResponse (call it TestBuildStoredAuctionResponse) by adding two more expected output parameters (e.g. seatBids and bidderNames). Then make the body of your test as simple as this: for _, test := range testCases { seatBids, bidderNames, err := buildStoredAuctionResponse(test.in.StoredAuctionResponses) assert.Equal(t, test.expected.seatBids, seatBids, test.desc) assert.Equal(t, test.expected.bidderNames, bidderNames, test.desc) assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err) }
  2. Create a second test that just has a single test case with a single stored auction response so it exercises the line if len(r.StoredAuctionResponses) > 0 { in exchange.go. You can take most of what you remove from TestStoredAuctionResponses to setup the mock bid request and the exchange here.

I'm not adamant about making this change, however, I think it would help us better understand buildStoredAuctionResponse.

bsardo
bsardo previously approved these changes Feb 23, 2022
Copy link
Collaborator

@bsardo bsardo left a comment

Choose a reason for hiding this comment

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

LGTM

bsardo
bsardo previously approved these changes Feb 25, 2022
Copy link
Contributor

@guscarreon guscarreon left a comment

Choose a reason for hiding this comment

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

LGTM

@VeronikaSolovei9 VeronikaSolovei9 merged commit 8966116 into master Mar 1, 2022
pm-nilesh-chate added a commit to PubMatic-OpenWrap/prebid-server that referenced this pull request Mar 30, 2022
jorgeluisrocha pushed a commit to jwplayer/prebid-server that referenced this pull request Sep 28, 2022
shunj-nb pushed a commit to ParticleMedia/prebid-server that referenced this pull request Nov 8, 2022
@SyntaxNode SyntaxNode deleted the single_stored_response branch March 13, 2023 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants