Skip to content

Commit a526e37

Browse files
authored
Add profiles (#4007)
1 parent fc0fd73 commit a526e37

File tree

112 files changed

+5439
-1242
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+5439
-1242
lines changed

sample/configs/prebid-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ settings:
2424
settings-filename: sample/configs/sample-app-settings.yaml
2525
stored-requests-dir: sample
2626
stored-imps-dir: sample
27+
profiles-dir: sample
2728
stored-responses-dir: sample
2829
categories-dir:
2930
gdpr:

src/main/java/org/prebid/server/auction/BidResponseCreator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.commons.lang3.StringUtils;
2626
import org.apache.commons.lang3.tuple.Pair;
2727
import org.prebid.server.auction.categorymapping.CategoryMappingService;
28+
import org.prebid.server.auction.externalortb.StoredRequestProcessor;
2829
import org.prebid.server.auction.model.AuctionContext;
2930
import org.prebid.server.auction.model.AuctionParticipation;
3031
import org.prebid.server.auction.model.BidInfo;

src/main/java/org/prebid/server/auction/ExchangeService.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.commons.collections4.map.CaseInsensitiveMap;
2424
import org.apache.commons.lang3.ObjectUtils;
2525
import org.apache.commons.lang3.StringUtils;
26+
import org.apache.commons.lang3.tuple.Pair;
2627
import org.prebid.server.activity.Activity;
2728
import org.prebid.server.activity.ComponentType;
2829
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
@@ -31,6 +32,7 @@
3132
import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload;
3233
import org.prebid.server.auction.aliases.AlternateBidderCodesConfig;
3334
import org.prebid.server.auction.aliases.BidderAliases;
35+
import org.prebid.server.auction.externalortb.StoredResponseProcessor;
3436
import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult;
3537
import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor;
3638
import org.prebid.server.auction.model.AuctionContext;
@@ -101,7 +103,6 @@
101103
import org.prebid.server.util.ListUtil;
102104
import org.prebid.server.util.PbsUtil;
103105
import org.prebid.server.util.StreamUtil;
104-
import org.apache.commons.lang3.tuple.Pair;
105106

106107
import java.math.BigDecimal;
107108
import java.time.Clock;
@@ -570,10 +571,10 @@ private static List<String> firstPartyDataBidders(ExtRequest requestExt) {
570571
}
571572

572573
private Map<String, Pair<User, Device>> prepareUsersAndDevices(List<String> bidders,
573-
AuctionContext context,
574-
BidderAliases aliases,
575-
Map<String, ExtBidderConfigOrtb> biddersToConfigs,
576-
Map<String, List<String>> eidPermissions) {
574+
AuctionContext context,
575+
BidderAliases aliases,
576+
Map<String, ExtBidderConfigOrtb> biddersToConfigs,
577+
Map<String, List<String>> eidPermissions) {
577578

578579
final BidRequest bidRequest = context.getBidRequest();
579580
final List<String> firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt());

src/main/java/org/prebid/server/auction/SkippedAuctionService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.apache.commons.collections4.CollectionUtils;
88
import org.apache.commons.collections4.ListUtils;
99
import org.apache.commons.lang3.StringUtils;
10+
import org.prebid.server.auction.externalortb.StoredResponseProcessor;
1011
import org.prebid.server.auction.model.AuctionContext;
1112
import org.prebid.server.auction.model.StoredResponseResult;
1213
import org.prebid.server.bidder.model.BidderError;

src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ private static BidRequest readBidRequest(String defaultBidRequestPath,
129129
: null;
130130
}
131131

132-
private StoredDataResult updateMetrics(StoredDataResult storedDataResult,
133-
Set<String> requestIds,
134-
Set<String> impIds) {
132+
private StoredDataResult<String> updateMetrics(StoredDataResult<String> storedDataResult,
133+
Set<String> requestIds,
134+
Set<String> impIds) {
135135

136136
requestIds.forEach(
137137
id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id)));
@@ -142,7 +142,7 @@ private StoredDataResult updateMetrics(StoredDataResult storedDataResult,
142142
return storedDataResult;
143143
}
144144

145-
private WithPodErrors<BidRequest> toBidRequestWithPodErrors(StoredDataResult storedResult,
145+
private WithPodErrors<BidRequest> toBidRequestWithPodErrors(StoredDataResult<String> storedResult,
146146
BidRequestVideo videoRequest,
147147
String storedBidRequestId) {
148148

@@ -161,7 +161,7 @@ private WithPodErrors<BidRequest> toBidRequestWithPodErrors(StoredDataResult sto
161161

162162
private BidRequestVideo mergeBidRequest(BidRequestVideo originalRequest,
163163
String storedRequestId,
164-
StoredDataResult storedDataResult) {
164+
StoredDataResult<String> storedDataResult) {
165165

166166
final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId);
167167
if (enforceStoredRequest && StringUtils.isBlank(storedRequest)) {
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
package org.prebid.server.auction.externalortb;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
6+
import com.iab.openrtb.request.BidRequest;
7+
import com.iab.openrtb.request.Imp;
8+
import io.vertx.core.Future;
9+
import org.apache.commons.lang3.StringUtils;
10+
import org.prebid.server.auction.model.AuctionContext;
11+
import org.prebid.server.exception.InvalidProfileException;
12+
import org.prebid.server.exception.InvalidRequestException;
13+
import org.prebid.server.execution.timeout.Timeout;
14+
import org.prebid.server.execution.timeout.TimeoutFactory;
15+
import org.prebid.server.json.JacksonMapper;
16+
import org.prebid.server.json.JsonMerger;
17+
import org.prebid.server.log.ConditionalLogger;
18+
import org.prebid.server.log.LoggerFactory;
19+
import org.prebid.server.metric.MetricName;
20+
import org.prebid.server.metric.Metrics;
21+
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
22+
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
23+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
24+
import org.prebid.server.settings.ApplicationSettings;
25+
import org.prebid.server.settings.model.Account;
26+
import org.prebid.server.settings.model.AccountAuctionConfig;
27+
import org.prebid.server.settings.model.AccountProfilesConfig;
28+
import org.prebid.server.settings.model.Profile;
29+
import org.prebid.server.settings.model.StoredDataResult;
30+
31+
import java.util.ArrayList;
32+
import java.util.Collection;
33+
import java.util.Collections;
34+
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.Objects;
38+
import java.util.Optional;
39+
import java.util.Set;
40+
import java.util.stream.Collectors;
41+
42+
public class ProfilesProcessor {
43+
44+
private static final ConditionalLogger conditionalLogger =
45+
new ConditionalLogger(LoggerFactory.getLogger(ProfilesProcessor.class));
46+
47+
private final int maxProfiles;
48+
private final long defaultTimeoutMillis;
49+
private final boolean failOnUnknown;
50+
private final double logSamplingRate;
51+
private final ApplicationSettings applicationSettings;
52+
private final TimeoutFactory timeoutFactory;
53+
private final Metrics metrics;
54+
private final JacksonMapper mapper;
55+
private final JsonMerger jsonMerger;
56+
57+
public ProfilesProcessor(int maxProfiles,
58+
long defaultTimeoutMillis,
59+
boolean failOnUnknown,
60+
double logSamplingRate,
61+
ApplicationSettings applicationSettings,
62+
TimeoutFactory timeoutFactory,
63+
Metrics metrics,
64+
JacksonMapper mapper,
65+
JsonMerger jsonMerger) {
66+
67+
this.maxProfiles = maxProfiles;
68+
this.defaultTimeoutMillis = defaultTimeoutMillis;
69+
this.failOnUnknown = failOnUnknown;
70+
this.logSamplingRate = logSamplingRate;
71+
this.applicationSettings = Objects.requireNonNull(applicationSettings);
72+
this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
73+
this.metrics = Objects.requireNonNull(metrics);
74+
this.mapper = Objects.requireNonNull(mapper);
75+
this.jsonMerger = Objects.requireNonNull(jsonMerger);
76+
}
77+
78+
public Future<BidRequest> process(AuctionContext auctionContext, BidRequest bidRequest) {
79+
final String accountId = Optional.ofNullable(auctionContext.getAccount())
80+
.map(Account::getId)
81+
.orElse(StringUtils.EMPTY);
82+
83+
final AllProfilesIds profilesIds = profilesIds(bidRequest, auctionContext, accountId);
84+
if (profilesIds.isEmpty()) {
85+
return Future.succeededFuture(bidRequest);
86+
}
87+
88+
final boolean failOnUnknown = isFailOnUnknown(auctionContext.getAccount());
89+
90+
return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest))
91+
.compose(profiles -> emitMetrics(accountId, profiles, auctionContext, failOnUnknown))
92+
.map(profiles -> mergeResults(
93+
applyRequestProfiles(
94+
profilesIds.request(),
95+
profiles.getStoredIdToRequest(),
96+
bidRequest,
97+
failOnUnknown),
98+
applyImpsProfiles(
99+
profilesIds.imps(),
100+
profiles.getStoredIdToImp(),
101+
bidRequest.getImp(),
102+
failOnUnknown)))
103+
.recover(error -> Future.failedFuture(
104+
new InvalidRequestException("Error during processing profiles: " + error.getMessage())));
105+
}
106+
107+
private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auctionContext, String accountId) {
108+
final AllProfilesIds initialProfilesIds = new AllProfilesIds(
109+
requestProfilesIds(bidRequest),
110+
bidRequest.getImp().stream().map(this::impProfilesIds).toList());
111+
112+
final AllProfilesIds profilesIds = truncate(
113+
initialProfilesIds,
114+
Optional.ofNullable(auctionContext.getAccount())
115+
.map(Account::getAuction)
116+
.map(AccountAuctionConfig::getProfiles)
117+
.map(AccountProfilesConfig::getLimit)
118+
.orElse(maxProfiles));
119+
120+
if (auctionContext.getDebugContext().isDebugEnabled() && !profilesIds.equals(initialProfilesIds)) {
121+
auctionContext.getDebugWarnings().add("Profiles exceeded the limit.");
122+
metrics.updateAccountProfileMetric(accountId, MetricName.limit_exceeded);
123+
}
124+
125+
return profilesIds;
126+
}
127+
128+
private static List<String> requestProfilesIds(BidRequest bidRequest) {
129+
return Optional.ofNullable(bidRequest)
130+
.map(BidRequest::getExt)
131+
.map(ExtRequest::getPrebid)
132+
.map(ExtRequestPrebid::getProfiles)
133+
.orElse(Collections.emptyList());
134+
}
135+
136+
private List<String> impProfilesIds(Imp imp) {
137+
return Optional.ofNullable(imp.getExt())
138+
.map(ext -> ext.get("prebid"))
139+
.map(this::parseImpExt)
140+
.map(ExtImpPrebid::getProfiles)
141+
.orElse(Collections.emptyList());
142+
}
143+
144+
private ExtImpPrebid parseImpExt(JsonNode jsonNode) {
145+
try {
146+
return mapper.mapper().treeToValue(jsonNode, ExtImpPrebid.class);
147+
} catch (JsonProcessingException e) {
148+
throw new InvalidRequestException(e.getMessage());
149+
}
150+
}
151+
152+
private static AllProfilesIds truncate(AllProfilesIds profilesIds, int maxProfiles) {
153+
final List<String> requestProfiles = profilesIds.request();
154+
final int impProfilesLimit = Math.max(0, maxProfiles - requestProfiles.size());
155+
156+
return new AllProfilesIds(
157+
truncate(requestProfiles, maxProfiles),
158+
profilesIds.imps().stream()
159+
.map(impProfiles -> truncate(impProfiles, impProfilesLimit))
160+
.toList());
161+
}
162+
163+
private static <T> List<T> truncate(List<T> list, int maxSize) {
164+
return list.size() > maxSize ? list.subList(0, maxSize) : list;
165+
}
166+
167+
private long timeoutMillis(BidRequest bidRequest) {
168+
final Long tmax = bidRequest.getTmax();
169+
return tmax != null && tmax > 0 ? tmax : defaultTimeoutMillis;
170+
}
171+
172+
private boolean isFailOnUnknown(Account account) {
173+
return Optional.ofNullable(account)
174+
.map(Account::getAuction)
175+
.map(AccountAuctionConfig::getProfiles)
176+
.map(AccountProfilesConfig::getFailOnUnknown)
177+
.orElse(failOnUnknown);
178+
}
179+
180+
private Future<StoredDataResult<Profile>> fetchProfiles(String accountId,
181+
AllProfilesIds allProfilesIds,
182+
long timeoutMillis) {
183+
184+
final Set<String> requestProfilesIds = new HashSet<>(allProfilesIds.request());
185+
final Set<String> impProfilesIds = allProfilesIds.imps().stream()
186+
.flatMap(Collection::stream)
187+
.collect(Collectors.toSet());
188+
final Timeout timeout = timeoutFactory.create(timeoutMillis);
189+
190+
return applicationSettings.getProfiles(accountId, requestProfilesIds, impProfilesIds, timeout);
191+
}
192+
193+
private Future<StoredDataResult<Profile>> emitMetrics(String accountId,
194+
StoredDataResult<Profile> fetchResult,
195+
AuctionContext auctionContext,
196+
boolean failOnUnknown) {
197+
198+
final List<String> errors = fetchResult.getErrors();
199+
if (!errors.isEmpty()) {
200+
metrics.updateProfileMetric(MetricName.missing);
201+
202+
if (auctionContext.getDebugContext().isDebugEnabled()) {
203+
metrics.updateAccountProfileMetric(accountId, MetricName.missing);
204+
auctionContext.getDebugWarnings().addAll(errors);
205+
}
206+
207+
if (failOnUnknown) {
208+
return Future.failedFuture(new InvalidProfileException(errors));
209+
}
210+
}
211+
212+
return Future.succeededFuture(fetchResult);
213+
}
214+
215+
private BidRequest applyRequestProfiles(List<String> profilesIds,
216+
Map<String, Profile> idToRequestProfile,
217+
BidRequest bidRequest,
218+
boolean failOnUnknown) {
219+
220+
return !idToRequestProfile.isEmpty()
221+
? applyProfiles(profilesIds, idToRequestProfile, bidRequest, failOnUnknown)
222+
: bidRequest;
223+
}
224+
225+
private <T> T applyProfiles(List<String> profilesIds,
226+
Map<String, Profile> idToProfile,
227+
T original,
228+
boolean failOnUnknown) {
229+
230+
if (profilesIds.isEmpty()) {
231+
return original;
232+
}
233+
234+
ObjectNode result = mapper.mapper().valueToTree(original);
235+
for (String profileId : profilesIds) {
236+
try {
237+
final Profile profile = idToProfile.get(profileId);
238+
result = profile != null ? mergeProfile(result, profile) : result;
239+
} catch (InvalidRequestException e) {
240+
final String message = "Can't merge with profile %s: %s".formatted(profileId, e.getMessage());
241+
242+
metrics.updateProfileMetric(MetricName.invalid);
243+
conditionalLogger.error(message, logSamplingRate);
244+
if (failOnUnknown) {
245+
throw new InvalidProfileException(message);
246+
}
247+
}
248+
}
249+
250+
try {
251+
return mapper.mapper().treeToValue(result, (Class<T>) original.getClass());
252+
} catch (JsonProcessingException e) {
253+
throw new InvalidProfileException(e.getMessage());
254+
}
255+
}
256+
257+
private ObjectNode mergeProfile(ObjectNode original, Profile profile) {
258+
return switch (profile.getMergePrecedence()) {
259+
case REQUEST -> merge(original, profile.getBody());
260+
case PROFILE -> merge(profile.getBody(), original);
261+
};
262+
}
263+
264+
private ObjectNode merge(JsonNode takePrecedence, JsonNode other) {
265+
if (!takePrecedence.isObject() || !other.isObject()) {
266+
throw new InvalidRequestException("One of the merge arguments is not an object.");
267+
}
268+
269+
return (ObjectNode) jsonMerger.merge(takePrecedence, other);
270+
}
271+
272+
private List<Imp> applyImpsProfiles(List<List<String>> profilesIds,
273+
Map<String, Profile> idToImpProfile,
274+
List<Imp> imps,
275+
boolean failOnUnknown) {
276+
277+
if (idToImpProfile.isEmpty()) {
278+
return imps;
279+
}
280+
281+
final List<Imp> updatedImps = new ArrayList<>(imps);
282+
for (int i = 0; i < profilesIds.size(); i++) {
283+
updatedImps.set(i, applyProfiles(
284+
profilesIds.get(i),
285+
idToImpProfile,
286+
imps.get(i),
287+
failOnUnknown));
288+
}
289+
290+
return Collections.unmodifiableList(updatedImps);
291+
}
292+
293+
private static BidRequest mergeResults(BidRequest bidRequest, List<Imp> imps) {
294+
return bidRequest.toBuilder().imp(imps).build();
295+
}
296+
297+
private record AllProfilesIds(List<String> request, List<List<String>> imps) {
298+
299+
public boolean isEmpty() {
300+
return request.isEmpty() && imps.stream().allMatch(List::isEmpty);
301+
}
302+
}
303+
}

0 commit comments

Comments
 (0)