Skip to content

Commit 587d6fb

Browse files
author
ilya
committed
DATA-22937: WIP
1 parent 5f15bf1 commit 587d6fb

7 files changed

Lines changed: 327 additions & 3 deletions

File tree

extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
import com.iab.openrtb.request.User;
66
import io.vertx.core.Future;
77
import io.vertx.core.MultiMap;
8+
import org.prebid.server.activity.Activity;
89
import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
10+
import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
11+
import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
12+
import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
913
import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
1014
import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse;
1115
import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.ModuleConfig;
@@ -39,6 +43,8 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements
3943
private final JacksonMapper mapper;
4044
private final HttpClient httpClient;
4145
private final RandomGenerator random;
46+
private final ActivityImpl enriched = ActivityImpl.of("liveintent-enriched", "success", List.of());
47+
private final ActivityImpl treatmentRate;
4248

4349
public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(
4450
ModuleConfig config,
@@ -50,6 +56,11 @@ public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(
5056
this.mapper = Objects.requireNonNull(mapper);
5157
this.httpClient = Objects.requireNonNull(httpClient);
5258
this.random = Objects.requireNonNull(random);
59+
this.treatmentRate = ActivityImpl.of(
60+
"liveintent-treatment-rate",
61+
String.valueOf(config.getTreatmentRate()),
62+
List.of()
63+
);
5364
}
5465

5566
@Override
@@ -63,13 +74,15 @@ public Future<InvocationResult<AuctionRequestPayload>> call(
6374
.status(InvocationStatus.success)
6475
.action(InvocationAction.update)
6576
.payloadUpdate(requestPayload -> updatedPayload(requestPayload, resolutionResult))
77+
.analyticsTags(TagsImpl.of(List.of(enriched, treatmentRate)))
6678
.build())
6779
.onFailure(throwable -> logger.error("Failed enrichment:", throwable));
6880
}
6981
return Future.succeededFuture(
7082
InvocationResultImpl.<AuctionRequestPayload>builder()
7183
.status(InvocationStatus.success)
7284
.action(InvocationAction.no_action)
85+
.analyticsTags(TagsImpl.of(List.of(treatmentRate)))
7386
.build());
7487

7588
}

extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,9 @@ public void setUp() {
6464
moduleConfig.setRequestTimeoutMs(5);
6565
moduleConfig.setIdentityResolutionEndpoint("https://test.com/idres");
6666
moduleConfig.setAuthToken("secret_auth_token");
67-
moduleConfig.setTreatmentRate(0.9f);
6867

6968
target = new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(
70-
moduleConfig, jacksonMapper, httpClient, random);
71-
69+
moduleConfig, 0.9f, jacksonMapper, httpClient, random);
7270
}
7371

7472
@Test
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package org.prebid.server.analytics.reporter.liveintent;
2+
3+
import com.iab.openrtb.request.BidRequest;
4+
import com.iab.openrtb.response.BidResponse;
5+
import io.vertx.core.Future;
6+
import org.prebid.server.analytics.AnalyticsReporter;
7+
import org.prebid.server.analytics.model.AuctionEvent;
8+
import org.prebid.server.analytics.model.NotificationEvent;
9+
import org.prebid.server.analytics.reporter.liveintent.model.LiveIntentAnalyticsProperties;
10+
import org.prebid.server.analytics.reporter.liveintent.model.PbsjBid;
11+
import org.prebid.server.auction.model.AuctionContext;
12+
import org.prebid.server.exception.PreBidException;
13+
import org.prebid.server.hooks.execution.model.ExecutionStatus;
14+
import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
15+
import org.prebid.server.hooks.execution.model.HookExecutionContext;
16+
import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
17+
import org.prebid.server.hooks.execution.model.Stage;
18+
import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
19+
import org.prebid.server.hooks.v1.analytics.Activity;
20+
import org.prebid.server.hooks.v1.analytics.Tags;
21+
import org.prebid.server.json.JacksonMapper;
22+
import org.prebid.server.log.Logger;
23+
import org.prebid.server.log.LoggerFactory;
24+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
25+
import org.prebid.server.vertx.httpclient.HttpClient;
26+
27+
import java.util.Collection;
28+
import java.util.List;
29+
import java.util.Objects;
30+
import java.util.Optional;
31+
import java.util.stream.Stream;
32+
33+
public class LiveIntentAnalyticsReporter implements AnalyticsReporter {
34+
35+
private final HttpClient httpClient;
36+
private final LiveIntentAnalyticsProperties properties;
37+
private final JacksonMapper jacksonMapper;
38+
39+
private static final Logger logger = LoggerFactory.getLogger(LiveIntentAnalyticsReporter.class);
40+
41+
public LiveIntentAnalyticsReporter(
42+
LiveIntentAnalyticsProperties properties,
43+
HttpClient httpClient,
44+
JacksonMapper jacksonMapper
45+
) {
46+
this.httpClient = Objects.requireNonNull(httpClient);
47+
this.properties = Objects.requireNonNull(properties);
48+
this.jacksonMapper = Objects.requireNonNull(jacksonMapper);
49+
}
50+
51+
@Override
52+
public <T> Future<Void> processEvent(T event) {
53+
if (event instanceof AuctionEvent auctionEvent) {
54+
return processAuctionEvent(auctionEvent.getAuctionContext());
55+
} else if (event instanceof NotificationEvent notificationEvent) {
56+
return processNotificationEvent(notificationEvent);
57+
}
58+
59+
return Future.succeededFuture();
60+
}
61+
62+
private Future<Void> processNotificationEvent(NotificationEvent notificationEvent) {
63+
final String url = properties.getAnalyticsEndpoint() + "/analytic-events/pbsj-winning-bid?"
64+
+ "b=" + notificationEvent.getBidder()
65+
+ "&bidId=" + notificationEvent.getBidId();
66+
return httpClient.get(url, properties.getTimeoutMs()).mapEmpty();
67+
}
68+
69+
private Future<Void> processAuctionEvent(AuctionContext auctionContext) {
70+
try {
71+
final BidRequest bidRequest = Optional.ofNullable(auctionContext.getBidRequest())
72+
.orElseThrow(() -> new PreBidException("Bid response should not be empty"));
73+
final BidResponse bidResponse = Optional.ofNullable(auctionContext.getBidResponse())
74+
.orElseThrow(() -> new PreBidException("Bid response should not be empty"));
75+
final Optional<ExtRequestPrebid> requestPrebid = Optional.ofNullable(bidRequest.getExt())
76+
.flatMap(ext -> Optional.of(ext.getPrebid()));
77+
78+
final Stream<Activity> activities = getActivities(auctionContext);
79+
80+
final List<PbsjBid> pbsjBids = bidResponse.getSeatbid().stream()
81+
.flatMap(seatBid -> seatBid.getBid().stream())
82+
.flatMap(bid ->
83+
bidRequest.getImp().stream()
84+
.filter(impItem -> impItem.getId().equals(bid.getImpid()))
85+
.findFirst()
86+
.stream()
87+
.map(imp ->
88+
PbsjBid.builder()
89+
.bidId(bid.getId())
90+
.price(bid.getPrice())
91+
.adUnitId(imp.getTagid())
92+
.enriched(isEnriched(activities))
93+
.currency(bidResponse.getCur())
94+
.treatmentRate(getTreatmentRate(activities))
95+
.timestamp(requestPrebid.map(ExtRequestPrebid::getAuctiontimestamp).orElse(0L))
96+
.partnerId(properties.getPartnerId())
97+
.build()
98+
)
99+
).toList();
100+
101+
return httpClient.post(
102+
properties.getAnalyticsEndpoint() + "/analytic-events/pbsj-bids",
103+
jacksonMapper.encodeToString(pbsjBids),
104+
properties.getTimeoutMs()
105+
).mapEmpty();
106+
} catch (Exception e) {
107+
logger.error("Error processing event: {}", e.getMessage());
108+
return Future.failedFuture(e);
109+
}
110+
}
111+
112+
private Stream<Activity> getActivities(AuctionContext auctionContext) {
113+
return Optional.ofNullable(auctionContext)
114+
.map(AuctionContext::getHookExecutionContext)
115+
.map(HookExecutionContext::getStageOutcomes)
116+
.map(stages -> stages.get(Stage.processed_auction_request)).stream()
117+
.flatMap(Collection::stream)
118+
.filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity()))
119+
.map(StageExecutionOutcome::getGroups)
120+
.flatMap(Collection::stream)
121+
.map(GroupExecutionOutcome::getHooks)
122+
.flatMap(Collection::stream)
123+
.filter(hook ->
124+
"liveintent-omni-channel-identity-enrichment-hook".equals(hook.getHookId().getModuleCode())
125+
&& hook.getStatus() == ExecutionStatus.success
126+
)
127+
.map(HookExecutionOutcome::getAnalyticsTags)
128+
.map(Tags::activities)
129+
.flatMap(Collection::stream);
130+
}
131+
132+
private Float getTreatmentRate(Stream<Activity> activities) {
133+
return activities
134+
.filter(activity -> "liveintent-treatment-rate".equals(activity.name()))
135+
.findFirst()
136+
.map(activity -> {
137+
try {
138+
return Float.parseFloat(activity.status());
139+
} catch (NumberFormatException e) {
140+
logger.warn("Invalid treatment rate value: {}", activity.status());
141+
throw e;
142+
}
143+
})
144+
.get();
145+
}
146+
147+
private boolean isEnriched(Stream<Activity> activities) {
148+
return activities.anyMatch(activity -> "liveintent-enriched".equals(activity.name()));
149+
}
150+
151+
@Override
152+
public int vendorId() {
153+
return 0;
154+
}
155+
156+
@Override
157+
public String name() {
158+
return "liveintentAnalytics";
159+
}
160+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.prebid.server.analytics.reporter.liveintent.model;
2+
3+
import lombok.Builder;
4+
import lombok.Data;
5+
import lombok.Value;
6+
7+
@Data
8+
@Builder(toBuilder = true)
9+
@Value
10+
public class LiveIntentAnalyticsProperties {
11+
12+
String partnerId;
13+
String analyticsEndpoint;
14+
long timeoutMs;
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.prebid.server.analytics.reporter.liveintent.model;
2+
3+
import lombok.Builder;
4+
import lombok.Data;
5+
import lombok.Value;
6+
7+
import java.math.BigDecimal;
8+
9+
@Data
10+
@Builder(toBuilder = true)
11+
@Value
12+
public class PbsjBid {
13+
14+
String bidId;
15+
boolean enriched;
16+
BigDecimal price;
17+
String adUnitId;
18+
String currency;
19+
Float treatmentRate;
20+
Long timestamp;
21+
String partnerId;
22+
}

src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties;
1313
import org.prebid.server.analytics.reporter.greenbids.GreenbidsAnalyticsReporter;
1414
import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties;
15+
import org.prebid.server.analytics.reporter.liveintent.LiveIntentAnalyticsReporter;
16+
import org.prebid.server.analytics.reporter.liveintent.model.LiveIntentAnalyticsProperties;
1517
import org.prebid.server.analytics.reporter.log.LogAnalyticsReporter;
1618
import org.prebid.server.analytics.reporter.pubstack.PubstackAnalyticsReporter;
1719
import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties;
@@ -302,4 +304,46 @@ private static class PubstackBufferProperties {
302304
Long reportTtlMs;
303305
}
304306
}
307+
308+
@Configuration
309+
@ConditionalOnProperty(prefix = "analytics.liveintent", name = "enabled", havingValue = "true")
310+
public static class LiveIntentAnalyticsConfiguration {
311+
312+
@Bean
313+
LiveIntentAnalyticsReporter liveIntentAnalyticsReporter(
314+
LiveIntentAnalyticsConfigurationProperties properties,
315+
HttpClient httpClient,
316+
JacksonMapper jacksonMapper
317+
) {
318+
return new LiveIntentAnalyticsReporter(
319+
properties.toComponentProperties(),
320+
httpClient,
321+
jacksonMapper
322+
);
323+
}
324+
325+
@Bean
326+
@ConfigurationProperties(prefix = "analytics.liveintent")
327+
LiveIntentAnalyticsConfigurationProperties liveIntentAnalyticsConfigurationProperties() {
328+
return new LiveIntentAnalyticsConfigurationProperties();
329+
}
330+
331+
@Validated
332+
@NoArgsConstructor
333+
@Data
334+
private static class LiveIntentAnalyticsConfigurationProperties {
335+
336+
String partnerId;
337+
String analyticsEndpoint;
338+
long timeoutMs;
339+
340+
public LiveIntentAnalyticsProperties toComponentProperties() {
341+
return LiveIntentAnalyticsProperties.builder()
342+
.partnerId(this.partnerId)
343+
.analyticsEndpoint(this.analyticsEndpoint)
344+
.timeoutMs(this.timeoutMs)
345+
.build();
346+
}
347+
}
348+
}
305349
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.prebid.server.analytics.reporter.liveintent;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.vertx.core.Future;
5+
import io.vertx.core.MultiMap;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.mockito.ArgumentCaptor;
9+
import org.mockito.Captor;
10+
import org.mockito.Mock;
11+
import org.junit.jupiter.api.Test;
12+
import org.mockito.junit.jupiter.MockitoExtension;
13+
import org.prebid.server.VertxTest;
14+
import org.prebid.server.analytics.model.NotificationEvent;
15+
import org.prebid.server.analytics.reporter.liveintent.model.LiveIntentAnalyticsProperties;
16+
import org.prebid.server.json.JacksonMapper;
17+
import org.prebid.server.vertx.httpclient.HttpClient;
18+
import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
19+
20+
import static org.mockito.ArgumentMatchers.anyLong;
21+
import static org.mockito.ArgumentMatchers.anyString;
22+
import static org.mockito.ArgumentMatchers.eq;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.verify;
25+
import static org.mockito.Mockito.when;
26+
27+
@ExtendWith(MockitoExtension.class)
28+
public class LiveintentAnalyticsReporterTest extends VertxTest {
29+
30+
private static final HttpClientResponse SUCCESS_RESPONSE = HttpClientResponse.of(
31+
200, MultiMap.caseInsensitiveMultiMap(), "OK");
32+
33+
@Mock
34+
private HttpClient httpClient;
35+
36+
private LiveIntentAnalyticsReporter target;
37+
38+
private LiveIntentAnalyticsProperties properties;
39+
40+
private JacksonMapper jacksonMapper;
41+
42+
@BeforeEach
43+
public void setUp() {
44+
final ObjectMapper mapper = new ObjectMapper();
45+
jacksonMapper = new JacksonMapper(mapper);
46+
47+
properties = LiveIntentAnalyticsProperties.builder()
48+
.analyticsEndpoint("https://localhost:8080")
49+
.partnerId("pbsj")
50+
.timeoutMs(1000L)
51+
.build();
52+
53+
target = new LiveIntentAnalyticsReporter(
54+
properties,
55+
httpClient,
56+
jacksonMapper);
57+
}
58+
59+
@Test
60+
public void shouldProcessNotificationEvent() {
61+
62+
// when
63+
target.processEvent(NotificationEvent.builder().bidId("123").bidder("foo").build());
64+
final HttpClientResponse mockResponse = mock(HttpClientResponse.class);
65+
66+
when(httpClient.get(anyString(), anyLong())).thenReturn(Future.succeededFuture(mockResponse));
67+
// then
68+
// Verify that the HTTP client was called with the expected parameters
69+
verify(httpClient).get(eq(properties.getAnalyticsEndpoint() + "/analytic-events/pbsj-winning-bid")
70+
+ "?b=foo&bidId=123", eq(properties.getTimeoutMs()));
71+
}
72+
}

0 commit comments

Comments
 (0)