Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -65,6 +65,11 @@
<artifactId>wurfl-devicedetection</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.30.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,39 @@
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;
import java.util.concurrent.ThreadLocalRandom;

@Configuration
@ConditionalOnProperty(
prefix = "hooks." + LiveIntentOmniChannelIdentityModule.CODE,
name = "enabled",
havingValue = "true")
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, () -> ThreadLocalRandom.current().nextLong()));
return new LiveIntentOmniChannelIdentityModule(hooks);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model;

import com.iab.openrtb.request.Eid;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@NoArgsConstructor
public class IdResResponse {

List<Eid> eids;
}
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;
Copy link
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
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,115 @@
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.util.ListUtil;
import org.prebid.server.vertx.httpclient.HttpClient;
import org.prebid.server.vertx.httpclient.model.HttpClientResponse;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.random.RandomGenerator;

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 RandomGenerator random;

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

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

@Override
public Future<InvocationResult<AuctionRequestPayload>> call(
AuctionRequestPayload auctionRequestPayload,
AuctionInvocationContext invocationContext) {
if (random.nextFloat() < config.getTreatmentRate()) {
return requestEnrichment(auctionRequestPayload)
.<InvocationResult<AuctionRequestPayload>>map(resolutionResult ->
InvocationResultImpl.<AuctionRequestPayload>builder()
.status(InvocationStatus.success)
.action(InvocationAction.update)
.payloadUpdate(requestPayload -> updatedPayload(requestPayload, resolutionResult))
.build())
.onFailure(throwable -> logger.error("Failed enrichment:", throwable));
}
return Future.succeededFuture(
InvocationResultImpl.<AuctionRequestPayload>builder()
.status(InvocationStatus.success)
.action(InvocationAction.no_action)
.build());

}

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 MultiMap headers() {
return MultiMap.caseInsensitiveMultiMap()
.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer " + config.getAuthToken());
}

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

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

final List<Eid> allEids = ListUtil.union(
Optional.ofNullable(user.getEids()).orElse(Collections.emptyList()), idResResponse.getEids());

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

return AuctionRequestPayloadImpl.of(updatedBidRequest);
}

@Override
public String code() {
return CODE;
}
}
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\", "
Copy link
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
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
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
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
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
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()).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");
}
}
Loading
Loading