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

Adserver targeting #2593

Merged
merged 19 commits into from
Apr 3, 2023
Merged

Adserver targeting #2593

merged 19 commits into from
Apr 3, 2023

Conversation

VeronikaSolovei9
Copy link
Contributor

Implementation of #1167

@VeronikaSolovei9 VeronikaSolovei9 marked this pull request as ready for review March 2, 2023 21:21
@VeronikaSolovei9 VeronikaSolovei9 changed the title [DRAFT] Adserver targeting Adserver targeting Mar 2, 2023
// path points to value in response, not seat or ext
// bidder response has limited number of properties
// instead of unmarshal the whole response with seats and bids
// try to find data by field name
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move comments like this up a level on getValueFromResp directly.

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Not really what I was thinking, but it could work too. I meant to suggest something like this:

// getValueFromResp optimizes retrieval of paths for a bid response (not a seat or ext)
func getValueFromResp(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh ok, modified

func (brh *respCache) GetRespData(bidderResp *openrtb2.BidResponse, field string) (string, error) {
if len(brh.bidRespData) == 0 {
// this code should be modified if there are changes in Op[enRtb format
// it's up to date with OpenRTB 2.5 and 2.6
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment will not be adhered to. You should add a test to fail when the assumptions made by this function change. I can reach out separately to help construct a test like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left a comment for now. Please reach me out to discuss how to create this test, it would be good to have it.

Copy link
Contributor

@SyntaxNode SyntaxNode left a comment

Choose a reason for hiding this comment

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

Excellent update! This is a lot easier to follow and appears to match the spec. I would like to see end to end tests to ensure this is hooked into the main auction flow correctly and to provide examples on the feature's usage.

The only feature from the spec I didn't see represented is:

If the value points to a JSON path that is an object or array, there should be a warning in debug mode

Is that already accounted for? .. and if so, where?

@@ -425,6 +427,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *

// Build the response
bidResponse, err := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs)

bidResponse = adservertargeting.ProcessAdServerTargeting(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute)
Copy link
Contributor

Choose a reason for hiding this comment

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

The "ad server targeting" information from the package name is repeated in the function call. To avoid, consider renaming ProcessAdServerTargeting to Apply, which then produces the simplified code:

bidResponse = adservertargeting.Appy(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute)

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!

@@ -1711,8 +1711,6 @@ func TestBidResponseCurrency(t *testing.T) {
ID: "some-request-id",
SeatBid: sampleSeatBid,
Cur: "USD",
Ext: json.RawMessage(`{"responsetimemillis":{"appnexus":5},"tmaxrequest":500}
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 you moved the call to encodeBidResponseExt. I assume these tests are only in scope of the old location. Is there test coverage of encodeBidResponseExt in its new location?

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 were no tests for encodeBidResponseExt. It was tested together with buildBidResponse function.
Func encodeBidResponseExt is now 100% covered by many other exchange tests including json tests. Do you want me to add specific test for encodeBidResponseExt?

@@ -71,6 +71,15 @@ type ExtRequestPrebid struct {
// - basic: excludes debugmessages and analytic_tags from output
// any other value or an empty string disables trace output at all.
Trace string `json:"trace,omitempty"`

//
AdServerTargeting []AdServerTargeting `json:"adservertargeting,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

No comment is necessary here IMHO. Feel free to remove the stub.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would suggest a slight renaming, to avoid thing = list of things. Either AdServerTargeting []AdServerTarget, AdServerTargeting []AdServerTargetRule, or AdserverTargets []AdServerTargeting

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed to AdServerTargeting []AdServerTarget

truncateTargetAttribute *int) *openrtb2.BidResponse {

adServerTargeting, warnings := CollectAdServerTargeting(reqWrapper, resolvedRequest, queryParams)
response, warnings = ResolveAdServerTargeting(adServerTargeting, response, warnings, truncateTargetAttribute)
Copy link
Contributor

Choose a reason for hiding this comment

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

The functions CollectAdServerTargeting and ResolveAdServerTargeting are no longer used outside of this package. You're welcomed to simplify their names and and make them package scoped. Perhaps collect and resolve?

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!

return response
}

func CollectAdServerTargeting(
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment to help the reader understand what this function is doing. Even if you change it to package scope.

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

@@ -0,0 +1,104 @@
package adservertargeting
Copy link
Contributor

Choose a reason for hiding this comment

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

The term "processor" is not descriptive of the functionality IMHO. How about something like "requestlookup"? .. or just a description which better translates to "looks up and returns a value".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed

return nil, err
}
return reqExtPrebid.AdServerTargeting, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding line breaks between the error checking:

	reqExt, err := reqWrapper.GetRequestExt()
	if err != nil {
		return nil, err
	}

	reqExtPrebid := reqExt.GetPrebid()
	if reqExtPrebid == nil {
		return nil, err
	}

	return reqExtPrebid.AdServerTargeting, nil

const MaxKeyLength = 20

func processRequestTargetingData(adServerTargetingData *adServerTargetingData, targetingData map[string]string, bidImpId string) {
if len(adServerTargetingData.RequestTargetingData) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

The length check isn't necessary. Keep in mind that Go's range loop gracefully handles nil or empty lists such that no optimization is necessary.

warnings []openrtb_ext.ExtBidderMessage) {
if len(adServerTargetingData.ResponseTargetingData) == 0 {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same deal here. No need for the length check.

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, removed.


}
targetingData[key] = value
}
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO it might be easier to read the method by moving the parameters to a single line. I've also noticed there is no return values which increases the number of arguments. How do you feel about:

func getValueFromSeatBidBid(path string, targetingData map[string]string, bidsHolder bidsCache, bidderName string, bid openrtb2.Bid) error {
	bidBytes, err := bidsHolder.GetBid(bidderName, bid.ID, bid)
	if err != nil {
		return createWarning(err.Error())

	}

	path = strings.TrimPrefix(path, "seatbid.bid.")
	value, err := splitAndGet(path, bidBytes, pathDelimiter)
	if err != nil {
		return createWarning(err.Error())

	}

	return value
}

where the call site now looks like:

if v, err := getValueFromSeatBidBid(path, targetingData, bidsHolder, bidderName, bid); err == nil {
	targetingData[key] = v
} else {
	warnings = append(warnings, err)
}

Such that the responsibility of setting the response and collecting errors is moved up a level. Same comment applies to the other getValue methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a great point! How I didn't see it while coding (I wanted to hide all those appends and set to map statements)... It's even better, error check and value set is only required once for all "getValue" functions at the end of processResponseTargetingData

@SyntaxNode SyntaxNode self-assigned this Mar 13, 2023
vsolovei added 3 commits March 13, 2023 19:41
# Conflicts:
#	errortypes/code.go
#	openrtb_ext/bid.go
#	openrtb_ext/request.go
@VeronikaSolovei9
Copy link
Contributor Author

Need to add end to end test.

warnings []openrtb_ext.ExtBidderMessage,
truncateTargetAttribute *int) (*openrtb2.BidResponse, []openrtb_ext.ExtBidderMessage) {
if adServerTargetingData == nil {
return response, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to cover this line with a test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I realized this will never be the case and removed the check.

}

// ad server targeting data will go to seatbid[].bid[].ext.prebid.targeting

Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: can this blank line be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed the entire comment. I don't think it's needed.

impsData []json.RawMessage
}

func (dh *reqImpCache) GetReqJson() []byte {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could the variable dh get a more descriptive name? Is it supposed to represent dataHolder?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed

impToBid := make(map[string][]byte, 0)
bdh.bids[bidderName] = impToBid
}
_, biddExists := bdh.bids[bidderName][bidId]
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: the variable biddExists is mispelled

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

return res, err
}

return res, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this line be covered by tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried and it looks like no. This case is logically unreachable, this line exists to satisfy the compiler requirement to have a return statement.
All "suspicious" cases will be handled and returned with a proper error message in the code statement above: getDataFromRequest

switch pathSplit[0] {

case "seatbid":
switch pathSplit[1] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any check needed here to ensure pathSplit[1] exists? Does a split call above always give us at least 2 values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! Path split may have just 1 element, for instance "cur". It means the value should be taken from bid response itself, like "resp.cur". However I added targeting keys path validation to "collect" function to make sure it's not empty.

if truncateTargetAttribute != nil {
maxLength = *truncateTargetAttribute
if maxLength < 0 {
maxLength = MaxKeyLength
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we cover this line with a test?

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

}

func getRespData(bidderResp *openrtb2.BidResponse, field string) (string, error) {
// this code should be modified if there are changes in Op[enRtb format
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: Mispelling Op[enRtb --> OpenRtb

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +156 to +168
case "id":
return bidderResp.ID, nil
case "bidid":
return bidderResp.BidID, nil
case "cur":
return bidderResp.Cur, nil
case "customdata":
return bidderResp.CustomData, nil
case "nbr":
return fmt.Sprint(bidderResp.NBR.Val()), nil

default:
return "", errors.Errorf("key not found for field in bid response: %s", field)
Copy link
Contributor

Choose a reason for hiding this comment

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

This block needs test coverage

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. And found and fixed related bug.

@VeronikaSolovei9
Copy link
Contributor Author

Good comments, Alex! Please re-review

@bsardo bsardo assigned hhhjort and unassigned SyntaxNode Mar 20, 2023

reqExtPrebid := reqExt.GetPrebid()
if reqExtPrebid == nil {
return nil, err
Copy link
Collaborator

Choose a reason for hiding this comment

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

What error are you returning here? Is it possible to get to this line when err != nil?

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, no error, refactored

warnings []openrtb_ext.ExtBidderMessage,
truncateTargetAttribute *int) json.RawMessage {

targetingDataTruncated := truncateTargetingKeys(targetingData, truncateTargetAttribute)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't it be better to truncate the keys back when we create them, rather than doing this loop through all the keys again?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My idea here was to separate "get values" logic from everything else, because "get logic" became complicated.
Func truncateTargetingKeys will be needed to execute for "request" keys and for "response" keys.
Also I'll need to pass truncateTargetAttribute parameter downstream to where we set keys and values to final map, I think it will overload logic and number of parameters.

I think having this logic here makes the code easier to understand.
I'm open to discuss moving it to another place, if you feel otherwise.

@@ -51,8 +51,7 @@
{
"id": "applogy-bid",
"impid": "some-impression-id",
"price": 0,
"ext": {"prebid":{"type":"banner"}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why did this ext need to be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It appears bid.ext was not validated in unit tests before.
Actual bid.ext turns out to be

{"origbidcpm":0,"origbidcur":"USD","prebid":{"meta":{"adaptercode":"applogy"},"type":"banner"}}

not

"ext": {"prebid":{"type":"banner"}}

After I added bid.ext comparison logic it stoped working.
File auction_test.go -> func assertBidResponseEqual:

if len(expectedBid.Ext) > 0 {
	assert.JSONEq(t, string(expectedBid.Ext), string(actualBidMap[bidID].Ext), "Incorrect bid extension")
}

I removed bid.ext from expected result, because it was not validated before and it was not valid anyways.
I can add expected bid.ext back and modify them accordingly. So far it's filled with default values only.

vsolovei added 2 commits March 20, 2023 18:49
Copy link
Contributor

@AlexBVolcy AlexBVolcy left a comment

Choose a reason for hiding this comment

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

Looks good! Left one small comment.

}

// get data by key from request
requestValue, err := getDataFromRequest(path, dataHolder)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: the function names getValueFromBidRequest vs. getDataFromRequest are pretty similar and I'm wondering if there's a better name for one of them that'll help distinguish the two functions? If you feel fine about them then I'm happy to keep them as they are, but thought it was worth a mention.

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, understand. in this function we try to get a key that was not found in query param or in imps. this is a default case. Renamed to getDataFromRequestJson

if test.expectedError {
assert.Error(t, err, "unexpected error returned")
} else {
assert.NoError(t, err, "expected error not returned")
Copy link
Contributor

Choose a reason for hiding this comment

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

Since what you're looking for is for no error to be returned, should the message be something like "unexpected error returned" I see you use that message for other NoError assertions in other parts of testing.

Copy link
Contributor Author

@VeronikaSolovei9 VeronikaSolovei9 Mar 29, 2023

Choose a reason for hiding this comment

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

Yes, these should be swapped.

Comment on lines 111 to 141
const (
reqValid = `{
"id": "req_id",
"imp": [
{
"id": "test_imp1",
"ext": {"appnexus": {"placementId": 250419771}},
"banner": {"format": [{"h": 250, "w": 300}]}
},
{
"id": "test_imp2",
"ext": {"appnexus": {"placementId": 250419771}},
"banner": {"format": [{"h": 250, "w": 300}]}
}
],
"site": {"page": "test.com"}
}`

reqInvalid = `{
"id": "req_id",
"imp": {
"incorrect":true
},
"site": {"page": "test.com"}
}`

reqNoImps = `{
"id": "req_id",
"site": {"page": "test.com"}
}`
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be too tedious to extract this consts to their own json files? I'm happy with whatever you think, but it might make tests a little cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like this idea!

expectedError: false,
},
{
description: "valid request with correct ext.prebid, no ad server targeting",
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this description needs to be updated? I think I see ad server targeting present in request ext.

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, I modified the description.

@AlexBVolcy
Copy link
Contributor

This looks good! I'm interested to see what @hhhjort comments before I approve.

Copy link
Collaborator

@hhhjort hhhjort 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 merged commit a81f4bd into master Apr 3, 2023
@VeronikaSolovei9 VeronikaSolovei9 deleted the adserver_targeting branch May 24, 2023 22:17
blueseasx pushed a commit to blueseasx/prebid-server that referenced this pull request Jun 2, 2023
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.

5 participants