Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 49 additions & 2 deletions extra/modules/optable-targeting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ would result in this nesting in the JSON configuration:
| adserver-targeting | no | boolean | false | If set to true - will add the Optable-specific adserver targeting keywords into the PBS response for every `seatbid[].bid[].ext.prebid.targeting` |
| timeout | no | integer | none | A soft timeout (in ms) sent as a hint to the Targeting API endpoint to limit the request times to Optable's external tokenizer services |
| id-prefix-order | no | string | none | An optional string of comma separated id prefixes that prioritizes and specifies the order in which ids are provided to Targeting API in a query string. F.e. "c,c1,id5" will guarantee that Targeting API will see id=c:...,c1:...,id5:... if these ids are provided. id-prefixes not mentioned in this list will be added in arbitrary order after the priority prefix ids. This affects Targeting API processing logic |
| hid-prefixes | no | string | none | An optional string of comma separated id prefixes that should additionally be sent to the Targeting API as resolver hints in `hid=prefix:value` query parameters. See the section on Resolver Hints (hid) below for more detail. |
| enrichment-percentage | no | integer | 100 | Default percentage (0-100) of bid requests per bidder that will receive enrichment data. Set to 100 to enrich all requests, 0 to disable enrichment by default. |
| bidder-enrichment-percentages | no | map | none | Per-bidder overrides for `enrichment-percentage`. Keys are bidder names, values are percentages (0-100). F.e. `{"appnexus": 75, "criteo": 0}` enriches 75% of appnexus requests and none for criteo. Bidders not listed in this map fall back to the default `enrichment-percentage` (100% unless overridden). |
| enrich-web | no | boolean | true | Whether to enrich web traffic (requests with a `site` object). |
Expand Down Expand Up @@ -239,8 +240,8 @@ on identifier types. Targeting API accepts multiple id parameters - and their or

### Optable input erasure

**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` fields will be removed by the module from the original
OpenRTB request before being sent to bidders.
**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` and `.id5_signature` fields will be removed by the module
from the original OpenRTB request before being sent to bidders.

### Publisher Provided IDs (PPID) Mapping

Expand All @@ -263,6 +264,52 @@ ppid-mapping: {"id5-sync.com": "c1"}

This will lead to id5 ID supplied as `id=c1:...` to the Targeting API.

### Resolver Hints (`hid`)

In addition to the regular `id=prefix:value` parameters, the module can forward selected identifiers to the Targeting
API as resolver hints using the `hid=prefix:value` query parameter form. The set of prefixes that should be sent as
`hid` is configured via the `hid-prefixes` parameter, which accepts a comma-separated list of prefix names, f.e.:

```yaml
hid-prefixes: "c, i6"
```

## Targeting API Query Attributes

In addition to the identifier parameters, the module forwards the following attributes as query string parameters to
the Targeting API:

| Attribute | Source |
|------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| `gdpr` | `1` if GDPR applies to the request, `0` otherwise. |
| `gdpr_consent` | TCF consent string, sent when available and the consent is valid. |
| `gpp` | GPP string, sent when available in the resolved GPP context. |
| `gpp_sid` | Comma-separated list of active GPP section IDs (limited to the first two), sent when the set is non-empty. |
| `timeout` | Soft timeout hint (in ms, suffixed with `ms`), sent when the module-level `timeout` property is configured. |
| `osdk` | Always set to `prebid-server`, identifying the caller. |
| `bundle` | App bundle identifier, URL-encoded. Sent when the incoming request has an `app` object (`request.app.bundle`) and the bundle is non-empty. |
| `ver` | App version, URL-encoded. Sent only when `bundle` is non-empty and `request.app.ver` is present and non-empty. |
| `id5_signature` | ID5 signature, URL-encoded. Sent when the resolved `user.ext.optable.id5_signature` is present in the incoming request |

### App bundle and version

For app traffic (requests with an `app` object) the module forwards the application's bundle identifier as the
`bundle=` query parameter and, when available, its version as `ver=`. Both values are URL-encoded. The `ver` parameter
is only sent when `bundle` is present and non-empty; requests with a version but no bundle will not produce either
parameter.

### ID5 signature

The ID5 signature is propagated to the Targeting API in two ways:

1. **Request-side signature**: when the incoming OpenRTB request carries `user.ext.optable.id5_signature`, that value
is sent to the Targeting API as the `id5_signature=` query parameter.
2. **Response-side signature**: when the Targeting API response contains refs with an ID5 signature, the module
resolves the signature through the matching optable-eid and propagates it back into the request context, where it
is later injected into the bid response (`ext.prebid.passthrough.optable.id5_signature`) for downstream use.

The `id5_signature` field is also part of the Optable input erasure, see above.

## Analytics Tags

The following 2 analytics tags are written by the module:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties

@Bean
TargetingRequestExecutor targetingRequestExecutor(OptableTargeting optableTargeting,
UserFpdActivityMask userFpdActivityMask,
TimeoutFactory timeoutFactory) {
UserFpdActivityMask userFpdActivityMask,
TimeoutFactory timeoutFactory,
@Value("${logging.sampling-rate:0.01}") double logSamplingRate) {

return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory);
return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory, logSamplingRate);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.prebid.server.hooks.modules.optable.targeting.model;

import lombok.Value;

@Value(staticConstructor = "of")
public class App {

String bundle;

String ver;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class ModuleContext {

private boolean shouldSkipEnrichment;

private String id5Signature;

public static ModuleContext of(AuctionInvocationContext invocationContext) {
final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext();
return moduleContext != null ? moduleContext : new ModuleContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ public class OptableAttributes {
String userAgent;

Long timeout;

App app;

String id5Signature;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ public class Query {

String ids;

String hid;

String attributes;

String hidAttributes;

public String toQueryString() {
return ids + attributes;
return ids + hid + attributes + hidAttributes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.collections4.MapUtils;

import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -33,6 +33,9 @@ public final class OptableTargetingProperties {
@JsonProperty("id-prefix-order")
String idPrefixOrder;

@JsonProperty("hid-prefixes")
String hidPrefixes;

@JsonProperty("optable-inserter-eids-merge")
Set<String> optableInserterEidsMerge = Set.of();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public class ExtUserOptable extends FlexibleExtension {
String zip;

String vid;

String id5Signature;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.prebid.server.hooks.modules.optable.targeting.model.openrtb;

import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Value;

import java.util.List;
Expand All @@ -10,4 +11,6 @@ public class TargetingResult {
List<Audience> audience;

Ortb2 ortb2;

ObjectNode refs;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderRequestEnricher;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
Expand Down Expand Up @@ -61,6 +62,7 @@ private Future<InvocationResult<BidderRequestPayload>> enrichedPayload(Targeting

if (hasData) {
moduleContext.setTargeting(targetingResult.getAudience());
moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New issue introduced by the refactor: the signature is silently dropped in the raw+bidder plan when per-bidder enrichment is off.

This is now the only place the signature is set in that plan, but the hook returns early at lines 37-39 when properties.isPerBidderEnrichmentEnabled() is false - and that is false for the default config, since it requires enrichmentPercentage != 100 || !bidderEnrichmentPercentages.isEmpty() (OptableTargetingProperties.java:59-61). With both hooks in the execution plan, OptableTargetingProcessedAuctionRequestHook.java:76-78 also returns without setting it.

So with enrichment-percentage: 100 and no per-bidder map, moduleContext.getId5Signature() is null at response time and the signature never reaches the client. At 073b1d543 the response hook resolved it from the future's TargetingResult, so it still got through. Behavior regression from the fix-up, and no test covers this combination.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());
}

Expand All @@ -82,7 +84,7 @@ private Future<InvocationResult<BidderRequestPayload>> failedAction(ModuleContex
final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags(
bidder, outcome, executionTime);

return noAction(moduleContext, analyticsTags);
return noActionResponse(moduleContext, analyticsTags);
}

private static boolean hasEnrichmentData(TargetingResult targetingResult) {
Expand All @@ -98,6 +100,27 @@ private static long calcExecutionTime(ModuleContext moduleContext) {
}

private Future<InvocationResult<BidderRequestPayload>> noAction(ModuleContext moduleContext, Tags analyticsTags) {
final Future<TargetingResult> targetingCall = moduleContext.getOptableTargetingCall();
if (targetingCall != null) {
return targetingCall.compose(targetingResult -> {
moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult));

return Future.succeededFuture(
InvocationResultImpl.<BidderRequestPayload>builder()
.status(InvocationStatus.success)
.action(InvocationAction.no_action)
.analyticsTags(analyticsTags)
.moduleContext(moduleContext)
.build());
});
Comment on lines +105 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This compose has no failure branch, so when the targeting call fails (timeout, non-2xx, circuit breaker open) the returned future fails and the hook execution is reported as failed. Before the signature change this path returned no_action unconditionally. This bidder is not being enriched anyway, so a failed targeting call should not change the outcome.

Adding .recover fixes it, and the inline builder can reuse the existing noActionResponse helper so both branches return the same thing:

Suggested change
return targetingCall.compose(targetingResult -> {
moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult));
return Future.succeededFuture(
InvocationResultImpl.<BidderRequestPayload>builder()
.status(InvocationStatus.success)
.action(InvocationAction.no_action)
.analyticsTags(analyticsTags)
.moduleContext(moduleContext)
.build());
});
return targetingCall
.compose(targetingResult -> {
moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult));
return noActionResponse(moduleContext, analyticsTags);
})
// this bidder is not enriched anyway, so a failed targeting call
// must not turn into a hook execution failure
.recover(ignored -> noActionResponse(moduleContext, analyticsTags));

}

return noActionResponse(moduleContext, analyticsTags);
}

private Future<InvocationResult<BidderRequestPayload>> noActionResponse(ModuleContext moduleContext,
Tags analyticsTags) {

return Future.succeededFuture(
InvocationResultImpl.<BidderRequestPayload>builder()
.status(InvocationStatus.success)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5SignatureBidResponseEnricher;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
Expand Down Expand Up @@ -62,15 +63,35 @@ public Future<InvocationResult<AuctionResponsePayload>> call(AuctionResponsePayl

return validationStatus.getStatus() == Status.SUCCESS
? enrichedPayload(moduleContext)
: success(moduleContext);
: enrichedById5SignaturePayload(moduleContext);
}

private Future<InvocationResult<AuctionResponsePayload>> enrichedPayload(ModuleContext moduleContext) {
final List<Audience> targeting = moduleContext.getTargeting();
final String id5Signature = moduleContext.getId5Signature();

return CollectionUtils.isNotEmpty(targeting)
? update(BidResponseEnricher.of(targeting, objectMapper, jsonMerger), moduleContext)
: success(moduleContext);
? update(fullEnrichmentChain(targeting, id5Signature), moduleContext)
: update(id5SignatureEnrichmentChain(id5Signature), moduleContext);
}

private Future<InvocationResult<AuctionResponsePayload>> enrichedById5SignaturePayload(
ModuleContext moduleContext) {

final String id5Signature = moduleContext.getId5Signature();

return update(id5SignatureEnrichmentChain(id5Signature), moduleContext);
}

private PayloadUpdate<AuctionResponsePayload> fullEnrichmentChain(final List<Audience> targeting,
final String id5Signature) {

return BidResponseEnricher.of(targeting, objectMapper, jsonMerger)
.andThen(Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger))::apply;
}

private PayloadUpdate<AuctionResponsePayload> id5SignatureEnrichmentChain(String id5Signature) {
return Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger);
}

private Future<InvocationResult<AuctionResponsePayload>> update(
Expand All @@ -88,13 +109,8 @@ private Future<InvocationResult<AuctionResponsePayload>> update(
}

private Future<InvocationResult<AuctionResponsePayload>> success(ModuleContext moduleContext) {
return Future.succeededFuture(
InvocationResultImpl.<AuctionResponsePayload>builder()
.status(InvocationStatus.success)
.action(InvocationAction.no_action)
.moduleContext(moduleContext)
.analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext))
.build());
final String id5Signature = moduleContext.getId5Signature();
return update(id5SignatureEnrichmentChain(id5Signature), moduleContext);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
Expand Down Expand Up @@ -105,6 +106,7 @@ private Future<InvocationResult<AuctionRequestPayload>> enrichPayload(
OptableTargetingProperties properties) {

moduleContext.setTargeting(targetingResult.getAudience());
moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult));
moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());

final PayloadUpdate<AuctionRequestPayload> payloadUpdate =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public class BidRequestCleaner implements PayloadUpdate<AuctionRequestPayload> {

private static final String OPTABLE_FIELD = "optable";
private static final List<String> FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid");
private static final List<String> FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid", "id5_signature");

public static BidRequestCleaner instance() {
return new BidRequestCleaner();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable;
import org.prebid.server.json.ObjectMapperProvider;
import org.prebid.server.log.ConditionalLogger;
import org.prebid.server.log.LoggerFactory;

public class ExtUserOptableResolver {

private static final ConditionalLogger conditionalLogger =
new ConditionalLogger(LoggerFactory.getLogger(ExtUserOptableResolver.class));

private ExtUserOptableResolver() {
}

public static ExtUserOptable resolveExtUserOptable(JsonNode node, double logSamplingRate) {
try {
return ObjectMapperProvider.mapper().treeToValue(node, ExtUserOptable.class);
} catch (JsonProcessingException e) {
conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate);
return null;
}
}
}
Loading