github.com/prebid/prebid-server/v2@v2.18.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/v20/adcom1" 14 "github.com/prebid/openrtb/v20/openrtb2" 15 "github.com/prebid/prebid-server/v2/config" 16 "github.com/prebid/prebid-server/v2/util/maputil" 17 "github.com/prebid/prebid-server/v2/util/ptrutil" 18 "github.com/prebid/prebid-server/v2/util/randomutil" 19 20 "github.com/prebid/prebid-server/v2/adapters" 21 "github.com/prebid/prebid-server/v2/errortypes" 22 "github.com/prebid/prebid-server/v2/metrics" 23 "github.com/prebid/prebid-server/v2/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 ImpIDs: openrtb_ext.GetImpIDs(request.Imp), 347 }) 348 startInd = endInd 349 } 350 return resArr, errs 351 } 352 353 func validateAppnexusExt(appnexusExt *openrtb_ext.ExtImpAppnexus) error { 354 if appnexusExt.PlacementId == 0 && (appnexusExt.InvCode == "" || appnexusExt.Member == "") { 355 return &errortypes.BadInput{ 356 Message: "No placement or member+invcode provided", 357 } 358 } 359 return nil 360 } 361 362 func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus, displayManagerVer string) error { 363 if appnexusExt.InvCode != "" { 364 imp.TagID = appnexusExt.InvCode 365 } 366 367 if imp.BidFloor <= 0 && appnexusExt.Reserve > 0 { 368 imp.BidFloor = appnexusExt.Reserve // This will be broken for non-USD currency. 369 } 370 371 if imp.Banner != nil { 372 bannerCopy := *imp.Banner 373 if appnexusExt.Position == "above" { 374 bannerCopy.Pos = adcom1.PositionAboveFold.Ptr() 375 } else if appnexusExt.Position == "below" { 376 bannerCopy.Pos = adcom1.PositionBelowFold.Ptr() 377 } 378 379 if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 { 380 firstFormat := bannerCopy.Format[0] 381 bannerCopy.W = &(firstFormat.W) 382 bannerCopy.H = &(firstFormat.H) 383 } 384 imp.Banner = &bannerCopy 385 } 386 387 // Populate imp.displaymanagerver if the SDK failed to do it. 388 if len(imp.DisplayManagerVer) == 0 && len(displayManagerVer) > 0 { 389 imp.DisplayManagerVer = displayManagerVer 390 } 391 392 impExt := impExt{Appnexus: impExtAppnexus{ 393 PlacementID: int(appnexusExt.PlacementId), 394 TrafficSourceCode: appnexusExt.TrafficSourceCode, 395 Keywords: appnexusExt.Keywords.String(), 396 UsePmtRule: appnexusExt.UsePaymentRule, 397 PrivateSizes: appnexusExt.PrivateSizes, 398 ExtInvCode: appnexusExt.ExtInvCode, 399 ExternalImpID: appnexusExt.ExternalImpId, 400 }} 401 402 var err error 403 imp.Ext, err = json.Marshal(&impExt) 404 405 return err 406 } 407 408 // getMediaTypeForBid determines which type of bid. 409 func getMediaTypeForBid(bid *bidExt) (openrtb_ext.BidType, error) { 410 switch bid.Appnexus.BidType { 411 case 0: 412 return openrtb_ext.BidTypeBanner, nil 413 case 1: 414 return openrtb_ext.BidTypeVideo, nil 415 case 3: 416 return openrtb_ext.BidTypeNative, nil 417 default: 418 return "", fmt.Errorf("Unrecognized bid_ad_type in response from appnexus: %d", bid.Appnexus.BidType) 419 } 420 } 421 422 // getIabCategoryForBid maps an appnexus brand id to an IAB category. 423 func (a *adapter) findIabCategoryForBid(bid *bidExt) (string, bool) { 424 brandIDString := strconv.Itoa(bid.Appnexus.BrandCategory) 425 iabCategory, ok := iabCategoryMap[brandIDString] 426 return iabCategory, ok 427 } 428 429 func appendMemberId(uri url.URL, memberId string) url.URL { 430 q := uri.Query() 431 q.Set("member_id", memberId) 432 uri.RawQuery = q.Encode() 433 return uri 434 } 435 436 func buildDisplayManageVer(req *openrtb2.BidRequest) string { 437 if req.App == nil { 438 return "" 439 } 440 441 source, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "source") 442 if err != nil { 443 return "" 444 } 445 446 version, err := jsonparser.GetString(req.App.Ext, openrtb_ext.PrebidExtKey, "version") 447 if err != nil { 448 return "" 449 } 450 451 return fmt.Sprintf("%s-%s", source, version) 452 } 453 454 // moveSupplyChain moves the supply chain object from source.ext.schain to ext.schain. 455 func moveSupplyChain(request *openrtb2.BidRequest, extMap map[string]json.RawMessage) error { 456 if request == nil || request.Source == nil || len(request.Source.Ext) == 0 { 457 return nil 458 } 459 460 sourceExtMap := make(map[string]json.RawMessage) 461 if err := json.Unmarshal(request.Source.Ext, &sourceExtMap); err != nil { 462 return err 463 } 464 465 schainJson, exists := sourceExtMap["schain"] 466 if !exists { 467 return nil 468 } 469 470 delete(sourceExtMap, "schain") 471 472 request.Source = ptrutil.Clone(request.Source) 473 474 if len(sourceExtMap) > 0 { 475 ext, err := json.Marshal(sourceExtMap) 476 if err != nil { 477 return err 478 } 479 request.Source.Ext = ext 480 } else { 481 request.Source.Ext = nil 482 } 483 484 extMap["schain"] = schainJson 485 486 return nil 487 } 488 489 func (a *adapter) buildAdPodRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExt map[string]json.RawMessage, requestExtAppnexus bidReqExtAppnexus, uri string) ([]*adapters.RequestData, []error) { 490 var errs []error 491 podImps := groupByPods(imps) 492 requests := make([]*adapters.RequestData, 0, len(podImps)) 493 for _, podImps := range podImps { 494 requestExtAppnexus.AdPodID = fmt.Sprint(a.randomGenerator.GenerateInt63()) 495 496 reqs, errors := splitRequests(podImps, request, requestExt, requestExtAppnexus, uri) 497 requests = append(requests, reqs...) 498 errs = append(errs, errors...) 499 } 500 501 return requests, errs 502 }