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

New Adapter: Smile Wanted #1877

Merged
merged 4 commits into from
Jun 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions adapters/smilewanted/params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package smilewanted

import (
"encoding/json"
"testing"

"github.com/prebid/prebid-server/openrtb_ext"
)

// This file actually intends to test static/bidder-params/smilewanted.json
//
// These also validate the format of the external API: request.imp[i].ext.smilewanted

// TestValidParams makes sure that the smilewanted schema accepts all imp.ext fields which we intend to support.
func TestValidParams(t *testing.T) {
validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
if err != nil {
t.Fatalf("Failed to fetch the json-schemas. %v", err)
}

for _, validParam := range validParams {
if err := validator.Validate(openrtb_ext.BidderSmileWanted, json.RawMessage(validParam)); err != nil {
t.Errorf("Schema rejected SmileWanted params: %s", validParam)
}
}
}

// TestInvalidParams makes sure that the SmileWanted schema rejects all the imp.ext fields we don't support.
func TestInvalidParams(t *testing.T) {
validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
if err != nil {
t.Fatalf("Failed to fetch the json-schemas. %v", err)
}

for _, invalidParam := range invalidParams {
if err := validator.Validate(openrtb_ext.BidderSmileWanted, json.RawMessage(invalidParam)); err == nil {
t.Errorf("Schema allowed unexpected params: %s", invalidParam)
}
}
}

var validParams = []string{
`{"zoneId": "zone_code"}`,
}

var invalidParams = []string{
`{"zoneId": 100}`,
`{"zoneId": true}`,
`{"zoneId": 123}`,
`{"zoneID": "1"}`,
``,
`null`,
`true`,
`9`,
`1.2`,
`[]`,
`{}`,
}
106 changes: 106 additions & 0 deletions adapters/smilewanted/smilewanted.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package smilewanted

import (
"encoding/json"
"fmt"
"net/http"

"github.com/mxmCherry/openrtb/v15/openrtb2"
"github.com/prebid/prebid-server/adapters"
"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/errortypes"
"github.com/prebid/prebid-server/openrtb_ext"
)

type adapter struct {
URI string
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {

request.AT = 1 //Defaulting to first price auction for all prebid requests

reqJSON, err := json.Marshal(request)
if err != nil {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Json not encoded. err: %s", err),
}}
}
SyntaxNode marked this conversation as resolved.
Show resolved Hide resolved

headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")
headers.Add("x-openrtb-version", "2.5")
headers.Add("sw-integration-type", "prebid_server")

return []*adapters.RequestData{{
Method: "POST",
Uri: a.URI,
Body: reqJSON,
Headers: headers,
}}, []error{}
}

func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {

if response.StatusCode == http.StatusNoContent {
return nil, nil
}

if response.StatusCode == http.StatusBadRequest {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("Unexpected status code: %d.", response.StatusCode),
}}
}

if response.StatusCode != http.StatusOK {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", response.StatusCode),
}}
}

var bidResp openrtb2.BidResponse
if err := json.Unmarshal(response.Body, &bidResp); err != nil {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("Bad server response: %s.", err),
}}
}

var bidReq openrtb2.BidRequest
if err := json.Unmarshal(externalRequest.Body, &bidReq); err != nil {
return nil, []error{err}
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid))
sb := bidResp.SeatBid[0]
for i := 0; i < len(sb.Bid); i++ {
bid := sb.Bid[i]
bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &bid,
BidType: getMediaTypeForImp(bid.ImpID, internalRequest.Imp),
})
}
return bidResponse, nil
}

// getMediaTypeForImp figures out which media type this bid is for.
func getMediaTypeForImp(impId string, imps []openrtb2.Imp) openrtb_ext.BidType {
mediaType := openrtb_ext.BidTypeBanner //default type
for _, imp := range imps {
if imp.ID == impId {
if imp.Video != nil {
mediaType = openrtb_ext.BidTypeVideo
}
return mediaType
}
}
return mediaType
}
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 anti-pattern. Does your bidding server return the media type of the bid in a bid response extension?

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't see changes for this comment. The main problem is ensuring the bid type is correct. If your bidding server sees an impression with a banner and a video object, will it always just make a video bid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We always attach a Type on the Zone that we create on our UI and the publisher implement this zone in the ZoneId params, So when we receive a new ad request, we know that it is a request for Display or Video in the zone configuration on our end.

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. Would your server return an error if it receives an Imp with both banner and video for a zone that is configured for 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.

No but we will force this on publisher side to have only one zone per type of request.

We have already done that with PrebidJS and it's works fine ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok so i looked at when I make a config with two types (Banner and Video) and we answer on our side with one ad (the most expensive one between Video and Display on our side), so then PrebidJS displays it ;)

It will be an optimization that we can do later to take into consideration several requests of different types and make several responses, but it requires quite a few changes on our side ;)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since it is not a guaranteed correct identification, one possibility, until you have the infrastructure in place to properly in place to identify bid type, is to set your adapter disabled by default. That will give you a chance to open a dialogue with the PBS host and ensure that the client side has the ability to properly detect and handle the difference between banner and video bids.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to clarify : We identify on our end the type, we see in each Bid Request BUT we will answer with only one ad (corresponding to the winning ad on our internal competition between all types).

We support multi request but we do not support multi response, but it's only an optimization for us to have more opportunities to win the competition on publisher's side.

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 overall the PR LGTM now. Just want to warn you that the above approach of assigning the media type for the bid might lead to the final ad not being rendered properly if the media type wasn't correctly set for a multi-format request. This might happen if for an imp that has both banner and a video object, your server returns a banner bid but then here you assign the media type to be video.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We have had issues with other bidders in the past. If the final rendering client is not smart enough, it can get confused as to what it is trying to display. Just considering how you can get ahead of it rather than having your publisher clients have a poor experience that needs to be debugged after the fact.


// Builder builds a new instance of the SmileWanted adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
bidder := &adapter{
URI: config.Endpoint,
}
return bidder, nil
}
20 changes: 20 additions & 0 deletions adapters/smilewanted/smilewanted_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package smilewanted

import (
"testing"

"github.com/prebid/prebid-server/adapters/adapterstest"
"github.com/prebid/prebid-server/config"
"github.com/prebid/prebid-server/openrtb_ext"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(openrtb_ext.BidderSmileWanted, config.Adapter{
Endpoint: "http://example.com"})

if buildErr != nil {
t.Fatalf("Builder returned unexpected error %v", buildErr)
}

adapterstest.RunJSONBidderTest(t, "smilewantedtest", bidder)
}
94 changes: 94 additions & 0 deletions adapters/smilewanted/smilewantedtest/exemplary/simple-banner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"mockBidRequest": {
"id": "test-request-id",
"imp": [
{
"id": "test-imp-id",
"at" : 1,
"banner": {
"format": [
{
"w": 300,
"h": 250
}
]
},
"ext": {
"bidder": {
"zoneId": "zone_code_test_display"
}
}
}
]
},

"httpCalls": [
{
"expectedRequest": {
"uri": "http://example.com",
"body": {
"id": "test-request-id",
"at" : 1,
"imp": [
{
"id": "test-imp-id",
"banner": {
"format": [
{
"w": 300,
"h": 250
}
]
},
"ext": {
"bidder": {
"zoneId": "zone_code_test_display"
}
}
}
]
}
},
"mockResponse": {
"status": 200,
"body": {
"id": "test-request-id",
"seatbid": [
{
"seat": "smilewanted",
"bid": [{
"id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
"impid": "test-imp-id",
"price": 0.5,
"adm": "some-test-ad",
"crid": "crid_10",
"h": 250,
"w": 300
}]
}
],
"cur": "USD"
}
}
}
],

"expectedBidResponses": [
{
"bids": [
{
"bid": {
"id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
"impid": "test-imp-id",
"price": 0.5,
"adm": "some-test-ad",
"crid": "crid_10",
"h": 250,
"w": 300
},
"type": "banner"
}
]
}
]
}
87 changes: 87 additions & 0 deletions adapters/smilewanted/smilewantedtest/exemplary/simple-video.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"mockBidRequest": {
"id": "test-request-id",
"imp": [
{
"id": "test-imp-id",
"video": {
"mimes": ["video/mp4"],
"protocols": [2, 5],
"w": 1024,
"h": 576
},
"ext":{
"bidder":{
"zoneId": "zone_code_test_video"
}
}
}
]
},

"httpCalls": [
{
"expectedRequest": {
"uri": "http://example.com",
"body": {
"id": "test-request-id",
"at": 1,
"imp": [
{
"id": "test-imp-id",
"video": {
"mimes": ["video/mp4"],
"protocols": [2, 5],
"w": 1024,
"h": 576
},
"ext": {
"bidder": {
"zoneId": "zone_code_test_video"
}
}
}
]
}
},
"mockResponse": {
"status": 200,
"body": {
"id": "test-request-id",
"cur": "USD",
"seatbid": [
{
"seat": "smilewanted",
"bid": [{
"id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
"impid": "test-imp-id",
"price": 0.5,
"adm": "some-test-ad",
"crid": "crid_10",
"w": 1024,
"h": 576
}]
}
]
}
}
}
],

"expectedBidResponses": [
{
"bids": [{
"bid": {
"id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
"impid": "test-imp-id",
"price": 0.5,
"adm": "some-test-ad",
"crid": "crid_10",
"w": 1024,
"h": 576
},
"type": "video"
}]
}
]
}
3 changes: 3 additions & 0 deletions adapters/smilewanted/smilewantedtest/params/race/banner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"zoneId": "zone_code_test_display"
}
3 changes: 3 additions & 0 deletions adapters/smilewanted/smilewantedtest/params/race/video.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"zoneId": "zone_code_test_video"
}
Loading