Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 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
d6dfcc7
Merge branch 'master' into cm-1776
Aug 5, 2025
53ca621
Merge branch 'master' into cm-1776
Aug 5, 2025
b61986a
cm-1776: PR issues fixed
Aug 5, 2025
e458a4c
cm-1776: PR issues fixed
Aug 5, 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
5 changes: 5 additions & 0 deletions extra/bundle/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
<artifactId>pb-request-correction</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.prebid.server.hooks.modules</groupId>
<artifactId>live-intent-omni-channel-identity</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<build>
Expand Down
46 changes: 46 additions & 0 deletions extra/modules/live-intent-omni-channel-identity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Overview

This module enriches bid requests with user EIDs.

The user EIDs to be enriched are configured per partner as part of the LiveIntent HIRO onboarding process. As part of this onboarding process, partners will also be provided with the `identity-resolution-endpoint` URL as well as with the `auth-token`.

`treatment-rate` is a value between 0.0 and 1.0 (including 0.0 and 1.0) and defines the percentage of requests for which identity enrichment should be performed. This value can be freely picked. We recommend a value between 0.9 and 0.95

## Configuration

To start using the LiveIntent Omni Channel Identity module you have to enable it and add configuration:

```yaml
hooks:
liveintent-omni-channel-identity:
enabled: true
host-execution-plan: >
{
"endpoints": {
"/openrtb2/auction": {
"stages": {
"processed-auction-request": {
"groups": [
{
"timeout": 100,
"hook-sequence": [
{
"module-code": "liveintent-omni-channel-identity",
"hook-impl-code": "liveintent-omni-channel-identity-enrichment-hook"
}
]
}
]
}
}
}
}
}
modules:
liveintent-omni-channel-identity:
request-timeout-ms: 2000
identity-resolution-endpoint: "https://liveintent.com/idx"
auth-token: "secret-token"
treatment-rate: 0.9
```

18 changes: 18 additions & 0 deletions extra/modules/live-intent-omni-channel-identity/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.prebid.server.hooks.modules</groupId>
<artifactId>all-modules</artifactId>
<version>3.26.0-SNAPSHOT</version>
</parent>

<artifactId>live-intent-omni-channel-identity</artifactId>

<name>live-intent-omni-channel-identity</name>
<description>LiveIntent Omni-Channel Identity</description>

<dependencies>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.config;

import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.ModuleConfig;
import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.LiveIntentOmniChannelIdentityModule;
import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks.LiveIntentOmniChannelIdentityProcessedAuctionRequestHook;
import org.prebid.server.hooks.v1.Hook;
import org.prebid.server.hooks.v1.InvocationContext;
import org.prebid.server.hooks.v1.Module;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.vertx.httpclient.HttpClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Set;

@Configuration
@ConditionalOnProperty(
prefix = "hooks." + LiveIntentOmniChannelIdentityModule.CODE, name = "enabled", havingValue = "true"
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, change parantesis placement, look at our code style guidelines.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5f15bf1

public class LiveIntentOmniChannelIdentityConfiguration {

@Bean
@ConfigurationProperties(prefix = "hooks.modules." + LiveIntentOmniChannelIdentityModule.CODE)
ModuleConfig moduleConfig() {
return new ModuleConfig();
}

@Bean
Module liveIntentOmniChannelIdentityModule(ModuleConfig config, JacksonMapper mapper, HttpClient httpClient) {
final Set<? extends Hook<?, ? extends InvocationContext>> hooks = Set.of(
new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(config, mapper, httpClient));
return new LiveIntentOmniChannelIdentityModule(hooks);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.iab.openrtb.request.Eid;
import lombok.Builder;
import lombok.Data;
import lombok.extern.jackson.Jacksonized;

import java.util.List;

@Builder
@Data
@Jacksonized

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use this annotation. We try to avoid experimental features of Lombok as muck as possible. I also believe that it will work without that annotation.

@3link 3link Jun 11, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to using NoArgsConstructor in b588f6a

public class IdResResponse {

@JsonProperty("eids")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useless annotation, please remove. Since you are using default mapper - it will deserialize by name of the field.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3ed7ee8

List<Eid> eids;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config;

import lombok.Data;

@Data
public final class ModuleConfig {

long requestTimeoutMs;
String identityResolutionEndpoint;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add separation lines between all fields.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bedb969

String authToken;
float treatmentRate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1;

import org.prebid.server.hooks.v1.Hook;
import org.prebid.server.hooks.v1.InvocationContext;
import org.prebid.server.hooks.v1.Module;

import java.util.Collection;

public record LiveIntentOmniChannelIdentityModule(
Collection<? extends Hook<?, ? extends InvocationContext>> hooks) implements Module {

public static final String CODE = "liveintent-omni-channel-identity";

@Override
public String code() {
return CODE;
}

@Override
public Collection<? extends Hook<?, ? extends InvocationContext>> hooks() {
return hooks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks;

import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Eid;
import com.iab.openrtb.request.User;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
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.ModuleConfig;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.log.Logger;
import org.prebid.server.log.LoggerFactory;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.vertx.httpclient.HttpClient;
import org.prebid.server.vertx.httpclient.model.HttpClientResponse;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;

public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements ProcessedAuctionRequestHook {

private static final Logger logger =
LoggerFactory.getLogger(LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.class);
private static final String CODE = "liveintent-omni-channel-identity-enrichment-hook";

private final ModuleConfig config;
private final JacksonMapper mapper;
private final HttpClient httpClient;
private final Random random;

public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(
ModuleConfig config,
JacksonMapper mapper,
HttpClient httpClient) {

this(config, mapper, httpClient, new Random());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one won't work. Default Random class is not thread safe, and module can be called on any threads concurrently. Please, to simplify code, just leave one constructor with 'RandomGenerator' as parameter (for tests) and provide it with lambda that implements that functional interface using ThreadLocalRandom like this:

() -> ThreadLocalRandom.current().nextLong()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d94e06f

}

public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(
ModuleConfig config,
JacksonMapper mapper,
HttpClient httpClient,
Random random) {

this.config = Objects.requireNonNull(config);
this.mapper = Objects.requireNonNull(mapper);
this.httpClient = Objects.requireNonNull(httpClient);
this.random = random;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objects.requiereNonNull here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5f15bf1

}

@Override
public Future<InvocationResult<AuctionRequestPayload>> call(
AuctionRequestPayload auctionRequestPayload,
AuctionInvocationContext invocationContext
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5f15bf1

if (random.nextFloat() < config.getTreatmentRate()) {
final Future<InvocationResult<AuctionRequestPayload>> update = requestEnrichment(auctionRequestPayload)
.map(resolutionResult ->
InvocationResultImpl.<AuctionRequestPayload>builder()
.status(InvocationStatus.success)
.action(InvocationAction.update)
.payloadUpdate(requestPayload -> updatedPayload(requestPayload, resolutionResult))
.build()
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, see our styling doc on how we style our code and apply changes accordingly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 61ceeed


return update.onFailure(throwable -> logger.error("Failed enrichment:", throwable));
} else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for else clause, unnecessary nesting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5f15bf1

return Future.succeededFuture(
InvocationResultImpl.<AuctionRequestPayload>builder()
.status(InvocationStatus.success)
.action(InvocationAction.no_action)
.build());
}
}

@Override
public String code() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here about method order.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fa66d18

return CODE;
}

private AuctionRequestPayload updatedPayload(AuctionRequestPayload requestPayload, IdResResponse idResResponse) {
final BidRequest bidRequest = Optional.ofNullable(
requestPayload.bidRequest()).orElse(BidRequest.builder().build());
final User user = Optional.ofNullable(bidRequest.getUser()).orElse(User.builder().build());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for building useless object and then rebuilding it right away.
Compute eids first and then just do:

final User updatedUser = Optional.ofNullable(bidRequest.getUser())
         .map(User::toBuilder)
         .orElseGet(User::builder)
         .eids(`your eids`)
         .build();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5f15bf1


final List<Eid> allEids = new ArrayList<>();
allEids.addAll(Optional.ofNullable(user.getEids()).orElse(Collections.emptyList()));
allEids.addAll(idResResponse.getEids());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ListUtils.union

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5f15bf1


final User updatedUser = user.toBuilder().eids(allEids).build();
final BidRequest updatedBidRequest = requestPayload.bidRequest().toBuilder().user(updatedUser).build();

return AuctionRequestPayloadImpl.of(updatedBidRequest);
}

private Future<IdResResponse> requestEnrichment(AuctionRequestPayload auctionRequestPayload) {
final String bidRequestJson = mapper.encodeToString(auctionRequestPayload.bidRequest());
return httpClient.post(
config.getIdentityResolutionEndpoint(),
headers(),
bidRequestJson,
config.getRequestTimeoutMs())
.map(this::processResponse);
}

private IdResResponse processResponse(HttpClientResponse response) {
return mapper.decodeValue(response.getBody(), IdResResponse.class);
}

private MultiMap headers() {
return MultiMap.caseInsensitiveMultiMap()
.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer " + config.getAuthToken());
}
}
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 com.fasterxml.jackson.databind.ObjectMapper;
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 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\", "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set up data with proper Objects. We don't use raw json strings in unit tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that's exactly what is tested here: that the json string is parsed correctly.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you test here?) Jackson object mapper or your Data class? Remove this entirely, as this unit test is useless.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of the test is to ensure that the API response is deserialized correctly, and the data-class is mapping the API response properly

+ "\"uids\": [ { \"atype\": 3, \"id\" : \"some_id\" } ] } ] }",
IdResResponse.class);
// when and then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add nice empty line before comment to separate test stages. Also, check for similar occurrences.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dine in 09450d2

assertThat(result.getEids()).hasSize(1);
assertThat(result.getEids().getFirst().getSource()).isEqualTo("liveintent.com");
assertThat(result.getEids().getFirst().getUids()).hasSize(1);
assertThat(result.getEids().getFirst().getUids().getFirst().getAtype()).isEqualTo(3);
assertThat(result.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("some_id");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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");
}
}
Loading
Loading