Skip to content

optable-targeting new features: non-blocking early network call + hid + id5 - #4601

Open
justadreamer wants to merge 92 commits into
prebid:masterfrom
Optable:optable-targeting-features
Open

justadreamer wants to merge 92 commits into
prebid:masterfrom
Optable:optable-targeting-features

Conversation

@justadreamer

@justadreamer justadreamer commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Type of changes

  • module update

What's the context?

Two related improvements to the optable-targeting module: the Optable API call no longer blocks the auction pipeline, and the Targeting API request now carries the parameters needed for mobile in-app resolution (hid, bundle/ver, ID5).

Latency: non-blocking early network call

The API call starts at raw-auction-request and runs in parallel with the rest of the auction. The result is consumed per bidder at bidder-request, where enrichment is actually injected, so the pipeline never waits on the network.

Stored requests and stored imps are merged after raw-auction-request, so for requests whose bidders or site/app live there (f.e. Prebid Mobile SDK traffic) the call is started by the async processed-auction-request hook (optable-targeting-async-processed-auction-request-hook) on the merged request instead. The same hook starts the call for AMP, which has no raw-auction-request stage. Bidders are read from both imp.ext.prebid.bidder and the old-style imp.ext.<bidder>.

Traffic controls

  • enrichment-percentage / bidder-enrichment-percentages: sampling rate (0-100) applied per bidder. A bidder with no entry falls back to the account default; 0 disables it entirely.
  • enrich-web / enrich-app: select which traffic sources are enriched.

Sampling applies to the split plan only. The legacy optable-targeting-processed-auction-request-hook enriches the whole bid request before bidders split out, so it cannot sample per bidder.

Targeting API request parameters

  • hid-prefixes (new config entry): comma-separated id prefixes to emit as hid=, in the given order (e.g. "e,p,i6,a,g,c"). Independent of id-prefix-order, and selective rather than merely ordering: only the listed prefixes are sent as hid.
  • bundle / ver: emitted from bidRequest.app when present.
  • id5_signature in: read from user.ext.optable, forwarded as a Targeting API query attribute alongside email/phone/zip/vid.
  • id5_signature out: returned in ext.prebid.passthrough.optable.id5_signature. PBS cannot persist the signature between requests, so it round-trips through the client, which caches it and replays it on the next auction.

Backwards compatibility

No breaking changes. The existing optable-targeting-processed-auction-request-hook keeps its previous synchronous behavior and is now deprecated. The split plan uses the new optable-targeting-async-processed-auction-request-hook at processed-auction-request, which never blocks and only starts the call for stored-request and AMP traffic.

Configuration (recommended)

hooks:
  modules:
    optable-targeting:
      enrichment-percentage: 100
      bidder-enrichment-percentages:
        appnexus: 75
        pubmatic: 0
      enrich-web: true
      enrich-app: true
      id-prefix-order: "e,v,c"
      hid-prefixes: "a,c,i6"

  execution-plan:
    endpoints:
      "/openrtb2/auction":
        stages:
          raw-auction-request:
            groups:
              - timeout: 50
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-raw-auction-request-hook"
          processed-auction-request:
            groups:
              - timeout: 50
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-async-processed-auction-request-hook"
          bidder-request:
            groups:
              - timeout: 500
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-bidder-request-hook"
          auction-response:
            groups:
              - timeout: 10
                hook-sequence:
                  - module-code: "optable-targeting"
                    hook-impl-code: "optable-targeting-auction-response-hook"

The bidder-request group timeout bounds the wait on the API call, so it must accommodate round-trip latency to the Optable edge. The raw-auction-request and processed-auction-request timeouts only cover dispatch.

To migrate from the legacy plan, replace optable-targeting-processed-auction-request-hook with optable-targeting-async-processed-auction-request-hook and add the raw-auction-request and bidder-request hooks.

Test plan

  • Unit tests
  • Semi-automated integration tests against the live Optable edge, covering enrichment with eids, per-bidder sampling (including a bidder pinned at 0%), ad server targeting keywords, ID5 signature passthrough, and the legacy execution plan

Quality check

  • Are your changes following our code style guidelines?
  • Are there any breaking changes in your code? No
  • Does your test coverage exceed 90%?
  • Are there any erroneous console logs, debuggers or leftover code in your changes? No

cc: @jrosendahl-opt @softcoder594

softcoder and others added 30 commits August 13, 2026 22:38
return early from the hook if (hasRawAuctionRequestHook && hasBidderRequestHook) - the new mode
random is 0-99, percentage is 0-100.  enrich when random < (strictly less than) percentage.  so if we specify percentage 0 we never enrich, if we specify percentage 100 we always enrich.
…ntage property is specified and it's value is 100%
The account config node is deserialized with the prebid mapper, which uses
SNAKE_CASE, so the unannotated fields bound to enrich_web and enrich_app while
the documented account keys are enrich-web and enrich-app. Per-account
overrides of both were silently dropped and the global value kept.
…change

Core changed ExecutionPlan.endpoints key type from Endpoint to
HookHttpEndpoint. Map.get accepts Object, so the stale lookup kept
compiling but always returned null against the EnumMap: the module read
a bidder-request hook timeout of 0, so the targeting API call inherited
only the raw-auction-request stage budget (50ms) instead of the
configured 500ms and timed out on every auction.
softcoder added 4 commits October 2, 2026 00:20
Bidders from stored imps are never enriched when other imps name bidders inline. If one imp lists its bidders and another only references a stored imp, the raw hook doesn't defer. This affects mixed Prebid Mobile and web setups.
@Net-burst
Net-burst requested a review from Lightwood13 October 9, 2026 13:53
import java.util.List;

public class BidRequestCleaner implements PayloadUpdate<AuctionRequestPayload> {
public class AuctionRequestCleaner implements PayloadUpdate<AuctionRequestPayload> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please make the default constructor private.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

}

private static BidRequest clearExtUserOptable(BidRequest bidRequest) {
protected static BidRequest clearExtUserOptable(BidRequest bidRequest) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This method should be private. There are no classes inheriting from this class.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

final ObjectNode cleanedOptable = cleanOptable((ObjectNode) optable);
final ExtUser extUserCopy = extUser.toBuilder().build();
extUserCopy.addProperties(extUser.getProperties());
if (cleanedOptable.isEmpty()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This if/else statement can be replaced with a ternary expression:

extUserCopy.addProperty(OPTABLE_FIELD, cleanedOptable.isEmpty() ? null : cleanedOptable);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

}

private static Set<String> extractUniqueBidders(BidRequest bidRequest) {
return Optional.ofNullable(bidRequest.getImp())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Optional.ofNullable().stream() can be simplified to Stream.ofNullable().

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

@AllArgsConstructor(staticName = "of")
public class BidderEnrichmentSampler {

private static final String PREBID_BIDDER_PATH = "/prebid/bidder";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Replace PREBID_BIDDER_PATH with

private static final JsonPointer PREBID_BIDDER_JSON_POINTER = JsonPointer.valueOf("/prebid/bidder");

to avoid recompiling the JsonPointer at every usage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

setEnrichRequestStatus(EnrichmentStatus.failure());
}

public boolean hasOptableTargetingProperties() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unused method, please remove.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

}

@Bean
OptableTargetingFlowResolver earlyOptableCallResolver(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Align bean name with the class name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

}
moduleContext.setBiddersToEnrich(biddersToEnrich);
final Account account = invocationContext.auctionContext().getAccount();
final long crossHookFutureTimeout =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a cross hook timeout, so it shouldn't be taken from a single hook's timeout. Please use the whole request's timeout invocationContext.auctionContext().getTimeoutContext().getTimeout(). The idea is to keep hook timeouts as small as possible to not stall the processing pipeline.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's done this way by design.
We understand the intention to keep hook execution times as short as possible to avoid blocking the auction processing pipeline. However, the current proposal doesn't address our requirements.

We need to make a remote API call during hook steps execution and receive its response before proceeding with bidder invocation, as the returned targeting data is required to enrich the auction request. If the hook times out before the API call completes, we lose that data and cannot properly enrich the request.

At the same time, we use this value to control the API call timeout. It should be shorter than the whole auction execution timeout, allowing us to fail fast if the remote service is slow or unresponsive, rather than waiting for the entire auction execution window.

In other words, Hook execution time:

  • lmits how long we wait for the remote service.
  • limits the total time the hook is allowed to consume.

This gives us predictable latency, prevents unnecessary slow API calls, and ensures that the hook has enough time to get a response from API call.

The shorter HTTP client timeout also allows us to keep Connection Pool more robust.

: defaultValue;
}

public boolean hasBidderRequestHook(Account account) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This class exposes two public functions: hasBidderRequestHook and getOptableTargetingBidderRequestTimeout. After fixing other comments, these functions shouldn't be necessary and this class can be removed.

@softcoder594 softcoder594 Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hasBidderRequestHook was removed. getOptableTargetingBidderRequestTimeout is left due to comment above.


final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext);

final Timeout timeout = apiTimeout == null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please remove this conditional from here. You can just change the apiTimeout parameter from type Long to type Timeout and perform these calculations in calling functions, instead of passing null in one of the cases and calculating timeout here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

enabled: true
triplelift:
enabled: true
adf:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please remove unnecessary bidders from sample config to keep it clean.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I kept several bidders because they are referenced in the sample request and configuration.

@Lightwood13

Copy link
Copy Markdown
Collaborator

Please update the PR description to match the latest changes.

  1. The "Backwards compatibility" section is currently incorrect.
  2. Recommended configuration incorrectly uses the legacy processed auction request hook instead of the new async hook.
  3. To migrate from the legacy plan, keep the processed-auction-request hook and add the raw-auction-request and bidder-request hooks. is incorrect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants