-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
BaseAdapter for the Prebid 0.x -> 1.x transition #1494
Conversation
…exusAst adapter onto it
…e multi-request architecture adapters.
…gle-request-bidder
I like this. I'll set up some time to review it more in-depth, but it might not be until next week. |
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.
Overall looking pretty good, but I left some feedback.
src/ajax.js
Outdated
@@ -5,6 +5,9 @@ var utils = require('./utils'); | |||
const XHR_DONE = 4; | |||
let _timeout = 3000; | |||
|
|||
export const GET = 'GET'; | |||
export const POST = 'POST'; |
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.
I don't think we need these. I think constants are useful when you're dealing with magic numbers or non-obvious strings, but the strings 'POST' and 'GET' are sufficiently descriptive and this makes an extra dependency for all the adapters.
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.
Ironically... I actually added these because I couldn't remember where this ajax function was expecting GET
or get
. Figured this would save me from having to look it up the next time.
If "10" is "I refuse to use strings", and "1" is "I refuse to use constants", I'm like... a 6. Enough to challenge you here, but not very hard. How strong is your preference?
Also open to other peoples' feedback, if people want to vote.
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.
If this were our API, then I'd probably be more open to the idea of adding these constants. But this is just a layer on top of an existing API that's already pretty standardized around using 'GET', 'POST', etc strings: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/open
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.
aha... ok, that makes plenty of sense then. My javascript inexperience is showing itself... didn't know those were so well established.
I'll take the constants out. I should probably rename type
to method
, too... will take a pass for consistency, now that I know where the conventions are coming from.
modules/appnexusAstBidAdapter.js
Outdated
bidmanager.addBidResponse(placementCode, createBid(STATUS.NO_BID)); | ||
}); | ||
return; | ||
// TODO: Patch UserSync into the spec, once the UserSync PR has been merged. |
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.
Do you have any ideas how you would like UserSync to be implemented? Would it be another interface function of the bidderFactory or something that you would tag along into the interpretResponse function?
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.
that's a good question... and I don't know. I actually chatted with our team about it, but didn't walk away with any super clear answers.
I'm not familiar enough with AdTech to know how it's done... but I could imagine three places where user syncs might come from.
- Constant values. "Always ping this usersync endpoint on every auction"
- Server-side logic: "Always ping this endpoint to get the usersync info. It will return one or more syncs"
- Winning bids. "Because this bid made it into the auction, drop this usersync pixel"
Depending on which cases are worth supporting, I'd choose different APIs. It'll require adding one or more optional properties to the spec:
For (1), it'd be getSyncs()
For (2), it'd be getSyncs(done)
For (3), it'd be getSyncs(serverResponse)
In all cases, the return value (or callback argument) would just be an object with the data which we need to call registerSync
in #1549.
Do you have any better background or opinion on it? I'm interested to hear what other adapter maintainers think too, if they see this.
src/adapters/bidderFactory.js
Outdated
* @param {BidderSpec} spec An object containing the bare-bones functions we need to make a Bidder. | ||
*/ | ||
export default function newBidder(spec) { | ||
return Object.assign(new Adapter(spec.code), { |
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.
We need to figure out how aliasing is going to work with this new pattern. Currently I have it calling new
on the .constructor
link of an existing instance. In this case it would be an instance of just plain Adapter, which means it wouldn't work.
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.
Added support for these. I'm not sure this is the best way, though... so let me know if you have better ideas.
If we think this API is general enough to be the only base class, I might re-organize the code a bit so that "bidder creation" is in a separate file from "bidder registration".
I still haven't heard anyone weigh in on whether or not this handles all the 1.0 use-cases, though, or whether i'm overly-constraining adapters with it.
src/adapters/bidderFactory.js
Outdated
* Typical usage looks something like: | ||
* | ||
* const adapter = newBidder({ | ||
* code: 'myBidderCode', |
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.
It would be nice and easy for implementers if we allowed aliases to be defined here somehow
e.g.
const adapter = newBidder({
code: 'rubicon',
aliases: ['rubiconLite', 'roooobicon']
modules/appnexusAstBidAdapter.js
Outdated
} | ||
|
||
adaptermanager.registerBidAdapter(new AppnexusAstAdapter(), 'appnexusAst', { |
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.
It might be nice to get rid of the adaptermanager dependency and have the bidderFactory be responsible for registering the new adapter.
I think part of our goal for this new interface to bidder adapter creation should be getting rid of any many of the imported dependencies as possible, that way the interface to creating and registering an adapter can be a singular call into newBidder
rather than importing various modules and using them all separately. I think that's where adapter creators get confused, by our current disparate set of interfaces to different modules when creating an adapter.
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.
This never even occurred to me... it's a great idea though
src/adapters/bidderFactory.js
Outdated
} | ||
|
||
/** | ||
* @typedef {object} ServerRequest |
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.
I think these typedefs are awesome and will help with a lot of the confusion that currently exists around what type of objects Prebid.js expects to operate with. Do you you think it'd be worthwhile making constructor functions that are used to create these objects?
e.g.
var serverRequest = new ServerRequest('GET', ENDPOINT, data);
I think this would help with data validation. We probably want to validate that the adapters are giving us back the correct data types. But rather than do stuff like if(!serverRequest.type || !serverRequest.endpoint ...
we could just do if(!serverRequest instanceof ServerRequest)
and the constructor function would could check for required parameters.
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.
I've been thinking about this... but I'm not so convinced.
First: instanceof
is a pretty flimsy check in javascript. For example:
var serverRequest = new ServerRequest(...);
delete serverRequest.type
serverRequest.type // Is undefined
serverRequest instanceof Request // Is true
Second: it sounds like all the complex validation code has just moved into the constructor. Rather than if (!serverRequest.type)
, the constructor has to do if (!type)
. I can see some advantage in having the validation code centralized... but if that's the goal, I'd rather define it an isRequest(obj)
function somewhere.
Third: constructors are a bad pattern. To be honest, I don't expect the drawbacks to affect this area much... but I do want to discourage them from spreading around the codebase. There's some good discussion on them in (2) and (3) here
Data validation is an open question as far as I'm concerned, though.
I left it out here because I was thinking of how React totally disabled all runtime property validation in the prod
version of their library because it was found to be a notable performance drain. Granted, React apps have hundreds (or even thousands) of nested components... so it may not be a fair role model. Since everyone in adtech seems to be super concerned about performance, though, I figured I'd leave it out.
Worth mentioning: Assuming management schedules time for me to work on it, I have a followup PR I hope to make for "simple, unified adapter tests." Basically I want to make it so that adapters can define some JSON files for valid bid params and expected server responses, and we can centralize all the actual adapter test code. One of those tests would be making sure that the spec
only returns request objects with valid params. If we do it at test time, then it doesn't need to be done at runtime.
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.
It's true, there are ways to subvert instanceof
checks. The reason I like them is because, as you said, type validation is expensive and instanceof
checks are not. Using a constructor function to define a type allows you to incur a one-time hit for the validation of an object (and then use instanceof
thereafter), as opposed to isRequest(obj)
which is a many-time hit for each time that object is used. That's the problem React encountered as they had to validate propTypes each type a component instance was used.
Either way, this isn't a make or break issue for me, it's just something I think of every time I see if (obj.property ...
. Also, if we did do this we'd have to expose the constructor functions on the public API as well so external adapters could pass the instanceof
checks, which isn't terrible but it's more of a hassle than just using typedef.
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.
yeah... I do understand where you're coming from, but I think I'd prefer to hold off on this for now.
I'd feel better talking about how to optimize runtime validation after people see the tests I have in mind, if they still think they have value.
src/adapters/bidderFactory.js
Outdated
export default function newBidder(spec) { | ||
return Object.assign(new Adapter(spec.code), { | ||
callBids: function(bidsRequest) { | ||
if (!bidsRequest.bids || !bidsRequest.bids.filter) { |
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.
This looks like an array check. Would be more clear as if (!Array.isArray(bidsRequest.bids) ) {
src/adapters/bidderFactory.js
Outdated
}); | ||
} | ||
|
||
const bidRequests = bidsRequest.bids.filter(filterAndWarn); |
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.
These names are confusing, especially here. I think bidRequests
is fine, but bidsRequest
I think is called bidderRequest
in other parts of Prebid.
bids = spec.interpretResponse(response); | ||
} catch (err) { | ||
logError(`Bidder ${spec.code} failed to interpret the server's response. Continuing without bids`, null, err); | ||
onResponse(); |
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.
I think the calling of onResponse
here is a good use case for a finally
clause so you don't have to scatter as many calls around.
I'd wrap all this code in a try block and put the onResponse
in a finally
so it's obvious that it will always be called.
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.
I like the goal here... but I'm really trying not to catch unexpected errors either.
For example, while writing this I originally had a bug in my addBidUsingRequestMap
function where something was unexpectedly undefined.
I easily may never have caught that if it'd been inside the try
block.
src/adapters/bidderFactory.js
Outdated
const bidRequestMap = {}; | ||
bidRequests.forEach(bid => { | ||
bidRequestMap[bid.bidId] = bid; | ||
}); |
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.
Why is this bidRequestMap
recreated for each successful request? Couldn't it just be made once in the same context as the bidRequests
?
src/adapters/bidderFactory.js
Outdated
/** | ||
* @typedef {object} BidRequest | ||
* | ||
* @param {string} bidId A string which uniquely identifies this BidRequest in the current Auction. |
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.
These should be @property
instead of @param
for typedefs.
src/mediaTypes.js
Outdated
* @typedef {('native'|'video'|'banner')} MediaType | ||
*/ | ||
|
||
/** @ype MediaType */ |
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.
I think you have a typo here
…quest-related properties for consistency with the native object.
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.
Overall I like the strategy. What scares me is the amount of code change in the adapters. Overall LGTM though.
src/adapters/bidderFactory.js
Outdated
*/ | ||
|
||
/** | ||
* @typedef {object} BidderSpec An object containing the adapter-specific functions needed to |
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.
maybe just me but I like to see these typedef
s inline with the code.
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.
I moved these, but I'm uh.... not sure I did what you had in mind.
The first mention of BidderSpec
is inside an @param {BidderSpec} spec
. That block documents the function... so the closest places for the typedef
are either before or after the function implementation.
I moved it before the function implementation... which might be better? I dunno though.
Let me know if you have something better in mind. The BidderSpec
is used at the top-level functions... and all the other typedef
blocks in here are used inside the BidderSpec
.
modules/appnexusAstBidAdapter.js
Outdated
export const spec = { | ||
code: BIDDER_CODE, | ||
supportedMediaTypes: [VIDEO, NATIVE], | ||
aliases: ['dummy'], |
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.
nice :)
modules/appnexusAstBidAdapter.js
Outdated
* @return {Bid[]} An array of bids which were nested inside the server. | ||
*/ | ||
interpretResponse: function(serverResponse) { | ||
const parsed = JSON.parse(serverResponse); |
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.
suppose the base adapter could do the parse, unless we need custom error handling in the adapter (I don't see any here).
src/adapters/bidderFactory.js
Outdated
// | ||
// In Prebid 1.0, this will be simplified to use the `addBidResponse` and `done` callbacks. | ||
const placementCodesHandled = {}; | ||
function addBidWithCode(placementCode, bid) { |
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.
We are using two names for same thing in our codebase i.e placementCode and adUnitCode. From 1.0 we should just use code
or adUnitCode
Gist for adUnit changes https://gist.github.com/mkendall07/51ee5f6b9f2df01a89162cf6de7fe5b6#adunit-obj
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.
Did not know that... I removed placementCode
from all the comments and local variable names.
I think I still have to use bidRequest.placementCode
in Prebid 0.x, though, right? Or is there somewhere else I should be getting it from?
Type of change
Refactoring (no functional changes, no api changes)
Description of change
#1421 changes the APIs for both Adapters and Publishers. We're planning on cutting a 1.0 branch soon... and this PR aims to reduce the maintenance cost of that branch during the transition period.
In a nutshell: the
singleRequestBidder
is a new "base" bidder intended to support any adapters which make only one request to the server. Its Adapter-facing API is compatible with both Prebid 0.x and Prebid 1.x.Any compatible Adapters can code against this Base in
master
. If they do, they won't have to duplicate changes across branches whenever they do Adapter code maintenance.Still TODO:
At this point, any API feedback on the
singleRequestBidder
would be super useful. Everything else is just details.