Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented audienceNetwork (facebook) bidder for ortb endpoint #4

Merged
merged 4 commits into from
Apr 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/bidders/audienceNetwork.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Audience Network Bidder

Audience Network bidder requires `placementId` attribute to be sent in the `ext` object of impressions.

## Mobile Bids

Audience Network will not bid on requests made from device simulators.
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/iab/openrtb/request/Publisher.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* This object describes the publisher of the media in which the ad will be
* displayed. The publisher is typically the seller in an OpenRTB transaction.
*/
@Builder
@Builder(toBuilder = true)
@Value
public class Publisher {

Expand Down
228 changes: 224 additions & 4 deletions src/main/java/org/prebid/server/bidder/facebook/FacebookBidder.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,254 @@
package org.prebid.server.bidder.facebook;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.App;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Publisher;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.Video;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.Json;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.BidderUtil;
import org.prebid.server.bidder.facebook.proto.ExtImpFacebook;
import org.prebid.server.bidder.facebook.proto.FacebookExt;
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.HttpCall;
import org.prebid.server.bidder.model.HttpRequest;
import org.prebid.server.bidder.model.Result;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.HttpUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;

/**
* Facebook {@link Bidder} implementation.
*/
public class FacebookBidder implements Bidder {
public class FacebookBidder implements Bidder<BidRequest> {

private static final Logger logger = LoggerFactory.getLogger(FacebookBidder.class);

private static final Random RANDOM = new Random();
private static final TypeReference<ExtPrebid<?, ExtImpFacebook>> FACEBOOK_EXT_TYPE_REFERENCE = new
TypeReference<ExtPrebid<?, ExtImpFacebook>>() {
};
private static final Integer LEGACY_BANNER_WIDTH = 320;
private static final Integer LEGACY_BANNER_HEIGHT = 50;

private final String endpointUrl;
private final String nonSecureEndpointUrl;
private final ObjectNode platformJson;

public FacebookBidder(String endpointUrl, String nonSecureEndpointUrl, String platformId) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.nonSecureEndpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(nonSecureEndpointUrl));
platformJson = createPlatformJson(Objects.requireNonNull(platformId));
}

@Override
public Result<List<HttpRequest>> makeHttpRequests(BidRequest bidRequest) {
throw new UnsupportedOperationException();
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
if (CollectionUtils.isEmpty(bidRequest.getImp())) {
return Result.of(Collections.emptyList(), Collections.emptyList());
}

final List<String> errors = new ArrayList<>();
final List<Imp> processedImps = new ArrayList<>();
final String placementId;
String pubId = null;

try {
placementId = parseExtImpFacebook(bidRequest.getImp().get(0)).getPlacementId();
pubId = extractPubId(placementId);

for (final Imp imp : bidRequest.getImp()) {
processedImps.add(makeImp(imp, placementId));
}

} catch (PreBidException e) {
errors.add(e.getMessage());
}

final BidRequest outgoingRequest = bidRequest.toBuilder()
.imp(processedImps)
.site(makeSite(bidRequest, pubId))
.app(makeApp(bidRequest, pubId))
.build();
final String body = Json.encode(outgoingRequest);

return Result.of(Collections.singletonList(
HttpRequest.of(HttpMethod.POST, endpointUrl(), body, BidderUtil.headers(), outgoingRequest)),
BidderUtil.errors(errors));
}

@Override
public Result<List<BidderBid>> makeBids(HttpCall httpCall, BidRequest bidRequest) {
throw new UnsupportedOperationException();
try {
return Result.of(extractBids(BidderUtil.parseResponse(httpCall.getResponse())), Collections.emptyList());
} catch (PreBidException e) {
return Result.of(Collections.emptyList(), Collections.singletonList(BidderError.create(e.getMessage())));
}
}

@Override
public Map<String, String> extractTargeting(ObjectNode ext) {
return Collections.emptyMap();
}

private static ObjectNode createPlatformJson(String platformId) {
final Integer platformIdAsInt;
try {
platformIdAsInt = Integer.valueOf(platformId);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("Platform ID is not valid number: '%s'", platformId), e);
}
return Json.mapper.valueToTree(FacebookExt.of(platformIdAsInt));
}

private ExtImpFacebook parseExtImpFacebook(Imp imp) {
if (imp.getExt() == null) {
throw new PreBidException("audienceNetwork parameters section is missing");
}

try {
return Json.mapper.<ExtPrebid<?, ExtImpFacebook>>convertValue(imp.getExt(), FACEBOOK_EXT_TYPE_REFERENCE)
.getBidder();
} catch (IllegalArgumentException e) {
logger.warn("Error occurred parsing audienceNetwork parameters", e);
throw new PreBidException(e.getMessage(), e);
}
}

private String extractPubId(String placementId) {
if (StringUtils.isBlank(placementId)) {
throw new PreBidException("Missing placementId param");
}

final String[] placementIdSplit = placementId.split("_");
if (placementIdSplit.length != 2) {
throw new PreBidException(String.format("Invalid placementId param '%s'", placementId));
}
return placementIdSplit[0];
}

private Imp makeImp(Imp imp, String placementId) {
validateMediaImpMediaTypes(imp);
return imp.toBuilder()
.tagid(placementId)
.banner(makeBanner(imp))
.ext(platformJson)
.build();
}

private static Site makeSite(BidRequest bidRequest, String pubId) {
final Site site = bidRequest.getSite();
if (site == null) {
return null;
}

final Publisher publisher = site.getPublisher();
if (publisher != null && StringUtils.isNotBlank(publisher.getId())) {
return site;
}

return site.toBuilder()
.publisher(makePublisher(publisher, pubId))
.build();
}

private static App makeApp(BidRequest bidRequest, String pubId) {
final App app = bidRequest.getApp();
if (app == null) {
return null;
}

final Publisher publisher = app.getPublisher();
if (publisher != null && StringUtils.isNotBlank(publisher.getId())) {
return app;
}

return app.toBuilder()
.publisher(makePublisher(publisher, pubId))
.build();
}

private static Publisher makePublisher(Publisher publisher, String pubId) {
final Publisher.PublisherBuilder publisherBuilder = publisher == null
? Publisher.builder()
: publisher.toBuilder();
return publisherBuilder
.id(pubId)
.build();
}

private static void validateMediaImpMediaTypes(Imp imp) {
if (imp.getXNative() != null || imp.getAudio() != null) {
throw new PreBidException(
String.format("audienceNetwork doesn't support native or audio Imps. Ignoring Imp ID=%s",
imp.getId()));
}

final Video video = imp.getVideo();
if (imp.getVideo() != null) {
if (CollectionUtils.isEmpty(video.getMimes())) {
throw new PreBidException("audienceNetwork doesn't support video type with no video data");
}
}
}

private static Banner makeBanner(Imp imp) {
final Banner banner = imp.getBanner();
if (banner == null) {
return null;
}

final Integer h = banner.getH();
final Integer w = banner.getW();

if (Objects.equals(w, LEGACY_BANNER_WIDTH) && Objects.equals(h, LEGACY_BANNER_HEIGHT)) {
// do not send legacy 320x50 size to facebook, instead use 0x50
return banner.toBuilder().w(0).build();
}
return banner;
}

private String endpointUrl() {
// 50% of traffic to non-secure endpoint
return RANDOM.nextBoolean() ? endpointUrl : nonSecureEndpointUrl;
}

private static List<BidderBid> extractBids(BidResponse bidResponse) {
return bidResponse == null || bidResponse.getSeatbid() == null
? Collections.emptyList()
: bidsFromResponse(bidResponse);
}

private static List<BidderBid> bidsFromResponse(BidResponse bidResponse) {

return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.map(bid -> BidderBid.of(bid, BidType.banner))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.prebid.server.bidder.facebook.proto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;

@Value
@AllArgsConstructor(staticName = "of")
public class ExtImpFacebook {

@JsonProperty("placementId")
String placementId;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package org.prebid.server.spring.config.bidder;

import io.vertx.core.http.HttpClient;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidder.Adapter;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.BidderRequester;
import org.prebid.server.bidder.HttpAdapterConnector;
import org.prebid.server.bidder.HttpAdapterRequester;
import org.prebid.server.bidder.HttpBidderRequester;
import org.prebid.server.bidder.MetaInfo;
import org.prebid.server.bidder.Usersyncer;
import org.prebid.server.bidder.facebook.FacebookAdapter;
Expand All @@ -25,8 +23,6 @@
@Configuration
public class FacebookConfiguration extends BidderConfiguration {

private static final Logger logger = LoggerFactory.getLogger(FacebookConfiguration.class);

private static final String BIDDER_NAME = "audienceNetwork";

@Value("${adapters.facebook.enabled}")
Expand Down Expand Up @@ -72,7 +68,7 @@ protected Usersyncer createUsersyncer() {

@Override
protected Bidder<?> createBidder(MetaInfo metaInfo) {
return new FacebookBidder();
return new FacebookBidder(endpoint, nonSecureEndpoint, platformId);
}

@Override
Expand All @@ -83,6 +79,6 @@ protected Bidder<?> createBidder(MetaInfo metaInfo) {
@Override
protected BidderRequester createBidderRequester(HttpClient httpClient, Bidder<?> bidder, Adapter<?, ?> adapter,
Usersyncer usersyncer, HttpAdapterConnector httpAdapterConnector) {
return new HttpAdapterRequester(BIDDER_NAME, adapter, usersyncer, httpAdapterConnector);
return new HttpBidderRequester<>(bidder, httpClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public void makeHttpRequestsShouldSkipImpAndAddErrorIfRequestContainsNotSupporte
.flatExtracting(BidRequest::getImp)
.isEmpty();
assertThat(result.getErrors()).hasSize(1)
.element(0).extracting(BidderError::getMessage)
.extracting(BidderError::getMessage)
.containsExactly("Appnexus doesn't support audio Imps. Ignoring Imp ID=23");
}

Expand Down Expand Up @@ -532,7 +532,7 @@ public void makeBidsShouldReturnErrorIfBidTypeValueFromResponseIsNotValid() thro
final Result<List<BidderBid>> result = appnexusBidder.makeBids(httpCall, bidRequest);

// then
assertThat(result.getErrors()).hasSize(1).element(0)
assertThat(result.getErrors()).hasSize(1)
.extracting(BidderError::getMessage)
.containsOnly("Unrecognized bid_ad_type in response from appnexus: 42");
assertThat(result.getValue()).isEmpty();
Expand All @@ -554,7 +554,7 @@ public void makeBidsShouldReturnErrorIfBidExtNotDefined() throws IOException {
final Result<List<BidderBid>> result = appnexusBidder.makeBids(httpCall, bidRequest);

// then
assertThat(result.getErrors()).hasSize(1).element(0)
assertThat(result.getErrors()).hasSize(1)
.extracting(BidderError::getMessage)
.containsOnly("bidResponse.bid.ext should be defined for appnexus");
assertThat(result.getValue()).isEmpty();
Expand All @@ -576,7 +576,7 @@ public void makeBidsShouldReturnErrorIfBidExtAppnexusNotDefined() throws IOExcep
final Result<List<BidderBid>> result = appnexusBidder.makeBids(httpCall, bidRequest);

// then
assertThat(result.getErrors()).hasSize(1).element(0)
assertThat(result.getErrors()).hasSize(1)
.extracting(BidderError::getMessage)
.containsOnly("bidResponse.bid.ext.appnexus should be defined");
assertThat(result.getValue()).isEmpty();
Expand All @@ -598,7 +598,7 @@ public void makeBidsShouldReturnErrorIfBidExtAppnexusBidTypeNotDefined() throws
final Result<List<BidderBid>> result = appnexusBidder.makeBids(httpCall, bidRequest);

// then
assertThat(result.getErrors()).hasSize(1).element(0)
assertThat(result.getErrors()).hasSize(1)
.extracting(BidderError::getMessage)
.containsOnly("bidResponse.bid.ext.appnexus.bid_ad_type should be defined");
assertThat(result.getValue()).isEmpty();
Expand Down
Loading