Skip to content

Commit 579de03

Browse files
Setuid: Cookie Family Fix (#3675)
1 parent 663f8b5 commit 579de03

6 files changed

Lines changed: 298 additions & 94 deletions

File tree

src/main/java/org/prebid/server/cookie/CookieSyncService.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource;
4444
import org.prebid.server.util.HttpUtil;
4545
import org.prebid.server.util.ObjectUtil;
46+
import org.prebid.server.util.StreamUtil;
4647

4748
import java.util.ArrayList;
4849
import java.util.Collection;
@@ -54,7 +55,6 @@
5455
import java.util.Map;
5556
import java.util.Objects;
5657
import java.util.Set;
57-
import java.util.function.Function;
5858
import java.util.function.Predicate;
5959
import java.util.stream.Collectors;
6060

@@ -385,16 +385,11 @@ private static Set<String> allowedBiddersByPriority(CookieSyncContext cookieSync
385385

386386
private List<BidderUsersyncStatus> validStatuses(Set<String> biddersToSync, CookieSyncContext cookieSyncContext) {
387387
return biddersToSync.stream()
388-
.filter(distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow()))
388+
.filter(StreamUtil.distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow()))
389389
.map(bidder -> validStatus(bidder, cookieSyncContext))
390390
.toList();
391391
}
392392

393-
private static <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) {
394-
final Set<Object> seen = new HashSet<>();
395-
return value -> seen.add(keyExtractor.apply(value));
396-
}
397-
398393
private BidderUsersyncStatus validStatus(String bidder, CookieSyncContext cookieSyncContext) {
399394
final BiddersContext biddersContext = cookieSyncContext.getBiddersContext();
400395
final RoutingContext routingContext = cookieSyncContext.getRoutingContext();

src/main/java/org/prebid/server/handler/SetuidHandler.java

Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
import io.vertx.core.http.HttpServerRequest;
1111
import io.vertx.core.http.HttpServerResponse;
1212
import io.vertx.ext.web.RoutingContext;
13-
import org.apache.commons.collections4.CollectionUtils;
1413
import org.apache.commons.lang3.BooleanUtils;
14+
import org.apache.commons.lang3.ObjectUtils;
1515
import org.apache.commons.lang3.StringUtils;
16+
import org.apache.commons.lang3.tuple.Pair;
1617
import org.prebid.server.activity.Activity;
1718
import org.prebid.server.activity.ComponentType;
1819
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
@@ -27,7 +28,6 @@
2728
import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory;
2829
import org.prebid.server.bidder.BidderCatalog;
2930
import org.prebid.server.bidder.UsersyncFormat;
30-
import org.prebid.server.bidder.UsersyncMethod;
3131
import org.prebid.server.bidder.UsersyncMethodType;
3232
import org.prebid.server.bidder.UsersyncUtil;
3333
import org.prebid.server.bidder.Usersyncer;
@@ -54,19 +54,20 @@
5454
import org.prebid.server.settings.model.AccountGdprConfig;
5555
import org.prebid.server.settings.model.AccountPrivacyConfig;
5656
import org.prebid.server.util.HttpUtil;
57+
import org.prebid.server.util.StreamUtil;
5758
import org.prebid.server.vertx.verticles.server.HttpEndpoint;
5859
import org.prebid.server.vertx.verticles.server.application.ApplicationResource;
5960

6061
import java.util.Collections;
62+
import java.util.Comparator;
6163
import java.util.List;
6264
import java.util.Map;
6365
import java.util.Objects;
6466
import java.util.Optional;
6567
import java.util.function.Consumer;
6668
import java.util.function.Function;
67-
import java.util.function.Supplier;
69+
import java.util.function.Predicate;
6870
import java.util.stream.Collectors;
69-
import java.util.stream.Stream;
7071

7172
public class SetuidHandler implements ApplicationResource {
7273

@@ -88,7 +89,7 @@ public class SetuidHandler implements ApplicationResource {
8889
private final AnalyticsReporterDelegator analyticsDelegator;
8990
private final Metrics metrics;
9091
private final TimeoutFactory timeoutFactory;
91-
private final Map<String, UsersyncMethodType> cookieNameToSyncType;
92+
private final Map<String, Pair<String, UsersyncMethodType>> cookieNameToBidderAndSyncType;
9293

9394
public SetuidHandler(long defaultTimeout,
9495
UidsCookieService uidsCookieService,
@@ -112,52 +113,57 @@ public SetuidHandler(long defaultTimeout,
112113
this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator);
113114
this.metrics = Objects.requireNonNull(metrics);
114115
this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
115-
this.cookieNameToSyncType = collectMap(bidderCatalog);
116+
this.cookieNameToBidderAndSyncType = collectUsersyncers(bidderCatalog);
116117
}
117118

118-
private static Map<String, UsersyncMethodType> collectMap(BidderCatalog bidderCatalog) {
119+
private static Map<String, Pair<String, UsersyncMethodType>> collectUsersyncers(BidderCatalog bidderCatalog) {
120+
validateUsersyncersDuplicates(bidderCatalog);
121+
122+
return bidderCatalog.usersyncReadyBidders().stream()
123+
.sorted(Comparator.comparing(bidderName -> BooleanUtils.toInteger(bidderCatalog.isAlias(bidderName))))
124+
.filter(StreamUtil.distinctBy(bidderCatalog::cookieFamilyName))
125+
.map(bidderName -> bidderCatalog.usersyncerByName(bidderName)
126+
.map(usersyncer -> Pair.of(bidderName, usersyncer)))
127+
.flatMap(Optional::stream)
128+
.collect(Collectors.toMap(
129+
pair -> pair.getRight().getCookieFamilyName(),
130+
pair -> Pair.of(pair.getLeft(), preferredUserSyncType(pair.getRight()))));
131+
}
119132

120-
final Supplier<Stream<Usersyncer>> usersyncers = () -> bidderCatalog.names()
121-
.stream()
122-
.filter(bidderCatalog::isActive)
133+
private static void validateUsersyncersDuplicates(BidderCatalog bidderCatalog) {
134+
final List<String> duplicatedCookieFamilyNames = bidderCatalog.usersyncReadyBidders().stream()
135+
.filter(bidderName -> !isAliasWithRootCookieFamilyName(bidderCatalog, bidderName))
123136
.map(bidderCatalog::usersyncerByName)
124-
.filter(Optional::isPresent)
125-
.map(Optional::get)
126-
.distinct();
127-
128-
validateUsersyncers(usersyncers.get());
137+
.flatMap(Optional::stream)
138+
.map(Usersyncer::getCookieFamilyName)
139+
.filter(Predicate.not(StreamUtil.distinctBy(Function.identity())))
140+
.distinct()
141+
.toList();
129142

130-
return usersyncers.get()
131-
.collect(Collectors.toMap(Usersyncer::getCookieFamilyName, SetuidHandler::preferredUserSyncType));
143+
if (!duplicatedCookieFamilyNames.isEmpty()) {
144+
throw new IllegalArgumentException(
145+
"Duplicated \"cookie-family-name\" found, values: "
146+
+ String.join(", ", duplicatedCookieFamilyNames));
147+
}
132148
}
133149

134-
@Override
135-
public List<HttpEndpoint> endpoints() {
136-
return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value()));
150+
private static boolean isAliasWithRootCookieFamilyName(BidderCatalog bidderCatalog, String bidder) {
151+
final String bidderCookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElse(StringUtils.EMPTY);
152+
final String parentCookieFamilyName =
153+
bidderCatalog.cookieFamilyName(bidderCatalog.resolveBaseBidder(bidder)).orElse(null);
154+
155+
return bidderCatalog.isAlias(bidder)
156+
&& parentCookieFamilyName != null
157+
&& parentCookieFamilyName.equals(bidderCookieFamilyName);
137158
}
138159

139160
private static UsersyncMethodType preferredUserSyncType(Usersyncer usersyncer) {
140-
return Stream.of(usersyncer.getIframe(), usersyncer.getRedirect())
141-
.filter(Objects::nonNull)
142-
.findFirst()
143-
.map(UsersyncMethod::getType)
144-
.get(); // when usersyncer is present, it will contain at least one method
161+
return ObjectUtils.firstNonNull(usersyncer.getIframe(), usersyncer.getRedirect()).getType();
145162
}
146163

147-
private static void validateUsersyncers(Stream<Usersyncer> usersyncers) {
148-
final List<String> cookieFamilyNameDuplicates = usersyncers.map(Usersyncer::getCookieFamilyName)
149-
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
150-
.entrySet()
151-
.stream()
152-
.filter(name -> name.getValue() > 1)
153-
.map(Map.Entry::getKey)
154-
.distinct()
155-
.toList();
156-
if (CollectionUtils.isNotEmpty(cookieFamilyNameDuplicates)) {
157-
throw new IllegalArgumentException(
158-
"Duplicated \"cookie-family-name\" found, values: "
159-
+ String.join(", ", cookieFamilyNameDuplicates));
160-
}
164+
@Override
165+
public List<HttpEndpoint> endpoints() {
166+
return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value()));
161167
}
162168

163169
@Override
@@ -173,6 +179,11 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
173179
final String requestAccount = httpRequest.getParam(ACCOUNT_PARAM);
174180
final Timeout timeout = timeoutFactory.create(defaultTimeout);
175181

182+
final UsersyncMethodType syncType = Optional.ofNullable(cookieName)
183+
.map(cookieNameToBidderAndSyncType::get)
184+
.map(Pair::getRight)
185+
.orElse(null);
186+
176187
return accountById(requestAccount, timeout)
177188
.compose(account -> setuidPrivacyContextFactory.contextFrom(httpRequest, account, timeout)
178189
.map(privacyContext -> SetuidContext.builder()
@@ -181,7 +192,7 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
181192
.timeout(timeout)
182193
.account(account)
183194
.cookieName(cookieName)
184-
.syncType(cookieNameToSyncType.get(cookieName))
195+
.syncType(syncType)
185196
.privacyContext(privacyContext)
186197
.build()))
187198

@@ -211,11 +222,11 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
211222

212223
if (setuidContextResult.succeeded()) {
213224
final SetuidContext setuidContext = setuidContextResult.result();
214-
final String bidderCookieName = setuidContext.getCookieName();
225+
final String bidderCookieFamily = setuidContext.getCookieName();
215226
final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
216227

217228
try {
218-
validateSetuidContext(setuidContext, bidderCookieName);
229+
validateSetuidContext(setuidContext, bidderCookieFamily);
219230
} catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e) {
220231
handleErrors(e, routingContext, tcfContext);
221232
return;
@@ -224,28 +235,33 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
224235
final AccountPrivacyConfig privacyConfig = setuidContext.getAccount().getPrivacy();
225236
final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig.getGdpr() : null;
226237

238+
final String bidderName = cookieNameToBidderAndSyncType.get(bidderCookieFamily).getLeft();
239+
227240
Future.all(
228-
tcfDefinerService.isAllowedForHostVendorId(tcfContext),
229-
tcfDefinerService.resultForBidderNames(
230-
Collections.singleton(bidderCookieName), tcfContext, accountGdprConfig))
231-
.onComplete(hostTcfResponseResult -> respondByTcfResponse(hostTcfResponseResult, setuidContext));
241+
tcfDefinerService.isAllowedForHostVendorId(tcfContext),
242+
tcfDefinerService.resultForBidderNames(
243+
Collections.singleton(bidderName), tcfContext, accountGdprConfig))
244+
.onComplete(hostTcfResponseResult -> respondByTcfResponse(
245+
hostTcfResponseResult,
246+
bidderName,
247+
setuidContext));
232248
} else {
233249
final Throwable error = setuidContextResult.cause();
234250
handleErrors(error, routingContext, null);
235251
}
236252
}
237253

238-
private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
254+
private void validateSetuidContext(SetuidContext setuidContext, String bidderCookieFamily) {
239255
final String cookieName = setuidContext.getCookieName();
240256
final boolean isCookieNameBlank = StringUtils.isBlank(cookieName);
241-
if (isCookieNameBlank || !cookieNameToSyncType.containsKey(cookieName)) {
257+
if (isCookieNameBlank || !cookieNameToBidderAndSyncType.containsKey(cookieName)) {
242258
final String cookieNameError = isCookieNameBlank ? "required" : "invalid";
243259
throw new InvalidRequestException("\"bidder\" query param is " + cookieNameError);
244260
}
245261

246262
final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
247263
if (tcfContext.isInGdprScope() && !tcfContext.isConsentValid()) {
248-
metrics.updateUserSyncTcfInvalidMetric(bidder);
264+
metrics.updateUserSyncTcfInvalidMetric(bidderCookieFamily);
249265
throw new InvalidRequestException("Consent string is invalid");
250266
}
251267

@@ -256,16 +272,18 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
256272

257273
final ActivityInfrastructure activityInfrastructure = setuidContext.getActivityInfrastructure();
258274
final ActivityInvocationPayload activityInvocationPayload = TcfContextActivityInvocationPayload.of(
259-
ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidder),
275+
ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidderCookieFamily),
260276
tcfContext);
261277

262278
if (!activityInfrastructure.isAllowed(Activity.SYNC_USER, activityInvocationPayload)) {
263279
throw new UnavailableForLegalReasonsException();
264280
}
265281
}
266282

267-
private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseResult, SetuidContext setuidContext) {
268-
final String bidderCookieName = setuidContext.getCookieName();
283+
private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseResult,
284+
String bidderName,
285+
SetuidContext setuidContext) {
286+
269287
final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
270288
final RoutingContext routingContext = setuidContext.getRoutingContext();
271289

@@ -276,7 +294,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
276294

277295
final Map<String, PrivacyEnforcementAction> vendorIdToAction = bidderTcfResponse.getActions();
278296
final PrivacyEnforcementAction action = vendorIdToAction != null
279-
? vendorIdToAction.get(bidderCookieName)
297+
? vendorIdToAction.get(bidderName)
280298
: null;
281299

282300
final boolean notInGdprScope = BooleanUtils.isFalse(bidderTcfResponse.getUserInGdprScope());
@@ -285,7 +303,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
285303
if (hostVendorTcfResponse.isVendorAllowed() && isBidderVendorAllowed) {
286304
respondWithCookie(setuidContext);
287305
} else {
288-
metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
306+
metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName());
289307

290308
final HttpResponseStatus status = new HttpResponseStatus(UNAVAILABLE_FOR_LEGAL_REASONS,
291309
"Unavailable for legal reasons");
@@ -300,7 +318,7 @@ private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseRe
300318
}
301319
} else {
302320
final Throwable error = hostTcfResponseResult.cause();
303-
metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
321+
metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName());
304322
handleErrors(error, routingContext, tcfContext);
305323
}
306324
}

src/main/java/org/prebid/server/util/StreamUtil.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package org.prebid.server.util;
22

3+
import java.util.HashSet;
34
import java.util.Iterator;
5+
import java.util.Set;
46
import java.util.Spliterator;
7+
import java.util.function.Function;
8+
import java.util.function.Predicate;
59
import java.util.stream.Stream;
610
import java.util.stream.StreamSupport;
711

@@ -17,4 +21,9 @@ public static <T> Stream<T> asStream(Spliterator<T> spliterator) {
1721
public static <T> Stream<T> asStream(Iterator<T> iterator) {
1822
return StreamSupport.stream(IterableUtil.iterable(iterator).spliterator(), false);
1923
}
24+
25+
public static <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) {
26+
final Set<Object> seen = new HashSet<>();
27+
return value -> seen.add(keyExtractor.apply(value));
28+
}
2029
}

0 commit comments

Comments
 (0)