From eee8e530b19cd9df75f72ecc2cf65930f7d4643b Mon Sep 17 00:00:00 2001 From: antonbabak Date: Fri, 18 Jul 2025 14:54:56 +0200 Subject: [PATCH 1/7] Add Get /vtrack --- .../prebid/server/cache/CoreCacheService.java | 66 ++++++++-- .../proto/response/CacheErrorResponse.java | 19 +++ .../server/handler/GetVtrackHandler.java | 105 +++++++++++++++ ...ackHandler.java => PostVtrackHandler.java} | 20 +-- .../ApplicationServerConfiguration.java | 15 ++- .../server/cache/CoreCacheServiceTest.java | 109 ++++++++++++++++ .../server/handler/GetVtrackHandlerTest.java | 123 ++++++++++++++++++ ...erTest.java => PostVtrackHandlerTest.java} | 14 +- .../org/prebid/server/it/ApplicationTest.java | 21 ++- .../org/prebid/server/it/IntegrationTest.java | 8 ++ .../server/it/vtrack/test-vtrack-response.xml | 12 ++ 11 files changed, 482 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/prebid/server/cache/proto/response/CacheErrorResponse.java create mode 100644 src/main/java/org/prebid/server/handler/GetVtrackHandler.java rename src/main/java/org/prebid/server/handler/{VtrackHandler.java => PostVtrackHandler.java} (94%) create mode 100644 src/test/java/org/prebid/server/handler/GetVtrackHandlerTest.java rename src/test/java/org/prebid/server/handler/{VtrackHandlerTest.java => PostVtrackHandlerTest.java} (98%) create mode 100644 src/test/resources/org/prebid/server/it/vtrack/test-vtrack-response.xml diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index 863c25ee38b..ea1ea41fe29 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -9,6 +9,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidInfo; import org.prebid.server.auction.model.CachedDebugLog; @@ -22,6 +23,7 @@ import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cache.proto.request.bid.BidCacheRequest; import org.prebid.server.cache.proto.request.bid.BidPutObject; +import org.prebid.server.cache.proto.response.CacheErrorResponse; import org.prebid.server.cache.proto.response.bid.BidCacheResponse; import org.prebid.server.cache.proto.response.bid.CacheObject; import org.prebid.server.cache.utils.CacheServiceUtil; @@ -45,6 +47,7 @@ import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; +import java.net.URISyntaxException; import java.net.URL; import java.time.Clock; import java.util.ArrayList; @@ -66,6 +69,8 @@ public class CoreCacheService { private static final String BID_WURL_ATTRIBUTE = "wurl"; private static final String TRACE_INFO_SEPARATOR = "-"; private static final int MAX_DATACENTER_REGION_LENGTH = 4; + private static final String UUID_QUERY_PARAMETER = "uuid"; + private static final String CH_QUERY_PARAMETER = "ch"; private final HttpClient httpClient; private final URL externalEndpointUrl; @@ -191,15 +196,6 @@ private Future makeRequest(BidCacheRequest bidCacheRequest, .recover(exception -> failResponse(exception, accountId, startTime)); } - private Future failResponse(Throwable exception, String accountId, long startTime) { - metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); - - logger.warn("Error occurred while interacting with cache service: {}", exception.getMessage()); - logger.debug("Error occurred while interacting with cache service", exception); - - return Future.failedFuture(exception); - } - public Future cachePutObjects(List bidPutObjects, Boolean isEventsEnabled, Set biddersAllowingVastUpdate, @@ -627,4 +623,56 @@ private static String normalizeDatacenterRegion(String datacenterRegion) { ? trimmedDatacenterRegion.substring(0, MAX_DATACENTER_REGION_LENGTH) : trimmedDatacenterRegion; } + + public Future getCachedObject(String key, String ch, Timeout timeout) { + final long remainingTimeout = timeout.remaining(); + if (remainingTimeout <= 0) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + final URL endpointUrl = ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl); + final String url; + try { + final URIBuilder uriBuilder = new URIBuilder(endpointUrl.toString()); + uriBuilder.addParameter(UUID_QUERY_PARAMETER, key); + if (StringUtils.isNotBlank(ch)) { + uriBuilder.addParameter(CH_QUERY_PARAMETER, ch); + } + url = uriBuilder.build().toString(); + } catch (URISyntaxException e) { + return Future.failedFuture(new IllegalArgumentException("Configured cache url is malformed", e)); + } + + return httpClient.get(url, cacheHeaders, remainingTimeout) + .map(this::handleResponse) + .recover(CoreCacheService::failResponse); + } + + private HttpClientResponse handleResponse(HttpClientResponse response) { + final int statusCode = response.getStatusCode(); + final String body = response.getBody(); + + if (statusCode == 200) { + return response; + } + + try { + final CacheErrorResponse errorResponse = mapper.decodeValue(body, CacheErrorResponse.class); + return HttpClientResponse.of(statusCode, response.getHeaders(), errorResponse.getMessage()); + } catch (DecodeException e) { + throw new PreBidException("Cannot parse response: " + body, e); + } + } + + private Future failResponse(Throwable exception, String accountId, long startTime) { + metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); + return failResponse(exception); + } + + private static Future failResponse(Throwable exception) { + logger.warn("Error occurred while interacting with cache service: {}", exception.getMessage()); + logger.debug("Error occurred while interacting with cache service", exception); + + return Future.failedFuture(exception); + } } diff --git a/src/main/java/org/prebid/server/cache/proto/response/CacheErrorResponse.java b/src/main/java/org/prebid/server/cache/proto/response/CacheErrorResponse.java new file mode 100644 index 00000000000..cf535ac1181 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/response/CacheErrorResponse.java @@ -0,0 +1,19 @@ +package org.prebid.server.cache.proto.response; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class CacheErrorResponse { + + String error; + + Integer status; + + String path; + + String message; + + Long timestamp; +} diff --git a/src/main/java/org/prebid/server/handler/GetVtrackHandler.java b/src/main/java/org/prebid/server/handler/GetVtrackHandler.java new file mode 100644 index 00000000000..deaa9a5b2b5 --- /dev/null +++ b/src/main/java/org/prebid/server/handler/GetVtrackHandler.java @@ -0,0 +1,105 @@ +package org.prebid.server.handler; + +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.cache.CoreCacheService; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.model.Endpoint; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class GetVtrackHandler implements ApplicationResource { + + private static final Logger logger = LoggerFactory.getLogger(GetVtrackHandler.class); + + private static final String UUID_PARAMETER = "uuid"; + private static final String CH_PARAMETER = "ch"; + + private final long defaultTimeout; + private final CoreCacheService coreCacheService; + private final TimeoutFactory timeoutFactory; + + public GetVtrackHandler(long defaultTimeout, CoreCacheService coreCacheService, TimeoutFactory timeoutFactory) { + this.defaultTimeout = defaultTimeout; + this.coreCacheService = Objects.requireNonNull(coreCacheService); + this.timeoutFactory = Objects.requireNonNull(timeoutFactory); + } + + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.vtrack.value())); + } + + @Override + public void handle(RoutingContext routingContext) { + final String uuid = routingContext.request().getParam(UUID_PARAMETER); + final String ch = routingContext.request().getParam(CH_PARAMETER); + if (StringUtils.isBlank(uuid)) { + respondWith( + routingContext, + HttpResponseStatus.BAD_REQUEST, + "'%s' is a required query parameter and can't be empty".formatted(UUID_PARAMETER)); + return; + } + + final Timeout timeout = timeoutFactory.create(defaultTimeout); + + coreCacheService.getCachedObject(uuid, ch, timeout) + .onComplete(asyncCache -> handleCacheResult(asyncCache, routingContext)); + } + + private static void respondWithServerError(RoutingContext routingContext, Throwable exception) { + logger.error("Error occurred while sending request to cache", exception); + respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, + "%s: %s".formatted("Error occurred while sending request to cache", exception.getMessage())); + } + + private static void respondWith(RoutingContext routingContext, + HttpResponseStatus status, + MultiMap headers, + String body) { + + HttpUtil.executeSafely(routingContext, Endpoint.vtrack, + response -> { + headers.forEach(response::putHeader); + response.setStatusCode(status.code()) + .end(body); + }); + } + + private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely(routingContext, Endpoint.vtrack, + response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .setStatusCode(status.code()) + .end(body)); + } + + private void handleCacheResult(AsyncResult async, RoutingContext routingContext) { + if (async.failed()) { + respondWithServerError(routingContext, async.cause()); + } else { + final HttpClientResponse response = async.result(); + final HttpResponseStatus status = HttpResponseStatus.valueOf(response.getStatusCode()); + if (status == HttpResponseStatus.OK) { + respondWith(routingContext, status, response.getHeaders(), response.getBody()); + } else { + respondWith(routingContext, status, response.getBody()); + } + } + } +} diff --git a/src/main/java/org/prebid/server/handler/VtrackHandler.java b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java similarity index 94% rename from src/main/java/org/prebid/server/handler/VtrackHandler.java rename to src/main/java/org/prebid/server/handler/PostVtrackHandler.java index f8b67b50d8c..194d8a4f3fb 100644 --- a/src/main/java/org/prebid/server/handler/VtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java @@ -39,9 +39,9 @@ import java.util.Set; import java.util.stream.Collectors; -public class VtrackHandler implements ApplicationResource { +public class PostVtrackHandler implements ApplicationResource { - private static final Logger logger = LoggerFactory.getLogger(VtrackHandler.class); + private static final Logger logger = LoggerFactory.getLogger(PostVtrackHandler.class); private static final String ACCOUNT_PARAMETER = "a"; private static final String INTEGRATION_PARAMETER = "int"; @@ -56,14 +56,14 @@ public class VtrackHandler implements ApplicationResource { private final TimeoutFactory timeoutFactory; private final JacksonMapper mapper; - public VtrackHandler(long defaultTimeout, - boolean allowUnknownBidder, - boolean modifyVastForUnknownBidder, - ApplicationSettings applicationSettings, - BidderCatalog bidderCatalog, - CoreCacheService coreCacheService, - TimeoutFactory timeoutFactory, - JacksonMapper mapper) { + public PostVtrackHandler(long defaultTimeout, + boolean allowUnknownBidder, + boolean modifyVastForUnknownBidder, + ApplicationSettings applicationSettings, + BidderCatalog bidderCatalog, + CoreCacheService coreCacheService, + TimeoutFactory timeoutFactory, + JacksonMapper mapper) { this.defaultTimeout = defaultTimeout; this.allowUnknownBidder = allowUnknownBidder; 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 88f1ef32f92..c6ae167ae94 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 @@ -34,13 +34,14 @@ import org.prebid.server.handler.BidderParamHandler; import org.prebid.server.handler.CookieSyncHandler; import org.prebid.server.handler.ExceptionHandler; +import org.prebid.server.handler.GetVtrackHandler; import org.prebid.server.handler.GetuidsHandler; import org.prebid.server.handler.NoCacheHandler; import org.prebid.server.handler.NotificationEventHandler; import org.prebid.server.handler.OptoutHandler; import org.prebid.server.handler.SetuidHandler; import org.prebid.server.handler.StatusHandler; -import org.prebid.server.handler.VtrackHandler; +import org.prebid.server.handler.PostVtrackHandler; import org.prebid.server.handler.info.BidderDetailsHandler; import org.prebid.server.handler.info.BiddersHandler; import org.prebid.server.handler.info.filters.BaseOnlyBidderInfoFilterStrategy; @@ -370,7 +371,7 @@ GetuidsHandler getuidsHandler(UidsCookieService uidsCookieService, JacksonMapper } @Bean - VtrackHandler vtrackHandler( + PostVtrackHandler postVtrackHandler( @Value("${vtrack.default-timeout-ms}") int defaultTimeoutMs, @Value("${vtrack.allow-unknown-bidder}") boolean allowUnknownBidder, @Value("${vtrack.modify-vast-for-unknown-bidder}") boolean modifyVastForUnknownBidder, @@ -380,7 +381,7 @@ VtrackHandler vtrackHandler( TimeoutFactory timeoutFactory, JacksonMapper mapper) { - return new VtrackHandler( + return new PostVtrackHandler( defaultTimeoutMs, allowUnknownBidder, modifyVastForUnknownBidder, @@ -391,6 +392,14 @@ VtrackHandler vtrackHandler( mapper); } + @Bean + GetVtrackHandler getVtrackHandler(@Value("${vtrack.default-timeout-ms}") int defaultTimeoutMs, + CoreCacheService coreCacheService, + TimeoutFactory timeoutFactory) { + + return new GetVtrackHandler(defaultTimeoutMs, coreCacheService, timeoutFactory); + } + @Bean OptoutHandler optoutHandler( @Value("${external-url}") String externalUrl, diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 6ce3836a695..0697e0c8b51 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -27,6 +27,7 @@ import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cache.proto.request.bid.BidCacheRequest; import org.prebid.server.cache.proto.request.bid.BidPutObject; +import org.prebid.server.cache.proto.response.CacheErrorResponse; import org.prebid.server.cache.proto.response.bid.BidCacheResponse; import org.prebid.server.cache.proto.response.bid.CacheObject; import org.prebid.server.events.EventsContext; @@ -1356,6 +1357,114 @@ public void cachePutObjectsShouldNotEmitEmptyTtlMetrics() { verify(metrics, never()).updateCacheCreativeTtl(any(), any(), any()); } + @Test + public void getCachedObjectShouldAddUuidAndChQueryParamsBeforeSendingWhenChIsPresent() { + // given + final HttpClientResponse response = HttpClientResponse.of( + 200, + MultiMap.caseInsensitiveMultiMap().add("Header", "Value"), + "body"); + + given(httpClient.get(eq("http://cache-service/cache?uuid=key&ch=ch"), any(), anyLong())) + .willReturn(Future.succeededFuture(response)); + + // when + final Future result = target.getCachedObject("key", "ch", timeout); + + // then + assertThat(result.result()).isEqualTo(response); + } + + @Test + public void getCachedObjectShouldAddUuidQueryParamsBeforeSendingWhenChIsAbsent() { + // given + final HttpClientResponse response = HttpClientResponse.of( + 200, + MultiMap.caseInsensitiveMultiMap().add("Header", "Value"), + "body"); + + given(httpClient.get(eq("http://cache-service/cache?uuid=key"), any(), anyLong())) + .willReturn(Future.succeededFuture(response)); + + // when + final Future result = target.getCachedObject("key", null, timeout); + + // then + assertThat(result.result()).isEqualTo(response); + } + + @Test + public void getCachedObjectShouldAddUuidQueryParamsToInternalBeforeSendingWhenChIsAbsent() + throws MalformedURLException { + + // given + target = new CoreCacheService( + httpClient, + new URL("http://cache-service/cache"), + new URL("http://internal-cache-service/cache"), + "http://cache-service-host/cache?uuid=", + 100L, + "ApiKey", + false, + true, + "apacific", + vastModifier, + eventsService, + metrics, + clock, + idGenerator, + jacksonMapper); + + final HttpClientResponse response = HttpClientResponse.of( + 200, + MultiMap.caseInsensitiveMultiMap().add("Header", "Value"), + "body"); + + given(httpClient.get(eq("http://internal-cache-service/cache?uuid=key"), any(), anyLong())) + .willReturn(Future.succeededFuture(response)); + + // when + final Future result = target.getCachedObject("key", null, timeout); + + // then + assertThat(result.result()).isEqualTo(response); + } + + @Test + public void getCachedObjectShouldHandleErrorResponse() { + // given + final HttpClientResponse response = HttpClientResponse.of( + 404, + null, + jacksonMapper.encodeToString(CacheErrorResponse.builder().message("Resource not found").build())); + + given(httpClient.get(eq("http://cache-service/cache?uuid=key&ch=ch"), any(), anyLong())) + .willReturn(Future.succeededFuture(response)); + + // when + final Future result = target.getCachedObject("key", "ch", timeout); + + // then + assertThat(result.result()).isEqualTo(HttpClientResponse.of(404, null, "Resource not found")); + } + + @Test + public void getCachedObjectShouldFailWhenErrorResponseCanNotBeParsed() { + // given + final HttpClientResponse response = HttpClientResponse.of(404, null, "Resource not found"); + + given(httpClient.get(eq("http://cache-service/cache?uuid=key&ch=ch"), any(), anyLong())) + .willReturn(Future.succeededFuture(response)); + + // when + final Future result = target.getCachedObject("key", "ch", timeout); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause()).hasMessage("Cannot parse response: Resource not found"); + assertThat(result.cause()).isInstanceOf(PreBidException.class); + } + private AuctionContext givenAuctionContext(UnaryOperator accountCustomizer, UnaryOperator bidRequestCustomizer) { diff --git a/src/test/java/org/prebid/server/handler/GetVtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/GetVtrackHandlerTest.java new file mode 100644 index 00000000000..f4d3bca3d3c --- /dev/null +++ b/src/test/java/org/prebid/server/handler/GetVtrackHandlerTest.java @@ -0,0 +1,123 @@ +package org.prebid.server.handler; + +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.util.AsciiString; +import io.vertx.core.Future; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; +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.cache.CoreCacheService; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import static io.vertx.core.MultiMap.caseInsensitiveMultiMap; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +public class GetVtrackHandlerTest extends VertxTest { + + @Mock + private CoreCacheService coreCacheService; + @Mock + private TimeoutFactory timeoutFactory; + + private GetVtrackHandler target; + + @Mock(strictness = LENIENT) + private RoutingContext routingContext; + @Mock(strictness = LENIENT) + private HttpServerRequest httpRequest; + @Mock(strictness = LENIENT) + private HttpServerResponse httpResponse; + + @BeforeEach + public void setUp() { + given(routingContext.request()).willReturn(httpRequest); + given(routingContext.response()).willReturn(httpResponse); + given(httpResponse.putHeader(any(CharSequence.class), any(AsciiString.class))).willReturn(httpResponse); + given(httpResponse.putHeader(anyString(), anyString())).willReturn(httpResponse); + + given(httpRequest.getParam("uuid")).willReturn("key"); + given(httpRequest.getParam("ch")).willReturn("test.com"); + + given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); + + target = new GetVtrackHandler(2000, coreCacheService, timeoutFactory); + } + + @Test + public void shouldRespondWithBadRequestWhenAccountParameterIsMissing() { + // given + given(httpRequest.getParam("uuid")).willReturn(null); + + // when + target.handle(routingContext); + + // then + verifyNoInteractions(coreCacheService); + + verify(httpResponse).setStatusCode(400); + verify(httpResponse).end("'uuid' is a required query parameter and can't be empty"); + } + + @Test + public void shouldRespondWithInternalServerErrorWhenCacheServiceReturnFailure() { + // given + given(coreCacheService.getCachedObject(eq("key"), eq("test.com"), any())) + .willReturn(Future.failedFuture("error")); + + // when + target.handle(routingContext); + + // then + verify(httpResponse).setStatusCode(500); + verify(httpResponse).putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + verify(httpResponse).end("Error occurred while sending request to cache: error"); + } + + @Test + public void shouldRespondWithBodyAndHeadersReturnedFromCacheWhenStatusCodeIsOk() { + // given + given(coreCacheService.getCachedObject(any(), any(), any())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, caseInsensitiveMultiMap().add("Header", "Value"), "body"))); + + // when + target.handle(routingContext); + + // then + verify(coreCacheService).getCachedObject(eq("key"), eq("test.com"), any()); + verify(httpResponse).setStatusCode(200); + verify(httpResponse).putHeader("Header", "Value"); + verify(httpResponse).end("body"); + } + + @Test + public void shouldRespondWithBodyAndDefaultHeadersReturnedFromCacheWhenStatusCodeIsNotOk() { + // given + given(coreCacheService.getCachedObject(any(), any(), any())).willReturn(Future.succeededFuture( + HttpClientResponse.of(404, caseInsensitiveMultiMap().add("Header", "Value"), "reason"))); + + // when + target.handle(routingContext); + + // then + verify(coreCacheService).getCachedObject(eq("key"), eq("test.com"), any()); + verify(httpResponse).setStatusCode(404); + verify(httpResponse).putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + verify(httpResponse).end("reason"); + } +} diff --git a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java similarity index 98% rename from src/test/java/org/prebid/server/handler/VtrackHandlerTest.java rename to src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java index a48af60aabf..98011db6ecf 100644 --- a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java @@ -51,7 +51,7 @@ import static org.mockito.Mockito.verifyNoInteractions; @ExtendWith(MockitoExtension.class) -public class VtrackHandlerTest extends VertxTest { +public class PostVtrackHandlerTest extends VertxTest { @Mock private ApplicationSettings applicationSettings; @@ -62,7 +62,7 @@ public class VtrackHandlerTest extends VertxTest { @Mock private TimeoutFactory timeoutFactory; - private VtrackHandler handler; + private PostVtrackHandler handler; @Mock(strictness = LENIENT) private RoutingContext routingContext; @Mock(strictness = LENIENT) @@ -84,7 +84,7 @@ public void setUp() { given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse); - handler = new VtrackHandler( + handler = new PostVtrackHandler( 2000, true, true, applicationSettings, bidderCatalog, coreCacheService, timeoutFactory, jacksonMapper); } @@ -314,7 +314,7 @@ public void shouldSendToCacheNullInAccountEnabledAndValidBiddersWhenAccountEvent public void shouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastNotAllowed() throws JsonProcessingException { // given - handler = new VtrackHandler( + handler = new PostVtrackHandler( 2000, false, true, applicationSettings, bidderCatalog, coreCacheService, timeoutFactory, jacksonMapper); final List bidPutObjects = asList( @@ -359,7 +359,7 @@ public void shouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastNotAll @Test public void shouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastAllowed() throws JsonProcessingException { // given - handler = new VtrackHandler( + handler = new PostVtrackHandler( 2000, false, false, applicationSettings, bidderCatalog, coreCacheService, timeoutFactory, jacksonMapper); @@ -447,7 +447,7 @@ public void shouldSendToCacheExpectedPutsWhenModifyVastForUnknownBidderAndAllowU public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenAllowUnknownBidderIsFalse() throws JsonProcessingException { // given - handler = new VtrackHandler( + handler = new PostVtrackHandler( 2000, false, true, applicationSettings, bidderCatalog, coreCacheService, timeoutFactory, jacksonMapper); final List bidPutObjects = asList( @@ -486,7 +486,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenAllowUnkn public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenModifyVastForUnknownBidderIsFalse() throws JsonProcessingException { // given - handler = new VtrackHandler( + handler = new PostVtrackHandler( 2000, true, false, applicationSettings, bidderCatalog, coreCacheService, timeoutFactory, jacksonMapper); final List bidPutObjects = asList( diff --git a/src/test/java/org/prebid/server/it/ApplicationTest.java b/src/test/java/org/prebid/server/it/ApplicationTest.java index ca4414b8897..3a3710c6e6e 100644 --- a/src/test/java/org/prebid/server/it/ApplicationTest.java +++ b/src/test/java/org/prebid/server/it/ApplicationTest.java @@ -53,6 +53,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToIgnoreCase; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static io.restassured.RestAssured.given; @@ -397,7 +398,7 @@ public void getuidsShouldReturnJsonWithUids() throws JSONException, IOException } @Test - public void vtrackShouldReturnJsonWithUids() throws JSONException, IOException { + public void vtrackShouldPutBidsAndReturnUids() throws JSONException, IOException { // given and when WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) .withRequestBody(equalToBidCacheRequest(jsonFrom("vtrack/test-cache-request.json"))) @@ -413,6 +414,24 @@ public void vtrackShouldReturnJsonWithUids() throws JSONException, IOException { assertJsonEquals("vtrack/test-vtrack-response.json", response, emptyList()); } + @Test + public void vtrackShouldReturnCachedObjects() throws IOException { + // given and when + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/cache")) + .withQueryParam("uuid", equalTo("key")) + .withQueryParam("ch", equalTo("test.com")) + .willReturn(aResponse().withBody(xmlFrom("vtrack/test-vtrack-response.xml")))); + + final Response response = given(SPEC) + .when() + .queryParam("uuid", "key") + .queryParam("ch", "test.com") + .get("/vtrack"); + + // then + assertThat(response.asString()).isEqualTo(xmlFrom("vtrack/test-vtrack-response.xml")); + } + @Test public void optionsRequestShouldRespondWithOriginalPolicyHeaders() { // when diff --git a/src/test/java/org/prebid/server/it/IntegrationTest.java b/src/test/java/org/prebid/server/it/IntegrationTest.java index 7df8040f599..7b8d42e0dc0 100644 --- a/src/test/java/org/prebid/server/it/IntegrationTest.java +++ b/src/test/java/org/prebid/server/it/IntegrationTest.java @@ -40,6 +40,8 @@ import org.springframework.test.context.TestPropertySource; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -118,6 +120,12 @@ protected static String jsonFrom(String file, PrebidVersionProvider prebidVersio .replace("{{ pbs.java.version }}", prebidVersionProvider.getNameVersionRecord()); } + protected static String xmlFrom(String file) throws IOException { + try (InputStream inputStream = IntegrationTest.class.getResourceAsStream(file)) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } + protected static String openrtbAuctionResponseFrom(String templatePath, Response response, List bidders) throws IOException { diff --git a/src/test/resources/org/prebid/server/it/vtrack/test-vtrack-response.xml b/src/test/resources/org/prebid/server/it/vtrack/test-vtrack-response.xml new file mode 100644 index 00000000000..19e3ace7ba3 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/vtrack/test-vtrack-response.xml @@ -0,0 +1,12 @@ + + + + prebid.org wrapper + + + + + + + + From fa4b64da67e37b0ad2ab5109d771a03011d1ad3d Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 21 Jul 2025 12:18:43 +0200 Subject: [PATCH 2/7] Add metrics --- .../prebid/server/cache/CoreCacheService.java | 48 +++++--- .../prebid/server/metric/CacheMetrics.java | 7 ++ .../server/metric/CacheReadMetrics.java | 20 +++ .../server/metric/CacheVtrackMetrics.java | 39 ++++++ .../server/metric/CacheWriteMetrics.java | 19 +++ .../org/prebid/server/metric/Metrics.java | 16 ++- .../server/cache/CoreCacheServiceTest.java | 115 +++++++++++++++++- .../org/prebid/server/metric/MetricsTest.java | 31 ++++- 8 files changed, 266 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/prebid/server/metric/CacheReadMetrics.java create mode 100644 src/main/java/org/prebid/server/metric/CacheVtrackMetrics.java create mode 100644 src/main/java/org/prebid/server/metric/CacheWriteMetrics.java diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index ea1ea41fe29..464bc29fdfc 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -191,9 +191,20 @@ private Future makeRequest(BidCacheRequest bidCacheRequest, cacheHeaders, mapper.encodeToString(bidCacheRequest), remainingTimeout) - .map(response -> toBidCacheResponse( + .map(response -> processVtrackWriteCacheResponse( response.getStatusCode(), response.getBody(), bidCount, accountId, startTime)) - .recover(exception -> failResponse(exception, accountId, startTime)); + .recover(exception -> failVtrackCacheWriteResponse(exception, accountId, startTime)); + } + + private BidCacheResponse processVtrackWriteCacheResponse(int statusCode, + String responseBody, + int bidCount, + String accountId, + long startTime) { + + final BidCacheResponse bidCacheResponse = toBidCacheResponse(statusCode, responseBody, bidCount); + metrics.updateVtrackCacheWriteRequestTime(accountId, clock.millis() - startTime, MetricName.ok); + return bidCacheResponse; } public Future cachePutObjects(List bidPutObjects, @@ -339,8 +350,8 @@ private CacheServiceResult processResponseOpenrtb(HttpClientResponse response, externalEndpointUrl.toString(), httpRequest, httpResponse, startTime); final BidCacheResponse bidCacheResponse; try { - bidCacheResponse = toBidCacheResponse( - responseStatusCode, response.getBody(), bidCount, accountId, startTime); + bidCacheResponse = toBidCacheResponse(responseStatusCode, response.getBody(), bidCount); + metrics.updateAuctionCacheRequestTime(accountId, clock.millis() - startTime, MetricName.ok); } catch (PreBidException e) { return CacheServiceResult.of(httpCall, e, Collections.emptyMap()); } @@ -357,7 +368,7 @@ private CacheServiceResult failResponseOpenrtb(Throwable exception, logger.warn("Error occurred while interacting with cache service: {}", exception.getMessage()); logger.debug("Error occurred while interacting with cache service", exception); - metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); + metrics.updateAuctionCacheRequestTime(accountId, clock.millis() - startTime, MetricName.err); final DebugHttpCall httpCall = makeDebugHttpCall(externalEndpointUrl.toString(), request, null, startTime); return CacheServiceResult.of(httpCall, exception, Collections.emptyMap()); @@ -456,9 +467,7 @@ private String generateWinUrl(String bidId, private BidCacheResponse toBidCacheResponse(int statusCode, String responseBody, - int bidCount, - String accountId, - long startTime) { + int bidCount) { if (statusCode != 200) { throw new PreBidException("HTTP status code " + statusCode); @@ -476,7 +485,6 @@ private BidCacheResponse toBidCacheResponse(int statusCode, throw new PreBidException("The number of response cache objects doesn't match with bids"); } - metrics.updateCacheRequestSuccessTime(accountId, clock.millis() - startTime); return bidCacheResponse; } @@ -643,29 +651,41 @@ public Future getCachedObject(String key, String ch, Timeout return Future.failedFuture(new IllegalArgumentException("Configured cache url is malformed", e)); } + final long startTime = clock.millis(); return httpClient.get(url, cacheHeaders, remainingTimeout) - .map(this::handleResponse) - .recover(CoreCacheService::failResponse); + .map(response -> processVtrackReadResponse(response, startTime)) + .recover(exception -> failVtrackCacheReadResponse(exception, startTime)); } - private HttpClientResponse handleResponse(HttpClientResponse response) { + private HttpClientResponse processVtrackReadResponse(HttpClientResponse response, long startTime) { final int statusCode = response.getStatusCode(); final String body = response.getBody(); if (statusCode == 200) { + metrics.updateVtrackCacheReadRequestTime(clock.millis() - startTime, MetricName.ok); return response; } try { final CacheErrorResponse errorResponse = mapper.decodeValue(body, CacheErrorResponse.class); + metrics.updateVtrackCacheReadRequestTime(clock.millis() - startTime, MetricName.err); return HttpClientResponse.of(statusCode, response.getHeaders(), errorResponse.getMessage()); } catch (DecodeException e) { throw new PreBidException("Cannot parse response: " + body, e); } } - private Future failResponse(Throwable exception, String accountId, long startTime) { - metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); + private Future failVtrackCacheWriteResponse(Throwable exception, String accountId, long startTime) { + if (exception instanceof PreBidException) { + metrics.updateVtrackCacheWriteRequestTime(accountId, clock.millis() - startTime, MetricName.err); + } + return failResponse(exception); + } + + private Future failVtrackCacheReadResponse(Throwable exception, long startTime) { + if (exception instanceof PreBidException) { + metrics.updateVtrackCacheReadRequestTime(clock.millis() - startTime, MetricName.err); + } return failResponse(exception); } diff --git a/src/main/java/org/prebid/server/metric/CacheMetrics.java b/src/main/java/org/prebid/server/metric/CacheMetrics.java index 5116e10e08f..4838c0848f6 100644 --- a/src/main/java/org/prebid/server/metric/CacheMetrics.java +++ b/src/main/java/org/prebid/server/metric/CacheMetrics.java @@ -13,6 +13,7 @@ class CacheMetrics extends UpdatableMetrics { private final RequestMetrics requestsMetrics; private final CacheCreativeSizeMetrics cacheCreativeSizeMetrics; private final CacheCreativeTtlMetrics cacheCreativeTtlMetrics; + private final CacheVtrackMetrics cacheVtrackMetrics; CacheMetrics(MetricRegistry metricRegistry, CounterType counterType) { super( @@ -23,6 +24,7 @@ class CacheMetrics extends UpdatableMetrics { requestsMetrics = new RequestMetrics(metricRegistry, counterType, createPrefix()); cacheCreativeSizeMetrics = new CacheCreativeSizeMetrics(metricRegistry, counterType, createPrefix()); cacheCreativeTtlMetrics = new CacheCreativeTtlMetrics(metricRegistry, counterType, createPrefix()); + cacheVtrackMetrics = new CacheVtrackMetrics(metricRegistry, counterType, createPrefix()); } CacheMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { @@ -34,6 +36,7 @@ class CacheMetrics extends UpdatableMetrics { requestsMetrics = new RequestMetrics(metricRegistry, counterType, createPrefix(prefix)); cacheCreativeSizeMetrics = new CacheCreativeSizeMetrics(metricRegistry, counterType, createPrefix(prefix)); cacheCreativeTtlMetrics = new CacheCreativeTtlMetrics(metricRegistry, counterType, createPrefix(prefix)); + cacheVtrackMetrics = new CacheVtrackMetrics(metricRegistry, counterType, createPrefix(prefix)); } private static String createPrefix(String prefix) { @@ -59,4 +62,8 @@ CacheCreativeSizeMetrics creativeSize() { CacheCreativeTtlMetrics creativeTtl() { return cacheCreativeTtlMetrics; } + + CacheVtrackMetrics vtrack() { + return cacheVtrackMetrics; + } } diff --git a/src/main/java/org/prebid/server/metric/CacheReadMetrics.java b/src/main/java/org/prebid/server/metric/CacheReadMetrics.java new file mode 100644 index 00000000000..356d96fb9d1 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheReadMetrics.java @@ -0,0 +1,20 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +public class CacheReadMetrics extends UpdatableMetrics { + + CacheReadMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix))); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.read.%s".formatted(prefix, metricName); + } + +} diff --git a/src/main/java/org/prebid/server/metric/CacheVtrackMetrics.java b/src/main/java/org/prebid/server/metric/CacheVtrackMetrics.java new file mode 100644 index 00000000000..759f16aabac --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheVtrackMetrics.java @@ -0,0 +1,39 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +class CacheVtrackMetrics extends UpdatableMetrics { + + private final CacheReadMetrics readMetrics; + private final CacheWriteMetrics writeMetrics; + + CacheVtrackMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + + readMetrics = new CacheReadMetrics(metricRegistry, counterType, createPrefix(prefix)); + writeMetrics = new CacheWriteMetrics(metricRegistry, counterType, createPrefix(prefix)); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.%s".formatted(prefix, metricName); + } + + private static String createPrefix(String prefix) { + return prefix + ".vtrack"; + } + + CacheReadMetrics read() { + return readMetrics; + } + + CacheWriteMetrics write() { + return writeMetrics; + } + +} diff --git a/src/main/java/org/prebid/server/metric/CacheWriteMetrics.java b/src/main/java/org/prebid/server/metric/CacheWriteMetrics.java new file mode 100644 index 00000000000..d0d598d6ae5 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheWriteMetrics.java @@ -0,0 +1,19 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +public class CacheWriteMetrics extends UpdatableMetrics { + + CacheWriteMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix))); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.write.%s".formatted(prefix, metricName); + } +} diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 00295decad3..41a828ed46c 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -606,14 +606,18 @@ public void updateStoredImpsMetric(boolean found) { } } - public void updateCacheRequestSuccessTime(String accountId, long timeElapsed) { - cache().requests().updateTimer(MetricName.ok, timeElapsed); - forAccount(accountId).cache().requests().updateTimer(MetricName.ok, timeElapsed); + public void updateVtrackCacheReadRequestTime(long timeElapsed, MetricName metricName) { + cache().vtrack().read().updateTimer(metricName, timeElapsed); } - public void updateCacheRequestFailedTime(String accountId, long timeElapsed) { - cache().requests().updateTimer(MetricName.err, timeElapsed); - forAccount(accountId).cache().requests().updateTimer(MetricName.err, timeElapsed); + public void updateVtrackCacheWriteRequestTime(String accountId, long timeElapsed, MetricName metricName) { + cache().vtrack().write().updateTimer(metricName, timeElapsed); + forAccount(accountId).cache().vtrack().write().updateTimer(metricName, timeElapsed); + } + + public void updateAuctionCacheRequestTime(String accountId, long timeElapsed, MetricName metricName) { + cache().requests().updateTimer(metricName, timeElapsed); + forAccount(accountId).cache().requests().updateTimer(metricName, timeElapsed); } public void updateCacheCreativeSize(String accountId, int creativeSize, MetricName creativeType) { diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 0697e0c8b51..ca2ee9b20cd 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -57,6 +57,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeoutException; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -234,7 +235,7 @@ public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws Json eventsContext); // then - verify(metrics).updateCacheRequestFailedTime(eq("accountId"), anyLong()); + verify(metrics).updateAuctionCacheRequestTime(eq("accountId"), anyLong(), eq(MetricName.err)); verify(httpClient).post(eq("http://cache-service/cache"), any(), any(), anyLong()); final CacheServiceResult result = future.result(); @@ -287,7 +288,7 @@ public void cacheBidsOpenrtbShouldTryCallingInternalEndpointAndTolerateReadingHt eventsContext); // then - verify(metrics).updateCacheRequestFailedTime(eq("accountId"), anyLong()); + verify(metrics).updateAuctionCacheRequestTime(eq("accountId"), anyLong(), eq(MetricName.err)); verify(httpClient).post(eq("http://cache-service-internal/cache"), any(), any(), anyLong()); final CacheServiceResult result = future.result(); @@ -508,6 +509,10 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IO .auctionTimestamp(1000L) .build(); + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"), CacheObject.of("uuid2"), CacheObject.of("uuid3"))))))); + // when target.cacheBidsOpenrtb( asList(bidInfo1, bidInfo2), @@ -527,6 +532,8 @@ public void cacheBidsOpenrtbShouldPerformHttpRequestWithExpectedBody() throws IO verify(metrics).updateCacheCreativeTtl(eq("accountId"), eq(1), eq(MetricName.json)); verify(metrics).updateCacheCreativeTtl(eq("accountId"), eq(2), eq(MetricName.json)); + verify(metrics).updateAuctionCacheRequestTime(eq("accountId"), anyLong(), eq(MetricName.ok)); + final Bid bid1 = bidInfo1.getBid(); final Bid bid2 = bidInfo2.getBid(); @@ -798,7 +805,7 @@ public void cachePutObjectsShouldReturnResultWithEmptyListWhenPutObjectsIsEmpty( } @Test - public void cachePutObjectsShould() throws IOException { + public void cachePutObjectsShouldCacheObjects() throws IOException { // given final BidPutObject firstBidPutObject = BidPutObject.builder() .type("json") @@ -830,6 +837,10 @@ public void cachePutObjectsShould() throws IOException { .willReturn(new TextNode("VAST")) .willReturn(new TextNode("updatedVast")); + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"), CacheObject.of("uuid2"), CacheObject.of("uuid3"))))))); + // when target.cachePutObjects( asList(firstBidPutObject, secondBidPutObject, thirdBidPutObject), @@ -850,6 +861,8 @@ public void cachePutObjectsShould() throws IOException { verify(metrics).updateCacheCreativeTtl(eq("account"), eq(2), eq(MetricName.xml)); verify(metrics).updateCacheCreativeTtl(eq("account"), eq(3), eq(MetricName.unknown)); + verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.ok)); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), firstBidPutObject, "account", "pbjs"); verify(vastModifier).modifyVastXml(true, singleton("bidder1"), secondBidPutObject, "account", "pbjs"); @@ -875,6 +888,78 @@ public void cachePutObjectsShould() throws IOException { .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); } + @Test + public void cachePutObjectsShouldLogErrorMetricsWhenStatusCodeIsNotOk() { + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(1) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")) + .willReturn(new TextNode("VAST")) + .willReturn(new TextNode("updatedVast")); + + given(httpClient.post(eq("http://cache-service/cache"), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(404, null, null))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + "pbjs", + timeout); + + // then + verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateCacheCreativeTtl(eq("account"), eq(1), eq(MetricName.json)); + verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.err)); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), bidObject, "account", "pbjs"); + } + + @Test + public void cachePutObjectsShouldNotLogErrorMetricsWhenCacheServiceIsNotConnected() { + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(1) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")) + .willReturn(new TextNode("VAST")) + .willReturn(new TextNode("updatedVast")); + + given(httpClient.post(eq("http://cache-service/cache"), any(), any(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("Timeout"))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + "pbjs", + timeout); + + // then + verify(metrics, never()).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), any()); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateCacheCreativeTtl(eq("account"), eq(1), eq(MetricName.json)); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), bidObject, "account", "pbjs"); + } + @Test public void cachePutObjectsShouldCallInternalCacheEndpointWhenProvided() throws IOException { // given @@ -1373,6 +1458,7 @@ public void getCachedObjectShouldAddUuidAndChQueryParamsBeforeSendingWhenChIsPre // then assertThat(result.result()).isEqualTo(response); + verify(metrics).updateVtrackCacheReadRequestTime(anyLong(), eq(MetricName.ok)); } @Test @@ -1391,6 +1477,7 @@ public void getCachedObjectShouldAddUuidQueryParamsBeforeSendingWhenChIsAbsent() // then assertThat(result.result()).isEqualTo(response); + verify(metrics).updateVtrackCacheReadRequestTime(anyLong(), eq(MetricName.ok)); } @Test @@ -1430,6 +1517,25 @@ public void getCachedObjectShouldAddUuidQueryParamsToInternalBeforeSendingWhenCh assertThat(result.result()).isEqualTo(response); } + @Test + public void getCachedObjectShouldNotLogErrorMetricsWhenCacheIsNotReached() { + // given + final HttpClientResponse response = HttpClientResponse.of( + 200, + MultiMap.caseInsensitiveMultiMap().add("Header", "Value"), + "body"); + + given(httpClient.get(eq("http://cache-service/cache?uuid=key&ch=ch"), any(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("Timeout"))); + + // when + final Future result = target.getCachedObject("key", "ch", timeout); + + // then + assertThat(result.failed()).isTrue(); + verify(metrics, never()).updateVtrackCacheReadRequestTime(anyLong(), any()); + } + @Test public void getCachedObjectShouldHandleErrorResponse() { // given @@ -1446,6 +1552,7 @@ public void getCachedObjectShouldHandleErrorResponse() { // then assertThat(result.result()).isEqualTo(HttpClientResponse.of(404, null, "Resource not found")); + verify(metrics).updateVtrackCacheReadRequestTime(anyLong(), eq(MetricName.err)); } @Test @@ -1463,6 +1570,8 @@ public void getCachedObjectShouldFailWhenErrorResponseCanNotBeParsed() { assertThat(result.failed()).isTrue(); assertThat(result.cause()).hasMessage("Cannot parse response: Resource not found"); assertThat(result.cause()).isInstanceOf(PreBidException.class); + + verify(metrics).updateVtrackCacheReadRequestTime(anyLong(), eq(MetricName.err)); } private AuctionContext givenAuctionContext(UnaryOperator accountCustomizer, diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 243ef33e66d..3d7625dc7f8 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -1218,23 +1218,42 @@ public void shouldIncrementStoredImpMissingMetric() { } @Test - public void shouldIncrementPrebidCacheRequestSuccessTimer() { + public void shouldIncrementAuctionPrebidCacheRequestTimer() { // when - metrics.updateCacheRequestSuccessTime("accountId", 1424L); + metrics.updateAuctionCacheRequestTime("accountId", 1424L, MetricName.ok); + metrics.updateAuctionCacheRequestTime("accountId", 1424L, MetricName.err); // then assertThat(metricRegistry.timer("prebid_cache.requests.ok").getCount()).isEqualTo(1); assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.ok").getCount()).isOne(); + + assertThat(metricRegistry.timer("prebid_cache.requests.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.err").getCount()).isOne(); } @Test - public void shouldIncrementPrebidCacheRequestFailedTimer() { + public void shouldIncrementVtrackReadPrebidCacheRequestTimer() { // when - metrics.updateCacheRequestFailedTime("accountId", 1424L); + metrics.updateVtrackCacheReadRequestTime(1424L, MetricName.ok); + metrics.updateVtrackCacheReadRequestTime(1424L, MetricName.err); // then - assertThat(metricRegistry.timer("prebid_cache.requests.err").getCount()).isEqualTo(1); - assertThat(metricRegistry.timer("account.accountId.prebid_cache.requests.err").getCount()).isOne(); + assertThat(metricRegistry.timer("prebid_cache.vtrack.read.ok").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("prebid_cache.vtrack.read.err").getCount()).isEqualTo(1); + } + + @Test + public void shouldIncrementVtrackWritePrebidCacheRequestTimer() { + // when + metrics.updateVtrackCacheWriteRequestTime("accountId", 1424L, MetricName.ok); + metrics.updateVtrackCacheWriteRequestTime("accountId", 1424L, MetricName.err); + + // then + assertThat(metricRegistry.timer("prebid_cache.vtrack.write.ok").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.prebid_cache.vtrack.write.ok").getCount()).isOne(); + + assertThat(metricRegistry.timer("prebid_cache.vtrack.write.err").getCount()).isEqualTo(1); + assertThat(metricRegistry.timer("account.accountId.prebid_cache.vtrack.write.err").getCount()).isOne(); } @Test From c7416d98c516d6ec4434a2f8148136177db3e472 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Wed, 13 Aug 2025 15:28:41 +0200 Subject: [PATCH 3/7] Vtrack Endpoint: Account TTL Config --- .../prebid/server/cache/CoreCacheService.java | 7 +- .../server/handler/PostVtrackHandler.java | 14 ++- .../prebid/server/settings/model/Account.java | 2 + .../settings/model/AccountVtrackConfig.java | 11 ++ .../server/cache/CoreCacheServiceTest.java | 102 +++++++++++++++++- .../server/handler/PostVtrackHandlerTest.java | 61 ++++++++--- 6 files changed, 173 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index 464bc29fdfc..95ed9bfd112 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -211,11 +211,12 @@ public Future cachePutObjects(List bidPutObjects Boolean isEventsEnabled, Set biddersAllowingVastUpdate, String accountId, + Integer accountTtl, String integration, Timeout timeout) { - final List cachedCreatives = - updatePutObjects(bidPutObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, integration); + final List cachedCreatives = updatePutObjects( + bidPutObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, accountTtl, integration); updateCreativeMetrics(accountId, cachedCreatives); @@ -226,6 +227,7 @@ private List updatePutObjects(List bidPutObjects, Boolean isEventsEnabled, Set allowedBidders, String accountId, + Integer accountTtl, String integration) { return bidPutObjects.stream() @@ -240,6 +242,7 @@ private List updatePutObjects(List bidPutObjects, putObject, accountId, integration)) + .ttlseconds(ObjectUtils.min(putObject.getTtlseconds(), accountTtl)) .build()) .map(payload -> CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))) .toList(); diff --git a/src/main/java/org/prebid/server/handler/PostVtrackHandler.java b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java index 194d8a4f3fb..c52068427ce 100644 --- a/src/main/java/org/prebid/server/handler/PostVtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java @@ -29,6 +29,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.settings.model.AccountVtrackConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.verticles.server.HttpEndpoint; import org.prebid.server.vertx.verticles.server.application.ApplicationResource; @@ -36,6 +37,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -180,10 +182,12 @@ private void handleAccountResult(AsyncResult asyncAccount, respondWithServerError(routingContext, "Error occurred while fetching account", asyncAccount.cause()); } else { // insert impression tracking if account allows events and bidder allows VAST modification - final Boolean isEventEnabled = accountEventsEnabled(asyncAccount.result()); + final Account account = asyncAccount.result(); + final Boolean isEventEnabled = accountEventsEnabled(account); + final Integer accountTtl = accountVtrackTtl(account); final Set allowedBidders = biddersAllowingVastUpdate(vtrackPuts); coreCacheService.cachePutObjects( - vtrackPuts, isEventEnabled, allowedBidders, accountId, integration, timeout) + vtrackPuts, isEventEnabled, allowedBidders, accountId, accountTtl, integration, timeout) .onComplete(asyncCache -> handleCacheResult(asyncCache, routingContext)); } } @@ -196,6 +200,12 @@ private static Boolean accountEventsEnabled(Account account) { return accountEventsConfig != null ? accountEventsConfig.getEnabled() : null; } + private static Integer accountVtrackTtl(Account account) { + return Optional.ofNullable(account.getVtrack()) + .map(AccountVtrackConfig::getTtl) + .orElse(null); + } + /** * Returns list of bidders that allow VAST XML modification. */ diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index 4286c5077f0..2aa8c974ffa 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -30,6 +30,8 @@ public class Account { @JsonAlias("alternate-bidder-codes") AccountAlternateBidderCodes alternateBidderCodes; + AccountVtrackConfig vtrack; + public static Account empty(String id) { return Account.builder().id(id).build(); } diff --git a/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java b/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java new file mode 100644 index 00000000000..a4fd7053827 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java @@ -0,0 +1,11 @@ +package org.prebid.server.settings.model; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class AccountVtrackConfig { + + Integer ttl; +} diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index ca2ee9b20cd..8a2039dc7a2 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -785,7 +785,7 @@ public void cacheBidsOpenrtbShouldRemoveCatDurPrefixFromVideoUuidFromResponse() public void cachePutObjectsShouldTolerateGlobalTimeoutAlreadyExpired() { // when final Future future = target.cachePutObjects( - singletonList(BidPutObject.builder().build()), true, emptySet(), "", "", + singletonList(BidPutObject.builder().build()), true, emptySet(), "", 100, "", expiredTimeout); // then @@ -797,7 +797,7 @@ public void cachePutObjectsShouldTolerateGlobalTimeoutAlreadyExpired() { public void cachePutObjectsShouldReturnResultWithEmptyListWhenPutObjectsIsEmpty() { // when final Future result = target.cachePutObjects(emptyList(), true, - emptySet(), null, null, null); + emptySet(), null, 100, null, null); // then verifyNoInteractions(httpClient); @@ -847,6 +847,7 @@ public void cachePutObjectsShouldCacheObjects() throws IOException { true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -888,6 +889,94 @@ public void cachePutObjectsShouldCacheObjects() throws IOException { .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); } + @Test + public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsMinBetweenRequestAndAccountTtl() throws IOException { + // given + final BidPutObject firstBidPutObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(99) + .build(); + final BidPutObject secondBidPutObject = BidPutObject.builder() + .type("xml") + .bidid("bidId2") + .bidder("bidder2") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(null) + .build(); + final BidPutObject thirdBidPutObject = BidPutObject.builder() + .type("text") + .bidid("bidId3") + .bidder("bidder3") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(101) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")) + .willReturn(new TextNode("VAST")) + .willReturn(new TextNode("updatedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"), CacheObject.of("uuid2"), CacheObject.of("uuid3"))))))); + + // when + target.cachePutObjects( + asList(firstBidPutObject, secondBidPutObject, thirdBidPutObject), + true, + singleton("bidder1"), + "account", + 100, + "pbjs", + timeout); + + // then + verify(httpClient).post(eq("http://cache-service/cache"), any(), any(), anyLong()); + + verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(11), eq(MetricName.unknown)); + + verify(metrics).updateCacheCreativeTtl(eq("account"), eq(99), eq(MetricName.json)); + verify(metrics).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.xml)); + verify(metrics).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.unknown)); + + verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.ok)); + + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), firstBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), secondBidPutObject, "account", "pbjs"); + + final BidPutObject modifiedFirstBidPutObject = firstBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(99) + .build(); + final BidPutObject modifiedSecondBidPutObject = secondBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .ttlseconds(100) + .build(); + final BidPutObject modifiedThirdBidPutObject = thirdBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("updatedVast")) + .ttlseconds(100) + .build(); + + assertThat(captureBidCacheRequest().getPuts()) + .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); + } + @Test public void cachePutObjectsShouldLogErrorMetricsWhenStatusCodeIsNotOk() { // given @@ -914,6 +1003,7 @@ public void cachePutObjectsShouldLogErrorMetricsWhenStatusCodeIsNotOk() { true, singleton("bidder1"), "account", + 100, "pbjs", timeout); @@ -950,6 +1040,7 @@ public void cachePutObjectsShouldNotLogErrorMetricsWhenCacheServiceIsNotConnecte true, singleton("bidder1"), "account", + 100, "pbjs", timeout); @@ -993,7 +1084,7 @@ public void cachePutObjectsShouldCallInternalCacheEndpointWhenProvided() throws .willReturn(new TextNode("modifiedVast")); // when - target.cachePutObjects(asList(firstBidPutObject), true, singleton("bidder1"), "account", "pbjs", timeout); + target.cachePutObjects(asList(firstBidPutObject), true, singleton("bidder1"), "account", 100, "pbjs", timeout); // then verify(httpClient).post(eq("http://cache-service-internal/cache"), any(), any(), anyLong()); @@ -1046,6 +1137,7 @@ public void cachePutObjectsShouldUseApiKeyWhenProvided() throws MalformedURLExce true, singleton("bidder1"), "account", + 100, "pbjs", timeout); @@ -1281,6 +1373,7 @@ public void cachePutObjectsShouldPrependTraceInfoWhenEnabled() throws IOExceptio true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -1329,6 +1422,7 @@ public void cachePutObjectsShouldPrependTraceInfoWithDatacenterWhenEnabled() thr true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -1376,6 +1470,7 @@ public void cachePutObjectsShouldNotPrependTraceInfoToPassedInKey() throws IOExc true, singleton("bidder1"), "account", + null, "pbjs", timeout); @@ -1435,6 +1530,7 @@ public void cachePutObjectsShouldNotEmitEmptyTtlMetrics() { true, singleton("bidder1"), "account", + null, "pbjs", timeout); diff --git a/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java index 98011db6ecf..0e2d73a41d8 100644 --- a/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java @@ -27,6 +27,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.settings.model.AccountVtrackConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -248,7 +249,7 @@ public void shouldRespondWithInternalServerErrorWhenCacheServiceReturnFailure() .events(AccountEventsConfig.of(true)) .build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.failedFuture("error")); // when @@ -273,15 +274,15 @@ public void shouldTolerateNotFoundAccount() throws JsonProcessingException { given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.failedFuture(new PreBidException("not found"))); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of(emptyList()))); // when handler.handle(routingContext); // then - verify(coreCacheService).cachePutObjects(eq(bidPutObjects), any(), eq(singleton("bidder")), eq("accountId"), - eq("pbjs"), any()); + verify(coreCacheService).cachePutObjects( + eq(bidPutObjects), any(), eq(singleton("bidder")), eq("accountId"), any(), eq("pbjs"), any()); } @Test @@ -299,15 +300,41 @@ public void shouldSendToCacheNullInAccountEnabledAndValidBiddersWhenAccountEvent given(applicationSettings.getAccountById(any(), any())) .willReturn(Future.succeededFuture(Account.builder().build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of(emptyList()))); // when handler.handle(routingContext); // then - verify(coreCacheService).cachePutObjects(eq(bidPutObjects), isNull(), eq(singleton("bidder")), eq("accountId"), - eq("pbjs"), any()); + verify(coreCacheService).cachePutObjects( + eq(bidPutObjects), isNull(), eq(singleton("bidder")), eq("accountId"), isNull(), eq("pbjs"), any()); + } + + @Test + public void shouldSendToCacheAccountTtlWhenAccountTtlIsPresent() + throws JsonProcessingException { + // given + final List bidPutObjects = singletonList( + BidPutObject.builder() + .bidid("bidId") + .bidder("bidder") + .type("xml") + .value(new TextNode(" expectedBidders = new HashSet<>(asList("bidder", "updatable_bidder")); verify(coreCacheService).cachePutObjects( - eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), eq("pbjs"), any()); + eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), isNull(), eq("pbjs"), any()); verify(httpResponse).end(eq("{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}")); } @@ -428,7 +455,7 @@ public void shouldSendToCacheExpectedPutsWhenModifyVastForUnknownBidderAndAllowU .events(AccountEventsConfig.of(true)) .build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of( asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"))))); @@ -438,7 +465,7 @@ public void shouldSendToCacheExpectedPutsWhenModifyVastForUnknownBidderAndAllowU // then final HashSet expectedBidders = new HashSet<>(asList("bidder", "updatable_bidder")); verify(coreCacheService).cachePutObjects( - eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), eq("pbjs"), any()); + eq(bidPutObjects), any(), eq(expectedBidders), eq("accountId"), isNull(), eq("pbjs"), any()); verify(httpResponse).end(eq("{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}")); } @@ -471,7 +498,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenAllowUnkn .willReturn(Future.succeededFuture(Account.builder().auction(AccountAuctionConfig.builder() .events(AccountEventsConfig.of(true)).build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of( asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"))))); @@ -479,7 +506,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenAllowUnkn handler.handle(routingContext); // then - verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any()); + verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any(), any()); } @Test @@ -510,7 +537,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenModifyVas .willReturn(Future.succeededFuture(Account.builder().auction(AccountAuctionConfig.builder() .events(AccountEventsConfig.of(true)).build()) .build())); - given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any())) + given(coreCacheService.cachePutObjects(any(), any(), any(), any(), any(), any(), any())) .willReturn(Future.succeededFuture(BidCacheResponse.of( asList(CacheObject.of("uuid1"), CacheObject.of("uuid2"))))); @@ -518,7 +545,7 @@ public void shouldSendToCacheWithEmptyBiddersAllowingVastUpdatePutsWhenModifyVas handler.handle(routingContext); // then - verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any()); + verify(coreCacheService).cachePutObjects(any(), any(), eq(emptySet()), any(), any(), any(), any()); } @SafeVarargs From ae78fc544af4a4093de86a3db622f862e734bd5e Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 20 Oct 2025 11:29:21 +0200 Subject: [PATCH 4/7] Handle negative ttl case --- .../prebid/server/cache/CoreCacheService.java | 9 +- .../server/cache/CoreCacheServiceTest.java | 185 +++++++++++++++++- .../server/handler/PostVtrackHandlerTest.java | 6 +- 3 files changed, 190 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index 95ed9bfd112..2fd5b2fb1b9 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -56,6 +56,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.function.Function; @@ -242,12 +243,18 @@ private List updatePutObjects(List bidPutObjects, putObject, accountId, integration)) - .ttlseconds(ObjectUtils.min(putObject.getTtlseconds(), accountTtl)) + .ttlseconds(resolveVtrackTtl(putObject.getTtlseconds(), accountTtl)) .build()) .map(payload -> CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))) .toList(); } + private static Integer resolveVtrackTtl(Integer initialObjectTtl, Integer initialAccountTtl) { + final Integer accountTtl = Optional.ofNullable(initialAccountTtl).filter(ttl -> ttl > 0).orElse(null); + final Integer objectTtl = Optional.ofNullable(initialObjectTtl).filter(ttl -> ttl > 0).orElse(null); + return ObjectUtils.min(objectTtl, accountTtl); + } + public Future cacheBidsOpenrtb(List bidsToCache, AuctionContext auctionContext, CacheContext cacheContext, diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 8a2039dc7a2..0746ea613c2 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -76,6 +76,7 @@ import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -916,6 +917,22 @@ public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsMinBetweenRequestAn .value(new TextNode("VAST")) .ttlseconds(101) .build(); + final BidPutObject fourthBidPutObject = BidPutObject.builder() + .type("json") + .bidid("bidId4") + .bidder("bidder4") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(-100) + .build(); + final BidPutObject fifthBidPutObject = BidPutObject.builder() + .type("xml") + .bidid("bidId4") + .bidder("bidder4") + .timestamp(1L) + .value(new TextNode("VAST")) + .ttlseconds(0) + .build(); given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) .willReturn(new TextNode("modifiedVast")) @@ -924,11 +941,16 @@ public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsMinBetweenRequestAn given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( - List.of(CacheObject.of("uuid1"), CacheObject.of("uuid2"), CacheObject.of("uuid3"))))))); + List.of( + CacheObject.of("uuid1"), + CacheObject.of("uuid2"), + CacheObject.of("uuid3"), + CacheObject.of("uuid4"), + CacheObject.of("uuid5"))))))); // when target.cachePutObjects( - asList(firstBidPutObject, secondBidPutObject, thirdBidPutObject), + asList(firstBidPutObject, secondBidPutObject, thirdBidPutObject, fourthBidPutObject, fifthBidPutObject), true, singleton("bidder1"), "account", @@ -942,15 +964,21 @@ public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsMinBetweenRequestAn verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); verify(metrics).updateCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); verify(metrics).updateCacheCreativeSize(eq("account"), eq(11), eq(MetricName.unknown)); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); verify(metrics).updateCacheCreativeTtl(eq("account"), eq(99), eq(MetricName.json)); - verify(metrics).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.xml)); + verify(metrics, times(2)).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.xml)); verify(metrics).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.unknown)); + verify(metrics).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.json)); verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.ok)); verify(vastModifier).modifyVastXml(true, singleton("bidder1"), firstBidPutObject, "account", "pbjs"); verify(vastModifier).modifyVastXml(true, singleton("bidder1"), secondBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), thirdBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), fourthBidPutObject, "account", "pbjs"); + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), fifthBidPutObject, "account", "pbjs"); final BidPutObject modifiedFirstBidPutObject = firstBidPutObject.toBuilder() .bidid(null) @@ -972,9 +1000,156 @@ public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsMinBetweenRequestAn .value(new TextNode("updatedVast")) .ttlseconds(100) .build(); + final BidPutObject modifiedFourthBidPutObject = fourthBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("updatedVast")) + .ttlseconds(100) + .build(); + final BidPutObject modifiedFifthBidPutObject = fifthBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("updatedVast")) + .ttlseconds(100) + .build(); - assertThat(captureBidCacheRequest().getPuts()) - .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); + assertThat(captureBidCacheRequest().getPuts()).containsExactly( + modifiedFirstBidPutObject, + modifiedSecondBidPutObject, + modifiedThirdBidPutObject, + modifiedFourthBidPutObject, + modifiedFifthBidPutObject); + } + + @Test + public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsPutObjectTtlWhenAccountTtlIsNegative() + throws IOException { + + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(100) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"))))))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + -20, + "pbjs", + timeout); + + // then + final BidPutObject expectedBidObject = bidObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(100) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly(expectedBidObject); + } + + @Test + public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsPutObjectTtlWhenAccountTtlIsZero() + throws IOException { + + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(100) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"))))))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + 0, + "pbjs", + timeout); + + // then + final BidPutObject expectedBidObject = bidObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(100) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly(expectedBidObject); + } + + @Test + public void cachePutObjectsShouldNotProvideTtlWhenAccountTtlIsNegativeAndPutObjectTtlIsZero() + throws IOException { + + // given + final BidPutObject bidObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(0) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")); + + given(httpClient.post(anyString(), any(), any(), anyLong())).willReturn(Future.succeededFuture( + HttpClientResponse.of(200, null, mapper.writeValueAsString(BidCacheResponse.of( + List.of(CacheObject.of("uuid1"))))))); + + // when + target.cachePutObjects( + singletonList(bidObject), + true, + singleton("bidder1"), + "account", + -10, + "pbjs", + timeout); + + // then + final BidPutObject expectedBidObject = bidObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .ttlseconds(null) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly(expectedBidObject); } @Test diff --git a/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java index 0e2d73a41d8..82d0e67c330 100644 --- a/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/PostVtrackHandlerTest.java @@ -312,8 +312,7 @@ public void shouldSendToCacheNullInAccountEnabledAndValidBiddersWhenAccountEvent } @Test - public void shouldSendToCacheAccountTtlWhenAccountTtlIsPresent() - throws JsonProcessingException { + public void shouldSendToCacheAccountTtlWhenAccountTtlIsPresent() throws JsonProcessingException { // given final List bidPutObjects = singletonList( BidPutObject.builder() @@ -321,8 +320,7 @@ public void shouldSendToCacheAccountTtlWhenAccountTtlIsPresent() .bidder("bidder") .type("xml") .value(new TextNode(" Date: Mon, 20 Oct 2025 17:03:12 +0300 Subject: [PATCH 5/7] Test: Vtrack account ttl (#4241) --- .../model/config/AccountConfig.groovy | 1 + .../model/config/AccountVtrackConfig.groovy | 9 ++ .../service/PrebidServerService.groovy | 2 +- .../server/functional/tests/CacheSpec.groovy | 122 +++++++++++++++++- 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy index 9dded674fee..83912535a57 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy @@ -26,6 +26,7 @@ class AccountConfig { AlternateBidderCodes alternateBidderCodes @JsonProperty("alternate_bidder_codes") AlternateBidderCodes alternateBidderCodesSnakeCase + AccountVtrackConfig vtrack static getDefaultAccountConfig() { new AccountConfig(status: AccountStatus.ACTIVE) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy new file mode 100644 index 00000000000..77397e20b4c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountVtrackConfig { + + Integer ttl +} diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index b9c173baa54..499f54d0272 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -214,7 +214,7 @@ class PrebidServerService implements ObjectMapperWrapper { } PrebidCacheResponse sendVtrackRequest(VtrackRequest request, String account) { - def response = given(requestSpecification).queryParam("a", account) + def response = given(requestSpecification).queryParams(["a": account]) .body(request) .post(VTRACK_ENDPOINT) diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy index b745ae53a1f..7a36ea011c2 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountCacheConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountEventsConfig +import org.prebid.server.functional.model.config.AccountVtrackConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.BidRequest @@ -13,8 +14,10 @@ import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static org.prebid.server.functional.model.response.auction.ErrorType.CACHE import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @@ -607,7 +610,7 @@ class CacheSpec extends BaseSpec { assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 and: "Bid response targeting should contain value" - verifyAll (bidResponse?.seatbid[0]?.bid[0]?.ext?.prebid?.targeting as Map) { + verifyAll(bidResponse?.seatbid[0]?.bid[0]?.ext?.prebid?.targeting as Map) { it.get("hb_cache_id") it.get("hb_cache_id_generic") it.get("hb_cache_path") == CACHE_PATH @@ -643,7 +646,7 @@ class CacheSpec extends BaseSpec { assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 and: "Bid response targeting should contain value" - verifyAll (bidResponse.seatbid[0].bid[0].ext.prebid.targeting) { + verifyAll(bidResponse.seatbid[0].bid[0].ext.prebid.targeting) { it.get("hb_cache_id") it.get("hb_cache_id_generic") it.get("hb_cache_path") == INTERNAL_CACHE_PATH @@ -785,4 +788,119 @@ class CacheSpec extends BaseSpec { where: enabledCacheConcfig << [null, false, true] } + + def "PBS should failed VTrack request when sending request without account"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendVtrackRequest(request, null) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Account 'a' is required query parameter and can't be empty" + } + + def "PBS shouldn't use negative value in tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[XML_CREATIVE_TTL_ACCOUNT_METRIC.formatted(accountId)] + == [requestedTtl, accountTtl].findAll { it -> it > 0 }.min() + + where: + requestedTtl | accountTtl + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + } + + def "PBS should use lowest tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[XML_CREATIVE_TTL_ACCOUNT_METRIC.formatted(accountId)] == [requestedTtl, accountTtl].min() + + where: + requestedTtl | accountTtl + null | null + null | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNumber(300, 1500) as Integer | null + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + } + + def "PBS should proceed request when account ttl and request ttl second are empty"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = null + } + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: null) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendVtrackRequest(request, accountId) + + then: "Pbs shouldn't emit creative_ttl.xml" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[XML_CREATIVE_TTL_ACCOUNT_METRIC.formatted(accountId)] + } } From 35462610745e020bc334c82caa6b7064eaa16d03 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Wed, 22 Oct 2025 15:54:58 +0200 Subject: [PATCH 6/7] Fix conflicts --- .../server/cache/CoreCacheServiceTest.java | 92 ++----------------- 1 file changed, 10 insertions(+), 82 deletions(-) diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 9cd0b593afc..9138183597f 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -963,16 +963,16 @@ public void cachePutObjectsShouldCacheObjectsWithTtlDefinedAsMinBetweenRequestAn // then verify(httpClient).post(eq("http://cache-service/cache"), any(), any(), anyLong()); - verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); - verify(metrics).updateCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); - verify(metrics).updateCacheCreativeSize(eq("account"), eq(11), eq(MetricName.unknown)); - verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); - verify(metrics).updateCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(11), eq(MetricName.unknown)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); - verify(metrics).updateCacheCreativeTtl(eq("account"), eq(99), eq(MetricName.json)); - verify(metrics, times(2)).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.xml)); - verify(metrics).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.unknown)); - verify(metrics).updateCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.json)); + verify(metrics).updateVtrackCacheCreativeTtl(eq("account"), eq(99), eq(MetricName.json)); + verify(metrics, times(2)).updateVtrackCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.xml)); + verify(metrics).updateVtrackCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.unknown)); + verify(metrics).updateVtrackCacheCreativeTtl(eq("account"), eq(100), eq(MetricName.json)); verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.ok)); @@ -1184,79 +1184,6 @@ public void cachePutObjectsShouldLogErrorMetricsWhenStatusCodeIsNotOk() { "pbjs", timeout); - // then - verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); - verify(metrics).updateCacheCreativeTtl(eq("account"), eq(1), eq(MetricName.json)); - verify(metrics).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), eq(MetricName.err)); - verify(vastModifier).modifyVastXml(true, singleton("bidder1"), bidObject, "account", "pbjs"); - } - - @Test - public void cachePutObjectsShouldNotLogErrorMetricsWhenCacheServiceIsNotConnected() { - // given - final BidPutObject bidObject = BidPutObject.builder() - .type("json") - .bidid("bidId1") - .bidder("bidder1") - .timestamp(1L) - .value(new TextNode("vast")) - .ttlseconds(1) - .build(); - - given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) - .willReturn(new TextNode("modifiedVast")) - .willReturn(new TextNode("VAST")) - .willReturn(new TextNode("updatedVast")); - - given(httpClient.post(eq("http://cache-service/cache"), any(), any(), anyLong())) - .willReturn(Future.failedFuture(new TimeoutException("Timeout"))); - - // when - target.cachePutObjects( - singletonList(bidObject), - true, - singleton("bidder1"), - "account", - 100, - "pbjs", - timeout); - - // then - verify(metrics, never()).updateVtrackCacheWriteRequestTime(eq("account"), anyLong(), any()); - verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); - verify(metrics).updateCacheCreativeTtl(eq("account"), eq(1), eq(MetricName.json)); - verify(vastModifier).modifyVastXml(true, singleton("bidder1"), bidObject, "account", "pbjs"); - } - - @Test - public void cachePutObjectsShouldLogErrorMetricsWhenStatusCodeIsNotOk() { - // given - final BidPutObject bidObject = BidPutObject.builder() - .type("json") - .bidid("bidId1") - .bidder("bidder1") - .timestamp(1L) - .value(new TextNode("vast")) - .ttlseconds(1) - .build(); - - given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) - .willReturn(new TextNode("modifiedVast")) - .willReturn(new TextNode("VAST")) - .willReturn(new TextNode("updatedVast")); - - given(httpClient.post(eq("http://cache-service/cache"), any(), any(), anyLong())) - .willReturn(Future.succeededFuture(HttpClientResponse.of(404, null, null))); - - // when - target.cachePutObjects( - singletonList(bidObject), - true, - singleton("bidder1"), - "account", - "pbjs", - timeout); - // then verify(metrics).updateVtrackCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); verify(metrics).updateVtrackCacheCreativeTtl(eq("account"), eq(1), eq(MetricName.json)); @@ -1290,6 +1217,7 @@ public void cachePutObjectsShouldNotLogErrorMetricsWhenCacheServiceIsNotConnecte true, singleton("bidder1"), "account", + 100, "pbjs", timeout); From 2408519e89c7a7a4bd3edebdbc9e0dc7e923ddbf Mon Sep 17 00:00:00 2001 From: markiian Date: Thu, 23 Oct 2025 11:51:54 +0300 Subject: [PATCH 7/7] Resolve test conflict --- .../server/functional/tests/CacheSpec.groovy | 118 ------- .../functional/tests/CacheVtrackSpec.groovy | 291 +++++++++++++----- 2 files changed, 207 insertions(+), 202 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy index ad90ce8f0dc..ce17a673424 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -3,7 +3,6 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountCacheConfig import org.prebid.server.functional.model.config.AccountConfig -import org.prebid.server.functional.model.config.AccountVtrackConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.BidRequest @@ -11,10 +10,8 @@ import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static org.prebid.server.functional.model.response.auction.ErrorType.CACHE import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @@ -707,119 +704,4 @@ class CacheSpec extends BaseSpec { assert !targetingKeyMap.containsKey('hb_uuid') assert !targetingKeyMap.containsKey("hb_uuid_${GENERIC}".toString()) } - - def "PBS should failed VTrack request when sending request without account"() { - given: "Default VtrackRequest" - def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) - def request = VtrackRequest.getDefaultVtrackRequest(creative) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendVtrackRequest(request, null) - - then: "Request should fail with an error" - def exception = thrown(PrebidServerException) - assert exception.statusCode == BAD_REQUEST.code() - assert exception.responseBody == "Account 'a' is required query parameter and can't be empty" - } - - def "PBS shouldn't use negative value in tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { - given: "Default VtrackRequest" - def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) - def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { - puts[0].ttlseconds = requestedTtl - } - - and: "Create and save vtrack in account" - def accountId = PBSUtils.randomNumber.toString() - def account = new Account().tap { - it.uuid = accountId - it.config = new AccountConfig().tap { - it.vtrack = new AccountVtrackConfig(ttl: accountTtl) - } - } - accountDao.save(account) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendVtrackRequest(request, accountId) - - then: "Pbs should emit creative_ttl.xml with lowest value" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics[XML_CREATIVE_TTL_ACCOUNT_METRIC.formatted(accountId)] - == [requestedTtl, accountTtl].findAll { it -> it > 0 }.min() - - where: - requestedTtl | accountTtl - PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer - PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer - PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer - } - - def "PBS should use lowest tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { - given: "Default VtrackRequest" - def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) - def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { - puts[0].ttlseconds = requestedTtl - } - - and: "Create and save vtrack in account" - def accountId = PBSUtils.randomNumber.toString() - def account = new Account().tap { - it.uuid = accountId - it.config = new AccountConfig().tap { - it.vtrack = new AccountVtrackConfig(ttl: accountTtl) - } - } - accountDao.save(account) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendVtrackRequest(request, accountId) - - then: "Pbs should emit creative_ttl.xml with lowest value" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics[XML_CREATIVE_TTL_ACCOUNT_METRIC.formatted(accountId)] == [requestedTtl, accountTtl].min() - - where: - requestedTtl | accountTtl - null | null - null | PBSUtils.getRandomNumber(300, 1500) as Integer - PBSUtils.getRandomNumber(300, 1500) as Integer | null - PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer - } - - def "PBS should proceed request when account ttl and request ttl second are empty"() { - given: "Default VtrackRequest" - def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) - def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { - puts[0].ttlseconds = null - } - - and: "Create and save vtrack in account" - def accountId = PBSUtils.randomNumber.toString() - def account = new Account().tap { - it.uuid = accountId - it.config = new AccountConfig().tap { - it.vtrack = new AccountVtrackConfig(ttl: null) - } - } - accountDao.save(account) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendVtrackRequest(request, accountId) - - then: "Pbs shouldn't emit creative_ttl.xml" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert !metrics[XML_CREATIVE_TTL_ACCOUNT_METRIC.formatted(accountId)] - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy index e96fc519fe5..e5637fe80e2 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountCacheConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountEventsConfig +import org.prebid.server.functional.model.config.AccountVtrackConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast @@ -55,6 +56,212 @@ class CacheVtrackSpec extends BaseSpec { prebidCache.reset() } + def "PBS should update prebid_cache.creative_size.xml metric and adding tracking xml when xml creative contain #wrapper and impression are valid xml value"() { + given: "Current value of metric prebid_cache.vtrack.write.ok" + def initialOkVTrackValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) + + and: "Create and save enabled events config in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + uuid = accountId + config = new AccountConfig().tap { + auction = new AccountAuctionConfig(events: new AccountEventsConfig(enabled: true)) + } + } + accountDao.save(account) + + and: "Set up prebid cache" + prebidCache.setResponse() + + and: "Vtrack request with custom tags" + def payload = PBSUtils.randomString + def creative = "<${wrapper}>prebid.org wrapper" + + "<![CDATA[//${payload}]]>" + + "<${impression}> <![CDATA[ ]]> " + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Vast xml is modified" + def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) + assert prebidCacheRequest.size() == 1 + assert prebidCacheRequest[0].contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") + + and: "prebid_cache.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + def ttlSeconds = request.puts[0].ttlseconds + assert metrics[VTRACK_WRITE_OK_METRIC] == initialOkVTrackValue + 1 + assert metrics[VTRACK_XML_CREATIVE_TTL_METRIC] == ttlSeconds + + and: "account..prebid_cache.vtrack.creative_size.xml should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId) as String] == 1 + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId) as String] == ttlSeconds + + where: + wrapper | impression + " wrapper " | " impression " + PBSUtils.getRandomCase(" wrapper ") | PBSUtils.getRandomCase(" impression ") + " wraPPer ${PBSUtils.getRandomString()} " | " imPreSSion ${PBSUtils.getRandomString()}" + " inLine " | " ImpreSSion $PBSUtils.randomNumber" + PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber " + " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " + } + + def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Default VtrackRequest" + def accountId = PBSUtils.randomNumber.toString() + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "prebid_cache.vtrack.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + def creativeSize = creative.bytes.length + assert metrics[VTRACK_WRITE_OK_METRIC] == initialValue + 1 + + and: "account..prebid_cache.creative_size.xml should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId)] == 1 + assert metrics[ACCOUNT_VTRACK_XML_CREATIVE_SIZE_METRIC.formatted(accountId)] == creativeSize + } + + def "PBS should failed VTrack request when sending request without account"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, null) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Account 'a' is required query parameter and can't be empty" + } + + def "PBS shouldn't use negative value in tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] + == [requestedTtl, accountTtl].findAll { it -> it > 0 }.min() + where: + requestedTtl | accountTtl + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + } + + def "PBS should use lowest tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] == [requestedTtl, accountTtl].min() + + where: + requestedTtl | accountTtl + null | null + null | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNumber(300, 1500) as Integer | null + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + } + + def "PBS should proceed request when account ttl and request ttl second are empty"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = null + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: null) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs shouldn't emit creative_ttl.xml" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] + } + def "PBS should return 400 status code when get vtrack request without uuid"() { when: "PBS processes get vtrack request" defaultPbsService.sendGetVtrackRequest(["uuid": null]) @@ -222,90 +429,6 @@ class CacheVtrackSpec extends BaseSpec { assert exception.responseBody == "'uuid' is a required query parameter and can't be empty" } - def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() { - given: "Current value of metric prebid_cache.requests.ok" - def initialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) - - and: "Cache set up response" - prebidCache.setResponse() - - and: "Default VtrackRequest" - def accountId = PBSUtils.randomNumber.toString() - def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) - def request = VtrackRequest.getDefaultVtrackRequest(creative) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendPostVtrackRequest(request, accountId) - - then: "prebid_cache.vtrack.creative_size.xml metric should be updated" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - def creativeSize = creative.bytes.length - assert metrics[VTRACK_WRITE_OK_METRIC] == initialValue + 1 - assert metrics[VTRACK_XML_CREATIVE_SIZE_METRIC] == creativeSize - - and: "account..prebid_cache.creative_size.xml should be updated" - assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId)] == 1 - assert metrics[ACCOUNT_VTRACK_XML_CREATIVE_SIZE_METRIC.formatted(accountId)] == creativeSize - } - - def "PBS should update prebid_cache.creative_size.xml metric and adding tracking xml when xml creative contain #wrapper and impression are valid xml value"() { - given: "Current value of metric prebid_cache.vtrack.write.ok" - def initialOkVTrackValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) - - and: "Create and save enabled events config in account" - def accountId = PBSUtils.randomNumber.toString() - def account = new Account().tap { - uuid = accountId - config = new AccountConfig().tap { - auction = new AccountAuctionConfig(events: new AccountEventsConfig(enabled: true)) - } - } - accountDao.save(account) - - and: "Set up prebid cache" - prebidCache.setResponse() - - and: "Vtrack request with custom tags" - def payload = PBSUtils.randomString - def creative = "<${wrapper}>prebid.org wrapper" + - "<![CDATA[//${payload}]]>" + - "<${impression}> <![CDATA[ ]]> " - def request = VtrackRequest.getDefaultVtrackRequest(creative) - - and: "Flush metrics" - flushMetrics(defaultPbsService) - - when: "PBS processes vtrack request" - defaultPbsService.sendPostVtrackRequest(request, accountId) - - then: "Vast xml is modified" - def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) - assert prebidCacheRequest.size() == 1 - assert prebidCacheRequest[0].contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") - - and: "prebid_cache.creative_size.xml metric should be updated" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - def ttlSeconds = request.puts[0].ttlseconds - assert metrics[VTRACK_WRITE_OK_METRIC] == initialOkVTrackValue + 1 - assert metrics[VTRACK_XML_CREATIVE_TTL_METRIC] == ttlSeconds - - and: "account..prebid_cache.vtrack.creative_size.xml should be updated" - assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId) as String] == 1 - assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId) as String] == ttlSeconds - - where: - wrapper | impression - " wrapper " | " impression " - PBSUtils.getRandomCase(" wrapper ") | PBSUtils.getRandomCase(" impression ") - " wraPPer ${PBSUtils.getRandomString()} " | " imPreSSion ${PBSUtils.getRandomString()}" - " inLine " | " ImpreSSion $PBSUtils.randomNumber" - PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber " - " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " - } - def "PBS should update prebid_cache.creative_size.xml metric when account cache config #enabledCacheConcfig"() { given: "Current value of metric prebid_cache.requests.ok" def okInitialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC)