diff --git a/extra/modules/live-intent-omni-channel-identity/README.md b/extra/modules/live-intent-omni-channel-identity/README.md index be5ad801ec1..a8a3ce5b113 100644 --- a/extra/modules/live-intent-omni-channel-identity/README.md +++ b/extra/modules/live-intent-omni-channel-identity/README.md @@ -2,9 +2,12 @@ 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`. +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 +`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 @@ -12,35 +15,35 @@ To start using the LiveIntent Omni Channel Identity module you have to enable it ```yaml hooks: - liveintent-omni-channel-identity: - enabled: true - host-execution-plan: > - { - "endpoints": { - "/openrtb2/auction": { - "stages": { - "processed-auction-request": { - "groups": [ - { - "timeout": 100, - "hook-sequence": [ + liveintent-omni-channel-identity: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ { - "module-code": "liveintent-omni-channel-identity", - "hook-impl-code": "liveintent-omni-channel-identity-enrichment-hook" + "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 + modules: + liveintent-omni-channel-identity: + request-timeout-ms: 2000 + identity-resolution-endpoint: "https://u.liveintent.com/idx" + auth-token: "secret-token" + treatment-rate: 0.9 ``` diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/FullSource.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/FullSource.java new file mode 100644 index 00000000000..ac6d393755c --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/FullSource.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class FullSource { + + String source; + + String inserter; +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java index 3afdb146018..ba5203e5e75 100644 --- a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java @@ -10,6 +10,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -58,6 +59,8 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements private static final String CODE = "liveintent-omni-channel-identity-enrichment-hook"; + private static final String INSERTER = "s2s.liveintent.com"; + private final LiveIntentOmniChannelProperties config; private final JacksonMapper mapper; private final HttpClient httpClient; @@ -161,7 +164,18 @@ private MultiMap headers() { } private IdResResponse processResponse(HttpClientResponse response) { - return mapper.decodeValue(response.getBody(), IdResResponse.class); + final IdResResponse res = mapper.decodeValue(response.getBody(), IdResResponse.class); + final List eids = res.getEids(); + + if (CollectionUtils.isEmpty(eids)) { + return res; + } + + final List modifiedEids = eids.stream() + .map(eid -> eid.toBuilder().inserter(INSERTER).build()) + .toList(); + + return IdResResponse.of(modifiedEids); } private static Future> noAction() { @@ -189,37 +203,37 @@ private InvocationResultImpl update(IdResResponse resolut } private AuctionRequestPayload updatedPayload(AuctionRequestPayload requestPayload, List resolvedEids) { - final List eids = ListUtils.emptyIfNull(resolvedEids); - final BidRequest bidRequest = updateAllowedBidders(requestPayload.bidRequest(), resolvedEids); - final User updatedUser = Optional.ofNullable(bidRequest.getUser()) - .map(user -> user.toBuilder().eids(ListUtil.union(ListUtils.emptyIfNull(user.getEids()), eids))) - .orElseGet(() -> User.builder().eids(eids)) - .build(); + return CollectionUtils.isNotEmpty(resolvedEids) + ? AuctionRequestPayloadImpl.of(updateBidRequest(requestPayload.bidRequest(), resolvedEids)) + : requestPayload; + } - return AuctionRequestPayloadImpl.of(bidRequest.toBuilder().user(updatedUser).build()); + private BidRequest updateBidRequest(BidRequest bidRequest, List resolvedEids) { + return bidRequest.toBuilder() + .ext(updateExtRequest(bidRequest.getExt(), resolvedEids)) + .user(updateUser(bidRequest.getUser(), resolvedEids)) + .build(); } - private BidRequest updateAllowedBidders(BidRequest bidRequest, List resolvedEids) { - if (CollectionUtils.isEmpty(targetBidders) || CollectionUtils.isEmpty(resolvedEids)) { - return bidRequest; - } + private ExtRequest updateExtRequest(ExtRequest ext, List resolvedEids) { + final Set uniqueSources = CollectionUtils.emptyIfNull(resolvedEids).stream() + .map(Eid::getSource) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toSet()); - final ExtRequest ext = bidRequest.getExt(); final ExtRequestPrebid extPrebid = ext != null ? ext.getPrebid() : null; final ExtRequestPrebidData extPrebidData = extPrebid != null ? extPrebid.getData() : null; + final List eidPermissions = + extPrebidData != null ? extPrebidData.getEidPermissions() : null; - final List existingPerms = extPrebidData != null - ? extPrebidData.getEidPermissions() - : null; - - if (CollectionUtils.isEmpty(existingPerms)) { - return bidRequest; - } + final List modifiedEidPermissions = CollectionUtils.isEmpty(eidPermissions) + ? createEidPermissions(uniqueSources) + : modifyEidPermissions(eidPermissions, uniqueSources); final ExtRequestPrebid updatedExtPrebid = Optional.ofNullable(extPrebid) .map(ExtRequestPrebid::toBuilder) .orElseGet(ExtRequestPrebid::builder) - .data(updatePrebidData(extPrebidData, resolvedEids)) + .data(updatePrebidData(extPrebidData, modifiedEidPermissions)) .build(); final ExtRequest updatedExtRequest = ExtRequest.of(updatedExtPrebid); @@ -227,42 +241,71 @@ private BidRequest updateAllowedBidders(BidRequest bidRequest, List resolve mapper.fillExtension(updatedExtRequest, ext.getProperties()); } - return bidRequest.toBuilder().ext(updatedExtRequest).build(); + return updatedExtRequest; } - private ExtRequestPrebidData updatePrebidData(ExtRequestPrebidData extPrebidData, List resolvedEids) { - final List originalBidders = extPrebidData != null ? extPrebidData.getBidders() : null; + private static User updateUser(User user, List resolvedEids) { + final List updatedEids = Optional.ofNullable(user) + .map(User::getEids) + .map(eids -> ListUtil.union(eids, resolvedEids)) + .orElse(resolvedEids); - final Set resolvedSources = resolvedEids.stream() - .map(Eid::getSource) - .collect(Collectors.toSet()); + return Optional.ofNullable(user) + .map(User::toBuilder) + .orElseGet(User::builder) + .eids(updatedEids) + .build(); + } - final List updatedPermissions = extPrebidData.getEidPermissions().stream() - .map(permission -> restrictEidPermission(permission, resolvedSources)) - .filter(Objects::nonNull) + private List createEidPermissions(Set sources) { + return sources.stream() + .map(source -> ExtRequestPrebidDataEidPermissions.builder() + .source(source) + .inserter(INSERTER) + .bidders(targetBidders.stream().toList()) + .build()) .toList(); + } - return ExtRequestPrebidData.of(originalBidders, updatedPermissions); + private List modifyEidPermissions( + List eidPermissions, + Set sources) { + final List modifiedEidPermissions = eidPermissions.stream() + .map(it -> updateEidPermission(it, sources)) + .filter(Objects::nonNull) + .toList(); + final List defaultEidPermissions = createEidPermissions(sources); + return ListUtils.union(modifiedEidPermissions, defaultEidPermissions); } - private ExtRequestPrebidDataEidPermissions restrictEidPermission(ExtRequestPrebidDataEidPermissions permission, - Set resolvedSources) { + private ExtRequestPrebidData updatePrebidData(ExtRequestPrebidData extPrebidData, + List eidPermissions) { + + final List originalBidders = extPrebidData != null ? extPrebidData.getBidders() : null; + + return ExtRequestPrebidData.of(originalBidders, eidPermissions); + } - if (!resolvedSources.contains(permission.getSource())) { - return permission; + private ExtRequestPrebidDataEidPermissions updateEidPermission(ExtRequestPrebidDataEidPermissions eidPermission, + Set sources) { + if (!sources.contains(eidPermission.getSource()) || !INSERTER.equals(eidPermission.getInserter())) { + return eidPermission; } - final List finalBidders = ListUtils.emptyIfNull(permission.getBidders()).stream() + final List allowedBidders = ListUtils.emptyIfNull(eidPermission.getBidders()); + final List finalBidders = allowedBidders.stream() .filter(targetBidders::contains) .toList(); - return CollectionUtils.isEmpty(finalBidders) - ? null - : ExtRequestPrebidDataEidPermissions - .builder() - .bidders(finalBidders) - .source(permission.getSource()) - .build(); + if (CollectionUtils.isEmpty(allowedBidders) || allowedBidders.contains("*")) { + return eidPermission.toBuilder().bidders(targetBidders.stream().toList()).build(); + } + + if (CollectionUtils.isEmpty(finalBidders)) { + return null; + } + + return eidPermission.toBuilder().bidders(finalBidders).build(); } @Override diff --git a/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java index 4dfaa8c8df4..e89910b02de 100644 --- a/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java +++ b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java @@ -32,6 +32,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; +import org.prebid.server.util.ListUtil; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; @@ -77,6 +78,8 @@ public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest { private Set configuredBidders; + private ExtRequestPrebidDataEidPermissions defaultPermissions; + @BeforeEach public void setUp() { configuredBidders = Set.of("bidder1", "bidder2"); @@ -86,6 +89,12 @@ public void setUp() { given(properties.getTreatmentRate()).willReturn(1.0f); given(properties.getTargetBidders()).willReturn(configuredBidders); + defaultPermissions = ExtRequestPrebidDataEidPermissions.builder() + .inserter("s2s.liveintent.com") + .bidders(configuredBidders.stream().toList()) + .source("liveintent.com") + .build(); + target = new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( properties, userFpdActivityMask, MAPPER, httpClient, 0.01d); } @@ -248,6 +257,7 @@ public void callShouldEnrichUserEidsWithRequestedEids() { final Eid expectedEid = Eid.builder() .source("liveintent.com") .uids(singletonList(Uid.builder().id("id2").atype(3).build())) + .matcher("liveintent.com") .build(); final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); @@ -273,7 +283,7 @@ public void callShouldEnrichUserEidsWithRequestedEids() { .extracting(AuctionRequestPayload::bidRequest) .extracting(BidRequest::getUser) .extracting(User::getEids) - .isEqualTo(List.of(givenEid, expectedEid)); + .isEqualTo(List.of(givenEid, expectedEid.toBuilder().inserter("s2s.liveintent.com").build())); verify(httpClient).post( eq("https://test.com/idres"), @@ -290,6 +300,7 @@ public void callShouldCreateUserAndUseRequestedEidsWhenUserIsAbsent() { final Eid expectedEid = Eid.builder() .source("liveintent.com") .uids(singletonList(Uid.builder().id("id2").atype(3).build())) + .matcher("liveintent.com") .build(); final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); @@ -315,7 +326,7 @@ public void callShouldCreateUserAndUseRequestedEidsWhenUserIsAbsent() { .extracting(AuctionRequestPayload::bidRequest) .extracting(BidRequest::getUser) .extracting(User::getEids) - .isEqualTo(List.of(expectedEid)); + .isEqualTo(List.of(expectedEid.toBuilder().inserter("s2s.liveintent.com").build())); verify(httpClient).post( eq("https://test.com/idres"), @@ -384,15 +395,18 @@ public void shouldRestrictExistingEidPermissionsByIntersectionAndKeepGlobalBidde final ExtRequestPrebidDataEidPermissions otherBidder = ExtRequestPrebidDataEidPermissions.builder() .source("some.other-source.com") + .inserter("some.other-inserter.com") .bidders(singletonList("bidderY")) .build(); final ExtRequestPrebidDataEidPermissions liBidder2 = ExtRequestPrebidDataEidPermissions.builder() .source("liveintent.com") + .inserter("s2s.liveintent.com") .bidders(singletonList("bidder2")) .build(); final ExtRequestPrebidDataEidPermissions liBidder23 = ExtRequestPrebidDataEidPermissions.builder() .source("liveintent.com") + .inserter("s2s.liveintent.com") .bidders(List.of("bidder2", "bidder3")) .build(); @@ -407,7 +421,7 @@ public void shouldRestrictExistingEidPermissionsByIntersectionAndKeepGlobalBidde final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of( List.of("bidderX"), - List.of(otherBidder, liBidder2)); + ListUtil.union(List.of(otherBidder, liBidder2), List.of(defaultPermissions))); final Eid expectedEid = Eid.builder().source("liveintent.com").build(); @@ -450,10 +464,12 @@ public void shouldNotAddNewEidPermissionsOrModifyGlobalBiddersWhenSourceNotPrese final User givenUser = User.builder().eids(singletonList(givenEid)).build(); final ExtRequestPrebidDataEidPermissions bidder1 = ExtRequestPrebidDataEidPermissions.builder() .source("some.other-source.com") + .inserter("some.other-inserter.com") .bidders(singletonList("bidder3")) .build(); final ExtRequestPrebidDataEidPermissions bidder2 = ExtRequestPrebidDataEidPermissions.builder() .source("some.source.com") + .inserter("s2s.liveintent.com") .bidders(singletonList("bidder3")) .build(); @@ -467,7 +483,8 @@ public void shouldNotAddNewEidPermissionsOrModifyGlobalBiddersWhenSourceNotPrese .build())) .build(); - final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of(List.of("bidder3"), bidders); + final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of(List.of("bidder3"), + ListUtil.union(bidders, List.of(defaultPermissions))); final Eid expectedEid = Eid.builder().source("liveintent.com").build(); @@ -514,6 +531,7 @@ public void shouldRemovePermissionWhenIntersectionIsEmpty() { List.of( ExtRequestPrebidDataEidPermissions.builder() .source("liveintent.com") + .inserter("s2s.liveintent.com") .bidders(singletonList("not-allowed")) .build(), ExtRequestPrebidDataEidPermissions.builder() @@ -548,9 +566,10 @@ public void shouldRemovePermissionWhenIntersectionIsEmpty() { final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of( List.of("bidderGlobal"), List.of(ExtRequestPrebidDataEidPermissions.builder() - .source("keep.com") - .bidders(singletonList("bidderGlobal")) - .build())); + .source("keep.com") + .bidders(singletonList("bidderGlobal")) + .build(), + defaultPermissions)); assertThat(result.status()).isEqualTo(InvocationStatus.success); assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java index 2373c40e886..0e717619cdb 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java @@ -7,7 +7,7 @@ import java.util.List; @Value -@Builder +@Builder(toBuilder = true) public class ExtRequestPrebidDataEidPermissions { /**