github.com/prebid/prebid-server/v2@v2.18.0/exchange/exchange.go (about) 1 package exchange 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "math/rand" 10 "net/url" 11 "runtime/debug" 12 "sort" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/prebid/prebid-server/v2/privacy" 18 19 "github.com/prebid/prebid-server/v2/adapters" 20 "github.com/prebid/prebid-server/v2/adservertargeting" 21 "github.com/prebid/prebid-server/v2/bidadjustment" 22 "github.com/prebid/prebid-server/v2/config" 23 "github.com/prebid/prebid-server/v2/currency" 24 "github.com/prebid/prebid-server/v2/dsa" 25 "github.com/prebid/prebid-server/v2/errortypes" 26 "github.com/prebid/prebid-server/v2/exchange/entities" 27 "github.com/prebid/prebid-server/v2/experiment/adscert" 28 "github.com/prebid/prebid-server/v2/firstpartydata" 29 "github.com/prebid/prebid-server/v2/floors" 30 "github.com/prebid/prebid-server/v2/gdpr" 31 "github.com/prebid/prebid-server/v2/hooks/hookexecution" 32 "github.com/prebid/prebid-server/v2/macros" 33 "github.com/prebid/prebid-server/v2/metrics" 34 "github.com/prebid/prebid-server/v2/openrtb_ext" 35 "github.com/prebid/prebid-server/v2/prebid_cache_client" 36 "github.com/prebid/prebid-server/v2/stored_requests" 37 "github.com/prebid/prebid-server/v2/stored_responses" 38 "github.com/prebid/prebid-server/v2/usersync" 39 "github.com/prebid/prebid-server/v2/util/jsonutil" 40 "github.com/prebid/prebid-server/v2/util/maputil" 41 42 "github.com/buger/jsonparser" 43 "github.com/gofrs/uuid" 44 "github.com/golang/glog" 45 "github.com/prebid/openrtb/v20/openrtb2" 46 "github.com/prebid/openrtb/v20/openrtb3" 47 ) 48 49 type extCacheInstructions struct { 50 cacheBids, cacheVAST, returnCreative bool 51 } 52 53 // Exchange runs Auctions. Implementations must be threadsafe, and will be shared across many goroutines. 54 type Exchange interface { 55 // HoldAuction executes an OpenRTB v2.5 Auction. 56 HoldAuction(ctx context.Context, r *AuctionRequest, debugLog *DebugLog) (*AuctionResponse, error) 57 } 58 59 // IdFetcher can find the user's ID for a specific Bidder. 60 type IdFetcher interface { 61 GetUID(key string) (uid string, exists bool, notExpired bool) 62 HasAnyLiveSyncs() bool 63 } 64 65 type exchange struct { 66 adapterMap map[openrtb_ext.BidderName]AdaptedBidder 67 bidderInfo config.BidderInfos 68 bidderToSyncerKey map[string]string 69 me metrics.MetricsEngine 70 cache prebid_cache_client.Client 71 cacheTime time.Duration 72 gdprPermsBuilder gdpr.PermissionsBuilder 73 currencyConverter *currency.RateConverter 74 externalURL string 75 gdprDefaultValue gdpr.Signal 76 privacyConfig config.Privacy 77 categoriesFetcher stored_requests.CategoryFetcher 78 bidIDGenerator BidIDGenerator 79 hostSChainNode *openrtb2.SupplyChainNode 80 adsCertSigner adscert.Signer 81 server config.Server 82 bidValidationEnforcement config.Validations 83 requestSplitter requestSplitter 84 macroReplacer macros.Replacer 85 priceFloorEnabled bool 86 priceFloorFetcher floors.FloorFetcher 87 } 88 89 // Container to pass out response ext data from the GetAllBids goroutines back into the main thread 90 type seatResponseExtra struct { 91 ResponseTimeMillis int 92 Errors []openrtb_ext.ExtBidderMessage 93 Warnings []openrtb_ext.ExtBidderMessage 94 // httpCalls is the list of debugging info. It should only be populated if the request.test == 1. 95 // This will become response.ext.debug.httpcalls.{bidder} on the final Response. 96 HttpCalls []*openrtb_ext.ExtHttpCall 97 } 98 99 type bidResponseWrapper struct { 100 adapterSeatBids []*entities.PbsOrtbSeatBid 101 adapterExtra *seatResponseExtra 102 bidder openrtb_ext.BidderName 103 adapter openrtb_ext.BidderName 104 bidderResponseStartTime time.Time 105 } 106 107 type BidIDGenerator interface { 108 New(bidder string) (string, error) 109 Enabled() bool 110 } 111 112 type bidIDGenerator struct { 113 enabled bool 114 } 115 116 func (big *bidIDGenerator) Enabled() bool { 117 return big.enabled 118 } 119 120 func (big *bidIDGenerator) New(bidder string) (string, error) { 121 rawUuid, err := uuid.NewV4() 122 return rawUuid.String(), err 123 } 124 125 type deduplicateChanceGenerator interface { 126 Generate() bool 127 } 128 129 type randomDeduplicateBidBooleanGenerator struct{} 130 131 func (randomDeduplicateBidBooleanGenerator) Generate() bool { 132 return rand.Intn(100) < 50 133 } 134 135 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 { 136 bidderToSyncerKey := map[string]string{} 137 for bidder, syncer := range syncersByBidder { 138 bidderToSyncerKey[bidder] = syncer.Key() 139 } 140 141 gdprDefaultValue := gdpr.SignalYes 142 if cfg.GDPR.DefaultValue == "0" { 143 gdprDefaultValue = gdpr.SignalNo 144 } 145 146 privacyConfig := config.Privacy{ 147 CCPA: cfg.CCPA, 148 GDPR: cfg.GDPR, 149 LMT: cfg.LMT, 150 } 151 requestSplitter := requestSplitter{ 152 bidderToSyncerKey: bidderToSyncerKey, 153 me: metricsEngine, 154 privacyConfig: privacyConfig, 155 gdprPermsBuilder: gdprPermsBuilder, 156 hostSChainNode: cfg.HostSChainNode, 157 bidderInfo: infos, 158 } 159 160 return &exchange{ 161 adapterMap: adapters, 162 bidderInfo: infos, 163 bidderToSyncerKey: bidderToSyncerKey, 164 cache: cache, 165 cacheTime: time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond, 166 categoriesFetcher: categoriesFetcher, 167 currencyConverter: currencyConverter, 168 externalURL: cfg.ExternalURL, 169 gdprPermsBuilder: gdprPermsBuilder, 170 me: metricsEngine, 171 gdprDefaultValue: gdprDefaultValue, 172 privacyConfig: privacyConfig, 173 bidIDGenerator: &bidIDGenerator{cfg.GenerateBidID}, 174 hostSChainNode: cfg.HostSChainNode, 175 adsCertSigner: adsCertSigner, 176 server: config.Server{ExternalUrl: cfg.ExternalURL, GvlID: cfg.GDPR.HostVendorID, DataCenter: cfg.DataCenter}, 177 bidValidationEnforcement: cfg.Validations, 178 requestSplitter: requestSplitter, 179 macroReplacer: macroReplacer, 180 priceFloorEnabled: cfg.PriceFloors.Enabled, 181 priceFloorFetcher: priceFloorFetcher, 182 } 183 } 184 185 type ImpExtInfo struct { 186 EchoVideoAttrs bool 187 StoredImp []byte 188 Passthrough json.RawMessage 189 } 190 191 // AuctionRequest holds the bid request for the auction 192 // and all other information needed to process that request 193 type AuctionRequest struct { 194 BidRequestWrapper *openrtb_ext.RequestWrapper 195 ResolvedBidRequest json.RawMessage 196 Account config.Account 197 UserSyncs IdFetcher 198 RequestType metrics.RequestType 199 StartTime time.Time 200 Warnings []error 201 GlobalPrivacyControlHeader string 202 ImpExtInfoMap map[string]ImpExtInfo 203 TCF2Config gdpr.TCF2ConfigReader 204 Activities privacy.ActivityControl 205 206 // LegacyLabels is included here for temporary compatibility with cleanOpenRTBRequests 207 // in HoldAuction until we get to factoring it away. Do not use for anything new. 208 LegacyLabels metrics.Labels 209 FirstPartyData map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData 210 // map of imp id to stored response 211 StoredAuctionResponses stored_responses.ImpsWithBidResponses 212 // map of imp id to bidder to stored response 213 StoredBidResponses stored_responses.ImpBidderStoredResp 214 BidderImpReplaceImpID stored_responses.BidderImpReplaceImpID 215 PubID string 216 HookExecutor hookexecution.StageExecutor 217 QueryParams url.Values 218 BidderResponseStartTime time.Time 219 TmaxAdjustments *TmaxAdjustmentsPreprocessed 220 } 221 222 // BidderRequest holds the bidder specific request and all other 223 // information needed to process that bidder request. 224 type BidderRequest struct { 225 BidRequest *openrtb2.BidRequest 226 BidderName openrtb_ext.BidderName 227 BidderCoreName openrtb_ext.BidderName 228 BidderLabels metrics.AdapterLabels 229 BidderStoredResponses map[string]json.RawMessage 230 IsRequestAlias bool 231 ImpReplaceImpId map[string]bool 232 } 233 234 func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog *DebugLog) (*AuctionResponse, error) { 235 if r == nil { 236 return nil, nil 237 } 238 239 err := r.HookExecutor.ExecuteProcessedAuctionStage(r.BidRequestWrapper) 240 if err != nil { 241 return nil, err 242 } 243 244 requestExt, err := r.BidRequestWrapper.GetRequestExt() 245 if err != nil { 246 return nil, err 247 } 248 249 // ensure prebid object always exists 250 requestExtPrebid := requestExt.GetPrebid() 251 if requestExtPrebid == nil { 252 requestExtPrebid = &openrtb_ext.ExtRequestPrebid{} 253 } 254 255 if !e.server.Empty() { 256 requestExtPrebid.Server = &openrtb_ext.ExtRequestPrebidServer{ 257 ExternalUrl: e.server.ExternalUrl, 258 GvlID: e.server.GvlID, 259 DataCenter: e.server.DataCenter} 260 requestExt.SetPrebid(requestExtPrebid) 261 } 262 263 cacheInstructions := getExtCacheInstructions(requestExtPrebid) 264 265 targData := getExtTargetData(requestExtPrebid, cacheInstructions) 266 if targData != nil { 267 _, targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData() 268 } 269 270 // Get currency rates conversions for the auction 271 conversions := currency.GetAuctionCurrencyRates(e.currencyConverter, requestExtPrebid.CurrencyConversions) 272 273 var floorErrs []error 274 if e.priceFloorEnabled { 275 floorErrs = floors.EnrichWithPriceFloors(r.BidRequestWrapper, r.Account, conversions, e.priceFloorFetcher) 276 } 277 278 responseDebugAllow, accountDebugAllow, debugLog := getDebugInfo(r.BidRequestWrapper.Test, requestExtPrebid, r.Account.DebugAllow, debugLog) 279 280 // save incoming request with stored requests (if applicable) to return in debug logs 281 if responseDebugAllow || len(requestExtPrebid.AdServerTargeting) > 0 { 282 if err := r.BidRequestWrapper.RebuildRequest(); err != nil { 283 return nil, err 284 } 285 resolvedBidReq, err := jsonutil.Marshal(r.BidRequestWrapper.BidRequest) 286 if err != nil { 287 return nil, err 288 } 289 r.ResolvedBidRequest = resolvedBidReq 290 } 291 e.me.RecordDebugRequest(responseDebugAllow || accountDebugAllow, r.PubID) 292 293 if r.RequestType == metrics.ReqTypeORTB2Web || 294 r.RequestType == metrics.ReqTypeORTB2App || 295 r.RequestType == metrics.ReqTypeAMP { 296 //Extract First party data for auction endpoint only 297 resolvedFPD, fpdErrors := firstpartydata.ExtractFPDForBidders(r.BidRequestWrapper) 298 if len(fpdErrors) > 0 { 299 var errMessages []string 300 for _, fpdError := range fpdErrors { 301 errMessages = append(errMessages, fpdError.Error()) 302 } 303 return nil, &errortypes.BadInput{ 304 Message: strings.Join(errMessages, ","), 305 } 306 } 307 r.FirstPartyData = resolvedFPD 308 } 309 310 bidAdjustmentFactors := getExtBidAdjustmentFactors(requestExtPrebid) 311 312 recordImpMetrics(r.BidRequestWrapper, e.me) 313 314 // Make our best guess if GDPR applies 315 gdprDefaultValue := e.parseGDPRDefaultValue(r.BidRequestWrapper) 316 gdprSignal, err := getGDPR(r.BidRequestWrapper) 317 if err != nil { 318 return nil, err 319 } 320 channelEnabled := r.TCF2Config.ChannelEnabled(channelTypeMap[r.LegacyLabels.RType]) 321 gdprEnforced := enforceGDPR(gdprSignal, gdprDefaultValue, channelEnabled) 322 dsaWriter := dsa.Writer{ 323 Config: r.Account.Privacy.DSA, 324 GDPRInScope: gdprEnforced, 325 } 326 if err := dsaWriter.Write(r.BidRequestWrapper); err != nil { 327 return nil, err 328 } 329 330 // rebuild/resync the request in the request wrapper. 331 if err := r.BidRequestWrapper.RebuildRequest(); err != nil { 332 return nil, err 333 } 334 335 // Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder 336 requestExtLegacy := &openrtb_ext.ExtRequest{ 337 Prebid: *requestExtPrebid, 338 SChain: requestExt.GetSChain(), 339 } 340 bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprSignal, gdprEnforced, bidAdjustmentFactors) 341 errs = append(errs, floorErrs...) 342 343 mergedBidAdj, err := bidadjustment.Merge(r.BidRequestWrapper, r.Account.BidAdjustments) 344 if err != nil { 345 if errortypes.ContainsFatalError([]error{err}) { 346 return nil, err 347 } 348 errs = append(errs, err) 349 } 350 bidAdjustmentRules := bidadjustment.BuildRules(mergedBidAdj) 351 352 e.me.RecordRequestPrivacy(privacyLabels) 353 354 if len(r.StoredAuctionResponses) > 0 || len(r.StoredBidResponses) > 0 { 355 e.me.RecordStoredResponse(r.PubID) 356 } 357 358 // If we need to cache bids, then it will take some time to call prebid cache. 359 // We should reduce the amount of time the bidders have, to compensate. 360 auctionCtx, cancel := e.makeAuctionContext(ctx, cacheInstructions.cacheBids) 361 defer cancel() 362 363 var ( 364 adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid 365 adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra 366 fledge *openrtb_ext.Fledge 367 anyBidsReturned bool 368 // List of bidders we have requests for. 369 liveAdapters []openrtb_ext.BidderName 370 ) 371 372 if len(r.StoredAuctionResponses) > 0 { 373 adapterBids, fledge, liveAdapters, err = buildStoredAuctionResponse(r.StoredAuctionResponses) 374 if err != nil { 375 return nil, err 376 } 377 anyBidsReturned = true 378 379 } else { 380 // List of bidders we have requests for. 381 liveAdapters = listBiddersWithRequests(bidderRequests) 382 383 //This will be used to validate bids 384 var alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes 385 if requestExtPrebid.AlternateBidderCodes != nil { 386 alternateBidderCodes = *requestExtPrebid.AlternateBidderCodes 387 } else if r.Account.AlternateBidderCodes != nil { 388 alternateBidderCodes = *r.Account.AlternateBidderCodes 389 } 390 var extraRespInfo extraAuctionResponseInfo 391 adapterBids, adapterExtra, extraRespInfo = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime, bidAdjustmentRules, r.TmaxAdjustments, responseDebugAllow) 392 fledge = extraRespInfo.fledge 393 anyBidsReturned = extraRespInfo.bidsFound 394 r.BidderResponseStartTime = extraRespInfo.bidderResponseStartTime 395 } 396 397 var ( 398 auc *auction 399 cacheErrs []error 400 bidResponseExt *openrtb_ext.ExtBidResponse 401 seatNonBids = nonBids{} 402 ) 403 404 if anyBidsReturned { 405 if e.priceFloorEnabled { 406 var rejectedBids []*entities.PbsOrtbSeatBid 407 var enforceErrs []error 408 409 adapterBids, enforceErrs, rejectedBids = floors.Enforce(r.BidRequestWrapper, adapterBids, r.Account, conversions) 410 errs = append(errs, enforceErrs...) 411 for _, rejectedBid := range rejectedBids { 412 errs = append(errs, &errortypes.Warning{ 413 Message: fmt.Sprintf("%s bid id %s rejected - bid price %.4f %s is less than bid floor %.4f %s for imp %s", rejectedBid.Seat, rejectedBid.Bids[0].Bid.ID, rejectedBid.Bids[0].Bid.Price, rejectedBid.Currency, rejectedBid.Bids[0].BidFloors.FloorValue, rejectedBid.Bids[0].BidFloors.FloorCurrency, rejectedBid.Bids[0].Bid.ImpID), 414 WarningCode: errortypes.FloorBidRejectionWarningCode}) 415 rejectionReason := ResponseRejectedBelowFloor 416 if rejectedBid.Bids[0].Bid.DealID != "" { 417 rejectionReason = ResponseRejectedBelowDealFloor 418 } 419 seatNonBids.addBid(rejectedBid.Bids[0], int(rejectionReason), rejectedBid.Seat) 420 } 421 } 422 423 var bidCategory map[string]string 424 //If includebrandcategory is present in ext then CE feature is on. 425 if requestExtPrebid.Targeting != nil && requestExtPrebid.Targeting.IncludeBrandCategory != nil { 426 var rejections []string 427 bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, *requestExtPrebid.Targeting, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{}, &seatNonBids) 428 if err != nil { 429 return nil, fmt.Errorf("Error in category mapping : %s", err.Error()) 430 } 431 for _, message := range rejections { 432 errs = append(errs, errors.New(message)) 433 } 434 } 435 436 if e.bidIDGenerator.Enabled() { 437 for bidder, seatBid := range adapterBids { 438 for i := range seatBid.Bids { 439 if bidID, err := e.bidIDGenerator.New(bidder.String()); err == nil { 440 seatBid.Bids[i].GeneratedBidID = bidID 441 } else { 442 errs = append(errs, errors.New("Error generating bid.ext.prebid.bidid")) 443 } 444 } 445 } 446 } 447 448 evTracking := getEventTracking(requestExtPrebid, r.StartTime, &r.Account, e.bidderInfo, e.externalURL) 449 adapterBids = evTracking.modifyBidsForEvents(adapterBids) 450 451 r.HookExecutor.ExecuteAllProcessedBidResponsesStage(adapterBids) 452 453 if targData != nil { 454 multiBidMap := buildMultiBidMap(requestExtPrebid) 455 456 // A non-nil auction is only needed if targeting is active. (It is used below this block to extract cache keys) 457 auc = newAuction(adapterBids, len(r.BidRequestWrapper.Imp), targData.preferDeals) 458 auc.validateAndUpdateMultiBid(adapterBids, targData.preferDeals, r.Account.DefaultBidLimit) 459 auc.setRoundedPrices(*targData) 460 461 if requestExtPrebid.SupportDeals { 462 dealErrs := applyDealSupport(r.BidRequestWrapper.BidRequest, auc, bidCategory, multiBidMap) 463 errs = append(errs, dealErrs...) 464 } 465 466 bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) 467 if debugLog.DebugEnabledOrOverridden { 468 if bidRespExtBytes, err := jsonutil.Marshal(bidResponseExt); err == nil { 469 debugLog.Data.Response = string(bidRespExtBytes) 470 } else { 471 debugLog.Data.Response = "Unable to marshal response ext for debugging" 472 errs = append(errs, err) 473 } 474 } 475 476 cacheErrs = auc.doCache(ctx, e.cache, targData, evTracking, r.BidRequestWrapper.BidRequest, 60, &r.Account.CacheTTL, bidCategory, debugLog) 477 if len(cacheErrs) > 0 { 478 errs = append(errs, cacheErrs...) 479 } 480 481 if targData.includeWinners || targData.includeBidderKeys || targData.includeFormat { 482 targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, multiBidMap) 483 } 484 } 485 bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) 486 } else { 487 bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) 488 489 if debugLog.DebugEnabledOrOverridden { 490 491 if bidRespExtBytes, err := jsonutil.Marshal(bidResponseExt); err == nil { 492 debugLog.Data.Response = string(bidRespExtBytes) 493 } else { 494 debugLog.Data.Response = "Unable to marshal response ext for debugging" 495 errs = append(errs, err) 496 } 497 } 498 } 499 500 if !accountDebugAllow && !debugLog.DebugOverride { 501 accountDebugDisabledWarning := openrtb_ext.ExtBidderMessage{ 502 Code: errortypes.AccountLevelDebugDisabledWarningCode, 503 Message: "debug turned off for account", 504 } 505 bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral] = append(bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral], accountDebugDisabledWarning) 506 } 507 508 for _, warning := range r.Warnings { 509 if errortypes.ReadScope(warning) == errortypes.ScopeDebug && !responseDebugAllow { 510 continue 511 } 512 generalWarning := openrtb_ext.ExtBidderMessage{ 513 Code: errortypes.ReadCode(warning), 514 Message: warning.Error(), 515 } 516 bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral] = append(bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral], generalWarning) 517 } 518 519 e.bidValidationEnforcement.SetBannerCreativeMaxSize(r.Account.Validations) 520 521 // Build the response 522 bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBids) 523 bidResponse = adservertargeting.Apply(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute) 524 525 bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt) 526 if err != nil { 527 return nil, err 528 } 529 bidResponseExt = setSeatNonBid(bidResponseExt, seatNonBids) 530 531 return &AuctionResponse{ 532 BidResponse: bidResponse, 533 ExtBidResponse: bidResponseExt, 534 }, nil 535 } 536 537 func buildMultiBidMap(prebid *openrtb_ext.ExtRequestPrebid) map[string]openrtb_ext.ExtMultiBid { 538 if prebid == nil || prebid.MultiBid == nil { 539 return nil 540 } 541 542 // validation already done in validateRequestExt(), directly build a map here for downstream processing 543 multiBidMap := make(map[string]openrtb_ext.ExtMultiBid) 544 for _, multiBid := range prebid.MultiBid { 545 if multiBid.Bidder != "" { 546 if bidderNormalized, bidderFound := openrtb_ext.NormalizeBidderName(multiBid.Bidder); bidderFound { 547 multiBidMap[string(bidderNormalized)] = *multiBid 548 } 549 } else { 550 for _, bidder := range multiBid.Bidders { 551 if bidderNormalized, bidderFound := openrtb_ext.NormalizeBidderName(bidder); bidderFound { 552 multiBidMap[string(bidderNormalized)] = *multiBid 553 } 554 } 555 } 556 } 557 558 return multiBidMap 559 } 560 561 func (e *exchange) parseGDPRDefaultValue(r *openrtb_ext.RequestWrapper) gdpr.Signal { 562 gdprDefaultValue := e.gdprDefaultValue 563 564 var geo *openrtb2.Geo 565 if r.User != nil && r.User.Geo != nil { 566 geo = r.User.Geo 567 } else if r.Device != nil && r.Device.Geo != nil { 568 geo = r.Device.Geo 569 } 570 571 if geo != nil { 572 // If we have a country set, and it is on the list, we assume GDPR applies if not set on the request. 573 // Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long). 574 if _, found := e.privacyConfig.GDPR.EEACountriesMap[strings.ToUpper(geo.Country)]; found { 575 gdprDefaultValue = gdpr.SignalYes 576 } else if len(geo.Country) == 3 { 577 // The country field is formatted properly as a three character country code 578 gdprDefaultValue = gdpr.SignalNo 579 } 580 } 581 582 return gdprDefaultValue 583 } 584 585 func recordImpMetrics(r *openrtb_ext.RequestWrapper, metricsEngine metrics.MetricsEngine) { 586 for _, impInRequest := range r.GetImp() { 587 var impLabels metrics.ImpLabels = metrics.ImpLabels{ 588 BannerImps: impInRequest.Banner != nil, 589 VideoImps: impInRequest.Video != nil, 590 AudioImps: impInRequest.Audio != nil, 591 NativeImps: impInRequest.Native != nil, 592 } 593 metricsEngine.RecordImps(impLabels) 594 } 595 } 596 597 // applyDealSupport updates targeting keys with deal prefixes if minimum deal tier exceeded 598 func applyDealSupport(bidRequest *openrtb2.BidRequest, auc *auction, bidCategory map[string]string, multiBid map[string]openrtb_ext.ExtMultiBid) []error { 599 errs := []error{} 600 impDealMap := getDealTiers(bidRequest) 601 602 for impID, topBidsPerImp := range auc.allBidsByBidder { 603 impDeal := impDealMap[impID] 604 for bidder, topBidsPerBidder := range topBidsPerImp { 605 bidderNormalized, bidderFound := openrtb_ext.NormalizeBidderName(bidder.String()) 606 if !bidderFound { 607 bidderNormalized = bidder 608 } 609 610 maxBid := bidsToUpdate(multiBid, bidderNormalized.String()) 611 for i, topBid := range topBidsPerBidder { 612 if i == maxBid { 613 break 614 } 615 if topBid.DealPriority > 0 { 616 if validateDealTier(impDeal[bidderNormalized]) { 617 updateHbPbCatDur(topBid, impDeal[bidderNormalized], bidCategory) 618 } else { 619 errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", string(bidder), impID)) 620 } 621 } 622 } 623 } 624 } 625 626 return errs 627 } 628 629 // By default, update 1 bid, 630 // For 2nd and the following bids, updateHbPbCatDur only if this bidder's multibid config is fully defined. 631 func bidsToUpdate(multiBid map[string]openrtb_ext.ExtMultiBid, bidder string) int { 632 if multiBid != nil { 633 if bidderMultiBid, ok := multiBid[bidder]; ok && bidderMultiBid.TargetBidderCodePrefix != "" { 634 return *bidderMultiBid.MaxBids 635 } 636 } 637 638 return openrtb_ext.DefaultBidLimit 639 } 640 641 // getDealTiers creates map of impression to bidder deal tier configuration 642 func getDealTiers(bidRequest *openrtb2.BidRequest) map[string]openrtb_ext.DealTierBidderMap { 643 impDealMap := make(map[string]openrtb_ext.DealTierBidderMap) 644 645 for _, imp := range bidRequest.Imp { 646 dealTierBidderMap, err := openrtb_ext.ReadDealTiersFromImp(imp) 647 if err != nil { 648 continue 649 } 650 impDealMap[imp.ID] = dealTierBidderMap 651 } 652 653 return impDealMap 654 } 655 656 func validateDealTier(dealTier openrtb_ext.DealTier) bool { 657 return len(dealTier.Prefix) > 0 && dealTier.MinDealTier > 0 658 } 659 660 func updateHbPbCatDur(bid *entities.PbsOrtbBid, dealTier openrtb_ext.DealTier, bidCategory map[string]string) { 661 if bid.DealPriority >= dealTier.MinDealTier { 662 prefixTier := fmt.Sprintf("%s%d_", dealTier.Prefix, bid.DealPriority) 663 bid.DealTierSatisfied = true 664 665 if oldCatDur, ok := bidCategory[bid.Bid.ID]; ok { 666 oldCatDurSplit := strings.SplitAfterN(oldCatDur, "_", 2) 667 oldCatDurSplit[0] = prefixTier 668 669 newCatDur := strings.Join(oldCatDurSplit, "") 670 bidCategory[bid.Bid.ID] = newCatDur 671 } 672 } 673 } 674 675 func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auctionCtx context.Context, cancel context.CancelFunc) { 676 auctionCtx = ctx 677 cancel = func() {} 678 if needsCache { 679 if deadline, ok := ctx.Deadline(); ok { 680 auctionCtx, cancel = context.WithDeadline(ctx, deadline.Add(-e.cacheTime)) 681 } 682 } 683 return 684 } 685 686 // This piece sends all the requests to the bidder adapters and gathers the results. 687 func (e *exchange) getAllBids( 688 ctx context.Context, 689 bidderRequests []BidderRequest, 690 bidAdjustments map[string]float64, 691 conversions currency.Conversions, 692 accountDebugAllowed bool, 693 globalPrivacyControlHeader string, 694 headerDebugAllowed bool, 695 alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, 696 experiment *openrtb_ext.Experiment, 697 hookExecutor hookexecution.StageExecutor, 698 pbsRequestStartTime time.Time, 699 bidAdjustmentRules map[string][]openrtb_ext.Adjustment, 700 tmaxAdjustments *TmaxAdjustmentsPreprocessed, 701 responseDebugAllowed bool) ( 702 map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, 703 map[openrtb_ext.BidderName]*seatResponseExtra, 704 extraAuctionResponseInfo) { 705 // Set up pointers to the bid results 706 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, len(bidderRequests)) 707 adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests)) 708 chBids := make(chan *bidResponseWrapper, len(bidderRequests)) 709 extraRespInfo := extraAuctionResponseInfo{} 710 711 e.me.RecordOverheadTime(metrics.MakeBidderRequests, time.Since(pbsRequestStartTime)) 712 713 for _, bidder := range bidderRequests { 714 // Here we actually call the adapters and collect the bids. 715 bidderRunner := e.recoverSafely(bidderRequests, func(bidderRequest BidderRequest, conversions currency.Conversions) { 716 // Passing in aName so a doesn't change out from under the go routine 717 if bidderRequest.BidderLabels.Adapter == "" { 718 glog.Errorf("Exchange: bidlables for %s (%s) missing adapter string", bidderRequest.BidderName, bidderRequest.BidderCoreName) 719 bidderRequest.BidderLabels.Adapter = bidderRequest.BidderCoreName 720 } 721 brw := new(bidResponseWrapper) 722 brw.bidder = bidderRequest.BidderName 723 brw.adapter = bidderRequest.BidderCoreName 724 // Defer basic metrics to insure we capture them after all the values have been set 725 defer func() { 726 e.me.RecordAdapterRequest(bidderRequest.BidderLabels) 727 }() 728 start := time.Now() 729 730 reqInfo := adapters.NewExtraRequestInfo(conversions) 731 reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType 732 reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader 733 734 bidReqOptions := bidRequestOptions{ 735 accountDebugAllowed: accountDebugAllowed, 736 headerDebugAllowed: headerDebugAllowed, 737 addCallSignHeader: isAdsCertEnabled(experiment, e.bidderInfo[string(bidderRequest.BidderName)]), 738 bidAdjustments: bidAdjustments, 739 tmaxAdjustments: tmaxAdjustments, 740 bidderRequestStartTime: start, 741 responseDebugAllowed: responseDebugAllowed, 742 } 743 seatBids, extraBidderRespInfo, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest, conversions, &reqInfo, e.adsCertSigner, bidReqOptions, alternateBidderCodes, hookExecutor, bidAdjustmentRules) 744 brw.bidderResponseStartTime = extraBidderRespInfo.respProcessingStartTime 745 746 // Add in time reporting 747 elapsed := time.Since(start) 748 brw.adapterSeatBids = seatBids 749 // Structure to record extra tracking data generated during bidding 750 ae := new(seatResponseExtra) 751 ae.ResponseTimeMillis = int(elapsed / time.Millisecond) 752 if len(seatBids) != 0 { 753 ae.HttpCalls = seatBids[0].HttpCalls 754 } 755 // Timing statistics 756 e.me.RecordAdapterTime(bidderRequest.BidderLabels, elapsed) 757 bidderRequest.BidderLabels.AdapterBids = bidsToMetric(brw.adapterSeatBids) 758 bidderRequest.BidderLabels.AdapterErrors = errorsToMetric(err) 759 // Append any bid validation errors to the error list 760 ae.Errors = errsToBidderErrors(err) 761 ae.Warnings = errsToBidderWarnings(err) 762 brw.adapterExtra = ae 763 for _, seatBid := range seatBids { 764 if seatBid != nil { 765 for _, bid := range seatBid.Bids { 766 var cpm = float64(bid.Bid.Price * 1000) 767 e.me.RecordAdapterPrice(bidderRequest.BidderLabels, cpm) 768 e.me.RecordAdapterBidReceived(bidderRequest.BidderLabels, bid.BidType, bid.Bid.AdM != "") 769 } 770 } 771 } 772 chBids <- brw 773 }, chBids) 774 go bidderRunner(bidder, conversions) 775 } 776 777 // Wait for the bidders to do their thing 778 for i := 0; i < len(bidderRequests); i++ { 779 brw := <-chBids 780 if !brw.bidderResponseStartTime.IsZero() { 781 extraRespInfo.bidderResponseStartTime = brw.bidderResponseStartTime 782 } 783 //if bidder returned no bids back - remove bidder from further processing 784 for _, seatBid := range brw.adapterSeatBids { 785 if seatBid != nil { 786 bidderName := openrtb_ext.BidderName(seatBid.Seat) 787 if len(seatBid.Bids) != 0 { 788 if val, ok := adapterBids[bidderName]; ok { 789 adapterBids[bidderName].Bids = append(val.Bids, seatBid.Bids...) 790 } else { 791 adapterBids[bidderName] = seatBid 792 } 793 extraRespInfo.bidsFound = true 794 } 795 // collect fledgeAuctionConfigs separately from bids, as empty seatBids may be discarded 796 extraRespInfo.fledge = collectFledgeFromSeatBid(extraRespInfo.fledge, bidderName, brw.adapter, seatBid) 797 } 798 } 799 //but we need to add all bidders data to adapterExtra to have metrics and other metadata 800 adapterExtra[brw.bidder] = brw.adapterExtra 801 } 802 803 return adapterBids, adapterExtra, extraRespInfo 804 } 805 806 func collectFledgeFromSeatBid(fledge *openrtb_ext.Fledge, bidderName openrtb_ext.BidderName, adapterName openrtb_ext.BidderName, seatBid *entities.PbsOrtbSeatBid) *openrtb_ext.Fledge { 807 if seatBid.FledgeAuctionConfigs != nil { 808 if fledge == nil { 809 fledge = &openrtb_ext.Fledge{ 810 AuctionConfigs: make([]*openrtb_ext.FledgeAuctionConfig, 0, len(seatBid.FledgeAuctionConfigs)), 811 } 812 } 813 for _, config := range seatBid.FledgeAuctionConfigs { 814 fledge.AuctionConfigs = append(fledge.AuctionConfigs, &openrtb_ext.FledgeAuctionConfig{ 815 Bidder: bidderName.String(), 816 Adapter: config.Bidder, 817 ImpId: config.ImpId, 818 Config: config.Config, 819 }) 820 } 821 } 822 return fledge 823 } 824 825 func (e *exchange) recoverSafely(bidderRequests []BidderRequest, 826 inner func(BidderRequest, currency.Conversions), 827 chBids chan *bidResponseWrapper) func(BidderRequest, currency.Conversions) { 828 return func(bidderRequest BidderRequest, conversions currency.Conversions) { 829 defer func() { 830 if r := recover(); r != nil { 831 832 allBidders := "" 833 sb := strings.Builder{} 834 for _, bidder := range bidderRequests { 835 sb.WriteString(bidder.BidderName.String()) 836 sb.WriteString(",") 837 } 838 if sb.Len() > 0 { 839 allBidders = sb.String()[:sb.Len()-1] 840 } 841 842 glog.Errorf("OpenRTB auction recovered panic from Bidder %s: %v. "+ 843 "Account id: %s, All Bidders: %s, Stack trace is: %v", 844 bidderRequest.BidderCoreName, r, bidderRequest.BidderLabels.PubID, allBidders, string(debug.Stack())) 845 e.me.RecordAdapterPanic(bidderRequest.BidderLabels) 846 // Let the master request know that there is no data here 847 brw := new(bidResponseWrapper) 848 brw.adapterExtra = new(seatResponseExtra) 849 chBids <- brw 850 } 851 }() 852 inner(bidderRequest, conversions) 853 } 854 } 855 856 func bidsToMetric(seatBids []*entities.PbsOrtbSeatBid) metrics.AdapterBid { 857 for _, seatBid := range seatBids { 858 if seatBid != nil && len(seatBid.Bids) != 0 { 859 return metrics.AdapterBidPresent 860 } 861 } 862 return metrics.AdapterBidNone 863 } 864 865 func errorsToMetric(errs []error) map[metrics.AdapterError]struct{} { 866 if len(errs) == 0 { 867 return nil 868 } 869 ret := make(map[metrics.AdapterError]struct{}, len(errs)) 870 var s struct{} 871 for _, err := range errs { 872 switch errortypes.ReadCode(err) { 873 case errortypes.TimeoutErrorCode: 874 ret[metrics.AdapterErrorTimeout] = s 875 case errortypes.BadInputErrorCode: 876 ret[metrics.AdapterErrorBadInput] = s 877 case errortypes.BadServerResponseErrorCode: 878 ret[metrics.AdapterErrorBadServerResponse] = s 879 case errortypes.FailedToRequestBidsErrorCode: 880 ret[metrics.AdapterErrorFailedToRequestBids] = s 881 case errortypes.AlternateBidderCodeWarningCode: 882 ret[metrics.AdapterErrorValidation] = s 883 case errortypes.TmaxTimeoutErrorCode: 884 ret[metrics.AdapterErrorTmaxTimeout] = s 885 default: 886 ret[metrics.AdapterErrorUnknown] = s 887 } 888 } 889 return ret 890 } 891 892 func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderMessage { 893 sErr := make([]openrtb_ext.ExtBidderMessage, 0) 894 for _, err := range errortypes.FatalOnly(errs) { 895 newErr := openrtb_ext.ExtBidderMessage{ 896 Code: errortypes.ReadCode(err), 897 Message: err.Error(), 898 } 899 sErr = append(sErr, newErr) 900 } 901 902 return sErr 903 } 904 905 func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { 906 sWarn := make([]openrtb_ext.ExtBidderMessage, 0) 907 for _, warn := range errortypes.WarningOnly(errs) { 908 newErr := openrtb_ext.ExtBidderMessage{ 909 Code: errortypes.ReadCode(warn), 910 Message: warn.Error(), 911 } 912 sWarn = append(sWarn, newErr) 913 } 914 return sWarn 915 } 916 917 // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester 918 func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBids *nonBids) *openrtb2.BidResponse { 919 bidResponse := new(openrtb2.BidResponse) 920 921 bidResponse.ID = bidRequest.ID 922 if len(liveAdapters) == 0 { 923 // signal "Invalid Request" if no valid bidders. 924 bidResponse.NBR = openrtb3.NoBidInvalidRequest.Ptr() 925 } 926 927 // Create the SeatBids. We use a zero sized slice so that we can append non-zero seat bids, and not include seatBid 928 // objects for seatBids without any bids. Preallocate the max possible size to avoid reallocating the array as we go. 929 seatBids := make([]openrtb2.SeatBid, 0, len(liveAdapters)) 930 for a, adapterSeatBids := range adapterSeatBids { 931 //while processing every single bib, do we need to handle categories here? 932 if adapterSeatBids != nil && len(adapterSeatBids.Bids) > 0 { 933 sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, pubID, seatNonBids) 934 seatBids = append(seatBids, *sb) 935 bidResponse.Cur = adapterSeatBids.Currency 936 } 937 } 938 bidResponse.SeatBid = seatBids 939 940 return bidResponse 941 } 942 943 func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, error) { 944 buffer := &bytes.Buffer{} 945 enc := json.NewEncoder(buffer) 946 947 enc.SetEscapeHTML(false) 948 err := enc.Encode(bidResponseExt) 949 950 return buffer.Bytes(), err 951 } 952 953 func applyCategoryMapping(ctx context.Context, targeting openrtb_ext.ExtRequestTargeting, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator, seatNonBids *nonBids) (map[string]string, map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []string, error) { 954 res := make(map[string]string) 955 956 type bidDedupe struct { 957 bidderName openrtb_ext.BidderName 958 bidIndex int 959 bidID string 960 bidPrice string 961 } 962 963 dedupe := make(map[string]bidDedupe) 964 965 // applyCategoryMapping doesn't get called unless 966 brandCatExt := targeting.IncludeBrandCategory 967 968 //If ext.prebid.targeting.includebrandcategory is present in ext then competitive exclusion feature is on. 969 var includeBrandCategory = brandCatExt != nil //if not present - category will no be appended 970 appendBidderNames := targeting.AppendBidderNames 971 972 var primaryAdServer string 973 var publisher string 974 var err error 975 var rejections []string 976 var translateCategories = true 977 978 if includeBrandCategory && brandCatExt.WithCategory { 979 if brandCatExt.TranslateCategories != nil { 980 translateCategories = *brandCatExt.TranslateCategories 981 } 982 //if translateCategories is set to false, ignore checking primaryAdServer and publisher 983 if translateCategories { 984 //if ext.prebid.targeting.includebrandcategory present but primaryadserver/publisher not present then error out the request right away. 985 primaryAdServer, err = getPrimaryAdServer(brandCatExt.PrimaryAdServer) //1-Freewheel 2-DFP 986 if err != nil { 987 return res, seatBids, rejections, err 988 } 989 publisher = brandCatExt.Publisher 990 } 991 } 992 993 seatBidsToRemove := make([]openrtb_ext.BidderName, 0) 994 995 for bidderName, seatBid := range seatBids { 996 bidsToRemove := make([]int, 0) 997 for bidInd := range seatBid.Bids { 998 bid := seatBid.Bids[bidInd] 999 bidID := bid.Bid.ID 1000 var duration int 1001 var category string 1002 var priceBucket string 1003 1004 if bid.BidVideo != nil { 1005 duration = bid.BidVideo.Duration 1006 category = bid.BidVideo.PrimaryCategory 1007 } 1008 if brandCatExt.WithCategory && category == "" { 1009 bidIabCat := bid.Bid.Cat 1010 if len(bidIabCat) != 1 { 1011 //TODO: add metrics 1012 //on receiving bids from adapters if no unique IAB category is returned or if no ad server category is returned discard the bid 1013 bidsToRemove = append(bidsToRemove, bidInd) 1014 rejections = updateRejections(rejections, bidID, "Bid did not contain a category") 1015 seatNonBids.addBid(bid, int(ResponseRejectedCategoryMappingInvalid), string(bidderName)) 1016 continue 1017 } 1018 if translateCategories { 1019 //if unique IAB category is present then translate it to the adserver category based on mapping file 1020 category, err = categoriesFetcher.FetchCategories(ctx, primaryAdServer, publisher, bidIabCat[0]) 1021 if err != nil || category == "" { 1022 //TODO: add metrics 1023 //if mapping required but no mapping file is found then discard the bid 1024 bidsToRemove = append(bidsToRemove, bidInd) 1025 reason := fmt.Sprintf("Category mapping file for primary ad server: '%s', publisher: '%s' not found", primaryAdServer, publisher) 1026 rejections = updateRejections(rejections, bidID, reason) 1027 continue 1028 } 1029 } else { 1030 //category translation is disabled, continue with IAB category 1031 category = bidIabCat[0] 1032 } 1033 } 1034 1035 // TODO: consider should we remove bids with zero duration here? 1036 1037 priceBucket = GetPriceBucket(*bid.Bid, *targData) 1038 1039 newDur, err := findDurationRange(duration, targeting.DurationRangeSec) 1040 if err != nil { 1041 bidsToRemove = append(bidsToRemove, bidInd) 1042 rejections = updateRejections(rejections, bidID, err.Error()) 1043 continue 1044 } 1045 1046 var categoryDuration string 1047 var dupeKey string 1048 if brandCatExt.WithCategory { 1049 categoryDuration = fmt.Sprintf("%s_%s_%ds", priceBucket, category, newDur) 1050 dupeKey = category 1051 } else { 1052 categoryDuration = fmt.Sprintf("%s_%ds", priceBucket, newDur) 1053 dupeKey = categoryDuration 1054 } 1055 1056 if appendBidderNames { 1057 categoryDuration = fmt.Sprintf("%s_%s", categoryDuration, bidderName.String()) 1058 } 1059 1060 if dupe, ok := dedupe[dupeKey]; ok { 1061 1062 dupeBidPrice, err := strconv.ParseFloat(dupe.bidPrice, 64) 1063 if err != nil { 1064 dupeBidPrice = 0 1065 } 1066 currBidPrice, err := strconv.ParseFloat(priceBucket, 64) 1067 if err != nil { 1068 currBidPrice = 0 1069 } 1070 if dupeBidPrice == currBidPrice { 1071 if booleanGenerator.Generate() { 1072 dupeBidPrice = -1 1073 } else { 1074 currBidPrice = -1 1075 } 1076 } 1077 1078 if dupeBidPrice < currBidPrice { 1079 if dupe.bidderName == bidderName { 1080 // An older bid from the current bidder 1081 bidsToRemove = append(bidsToRemove, dupe.bidIndex) 1082 rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") 1083 } else { 1084 // An older bid from a different seatBid we've already finished with 1085 oldSeatBid := (seatBids)[dupe.bidderName] 1086 rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated") 1087 if len(oldSeatBid.Bids) == 1 { 1088 seatBidsToRemove = append(seatBidsToRemove, dupe.bidderName) 1089 } else { 1090 // This is a very rare, but still possible case where bid needs to be removed from already processed bidder 1091 // This happens when current processing bidder has a bid that has same deduplication key as a bid from already processed bidder 1092 // and already processed bid was selected to be removed 1093 // See example of input data in unit test `TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice` 1094 // Need to remove bid by name, not index in this case 1095 removeBidById(oldSeatBid, dupe.bidID) 1096 } 1097 } 1098 delete(res, dupe.bidID) 1099 } else { 1100 // Remove this bid 1101 bidsToRemove = append(bidsToRemove, bidInd) 1102 rejections = updateRejections(rejections, bidID, "Bid was deduplicated") 1103 continue 1104 } 1105 } 1106 res[bidID] = categoryDuration 1107 dedupe[dupeKey] = bidDedupe{bidderName: bidderName, bidIndex: bidInd, bidID: bidID, bidPrice: priceBucket} 1108 } 1109 1110 if len(bidsToRemove) > 0 { 1111 sort.Ints(bidsToRemove) 1112 if len(bidsToRemove) == len(seatBid.Bids) { 1113 //if all bids are invalid - remove entire seat bid 1114 seatBidsToRemove = append(seatBidsToRemove, bidderName) 1115 } else { 1116 bids := seatBid.Bids 1117 for i := len(bidsToRemove) - 1; i >= 0; i-- { 1118 remInd := bidsToRemove[i] 1119 bids = append(bids[:remInd], bids[remInd+1:]...) 1120 } 1121 seatBid.Bids = bids 1122 } 1123 } 1124 1125 } 1126 for _, seatBidInd := range seatBidsToRemove { 1127 seatBids[seatBidInd].Bids = nil 1128 } 1129 1130 return res, seatBids, rejections, nil 1131 } 1132 1133 // findDurationRange returns the element in the array 'durationRanges' that is both greater than 'dur' and closest 1134 // in value to 'dur' unless a value equal to 'dur' is found. Returns an error if all elements in 'durationRanges' 1135 // are less than 'dur'. 1136 func findDurationRange(dur int, durationRanges []int) (int, error) { 1137 newDur := dur 1138 madeSelection := false 1139 var err error 1140 1141 for i := range durationRanges { 1142 if dur > durationRanges[i] { 1143 continue 1144 } 1145 if dur == durationRanges[i] { 1146 return durationRanges[i], nil 1147 } 1148 // dur < durationRanges[i] 1149 if durationRanges[i] < newDur || !madeSelection { 1150 newDur = durationRanges[i] 1151 madeSelection = true 1152 } 1153 } 1154 if !madeSelection && len(durationRanges) > 0 { 1155 err = errors.New("bid duration exceeds maximum allowed") 1156 } 1157 return newDur, err 1158 } 1159 1160 func removeBidById(seatBid *entities.PbsOrtbSeatBid, bidID string) { 1161 //Find index of bid to remove 1162 dupeBidIndex := -1 1163 for i, bid := range seatBid.Bids { 1164 if bid.Bid.ID == bidID { 1165 dupeBidIndex = i 1166 break 1167 } 1168 } 1169 if dupeBidIndex != -1 { 1170 if dupeBidIndex < len(seatBid.Bids)-1 { 1171 seatBid.Bids = append(seatBid.Bids[:dupeBidIndex], seatBid.Bids[dupeBidIndex+1:]...) 1172 } else if dupeBidIndex == len(seatBid.Bids)-1 { 1173 seatBid.Bids = seatBid.Bids[:len(seatBid.Bids)-1] 1174 } 1175 } 1176 } 1177 1178 func updateRejections(rejections []string, bidID string, reason string) []string { 1179 message := fmt.Sprintf("bid rejected [bid ID: %s] reason: %s", bidID, reason) 1180 return append(rejections, message) 1181 } 1182 1183 func getPrimaryAdServer(adServerId int) (string, error) { 1184 switch adServerId { 1185 case 1: 1186 return "freewheel", nil 1187 case 2: 1188 return "dfp", nil 1189 default: 1190 return "", fmt.Errorf("Primary ad server %d not recognized", adServerId) 1191 } 1192 } 1193 1194 // Extract all the data from the SeatBids and build the ExtBidResponse 1195 func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, r AuctionRequest, debugInfo bool, passthrough json.RawMessage, fledge *openrtb_ext.Fledge, errList []error) *openrtb_ext.ExtBidResponse { 1196 bidResponseExt := &openrtb_ext.ExtBidResponse{ 1197 Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage, len(adapterBids)), 1198 Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage, len(adapterBids)), 1199 ResponseTimeMillis: make(map[openrtb_ext.BidderName]int, len(adapterBids)), 1200 RequestTimeoutMillis: r.BidRequestWrapper.BidRequest.TMax, 1201 } 1202 if debugInfo { 1203 bidResponseExt.Debug = &openrtb_ext.ExtResponseDebug{ 1204 HttpCalls: make(map[openrtb_ext.BidderName][]*openrtb_ext.ExtHttpCall), 1205 ResolvedRequest: r.ResolvedBidRequest, 1206 } 1207 } 1208 1209 var auctionTimestamp int64 1210 if !r.StartTime.IsZero() { 1211 auctionTimestamp = r.StartTime.UnixMilli() 1212 } 1213 1214 if auctionTimestamp > 0 || 1215 passthrough != nil || 1216 fledge != nil { 1217 bidResponseExt.Prebid = &openrtb_ext.ExtResponsePrebid{ 1218 AuctionTimestamp: auctionTimestamp, 1219 Passthrough: passthrough, 1220 Fledge: fledge, 1221 } 1222 } 1223 1224 for bidderName, responseExtra := range adapterExtra { 1225 1226 if debugInfo && len(responseExtra.HttpCalls) > 0 { 1227 bidResponseExt.Debug.HttpCalls[bidderName] = responseExtra.HttpCalls 1228 } 1229 if len(responseExtra.Warnings) > 0 { 1230 bidResponseExt.Warnings[bidderName] = responseExtra.Warnings 1231 } 1232 // Only make an entry for bidder errors if the bidder reported any. 1233 if len(responseExtra.Errors) > 0 { 1234 bidResponseExt.Errors[bidderName] = responseExtra.Errors 1235 } 1236 if len(errList) > 0 { 1237 bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = errsToBidderErrors(errList) 1238 if prebidWarn := errsToBidderWarnings(errList); len(prebidWarn) > 0 { 1239 bidResponseExt.Warnings[openrtb_ext.PrebidExtKey] = prebidWarn 1240 } 1241 } 1242 bidResponseExt.ResponseTimeMillis[bidderName] = responseExtra.ResponseTimeMillis 1243 // Defering the filling of bidResponseExt.Usersync[bidderName] until later 1244 1245 } 1246 1247 return bidResponseExt 1248 } 1249 1250 // Return an openrtb seatBid for a bidder 1251 // buildBidResponse is responsible for ensuring nil bid seatbids are not included 1252 func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBids *nonBids) *openrtb2.SeatBid { 1253 seatBid := &openrtb2.SeatBid{ 1254 Seat: adapter.String(), 1255 Group: 0, // Prebid cannot support roadblocking 1256 } 1257 1258 var errList []error 1259 seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, adapter, pubID, seatNonBids) 1260 if len(errList) > 0 { 1261 adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...) 1262 } 1263 1264 return seatBid 1265 } 1266 1267 func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBids *nonBids) ([]openrtb2.Bid, []error) { 1268 result := make([]openrtb2.Bid, 0, len(bids)) 1269 errs := make([]error, 0, 1) 1270 1271 for _, bid := range bids { 1272 if err := dsa.Validate(bidRequest, bid); err != nil { 1273 dsaMessage := openrtb_ext.ExtBidderMessage{ 1274 Code: errortypes.InvalidBidResponseDSAWarningCode, 1275 Message: fmt.Sprintf("bid rejected: %s", err.Error()), 1276 } 1277 bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], dsaMessage) 1278 1279 seatNonBids.addBid(bid, int(ResponseRejectedGeneral), adapter.String()) 1280 continue // Don't add bid to result 1281 } 1282 if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner { 1283 if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) { 1284 seatNonBids.addBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String()) 1285 continue // Don't add bid to result 1286 } 1287 } else if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationWarn && bid.BidType == openrtb_ext.BidTypeBanner { 1288 e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) 1289 } 1290 if _, ok := impExtInfoMap[bid.Bid.ImpID]; ok { 1291 if e.bidValidationEnforcement.SecureMarkup == config.ValidationEnforce && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { 1292 if !e.validateBidAdM(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.SecureMarkup) { 1293 seatNonBids.addBid(bid, int(ResponseRejectedCreativeNotSecure), adapter.String()) 1294 continue // Don't add bid to result 1295 } 1296 } else if e.bidValidationEnforcement.SecureMarkup == config.ValidationWarn && (bid.BidType == openrtb_ext.BidTypeBanner || bid.BidType == openrtb_ext.BidTypeVideo) { 1297 e.validateBidAdM(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.SecureMarkup) 1298 } 1299 1300 } 1301 bidExtPrebid := &openrtb_ext.ExtBidPrebid{ 1302 DealPriority: bid.DealPriority, 1303 DealTierSatisfied: bid.DealTierSatisfied, 1304 Events: bid.BidEvents, 1305 Targeting: bid.BidTargets, 1306 Floors: bid.BidFloors, 1307 Type: bid.BidType, 1308 Meta: bid.BidMeta, 1309 Video: bid.BidVideo, 1310 BidId: bid.GeneratedBidID, 1311 TargetBidderCode: bid.TargetBidderCode, 1312 } 1313 1314 if cacheInfo, found := e.getBidCacheInfo(bid, auc); found { 1315 bidExtPrebid.Cache = &openrtb_ext.ExtBidPrebidCache{ 1316 Bids: &cacheInfo, 1317 } 1318 } 1319 1320 if bidExtJSON, err := makeBidExtJSON(bid.Bid.Ext, bidExtPrebid, impExtInfoMap, bid.Bid.ImpID, bid.OriginalBidCPM, bid.OriginalBidCur, bid.AdapterCode); err != nil { 1321 errs = append(errs, err) 1322 } else { 1323 result = append(result, *bid.Bid) 1324 resultBid := &result[len(result)-1] 1325 resultBid.Ext = bidExtJSON 1326 if !returnCreative { 1327 resultBid.AdM = "" 1328 } 1329 } 1330 } 1331 return result, errs 1332 } 1333 1334 func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid, impExtInfoMap map[string]ImpExtInfo, impId string, originalBidCpm float64, originalBidCur string, adapter openrtb_ext.BidderName) (json.RawMessage, error) { 1335 var extMap map[string]interface{} 1336 1337 if len(ext) != 0 { 1338 if err := jsonutil.Unmarshal(ext, &extMap); err != nil { 1339 return nil, err 1340 } 1341 } else { 1342 extMap = make(map[string]interface{}) 1343 } 1344 1345 //ext.origbidcpm 1346 if originalBidCpm >= 0 { 1347 extMap[openrtb_ext.OriginalBidCpmKey] = originalBidCpm 1348 } 1349 1350 //ext.origbidcur 1351 if originalBidCur != "" { 1352 extMap[openrtb_ext.OriginalBidCurKey] = originalBidCur 1353 } 1354 1355 // ext.prebid 1356 if prebid.Meta == nil && maputil.HasElement(extMap, "prebid", "meta") { 1357 metaContainer := struct { 1358 Prebid struct { 1359 Meta openrtb_ext.ExtBidPrebidMeta `json:"meta"` 1360 } `json:"prebid"` 1361 }{} 1362 if err := jsonutil.Unmarshal(ext, &metaContainer); err != nil { 1363 return nil, fmt.Errorf("error validating response from server, %s", err) 1364 } 1365 prebid.Meta = &metaContainer.Prebid.Meta 1366 } 1367 1368 if prebid.Meta == nil { 1369 prebid.Meta = &openrtb_ext.ExtBidPrebidMeta{} 1370 } 1371 1372 prebid.Meta.AdapterCode = adapter.String() 1373 1374 // ext.prebid.storedrequestattributes and ext.prebid.passthrough 1375 if impExtInfo, ok := impExtInfoMap[impId]; ok { 1376 prebid.Passthrough = impExtInfoMap[impId].Passthrough 1377 if impExtInfo.EchoVideoAttrs { 1378 videoData, _, _, err := jsonparser.Get(impExtInfo.StoredImp, "video") 1379 if err != nil && err != jsonparser.KeyPathNotFoundError { 1380 return nil, err 1381 } 1382 //handler for case where EchoVideoAttrs is true, but video data is not found 1383 if len(videoData) > 0 { 1384 extMap[openrtb_ext.StoredRequestAttributes] = json.RawMessage(videoData) 1385 } 1386 } 1387 } 1388 extMap[openrtb_ext.PrebidExtKey] = prebid 1389 return jsonutil.Marshal(extMap) 1390 } 1391 1392 // If bid got cached inside `(a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, targData *targetData, bidRequest *openrtb2.BidRequest, ttlBuffer int64, defaultTTLs *config.DefaultTTLs, bidCategory map[string]string)`, 1393 // a UUID should be found inside `a.cacheIds` or `a.vastCacheIds`. This function returns the UUID along with the internal cache URL 1394 func (e *exchange) getBidCacheInfo(bid *entities.PbsOrtbBid, auction *auction) (cacheInfo openrtb_ext.ExtBidPrebidCacheBids, found bool) { 1395 uuid, found := findCacheID(bid, auction) 1396 1397 if found { 1398 cacheInfo.CacheId = uuid 1399 cacheInfo.Url = buildCacheURL(e.cache, uuid) 1400 } 1401 1402 return 1403 } 1404 1405 func findCacheID(bid *entities.PbsOrtbBid, auction *auction) (string, bool) { 1406 if bid != nil && bid.Bid != nil && auction != nil { 1407 if id, found := auction.cacheIds[bid.Bid]; found { 1408 return id, true 1409 } 1410 1411 if id, found := auction.vastCacheIds[bid.Bid]; found { 1412 return id, true 1413 } 1414 } 1415 1416 return "", false 1417 } 1418 1419 func buildCacheURL(cache prebid_cache_client.Client, uuid string) string { 1420 scheme, host, path := cache.GetExtCacheData() 1421 1422 if host == "" || path == "" { 1423 return "" 1424 } 1425 1426 query := url.Values{"uuid": []string{uuid}} 1427 cacheURL := url.URL{ 1428 Scheme: scheme, 1429 Host: host, 1430 Path: path, 1431 RawQuery: query.Encode(), 1432 } 1433 cacheURL.Query() 1434 1435 // URLs without a scheme will begin with //, in which case we 1436 // want to trim it off to keep compatbile with current behavior. 1437 return strings.TrimPrefix(cacheURL.String(), "//") 1438 } 1439 1440 func listBiddersWithRequests(bidderRequests []BidderRequest) []openrtb_ext.BidderName { 1441 liveAdapters := make([]openrtb_ext.BidderName, len(bidderRequests)) 1442 i := 0 1443 for _, bidderRequest := range bidderRequests { 1444 liveAdapters[i] = bidderRequest.BidderName 1445 i++ 1446 } 1447 // Randomize the list of adapters to make the auction more fair 1448 randomizeList(liveAdapters) 1449 1450 return liveAdapters 1451 } 1452 1453 func buildStoredAuctionResponse(storedAuctionResponses map[string]json.RawMessage) ( 1454 map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, 1455 *openrtb_ext.Fledge, 1456 []openrtb_ext.BidderName, 1457 error) { 1458 1459 adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, 0) 1460 var fledge *openrtb_ext.Fledge 1461 liveAdapters := make([]openrtb_ext.BidderName, 0) 1462 for impId, storedResp := range storedAuctionResponses { 1463 var seatBids []openrtb2.SeatBid 1464 1465 if err := jsonutil.UnmarshalValid(storedResp, &seatBids); err != nil { 1466 return nil, nil, nil, err 1467 } 1468 for _, seat := range seatBids { 1469 var bidsToAdd []*entities.PbsOrtbBid 1470 //set imp id from request 1471 for i := range seat.Bid { 1472 seat.Bid[i].ImpID = impId 1473 bidType, err := getMediaTypeForBid(seat.Bid[i]) 1474 if err != nil { 1475 return nil, nil, nil, err 1476 } 1477 bidsToAdd = append(bidsToAdd, &entities.PbsOrtbBid{Bid: &seat.Bid[i], BidType: bidType}) 1478 } 1479 1480 bidderName := openrtb_ext.BidderName(seat.Seat) 1481 1482 if seat.Ext != nil { 1483 var seatExt openrtb_ext.ExtBidResponse 1484 if err := jsonutil.Unmarshal(seat.Ext, &seatExt); err != nil { 1485 return nil, nil, nil, err 1486 } 1487 // add in FLEDGE response with impId substituted 1488 if seatExt.Prebid != nil && 1489 seatExt.Prebid.Fledge != nil && 1490 seatExt.Prebid.Fledge.AuctionConfigs != nil { 1491 auctionConfigs := seatExt.Prebid.Fledge.AuctionConfigs 1492 if fledge == nil { 1493 fledge = &openrtb_ext.Fledge{ 1494 AuctionConfigs: make([]*openrtb_ext.FledgeAuctionConfig, 0, len(auctionConfigs)), 1495 } 1496 } 1497 for _, config := range auctionConfigs { 1498 newConfig := &openrtb_ext.FledgeAuctionConfig{ 1499 ImpId: impId, 1500 Bidder: string(bidderName), 1501 Adapter: string(bidderName), 1502 Config: config.Config, 1503 } 1504 fledge.AuctionConfigs = append(fledge.AuctionConfigs, newConfig) 1505 } 1506 } 1507 } 1508 1509 if _, ok := adapterBids[bidderName]; ok { 1510 adapterBids[bidderName].Bids = append(adapterBids[bidderName].Bids, bidsToAdd...) 1511 1512 } else { 1513 //create new seat bid and add it to live adapters 1514 liveAdapters = append(liveAdapters, bidderName) 1515 newSeatBid := entities.PbsOrtbSeatBid{ 1516 Bids: bidsToAdd, 1517 Currency: "", 1518 Seat: "", 1519 } 1520 adapterBids[bidderName] = &newSeatBid 1521 1522 } 1523 } 1524 } 1525 1526 return adapterBids, fledge, liveAdapters, nil 1527 } 1528 1529 func isAdsCertEnabled(experiment *openrtb_ext.Experiment, info config.BidderInfo) bool { 1530 requestAdsCertEnabled := experiment != nil && experiment.AdsCert != nil && experiment.AdsCert.Enabled 1531 bidderAdsCertEnabled := info.Experiment.AdsCert.Enabled 1532 return requestAdsCertEnabled && bidderAdsCertEnabled 1533 } 1534 1535 func (e exchange) validateBannerCreativeSize(bid *entities.PbsOrtbBid, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, validationType string) bool { 1536 if bid.Bid.W > e.bidValidationEnforcement.MaxCreativeWidth || bid.Bid.H > e.bidValidationEnforcement.MaxCreativeHeight { 1537 // Add error to debug array 1538 errorMessage := setErrorMessageCreativeSize(validationType) 1539 bidCreativeMaxSizeError := openrtb_ext.ExtBidderMessage{ 1540 Code: errortypes.BadServerResponseErrorCode, 1541 Message: errorMessage, 1542 } 1543 bidResponseExt.Errors[adapter] = append(bidResponseExt.Errors[adapter], bidCreativeMaxSizeError) 1544 1545 // Log Metrics 1546 e.me.RecordBidValidationCreativeSizeError(adapter, pubID) 1547 1548 return false 1549 } 1550 return true 1551 } 1552 1553 func (e exchange) validateBidAdM(bid *entities.PbsOrtbBid, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, validationType string) bool { 1554 invalidAdM := []string{"http:", "http%3A"} 1555 requiredAdM := []string{"https:", "https%3A"} 1556 1557 if (strings.Contains(bid.Bid.AdM, invalidAdM[0]) || strings.Contains(bid.Bid.AdM, invalidAdM[1])) && (!strings.Contains(bid.Bid.AdM, requiredAdM[0]) && !strings.Contains(bid.Bid.AdM, requiredAdM[1])) { 1558 // Add error to debug array 1559 errorMessage := setErrorMessageSecureMarkup(validationType) 1560 bidSecureMarkupError := openrtb_ext.ExtBidderMessage{ 1561 Code: errortypes.BadServerResponseErrorCode, 1562 Message: errorMessage, 1563 } 1564 bidResponseExt.Errors[adapter] = append(bidResponseExt.Errors[adapter], bidSecureMarkupError) 1565 1566 // Log Metrics 1567 e.me.RecordBidValidationSecureMarkupError(adapter, pubID) 1568 1569 return false 1570 } 1571 return true 1572 } 1573 1574 func setErrorMessageCreativeSize(validationType string) string { 1575 if validationType == config.ValidationEnforce { 1576 return "bidResponse rejected: size WxH" 1577 } else if validationType == config.ValidationWarn { 1578 return "bidResponse creative size warning: size WxH larger than AdUnit sizes" 1579 } 1580 return "" 1581 } 1582 1583 func setErrorMessageSecureMarkup(validationType string) string { 1584 if validationType == config.ValidationEnforce { 1585 return "bidResponse rejected: insecure creative in secure context" 1586 } else if validationType == config.ValidationWarn { 1587 return "bidResponse secure markup warning: insecure creative in secure contexts" 1588 } 1589 return "" 1590 } 1591 1592 // setSeatNonBid adds SeatNonBids within bidResponse.Ext.Prebid.SeatNonBid 1593 func setSeatNonBid(bidResponseExt *openrtb_ext.ExtBidResponse, seatNonBids nonBids) *openrtb_ext.ExtBidResponse { 1594 if len(seatNonBids.seatNonBidsMap) == 0 { 1595 return bidResponseExt 1596 } 1597 if bidResponseExt == nil { 1598 bidResponseExt = &openrtb_ext.ExtBidResponse{} 1599 } 1600 if bidResponseExt.Prebid == nil { 1601 bidResponseExt.Prebid = &openrtb_ext.ExtResponsePrebid{} 1602 } 1603 1604 bidResponseExt.Prebid.SeatNonBid = seatNonBids.get() 1605 return bidResponseExt 1606 }