From b8898cd437254271c12e96d842ca16a2be7eaceb Mon Sep 17 00:00:00 2001 From: Ad Generation Date: Fri, 24 Apr 2020 00:45:45 +0900 Subject: [PATCH] Ad Generation Adapter Integration. (#1253) * AdGeneration Integration. * update AdGeneration adapter. fix: some methods of the adgAdapter replace to functions. fix: unmarshal functions return a pointer. fix: header is defined once. fix: return when imps is appended * update AdGeneration Adapter. add: Added a comment in usersync. add: Added a test for parameters whose ID does not exist in params_test. change: Change to query creation by net/url. Added getRawQuery Test. fix: Changed variable names related to bidRequest. --- adapters/adgeneration/adgeneration.go | 260 ++++++++++++++++++ adapters/adgeneration/adgeneration_test.go | 176 ++++++++++++ .../exemplary/single-banner.json | 151 ++++++++++ .../adgenerationtest/params/race/banner.json | 3 + .../supplemental/204-bid-response.json | 72 +++++ .../supplemental/400-bid-response.json | 77 ++++++ .../supplemental/invalid-adg-param.json | 31 +++ .../supplemental/no-bid-response.json | 89 ++++++ adapters/adgeneration/params_test.go | 47 ++++ config/config.go | 2 + exchange/adapter_map.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_adgeneration.go | 5 + static/bidder-info/adgeneration.yaml | 10 + static/bidder-params/adgeneration.json | 15 + usersync/usersyncers/syncer_test.go | 13 +- 16 files changed, 949 insertions(+), 6 deletions(-) create mode 100644 adapters/adgeneration/adgeneration.go create mode 100644 adapters/adgeneration/adgeneration_test.go create mode 100644 adapters/adgeneration/adgenerationtest/exemplary/single-banner.json create mode 100644 adapters/adgeneration/adgenerationtest/params/race/banner.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/invalid-adg-param.json create mode 100644 adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json create mode 100644 adapters/adgeneration/params_test.go create mode 100644 openrtb_ext/imp_adgeneration.go create mode 100644 static/bidder-info/adgeneration.yaml create mode 100644 static/bidder-params/adgeneration.json diff --git a/adapters/adgeneration/adgeneration.go b/adapters/adgeneration/adgeneration.go new file mode 100644 index 00000000000..4b1215dea9d --- /dev/null +++ b/adapters/adgeneration/adgeneration.go @@ -0,0 +1,260 @@ +package adgeneration + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type AdgenerationAdapter struct { + endpoint string + version string + defaultCurrency string +} + +// Server Responses +type adgServerResponse struct { + Locationid string `json:"locationid"` + Dealid string `json:"dealid"` + Ad string `json:"ad"` + Beacon string `json:"beacon"` + Beaconurl string `json:"beaconurl"` + Cpm float64 `jsons:"cpm"` + Creativeid string `json:"creativeid"` + H uint64 `json:"h"` + W uint64 `json:"w"` + Ttl uint64 `json:"ttl"` + Vastxml string `json:"vastxml,omitempty"` + LandingUrl string `json:"landing_url"` + Scheduleid string `json:"scheduleid"` + Results []interface{} `json:"results"` +} + +func (adg *AdgenerationAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + numRequests := len(request.Imp) + var errs []error + + if numRequests == 0 { + errs = append(errs, &errortypes.BadInput{ + Message: "No impression in the bid request", + }) + return nil, errs + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + bidRequestArray := make([]*adapters.RequestData, 0, numRequests) + + for index := 0; index < numRequests; index++ { + bidRequestUri, err := adg.getRequestUri(request, index) + if err != nil { + errs = append(errs, err) + return nil, errs + } + bidRequest := &adapters.RequestData{ + Method: "GET", + Uri: bidRequestUri, + Body: nil, + Headers: headers, + } + bidRequestArray = append(bidRequestArray, bidRequest) + } + + return bidRequestArray, errs +} + +func (adg *AdgenerationAdapter) getRequestUri(request *openrtb.BidRequest, index int) (string, error) { + imp := request.Imp[index] + adgExt, err := unmarshalExtImpAdgeneration(&imp) + if err != nil { + return "", &errortypes.BadInput{ + Message: err.Error(), + } + } + uriObj, err := url.Parse(adg.endpoint) + if err != nil { + return "", &errortypes.BadInput{ + Message: err.Error(), + } + } + v := adg.getRawQuery(adgExt.Id, request, &imp) + uriObj.RawQuery = v.Encode() + return uriObj.String(), err +} + +func (adg *AdgenerationAdapter) getRawQuery(id string, request *openrtb.BidRequest, imp *openrtb.Imp) *url.Values { + v := url.Values{} + v.Set("posall", "SSPLOC") + v.Set("id", id) + v.Set("sdktype", "0") + v.Set("hb", "true") + v.Set("t", "json3") + v.Set("currency", adg.getCurrency(request)) + v.Set("sdkname", "prebidserver") + v.Set("adapterver", adg.version) + adSize := getSizes(imp) + if adSize != "" { + v.Set("size", adSize) + } + if request.Site != nil && request.Site.Page != "" { + v.Set("tp", request.Site.Page) + } + return &v +} + +func unmarshalExtImpAdgeneration(imp *openrtb.Imp) (*openrtb_ext.ExtImpAdgeneration, error) { + var bidderExt adapters.ExtImpBidder + var adgExt openrtb_ext.ExtImpAdgeneration + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, err + } + if err := json.Unmarshal(bidderExt.Bidder, &adgExt); err != nil { + return nil, err + } + if adgExt.Id == "" { + return nil, errors.New("No Location ID in ExtImpAdgeneration.") + } + return &adgExt, nil +} + +func getSizes(imp *openrtb.Imp) string { + if imp.Banner == nil || len(imp.Banner.Format) == 0 { + return "" + } + var sizeStr string + for _, v := range imp.Banner.Format { + sizeStr += strconv.FormatUint(v.W, 10) + "×" + strconv.FormatUint(v.H, 10) + "," + } + if len(sizeStr) > 0 && strings.LastIndex(sizeStr, ",") == len(sizeStr)-1 { + sizeStr = sizeStr[:len(sizeStr)-1] + } + return sizeStr +} + +func (adg *AdgenerationAdapter) getCurrency(request *openrtb.BidRequest) string { + if len(request.Cur) <= 0 { + return adg.defaultCurrency + } else { + for _, c := range request.Cur { + if adg.defaultCurrency == c { + return c + } + } + return request.Cur[0] + } +} + +func (adg *AdgenerationAdapter) MakeBids(internalRequest *openrtb.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. Run with request.debug = 1 for more info", 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 adgServerResponse + err := json.Unmarshal(response.Body, &bidResp) + if err != nil { + return nil, []error{err} + } + if len(bidResp.Results) <= 0 { + return nil, nil + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + var impId string + var bitType openrtb_ext.BidType + var adm string + for _, v := range internalRequest.Imp { + adgExt, err := unmarshalExtImpAdgeneration(&v) + if err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: err.Error(), + }, + } + } + if adgExt.Id == bidResp.Locationid { + impId = v.ID + bitType = openrtb_ext.BidTypeBanner + adm = createAd(&bidResp, impId) + bid := openrtb.Bid{ + ID: bidResp.Locationid, + ImpID: impId, + AdM: adm, + Price: bidResp.Cpm, + W: bidResp.W, + H: bidResp.H, + CrID: bidResp.Creativeid, + DealID: bidResp.Dealid, + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: bitType, + }) + return bidResponse, nil + } + } + return nil, nil +} + +func createAd(body *adgServerResponse, impId string) string { + ad := body.Ad + if body.Vastxml != "" { + ad = "
" + insertVASTMethod(impId, body.Vastxml) + "" + } + ad = appendChildToBody(ad, body.Beacon) + unwrappedAd := removeWrapper(ad) + if unwrappedAd != "" { + return unwrappedAd + } + return ad +} + +func insertVASTMethod(bidId string, vastxml string) string { + rep := regexp.MustCompile(`/\r?\n/g`) + var replacedVastxml = rep.ReplaceAllString(vastxml, "") + return "" +} + +func appendChildToBody(ad string, data string) string { + rep := regexp.MustCompile(`<\/\s?body>`) + return rep.ReplaceAllString(ad, data+"") +} + +func removeWrapper(ad string) string { + bodyIndex := strings.Index(ad, "") + lastBodyIndex := strings.LastIndex(ad, "") + if bodyIndex == -1 || lastBodyIndex == -1 { + return "" + } + + str := strings.TrimSpace(strings.Replace(strings.Replace(ad[bodyIndex:lastBodyIndex], "", "", 1), "", "", 1)) + return str +} + +func NewAdgenerationAdapter(endpoint string) *AdgenerationAdapter { + return &AdgenerationAdapter{ + endpoint, + "1.0.0", + "JPY", + } +} diff --git a/adapters/adgeneration/adgeneration_test.go b/adapters/adgeneration/adgeneration_test.go new file mode 100644 index 00000000000..e76995fc5e4 --- /dev/null +++ b/adapters/adgeneration/adgeneration_test.go @@ -0,0 +1,176 @@ +package adgeneration + +import ( + "encoding/json" + "testing" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + adapterstest.RunJSONBidderTest(t, "adgenerationtest", NewAdgenerationAdapter("https://d.socdm.com/adsv/v1")) +} + +func TestgetRequestUri(t *testing.T) { + bidder := NewAdgenerationAdapter("https://d.socdm.com/adsv/v1") + // Test items + failedRequest := &openrtb.BidRequest{ + ID: "test-failed-bid-request", + Imp: []openrtb.Imp{ + {ID: "extImpBidder-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{{ "id": "58278" }}`)}, + {ID: "extImpBidder-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"_bidder": { "id": "58278" }}`)}, + {ID: "extImpAdgeneration-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "_id": "58278" }}`)}, + }, + Device: &openrtb.Device{UA: "testUA", IP: "testIP"}, + Site: &openrtb.Site{Page: "https://supership.com"}, + User: &openrtb.User{BuyerUID: "buyerID"}, + } + successRequest := &openrtb.BidRequest{ + ID: "test-success-bid-request", + Imp: []openrtb.Imp{ + {ID: "bidRequest-success-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "id": "58278" }}`)}, + }, + Device: &openrtb.Device{UA: "testUA", IP: "testIP"}, + Site: &openrtb.Site{Page: "https://supership.com"}, + User: &openrtb.User{BuyerUID: "buyerID"}, + } + + numRequests := len(failedRequest.Imp) + for index := 0; index < numRequests; index++ { + httpRequests, err := bidder.getRequestUri(failedRequest, index) + if err == nil { + t.Errorf("getRequestUri: %v did not throw an error", failedRequest.Imp[index]) + } + if httpRequests != "" { + t.Errorf("getRequestUri: %v did return Request: %s", failedRequest.Imp[index], httpRequests) + } + } + numRequests = len(successRequest.Imp) + for index := 0; index < numRequests; index++ { + // RequestUri Test. + httpRequests, err := bidder.getRequestUri(successRequest, index) + if err != nil { + t.Errorf("getRequestUri: %v did throw an error: %v", successRequest.Imp[index], err) + } + if httpRequests == "adapterver="+bidder.version+"¤cy=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html" { + t.Errorf("getRequestUri: %v did return Request: %s", successRequest.Imp[index], httpRequests) + } + // getRawQuery Test. + adgExt, err := unmarshalExtImpAdgeneration(&successRequest.Imp[index]) + if err != nil { + t.Errorf("unmarshalExtImpAdgeneration: %v did throw an error: %v", successRequest.Imp[index], err) + } + rawQuery := bidder.getRawQuery(adgExt.Id, successRequest, &successRequest.Imp[index]) + expectQueries := map[string]string{ + "posall": "SSPLOC", + "id": adgExt.Id, + "sdktype": "0", + "hb": "true", + "currency": bidder.getCurrency(successRequest), + "sdkname": "prebidserver", + "adapterver": bidder.version, + "size": getSizes(&successRequest.Imp[index]), + "tp": successRequest.Site.Name, + } + for key, expectedValue := range expectQueries { + actualValue := rawQuery.Get(key) + if actualValue == "" { + if !(key == "size" || key == "tp") { + t.Errorf("getRawQuery: key %s is required value.", key) + } + } + if actualValue != expectedValue { + t.Errorf("getRawQuery: %s value does not match expected %s, actual %s", key, expectedValue, actualValue) + } + } + } +} + +func TestGetSizes(t *testing.T) { + // Test items + var request *openrtb.Imp + var size string + multiFormatBanner := &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}, {W: 320, H: 50}}} + noFormatBanner := &openrtb.Banner{Format: []openrtb.Format{}} + nativeFormat := &openrtb.Native{} + + request = &openrtb.Imp{Banner: multiFormatBanner} + size = getSizes(request) + if size != "300×250,320×50" { + t.Errorf("%v does not match size.", multiFormatBanner) + } + request = &openrtb.Imp{Banner: noFormatBanner} + size = getSizes(request) + if size != "" { + t.Errorf("%v does not match size.", noFormatBanner) + } + request = &openrtb.Imp{Native: nativeFormat} + size = getSizes(request) + if size != "" { + t.Errorf("%v does not match size.", nativeFormat) + } +} + +func TestGetCurrency(t *testing.T) { + bidder := NewAdgenerationAdapter("https://d.socdm.com/adsv/v1") + // Test items + var request *openrtb.BidRequest + var currency string + innerDefaultCur := []string{"USD", "JPY"} + usdCur := []string{"USD", "EUR"} + + request = &openrtb.BidRequest{Cur: innerDefaultCur} + currency = bidder.getCurrency(request) + if currency != "JPY" { + t.Errorf("%v does not match currency.", innerDefaultCur) + } + request = &openrtb.BidRequest{Cur: usdCur} + currency = bidder.getCurrency(request) + if currency != "USD" { + t.Errorf("%v does not match currency.", usdCur) + } +} + +func TestCreateAd(t *testing.T) { + // Test items + adgBannerImpId := "test-banner-imp" + adgBannerResponse := adgServerResponse{ + Ad: "\n\n\n\n\n
\n\n
\n\n", + Beacon: "", + Beaconurl: "https://dummy-beacon.com", + Cpm: 50, + Creativeid: "DummyDsp_SdkTeam_supership.jp", + H: 300, + W: 250, + Ttl: 10, + LandingUrl: "", + Scheduleid: "111111", + } + matchBannerTag := "
\n\n
\n" + + adgVastImpId := "test-vast-imp" + adgVastResponse := adgServerResponse{ + Ad: "\n\n\n\n\n
\n\n
\n\n", + Beacon: "", + Beaconurl: "https://dummy-beacon.com", + Cpm: 50, + Creativeid: "DummyDsp_SdkTeam_supership.jp", + H: 300, + W: 250, + Ttl: 10, + LandingUrl: "", + Vastxml: "", + Scheduleid: "111111", + } + matchVastTag := "
" + + bannerAd := createAd(&adgBannerResponse, adgBannerImpId) + if bannerAd != matchBannerTag { + t.Errorf("%v does not match createAd.", adgBannerResponse) + } + vastAd := createAd(&adgVastResponse, adgVastImpId) + if vastAd != matchVastTag { + t.Errorf("%v does not match createAd.", adgVastResponse) + } +} diff --git a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json new file mode 100644 index 00000000000..d23a510bee5 --- /dev/null +++ b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json @@ -0,0 +1,151 @@ +{ + "mockBidRequest":{ + "id": "some-request-id", + "site": { + "page": "http://example.com/test.html" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "id": "58278" + } + } + } + ], + "tmax": 500 + }, + "httpCalls": [ + { + "internalRequest": { + "id": "some-request-id", + "site": { + "page": "http://example.com/test.html" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "id": "58278" + } + } + } + ], + "tmax": 500 + }, + "expectedRequest":{ + "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.0¤cy=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + } + }, + "mockResponse":{ + "status": 200, + "body": { + "ad": "\n \n \n