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

DSA: Inject default in bid requests if not present #3540

Merged
merged 21 commits into from
Apr 9, 2024

Conversation

bsardo
Copy link
Collaborator

@bsardo bsardo commented Feb 25, 2024

Implements #3424

@bsardo bsardo marked this pull request as ready for review February 27, 2024 22:27
dsa/writer.go Outdated
)

// DSAWriter is used to write the default DSA to the request (req.regs.ext.dsa)
type DSAWriter struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: This is the dsa package. It's fine to call this just Writer.

dsa/writer.go Outdated

// Write sets the default DSA object on the request at regs.ext.dsa if it is
// defined in the account config and it is not already present on the request
func (dw DSAWriter) Write(req *openrtb_ext.RequestWrapper) (err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: We don't often name the return argument. We typically prefer to return nil to indicate no error.

openrtb_ext/regs.go Show resolved Hide resolved
IPv6Config IPv6 `mapstructure:"ipv6" json:"ipv6"`
IPv4Config IPv4 `mapstructure:"ipv4" json:"ipv4"`
}

// AccountDSA represents DSA configuration
type AccountDSA struct {
Default *openrtb_ext.ExtRegsDSA `mapstructure:"default" json:"default"`
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an interesting approach. It enforces validation such that the object is always valid, but requires that it be configured not in json... but in whatever config format the host uses. Would it be weird to express this in yaml? Since there is no environment binding, that wouldn't be an option.

How do you feel about accepting a json.RawMessage such that it's always a json blob and parsing it during config loading? Easy for the default account, but perhaps less clean for the account specific configs.

Thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Discussed offline. I overlooked the fact that viper will make it difficult to map the transparency array of objects to the config and env vars will be a problem so we agreed that hosts and accounts will specify a default DSA object as a JSON string.

The approach I took was as follows:

Hosts specify a default DSA object as a JSON string
On startup, the JSON string is validated via unmarshaling to a DSA default struct
If it is not well-formed we fail hard

Account configs also specify DSA objects as a JSON string
The account fetching logic already merges account defaults with an account config. Given that DSA objects are expressed as a JSON string, either the account config or account default is chosen (no merging)
The selected DSA default is validated via unmarshaling to a DSA default struct
If it is not well-formed we throw a malformed JSON error (already visible via a metric)

The default structs are used when writing to a bid request or validating a bid response.

openrtb_ext/regs.go Show resolved Hide resolved
if err := dsaWriter.Write(req); err != nil {
errs = append(errs, err)
}
req.RebuildRequest()
Copy link
Contributor

Choose a reason for hiding this comment

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

This can produce an error.

@bsardo bsardo assigned AlexBVolcy and unassigned VeronikaSolovei9 Apr 2, 2024
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 refactor idea.


// Write sets the default DSA object on the request at regs.ext.dsa if it is
// defined in the account config and it is not already present on the request
func (dw Writer) Write(req *openrtb_ext.RequestWrapper) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know if this is a stronger version of the function, but here's a version where we combine all of the conditions that result in us returning nil, rather than breaking them all up:

func (dw Writer) Write(req *openrtb_ext.RequestWrapper) error {
	if req == nil || getReqDSA(req) != nil || dw.Config == nil || dw.Config.DefaultUnpacked == nil ||
		(dw.Config.GDPROnly && !dw.GDPRInScope) {
		return nil
	}

	regExt, err := req.GetRegExt()
	if err != nil {
		return err
	}

	regExt.SetDSA(dw.Config.DefaultUnpacked)
	return nil
}

What do you think?

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 think this is really a matter of preference. I see your perspective that there are multiple conditionals that result in the same return value, though my tendency is to break complex conditionals into logical groupings because I think it is easier to digest and instills confidence in the intended behavior. I also think it is easier to scan code vertically. In that case, at a minimum the first two conditionals could be grouped as such: if req == nil || getReqDSA(req) != nil {.
@SyntaxNode do you feel strongly either way here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I'm thinking your latest update looks great

AlexBVolcy
AlexBVolcy previously approved these changes Apr 3, 2024

// Write sets the default DSA object on the request at regs.ext.dsa if it is
// defined in the account config and it is not already present on the request
func (dw Writer) Write(req *openrtb_ext.RequestWrapper) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I'm thinking your latest update looks great

dsa/writer.go Outdated Show resolved Hide resolved
dsa/writer.go Outdated
if err != nil {
return err
}
regExt.SetDSA(dw.Config.DefaultUnpacked)
Copy link
Contributor

Choose a reason for hiding this comment

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

There is some danger for memory corruption in pointing the request to using the same DefaultUnpacked instance for all requests. However, we also have the convention of "clone + set" when making changes to the request which likely mitigates this issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please add a second integration test for where the dsa object is already provided and the default dsa object is ignored?

GDPRInScope: gdprEnforced,
}
if err := dsaWriter.Write(req); err != nil {
errs = append(errs, err)
Copy link
Contributor

@SyntaxNode SyntaxNode Apr 3, 2024

Choose a reason for hiding this comment

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

Should we return if there is an error? Same for getGDPR , RebuildRequest, and getAuctionBidderRequests.

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 think we should return an error for RebuildRequest but not for the others. getGDPR will only ever result in a wrapper unmarshal error and it should theoretically never happen here since any problem should be caught upstream. And also we plan on it not returning an error in the near future anyway. getAuctionBidderRequests returns some errors that should not result in an early return and others that are accompanied by a nil allBidderRequests which prevents the logic that follows from having any effect since it involves looping over allBidderRequests.

config/config.go Outdated Show resolved Hide resolved
@SyntaxNode SyntaxNode merged commit b42841e into prebid:master Apr 9, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants