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

BaseAdapter for the Prebid 0.x -> 1.x transition #1494

Merged
merged 22 commits into from
Sep 13, 2017

Conversation

dbemiller
Copy link
Contributor

@dbemiller dbemiller commented Aug 17, 2017

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.

@dbemiller dbemiller changed the title Added a base adapter for single-request adapters, and ported the appn… Adding a BaseAdapter for the 0.x -> 1.x transition Aug 18, 2017
@dbemiller dbemiller changed the title Adding a BaseAdapter for the 0.x -> 1.x transition Adding a BaseAdapter for the Prebid 0.x -> 1.x transition Aug 18, 2017
@dbemiller dbemiller changed the title Adding a BaseAdapter for the Prebid 0.x -> 1.x transition BaseAdapter for the Prebid 0.x -> 1.x transition Aug 18, 2017
@mkendall07 mkendall07 self-assigned this Aug 24, 2017
@snapwich
Copy link
Collaborator

I like this. I'll set up some time to review it more in-depth, but it might not be until next week.

Copy link
Collaborator

@snapwich snapwich left a 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';
Copy link
Collaborator

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.

Copy link
Contributor Author

@dbemiller dbemiller Aug 31, 2017

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.

Copy link
Collaborator

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

Copy link
Contributor Author

@dbemiller dbemiller Sep 1, 2017

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.

bidmanager.addBidResponse(placementCode, createBid(STATUS.NO_BID));
});
return;
// TODO: Patch UserSync into the spec, once the UserSync PR has been merged.
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

  1. Constant values. "Always ping this usersync endpoint on every auction"
  2. Server-side logic: "Always ping this endpoint to get the usersync info. It will return one or more syncs"
  3. 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.

* @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), {
Copy link
Collaborator

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.

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

* Typical usage looks something like:
*
* const adapter = newBidder({
* code: 'myBidderCode',
Copy link
Collaborator

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']

}

adaptermanager.registerBidAdapter(new AppnexusAstAdapter(), 'appnexusAst', {
Copy link
Collaborator

@snapwich snapwich Aug 31, 2017

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.

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 never even occurred to me... it's a great idea though

}

/**
* @typedef {object} ServerRequest
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 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.

Copy link
Contributor Author

@dbemiller dbemiller Sep 1, 2017

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.

Copy link
Collaborator

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.

Copy link
Contributor Author

@dbemiller dbemiller Sep 5, 2017

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.

export default function newBidder(spec) {
return Object.assign(new Adapter(spec.code), {
callBids: function(bidsRequest) {
if (!bidsRequest.bids || !bidsRequest.bids.filter) {
Copy link
Collaborator

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) ) {

});
}

const bidRequests = bidsRequest.bids.filter(filterAndWarn);
Copy link
Collaborator

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();
Copy link
Collaborator

@snapwich snapwich Aug 31, 2017

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.

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

const bidRequestMap = {};
bidRequests.forEach(bid => {
bidRequestMap[bid.bidId] = bid;
});
Copy link
Collaborator

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?

/**
* @typedef {object} BidRequest
*
* @param {string} bidId A string which uniquely identifies this BidRequest in the current Auction.
Copy link
Collaborator

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.

* @typedef {('native'|'video'|'banner')} MediaType
*/

/** @ype MediaType */
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 have a typo here

…quest-related properties for consistency with the native object.
Copy link
Member

@mkendall07 mkendall07 left a 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.

*/

/**
* @typedef {object} BidderSpec An object containing the adapter-specific functions needed to
Copy link
Member

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 typedefs inline with the code.

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

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [VIDEO, NATIVE],
aliases: ['dummy'],
Copy link
Member

Choose a reason for hiding this comment

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

nice :)

* @return {Bid[]} An array of bids which were nested inside the server.
*/
interpretResponse: function(serverResponse) {
const parsed = JSON.parse(serverResponse);
Copy link
Member

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).

//
// In Prebid 1.0, this will be simplified to use the `addBidResponse` and `done` callbacks.
const placementCodesHandled = {};
function addBidWithCode(placementCode, bid) {
Copy link
Collaborator

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

Copy link
Contributor Author

@dbemiller dbemiller Sep 7, 2017

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?

@mkendall07 mkendall07 merged commit a4d3ce7 into master Sep 13, 2017
outoftime pushed a commit to Genius/Prebid.js that referenced this pull request Sep 18, 2017
…built

* 'master' of https://github.com/prebid/Prebid.js: (46 commits)
  Serverbid alias (prebid#1560)
  Add user sync to process for approving adapter PRs (prebid#1457)
  fix travis build (prebid#1595)
  Rubicon project improvement/user sync (prebid#1549)
  Adding Orbitsoft adapter (prebid#1378)
  Fix renderer test for new validation rule (prebid#1592)
  Allow SET_TARGETING to be used in AnalyticsAdapter (prebid#1577)
  Add support for video stream context (prebid#1483)
  Invalidate bid if matching bid request not found (prebid#1575)
  allow adapters to set default adserverTargeting for specific bid (prebid#1568)
  Custom granularity precision should honor 0 if it is passed in closes prebid#1479 (prebid#1591)
  BaseAdapter for the Prebid 0.x -> 1.x transition  (prebid#1494)
  Add a version to the Criteo adapter (prebid#1573)
  Allow bundling from node.js or with new gulp task bundle-to-stdout  (prebid#1570)
  Add url.parse option to not decode the whole URL (prebid#1480)
  Tremor Video Bid Adapter (prebid#1552)
  Yieldmo bid adapter (prebid#1415)
  Switch `gulp docs` to build its output using documentation.js (prebid#1545)
  Increment pre version
  Prebid 0.28.0 Release
  ...
jbAdyoulike pushed a commit to jbAdyoulike/Prebid.js that referenced this pull request Sep 21, 2017
* Added a base adapter for single-request adapters, and ported the appnexusAst adapter onto it

* Renamed the SingleRequestBidder to BidderFactory. Updated it to handle multi-request architecture adapters.

* Added a unit test for the delayExecution function.

* Made newBidder a default import. Added some unit tests.

* Added more tests.

* Added more tests, and fixed a few bugs.

* Changed an error to a log message. Fixed a small bug.

* Did the no-brainer improvements from PR comments.

* Added spec-level support for aliases and mediaTypes. Aliases may still be broken.

* Added support for aliases. Added more tests

* Cleaned up some unnecessary code.

* Removed the GET/POST constants. Fixed some typos, and renamed some Request-related properties for consistency with the native object.

* Re-added some code for outstream rendering, which was apparently lost in the merges somewhere.

* Removed confusing use of this

* Fixed lint error

* Moved JSON parsing into the bidderFactory, and moved the JSDocs to the top.

* Removed placementCode from everywhere I could.
@matthewlane matthewlane deleted the single-request-bidder branch October 6, 2017 23:54
dluxemburg pushed a commit to Genius/Prebid.js that referenced this pull request Jul 17, 2018
* Added a base adapter for single-request adapters, and ported the appnexusAst adapter onto it

* Renamed the SingleRequestBidder to BidderFactory. Updated it to handle multi-request architecture adapters.

* Added a unit test for the delayExecution function.

* Made newBidder a default import. Added some unit tests.

* Added more tests.

* Added more tests, and fixed a few bugs.

* Changed an error to a log message. Fixed a small bug.

* Did the no-brainer improvements from PR comments.

* Added spec-level support for aliases and mediaTypes. Aliases may still be broken.

* Added support for aliases. Added more tests

* Cleaned up some unnecessary code.

* Removed the GET/POST constants. Fixed some typos, and renamed some Request-related properties for consistency with the native object.

* Re-added some code for outstream rendering, which was apparently lost in the merges somewhere.

* Removed confusing use of this

* Fixed lint error

* Moved JSON parsing into the bidderFactory, and moved the JSDocs to the top.

* Removed placementCode from everywhere I could.
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.

6 participants