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

Change /vtrack endpoint for updated requirements #466

Merged
merged 1 commit into from
Sep 19, 2019
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
22 changes: 11 additions & 11 deletions src/main/java/org/prebid/server/auction/ExchangeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ public Future<BidResponse> holdAuction(AuctionContext context) {

return storedResponseProcessor.getStoredResponseResult(imps, aliases, timeout)
.map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedResponse))
.compose(impsRequiredRequest -> extractBidderRequests(bidRequest, impsRequiredRequest, requestExt,
uidsCookie, aliases, isGdprEnforced, timeout))
.compose(impsRequiredRequest -> extractBidderRequests(context, impsRequiredRequest, requestExt,
aliases, isGdprEnforced))
.map(bidderRequests ->
updateRequestMetric(bidderRequests, uidsCookie, aliases, publisherId,
requestTypeMetric))
Expand Down Expand Up @@ -252,10 +252,9 @@ private static Map<String, Map<String, BigDecimal>> currencyRates(ExtRequestTarg
* NOTE: the return list will only contain entries for bidders that both have the extension field in at least one
* {@link Imp}, and are known to {@link BidderCatalog} or aliases from bidRequest.ext.prebid.aliases.
*/
private Future<List<BidderRequest>> extractBidderRequests(BidRequest bidRequest, List<Imp> requestedImps,
ExtBidRequest requestExt, UidsCookie uidsCookie,
Map<String, String> aliases, Boolean isGdprEnforced,
Timeout timeout) {
private Future<List<BidderRequest>> extractBidderRequests(AuctionContext context, List<Imp> requestedImps,
ExtBidRequest requestExt, Map<String, String> aliases,
Boolean isGdprEnforced) {
// sanity check: discard imps without extension
final List<Imp> imps = requestedImps.stream()
.filter(imp -> imp.getExt() != null)
Expand All @@ -269,7 +268,7 @@ private Future<List<BidderRequest>> extractBidderRequests(BidRequest bidRequest,
.distinct()
.collect(Collectors.toList());

return makeBidderRequests(bidders, aliases, bidRequest, requestExt, uidsCookie, imps, isGdprEnforced, timeout);
return makeBidderRequests(bidders, context, aliases, requestExt, imps, isGdprEnforced);
}

private static <T> Stream<T> asStream(Iterator<T> iterator) {
Expand Down Expand Up @@ -298,9 +297,10 @@ private boolean isValidBidder(String bidder, Map<String, String> aliases) {
* that don't have first party data allowed.
*/
private Future<List<BidderRequest>> makeBidderRequests(
List<String> bidders, Map<String, String> aliases, BidRequest bidRequest, ExtBidRequest requestExt,
UidsCookie uidsCookie, List<Imp> imps, Boolean isGdprEnforced, Timeout timeout) {
List<String> bidders, AuctionContext context, Map<String, String> aliases,
ExtBidRequest requestExt, List<Imp> imps, Boolean isGdprEnforced) {

final BidRequest bidRequest = context.getBidRequest();
final ExtUser extUser = extUser(bidRequest.getUser());
final Map<String, String> uidsBody = uidsFromBody(extUser);

Expand All @@ -309,11 +309,11 @@ private Future<List<BidderRequest>> makeBidderRequests(
final Map<String, User> bidderToUser = new HashMap<>();
for (String bidder : bidders) {
bidderToUser.put(bidder, prepareUser(bidRequest.getUser(), extUser, bidder, aliases, uidsBody,
uidsCookie, firstPartyDataBidders.contains(bidder)));
context.getUidsCookie(), firstPartyDataBidders.contains(bidder)));
}

return privacyEnforcementService
.mask(bidderToUser, extUser, bidders, aliases, bidRequest, isGdprEnforced, timeout)
.mask(bidderToUser, extUser, bidders, aliases, bidRequest, isGdprEnforced, context.getTimeout())
.map(bidderToPrivacyEnforcementResult -> getBidderRequests(bidderToPrivacyEnforcementResult,
bidRequest, requestExt, imps, firstPartyDataBidders));
}
Expand Down
18 changes: 9 additions & 9 deletions src/main/java/org/prebid/server/cache/CacheService.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -146,30 +147,29 @@ private static Future<BidCacheResponse> failResponse(Throwable exception) {
/**
* Makes cache for Vtrack puts.
* <p>
* Modify vast value in putObjects and stores in the cache.
* Modify VAST value in putObjects and stores in the cache.
* <p>
* The returned result will always have the number of elements equals putObjects list size.
*/
public Future<BidCacheResponse> cachePutObjects(List<PutObject> putObjects, List<String> updatableBidders,
public Future<BidCacheResponse> cachePutObjects(List<PutObject> putObjects, Set<String> biddersAllowingVastUpdate,
String accountId, Timeout timeout) {
final List<PutObject> updatedVtrackPuts = updatePutObjects(putObjects, updatableBidders, accountId);
final int size = updatedVtrackPuts == null ? 0 : updatedVtrackPuts.size();
return makeRequest(BidCacheRequest.of(updatedVtrackPuts), size, timeout);
final List<PutObject> updatedPutObjects = updatePutObjects(putObjects, biddersAllowingVastUpdate, accountId);
return makeRequest(BidCacheRequest.of(updatedPutObjects), updatedPutObjects.size(), timeout);
}

/**
* Modify vast value in putObjects.
* Modify VAST value in putObjects.
*/
private List<PutObject> updatePutObjects(List<PutObject> putObjects, List<String> updatableBidders,
private List<PutObject> updatePutObjects(List<PutObject> putObjects, Set<String> biddersAllowingVastUpdate,
String accountId) {
if (CollectionUtils.isEmpty(updatableBidders)) {
if (CollectionUtils.isEmpty(biddersAllowingVastUpdate)) {
return putObjects;
}

final List<PutObject> updatedPutObjects = new ArrayList<>();
for (PutObject putObject : putObjects) {
final JsonNode value = putObject.getValue();
if (updatableBidders.contains(putObject.getBidder()) && value != null) {
if (biddersAllowingVastUpdate.contains(putObject.getBidder()) && value != null) {
final String updatedVastValue = modifyVastXml(value.asText(), putObject.getBidid(), accountId);
final PutObject updatedPutObject = putObject.toBuilder().value(new TextNode(updatedVastValue)).build();
updatedPutObjects.add(updatedPutObject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ private static Future<Account> handleAccountExceptionOrFallback(Throwable except
if (exception instanceof PreBidException) {
return Future.succeededFuture(Account.builder().id(accountId).eventsEnabled(false).build());
}
logger.warn("Error occurred while fetching account", exception);
return Future.failedFuture(exception);
}

Expand Down Expand Up @@ -132,28 +133,29 @@ private void handleEvent(AsyncResult<Account> async, EventRequest eventRequest,

private void respondWithOkStatus(RoutingContext context, boolean respondWithPixel) {
if (respondWithPixel) {
context.response().putHeader(HttpHeaders.CONTENT_TYPE, trackingPixel.getContentType())
context.response()
.putHeader(HttpHeaders.CONTENT_TYPE, trackingPixel.getContentType())
.end(Buffer.buffer(trackingPixel.getContent()));
} else {
context.response().end();
}
}

private static void respondWithBadStatus(RoutingContext context, String message) {
respondWithError(context, message, HttpResponseStatus.BAD_REQUEST);
respondWithError(context, HttpResponseStatus.BAD_REQUEST, message);
}

private static void respondWithUnauthorized(RoutingContext context, String message) {
respondWithError(context, message, HttpResponseStatus.UNAUTHORIZED);
respondWithError(context, HttpResponseStatus.UNAUTHORIZED, message);
}

private static void respondWithServerError(RoutingContext context, Throwable exception) {
final String message = "Error occurred while fetching account";
logger.warn(message, exception);
respondWithError(context, message, HttpResponseStatus.INTERNAL_SERVER_ERROR);
respondWithError(context, HttpResponseStatus.INTERNAL_SERVER_ERROR, message);
}

private static void respondWithError(RoutingContext context, String message, HttpResponseStatus status) {
private static void respondWithError(RoutingContext context, HttpResponseStatus status, String message) {
context.response().setStatusCode(status.code()).end(message);
}

Expand Down
139 changes: 93 additions & 46 deletions src/main/java/org/prebid/server/handler/VtrackHandler.java
Original file line number Diff line number Diff line change
@@ -1,125 +1,172 @@
package org.prebid.server.handler;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.EncodeException;
import io.vertx.core.json.Json;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.RoutingContext;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidder.BidderCatalog;
import org.prebid.server.cache.CacheService;
import org.prebid.server.cache.proto.request.BidCacheRequest;
import org.prebid.server.cache.proto.request.PutObject;
import org.prebid.server.cache.proto.response.BidCacheResponse;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.execution.Timeout;
import org.prebid.server.execution.TimeoutFactory;
import org.prebid.server.settings.ApplicationSettings;
import org.prebid.server.settings.model.Account;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

public class VtrackHandler implements Handler<RoutingContext> {

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

private static final String ACCOUNT_REQUEST_PARAMETER = "a";
private static final String ACCOUNT_PARAMETER = "a";

private final ApplicationSettings applicationSettings;
private final CacheService cacheService;
private final BidderCatalog bidderCatalog;
private final TimeoutFactory timeoutFactory;
private final long defaultTimeout;

public VtrackHandler(CacheService cacheService, BidderCatalog bidderCatalog, TimeoutFactory timeoutFactory,
long defaultTimeout) {
this.cacheService = Objects.requireNonNull(cacheService);
public VtrackHandler(ApplicationSettings applicationSettings, BidderCatalog bidderCatalog,
CacheService cacheService, TimeoutFactory timeoutFactory, long defaultTimeout) {

this.applicationSettings = Objects.requireNonNull(applicationSettings);
this.bidderCatalog = Objects.requireNonNull(bidderCatalog);
this.cacheService = Objects.requireNonNull(cacheService);
this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
this.defaultTimeout = defaultTimeout;
}

@Override
public void handle(RoutingContext context) {
final String accountId;
final List<PutObject> vtrackPuts;
try {
vtrackPuts = parsePuts(context.getBody());
accountId = accountId(context);
vtrackPuts = vtrackPuts(context);
} catch (IllegalArgumentException e) {
respondWith(context, HttpResponseStatus.BAD_REQUEST.code(), e.getMessage());
respondWithBadRequest(context, e.getMessage());
return;
}

String accountId = null;
final List<String> updatableBidders = biddersWithUpdatableVast(vtrackPuts);
if (!updatableBidders.isEmpty()) {
try {
accountId = accountId(context);
} catch (IllegalArgumentException e) {
respondWith(context, HttpResponseStatus.BAD_REQUEST.code(), e.getMessage());
return;
}
}
final Timeout timeout = timeoutFactory.create(defaultTimeout);
applicationSettings.getAccountById(accountId, timeout)
.recover(exception -> handleAccountExceptionOrFallback(exception, accountId))
.setHandler(async -> handleAccountResult(async, context, vtrackPuts, accountId, timeout));
}

cacheService.cachePutObjects(vtrackPuts, updatableBidders, accountId, timeoutFactory.create(defaultTimeout))
.setHandler(bidCacheResponseResult -> handleCacheResult(context, bidCacheResponseResult));
private static String accountId(RoutingContext context) {
final String accountId = context.request().getParam(ACCOUNT_PARAMETER);
if (StringUtils.isEmpty(accountId)) {
throw new IllegalArgumentException(
String.format("Account '%s' is required query parameter and can't be empty", ACCOUNT_PARAMETER));
}
return accountId;
}

private static List<PutObject> parsePuts(Buffer body) {
private static List<PutObject> vtrackPuts(RoutingContext context) {
final Buffer body = context.getBody();
if (body == null || body.length() == 0) {
throw new IllegalArgumentException("Incoming request has no body");
}

final BidCacheRequest bidCacheRequest;
try {
final List<PutObject> puts = Json.decodeValue(body, BidCacheRequest.class).getPuts();
return puts == null ? Collections.emptyList() : puts;
bidCacheRequest = Json.decodeValue(body, BidCacheRequest.class);
} catch (DecodeException e) {
throw new IllegalArgumentException("Failed to parse /vtrack request body", e);
throw new IllegalArgumentException("Failed to parse request body", e);
}

final List<PutObject> putObjects = ListUtils.emptyIfNull(bidCacheRequest.getPuts());
for (PutObject putObject : putObjects) {
if (StringUtils.isEmpty(putObject.getBidid())) {
throw new IllegalArgumentException("'bidid' is required field and can't be empty");
}
if (StringUtils.isEmpty(putObject.getBidder())) {
throw new IllegalArgumentException("'bidder' is required field and can't be empty");
}
}
return putObjects;
}

private static String accountId(RoutingContext context) {
final String accountId = context.request().getParam(ACCOUNT_REQUEST_PARAMETER);
if (accountId == null) {
throw new IllegalArgumentException("Request must contain 'a'=accountId parameter");
/**
* Returns fallback {@link Account} if account not found or propagate error if fetching failed.
*/
private static Future<Account> handleAccountExceptionOrFallback(Throwable exception, String accountId) {
if (exception instanceof PreBidException) {
return Future.succeededFuture(Account.builder().id(accountId).eventsEnabled(false).build());
}
return Future.failedFuture(exception);
}

private void handleAccountResult(AsyncResult<Account> asyncAccount, RoutingContext context,
List<PutObject> vtrackPuts, String accountId, Timeout timeout) {
if (asyncAccount.failed()) {
respondWithServerError(context, "Error occurred while fetching account", asyncAccount.cause());
} else {
// insert impression tracking if account allows events and bidder allows VAST modification
final Set<String> biddersAllowingVastUpdate = asyncAccount.result().getEventsEnabled()
? biddersAllowingVastUpdate(vtrackPuts)
: Collections.emptySet();

cacheService.cachePutObjects(vtrackPuts, biddersAllowingVastUpdate, accountId, timeout)
.setHandler(asyncCache -> handleCacheResult(asyncCache, context));
}
return accountId;
}

private List<String> biddersWithUpdatableVast(List<PutObject> vtrackPuts) {
/**
* Returns list of bidders that allow VAST XML modification.
*/
private Set<String> biddersAllowingVastUpdate(List<PutObject> vtrackPuts) {
return vtrackPuts.stream()
.map(PutObject::getBidder)
.distinct()
.filter(bidderCatalog::isModifyingVastXmlAllowed)
.collect(Collectors.toList());
.collect(Collectors.toSet());
}

private void handleCacheResult(RoutingContext context, AsyncResult<BidCacheResponse> bidCacheResponseResult) {
if (bidCacheResponseResult.succeeded()) {
respondWithCache(context, bidCacheResponseResult.result());
private static void handleCacheResult(AsyncResult<BidCacheResponse> async, RoutingContext context) {
if (async.failed()) {
respondWithServerError(context, "Error occurred while sending request to cache", async.cause());
} else {
final String message = bidCacheResponseResult.cause().getMessage();
respondWith(context, HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), message);
try {
respondWith(context, HttpResponseStatus.OK, Json.encode(async.result()));
} catch (EncodeException e) {
respondWithServerError(context, "Error occurred while encoding response", e);
}
}
}

private void respondWithCache(RoutingContext context, BidCacheResponse bidCacheResponse) {
try {
respondWith(context, HttpResponseStatus.OK.code(), Json.mapper.writeValueAsString(bidCacheResponse));
} catch (JsonProcessingException e) {
logger.error("/vtrack Critical error when trying to marshal cache response: {0}", e.getMessage());
respondWith(context, HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), e.getMessage());
}
private static void respondWithBadRequest(RoutingContext context, String message) {
respondWith(context, HttpResponseStatus.BAD_REQUEST, message);
}

private static void respondWith(RoutingContext context, int status, String body) {
private static void respondWithServerError(RoutingContext context, String message, Throwable exception) {
logger.warn(message, exception);
respondWith(context, HttpResponseStatus.INTERNAL_SERVER_ERROR,
String.format("%s: %s", message, exception.getMessage()));
}

private static void respondWith(RoutingContext context, HttpResponseStatus status, String body) {
// don't send the response if client has gone
if (context.response().closed()) {
logger.warn("The client already closed connection, response will be skipped");
return;
}
context.response().setStatusCode(status).end(body);
context.response().setStatusCode(status.code()).end(body);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,13 @@ GetuidsHandler getuidsHandler(UidsCookieService uidsCookieService) {

@Bean
VtrackHandler vtrackHandler(
CacheService cacheService,
ApplicationSettings applicationSettings,
BidderCatalog bidderCatalog,
CacheService cacheService,
TimeoutFactory timeoutFactory,
@Value("${vtrack.default-timeout-ms}") int defaultTimeoutMs) {
return new VtrackHandler(cacheService, bidderCatalog, timeoutFactory, defaultTimeoutMs);

return new VtrackHandler(applicationSettings, bidderCatalog, cacheService, timeoutFactory, defaultTimeoutMs);
}

@Bean
Expand Down
Loading