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