github.com/prebid/prebid-server@v0.275.0/adapters/appnexus/appnexus.go (about) 1 package appnexus 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strconv" 10 "strings" 11 12 "github.com/buger/jsonparser" 13 "github.com/prebid/openrtb/v19/adcom1" 14 "github.com/prebid/openrtb/v19/openrtb2" 15 "github.com/prebid/prebid-server/config" 16 "github.com/prebid/prebid-server/util/maputil" 17 "github.com/prebid/prebid-server/util/ptrutil" 18 "github.com/prebid/prebid-server/util/randomutil" 19 20 "github.com/prebid/prebid-server/adapters" 21 "github.com/prebid/prebid-server/errortypes" 22 "github.com/prebid/prebid-server/metrics" 23 "github.com/prebid/prebid-server/openrtb_ext" 24 ) 25 26 const ( 27 defaultPlatformID = 5 28 maxImpsPerReq = 10 29 ) 30 31 type adapter struct { 32 uri url.URL 33 hbSource int 34 randomGenerator randomutil.RandomGenerator 35 } 36 37 // Builder builds a new instance of the AppNexus adapter for the given bidder with the given config. 38 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 39 uri, err := url.Parse(config.Endpoint) 40 if err != nil { 41 return nil, err 42 } 43 44 bidder := &adapter{ 45 uri: *uri, 46 hbSource: resolvePlatformID(config.PlatformID), 47 randomGenerator: randomutil.RandomNumberGenerator{}, 48 } 49 return bidder, nil 50 } 51 52 func resolvePlatformID(platformID string) int { 53 if len(platformID) > 0 { 54 if val, err := strconv.Atoi(platformID); err == nil { 55 return val 56 } 57 } 58 59 return defaultPlatformID 60 } 61 62 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 63 // appnexus adapter expects imp.displaymanagerver to be populated in openrtb2 endpoint 64 // but some SDKs will put it in imp.ext.prebid instead 65 displayManagerVer := buildDisplayManageVer(request) 66 67 var ( 68 shouldGenerateAdPodId *bool 69 uniqueMemberID string 70 errs []error 71 ) 72 73 validImps := []openrtb2.Imp{} 74 for i := 0; i < len(request.Imp); i++ { 75 appnexusExt, err := validateAndBuildAppNexusExt(&request.Imp[i]) 76 if err != nil { 77 errs = append(errs, err) 78 continue 79 } 80 81 if err := buildRequestImp(&request.Imp[i], &appnexusExt, displayManagerVer); err != nil { 82 errs = append(errs, err) 83 continue 84 } 85 86 memberId := appnexusExt.Member 87 if memberId != "" { 88 // The Appnexus API requires a Member ID in the URL. This means the request may fail if 89 // different impressions have different member IDs. 90 // Check for this condition, and log an error if it's a problem. 91 if uniqueMemberID == "" { 92 uniqueMemberID = memberId 93 } else if uniqueMemberID != memberId { 94 errs = append(errs, fmt.Errorf("all request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained member IDs %s and %s", uniqueMemberID, memberId)) 95 return nil, errs 96 } 97 } 98 99 shouldGenerateAdPodIdForImp := appnexusExt.AdPodId 100 if shouldGenerateAdPodId == nil { 101 shouldGenerateAdPodId = &shouldGenerateAdPodIdForImp 102 } else if *shouldGenerateAdPodId != shouldGenerateAdPodIdForImp { 103 errs = append(errs, errors.New("generate ad pod option should be same for all pods in request")) 104 return nil, errs 105 } 106 107 validImps = append(validImps, request.Imp[i]) 108 } 109 request.Imp = validImps 110 111 // If all the requests were malformed, don't bother making a server call with no impressions. 112 if len(request.Imp) == 0 { 113 return nil, errs 114 } 115 116 requestURI := a.uri 117 if uniqueMemberID != "" { 118 requestURI = appendMemberId(requestURI, uniqueMemberID) 119 } 120 121 // Add Appnexus request level extension 122 var isAMP, isVIDEO int 123 if reqInfo.PbsEntryPoint == metrics.ReqTypeAMP { 124 isAMP = 1 125 } else if reqInfo.PbsEntryPoint == metrics.ReqTypeVideo { 126 isVIDEO = 1 127 } 128 129 reqExt, err := getRequestExt(request.Ext) 130 if err != nil { 131 return nil, append(errs, err) 132 } 133 134 reqExtAppnexus, err := a.getAppnexusExt(reqExt, isAMP, isVIDEO) 135 if err != nil { 136 return nil, append(errs, err) 137 } 138 139 if err := moveSupplyChain(request, reqExt); err != nil { 140 return nil, append(errs, err) 141 } 142 143 // For long form requests if adpodId feature enabled, adpod_id must be sent downstream. 144 // Adpod id is a unique identifier for pod 145 // All impressions in the same pod must have the same pod id in request extension 146 // For this all impressions in request should belong to the same pod 147 // If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same 148 // If adpodId feature disabled and impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but do not include ad pod id 149 if isVIDEO == 1 && *shouldGenerateAdPodId { 150 requests, errors := a.buildAdPodRequests(request.Imp, request, reqExt, reqExtAppnexus, requestURI.String()) 151 return requests, append(errs, errors...) 152 } 153 154 requests, errors := splitRequests(request.Imp, request, reqExt, reqExtAppnexus, requestURI.String()) 155 return requests, append(errs, errors...) 156 } 157 158 func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 159 if adapters.IsResponseStatusCodeNoContent(response) { 160 return nil, nil 161 } 162 163 if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { 164 return nil, []error{err} 165 } 166 167 var appnexusResponse openrtb2.BidResponse 168 if err := json.Unmarshal(response.Body, &appnexusResponse); err != nil { 169 return nil, []error{err} 170 } 171 172 var errs []error 173 bidderResponse := adapters.NewBidderResponseWithBidsCapacity(5) 174 for _, sb := range appnexusResponse.SeatBid { 175 for i := range sb.Bid { 176 bid := sb.Bid[i] 177 178 var bidExt bidExt 179 if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { 180 errs = append(errs, err) 181 continue 182 } 183 184 bidType, err := getMediaTypeForBid(&bidExt) 185 if err != nil { 186 errs = append(errs, err) 187 continue 188 } 189 190 iabCategory, found := a.findIabCategoryForBid(&bidExt) 191 if found { 192 bid.Cat = []string{iabCategory} 193 } else if len(bid.Cat) > 1 { 194 //create empty categories array to force bid to be rejected 195 bid.Cat = []string{} 196 } 197 198 bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ 199 Bid: &bid, 200 BidType: bidType, 201 BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: bidExt.Appnexus.CreativeInfo.Video.Duration}, 202 DealPriority: bidExt.Appnexus.DealPriority, 203 }) 204 } 205 } 206 207 if appnexusResponse.Cur != "" { 208 bidderResponse.Currency = appnexusResponse.Cur 209 } 210 211 return bidderResponse, errs 212 } 213 214 func getRequestExt(ext json.RawMessage) (map[string]json.RawMessage, error) { 215 extMap := make(map[string]json.RawMessage) 216 217 if len(ext) > 0 { 218 if err := json.Unmarshal(ext, &extMap); err != nil { 219 return nil, err 220 } 221 } 222 223 return extMap, nil 224 } 225 226 func (a *adapter) getAppnexusExt(extMap map[string]json.RawMessage, isAMP int, isVIDEO int) (bidReqExtAppnexus, error) { 227 var appnexusExt bidReqExtAppnexus 228 229 if appnexusExtJson, exists := extMap["appnexus"]; exists && len(appnexusExtJson) > 0 { 230 if err := json.Unmarshal(appnexusExtJson, &appnexusExt); err != nil { 231 return appnexusExt, err 232 } 233 } 234 235 if prebidJson, exists := extMap["prebid"]; exists { 236 _, valueType, _, err := jsonparser.Get(prebidJson, "targeting", "includebrandcategory") 237 if err != nil && !errors.Is(err, jsonparser.KeyPathNotFoundError) { 238 return appnexusExt, err 239 } 240 241 if valueType == jsonparser.Object { 242 appnexusExt.BrandCategoryUniqueness = ptrutil.ToPtr(true) 243 appnexusExt.IncludeBrandCategory = ptrutil.ToPtr(true) 244 } 245 } 246 247 appnexusExt.IsAMP = isAMP 248 appnexusExt.HeaderBiddingSource = a.hbSource + isVIDEO 249 250 return appnexusExt, nil 251 } 252 253 func validateAndBuildAppNexusExt(imp *openrtb2.Imp) (openrtb_ext.ExtImpAppnexus, error) { 254 var bidderExt adapters.ExtImpBidder 255 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 256 return openrtb_ext.ExtImpAppnexus{}, err 257 } 258 259 var appnexusExt openrtb_ext.ExtImpAppnexus 260 if err := json.Unmarshal(bidderExt.Bidder, &appnexusExt); err != nil { 261 return openrtb_ext.ExtImpAppnexus{}, err 262 } 263 264 handleLegacyParams(&appnexusExt) 265 266 if err := validateAppnexusExt(&appnexusExt); err != nil { 267 return openrtb_ext.ExtImpAppnexus{}, err 268 } 269 270 return appnexusExt, nil 271 } 272 273 func handleLegacyParams(appnexusExt *openrtb_ext.ExtImpAppnexus) { 274 if appnexusExt.PlacementId == 0 && appnexusExt.DeprecatedPlacementId != 0 { 275 appnexusExt.PlacementId = appnexusExt.DeprecatedPlacementId 276 } 277 if appnexusExt.InvCode == "" && appnexusExt.LegacyInvCode != "" { 278 appnexusExt.InvCode = appnexusExt.LegacyInvCode 279 } 280 if appnexusExt.TrafficSourceCode == "" && appnexusExt.LegacyTrafficSourceCode != "" { 281 appnexusExt.TrafficSourceCode = appnexusExt.LegacyTrafficSourceCode 282 } 283 if appnexusExt.UsePaymentRule == nil && appnexusExt.DeprecatedUsePaymentRule != nil { 284 appnexusExt.UsePaymentRule = appnexusExt.DeprecatedUsePaymentRule 285 } 286 } 287 288 func groupByPods(imps []openrtb2.Imp) map[string]([]openrtb2.Imp) { 289 // find number of pods in response 290 podImps := make(map[string][]openrtb2.Imp) 291 for _, imp := range imps { 292 pod := strings.Split(imp.ID, "_")[0] 293 podImps[pod] = append(podImps[pod], imp) 294 } 295 return podImps 296 } 297 298 func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtAppnexus bidReqExtAppnexus, uri string) ([]*adapters.RequestData, []error) { 299 var errs []error 300 // Initial capacity for future array of requests, memory optimization. 301 // Let's say there are 35 impressions and limit impressions per request equals to 10. 302 // In this case we need to create 4 requests with 10, 10, 10 and 5 impressions. 303 // With this formula initial capacity=(35+10-1)/10 = 4 304 initialCapacity := (len(imps) + maxImpsPerReq - 1) / maxImpsPerReq 305 resArr := make([]*adapters.RequestData, 0, initialCapacity) 306 startInd := 0 307 impsLeft := len(imps) > 0 308 309 headers := http.Header{} 310 headers.Add("Content-Type", "application/json;charset=utf-8") 311 headers.Add("Accept", "application/json") 312 313 appnexusExtJson, err := json.Marshal(requestExtAppnexus) 314 if err != nil { 315 errs = append(errs, err) 316 } 317 318 requestExtClone := maputil.Clone(requestExt) 319 requestExtClone["appnexus"] = appnexusExtJson 320 321 request.Ext, err = json.Marshal(requestExtClone) 322 if err != nil { 323 errs = append(errs, err) 324 } 325 326 for impsLeft { 327 endInd := startInd + maxImpsPerReq 328 if endInd >= len(imps) { 329 endInd = len(imps) 330 impsLeft = false 331 } 332 impsForReq := imps[startInd:endInd] 333 request.Imp = impsForReq 334 335 reqJSON, err := json.Marshal(request) 336 if err != nil { 337 errs = append(errs, err) 338 return nil, errs 339 } 340 341 resArr = append(resArr, &adapters.RequestData{ 342 Method: "POST", 343 Uri: uri, 344 Body: reqJSON, 345 Headers: headers, 346 }) 347 startInd = endInd 348 } 349 return resArr, errs 350 } 351 352 func validateAppnexusExt(appnexusExt *openrtb_ext.ExtImpAppnexus) error { 353 if appnexusExt.PlacementId == 0 && (appnexusExt.InvCode == "" || appnexusExt.Member == "") { 354 return &errortypes.BadInput{ 355 Message: "No placement or member+invcode provided", 356 } 357 } 358 return nil 359 } 360 361 func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus, displayManagerVer string) error { 362 if appnexusExt.InvCode != "" { 363 imp.TagID = appnexusExt.InvCode 364 } 365 366 if imp.BidFloor <= 0 && appnexusExt.Reserve > 0 { 367 imp.BidFloor = appnexusExt.Reserve // This will be broken for non-USD currency. 368 } 369 370 if imp.Banner != nil { 371 bannerCopy := *imp.Banner 372 if appnexusExt.Position == "above" { 373 bannerCopy.Pos = adcom1.PositionAboveFold.Ptr() 374 } else if appnexusExt.Position == "below" { 375 bannerCopy.Pos = adcom1.PositionBelowFold.Ptr() 376 } 377 378 if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 { 379 firstFormat := bannerCopy.Format[0] 380 bannerCopy.W = &(firstFormat.W) 381 bannerCopy.H = &(firstFormat.H) 382 } 383 imp.Banner = &bannerCopy 384 } 385 386 // Populate imp.displaymanagerver if the SDK failed to do it. 387 if len(imp.DisplayManagerVer) == 0 && len(displayManagerVer) > 0 { 388 imp.DisplayManagerVer = displayManagerVer 389 } 390 391 impExt := impExt{Appnexus: impExtAppnexus{ 392 PlacementID: int(appnexusExt.PlacementId), 393 TrafficSourceCode: appnexusExt.TrafficSourceCode, 394 Keywords: appnexusExt.Keywords.String(), 395 UsePmtRule: appnexusExt.UsePaymentRule, 396 PrivateSizes: appnexusExt.PrivateSizes, 397 ExtInvCode: appnexusExt.ExtInvCode, 398 ExternalImpID: appnexusExt.ExternalImpId, 399 }} 400 401 var err error 402 imp.Ext, err = json.Marshal(&impExt) 403 404 return err 405 } 406 407 // getMediaTypeForBid determines which type of bid. 408 func getMediaTypeForBid(bid *bidExt) (openrtb_ext.BidType, error) { 409 switch bid.Appnexus.BidType { 410 case 0: 411 return openrtb_ext.BidTypeBanner, nil 412 case 1: 413 return openrtb_ext.BidTypeVideo, nil 414 case 3: 415 return openrtb_ext.BidTypeNative, nil 416 default: 417 return "", fmt.Errorf("Unrecognized bid_ad_type in response from appnexus: %d", bid.Appnexus.BidType) 418 } 419 } 420 421 // getIabCategoryForBid maps an appnexus brand id to an IAB category. 422 func (a *adapter) findIabCategoryForBid(bid *bidExt) (string, bool) { 423 brandIDString := strconv.Itoa(bid.Appnexus.BrandCategory) 424 iabCategory, ok := iabCategoryMap[brandIDString] 425 return iabCategory, ok 426 } 427 428 func appendMemberId(uri url.URL, memberId string) url.URL { 429 q := uri.Query() 430 q.Set("member_id", memberId) 431 uri.RawQuery = q.Encode() 432 return uri 433 } 434 435 func buildDisplayManageVer(req *openrtb2.BidRequest) string { 436 if req.App == nil { 437 return "" 438 } 439 440 source, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "source") 441 if err != nil { 442 return "" 443 } 444 445 version, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "version") 446 if err != nil { 447 return "" 448 } 449 450 return fmt.Sprintf("%s-%s", source, version) 451 } 452 453 // moveSupplyChain moves the supply chain object from source.ext.schain to ext.schain. 454 func moveSupplyChain(request *openrtb2.BidRequest, extMap map[string]json.RawMessage) error { 455 if request == nil || request.Source == nil || len(request.Source.Ext) == 0 { 456 return nil 457 } 458 459 sourceExtMap := make(map[string]json.RawMessage) 460 if err := json.Unmarshal(request.Source.Ext, &sourceExtMap); err != nil { 461 return err 462 } 463 464 schainJson, exists := sourceExtMap["schain"] 465 if !exists { 466 return nil 467 } 468 469 delete(sourceExtMap, "schain") 470 471 request.Source = ptrutil.Clone(request.Source) 472 473 if len(sourceExtMap) > 0 { 474 ext, err := json.Marshal(sourceExtMap) 475 if err != nil { 476 return err 477 } 478 request.Source.Ext = ext 479 } else { 480 request.Source.Ext = nil 481 } 482 483 extMap["schain"] = schainJson 484 485 return nil 486 } 487 488 func (a *adapter) buildAdPodRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtAppnexus bidReqExtAppnexus, uri string) ([]*adapters.RequestData, []error) { 489 var errs []error 490 podImps := groupByPods(imps) 491 requests := make([]*adapters.RequestData, 0, len(podImps)) 492 for _, podImps := range podImps { 493 requestExtAppnexus.AdPodID = fmt.Sprint(a.randomGenerator.GenerateInt63()) 494 495 reqs, errors := splitRequests(podImps, request, requestExt, requestExtAppnexus, uri) 496 requests = append(requests, reqs...) 497 errs = append(errs, errors...) 498 } 499 500 return requests, errs 501 }