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

Bidder-specific imp FPD #3704

Merged
merged 8 commits into from
Jun 4, 2024
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
4 changes: 2 additions & 2 deletions endpoints/openrtb2/amp_auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,8 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
errL = append(errL, errs...)
}

hasStoredResponses := len(storedAuctionResponses) > 0
errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false)
hasStoredAuctionResponses := len(storedAuctionResponses) > 0
errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredAuctionResponses, storedBidResponses, false)
errL = append(errL, errs...)
ao.Errors = append(ao.Errors, errs...)
if errortypes.ContainsFatalError(errs) {
Expand Down
8 changes: 4 additions & 4 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,8 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
return nil, nil, nil, nil, nil, nil, errs
}

hasStoredResponses := len(storedAuctionResponses) > 0
errL = deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest)
hasStoredAuctionResponses := len(storedAuctionResponses) > 0
errL = deps.validateRequest(account, httpRequest, req, false, hasStoredAuctionResponses, storedBidResponses, hasStoredBidRequest)
if len(errL) > 0 {
errs = append(errs, errL...)
}
Expand Down Expand Up @@ -757,7 +757,7 @@ func mergeBidderParamsImpExtPrebid(impExt *openrtb_ext.ImpExt, reqExtParams map[
return nil
}

func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http.Request, req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error {
func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http.Request, req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredAuctionResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error {
errL := []error{}
if req.ID == "" {
return []error{errors.New("request missing required field: \"id\"")}
Expand Down Expand Up @@ -918,7 +918,7 @@ func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http
}
impIDs[imp.ID] = i

errs := deps.requestValidator.ValidateImp(imp, i, requestAliases, hasStoredResponses, storedBidResp)
errs := deps.requestValidator.ValidateImp(imp, i, requestAliases, hasStoredAuctionResponses, storedBidResp)
if len(errs) > 0 {
errL = append(errL, errs...)
}
Expand Down
4 changes: 3 additions & 1 deletion endpoints/openrtb2/auction_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
if err != nil {
return
}
requestValidator := ortb.NewRequestValidator(nil, map[string]string{}, paramValidator)

nilMetrics := &metricsConfig.NilMetricsEngine{}

Expand All @@ -88,6 +89,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
adapters,
nil,
&config.Configuration{},
requestValidator,
map[string]usersync.Syncer{},
nilMetrics,
infos,
Expand All @@ -102,7 +104,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
endpoint, _ := NewEndpoint(
fakeUUIDGenerator{},
exchange,
ortb.NewRequestValidator(nil, map[string]string{}, paramValidator),
requestValidator,
empty_fetcher.EmptyFetcher{},
empty_fetcher.EmptyFetcher{},
&config.Configuration{MaxRequestSize: maxSize},
Expand Down
5 changes: 3 additions & 2 deletions endpoints/openrtb2/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r *exchange.Auct
}

// buildTestExchange returns an exchange with mock bidder servers and mock currency conversion server
func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.BidderName]exchange.AdaptedBidder, mockBidServersArray []*httptest.Server, mockCurrencyRatesServer *httptest.Server, bidderInfos config.BidderInfos, cfg *config.Configuration, met metrics.MetricsEngine, mockFetcher stored_requests.CategoryFetcher) (exchange.Exchange, []*httptest.Server) {
func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.BidderName]exchange.AdaptedBidder, mockBidServersArray []*httptest.Server, mockCurrencyRatesServer *httptest.Server, bidderInfos config.BidderInfos, cfg *config.Configuration, met metrics.MetricsEngine, mockFetcher stored_requests.CategoryFetcher, requestValidator ortb.RequestValidator) (exchange.Exchange, []*httptest.Server) {
if len(testCfg.MockBidders) == 0 {
testCfg.MockBidders = append(testCfg.MockBidders, mockBidderHandler{BidderName: "appnexus", Currency: "USD", Price: 0.00})
}
Expand All @@ -1220,6 +1220,7 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
testExchange := exchange.NewExchange(adapterMap,
&wellBehavedCache{},
cfg,
requestValidator,
nil,
met,
bidderInfos,
Expand Down Expand Up @@ -1276,7 +1277,7 @@ func buildTestEndpoint(test testCase, cfg *config.Configuration) (httprouter.Han
}
mockCurrencyRatesServer := httptest.NewServer(http.HandlerFunc(mockCurrencyConversionService.handle))

testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher)
testExchange, mockBidServersArray := buildTestExchange(test.Config, adapterMap, mockBidServersArray, mockCurrencyRatesServer, bidderInfos, cfg, met, mockFetcher, requestValidator)

var storedRequestFetcher stored_requests.Fetcher
if len(test.StoredRequest) > 0 {
Expand Down
1 change: 1 addition & 0 deletions errortypes/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
TmaxTimeoutErrorCode
FailedToMarshalErrorCode
FailedToUnmarshalErrorCode
InvalidImpFirstPartyDataErrorCode
)

// Defines numeric codes for well-known warnings.
Expand Down
18 changes: 18 additions & 0 deletions errortypes/errortypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,21 @@ func (err *DebugWarning) Severity() Severity {
func (err *DebugWarning) Scope() Scope {
return ScopeDebug
}

// InvalidImpFirstPartyData should be used when the retrieved account config cannot be unmarshaled
// These errors will be written to http.ResponseWriter before canceling execution
type InvalidImpFirstPartyData struct {
Message string
}

func (err *InvalidImpFirstPartyData) Error() string {
return err.Message
}

func (err *InvalidImpFirstPartyData) Code() int {
return InvalidImpFirstPartyDataErrorCode
}

func (err *InvalidImpFirstPartyData) Severity() Severity {
return SeverityFatal
}
9 changes: 8 additions & 1 deletion exchange/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"time"

"github.com/prebid/prebid-server/v2/ortb"
"github.com/prebid/prebid-server/v2/privacy"

"github.com/prebid/prebid-server/v2/adapters"
Expand Down Expand Up @@ -132,7 +133,7 @@ func (randomDeduplicateBidBooleanGenerator) Generate() bool {
return rand.Intn(100) < 50
}

func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer, priceFloorFetcher floors.FloorFetcher) Exchange {
func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, requestValidator ortb.RequestValidator, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer, priceFloorFetcher floors.FloorFetcher) Exchange {
bidderToSyncerKey := map[string]string{}
for bidder, syncer := range syncersByBidder {
bidderToSyncerKey[bidder] = syncer.Key()
Expand All @@ -155,6 +156,7 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid
gdprPermsBuilder: gdprPermsBuilder,
hostSChainNode: cfg.HostSChainNode,
bidderInfo: infos,
requestValidator: requestValidator,
}

return &exchange{
Expand Down Expand Up @@ -338,6 +340,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
SChain: requestExt.GetSChain(),
}
bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprSignal, gdprEnforced, bidAdjustmentFactors)
for _, err := range errs {
if errortypes.ReadCode(err) == errortypes.InvalidImpFirstPartyDataErrorCode {
return nil, err
}
}
Comment on lines +343 to +347
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider replacing this loop to existing ContainsFatalError function, it is also used in Line325

if errortypes.ContainsFatalError([]error{err}) {
       return nil, err
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah this is the direction I was originally going in but figured that it could change the existing behavior for other types of errors and could also introduce a scenario where there are multiple fatal errors that need to be aggregated. As a result, I decided to take an approach that did not disrupt current behavior (even though it is currently flawed) and just handle this one scenario. In that case we can tackle error handling as a whole at some point and address these cases properly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, this works. this is just a minor optional suggestion. Approved!

errs = append(errs, floorErrs...)

mergedBidAdj, err := bidadjustment.Merge(r.BidRequestWrapper, r.Account.BidAdjustments)
Expand Down
40 changes: 30 additions & 10 deletions exchange/exchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ import (
metricsConf "github.com/prebid/prebid-server/v2/metrics/config"
metricsConfig "github.com/prebid/prebid-server/v2/metrics/config"
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/prebid/prebid-server/v2/ortb"
pbc "github.com/prebid/prebid-server/v2/prebid_cache_client"
"github.com/prebid/prebid-server/v2/privacy"
"github.com/prebid/prebid-server/v2/stored_requests"
"github.com/prebid/prebid-server/v2/stored_requests/backends/file_fetcher"
"github.com/prebid/prebid-server/v2/stored_responses"
"github.com/prebid/prebid-server/v2/usersync"
"github.com/prebid/prebid-server/v2/util/jsonutil"
"github.com/prebid/prebid-server/v2/util/ptrutil"
Expand Down Expand Up @@ -82,7 +84,7 @@ func TestNewExchange(t *testing.T) {
},
}.Builder

e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
for _, bidderName := range knownAdapters {
if _, ok := e.adapterMap[bidderName]; !ok {
if biddersInfo[string(bidderName)].IsEnabled() {
Expand Down Expand Up @@ -132,7 +134,7 @@ func TestCharacterEscape(t *testing.T) {
},
}.Builder

e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)

// 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
//liveAdapters []openrtb_ext.BidderName,
Expand Down Expand Up @@ -1235,7 +1237,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) {
},
}.Builder

e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, pbc, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
// 3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
liveAdapters := []openrtb_ext.BidderName{bidderName}

Expand Down Expand Up @@ -1594,7 +1596,7 @@ func TestBidResponseCurrency(t *testing.T) {
},
}.Builder

e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)

liveAdapters := make([]openrtb_ext.BidderName, 1)
liveAdapters[0] = "appnexus"
Expand Down Expand Up @@ -1742,7 +1744,7 @@ func TestBidResponseImpExtInfo(t *testing.T) {
t.Fatalf("Error intializing adapters: %v", adaptersErr)
}

e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)

liveAdapters := make([]openrtb_ext.BidderName, 1)
liveAdapters[0] = "appnexus"
Expand Down Expand Up @@ -1836,7 +1838,7 @@ func TestRaceIntegration(t *testing.T) {
},
}.Builder

ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
ex := NewExchange(adapters, &wellBehavedCache{}, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
_, err = ex.HoldAuction(context.Background(), auctionRequest, &debugLog)
if err != nil {
t.Errorf("HoldAuction returned unexpected error: %v", err)
Expand Down Expand Up @@ -1934,7 +1936,7 @@ func TestPanicRecovery(t *testing.T) {
},
}.Builder

e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)

chBids := make(chan *bidResponseWrapper, 1)
panicker := func(bidderRequest BidderRequest, conversions currency.Conversions) {
Expand Down Expand Up @@ -2004,7 +2006,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) {
allowAllBidders: true,
},
}.Builder
e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, &mockCache{}, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)

e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{}
e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{}
Expand Down Expand Up @@ -2335,7 +2337,8 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
}

func findBiddersInAuction(t *testing.T, context string, req *openrtb2.BidRequest) []string {
if splitImps, err := splitImps(req.Imp); err != nil {

if splitImps, err := splitImps(req.Imp, &mockRequestValidator{}, nil, false, nil); err != nil {
t.Errorf("%s: Failed to parse Bidders from request: %v", context, err)
return nil
} else {
Expand Down Expand Up @@ -2384,6 +2387,8 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
mockResponses: map[string]bidderResponse{string(bidderName): spec.MockResponse},
}
bidderInfos[string(bidderName)] = config.BidderInfo{ModifyingVastXmlAllowed: spec.ModifyingVastXmlAllowed}
} else {
bidderInfos[string(bidderName)] = config.BidderInfo{}
}
}

Expand Down Expand Up @@ -2433,13 +2438,20 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
}

metricsEngine := metricsConf.NewMetricsEngine(&config.Configuration{}, openrtb_ext.CoreBidderNames(), nil, nil)
paramValidator, err := openrtb_ext.NewBidderParamsValidator("../static/bidder-params")
if err != nil {
t.Fatalf("Failed to create params validator: %v", error)
}
bidderMap := GetActiveBidders(bidderInfos)
requestValidator := ortb.NewRequestValidator(bidderMap, map[string]string{}, paramValidator)
requestSplitter := requestSplitter{
bidderToSyncerKey: bidderToSyncerKey,
me: metricsEngine,
privacyConfig: privacyConfig,
gdprPermsBuilder: gdprPermsBuilder,
hostSChainNode: hostSChainNode,
bidderInfo: bidderInfos,
requestValidator: requestValidator,
}

return &exchange{
Expand Down Expand Up @@ -4565,7 +4577,7 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) {
},
}.Builder

e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
e := NewExchange(adapters, nil, cfg, &mockRequestValidator{}, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange)

// Define mock incoming bid requeset
mockBidRequest := &openrtb2.BidRequest{
Expand Down Expand Up @@ -6332,3 +6344,11 @@ func TestBidsToUpdate(t *testing.T) {
})
}
}

type mockRequestValidator struct {
errors []error
}

func (mrv *mockRequestValidator) ValidateImp(imp *openrtb_ext.ImpWrapper, index int, aliases map[string]string, hasStoredResponses bool, storedBidResponses stored_responses.ImpBidderStoredResp) []error {
return mrv.errors
}
55 changes: 55 additions & 0 deletions exchange/exchangetest/imp-fpd-invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"requestType": "openrtb2-web",
"incomingRequest": {
"ortbRequest": {
"id": "some-request-id",
"site": {
"page": "test.somepage.com"
},
"imp": [
{
"id": "imp-id-1",
"banner": {
"format": [
{
"w": 300,
"h": 600
}
]
},
"ext": {
"prebid": {
"bidder": {
"appnexus": {
"placementId": 1
},
"audienceNetwork": {
"placementId": "some-placement"
}
},
"imp": {
"appnexus": {
"id": "imp-id-1-appnexus"
},
"audienceNetwork": {
"banner": {
"format": [
{
"w": -1,
"h": -1
}
]
}
}
}
}
}
}
]
}
},
"outgoingRequests": {},
"response": {
"error": "merging bidder imp first party data for imp imp-id-1 results in an invalid imp: [request.imp[0].banner.format[0].w must be a positive number]"
}
}
Loading
Loading