diff --git a/adapters/feedad/feedad.go b/adapters/feedad/feedad.go new file mode 100644 index 0000000000..dab008bca4 --- /dev/null +++ b/adapters/feedad/feedad.go @@ -0,0 +1,105 @@ +package feedad + +import ( + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v3/adapters" + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/errortypes" + "github.com/prebid/prebid-server/v3/openrtb_ext" + "github.com/prebid/prebid-server/v3/util/jsonutil" +) + +const feedAdAdapterVersion = "1.0.0" + +func getHeaders(request *openrtb2.BidRequest) http.Header { + headers := http.Header{} + headers.Add("Accept", "application/json") + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("X-FA-PBS-Adapter-Version", feedAdAdapterVersion) + headers.Add("X-Openrtb-Version", "2.5") + + if request.Device != nil { + if len(request.Device.IPv6) > 0 { + headers.Add("X-Forwarded-For", request.Device.IPv6) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Forwarded-For", request.Device.IP) + } + } + + return headers +} + +type adapter struct { + endpoint string +} + +func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if responseData.StatusCode == http.StatusNoContent { + return nil, nil + } + + if responseData.StatusCode == http.StatusBadRequest { + err := &errortypes.BadInput{ + Message: "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.", + } + return nil, []error{err} + } + + if responseData.StatusCode != http.StatusOK { + err := &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode), + } + return nil, []error{err} + } + + var response openrtb2.BidResponse + err := jsonutil.Unmarshal(responseData.Body, &response) + if err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp)) + bidResponse.Currency = response.Cur + + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidResponse.Bids = append( + bidResponse.Bids, + &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: openrtb_ext.BidTypeBanner, + }, + ) + } + } + + return bidResponse, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + requestBody, err := jsonutil.Marshal(request) + if err != nil { + return nil, []error{err} + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestBody, + Headers: getHeaders(request), + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + } + return []*adapters.RequestData{requestData}, nil +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} diff --git a/adapters/feedad/feedad_test.go b/adapters/feedad/feedad_test.go new file mode 100644 index 0000000000..4c2d9031d8 --- /dev/null +++ b/adapters/feedad/feedad_test.go @@ -0,0 +1,25 @@ +package feedad + +import ( + "testing" + + "github.com/prebid/prebid-server/v3/adapters/adapterstest" + "github.com/prebid/prebid-server/v3/config" + "github.com/prebid/prebid-server/v3/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderFeedAd, + config.Adapter{ + Endpoint: "https://ortb.feedad.com/1/prebid/requests", + }, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, + ) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error: %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "feedadtest", bidder) +} diff --git a/adapters/feedad/feedadtest/exemplary/banner-app.json b/adapters/feedad/feedadtest/exemplary/banner-app.json new file mode 100644 index 0000000000..85c63f7412 --- /dev/null +++ b/adapters/feedad/feedadtest/exemplary/banner-app.json @@ -0,0 +1,161 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "clientToken": "some-client-token", + "placementId": "some-placement-id" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "X-Fa-Pbs-Adapter-Version": [ + "1.0.0" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "https://ortb.feedad.com/1/prebid/requests", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "1", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "clientToken": "some-client-token", + "placementId": "some-placement-id" + } + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "1" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/feedad/feedadtest/exemplary/banner-site.json b/adapters/feedad/feedadtest/exemplary/banner-site.json new file mode 100644 index 0000000000..d89a253906 --- /dev/null +++ b/adapters/feedad/feedadtest/exemplary/banner-site.json @@ -0,0 +1,207 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "clientToken": "some-client-token", + "placementId": "some-placement-id" + } + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "clientToken": "some-client-token", + "placementId": "some-placement-id" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "X-Fa-Pbs-Adapter-Version": [ + "1.0.0" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "X-Openrtb-Version": [ + "2.5" + ] + }, + "uri": "https://ortb.feedad.com/1/prebid/requests", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "clientToken": "some-client-token", + "placementId": "some-placement-id" + } + } + }, + { + "id": "some-impression-id2", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "clientToken": "some-client-token", + "placementId": "some-placement-id" + } + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + }, + "impIDs": [ + "some-impression-id1", + "some-impression-id2" + ] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "type": "banner", + "seat": "escalax" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + }, + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e162", + "impid": "some-impression-id2", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/feedad/params_test.go b/adapters/feedad/params_test.go new file mode 100644 index 0000000000..75f5d6098b --- /dev/null +++ b/adapters/feedad/params_test.go @@ -0,0 +1,56 @@ +package feedad + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v3/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema: %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderFeedAd, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema: %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderFeedAd, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"clientToken":"some-clienttoken","placementId":"some-placementid"}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{}}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"hybrid_platform":"ios"}}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"hybrid_platform":"windows"}}`, + `{"clientToken":"some-clienttoken","decoration":"some-decoration","placementId":"some-placementid","sdkOptions":{"advertising_id":"","app_name":"","bundle_id":"","hybrid_app":false,"hybrid_platform":"","limit_ad_tracking":false}}`, + `{"clientToken":"some-clienttoken","decoration":"some-decoration","placementId":"some-placementid","sdkOptions":{"advertising_id":"some-advertisingid","app_name":"some-appname","bundle_id":"some-bundleid","hybrid_app":true,"hybrid_platform":"android","limit_ad_tracking":true}}`, +} + +var invalidParams = []string{ + `{}`, + `{"clientToken":"","placementId":"some-placementid"}`, + `{"clientToken":"some-clienttoken","placementId":""}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":"complete-garbage"}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"advertising_id":{}}}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"advertising_id":{}}}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"app_name":{}}}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"bundle_id":{}}}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"hybrid_platform":{}}}`, + `{"clientToken":"some-clienttoken","placementId":"some-placementid","sdkOptions":{"limit_ad_tracking":{}}}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index d1d6a87fb2..da41455740 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -95,6 +95,7 @@ import ( "github.com/prebid/prebid-server/v3/adapters/eplanning" "github.com/prebid/prebid-server/v3/adapters/epom" "github.com/prebid/prebid-server/v3/adapters/escalax" + "github.com/prebid/prebid-server/v3/adapters/feedad" "github.com/prebid/prebid-server/v3/adapters/flipp" "github.com/prebid/prebid-server/v3/adapters/freewheelssp" "github.com/prebid/prebid-server/v3/adapters/frvradn" @@ -328,6 +329,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderEpom: epom.Builder, openrtb_ext.BidderEscalax: escalax.Builder, openrtb_ext.BidderEVolution: evolution.Builder, + openrtb_ext.BidderFeedAd: feedad.Builder, openrtb_ext.BidderFlipp: flipp.Builder, openrtb_ext.BidderFreewheelSSP: freewheelssp.Builder, openrtb_ext.BidderFRVRAdNetwork: frvradn.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 3f6b0a6703..c203ad5adc 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -112,6 +112,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderEpom, BidderEscalax, BidderEVolution, + BidderFeedAd, BidderFlipp, BidderFreewheelSSP, BidderFRVRAdNetwork, @@ -451,6 +452,7 @@ const ( BidderEpom BidderName = "epom" BidderEscalax BidderName = "escalax" BidderEVolution BidderName = "e_volution" + BidderFeedAd BidderName = "feedad" BidderFlipp BidderName = "flipp" BidderFreewheelSSP BidderName = "freewheelssp" BidderFRVRAdNetwork BidderName = "frvradn" diff --git a/openrtb_ext/imp_feedad.go b/openrtb_ext/imp_feedad.go new file mode 100644 index 0000000000..4364fd7b9a --- /dev/null +++ b/openrtb_ext/imp_feedad.go @@ -0,0 +1,17 @@ +package openrtb_ext + +type ExtImpFeedAd struct { + ClientToken string `json:"clientToken"` + Decoration string `json:"decoration"` + PlacementId string `json:"placementId"` + SdkOptions *ExtImpFeedAdSdkOptions `json:"sdkOptions"` +} + +type ExtImpFeedAdSdkOptions struct { + AdvertisingId string `json:"advertising_id"` + AppName string `json:"app_name"` + BundleId string `json:"bundle_id"` + HybridApp bool `json:"hybrid_app"` + HybridPlatform string `json:"hybrid_platform"` + LimitAdTracking bool `json:"limit_ad_tracking"` +} diff --git a/static/bidder-info/feedad.yaml b/static/bidder-info/feedad.yaml new file mode 100644 index 0000000000..a1f55ccaf1 --- /dev/null +++ b/static/bidder-info/feedad.yaml @@ -0,0 +1,17 @@ +endpoint: "https://ortb.feedad.com/1/prebid/requests" +endpointCompression: gzip +maintainer: + email: support@feedad.com +gvlVendorID: 781 +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + site: + mediaTypes: + - banner +userSync: + iframe: + url: https://ortb.feedad.com/1/usersyncs/supply?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + userMacro: $UID diff --git a/static/bidder-params/feedad.json b/static/bidder-params/feedad.json new file mode 100644 index 0000000000..84f5394b4b --- /dev/null +++ b/static/bidder-params/feedad.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "FeedAd Adapter Params", + "description": "A schema which validates params accepted by the FeedAd adapter", + "properties": { + "clientToken": { + "description": "Your FeedAd client token. Check your FeedAd admin panel.", + "minLength": 1, + "type": "string" + }, + "decoration": { + "description": "A decoration to apply to the ad slot. See our documentation at https://docs.feedad.com/web/feed_ad/#decorations", + "type": "string" + }, + "placementId": { + "description": "A FeedAd placement ID of your choice", + "minLength": 1, + "pattern": "^(([a-z0-9])+[-_]?)+$", + "type": "string" + }, + "sdkOptions": { + "description": "Optional: Only required if you are using Prebid.JS in an app environment (aka hybrid app). See our documentation at https://docs.feedad.com/web/configuration/#hybrid-app-config-parameters", + "properties": { + "advertising_id": { + "type": "string", + "description": "Optional: The advertising id of the device or user (e.g. Apple IDFA, Google Advertising Client Id). We highly recommend setting this parameter to maximize your fill rate." + }, + "app_name": { + "type": "string", + "description": "The name of your app. This name will identify your app within the FeedAd admin dashboard." + }, + "bundle_id": { + "type": "string", + "description": "The unique package name or bundle id of your app." + }, + "hybrid_app": { + "type": "boolean", + "description": "Boolean indicating that the SDK is loaded within a hybrid app." + }, + "hybrid_platform": { + "description": "String identifying the device platform.", + "enum": [ + "", + "android", + "ios", + "windows" + ] + }, + "limit_ad_tracking": { + "type": "boolean", + "description": "Whether the app's user has limited ad tracking enabled." + } + }, + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "clientToken", + "placementId" + ], + "type": "object" +} \ No newline at end of file