diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 6fff31c6459..e837b90d767 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -62,14 +62,7 @@ import org.prebid.server.floors.PriceFloorAdjuster; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; -import org.prebid.server.hooks.execution.model.ExecutionAction; -import org.prebid.server.hooks.execution.model.ExecutionStatus; -import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; -import org.prebid.server.hooks.execution.model.HookExecutionOutcome; -import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; -import org.prebid.server.hooks.execution.model.Stage; -import org.prebid.server.hooks.execution.model.StageExecutionOutcome; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.json.JacksonMapper; @@ -110,7 +103,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -221,8 +213,7 @@ public Future holdAuction(AuctionContext context) { return processAuctionRequest(context) .compose(this::invokeResponseHooks) .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) - .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) - .map(this::updateHooksMetrics); + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo); } private Future processAuctionRequest(AuctionContext context) { @@ -1378,58 +1369,4 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { case rejected_ipf, generic -> MetricName.unknown_error; }; } - - private AuctionContext updateHooksMetrics(AuctionContext context) { - final EnumMap> stageOutcomes = - context.getHookExecutionContext().getStageOutcomes(); - - final Account account = context.getAccount(); - - stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes)); - - // account might be null if request is rejected by the entrypoint hook - if (account != null) { - stageOutcomes.values().stream() - .flatMap(Collection::stream) - .map(StageExecutionOutcome::getGroups) - .flatMap(Collection::stream) - .map(GroupExecutionOutcome::getHooks) - .flatMap(Collection::stream) - .filter(hookOutcome -> hookOutcome.getAction() != ExecutionAction.no_invocation) - .collect(Collectors.groupingBy( - outcome -> outcome.getHookId().getModuleCode(), - Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) - .forEach((moduleCode, executionTime) -> - metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime)); - } - - return context; - } - - private void updateHooksStageMetrics(Account account, Stage stage, List stageOutcomes) { - stageOutcomes.stream() - .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) - .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) - .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome)); - } - - private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) { - final HookId hookId = hookOutcome.getHookId(); - final ExecutionStatus status = hookOutcome.getStatus(); - final ExecutionAction action = hookOutcome.getAction(); - final String moduleCode = hookId.getModuleCode(); - - metrics.updateHooksMetrics( - moduleCode, - stage, - hookId.getHookImplCode(), - status, - hookOutcome.getExecutionTime(), - action); - - // account might be null if request is rejected by the entrypoint hook - if (account != null) { - metrics.updateAccountHooksMetrics(account, moduleCode, status, action); - } - } } diff --git a/src/main/java/org/prebid/server/auction/HooksMetricsService.java b/src/main/java/org/prebid/server/auction/HooksMetricsService.java new file mode 100644 index 00000000000..0b31d28444f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/HooksMetricsService.java @@ -0,0 +1,81 @@ +package org.prebid.server.auction; + +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.model.Account; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class HooksMetricsService { + + private final Metrics metrics; + + public HooksMetricsService(Metrics metrics) { + this.metrics = Objects.requireNonNull(metrics); + } + + public AuctionContext updateHooksMetrics(AuctionContext context) { + final EnumMap> stageOutcomes = + context.getHookExecutionContext().getStageOutcomes(); + + final Account account = context.getAccount(); + + stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes)); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + stageOutcomes.values().stream() + .flatMap(Collection::stream) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookOutcome -> hookOutcome.getAction() != ExecutionAction.no_invocation) + .collect(Collectors.groupingBy( + outcome -> outcome.getHookId().getModuleCode(), + Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) + .forEach((moduleCode, executionTime) -> + metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime)); + } + + return context; + } + + private void updateHooksStageMetrics(Account account, Stage stage, List stageOutcomes) { + stageOutcomes.stream() + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome)); + } + + private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) { + final HookId hookId = hookOutcome.getHookId(); + final ExecutionStatus status = hookOutcome.getStatus(); + final ExecutionAction action = hookOutcome.getAction(); + final String moduleCode = hookId.getModuleCode(); + + metrics.updateHooksMetrics( + moduleCode, + stage, + hookId.getHookImplCode(), + status, + hookOutcome.getExecutionTime(), + action); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + metrics.updateAccountHooksMetrics(account, moduleCode, status, action); + } + } +} diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 90b20dd4f29..01c4c8a43dc 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -385,6 +385,7 @@ private static HttpRequestContext toHttpRequest(HookStageExecutionResult biddersSupportingCustomTargeting; private final AmpResponsePostProcessor ampResponsePostProcessor; private final HttpInteractionLogger httpInteractionLogger; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; private final double logSamplingRate; @@ -94,12 +101,14 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, ExchangeService exchangeService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, BidderCatalog bidderCatalog, Set biddersSupportingCustomTargeting, AmpResponsePostProcessor ampResponsePostProcessor, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper, double logSamplingRate) { @@ -107,12 +116,14 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, this.exchangeService = Objects.requireNonNull(exchangeService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.biddersSupportingCustomTargeting = Objects.requireNonNull(biddersSupportingCustomTargeting); this.ampResponsePostProcessor = Objects.requireNonNull(ampResponsePostProcessor); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.mapper = Objects.requireNonNull(mapper); this.logSamplingRate = logSamplingRate; } @@ -134,18 +145,25 @@ public void handle(RoutingContext routingContext) { .httpContext(HttpRequestContext.from(routingContext)); ampRequestFactory.fromRequest(routingContext, startTime) - .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) .map(this::updateAppAndNoCookieAndImpsMetrics) - .compose(exchangeService::holdAuction) - .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) - .map(context -> addToEvent(context.getBidResponse(), ampEventBuilder::bidResponse, context)) - .compose(context -> prepareAmpResponse(context, routingContext)) - .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result)) + .map(context -> addContextAndBidResponseToEvent(context, ampEventBuilder, context)) + .compose(context -> prepareSuccessfulResponse(context, routingContext, ampEventBuilder)) + .compose(this::invokeExitpointHooks) + .map(context -> addContextAndBidResponseToEvent(context.getAuctionContext(), ampEventBuilder, context)) .onComplete(responseResult -> handleResult(responseResult, ampEventBuilder, routingContext, startTime)); } + private static R addContextAndBidResponseToEvent(AuctionContext context, + AmpEvent.AmpEventBuilder ampEventBuilder, + R result) { + + ampEventBuilder.auctionContext(context); + ampEventBuilder.bidResponse(context.getBidResponse()); + return result; + } + private static R addToEvent(T field, Consumer consumer, R result) { consumer.accept(field); return result; @@ -166,8 +184,44 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context return context; } + private Future prepareSuccessfulResponse(AuctionContext auctionContext, + RoutingContext routingContext, + AmpEvent.AmpEventBuilder ampEventBuilder) { + + final String origin = originFrom(routingContext); + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext, origin) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + + return prepareAmpResponse(auctionContext, routingContext) + .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result)) + .map(result -> RawResponseContext.builder() + .responseBody(mapper.encodeToString(result.getLeft())) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build()); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); + } + private Future> prepareAmpResponse(AuctionContext context, RoutingContext routingContext) { + final BidRequest bidRequest = context.getBidRequest(); final BidResponse bidResponse = context.getBidResponse(); final AmpResponse ampResponse = toAmpResponse(bidResponse); @@ -271,12 +325,13 @@ private static ExtAmpVideoResponse extResponseFrom(BidResponse bidResponse) { : null; } - private void handleResult(AsyncResult> responseResult, + private void handleResult(AsyncResult responseResult, AmpEvent.AmpEventBuilder ampEventBuilder, RoutingContext routingContext, long startTime) { final boolean responseSucceeded = responseResult.succeeded(); + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; final MetricName metricRequestStatus; final List errorMessages; @@ -287,16 +342,22 @@ private void handleResult(AsyncResult> respo ampEventBuilder.origin(origin); final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext, origin); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(responseResult.result().getLeft()); + + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext, origin) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException invalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -355,8 +416,7 @@ private void handleResult(AsyncResult> respo final int statusCode = status.code(); final AmpEvent ampEvent = ampEventBuilder.status(statusCode).errors(errorMessages).build(); - - final AuctionContext auctionContext = responseSucceeded ? responseResult.result().getRight() : null; + final AuctionContext auctionContext = ampEvent.getAuctionContext(); final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null; final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty(); @@ -406,8 +466,8 @@ private void handleResponseException(Throwable exception) { metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext, String origin) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext, String origin) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -419,10 +479,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext, Stri // Add AMP headers responseHeaders.add("AMP-Access-Control-Allow-Source-Origin", origin) .add("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"); - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - final MultiMap headers = response.headers(); - headers.add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java index b8664bc75fd..e0dbe2ea4e1 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java @@ -12,7 +12,10 @@ import io.vertx.ext.web.RoutingContext; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; @@ -22,6 +25,8 @@ import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.HttpInteractionLogger; @@ -55,9 +60,11 @@ public class AuctionHandler implements ApplicationResource { private final SkippedAuctionService skippedAuctionService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; + private final HooksMetricsService hooksMetricsService; private final Clock clock; private final HttpInteractionLogger httpInteractionLogger; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; public AuctionHandler(double logSamplingRate, @@ -66,9 +73,11 @@ public AuctionHandler(double logSamplingRate, SkippedAuctionService skippedAuctionService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { this.logSamplingRate = logSamplingRate; @@ -77,9 +86,11 @@ public AuctionHandler(double logSamplingRate, this.skippedAuctionService = Objects.requireNonNull(skippedAuctionService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.mapper = Objects.requireNonNull(mapper); } @@ -102,7 +113,21 @@ public void handle(RoutingContext routingContext) { auctionRequestFactory.parseRequest(routingContext, startTime) .compose(auctionContext -> skippedAuctionService.skipAuction(auctionContext) .recover(throwable -> holdAuction(auctionEventBuilder, auctionContext))) - .onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime)); + .map(context -> addContextAndBidResponseToEvent(context, auctionEventBuilder, context)) + .map(context -> prepareSuccessfulResponse(context, routingContext)) + .compose(this::invokeExitpointHooks) + .map(context -> addContextAndBidResponseToEvent( + context.getAuctionContext(), auctionEventBuilder, context)) + .onComplete(result -> handleResult(result, auctionEventBuilder, routingContext, startTime)); + } + + private static R addContextAndBidResponseToEvent(AuctionContext context, + AuctionEvent.AuctionEventBuilder auctionEventBuilder, + R result) { + + auctionEventBuilder.auctionContext(context); + auctionEventBuilder.bidResponse(context.getBidResponse()); + return result; } private Future holdAuction(AuctionEvent.AuctionEventBuilder auctionEventBuilder, @@ -110,14 +135,9 @@ private Future holdAuction(AuctionEvent.AuctionEventBuilder auct return auctionRequestFactory.enrichAuctionContext(auctionContext) .map(this::updateAppAndNoCookieAndImpsMetrics) - // In case of holdAuction Exception and auctionContext is not present below .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - - .compose(exchangeService::holdAuction) - // populate event with updated context - .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - .map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context)); + .compose(exchangeService::holdAuction); } private static R addToEvent(T field, Consumer consumer, R result) { @@ -142,14 +162,53 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context return context; } - private void handleResult(AsyncResult responseResult, + private RawResponseContext prepareSuccessfulResponse(AuctionContext auctionContext, RoutingContext routingContext) { + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + + return RawResponseContext.builder() + .responseBody(mapper.encodeToString(auctionContext.getBidResponse())) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build(); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + + if (auctionContext.isAuctionSkipped()) { + return Future.succeededFuture(auctionContext) + .map(hooksMetricsService::updateHooksMetrics) + .map(rawResponseContext); + } + + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); + } + + private void handleResult(AsyncResult responseResult, AuctionEvent.AuctionEventBuilder auctionEventBuilder, RoutingContext routingContext, long startTime) { final boolean responseSucceeded = responseResult.succeeded(); - final AuctionContext auctionContext = responseSucceeded ? responseResult.result() : null; + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; + final AuctionContext auctionContext = rawResponseContext != null + ? rawResponseContext.getAuctionContext() + : null; final boolean isAuctionSkipped = responseSucceeded && auctionContext.isAuctionSkipped(); final MetricName requestType = responseSucceeded ? auctionContext.getRequestTypeMetric() @@ -161,16 +220,22 @@ private void handleResult(AsyncResult responseResult, final String body; final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(responseResult.result().getBidResponse()); + + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException invalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -263,8 +328,8 @@ private void handleResponseException(Throwable throwable, MetricName requestType metrics.updateRequestTypeMetric(requestType, MetricName.networkerr); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -272,10 +337,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) { responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1"); } - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - response.headers() - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java new file mode 100644 index 00000000000..5fe80a55c1d --- /dev/null +++ b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java @@ -0,0 +1,18 @@ +package org.prebid.server.handler.openrtb2; + +import io.vertx.core.MultiMap; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.auction.model.AuctionContext; + +@Value(staticConstructor = "of") +@Builder(toBuilder = true) +public class RawResponseContext { + + AuctionContext auctionContext; + + String responseBody; + + MultiMap responseHeaders; + +} diff --git a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java index d5957c15aa7..0bb31bab72b 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java @@ -1,15 +1,20 @@ package org.prebid.server.handler.openrtb2; +import com.iab.openrtb.request.video.PodError; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; +import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; import org.prebid.server.analytics.model.VideoEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.CachedDebugLog; @@ -18,6 +23,8 @@ import org.prebid.server.cache.CoreCacheService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; @@ -56,17 +63,22 @@ public class VideoHandler implements ApplicationResource { private final CoreCacheService coreCacheService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; + private final HooksMetricsService hooksMetricsService; private final Clock clock; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; public VideoHandler(VideoRequestFactory videoRequestFactory, VideoResponseFactory videoResponseFactory, ExchangeService exchangeService, - CoreCacheService coreCacheService, AnalyticsReporterDelegator analyticsDelegator, + CoreCacheService coreCacheService, + AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { this.videoRequestFactory = Objects.requireNonNull(videoRequestFactory); @@ -75,8 +87,10 @@ public VideoHandler(VideoRequestFactory videoRequestFactory, this.coreCacheService = Objects.requireNonNull(coreCacheService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.mapper = Objects.requireNonNull(mapper); } @@ -106,13 +120,55 @@ public void handle(RoutingContext routingContext) { .map(contextToErrors -> addToEvent(contextToErrors.getData(), videoEventBuilder::auctionContext, contextToErrors)) - .map(result -> videoResponseFactory.toVideoResponse( - result.getData(), result.getData().getBidResponse(), - result.getPodErrors())) + .compose(contextToErrors -> + prepareSuccessfulResponse(contextToErrors, routingContext, videoEventBuilder) + .compose(this::invokeExitpointHooks) + .compose(context -> toVideoResponse(context.getAuctionContext(), contextToErrors.getPodErrors()) + .map(videoResponse -> + addToEvent(videoResponse, videoEventBuilder::bidResponse, context))) + .map(context -> + addToEvent(context.getAuctionContext(), videoEventBuilder::auctionContext, context))) + .onComplete(result -> handleResult(result, videoEventBuilder, routingContext, startTime)); + } + + private Future prepareSuccessfulResponse(WithPodErrors context, + RoutingContext routingContext, + VideoEvent.VideoEventBuilder videoEventBuilder) { + + final AuctionContext auctionContext = context.getData(); + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return toVideoResponse(auctionContext, context.getPodErrors()) .map(videoResponse -> addToEvent(videoResponse, videoEventBuilder::bidResponse, videoResponse)) - .onComplete(responseResult -> handleResult(responseResult, videoEventBuilder, routingContext, - startTime)); + .map(videoResponse -> RawResponseContext.builder() + .responseBody(mapper.encodeToString(videoResponse)) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build()); + } + + private Future toVideoResponse(AuctionContext auctionContext, List podErrors) { + return Future.succeededFuture( + videoResponseFactory.toVideoResponse(auctionContext, auctionContext.getBidResponse(), podErrors)); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); } private static R addToEvent(T field, Consumer consumer, R result) { @@ -120,7 +176,7 @@ private static R addToEvent(T field, Consumer consumer, R result) { return result; } - private void handleResult(AsyncResult responseResult, + private void handleResult(AsyncResult responseResult, VideoEvent.VideoEventBuilder videoEventBuilder, RoutingContext routingContext, long startTime) { @@ -130,19 +186,25 @@ private void handleResult(AsyncResult responseResult, final List errorMessages; final HttpResponseStatus status; final String body; - final VideoResponse videoResponse = responseSucceeded ? responseResult.result() : null; + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(videoResponse); + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -240,8 +302,8 @@ private void handleResponseException(Throwable throwable) { metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -249,10 +311,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) { responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1"); } - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - response.headers() - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java index 048b42bb0aa..c5f3c81894a 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -5,6 +5,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.Vertx; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; @@ -38,6 +39,7 @@ import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; @@ -48,6 +50,7 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -70,6 +73,7 @@ public class HookStageExecutor { private static final String ENTITY_HTTP_REQUEST = "http-request"; + private static final String ENTITY_HTTP_RESPONSE = "http-response"; private static final String ENTITY_AUCTION_REQUEST = "auction-request"; private static final String ENTITY_AUCTION_RESPONSE = "auction-response"; private static final String ENTITY_ALL_PROCESSED_BID_RESPONSES = "all-processed-bid-responses"; @@ -313,6 +317,22 @@ public Future> executeAuctionRe .execute(); } + public Future> executeExitpointStage(MultiMap responseHeaders, + String responseBody, + AuctionContext auctionContext) { + + final Account account = ObjectUtils.defaultIfNull(auctionContext.getAccount(), EMPTY_ACCOUNT); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final Endpoint endpoint = context.getEndpoint(); + + return stageExecutor(StageWithHookType.EXITPOINT, ENTITY_HTTP_RESPONSE, context, account, endpoint) + .withInitialPayload(ExitpointPayloadImpl.of(responseHeaders, responseBody)) + .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) + .withRejectAllowed(false) + .execute(); + } + private StageExecutor stageExecutor( StageWithHookType> stage, String entity, diff --git a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java index 47896d8c9ab..bb7c151ed6f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java @@ -33,5 +33,7 @@ public enum Stage { @JsonProperty("auction-response") @JsonAlias("auction_response") - auction_response + auction_response, + + exitpoint } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java index f8738d2c2db..961450a3c3f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java @@ -10,6 +10,7 @@ import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.exitpoint.ExitpointHook; public interface StageWithHookType> { @@ -29,6 +30,8 @@ public interface StageWithHookType(Stage.all_processed_bid_responses, AllProcessedBidResponsesHook.class); StageWithHookType AUCTION_RESPONSE = new StageWithHookTypeImpl<>(Stage.auction_response, AuctionResponseHook.class); + StageWithHookType EXITPOINT = + new StageWithHookTypeImpl<>(Stage.exitpoint, ExitpointHook.class); Stage stage(); @@ -44,6 +47,7 @@ public interface StageWithHookType ALL_PROCESSED_BID_RESPONSES; case processed_bidder_response -> PROCESSED_BIDDER_RESPONSE; case auction_response -> AUCTION_RESPONSE; + case exitpoint -> EXITPOINT; }; } } diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java new file mode 100644 index 00000000000..d57080f6b90 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.execution.v1.exitpoint; + +import io.vertx.core.MultiMap; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ExitpointPayloadImpl implements ExitpointPayload { + + MultiMap responseHeaders; + + String responseBody; +} diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java new file mode 100644 index 00000000000..02e36af17a5 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.exitpoint; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +public interface ExitpointHook extends Hook { +} diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java new file mode 100644 index 00000000000..ae596949fa0 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.v1.exitpoint; + +import io.vertx.core.MultiMap; + +public interface ExitpointPayload { + + MultiMap responseHeaders(); + + String responseBody(); +} diff --git a/src/main/java/org/prebid/server/metric/StageMetrics.java b/src/main/java/org/prebid/server/metric/StageMetrics.java index 1348266b0f7..025a47368bd 100644 --- a/src/main/java/org/prebid/server/metric/StageMetrics.java +++ b/src/main/java/org/prebid/server/metric/StageMetrics.java @@ -22,6 +22,7 @@ class StageMetrics extends UpdatableMetrics { STAGE_TO_METRIC.put(Stage.processed_bidder_response, "procbidresponse"); STAGE_TO_METRIC.put(Stage.auction_response, "auctionresponse"); STAGE_TO_METRIC.put(Stage.all_processed_bid_responses, "allprocbidresponses"); + STAGE_TO_METRIC.put(Stage.exitpoint, "exitpoint"); } private static final String UNKNOWN_STAGE = "unknown"; diff --git a/src/main/java/org/prebid/server/model/HttpRequestContext.java b/src/main/java/org/prebid/server/model/HttpRequestContext.java index efa07ed621e..9237e9b1803 100644 --- a/src/main/java/org/prebid/server/model/HttpRequestContext.java +++ b/src/main/java/org/prebid/server/model/HttpRequestContext.java @@ -2,6 +2,7 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import lombok.Builder; import lombok.Value; @@ -16,6 +17,8 @@ @Value public class HttpRequestContext { + HttpMethod httpMethod; + String absoluteUri; CaseInsensitiveMultiMap queryParams; @@ -30,6 +33,7 @@ public class HttpRequestContext { public static HttpRequestContext from(RoutingContext context) { return HttpRequestContext.builder() + .httpMethod(context.request().method()) .absoluteUri(context.request().uri()) .queryParams(CaseInsensitiveMultiMap.builder().addAll(toMap(context.request().params())).build()) .headers(headers(context)) diff --git a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java index 21d145bf826..daba3fda594 100644 --- a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java @@ -1,5 +1,6 @@ package org.prebid.server.spring.config.metrics; +import org.prebid.server.auction.HooksMetricsService; import org.slf4j.LoggerFactory; import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.ConsoleReporter; @@ -134,6 +135,11 @@ AccountMetricsVerbosityResolver accountMetricsVerbosity(AccountsProperties accou accountsProperties.getDetailedVerbosity()); } + @Bean + HooksMetricsService hooksMetricsService(Metrics metrics) { + return new HooksMetricsService(metrics); + } + @Component @ConfigurationProperties(prefix = "metrics.graphite") @ConditionalOnProperty(prefix = "metrics.graphite", name = "enabled", havingValue = "true") diff --git a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java index 8c93941c679..b7c9eb405da 100644 --- a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java @@ -15,6 +15,7 @@ import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.gpp.CookieSyncGppService; @@ -49,6 +50,7 @@ import org.prebid.server.handler.openrtb2.VideoHandler; import org.prebid.server.health.HealthChecker; import org.prebid.server.health.PeriodicHealthChecker; +import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.Metrics; @@ -210,9 +212,11 @@ org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler( AuctionRequestFactory auctionRequestFactory, AnalyticsReporterDelegator analyticsReporter, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { return new org.prebid.server.handler.openrtb2.AuctionHandler( @@ -222,9 +226,11 @@ org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler( skippedAuctionService, analyticsReporter, metrics, + hooksMetricsService, clock, httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, mapper); } @@ -234,12 +240,14 @@ AmpHandler openrtbAmpHandler( ExchangeService exchangeService, AnalyticsReporterDelegator analyticsReporter, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, BidderCatalog bidderCatalog, AmpProperties ampProperties, AmpResponsePostProcessor ampResponsePostProcessor, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { return new AmpHandler( @@ -247,12 +255,14 @@ AmpHandler openrtbAmpHandler( exchangeService, analyticsReporter, metrics, + hooksMetricsService, clock, bidderCatalog, ampProperties.getCustomTargetingSet(), ampResponsePostProcessor, httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, mapper, logSamplingRate); } @@ -265,8 +275,10 @@ VideoHandler openrtbVideoHandler( CoreCacheService coreCacheService, AnalyticsReporterDelegator analyticsReporter, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { return new VideoHandler( @@ -275,8 +287,10 @@ VideoHandler openrtbVideoHandler( exchangeService, coreCacheService, analyticsReporter, metrics, + hooksMetricsService, clock, prebidVersionProvider, + hookStageExecutor, mapper); } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 8e538acafac..8c1c20ac35e 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -188,7 +188,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; @@ -197,7 +196,6 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -3567,124 +3565,6 @@ public void shouldReturnBidResponseWithWarningWhenAnalyticsTagsDisabledAndReques ExtBidderError.of(999, "analytics.options.enableclientdetails not enabled for account")); } - @Test - public void shouldIncrementHooksGlobalMetrics() { - // given - final AuctionContext auctionContext = AuctionContext.builder() - .hookExecutionContext(HookExecutionContext.of( - Endpoint.openrtb2_auction, - stageOutcomes(givenAppliedToImpl(identity())))) - .debugContext(DebugContext.empty()) - .requestRejected(true) - .build(); - - // when - target.holdAuction(auctionContext); - - // then - verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); - verify(metrics).updateHooksMetrics( - eq("module1"), - eq(Stage.entrypoint), - eq("hook1"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.update)); - verify(metrics).updateHooksMetrics( - eq("module1"), - eq(Stage.entrypoint), - eq("hook2"), - eq(ExecutionStatus.invocation_failure), - eq(6L), - isNull()); - verify(metrics).updateHooksMetrics( - eq("module1"), - eq(Stage.entrypoint), - eq("hook2"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.no_action)); - verify(metrics).updateHooksMetrics( - eq("module2"), - eq(Stage.entrypoint), - eq("hook1"), - eq(ExecutionStatus.timeout), - eq(6L), - isNull()); - verify(metrics).updateHooksMetrics( - eq("module3"), - eq(Stage.auction_response), - eq("hook1"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.update)); - verify(metrics).updateHooksMetrics( - eq("module3"), - eq(Stage.auction_response), - eq("hook2"), - eq(ExecutionStatus.success), - eq(4L), - eq(ExecutionAction.no_action)); - verify(metrics, never()).updateAccountHooksMetrics(any(), any(), any(), any()); - verify(metrics, never()).updateAccountModuleDurationMetric(any(), any(), any()); - } - - @Test - public void shouldIncrementHooksGlobalAndAccountMetrics() { - // given - given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean())) - .willReturn(Future.succeededFuture(givenSeatBid(emptyList()))); - - final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2))); - final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder() - .hookExecutionContext(HookExecutionContext.of( - Endpoint.openrtb2_auction, - stageOutcomes(givenAppliedToImpl(identity())))) - .debugContext(DebugContext.empty()) - .build(); - - // when - target.holdAuction(auctionContext); - - // then - verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); - verify(metrics, times(6)).updateAccountHooksMetrics(any(), any(), any(), any()); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module1"), - eq(ExecutionStatus.success), - eq(ExecutionAction.update)); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module1"), - eq(ExecutionStatus.invocation_failure), - isNull()); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module1"), - eq(ExecutionStatus.success), - eq(ExecutionAction.no_action)); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module2"), - eq(ExecutionStatus.timeout), - isNull()); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module3"), - eq(ExecutionStatus.success), - eq(ExecutionAction.update)); - verify(metrics).updateAccountHooksMetrics( - any(), - eq("module3"), - eq(ExecutionStatus.success), - eq(ExecutionAction.no_action)); - verify(metrics, times(3)).updateAccountModuleDurationMetric(any(), any(), any()); - verify(metrics).updateAccountModuleDurationMetric(any(), eq("module1"), eq(14L)); - verify(metrics).updateAccountModuleDurationMetric(any(), eq("module2"), eq(6L)); - verify(metrics).updateAccountModuleDurationMetric(any(), eq("module3"), eq(8L)); - } - @Test public void shouldProperPopulateImpExtPrebidEvenIfInExtImpPrebidContainNotCorrectField() { // given diff --git a/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java b/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java new file mode 100644 index 00000000000..4d90cc637d7 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java @@ -0,0 +1,260 @@ +package org.prebid.server.auction; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountEventsConfig; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class HooksMetricsServiceTest extends VertxTest { + + @Mock + private Metrics metrics; + + private HooksMetricsService target; + + @BeforeEach + public void before() { + target = new HooksMetricsService(metrics); + } + + @Test + public void shouldIncrementHooksGlobalMetrics() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_auction, + stageOutcomes(givenAppliedToImpl()))) + .debugContext(DebugContext.empty()) + .requestRejected(true) + .build(); + + // when + target.updateHooksMetrics(auctionContext); + + // then + verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); + verify(metrics).updateHooksMetrics( + eq("module1"), + eq(Stage.entrypoint), + eq("hook1"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.update)); + verify(metrics).updateHooksMetrics( + eq("module1"), + eq(Stage.entrypoint), + eq("hook2"), + eq(ExecutionStatus.invocation_failure), + eq(6L), + isNull()); + verify(metrics).updateHooksMetrics( + eq("module1"), + eq(Stage.entrypoint), + eq("hook2"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.no_action)); + verify(metrics).updateHooksMetrics( + eq("module2"), + eq(Stage.entrypoint), + eq("hook1"), + eq(ExecutionStatus.timeout), + eq(6L), + isNull()); + verify(metrics).updateHooksMetrics( + eq("module3"), + eq(Stage.auction_response), + eq("hook1"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.update)); + verify(metrics).updateHooksMetrics( + eq("module3"), + eq(Stage.auction_response), + eq("hook2"), + eq(ExecutionStatus.success), + eq(4L), + eq(ExecutionAction.no_action)); + verify(metrics, never()).updateAccountHooksMetrics(any(), any(), any(), any()); + verify(metrics, never()).updateAccountModuleDurationMetric(any(), any(), any()); + } + + @Test + public void shouldIncrementHooksGlobalAndAccountMetrics() { + // given + final AuctionContext auctionContext = AuctionContext.builder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_auction, + stageOutcomes(givenAppliedToImpl()))) + .debugContext(DebugContext.empty()) + .requestRejected(true) + .account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .build()) + .build()) + .build(); + + // when + target.updateHooksMetrics(auctionContext); + + // then + verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any()); + verify(metrics, times(6)).updateAccountHooksMetrics(any(), any(), any(), any()); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module1"), + eq(ExecutionStatus.success), + eq(ExecutionAction.update)); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module1"), + eq(ExecutionStatus.invocation_failure), + isNull()); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module1"), + eq(ExecutionStatus.success), + eq(ExecutionAction.no_action)); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module2"), + eq(ExecutionStatus.timeout), + isNull()); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module3"), + eq(ExecutionStatus.success), + eq(ExecutionAction.update)); + verify(metrics).updateAccountHooksMetrics( + any(), + eq("module3"), + eq(ExecutionStatus.success), + eq(ExecutionAction.no_action)); + verify(metrics, times(3)).updateAccountModuleDurationMetric(any(), any(), any()); + verify(metrics).updateAccountModuleDurationMetric(any(), eq("module1"), eq(14L)); + verify(metrics).updateAccountModuleDurationMetric(any(), eq("module2"), eq(6L)); + verify(metrics).updateAccountModuleDurationMetric(any(), eq("module3"), eq(8L)); + } + + private static AppliedToImpl givenAppliedToImpl() { + return AppliedToImpl.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static EnumMap> stageOutcomes(AppliedToImpl appliedToImp) { + final Map> stageOutcomes = new HashMap<>(); + + stageOutcomes.put(Stage.entrypoint, singletonList(StageExecutionOutcome.of( + "http-request", + asList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("Message 1-1") + .action(ExecutionAction.update) + .errors(asList("error message 1-1 1", "error message 1-1 2")) + .warnings(asList("warning message 1-1 1", "warning message 1-1 2")) + .debugMessages(asList("debug message 1-1 1", "debug message 1-1 2")) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + appliedToImp)))))) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(6L) + .status(ExecutionStatus.invocation_failure) + .message("Message 1-2") + .errors(asList("error message 1-2 1", "error message 1-2 2")) + .warnings(asList("warning message 1-2 1", "warning message 1-2 2")) + .build())), + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("Message 1-2") + .action(ExecutionAction.no_action) + .errors(asList("error message 1-2 3", "error message 1-2 4")) + .warnings(asList("warning message 1-2 3", "warning message 1-2 4")) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module2", "hook1")) + .executionTime(6L) + .status(ExecutionStatus.timeout) + .message("Message 2-1") + .errors(asList("error message 2-1 1", "error message 2-1 2")) + .warnings(asList("warning message 2-1 1", "warning message 2-1 2")) + .build())))))); + + stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of( + "auction-response", + singletonList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module3", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("Message 3-1") + .action(ExecutionAction.update) + .errors(asList("error message 3-1 1", "error message 3-1 2")) + .warnings(asList("warning message 3-1 1", "warning message 3-1 2")) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module3", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .action(ExecutionAction.no_action) + .build())))))); + + return new EnumMap<>(stageOutcomes); + } + +} diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index 5b4c0bd72e5..23d485e99a0 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -14,6 +14,7 @@ import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.net.impl.SocketAddressImpl; import io.vertx.ext.web.RoutingContext; @@ -1079,6 +1080,7 @@ public void executeEntrypointHooksShouldReturnExpectedHttpRequest() { given(httpServerRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); given(httpServerRequest.absoluteURI()).willReturn("absoluteUri"); + given(httpServerRequest.method()).willReturn(HttpMethod.POST); given(httpServerRequest.scheme()).willReturn("https"); given(httpServerRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host")); @@ -1107,6 +1109,7 @@ public void executeEntrypointHooksShouldReturnExpectedHttpRequest() { // then final HttpRequestContext httpRequest = result.result(); assertThat(httpRequest.getAbsoluteUri()).isEqualTo("absoluteUri"); + assertThat(httpRequest.getHttpMethod()).isEqualTo(HttpMethod.POST); assertThat(httpRequest.getQueryParams()).isSameAs(updatedQueryParam); assertThat(httpRequest.getHeaders()).isSameAs(headerParams); assertThat(httpRequest.getBody()).isEqualTo("{\"app\":{\"bundle\":\"org.company.application\"}}"); diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java index e22a9178e84..aee1397e4c9 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -27,6 +27,7 @@ import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.debug.DebugContext; @@ -41,34 +42,67 @@ import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; import org.prebid.server.proto.openrtb.ext.response.ExtModules; import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; import java.time.Clock; import java.time.Instant; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.function.UnaryOperator; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; -import static java.util.function.Function.identity; +import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; @@ -104,8 +138,15 @@ public class AmpHandlerTest extends VertxTest { private Clock clock; @Mock private HttpInteractionLogger httpInteractionLogger; + @Mock + private PrebidVersionProvider prebidVersionProvider; + @Mock(strictness = LENIENT) + private HooksMetricsService hooksMetricsService; + @Mock(strictness = LENIENT) + private HookStageExecutor hookStageExecutor; + + private AmpHandler target; - private AmpHandler ampHandler; @Mock private RoutingContext routingContext; @Mock(strictness = LENIENT) @@ -114,8 +155,6 @@ public class AmpHandlerTest extends VertxTest { private HttpServerResponse httpResponse; @Mock(strictness = LENIENT) private UidsCookie uidsCookie; - @Mock - private PrebidVersionProvider prebidVersionProvider; private Timeout timeout; @@ -139,19 +178,28 @@ public void setUp() { given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( + false, + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))))); + + given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0)); + timeout = new TimeoutFactory(clock).create(2000L); - ampHandler = new AmpHandler( + target = new AmpHandler( ampRequestFactory, exchangeService, analyticsReporterDelegator, metrics, + hooksMetricsService, clock, bidderCatalog, singleton("bidder1"), new AmpResponsePostProcessor.NoOpAmpResponsePostProcessor(), httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, jacksonMapper, 0); } @@ -165,7 +213,7 @@ public void shouldSetRequestTypeMetricToAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionContext auctionContext = captureAuctionContext(); @@ -181,7 +229,7 @@ public void shouldUseTimeoutFromAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -203,7 +251,7 @@ public void shouldAddPrebidVersionResponseHeader() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -225,7 +273,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -245,7 +293,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() { given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -263,7 +311,7 @@ public void shouldRespondWithBadRequestIfRequestIsInvalid() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -276,6 +324,7 @@ public void shouldRespondWithBadRequestIfRequestIsInvalid() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Invalid request format: Request is invalid")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -285,7 +334,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedAccount() { .willReturn(Future.failedFuture(new BlocklistedAccountException("Blocklisted account"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -297,6 +346,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedAccount() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Blocklisted: Blocklisted account")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -306,7 +356,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedApp() { .willReturn(Future.failedFuture(new BlocklistedAppException("Blocklisted app"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -318,6 +368,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedApp() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Blocklisted: Blocklisted app")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -327,7 +378,7 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", null))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -339,6 +390,7 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Account id is not provided")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -348,7 +400,7 @@ public void shouldRespondWithBadRequestOnInvalidAccountConfigException() { .willReturn(Future.failedFuture(new InvalidAccountConfigException("Account is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); @@ -361,6 +413,7 @@ public void shouldRespondWithBadRequestOnInvalidAccountConfigException() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Invalid account configuration: Account is invalid")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -373,7 +426,7 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { .willThrow(new RuntimeException("Unexpected exception")); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); @@ -384,6 +437,7 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -398,7 +452,7 @@ public void shouldRespondWithInternalServerErrorIfCannotExtractBidTargeting() { givenHoldAuction(givenBidResponse(ext)); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); @@ -410,6 +464,7 @@ public void shouldRespondWithInternalServerErrorIfCannotExtractBidTargeting() { tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end( startsWith("Critical error while running the auction: Critical error while unpacking AMP targets:")); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -421,10 +476,11 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).end(anyString()); + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -442,7 +498,7 @@ public void shouldRespondWithExpectedResponse() { givenHoldAuction(givenBidResponse(mapper.valueToTree(extPrebid))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()).hasSize(4) @@ -453,6 +509,68 @@ public void shouldRespondWithExpectedResponse() { tuple("Content-Type", "application/json"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithExpectedResponseWhenExitpointHookChangesResponseAndHeaders() { + // given + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + + final Map targeting = new HashMap<>(); + targeting.put("key1", "value1"); + targeting.put("hb_cache_id_bidder1", "value2"); + final ExtPrebid extPrebid = ExtPrebid.of( + ExtBidPrebid.builder().targeting(targeting).build(), + null); + givenHoldAuction(givenBidResponse(mapper.valueToTree(extPrebid))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"targeting\":{\"new-key\":\"new-value\"}}")))); + + // when + target.handle(routingContext); + + // then + assertThat(httpResponse.headers()).hasSize(1) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly(tuple("New-Header", "New-Header-Value")); + verify(httpResponse).end(eq("{\"targeting\":{\"new-key\":\"new-value\"}}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -485,11 +603,18 @@ public void shouldRespondWithCustomTargetingIncluded() { willReturn(bidder).given(bidderCatalog).bidderByName(anyString()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq("{\"targeting\":{\"key1\":\"value1\",\"rpfl_11078\":\"15_tier0030\"," + "\"hb_cache_id_bidder1\":\"value2\"}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{\"key1\":\"value1\",\"rpfl_11078\":\"15_tier0030\"," + + "\"hb_cache_id_bidder1\":\"value2\"}}"), + any()); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -524,10 +649,15 @@ public void shouldRespondWithAdditionalTargetingIncludedWhenSeatBidExists() { .build()); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -547,10 +677,15 @@ public void shouldRespondWithAdditionalTargetingIncludedWhenNoSeatBidExists() { givenHoldAuction(givenBidResponseWithExt(ExtBidResponse.builder().prebid(extBidResponsePrebid).build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -569,12 +704,19 @@ public void shouldRespondWithDebugInfoIncludedIfTestFlagIsTrue() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq( "{\"targeting\":{}," + "\"ext\":{\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000}}}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{}," + + "\"ext\":{\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000}}}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); + } @Test @@ -597,7 +739,7 @@ public void shouldRespondWithHooksDebugAndTraceOutput() { .build())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).end(eq( @@ -606,6 +748,15 @@ public void shouldRespondWithHooksDebugAndTraceOutput() { + "\"errors\":{\"module1\":{\"hook1\":[\"error1\"]}}," + "\"warnings\":{\"module1\":{\"hook1\":[\"warning1\"]}}," + "\"trace\":{\"executiontimemillis\":2,\"stages\":[]}}}}}")); + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"targeting\":{}," + + "\"ext\":{\"prebid\":{\"modules\":{" + + "\"errors\":{\"module1\":{\"hook1\":[\"error1\"]}}," + + "\"warnings\":{\"module1\":{\"hook1\":[\"warning1\"]}}," + + "\"trace\":{\"executiontimemillis\":2,\"stages\":[]}}}}}"), + any()); + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -618,7 +769,7 @@ public void shouldIncrementOkAmpRequestMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.ok)); @@ -634,7 +785,7 @@ public void shouldIncrementAppRequestMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); @@ -655,7 +806,7 @@ public void shouldIncrementNoCookieMetrics() { + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7"); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); @@ -672,7 +823,7 @@ public void shouldIncrementImpsRequestedMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); @@ -690,7 +841,7 @@ public void shouldIncrementImpsTypesMetrics() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateImpTypesMetrics(same(imps)); @@ -703,7 +854,7 @@ public void shouldIncrementBadinputAmpRequestMetrics() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.badinput)); @@ -716,7 +867,7 @@ public void shouldIncrementErrAmpRequestMetrics() { .willReturn(Future.failedFuture(new RuntimeException())); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.err)); @@ -741,7 +892,7 @@ public void shouldUpdateRequestTimeMetric() { }); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L)); @@ -754,7 +905,7 @@ public void shouldNotUpdateRequestTimeMetricIfRequestFails() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).endHandler(any()); @@ -777,7 +928,7 @@ public void shouldUpdateNetworkErrorMetric() { }); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr)); @@ -793,7 +944,7 @@ public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() { ExtPrebid.of(ExtBidPrebid.builder().build(), null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr)); @@ -811,7 +962,7 @@ public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr)); @@ -824,7 +975,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AmpEvent ampEvent = captureAmpEvent(); @@ -834,6 +985,8 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .status(400) .errors(singletonList("Invalid request format: Request is invalid")) .build()); + + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -847,7 +1000,7 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .willThrow(new RuntimeException("Unexpected exception")); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AmpEvent ampEvent = captureAmpEvent(); @@ -862,6 +1015,8 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .status(500) .errors(singletonList("Unexpected exception")) .build()); + + verifyNoInteractions(hookStageExecutor, hooksMetricsService); } @Test @@ -876,7 +1031,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { null)))); // when - ampHandler.handle(routingContext); + target.handle(routingContext); // then final AmpEvent ampEvent = captureAmpEvent(); @@ -889,33 +1044,317 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { .build())) .build())) .build(); - final AuctionContext expectedAuctionContext = auctionContext.toBuilder() - .requestTypeMetric(MetricName.amp) - .bidResponse(expectedBidResponse) + + assertThat(ampEvent.getHttpContext()).isEqualTo(givenHttpContext(singletonMap("Origin", "http://example.com"))); + assertThat(ampEvent.getBidResponse()).isEqualTo(expectedBidResponse); + assertThat(ampEvent.getTargeting()) + .isEqualTo(singletonMap("hb_cache_id_bidder1", TextNode.valueOf("value1"))); + assertThat(ampEvent.getOrigin()).isEqualTo("http://example.com"); + assertThat(ampEvent.getStatus()).isEqualTo(200); + assertThat(ampEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.amp); + assertThat(ampEvent.getAuctionContext().getBidResponse()).isEqualTo(expectedBidResponse); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"hb_cache_id_bidder1\":\"value1\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldPassSuccessfulEventToAnalyticsReporterWhenExitpointHookChangesResponseAndHeaders() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"targeting\":{\"new-key\":\"new-value\"}}")))); + + givenHoldAuction(givenBidResponse(mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(), + null)))); + + // when + target.handle(routingContext); + + // then + final AmpEvent ampEvent = captureAmpEvent(); + final BidResponse expectedBidResponse = BidResponse.builder().seatbid(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder() + .ext(mapper.valueToTree(ExtPrebid.of( + ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")) + .build(), + null))) + .build())) + .build())) .build(); - assertThat(ampEvent).isEqualTo(AmpEvent.builder() - .httpContext(givenHttpContext(singletonMap("Origin", "http://example.com"))) - .auctionContext(expectedAuctionContext) - .bidResponse(expectedBidResponse) - .targeting(singletonMap("hb_cache_id_bidder1", TextNode.valueOf("value1"))) - .origin("http://example.com") - .status(200) - .errors(emptyList()) - .build()); + assertThat(ampEvent.getAuctionContext().getBidResponse()).isEqualTo(expectedBidResponse); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"targeting\":{\"hb_cache_id_bidder1\":\"value1\"}}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(4) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"), + tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"), + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldReturnSendAmpEventWithAuctionContextBidResponseDebugInfoHoldingExitpointHookOutcome() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of("exitpoint-module", "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(givenBidResponse(mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(), + null)))); + + // when + target.handle(routingContext); + + // then + final AmpEvent ampEvent = captureAmpEvent(); + final BidResponse bidResponse = ampEvent.getBidResponse(); + final ExtModulesTraceAnalyticsTags expectedAnalyticsTags = ExtModulesTraceAnalyticsTags.of(singletonList( + ExtModulesTraceAnalyticsActivity.of( + "some-activity", + "success", + singletonList(ExtModulesTraceAnalyticsResult.of( + "success", + mapper.createObjectNode(), + givenExtModulesTraceAnalyticsAppliedTo()))))); + assertThat(bidResponse.getExt().getPrebid().getModules().getTrace()).isEqualTo(ExtModulesTrace.of( + 8L, + List.of( + ExtModulesTraceStage.of( + Stage.auction_response, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "auction-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + asList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook2") + .action(ExecutionAction.no_action) + .build())))))), + + ExtModulesTraceStage.of( + Stage.exitpoint, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "http-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + singletonList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(expectedAnalyticsTags) + .build()))))))))); + } + + @Test + public void shouldReturnSendAmpEventWithAuctionContextBidResponseAnalyticsTagsHoldingExitpointHookOutcome() { + // given + final ObjectNode analyticsNode = mapper.createObjectNode(); + final ObjectNode optionsNode = analyticsNode.putObject("options"); + optionsNode.put("enableclientdetails", true); + + final AuctionContext auctionContext = givenAuctionContext( + request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() + .analytics(analyticsNode) + .build()))).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(ampRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(givenBidResponse(mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(), + null)))); + + // when + target.handle(routingContext); + + // then + final AmpEvent ampEvent = captureAmpEvent(); + final BidResponse bidResponse = ampEvent.getBidResponse(); + assertThat(bidResponse.getExt()) + .extracting(ExtBidResponse::getPrebid) + .extracting(ExtBidResponsePrebid::getAnalytics) + .extracting(ExtAnalytics::getTags) + .asInstanceOf(InstanceOfAssertFactories.list(ExtAnalyticsTags.class)) + .hasSize(1) + .allSatisfy(extAnalyticsTags -> { + assertThat(extAnalyticsTags.getStage()).isEqualTo(Stage.exitpoint); + assertThat(extAnalyticsTags.getModule()).isEqualTo("exitpoint-module"); + assertThat(extAnalyticsTags.getAnalyticsTags()).isNotNull(); + }); + } + + private static AppliedToImpl givenAppliedToImpl() { + return AppliedToImpl.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static ExtModulesTraceAnalyticsAppliedTo givenExtModulesTraceAnalyticsAppliedTo() { + return ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static EnumMap> stageOutcomes() { + final Map> stageOutcomes = new HashMap<>(); + + stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of( + "auction-response", + singletonList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .message("module1 hook2") + .status(ExecutionStatus.success) + .action(ExecutionAction.no_action) + .build())))))); + + return new EnumMap<>(stageOutcomes); } private AuctionContext givenAuctionContext( - Function bidRequestBuilderCustomizer) { + UnaryOperator bidRequestBuilderCustomizer) { + final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(BidRequest.builder() .imp(emptyList()).tmax(5000L)).build(); return AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of(true, null, null)) + .build()) .uidsCookie(uidsCookie) .bidRequest(bidRequest) .requestTypeMetric(MetricName.amp) .timeoutContext(TimeoutContext.of(0, timeout, 0)) - .debugContext(DebugContext.empty()) + .debugContext(DebugContext.of(true, false, TraceLevel.verbose)) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_amp)) .build(); } @@ -924,7 +1363,6 @@ private void givenHoldAuction(BidResponse bidResponse) { .willAnswer(inv -> Future.succeededFuture(((AuctionContext) inv.getArgument(0)).toBuilder() .bidResponse(bidResponse) .build())); - } private static BidResponse givenBidResponse(ObjectNode extBid) { diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java index 7ff40d09899..1618caea8d2 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -1,5 +1,6 @@ package org.prebid.server.handler.openrtb2; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -21,9 +22,11 @@ import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.BlocklistedAccountException; @@ -33,10 +36,26 @@ import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.Endpoint; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; @@ -44,18 +63,36 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; import java.time.Clock; import java.time.Instant; +import java.util.EnumMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.UnaryOperator; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.function.UnaryOperator.identity; @@ -93,8 +130,12 @@ public class AuctionHandlerTest extends VertxTest { private HttpInteractionLogger httpInteractionLogger; @Mock private PrebidVersionProvider prebidVersionProvider; + @Mock(strictness = LENIENT) + private HooksMetricsService hooksMetricsService; + @Mock(strictness = LENIENT) + private HookStageExecutor hookStageExecutor; - private AuctionHandler auctionHandler; + private AuctionHandler target; @Mock private RoutingContext routingContext; @Mock @@ -118,24 +159,34 @@ public void setUp() { given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); given(httpResponse.headers()).willReturn(MultiMap.caseInsensitiveMultiMap()); - given(skippedAuctionService.skipAuction(any())).willReturn(Future.failedFuture("Auction cannot be skipped")); + given(skippedAuctionService.skipAuction(any())) + .willReturn(Future.failedFuture("Auction cannot be skipped")); given(clock.millis()).willReturn(Instant.now().toEpochMilli()); given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( + false, + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))))); + + given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0)); + timeout = new TimeoutFactory(clock).create(2000L); - auctionHandler = new AuctionHandler( + target = new AuctionHandler( 0.01, auctionRequestFactory, exchangeService, skippedAuctionService, analyticsReporterDelegator, metrics, + hooksMetricsService, clock, httpInteractionLogger, prebidVersionProvider, + hookStageExecutor, jacksonMapper); } @@ -150,7 +201,7 @@ public void shouldSetRequestTypeMetricToAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionContext auctionContext = captureAuctionContext(); @@ -168,7 +219,7 @@ public void shouldUseTimeoutFromAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -194,7 +245,7 @@ public void shouldAddPrebidVersionResponseHeader() { .build())); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -218,7 +269,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() { .build())); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -240,7 +291,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() { given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -260,13 +311,14 @@ public void shouldRespondWithServiceUnavailableIfBidRequestHasAccountBlocklisted .willReturn(Future.failedFuture(new BlocklistedAccountException("Blocklisted account"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(403)); verify(httpResponse).end(eq("Blocklisted: Blocklisted account")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_account)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -278,13 +330,14 @@ public void shouldRespondWithBadRequestIfBidRequestHasAccountWithInvalidConfig() .willReturn(Future.failedFuture(new InvalidAccountConfigException("Invalid config"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); verify(httpResponse).end(eq("Invalid config")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.bad_requests)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -296,13 +349,14 @@ public void shouldRespondWithServiceUnavailableIfBidRequestHasAppBlocklisted() { .willReturn(Future.failedFuture(new BlocklistedAppException("Blocklisted app"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(403)); verify(httpResponse).end(eq("Blocklisted: Blocklisted app")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_app)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -314,13 +368,14 @@ public void shouldRespondWithBadRequestIfBidRequestIsInvalid() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); verify(httpResponse).end(eq("Invalid request format: Request is invalid")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -332,12 +387,13 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", null))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); verify(httpResponse).setStatusCode(eq(401)); verify(httpResponse).end(eq("Account id is not provided")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -352,13 +408,14 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { .willThrow(new RuntimeException("Unexpected exception")); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception")); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err)); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -372,28 +429,26 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).end(anyString()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test public void shouldRespondWithBidResponse() { // given + final AuctionContext auctionContext = givenAuctionContext(identity()); given(auctionRequestFactory.parseRequest(any(), anyLong())) - .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + .willReturn(Future.succeededFuture(auctionContext)); given(auctionRequestFactory.enrichAuctionContext(any())) .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); - - final AuctionContext auctionContext = AuctionContext.builder() - .bidResponse(BidResponse.builder().build()) - .build(); given(exchangeService.holdAuction(any())) - .willReturn(Future.succeededFuture(auctionContext)); + .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build()))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(exchangeService).holdAuction(any()); @@ -404,13 +459,70 @@ public void shouldRespondWithBidResponse() { tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("{}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithBidResponseWhenExitpointChangesHeadersAndResponse() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + given(exchangeService.holdAuction(any())) + .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build()))); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"response\":{}}")))); + + // when + target.handle(routingContext); + + // then + verify(exchangeService).holdAuction(any()); + assertThat(httpResponse.headers()).hasSize(1) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder(tuple("New-Header", "New-Header-Value")); + + verify(httpResponse).end(eq("{\"response\":{}}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() { // given + final AuctionContext auctionContext = givenAuctionContext(identity()); given(auctionRequestFactory.parseRequest(any(), anyLong())) - .willReturn(Future.succeededFuture(givenAuctionContext(identity()))); + .willReturn(Future.succeededFuture(auctionContext)); given(auctionRequestFactory.enrichAuctionContext(any())) .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); @@ -430,20 +542,26 @@ public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() { .debug(ExtResponseDebug.of(null, resolvedRequest, null)) .build()) .build(); - final AuctionContext auctionContext = AuctionContext.builder() - .bidResponse(bidResponse) - .build(); given(exchangeService.holdAuction(any())) - .willReturn(Future.succeededFuture(auctionContext)); + .willReturn(Future.succeededFuture(auctionContext.with(bidResponse))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(exchangeService).holdAuction(any()); verify(httpResponse).end(eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":" + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":" + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}")); + + verify(hookStageExecutor).executeExitpointStage( + any(), + eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":" + + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":" + + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}"), + any()); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -457,7 +575,7 @@ public void shouldIncrementOkOpenrtb2WebRequestMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok)); @@ -475,7 +593,7 @@ public void shouldIncrementOkOpenrtb2AppRequestMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2app), eq(MetricName.ok)); @@ -492,7 +610,7 @@ public void shouldIncrementAppRequestMetrics() { .willReturn(Future.succeededFuture(givenAuctionContext(builder -> builder.app(App.builder().build())))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt()); @@ -514,7 +632,7 @@ public void shouldIncrementNoCookieMetrics() { + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7"); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt()); @@ -532,7 +650,7 @@ public void shouldIncrementImpsRequestedMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1)); @@ -551,7 +669,7 @@ public void shouldIncrementImpTypesMetrics() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateImpTypesMetrics(same(imps)); @@ -564,7 +682,7 @@ public void shouldIncrementBadinputOnParsingRequestOpenrtb2WebRequestMetrics() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput)); @@ -577,7 +695,7 @@ public void shouldIncrementErrOpenrtb2WebRequestMetrics() { .willReturn(Future.failedFuture(new RuntimeException())); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err)); @@ -604,7 +722,7 @@ public void shouldUpdateRequestTimeMetric() { }); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L)); @@ -617,7 +735,7 @@ public void shouldNotUpdateRequestTimeMetricIfRequestFails() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).endHandler(any()); @@ -641,7 +759,7 @@ public void shouldUpdateNetworkErrorMetric() { }); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); @@ -658,7 +776,7 @@ public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); @@ -677,7 +795,7 @@ public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr)); @@ -690,7 +808,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -699,6 +817,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid() .status(400) .errors(singletonList("Invalid request format: Request is invalid")) .build()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -714,7 +833,7 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .willThrow(new RuntimeException("Unexpected exception")); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -728,6 +847,8 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails( .status(500) .errors(singletonList("Unexpected exception")) .build()); + + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -742,22 +863,71 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); - final AuctionContext expectedAuctionContext = auctionContext.toBuilder() - .requestTypeMetric(MetricName.openrtb2web) - .bidResponse(BidResponse.builder().build()) - .build(); + assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext()); + assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build()); + assertThat(auctionEvent.getStatus()).isEqualTo(200); + assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web); + assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build()); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); - assertThat(auctionEvent).isEqualTo(AuctionEvent.builder() - .httpContext(givenHttpContext()) - .auctionContext(expectedAuctionContext) - .bidResponse(BidResponse.builder().build()) - .status(200) - .errors(emptyList()) - .build()); + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldPassSuccessfulEventToAnalyticsReporterWhenExitpointHookChangesResponseAndHeaders() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()); + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"response\":{}}")))); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext()); + assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build()); + assertThat(auctionEvent.getStatus()).isEqualTo(200); + assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web); + assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build()); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -774,7 +944,7 @@ public void shouldTolerateDuplicateQueryParamNames() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -798,7 +968,7 @@ public void shouldTolerateDuplicateHeaderNames() { givenHoldAuction(BidResponse.builder().build()); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then final AuctionEvent auctionEvent = captureAuctionEvent(); @@ -820,17 +990,236 @@ public void shouldSkipAuction() { givenAuctionContext.skipAuction().with(BidResponse.builder().build()))); // when - auctionHandler.handle(routingContext); + target.handle(routingContext); // then verify(auctionRequestFactory, never()).enrichAuctionContext(any()); verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok)); - verifyNoInteractions(exchangeService); - verifyNoInteractions(analyticsReporterDelegator); + verifyNoInteractions(exchangeService, analyticsReporterDelegator, hookStageExecutor); + verify(hooksMetricsService).updateHooksMetrics(any()); verify(httpResponse).setStatusCode(eq(200)); verify(httpResponse).end("{}"); } + @Test + public void shouldReturnSendAuctionEventWithAuctionContextBidResponseDebugInfoHoldingExitpointHookOutcome() { + // given + final AuctionContext auctionContext = givenAuctionContext(identity()).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(auctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final BidResponse bidResponse = auctionEvent.getBidResponse(); + final ExtModulesTraceAnalyticsTags expectedAnalyticsTags = ExtModulesTraceAnalyticsTags.of(singletonList( + ExtModulesTraceAnalyticsActivity.of( + "some-activity", + "success", + singletonList(ExtModulesTraceAnalyticsResult.of( + "success", + mapper.createObjectNode(), + givenExtModulesTraceAnalyticsAppliedTo()))))); + assertThat(bidResponse.getExt().getPrebid().getModules().getTrace()).isEqualTo(ExtModulesTrace.of( + 8L, + List.of( + ExtModulesTraceStage.of( + Stage.auction_response, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "auction-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + asList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook2") + .action(ExecutionAction.no_action) + .build())))))), + + ExtModulesTraceStage.of( + Stage.exitpoint, + 4L, + singletonList(ExtModulesTraceStageOutcome.of( + "http-response", + 4L, + singletonList( + ExtModulesTraceGroup.of( + 4L, + singletonList( + ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(expectedAnalyticsTags) + .build()))))))))); + } + + @Test + public void shouldReturnSendAuctionEventWithAuctionContextBidResponseAnalyticsTagsHoldingExitpointHookOutcome() { + // given + final ObjectNode analyticsNode = mapper.createObjectNode(); + final ObjectNode optionsNode = analyticsNode.putObject("options"); + optionsNode.put("enableclientdetails", true); + + final AuctionContext givenAuctionContext = givenAuctionContext( + request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() + .analytics(analyticsNode) + .build()))).toBuilder() + .hookExecutionContext(HookExecutionContext.of( + Endpoint.openrtb2_amp, + stageOutcomes())) + .build(); + + given(auctionRequestFactory.parseRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext)); + given(auctionRequestFactory.enrichAuctionContext(any())) + .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0))); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> { + final AuctionContext context = invocation.getArgument(2, AuctionContext.class); + final HookExecutionContext hookExecutionContext = context.getHookExecutionContext(); + hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of( + "http-response", + singletonList( + GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(HookId.of( + "exitpoint-module", + "exitpoint-hook")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("exitpoint hook has been executed") + .action(ExecutionAction.update) + .analyticsTags(TagsImpl.of(singletonList( + ActivityImpl.of( + "some-activity", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode(), + givenAppliedToImpl())))))) + .build())))))); + return Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))); + }); + + givenHoldAuction(BidResponse.builder().build()); + + // when + target.handle(routingContext); + + // then + final AuctionEvent auctionEvent = captureAuctionEvent(); + final BidResponse bidResponse = auctionEvent.getBidResponse(); + assertThat(bidResponse.getExt()) + .extracting(ExtBidResponse::getPrebid) + .extracting(ExtBidResponsePrebid::getAnalytics) + .extracting(ExtAnalytics::getTags) + .asInstanceOf(InstanceOfAssertFactories.list(ExtAnalyticsTags.class)) + .hasSize(1) + .allSatisfy(extAnalyticsTags -> { + assertThat(extAnalyticsTags.getStage()).isEqualTo(Stage.exitpoint); + assertThat(extAnalyticsTags.getModule()).isEqualTo("exitpoint-module"); + assertThat(extAnalyticsTags.getAnalyticsTags()).isNotNull(); + }); + } + + private static AppliedToImpl givenAppliedToImpl() { + return AppliedToImpl.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static ExtModulesTraceAnalyticsAppliedTo givenExtModulesTraceAnalyticsAppliedTo() { + return ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(asList("impId1", "impId2")) + .request(true) + .build(); + } + + private static EnumMap> stageOutcomes() { + final Map> stageOutcomes = new HashMap<>(); + + stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of( + "auction-response", + singletonList( + GroupExecutionOutcome.of(asList( + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook1")) + .executionTime(4L) + .status(ExecutionStatus.success) + .message("module1 hook1") + .action(ExecutionAction.update) + .build(), + HookExecutionOutcome.builder() + .hookId(HookId.of("module1", "hook2")) + .executionTime(4L) + .message("module1 hook2") + .status(ExecutionStatus.success) + .action(ExecutionAction.no_action) + .build())))))); + + return new EnumMap<>(stageOutcomes); + } + private AuctionContext captureAuctionContext() { final ArgumentCaptor captor = ArgumentCaptor.forClass(AuctionContext.class); verify(exchangeService).holdAuction(captor.capture()); @@ -862,9 +1251,14 @@ private AuctionContext givenAuctionContext( .imp(emptyList())).build(); final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of(true, null, null)) + .build()) .uidsCookie(uidsCookie) .bidRequest(bidRequest) .requestTypeMetric(MetricName.openrtb2web) + .debugContext(DebugContext.of(true, false, TraceLevel.verbose)) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_auction)) .timeoutContext(TimeoutContext.of(0, timeout, 0)); return auctionContextCustomizer.apply(auctionContextBuilder) diff --git a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java index 5da93cd4c94..64efc34c093 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java @@ -19,11 +19,13 @@ import org.prebid.server.analytics.model.VideoEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.CachedDebugLog; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.model.WithPodErrors; +import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.cache.CoreCacheService; import org.prebid.server.cookie.UidsCookie; @@ -31,7 +33,13 @@ import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.metric.Metrics; +import org.prebid.server.model.Endpoint; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; import org.prebid.server.proto.response.VideoResponse; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; @@ -56,6 +64,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -86,8 +95,12 @@ public class VideoHandlerTest extends VertxTest { private UidsCookie uidsCookie; @Mock private PrebidVersionProvider prebidVersionProvider; + @Mock(strictness = LENIENT) + private HooksMetricsService hooksMetricsService; + @Mock(strictness = LENIENT) + private HookStageExecutor hookStageExecutor; - private VideoHandler videoHandler; + private VideoHandler target; private Timeout timeout; @@ -107,16 +120,25 @@ public void setUp() { given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00"); + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of( + false, + ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))))); + + given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0)); + timeout = new TimeoutFactory(clock).create(2000L); - videoHandler = new VideoHandler( + target = new VideoHandler( videoRequestFactory, videoResponseFactory, exchangeService, coreCacheService, analyticsReporterDelegator, metrics, + hooksMetricsService, clock, prebidVersionProvider, + hookStageExecutor, jacksonMapper); } @@ -130,7 +152,7 @@ public void shouldUseTimeoutFromAuctionContext() { givenHoldAuction(BidResponse.builder().build()); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -154,7 +176,7 @@ public void shouldAddPrebidVersionResponseHeader() { .build())); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -176,7 +198,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() { .build())); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(httpResponse.headers()) @@ -196,7 +218,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() { given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli()); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then assertThat(captureAuctionContext()) @@ -214,11 +236,12 @@ public void shouldRespondWithBadRequestIfBidRequestIsInvalid() { .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid"))); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(400)); verify(httpResponse).end(eq("Invalid request format: Request is invalid")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -228,12 +251,13 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() { .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", "1"))); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verifyNoInteractions(exchangeService); verify(httpResponse).setStatusCode(eq(401)); verify(httpResponse).end(eq("Unauthorised: Account id is not provided")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -246,11 +270,12 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() { .willThrow(new RuntimeException("Unexpected exception")); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse).setStatusCode(eq(500)); verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception")); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -262,10 +287,11 @@ public void shouldNotSendResponseIfClientClosedConnection() { given(routingContext.response().closed()).willReturn(true); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(httpResponse, never()).end(anyString()); + verifyNoInteractions(hooksMetricsService, hookStageExecutor); } @Test @@ -280,10 +306,10 @@ public void shouldRespondWithBidResponse() { .willReturn(VideoResponse.of(emptyList(), null)); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then - verify(videoResponseFactory).toVideoResponse(any(), any(), any()); + verify(videoResponseFactory, times(2)).toVideoResponse(any(), any(), any()); assertThat(httpResponse.headers()).hasSize(2) .extracting(Map.Entry::getKey, Map.Entry::getValue) @@ -291,6 +317,63 @@ public void shouldRespondWithBidResponse() { tuple("Content-Type", "application/json"), tuple("x-prebid", "pbs-java/1.00")); verify(httpResponse).end(eq("{\"adPods\":[]}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"adPods\":[]}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); + } + + @Test + public void shouldRespondWithBidResponseWhenExitpointHookChangesResponseAndHeaders() { + // given + given(videoRequestFactory.fromRequest(any(), anyLong())) + .willReturn(Future.succeededFuture(givenAuctionContext(identity(), emptyList()))); + + givenHoldAuction(BidResponse.builder().build()); + + given(videoResponseFactory.toVideoResponse(any(), any(), any())) + .willReturn(VideoResponse.of(emptyList(), null)); + + given(hookStageExecutor.executeExitpointStage(any(), any(), any())) + .willReturn(Future.succeededFuture(HookStageExecutionResult.success( + ExitpointPayloadImpl.of( + MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"), + "{\"adPods\":[{\"something\":1}]}")))); + + // when + target.handle(routingContext); + + // then + verify(videoResponseFactory, times(2)).toVideoResponse(any(), any(), any()); + + assertThat(httpResponse.headers()).hasSize(1) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder(tuple("New-Header", "New-Header-Value")); + verify(httpResponse).end(eq("{\"adPods\":[{\"something\":1}]}")); + + final ArgumentCaptor responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(hookStageExecutor).executeExitpointStage( + responseHeadersCaptor.capture(), + eq("{\"adPods\":[]}"), + any()); + + assertThat(responseHeadersCaptor.getValue()).hasSize(2) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Content-Type", "application/json"), + tuple("x-prebid", "pbs-java/1.00")); + + verify(hooksMetricsService).updateHooksMetrics(any()); } @Test @@ -309,7 +392,7 @@ public void shouldUpdateVideoEventWithCacheLogIdErrorAndCallCacheForDebugLogWhen given(coreCacheService.cacheVideoDebugLog(any(), anyInt())).willReturn("cacheKey"); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(coreCacheService).cacheVideoDebugLog(any(), anyInt()); @@ -327,6 +410,7 @@ public void shouldCacheDebugLogWhenNoBidsWereReturnedAndDoesNotAddErrorToVideoEv final AuctionContext auctionContext = AuctionContext.builder() .bidRequest(BidRequest.builder().imp(emptyList()).build()) .account(Account.builder().auction(AccountAuctionConfig.builder().videoCacheTtl(100).build()).build()) + .debugContext(DebugContext.empty()) .cachedDebugLog(cachedDebugLog) .build(); @@ -343,7 +427,7 @@ public void shouldCacheDebugLogWhenNoBidsWereReturnedAndDoesNotAddErrorToVideoEv .willReturn(VideoResponse.of(emptyList(), null)); // when - videoHandler.handle(routingContext); + target.handle(routingContext); // then verify(coreCacheService).cacheVideoDebugLog(any(), anyInt()); @@ -377,6 +461,8 @@ private WithPodErrors givenAuctionContext( .uidsCookie(uidsCookie) .bidRequest(bidRequest) .timeoutContext(TimeoutContext.of(0, timeout, 0)) + .debugContext(DebugContext.of(true, false, TraceLevel.verbose)) + .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_video)) .build(); return WithPodErrors.of(auctionContext, errors); diff --git a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java index 7d4f46adf16..ea9ad000891 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java @@ -7,6 +7,7 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.junit5.Checkpoint; @@ -57,6 +58,7 @@ import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; @@ -78,6 +80,8 @@ import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.hooks.v1.exitpoint.ExitpointHook; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.Endpoint; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -103,6 +107,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -2879,6 +2884,183 @@ public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(VertxTestContext })); } + @Test + public void shouldExecuteExitpointHooksHappyPath(VertxTestContext context) { + // given + givenExitpointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-alpha-a", "alpha-a"), + "{\"execution1\":\"alpha-a\"")))); + + givenExitpointHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-alpha-b", "alpha-b"), + payload.responseBody() + ",\"execution4\":\"alpha-b\"}")))); + + givenExitpointHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-beta-a", "beta-a"), + payload.responseBody() + ",\"execution2\":\"beta-a\"")))); + + givenExitpointHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of( + payload.responseHeaders().add("Header-beta-b", "beta-b"), + payload.responseBody() + ",\"execution3\":\"beta-b\"")))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.exitpoint, + execPlanTwoGroupsTwoHooksEach()))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeExitpointStage( + MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"), + "{}", + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> { + assertThat(payload.responseBody()) + .isEqualTo("{\"execution1\":\"alpha-a\",\"execution2\":\"beta-a\"," + + "\"execution3\":\"beta-b\",\"execution4\":\"alpha-b\"}"); + assertThat(payload.responseHeaders()).hasSize(5) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("Header-Name", "Header-Value"), + tuple("Header-alpha-a", "alpha-a"), + tuple("Header-alpha-b", "alpha-b"), + tuple("Header-beta-a", "beta-a"), + tuple("Header-beta-b", "beta-b")); + }); + + context.completeNow(); + })); + } + + @Test + public void shouldExecuteExitpointHooksAndPassAuctionInvocationContext(VertxTestContext context) { + // given + final ExitpointHookImpl hookImpl = spy( + ExitpointHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.EXITPOINT))) + .willReturn(hookImpl); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.exitpoint, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeExitpointStage( + MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"), + "{}", + AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .account(Account.builder() + .hooks(AccountHooksConfiguration.of( + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + final ArgumentCaptor invocationContextCaptor = + ArgumentCaptor.forClass(AuctionInvocationContext.class); + verify(hookImpl).call(any(), invocationContextCaptor.capture()); + + assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> { + assertThat(invocationContext.endpoint()).isNotNull(); + assertThat(invocationContext.timeout()).isNotNull(); + assertThat(invocationContext.accountConfig()).isNotNull(); + }); + + context.completeNow(); + })); + } + + @Test + public void shouldExecuteExitpointHooksAndIgnoreRejection(VertxTestContext context) { + // given + givenExitpointHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.rejected("Will not apply"))); + + final HookStageExecutor executor = createExecutor( + executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.exitpoint, execPlanOneGroupOneHook("module-alpha", "hook-a")))))); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final Future> future = executor.executeExitpointStage( + MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"), + "{}", + AuctionContext.builder() + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result.isShouldReject()).isFalse(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> { + assertThat(payload.responseBody()).isNotNull(); + assertThat(payload.responseBody()).isNotEmpty(); + }); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.exitpoint, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .allSatisfy(stageOutcome -> { + assertThat(stageOutcome.getEntity()).isEqualTo("http-response"); + + final List groups = stageOutcome.getGroups(); + + final List group0Hooks = groups.getFirst().getHooks(); + assertThat(group0Hooks.getFirst()).satisfies(hookOutcome -> { + assertThat(hookOutcome.getHookId()) + .isEqualTo(HookId.of("module-alpha", "hook-a")); + assertThat(hookOutcome.getStatus()) + .isEqualTo(ExecutionStatus.execution_failure); + assertThat(hookOutcome.getMessage()) + .isEqualTo("Rejection is not supported during this stage"); + }); + })); + + context.completeNow(); + })); + } + @Test public void abTestsForEntrypointStageShouldReturnEnabledTests() { // given @@ -3075,6 +3257,18 @@ private void givenAuctionResponseHook( .willReturn(AuctionResponseHookImpl.of(delegate)); } + private void givenExitpointHook( + String moduleCode, + String hookImplCode, + BiFunction< + ExitpointPayload, + AuctionInvocationContext, + Future>> delegate) { + + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.EXITPOINT))) + .willReturn(ExitpointHookImpl.of(delegate)); + } + private BiFunction>> delayedHook( InvocationResult result, int delay) { @@ -3301,4 +3495,28 @@ public String code() { return code; } } + + @Value(staticConstructor = "of") + @NonFinal + private static class ExitpointHookImpl implements ExitpointHook { + + String code = "hook-code"; + + BiFunction< + ExitpointPayload, + AuctionInvocationContext, + Future>> delegate; + + @Override + public Future> call(ExitpointPayload payload, + AuctionInvocationContext invocationContext) { + + return delegate.apply(payload, invocationContext); + } + + @Override + public String code() { + return code; + } + } } diff --git a/src/test/java/org/prebid/server/it/hooks/HooksTest.java b/src/test/java/org/prebid/server/it/hooks/HooksTest.java index 3943338630c..902f12c6589 100644 --- a/src/test/java/org/prebid/server/it/hooks/HooksTest.java +++ b/src/test/java/org/prebid/server/it/hooks/HooksTest.java @@ -1,8 +1,21 @@ package org.prebid.server.it.hooks; +import io.restassured.http.Header; import io.restassured.response.Response; import org.json.JSONException; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.it.IntegrationTest; import org.prebid.server.version.PrebidVersionProvider; import org.skyscreamer.jsonassert.JSONAssert; @@ -10,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; +import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; @@ -18,6 +32,8 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.hamcrest.Matchers.empty; public class HooksTest extends IntegrationTest { @@ -27,8 +43,13 @@ public class HooksTest extends IntegrationTest { @Autowired private PrebidVersionProvider versionProvider; + @Autowired + private AnalyticsReporterDelegator analyticsReporterDelegator; + @Test public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONException { + Mockito.reset(analyticsReporterDelegator); + // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) .withRequestBody(equalToJson( @@ -47,6 +68,39 @@ public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONE "hooks/sample-module/test-auction-sample-module-response.json", response, singletonList(RUBICON)); JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); + + //todo: remove everything below after at least one exitpoint module is added and tested by functional tests + assertThat(response.getHeaders()) + .extracting(Header::getName, Header::getValue) + .contains(tuple("Exitpoint-Hook-Header", "Exitpoint-Hook-Value")); + + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + Mockito.verify(analyticsReporterDelegator).processEvent(eventCaptor.capture(), Mockito.any()); + + final AuctionEvent actualEvent = eventCaptor.getValue(); + final List exitpointHookOutcomes = actualEvent.getAuctionContext() + .getHookExecutionContext().getStageOutcomes().get(Stage.exitpoint); + + final TagsImpl expectedTags = TagsImpl.of(singletonList(ActivityImpl.of( + "exitpoint-device-id", + "success", + singletonList(ResultImpl.of( + "success", + mapper.createObjectNode().put("exitpoint-some-field", "exitpoint-some-value"), + AppliedToImpl.builder() + .impIds(singletonList("impId1")) + .request(true) + .build()))))); + + assertThat(exitpointHookOutcomes).isNotEmpty().hasSize(1).first() + .extracting(StageExecutionOutcome::getGroups) + .extracting(List::getFirst) + .extracting(GroupExecutionOutcome::getHooks) + .extracting(List::getFirst) + .extracting(HookExecutionOutcome::getAnalyticsTags) + .isEqualTo(expectedTags); + + Mockito.reset(analyticsReporterDelegator); } @Test diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java new file mode 100644 index 00000000000..82a494e7158 --- /dev/null +++ b/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java @@ -0,0 +1,80 @@ +package org.prebid.server.it.hooks; + +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.exitpoint.ExitpointHook; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; +import org.prebid.server.json.JacksonMapper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SampleItExitpointHook implements ExitpointHook { + + private final JacksonMapper mapper; + + public SampleItExitpointHook(JacksonMapper mapper) { + this.mapper = mapper; + } + + @Override + public Future> call(ExitpointPayload exitpointPayload, + AuctionInvocationContext invocationContext) { + + final BidResponse bidResponse = invocationContext.auctionContext().getBidResponse(); + final List seatBids = updateBids(bidResponse.getSeatbid()); + final BidResponse updatedResponse = bidResponse.toBuilder().seatbid(seatBids).build(); + + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payload -> ExitpointPayloadImpl.of( + exitpointPayload.responseHeaders().add("Exitpoint-Hook-Header", "Exitpoint-Hook-Value"), + mapper.encodeToString(updatedResponse))) + .debugMessages(Arrays.asList( + "exitpoint debug message 1", + "exitpoint debug message 2")) + .analyticsTags(TagsImpl.of(Collections.singletonList(ActivityImpl.of( + "exitpoint-device-id", + "success", + Collections.singletonList(ResultImpl.of( + "success", + mapper.mapper().createObjectNode().put("exitpoint-some-field", "exitpoint-some-value"), + AppliedToImpl.builder() + .impIds(Collections.singletonList("impId1")) + .request(true) + .build())))))) + .build()); + } + + private List updateBids(List seatBids) { + return seatBids.stream() + .map(seatBid -> seatBid.toBuilder().bid(seatBid.getBid().stream() + .map(bid -> bid.toBuilder() + .adm(bid.getAdm() + + "" + + "") + .build()) + .toList()) + .build()) + .toList(); + } + + @Override + public String code() { + return "exitpoint"; + } + +} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItModule.java b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java index e2806f8c87f..441240e7a32 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItModule.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java @@ -31,7 +31,8 @@ public SampleItModule(JacksonMapper mapper) { new SampleItRejectingProcessedAuctionRequestHook(), new SampleItRejectingBidderRequestHook(), new SampleItRejectingRawBidderResponseHook(), - new SampleItRejectingProcessedBidderResponseHook()); + new SampleItRejectingProcessedBidderResponseHook(), + new SampleItExitpointHook(mapper)); } @Override diff --git a/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java index a08845f9c19..5fdf0724015 100644 --- a/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java +++ b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java @@ -1,9 +1,12 @@ package org.prebid.server.it.hooks; +import org.mockito.Mockito; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.hooks.v1.Module; import org.prebid.server.json.JacksonMapper; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; @TestConfiguration public class TestHooksConfiguration { @@ -12,4 +15,10 @@ public class TestHooksConfiguration { Module sampleItModule(JacksonMapper mapper) { return new SampleItModule(mapper); } + + @Bean + @Primary + AnalyticsReporterDelegator spyAnalyticsReporterDelegator(AnalyticsReporterDelegator analyticsReporterDelegator) { + return Mockito.spy(analyticsReporterDelegator); + } } diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index f4943895116..47b1da61b37 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -1213,6 +1213,9 @@ public void updateHooksMetricsShouldIncrementMetrics() { metrics.updateHooksMetrics( "module2", Stage.auction_response, "hook4", ExecutionStatus.invocation_failure, 5L, null); + metrics.updateHooksMetrics( + "module1", Stage.exitpoint, "hook5", ExecutionStatus.success, 5L, ExecutionAction.update); + // then assertThat(metricRegistry.counter("modules.module.module1.stage.entrypoint.hook.hook1.call") .getCount()) @@ -1272,6 +1275,15 @@ public void updateHooksMetricsShouldIncrementMetrics() { .isEqualTo(1); assertThat(metricRegistry.timer("modules.module.module2.stage.auctionresponse.hook.hook4.duration").getCount()) .isEqualTo(1); + + assertThat(metricRegistry.counter("modules.module.module1.stage.exitpoint.hook.hook5.call") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module1.stage.exitpoint.hook.hook5.success.update") + .getCount()) + .isEqualTo(1); + assertThat(metricRegistry.timer("modules.module.module1.stage.exitpoint.hook.hook5.duration").getCount()) + .isEqualTo(1); } @Test diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json index eacb079d5fe..cc893e56246 100644 --- a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json @@ -7,7 +7,7 @@ "id": "880290288", "impid": "impId1", "price": 8.43, - "adm": "", + "adm": "", "crid": "crid1", "w": 300, "h": 250, diff --git a/src/test/resources/org/prebid/server/it/test-app-settings.yaml b/src/test/resources/org/prebid/server/it/test-app-settings.yaml index 786b376ffed..c377f8ad3ce 100644 --- a/src/test/resources/org/prebid/server/it/test-app-settings.yaml +++ b/src/test/resources/org/prebid/server/it/test-app-settings.yaml @@ -60,6 +60,12 @@ accounts: hook-sequence: - module-code: sample-it-module hook-impl-code: auction-response + exitpoint: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: exitpoint - id: 7001 hooks: execution-plan: @@ -120,6 +126,18 @@ accounts: hook-sequence: - module-code: sample-it-module hook-impl-code: rejecting-processed-bidder-response + - id: 13001 + hooks: + execution-plan: + endpoints: + /openrtb2/auction: + stages: + exitpoint: + groups: + - timeout: 5 + hook-sequence: + - module-code: sample-it-module + hook-impl-code: exitpoint - id: 12001 auction: price-floors: