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: Alkimi #3247

Merged
merged 15 commits into from
Nov 20, 2023
192 changes: 192 additions & 0 deletions adapters/alkimi/alkimi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package alkimi

import (
"encoding/json"
"fmt"
"github.com/prebid/prebid-server/v2/errortypes"
"github.com/prebid/prebid-server/v2/floors"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/prebid/openrtb/v19/openrtb2"
"github.com/prebid/prebid-server/v2/adapters"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

const price_macro = "${AUCTION_PRICE}"

type adapter struct {
endpoint string
}

type extObj struct {
AlkimiBidderExt openrtb_ext.ExtImpAlkimi `json:"bidder"`
}

// Builder builds a new instance of the Alkimi adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
endpointURL, err := url.Parse(config.Endpoint)
if err != nil || len(endpointURL.String()) == 0 {
return nil, fmt.Errorf("invalid endpoint: %v", err)
}

bidder := &adapter{
endpoint: endpointURL.String(),
}
return bidder, nil
}

// MakeRequests creates Alkimi adapter requests
func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, req *adapters.ExtraRequestInfo) (reqsBidder []*adapters.RequestData, errs []error) {
reqCopy := *request

updated, errs := updateImps(reqCopy)
if len(errs) > 0 || len(reqCopy.Imp) != len(updated) {
return nil, errs
}

reqCopy.Imp = updated
encoded, err := json.Marshal(reqCopy)
if err != nil {
errs = append(errs, err)
} else {
reqBidder := buildBidderRequest(adapter, encoded)
reqsBidder = append(reqsBidder, reqBidder)
}
return
}

func updateImps(bidRequest openrtb2.BidRequest) ([]openrtb2.Imp, []error) {
var errs []error

updatedImps := make([]openrtb2.Imp, 0, len(bidRequest.Imp))
for _, imp := range bidRequest.Imp {

var bidderExt adapters.ExtImpBidder
var extImpAlkimi openrtb_ext.ExtImpAlkimi

if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
errs = append(errs, err)
continue
}

if err := json.Unmarshal(bidderExt.Bidder, &extImpAlkimi); err != nil {
errs = append(errs, err)
continue
}

var bidFloorPrice floors.Price
bidFloorPrice.FloorMinCur = imp.BidFloorCur
bidFloorPrice.FloorMin = imp.BidFloor

if len(bidFloorPrice.FloorMinCur) > 0 && bidFloorPrice.FloorMin > 0 {
imp.BidFloor = bidFloorPrice.FloorMin
} else {
imp.BidFloor = extImpAlkimi.BidFloor
}
imp.Instl = extImpAlkimi.Instl
onkarvhanumante marked this conversation as resolved.
Show resolved Hide resolved
imp.Exp = extImpAlkimi.Exp
onkarvhanumante marked this conversation as resolved.
Show resolved Hide resolved

temp := extObj{AlkimiBidderExt: extImpAlkimi}
temp.AlkimiBidderExt.AdUnitCode = imp.ID

extJson, err := json.Marshal(temp)
if err != nil {
errs = append(errs, err)
continue
}
imp.Ext = extJson
updatedImps = append(updatedImps, imp)
}

return updatedImps, errs
}

func buildBidderRequest(adapter *adapter, encoded []byte) *adapters.RequestData {
headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")

reqBidder := &adapters.RequestData{
Method: "POST",
Uri: adapter.endpoint,
Body: encoded,
Headers: headers,
}
return reqBidder
}

// MakeBids will parse the bids from the Alkimi server
func (adapter *adapter) MakeBids(request *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
var errs []error

if adapters.IsResponseStatusCodeNoContent(response) {
return nil, nil
}

if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
return nil, []error{err}
}

var bidResp openrtb2.BidResponse
err := json.Unmarshal(response.Body, &bidResp)
if err != nil {
return nil, []error{err}
}

seatBidCount := len(bidResp.SeatBid)
if seatBidCount == 0 {
return nil, []error{&errortypes.BadServerResponse{
Message: "Empty SeatBid array",
}}
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
for _, seatBid := range bidResp.SeatBid {
for _, bid := range seatBid.Bid {
copyBid := bid
resolveMacros(&copyBid)
impId := copyBid.ImpID
imp := request.Imp
bidType, err := getMediaTypeForImp(impId, imp)
if err != nil {
errs = append(errs, err)
continue
}
bidderBid := &adapters.TypedBid{
Bid: &copyBid,
BidType: bidType,
}
bidResponse.Bids = append(bidResponse.Bids, bidderBid)
}
}
return bidResponse, errs
}

func resolveMacros(bid *openrtb2.Bid) {
strPrice := strconv.FormatFloat(bid.Price, 'f', -1, 64)
bid.NURL = strings.Replace(bid.NURL, price_macro, strPrice, -1)
bid.AdM = strings.Replace(bid.AdM, price_macro, strPrice, -1)
onkarvhanumante marked this conversation as resolved.
Show resolved Hide resolved
}

func getMediaTypeForImp(impId string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
for _, imp := range imps {
if imp.ID == impId {
if imp.Banner != nil {
onkarvhanumante marked this conversation as resolved.
Show resolved Hide resolved
return openrtb_ext.BidTypeBanner, nil
}
if imp.Video != nil {
return openrtb_ext.BidTypeVideo, nil
}
if imp.Audio != nil {
return openrtb_ext.BidTypeAudio, nil
}
}
}
return "", &errortypes.BadInput{
Message: fmt.Sprintf("Failed to find imp \"%s\"", impId),
}
}
57 changes: 57 additions & 0 deletions adapters/alkimi/alkimi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package alkimi

import (
"testing"

"github.com/prebid/prebid-server/v2/adapters"
"github.com/prebid/prebid-server/v2/adapters/adapterstest"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/stretchr/testify/assert"
)

const (
alkimiTestEndpoint = "https://exchange.alkimi-onboarding.com/server/bid"
)

func TestJsonSamples(t *testing.T) {
bidder, buildErr := Builder(
openrtb_ext.BidderAlkimi,
config.Adapter{Endpoint: alkimiTestEndpoint},
config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"},
)

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

adapterstest.RunJSONBidderTest(t, "alkimitest", bidder)
}

func TestEndpointEmpty(t *testing.T) {
_, buildErr := Builder(openrtb_ext.BidderAlkimi, config.Adapter{
Endpoint: ""}, config.Server{ExternalUrl: alkimiTestEndpoint, GvlID: 1, DataCenter: "2"})
assert.Error(t, buildErr)
}

func TestEndpointMalformed(t *testing.T) {
_, buildErr := Builder(openrtb_ext.BidderAlkimi, config.Adapter{
Endpoint: " http://leading.space.is.invalid"}, config.Server{ExternalUrl: alkimiTestEndpoint, GvlID: 1, DataCenter: "2"})
assert.Error(t, buildErr)
}

func TestBuilder(t *testing.T) {
bidder, buildErr := buildBidder()
if buildErr != nil {
t.Fatalf("Failed to build bidder: %v", buildErr)
}
assert.NotNil(t, bidder)
}

func buildBidder() (adapters.Bidder, error) {
return Builder(
openrtb_ext.BidderAlkimi,
config.Adapter{Endpoint: alkimiTestEndpoint},
config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"},
)
}
143 changes: 143 additions & 0 deletions adapters/alkimi/alkimitest/exemplary/simple-audio.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
{
"mockBidRequest": {
"id": "test-request-id",
"device": {
"ip": "123.123.123.123",
"ua": "iPad"
},
"site": {
"domain": "www.example.com",
"page": "http://www.example.com",
"publisher": {
"domain": "example.com"
},
"ext": {
"amp": 0
}
},
"imp": [
{
"id": "test-imp-id",
"tagid": "test",
"audio": {
"mimes": [
"audio/mpeg",
"audio/mp3"
],
"minduration": 5,
"maxduration": 30,
"minbitrate": 32,
"maxbitrate": 128
},
"bidfloor": 0.7,
"bidfloorcur": "USD",
"ext": {
"bidder": {
"token": "XXX",
"bidFloor": 0.5
}
}
}
]
},
"httpCalls": [
{
"expectedRequest": {
"uri": "https://exchange.alkimi-onboarding.com/server/bid",
"body": {
"id": "test-request-id",
"imp": [
{
"id": "test-imp-id",
"tagid": "test",
"audio": {
"mimes": [
"audio/mpeg",
"audio/mp3"
],
"minduration": 5,
"maxduration": 30,
"minbitrate": 32,
"maxbitrate": 128
},
"bidfloor": 0.7,
"bidfloorcur": "USD",
"ext": {
"bidder": {
"token": "XXX",
"bidFloor": 0.5,
"adUnitCode": "test-imp-id",
"exp": 0,
"instl": 0
}
}
}
],
"site": {
"domain": "www.example.com",
"page": "http://www.example.com",
"publisher": {
"domain": "example.com"
},
"ext": {
"amp": 0
}
},
"device": {
"ip": "123.123.123.123",
"ua": "iPad"
}
}
},
"mockResponse": {
"status": 200,
"body": {
"id": "test-request-id",
"seatbid": [
{
"bid": [
{
"id": "test_bid_id",
"impid": "test-imp-id",
"price": 0.9,
"adm": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><VAST version=\"2.0\"><Ad id=\"128a6.44d74.46b3\"><InLine><Error><![CDATA[http:\/\/example.net\/hbx\/verr?e=]]><\/Error><Impression><![CDATA[http:\/\/example.net\/hbx\/vimp?lid=test&aid=testapp]]><\/Impression><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15<\/Duration><TrackingEvents><Tracking event=\"firstQuartile\"><![CDATA[https:\/\/example.com?event=first_quartile]]><\/Tracking><\/TrackingEvents><VideoClicks><ClickThrough><![CDATA[http:\/\/example.com]]><\/ClickThrough><\/VideoClicks><MediaFiles><MediaFile delivery=\"progressive\" width=\"16\" height=\"9\" type=\"audio\/mp3\" bitrate=\"128\"><![CDATA[https:\/\/example.com\/media.mp4]]><\/MediaFile><\/MediaFiles><\/Linear><\/Creative><\/Creatives><\/InLine><\/Ad><\/VAST>",
"cid": "test_cid",
"crid": "test_crid",
"ext": {
"prebid": {
"type": "audio"
}
}
}
],
"seat": "alkimi"
}
],
"cur": "USD"
}
}
}
],
"expectedBidResponses": [
{
"bids": [
{
"bid": {
"id": "test_bid_id",
"impid": "test-imp-id",
"price": 0.9,
"adm": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><VAST version=\"2.0\"><Ad id=\"128a6.44d74.46b3\"><InLine><Error><![CDATA[http:\/\/example.net\/hbx\/verr?e=]]><\/Error><Impression><![CDATA[http:\/\/example.net\/hbx\/vimp?lid=test&aid=testapp]]><\/Impression><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15<\/Duration><TrackingEvents><Tracking event=\"firstQuartile\"><![CDATA[https:\/\/example.com?event=first_quartile]]><\/Tracking><\/TrackingEvents><VideoClicks><ClickThrough><![CDATA[http:\/\/example.com]]><\/ClickThrough><\/VideoClicks><MediaFiles><MediaFile delivery=\"progressive\" width=\"16\" height=\"9\" type=\"audio\/mp3\" bitrate=\"128\"><![CDATA[https:\/\/example.com\/media.mp4]]><\/MediaFile><\/MediaFiles><\/Linear><\/Creative><\/Creatives><\/InLine><\/Ad><\/VAST>",
"cid": "test_cid",
"crid": "test_crid",
"ext": {
"prebid": {
"type": "audio"
}
}
},
"type": "audio"
}
]
}
]
}
Loading
Loading