Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
bf8c1fa
WIP - LiveIntent Module
3link Apr 25, 2025
8429f34
Adjust the hook code
3link Apr 25, 2025
459028d
WIP - polish, add config tests
3link Apr 25, 2025
c846d1c
Add unit tests
3link Apr 28, 2025
8348d46
Add auth token
3link Apr 29, 2025
c0f6566
Add README.md
3link May 6, 2025
4f90c17
Fix stage
3link May 6, 2025
c6dc20e
Add logging
3link May 7, 2025
a863d0a
Add docs
3link May 7, 2025
62a4508
Format
3link May 7, 2025
e45ccbc
Impro docs
3link May 7, 2025
3dbb46a
Add IdResResponse decode test
3link May 7, 2025
69fc413
Improve code style
3link May 7, 2025
c1ad22f
Fix Collections API usage
3link May 7, 2025
26288b7
Clean up/format
3link May 14, 2025
4192fa8
Merge branch 'prebid:master' into cm-1776
SuperIzya May 15, 2025
1f1a7cf
Format
3link May 19, 2025
c9659ad
Add treatment rate
3link May 19, 2025
3ed7ee8
Remove superflous JsonProperty annotation
3link Jun 11, 2025
bedb969
Separate field by line
3link Jun 11, 2025
d94e06f
Use RandomGenerator + ThreadLocalRandom instead of Random
3link Jun 11, 2025
61ceeed
Apply code style
3link Jun 11, 2025
fa66d18
Apply style guide: method order
3link Jun 11, 2025
09450d2
Add empty lines to separate test stages
3link Jun 11, 2025
b588f6a
Use NoArgsConstructor instead of Jacksonized
3link Jun 11, 2025
1ae6da8
Merge branch 'master' into cm-1776
3link Jun 11, 2025
f597339
Bump dependency version
3link Jun 11, 2025
5f15bf1
cm-1776: PR issues fixed
Jul 29, 2025
587d6fb
DATA-22937: WIP
Jul 29, 2025
5bbb155
DATA-22937: LI Analytic module.
Jul 30, 2025
432625a
DATA-22937: LI Analytic module.
Jul 30, 2025
bc299e9
DATA-22937: LI Analytic module.
Jul 30, 2025
7d50a0b
Merge branch 'master' into DATA-22937
Aug 25, 2025
61cdcaf
DATA-22937: Minor fixes after merge with master
Aug 25, 2025
9c39379
DATA-22937: PR issues fixed
Sep 2, 2025
704b89a
DATA-22937: PR issues fixed
Sep 5, 2025
a7b75b7
DATA-22937: PR issues fixed
Sep 5, 2025
2d9cc59
DATA-36551: PR compilation issues fixed
Sep 15, 2025
58fc8c0
DATA-22937: PR issues fixed
Sep 18, 2025
d946509
DATA-22937: PR issues fixed
Sep 18, 2025
d2eefef
DATA-22937: PR issues fixed
Sep 19, 2025
359ed36
DATA-22937: Fixing linting
Sep 23, 2025
be5bf0b
DATA-22937: PR issues fixed
Sep 24, 2025
3f65d47
DATA-22937: Build fixed
Sep 26, 2025
7d11882
update test
peixunzhang Oct 23, 2025
3e02eff
formatting
peixunzhang Oct 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config;

import lombok.Data;

@Data
public final class ModuleConfig {

long requestTimeoutMs;

String identityResolutionEndpoint;

String authToken;

float treatmentRate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import io.vertx.core.MultiMap;
import org.apache.commons.collections4.ListUtils;
import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse;
import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties;
Expand Down Expand Up @@ -40,6 +42,8 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements
private final JacksonMapper mapper;
private final HttpClient httpClient;
private final double logSamplingRate;
private final ActivityImpl enriched = ActivityImpl.of("liveintent-enriched", "success", List.of());
private final ActivityImpl treatmentRate;

public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniChannelProperties config,
JacksonMapper mapper,
Expand All @@ -51,6 +55,11 @@ public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniCh
this.mapper = Objects.requireNonNull(mapper);
this.httpClient = Objects.requireNonNull(httpClient);
this.logSamplingRate = logSamplingRate;
this.treatmentRate = ActivityImpl.of(
"liveintent-treatment-rate",
String.valueOf(config.getTreatmentRate()),
List.of()
);
}

@Override
Expand Down Expand Up @@ -95,6 +104,7 @@ private InvocationResultImpl<AuctionRequestPayload> update(IdResResponse resolut
.status(InvocationStatus.success)
.action(InvocationAction.update)
.payloadUpdate(payload -> updatedPayload(payload, resolutionResult.getEids()))
.analyticsTags(TagsImpl.of(List.of(enriched, treatmentRate)))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.iab.openrtb.request.Eid;
import com.iab.openrtb.request.Uid;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse;
import org.prebid.server.json.JacksonMapper;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

public class IdResResponseTest {

private JacksonMapper jacksonMapper;

@BeforeEach
public void setUp() {
final ObjectMapper mapper = new ObjectMapper();
jacksonMapper = new JacksonMapper(mapper);
}

@Test
public void shouldDecodeFromString() {
// given
final IdResResponse result = jacksonMapper.decodeValue(
"{\"eids\": [ { \"source\": \"liveintent.com\", "
+ "\"uids\": [ { \"atype\": 3, \"id\" : \"some_id\" } ] } ] }",
IdResResponse.class);

// when and then
assertThat(result.getEids()).isEqualTo(List.of(
Eid.builder()
.source("liveintent.com")
.uids(List.of(Uid.builder().atype(3).id("some_id").build()))
.build()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class ModuleConfigTest {

@Test
public void shouldReturnRequestTimeoutMs() {
final ModuleConfig moduleConfig = new ModuleConfig();
moduleConfig.setRequestTimeoutMs(5);
assertThat(moduleConfig.getRequestTimeoutMs()).isEqualTo(5);
}

@Test
public void shouldReturnIdentityResolutionEndpoint() {
// given
final ModuleConfig moduleConfig = new ModuleConfig();
moduleConfig.setIdentityResolutionEndpoint("https://test.com/idres");

// when and then
assertThat(moduleConfig.getIdentityResolutionEndpoint()).isEqualTo("https://test.com/idres");
}

@Test
public void shouldReturnAuthToken() {
// given
final ModuleConfig moduleConfig = new ModuleConfig();
moduleConfig.setAuthToken("secret_token");

// when and then
assertThat(moduleConfig.getAuthToken()).isEqualTo("secret_token");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package org.prebid.server.analytics.reporter.liveintent;

import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.response.BidResponse;
import io.vertx.core.Future;
import org.prebid.server.analytics.AnalyticsReporter;
import org.prebid.server.analytics.model.AuctionEvent;
import org.prebid.server.analytics.model.NotificationEvent;
import org.prebid.server.analytics.reporter.liveintent.model.LiveIntentAnalyticsProperties;
import org.prebid.server.analytics.reporter.liveintent.model.PbsjBid;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.hooks.execution.model.ExecutionStatus;
import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
import org.prebid.server.hooks.execution.model.HookExecutionContext;
import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
import org.prebid.server.hooks.execution.model.Stage;
import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
import org.prebid.server.hooks.v1.analytics.Activity;
import org.prebid.server.hooks.v1.analytics.Tags;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.log.Logger;
import org.prebid.server.log.LoggerFactory;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.vertx.httpclient.HttpClient;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class LiveIntentAnalyticsReporter implements AnalyticsReporter {

private final HttpClient httpClient;
private final LiveIntentAnalyticsProperties properties;
private final JacksonMapper jacksonMapper;

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

public LiveIntentAnalyticsReporter(
LiveIntentAnalyticsProperties properties,
HttpClient httpClient,
JacksonMapper jacksonMapper
) {
this.httpClient = Objects.requireNonNull(httpClient);
this.properties = Objects.requireNonNull(properties);
this.jacksonMapper = Objects.requireNonNull(jacksonMapper);
}

@Override
public <T> Future<Void> processEvent(T event) {
if (event instanceof AuctionEvent auctionEvent) {
return processAuctionEvent(auctionEvent.getAuctionContext());
} else if (event instanceof NotificationEvent notificationEvent) {
return processNotificationEvent(notificationEvent);
}

return Future.succeededFuture();
}

private Future<Void> processNotificationEvent(NotificationEvent notificationEvent) {
final String url = properties.getAnalyticsEndpoint() + "/analytic-events/pbsj-winning-bid?"
+ "b=" + notificationEvent.getBidder()
+ "&bidId=" + notificationEvent.getBidId();
return httpClient.get(url, properties.getTimeoutMs()).mapEmpty();
}

private Future<Void> processAuctionEvent(AuctionContext auctionContext) {
try {
final BidRequest bidRequest = Optional.ofNullable(auctionContext.getBidRequest())
.orElseThrow(() -> new PreBidException("Bid request should not be empty"));
final BidResponse bidResponse = Optional.ofNullable(auctionContext.getBidResponse())
.orElseThrow(() -> new PreBidException("Bid response should not be empty"));
final Optional<ExtRequestPrebid> requestPrebid = Optional.ofNullable(bidRequest.getExt())
.flatMap(ext -> Optional.of(ext.getPrebid()));

final List<Activity> activities = getActivities(auctionContext);

final List<PbsjBid> pbsjBids = bidResponse.getSeatbid().stream()
.flatMap(seatBid -> seatBid.getBid().stream())
.flatMap(bid ->
bidRequest.getImp().stream()
.filter(impItem -> impItem.getId().equals(bid.getImpid()))
.map(imp ->
PbsjBid.builder()
.bidId(bid.getId())
.price(bid.getPrice())
.adUnitId(imp.getTagid())
.enriched(isEnriched(activities))
.currency(bidResponse.getCur())
.treatmentRate(getTreatmentRate(activities))
.timestamp(requestPrebid.map(ExtRequestPrebid::getAuctiontimestamp).orElse(0L))
.partnerId(properties.getPartnerId())
.build()
)
).toList();

return httpClient.post(
properties.getAnalyticsEndpoint() + "/analytic-events/pbsj-bids",
jacksonMapper.encodeToString(pbsjBids),
properties.getTimeoutMs()
).mapEmpty();
} catch (Exception e) {
logger.error("Error processing event: {}", e.getMessage());
return Future.failedFuture(e);
}
}

private List<Activity> getActivities(AuctionContext auctionContext) {
return Optional.ofNullable(auctionContext)
.map(AuctionContext::getHookExecutionContext)
.map(HookExecutionContext::getStageOutcomes)
.map(stages -> stages.get(Stage.processed_auction_request)).stream()
.flatMap(Collection::stream)
.filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity()))
.map(StageExecutionOutcome::getGroups)
.flatMap(Collection::stream)
.map(GroupExecutionOutcome::getHooks)
.flatMap(Collection::stream)
.filter(hook ->
"liveintent-omni-channel-identity-enrichment-hook".equals(hook.getHookId().getModuleCode())
&& hook.getStatus() == ExecutionStatus.success
)
.map(HookExecutionOutcome::getAnalyticsTags)
.map(Tags::activities)
.flatMap(Collection::stream)
.toList();
}

private Float getTreatmentRate(List<Activity> activities) {
return activities
.stream()
.filter(activity -> "liveintent-treatment-rate".equals(activity.name()))
.findFirst()
.map(activity -> {
try {
return Float.parseFloat(activity.status());
} catch (NumberFormatException e) {
logger.warn("Invalid treatment rate value: {}", activity.status());
throw e;
}
})
.orElse(null);
}

private boolean isEnriched(List<Activity> activities) {
return activities.stream().anyMatch(activity -> "liveintent-enriched".equals(activity.name()));
}

@Override
public int vendorId() {
return 0;
}

@Override
public String name() {
return "liveintentAnalytics";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.prebid.server.analytics.reporter.liveintent.model;

import lombok.Builder;
import lombok.Data;
import lombok.Value;

@Data
@Builder(toBuilder = true)
@Value
public class LiveIntentAnalyticsProperties {

String partnerId;
String analyticsEndpoint;
long timeoutMs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.prebid.server.analytics.reporter.liveintent.model;

import lombok.Builder;
import lombok.Data;
import lombok.Value;

import java.math.BigDecimal;

@Data
@Builder(toBuilder = true)
@Value
public class PbsjBid {

String bidId;
boolean enriched;
BigDecimal price;
String adUnitId;
String currency;
Float treatmentRate;
Long timestamp;
String partnerId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties;
import org.prebid.server.analytics.reporter.greenbids.GreenbidsAnalyticsReporter;
import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties;
import org.prebid.server.analytics.reporter.liveintent.LiveIntentAnalyticsReporter;
import org.prebid.server.analytics.reporter.liveintent.model.LiveIntentAnalyticsProperties;
import org.prebid.server.analytics.reporter.log.LogAnalyticsReporter;
import org.prebid.server.analytics.reporter.pubstack.PubstackAnalyticsReporter;
import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties;
Expand Down Expand Up @@ -302,4 +304,46 @@ private static class PubstackBufferProperties {
Long reportTtlMs;
}
}

@Configuration
@ConditionalOnProperty(prefix = "analytics.liveintent", name = "enabled", havingValue = "true")
public static class LiveIntentAnalyticsConfiguration {

@Bean
LiveIntentAnalyticsReporter liveIntentAnalyticsReporter(
LiveIntentAnalyticsConfigurationProperties properties,
HttpClient httpClient,
JacksonMapper jacksonMapper
) {
return new LiveIntentAnalyticsReporter(
properties.toComponentProperties(),
httpClient,
jacksonMapper
);
}

@Bean
@ConfigurationProperties(prefix = "analytics.liveintent")
LiveIntentAnalyticsConfigurationProperties liveIntentAnalyticsConfigurationProperties() {
return new LiveIntentAnalyticsConfigurationProperties();
}

@Validated
@NoArgsConstructor
@Data
private static class LiveIntentAnalyticsConfigurationProperties {

String partnerId;
String analyticsEndpoint;
long timeoutMs;

public LiveIntentAnalyticsProperties toComponentProperties() {
return LiveIntentAnalyticsProperties.builder()
.partnerId(this.partnerId)
.analyticsEndpoint(this.analyticsEndpoint)
.timeoutMs(this.timeoutMs)
.build();
}
}
}
}
Loading