-
Notifications
You must be signed in to change notification settings - Fork 758
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Prebid Server adapter for Telaria (#1231)
* TELARIA adapter. First Pass * Some refactoring * added the json files * fixed some tests and added the bidder info * fixed some tests and added the bidder info * added default user sync ur; * - Handling gzipped responses from our server * - more refactoring. * added the proper user sync default URL * changed the urls from dev to prod * changed up the required fields. Now AdCode in the Imp.Ext isn't required but Bid.SeatCode is required * change in the return type after decompressing * some refactoring * change in our config url * using pbs.yml to switch between our production and test URLs * setting default endpoint * - fixed the issue that was preventing telaria test cases to run. - added more test cases * - Modifications as per the changes requested by the maintainers. * Moved the seat code to imp.ext * Moved the seat code to imp.ext * Added 'Telaria: ' prefix for error messages * - Fixes for race conditions. Was modifying the original request object instead of a copy * cosmetic changes. * added params_test.go Co-authored-by: Vinay Prasad <[email protected]>
- Loading branch information
1 parent
d416035
commit cc7a247
Showing
26 changed files
with
1,388 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package telaria | ||
|
||
import ( | ||
"encoding/json" | ||
"github.com/prebid/prebid-server/openrtb_ext" | ||
"testing" | ||
) | ||
|
||
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.BidderTelaria, json.RawMessage(validParam)); err != nil { | ||
t.Errorf("Schema rejected Telaria params: %s", validParam) | ||
} | ||
} | ||
} | ||
|
||
// TestInvalidParams makes sure that the Telaria 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.BidderTelaria, json.RawMessage(invalidParam)); err == nil { | ||
t.Errorf("Schema allowed unexpected params: %s", invalidParam) | ||
} | ||
} | ||
} | ||
|
||
var validParams = []string{ | ||
`{"adCode": "string", "seatCode": "string", "originalPublisherid": "string"}`, | ||
} | ||
|
||
var invalidParams = []string{ | ||
``, | ||
`null`, | ||
`true`, | ||
`5`, | ||
`4.2`, | ||
`[]`, | ||
`{}`, | ||
`{"adCode": "string", "originalPublisherid": "string"}`, | ||
`{"adCode": "string", "seatCode": 5, "originalPublisherid": "string"}`, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,330 @@ | ||
package telaria | ||
|
||
import ( | ||
"bytes" | ||
"compress/gzip" | ||
"encoding/json" | ||
"fmt" | ||
"github.com/mxmCherry/openrtb" | ||
"github.com/prebid/prebid-server/adapters" | ||
"github.com/prebid/prebid-server/errortypes" | ||
"github.com/prebid/prebid-server/openrtb_ext" | ||
"net/http" | ||
"strconv" | ||
) | ||
|
||
const Endpoint = "https://ads.tremorhub.com/ad/rtb/prebid" | ||
|
||
type TelariaAdapter struct { | ||
URI string | ||
} | ||
|
||
// This will be part of Imp[i].Ext when this adapter calls out the Telaria Ad Server | ||
type ImpressionExtOut struct { | ||
OriginalTagID string `json:"originalTagid"` | ||
OriginalPublisherID string `json:"originalPublisherid"` | ||
} | ||
|
||
// used for cookies and such | ||
func (a *TelariaAdapter) Name() string { | ||
return "telaria" | ||
} | ||
|
||
func (a *TelariaAdapter) SkipNoCookies() bool { | ||
return false | ||
} | ||
|
||
// Endpoint for Telaria Ad server | ||
func (a *TelariaAdapter) FetchEndpoint() string { | ||
return a.URI | ||
} | ||
|
||
// Checker method to ensure len(request.Imp) > 0 | ||
func (a *TelariaAdapter) CheckHasImps(request *openrtb.BidRequest) error { | ||
if len(request.Imp) == 0 { | ||
err := &errortypes.BadInput{ | ||
Message: "Telaria: Missing Imp Object", | ||
} | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// Checking if Imp[i].Video exists and Imp[i].Banner doesn't exist | ||
func (a *TelariaAdapter) CheckHasVideoObject(request *openrtb.BidRequest) error { | ||
hasVideoObject := false | ||
|
||
for _, imp := range request.Imp { | ||
if imp.Banner != nil { | ||
return &errortypes.BadInput{ | ||
Message: "Telaria: Banner not supported", | ||
} | ||
} | ||
|
||
hasVideoObject = hasVideoObject || imp.Video != nil | ||
} | ||
|
||
if !hasVideoObject { | ||
return &errortypes.BadInput{ | ||
Message: "Telaria: Only Supports Video", | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Fetches the populated header object | ||
func GetHeaders(request *openrtb.BidRequest) *http.Header { | ||
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("Accept-Encoding", "gzip") | ||
|
||
if request.Device != nil { | ||
if len(request.Device.UA) > 0 { | ||
headers.Add("User-Agent", request.Device.UA) | ||
} | ||
|
||
if len(request.Device.IP) > 0 { | ||
headers.Add("X-Forwarded-For", request.Device.IP) | ||
} | ||
|
||
if len(request.Device.Language) > 0 { | ||
headers.Add("Accept-Language", request.Device.Language) | ||
} | ||
|
||
if request.Device.DNT != nil { | ||
headers.Add("Dnt", strconv.Itoa(int(*request.Device.DNT))) | ||
} | ||
} | ||
|
||
return &headers | ||
} | ||
|
||
// Checks the imp[i].ext object and returns a imp.ext object as per ExtImpTelaria format | ||
func (a *TelariaAdapter) FetchTelariaExtImpParams(imp *openrtb.Imp) (*openrtb_ext.ExtImpTelaria, error) { | ||
var bidderExt adapters.ExtImpBidder | ||
err := json.Unmarshal(imp.Ext, &bidderExt) | ||
|
||
if err != nil { | ||
err = &errortypes.BadInput{ | ||
Message: "Telaria: ext.bidder not provided", | ||
} | ||
|
||
return nil, err | ||
} | ||
|
||
var telariaExt openrtb_ext.ExtImpTelaria | ||
err = json.Unmarshal(bidderExt.Bidder, &telariaExt) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if telariaExt.SeatCode == "" { | ||
return nil, &errortypes.BadInput{Message: "Telaria: Seat Code required"} | ||
} | ||
|
||
return &telariaExt, nil | ||
} | ||
|
||
// Method to fetch the original publisher ID. Note that this method must be called | ||
// before we replace publisher.ID with seatCode | ||
func (a *TelariaAdapter) FetchOriginalPublisherID(request *openrtb.BidRequest) string { | ||
|
||
if request.Site != nil && request.Site.Publisher != nil { | ||
return request.Site.Publisher.ID | ||
} else if request.App != nil && request.App.Publisher != nil { | ||
return request.App.Publisher.ID | ||
} | ||
|
||
return "" | ||
} | ||
|
||
// Method to do a deep copy of the publisher object. It also adds the seatCode as publisher.ID | ||
func (a *TelariaAdapter) MakePublisherObject(seatCode string, publisher *openrtb.Publisher) *openrtb.Publisher { | ||
var pub = &openrtb.Publisher{ID: seatCode} | ||
|
||
if publisher != nil { | ||
pub.Domain = publisher.Domain | ||
pub.Name = publisher.Name | ||
pub.Cat = publisher.Cat | ||
pub.Ext = publisher.Ext | ||
} | ||
|
||
return pub | ||
} | ||
|
||
// This method changes <site/app>.publisher.id to the seatCode | ||
func (a *TelariaAdapter) PopulatePublisherId(request *openrtb.BidRequest, seatCode string) (*openrtb.Site, *openrtb.App) { | ||
if request.Site != nil { | ||
siteCopy := *request.Site | ||
siteCopy.Publisher = a.MakePublisherObject(seatCode, request.Site.Publisher) | ||
return &siteCopy, nil | ||
} else if request.App != nil { | ||
appCopy := *request.App | ||
appCopy.Publisher = a.MakePublisherObject(seatCode, request.App.Publisher) | ||
return nil, &appCopy | ||
} | ||
return nil, nil | ||
} | ||
|
||
func (a *TelariaAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { | ||
|
||
// make a copy of the incoming request | ||
request := *requestIn | ||
|
||
// ensure that the request has Impressions | ||
if noImps := a.CheckHasImps(&request); noImps != nil { | ||
return nil, []error{noImps} | ||
} | ||
|
||
// ensure that the request has a Video object | ||
if noVideoObjectError := a.CheckHasVideoObject(&request); noVideoObjectError != nil { | ||
return nil, []error{noVideoObjectError} | ||
} | ||
|
||
var seatCode string | ||
originalPublisherID := a.FetchOriginalPublisherID(&request) | ||
|
||
var errors []error | ||
for i, imp := range request.Imp { | ||
// fetch adCode & seatCode from Imp[i].Ext | ||
telariaExt, err := a.FetchTelariaExtImpParams(&imp) | ||
if err != nil { | ||
errors = append(errors, err) | ||
break | ||
} | ||
|
||
seatCode = telariaExt.SeatCode | ||
|
||
// move the original tagId and the original publisher.id into the Imp[i].Ext object | ||
request.Imp[i].Ext, err = json.Marshal(&ImpressionExtOut{request.Imp[i].TagID, originalPublisherID}) | ||
if err != nil { | ||
errors = append(errors, err) | ||
break | ||
} | ||
|
||
// Swap the tagID with adCode | ||
request.Imp[i].TagID = telariaExt.AdCode | ||
} | ||
|
||
if len(errors) > 0 { | ||
return nil, errors | ||
} | ||
|
||
// Add seatCode to <Site/App>.Publisher.ID | ||
siteObject, appObject := a.PopulatePublisherId(&request, seatCode) | ||
|
||
request.Site = siteObject | ||
request.App = appObject | ||
|
||
reqJSON, err := json.Marshal(request) | ||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
|
||
return []*adapters.RequestData{{ | ||
Method: "POST", | ||
Uri: a.FetchEndpoint(), | ||
Body: reqJSON, | ||
Headers: *GetHeaders(&request), | ||
}}, nil | ||
} | ||
|
||
// response isn't automatically decompressed. This method unzips the response if Content-Encoding is gzip | ||
func GetResponseBody(response *adapters.ResponseData) ([]byte, error) { | ||
|
||
if "gzip" == response.Headers.Get("Content-Encoding") { | ||
body := bytes.NewBuffer(response.Body) | ||
r, readerErr := gzip.NewReader(body) | ||
if readerErr != nil { | ||
return nil, &errortypes.BadServerResponse{ | ||
Message: fmt.Sprintf("Error while trying to unzip data [ %d ]", response.StatusCode), | ||
} | ||
} | ||
var resB bytes.Buffer | ||
var err error | ||
_, err = resB.ReadFrom(r) | ||
if err != nil { | ||
return nil, &errortypes.BadServerResponse{ | ||
Message: fmt.Sprintf("Error while trying to unzip data [ %d ]", response.StatusCode), | ||
} | ||
} | ||
|
||
response.Headers.Del("Content-Encoding") | ||
|
||
return resB.Bytes(), nil | ||
} else { | ||
return response.Body, nil | ||
} | ||
} | ||
|
||
func (a *TelariaAdapter) CheckResponseStatusCodes(response *adapters.ResponseData) error { | ||
if response.StatusCode == http.StatusNoContent { | ||
return &errortypes.BadInput{Message: "Telaria: Invalid Bid Request received by the server"} | ||
} | ||
|
||
if response.StatusCode == http.StatusBadRequest { | ||
return &errortypes.BadInput{ | ||
Message: fmt.Sprintf("Telaria: Unexpected status code: [ %d ] ", response.StatusCode), | ||
} | ||
} | ||
|
||
if response.StatusCode == http.StatusServiceUnavailable { | ||
return &errortypes.BadInput{ | ||
Message: fmt.Sprintf("Telaria: Something went wrong, please contact your Account Manager. Status Code: [ %d ] ", response.StatusCode), | ||
} | ||
} | ||
|
||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { | ||
return &errortypes.BadInput{ | ||
Message: fmt.Sprintf("Telaria: Something went wrong, please contact your Account Manager. Status Code: [ %d ] ", response.StatusCode), | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (a *TelariaAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { | ||
|
||
httpStatusError := a.CheckResponseStatusCodes(response) | ||
if httpStatusError != nil { | ||
return nil, []error{httpStatusError} | ||
} | ||
|
||
responseBody, err := GetResponseBody(response) | ||
|
||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
|
||
var bidResp openrtb.BidResponse | ||
if err := json.Unmarshal(responseBody, &bidResp); err != nil { | ||
return nil, []error{&errortypes.BadServerResponse{ | ||
Message: "Telaria: Bad Server Response", | ||
}} | ||
} | ||
|
||
bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) | ||
sb := bidResp.SeatBid[0] | ||
|
||
for _, bid := range sb.Bid { | ||
bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ | ||
Bid: &bid, | ||
BidType: openrtb_ext.BidTypeVideo, | ||
}) | ||
} | ||
return bidResponse, nil | ||
} | ||
|
||
func NewTelariaBidder(endpoint string) *TelariaAdapter { | ||
if endpoint == "" { | ||
endpoint = Endpoint | ||
} | ||
|
||
return &TelariaAdapter{ | ||
URI: endpoint, | ||
} | ||
} |
Oops, something went wrong.