diff --git a/.gitignore b/.gitignore
index 5f0817bd269..b1ceb24647f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,9 +5,19 @@
*.classpath
.vscode/
+.vscode_old/
*.project
*.factorypath
+# AI assistant / editor local configuration
+.cursorrules
+.cursor/
+.gemini/
+GEMINI.md
+CLAUDE.md
+AGENTS.md
+.claude/
+
.vertx/
target/
diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md
index 3ae7bd5f659..586627d1288 100644
--- a/extra/modules/optable-targeting/README.md
+++ b/extra/modules/optable-targeting/README.md
@@ -12,10 +12,11 @@ Targeting API endpoint is configurable per publisher.
### Execution Plan
-This module runs at two stages:
+This module runs at three stages:
-* Processed Auction Request: to enrich `user.eids` and `user.data`.
-* Auction Response: to inject ad server targeting.
+* Raw Auction Request: initiates a non-blocking Optable API call early in the auction lifecycle.
+* Bidder Request: awaits the API response and enriches individual bidder requests with `user.eids` and `user.data`.
+* Auction Response: injects ad server targeting.
We recommend defining the execution plan in the account config so the module is only invoked for specific accounts. See
below for an example.
@@ -26,10 +27,8 @@ There is no host-company level config for this module.
### Account-Level Config
-To start using current module in PBS-Java you have to enable module and add
-`optable-targeting-processed-auction-request-hook` and `optable-targeting-auction-response-hook` into hooks execution
-plan inside your config file:
-Here's a general template for the account config used in PBS-Java:
+To start using the module in PBS-Java you have to enable it and add the hooks into the execution plan in your config
+file. Here's the recommended configuration:
```yaml
hooks:
@@ -40,14 +39,27 @@ hooks:
"endpoints": {
"/openrtb2/auction": {
"stages": {
- "processed-auction-request": {
+ "raw-auction-request": {
"groups": [
{
- "timeout": 100,
+ "timeout": 50,
"hook-sequence": [
{
"module-code": "optable-targeting",
- "hook-impl-code": "optable-targeting-processed-auction-request-hook"
+ "hook-impl-code": "optable-targeting-raw-auction-request-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "bidder-request": {
+ "groups": [
+ {
+ "timeout": 50,
+ "hook-sequence": [
+ {
+ "module-code": "optable-targeting",
+ "hook-impl-code": "optable-targeting-bidder-request-hook"
}
]
}
@@ -83,6 +95,15 @@ Sample module enablement configuration in JSON and YAML formats:
"api-endpoint": "endpoint",
"api-key": "key",
"timeout": 50,
+ "enrichment-percentage": 100,
+ "bidder-enrichment-percentages": {
+ "appnexus": 75,
+ "rubicon": 75,
+ "pubmatic": 100,
+ "criteo": 0
+ },
+ "enrich-web": true,
+ "enrich-app": true,
"ppid-mapping": {
"pubcid.org": "c"
},
@@ -98,24 +119,65 @@ Sample module enablement configuration in JSON and YAML formats:
api-endpoint: endpoint
api-key: key
timeout: 50
+ enrichment-percentage: 100
+ bidder-enrichment-percentages:
+ appnexus: 75
+ rubicon: 75
+ pubmatic: 100
+ criteo: 0
+ enrich-web: true
+ enrich-app: true
ppid-mapping: {
"pubcid.org": "c"
}
adserver-targeting: false
```
+### Migrating from legacy configuration
+
+Previous versions of the module used a `processed-auction-request` hook (alongside the `auction-response` hook) that both
+made the API call and enriched the request synchronously in one step, blocking the auction pipeline. The new
+configuration replaces it with two hooks: `raw-auction-request` (initiates the API call early) and `bidder-request`
+(awaits the result and enriches per-bidder), while the `auction-response` hook remains unchanged. If your execution plan contains the following fragment, it should
+be replaced with the `raw-auction-request` and `bidder-request` hooks shown above:
+
+```json
+"processed-auction-request": {
+ "groups": [
+ {
+ "timeout": 600,
+ "hook-sequence": [
+ {
+ "module-code": "optable-targeting",
+ "hook-impl-code": "optable-targeting-processed-auction-request-hook"
+ }
+ ]
+ }
+ ]
+}
+```
+
+The `processed-auction-request` hook is still supported for backwards compatibility. It detects whether the new hooks
+(`raw-auction-request` and `bidder-request`) are present in the execution plan. If both are active, it passes through
+immediately without blocking the pipeline. If the new hooks are absent, it falls back to the legacy synchronous
+behavior. This means the legacy fragment can be kept during migration without negating the latency benefit of the new
+configuration.
+
### Timeout considerations
-The timeout value specified in the execution plan for the `processed-auction-request` hook is very important to be
-picked such that the hook has enough time to make a roundtrip to Optable Targeting Edge API over HTTP.
+The `bidder-request` hook timeout is used as the timeout budget for the Optable Targeting API call Future that is
+initiated in the `raw-auction-request` stage. The API call runs in parallel with other auction processing, so the
+effective wait time at the `bidder-request` stage is typically much shorter than the full API roundtrip. The
+`raw-auction-request` hook timeout only needs to cover its own lightweight setup (validation, sampling) and can be kept
+short.
**Note:** Do not confuse hook timeout value with the module timeout parameter which is optional. The hook timeout value
would depend on the cloud/region where the PBS instance is hosted and the latency to reach the Optable's servers. This
will need to be verified experimentally upon deployment.
The timeout value for the `auction-response` can be set to 10 ms - usually it will be sub-millisecond time as there are
-no HTTP calls made in this hook - Optable-specific keywords are cached on the `processed-auction-request` stage and
-retrieved from the module invocation context later.
+no HTTP calls made in this hook - Optable-specific keywords are cached on earlier stages and retrieved from the module
+invocation context later.
## Module Configuration Parameters for PBS-Java
@@ -133,14 +195,19 @@ would result in this nesting in the JSON configuration:
```
-| Param Name | Required | Type | Default value | Description |
-|:-------------------|:---------|:--------|:---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| api-endpoint | yes | string | none | Optable Targeting Edge API endpoint URL, required |
-| api-key | no | string | none | If the API is protected with a key - this param needs to be specified to be sent in the auth header |
-| ppid-mapping | no | map | none | This specifies PPID source (`user.ext.eids[].source`) to a custom identifier prefix mapping, f.e. `{"example.com" : "c"}`. See the section on ID Mapping below for more detail. |
-| 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 | false | 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 |
+| Param Name | Required | Type | Default value | Description |
+|:-------------------------------|:---------|:--------|:--------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| api-endpoint | yes | string | none | Optable Targeting Edge API endpoint URL, required |
+| api-key | no | string | none | If the API is protected with a key - this param needs to be specified to be sent in the auth header |
+| ppid-mapping | no | map | none | This specifies PPID source (`user.ext.eids[].source`) to a custom identifier prefix mapping, f.e. `{"example.com" : "c"}`. See the section on ID Mapping below for more detail. |
+| 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). |
+| enrich-app | no | boolean | true | Whether to enrich app traffic (requests with an `app` object). |
## ID Mapping
@@ -173,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
@@ -197,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:
diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml
index 66ed6f1d1ca..644829188ed 100644
--- a/extra/modules/optable-targeting/pom.xml
+++ b/extra/modules/optable-targeting/pom.xml
@@ -12,4 +12,12 @@
optable-targeting
Optable targeting module
+
+
+
+ io.vertx
+ vertx-junit5
+ test
+
+
diff --git a/extra/modules/optable-targeting/sample-requests/data.json b/extra/modules/optable-targeting/sample-requests/data.json
index d05f9a5eebc..b59400cc6d4 100644
--- a/extra/modules/optable-targeting/sample-requests/data.json
+++ b/extra/modules/optable-targeting/sample-requests/data.json
@@ -1,127 +1,269 @@
{
- "test": 1,
- "id": "1",
- "imp":
- [
+ "imp": [
{
- "id": "1",
- "banner":
- {
- "w": 300,
- "h": 250
+ "ext": {
+ "prebid": {
+ "bidder": {
+ "triplelift": {
+ "inventoryCode": "123"
+ },
+ "pubmatic": {
+ "publisherId": "156209",
+ "adSlot": "/1234567/test"
+ },
+ "yieldmo": {
+ "placementId": "12314235"
+ },
+ "openx": {
+ "delDomain": "domain.openx.net",
+ "unit": "12324324",
+ "customParams": {
+ "sens": [
+ "alc"
+ ]
+ }
+ },
+ "grid": {
+ "uid": 123
+ },
+ "unruly": {
+ "siteId": 567890
+ },
+ "conversant": {
+ "tag_id": "some_id",
+ "site_id": "345678"
+ },
+ "33across": {
+ "siteId": "some_id",
+ "productId": "ddff"
+ },
+ "adform": {
+ "mname": "some_name",
+ "mid": 123456789,
+ "adxDomain": "adx2.adform.net"
+ },
+ "improvedigital": {
+ "publisherId": 1234,
+ "placementId": 123456789
+ },
+ "colossus": {
+ "groupId": "123"
+ }
+ },
+ "adunitcode": "AdUnit_Name_1"
+ }
},
- "ext":
- {
- "prebid":
- {
- "storedauctionresponse": { "id": "optable-stored-response" },
- "bidder":
+ "id": "imp_id",
+ "banner": {
+ "topframe": 1,
+ "format": [
{
- "appnexus":
- {
- "placementId": 0
- }
+ "w": 320,
+ "h": 50
}
- }
- }
+ ]
+ },
+ "bidfloor": 1.0,
+ "bidfloorcur": "USD"
}
],
- "site":
- {
- "domain": "test.com",
- "publisher":
- {
- "domain": "test.com",
- "id": "1"
- },
- "page": "https://www.test.com/"
- },
- "device":
- {
- "ip": "8.8.8.8"
- },
- "user":
- {
- "ext":
- {
- "optable":
- {
- "email": "5837d278eabede28e37b5766399ed0d1a4cdc36acee8d35710a255032f45beda"
+ "cur": [
+ "USD"
+ ],
+ "at": 1,
+ "device": {
+ "w": 591,
+ "h": 771,
+ "dnt": 0,
+ "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
+ "language": "en",
+ "js": 1,
+ "ip": "82.212.42.32",
+ "sua": {
+ "source": 2,
+ "platform": {
+ "brand": "macOS",
+ "version": [
+ "14",
+ "0",
+ "0"
+ ]
},
- "eids":
- [
+ "browsers": [
{
- "source": "growthcode.io",
- "uids":
- [
- {
- "id": "fb58593e-7ac6-48bd-b2de-89a758726362",
- "atype": 1
- }
- ]
- },
- {
- "source": "pubcid.org",
- "uids": [
- {
- "id": "test",
- "atype": 1
- }
+ "brand": "Chromium",
+ "version": [
+ "120",
+ "0",
+ "0",
+ "0"
]
},
{
- "source": "crwdcntrl.net",
- "uids":
- [
- {
- "id": "dd1b31e65f5e45548c11a0275ba3a8072c00e3a2a0493e8f5a8f54f8067e8b00",
- "atype": 1
- }
+ "brand": "Not-A.Brand",
+ "version": [
+ "24",
+ "0",
+ "0",
+ "0"
]
},
{
- "source": "amxdt.net",
- "uids":
- [
- {
- "id": "amx*3*a583802a-e6fe-48d7-87c6-7db1b6a4a73a*70f06cdcf8ab0b4ac07a56860ed0e0b6ef0388dc0b0ab5a1dd725999d3b339cf",
- "atype": 1
- }
+ "brand": "Google Chrome",
+ "version": [
+ "120",
+ "0",
+ "0",
+ "0"
]
- },
+ }
+ ],
+ "mobile": 0,
+ "model": "",
+ "architecture": "arm"
+ }
+ },
+ "site": {
+ "domain": "example-site.com",
+ "publisher": {
+ "domain": "example-publisher.com",
+ "id": "1"
+ },
+ "page": "https://example-site.com/sample-article-slug/",
+ "content": {
+ "language": "en"
+ },
+ "cat": [
+ "IAB8"
+ ],
+ "pagecat": [
+ "IAB8"
+ ],
+ "privacypolicy": 1,
+ "mobile": 1
+ },
+ "user": {
+ "data": [],
+ "ext": {
+ "optable": {
+ "email": "5837d278eabede28e37b5766399ed0d1a4cdc36acee8d35710a255032f45beda"
+ },
+ "eids": [
{
- "source": "audigent.com",
- "uids":
- [
+ "source": "id5-sync.com",
+ "uids": [
{
- "id": "f84456cd3c72296d7898f62e1c46dd964206ff4d47e64b690c3c5a1d6b1bd286",
- "atype": 1
+ "id": "ID5*a7fF5d6f1GZgAUunYu5C3Edu5H4_nlp9aN34PiHotjzCtKWRh-vXq84ijeusCYHD",
+ "atype": 1,
+ "ext": {
+ "linkType": 2,
+ "pba": "EGg0hDGKtMSoVfdiLRmUPZc+x7Hm8I37s1sAPcxYmWA="
+ }
}
]
},
{
- "source": "adnxs.com",
- "uids":
- [
+ "source": "pubmatic.com",
+ "uids": [
{
- "id": "d4fd63f0f4f7ce0d128348cb145c7e0f"
+ "id": "A672B525-C053-45D6-B68E-B1DF0B4D6F3B",
+ "atype": 3,
+ "ext": {
+ "provider": "liveintent.com"
+ }
}
]
}
- ]
+ ],
+ "consent": "",
+ "ConsentedProvidersSettings": {
+ "consented_providers": "2~70.1516.89.320.2778.2322.510.1516.2343~dv."
+ }
+ }
+ },
+ "regs": {
+ "gpp": "DBABzw~1YNY~BVQqAAAAAgA",
+ "gpp_sid": [
+ 6,
+ 5,
+ 7
+ ],
+ "ext": {
+ "us_privacy": "1YNY"
}
},
"ext": {
"prebid": {
"targeting": {
- "includebidderkeys": true
+ "includewinners": true,
+ "includebidderkeys": false
},
- "analytics":
- {
+ "analytics": {
"options": {
"enableclientdetails": true
}
- }
+ },
+ "eidPermissions": [
+ {
+ "source": "bidswitch.net",
+ "bidders": [
+ "conversant",
+ "grid",
+ "improvedigital",
+ "pubmatic",
+ "undertone",
+ "unruly",
+ "yieldmo",
+ "adform",
+ "colossus",
+ "openx",
+ "triplelift"
+ ]
+ }
+ ],
+ "bidderconfig": [
+ {
+ "bidders": [
+ "openx"
+ ],
+ "config": {
+ "ortb2": {
+ "site": {
+ "ext": {
+ "data": {
+ "refresh": "0",
+ "vp": "0",
+ "hvp": "70",
+ "site_code": [
+ "AFOI_2020",
+ "APIO_2022",
+ "BIPOC_2024",
+ "FMLO_2021",
+ "FMOOI_2022",
+ "MINO_2021",
+ "MOMS_2020",
+ "MRLOI_22",
+ "NWPR_2021",
+ "RMOI_24",
+ "STKCONT_24"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "bidders": [
+ "pubmatic"
+ ],
+ "config": {
+ }
+ }
+ ]
}
- }
+ },
+ "tmax": 1000,
+ "id": "id",
+ "test": 1
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java
index 91afba88a31..ef36467d2b5 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java
@@ -1,15 +1,25 @@
package org.prebid.server.hooks.modules.optable.targeting.config;
import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
+import org.prebid.server.bidder.BidderCatalog;
import org.prebid.server.cache.PbcStorageService;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook;
import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook;
import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule;
import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.AliasesResolver;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache;
+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.IdsMapper;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting;
import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl;
import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient;
@@ -38,7 +48,7 @@ OptableTargetingProperties optableTargetingProperties() {
@Bean
IdsMapper queryParametersExtractor(@Value("${logging.sampling-rate:0.01}") double logSamplingRate) {
- return new IdsMapper(ObjectMapperProvider.mapper(), logSamplingRate);
+ return new IdsMapper(logSamplingRate);
}
@Bean
@@ -46,12 +56,12 @@ APIClientImpl apiClient(HttpClient httpClient,
@Value("${logging.sampling-rate:0.01}")
double logSamplingRate,
OptableTargetingProperties optableTargetingProperties,
- JacksonMapper jacksonMapperr) {
+ JacksonMapper jacksonMapper) {
return new APIClientImpl(
optableTargetingProperties.getApiEndpoint(),
httpClient,
- jacksonMapperr,
+ jacksonMapper,
logSamplingRate);
}
@@ -86,19 +96,43 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties
return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties);
}
+ @Bean
+ TargetingRequestExecutor targetingRequestExecutor(OptableTargeting optableTargeting,
+ UserFpdActivityMask userFpdActivityMask,
+ TimeoutFactory timeoutFactory,
+ @Value("${logging.sampling-rate:0.01}") double logSamplingRate) {
+
+ return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory, logSamplingRate);
+ }
+
@Bean
OptableTargetingModule optableTargetingModule(ConfigResolver configResolver,
- OptableTargeting optableTargeting,
- UserFpdActivityMask userFpdActivityMask,
+ TargetingRequestExecutor targetingRequestExecutor,
JsonMerger jsonMerger,
+ BidderCatalog bidderCatalog,
+ JacksonMapper mapper,
+ @Value("${hooks.host-execution-plan:}")
+ String executionPlan,
@Value("${logging.sampling-rate:0.01}") double logSamplingRate) {
+ final CompositeHookExecutionPlan hooksExecutionPlan = CompositeHookExecutionPlan.of(
+ StringUtils.isNoneEmpty(executionPlan)
+ ? mapper.decodeValue(executionPlan, ExecutionPlan.class)
+ : null);
+
return new OptableTargetingModule(List.of(
+ new OptableRawAuctionRequestHook(
+ configResolver,
+ targetingRequestExecutor,
+ BidderEnrichmentSampler.of(AliasesResolver.of(bidderCatalog)),
+ hooksExecutionPlan,
+ logSamplingRate),
new OptableTargetingProcessedAuctionRequestHook(
configResolver,
- optableTargeting,
- userFpdActivityMask,
+ targetingRequestExecutor,
+ hooksExecutionPlan,
logSamplingRate),
+ new OptableBidderRequestHook(),
new OptableTargetingAuctionResponseHook(
configResolver,
ObjectMapperProvider.mapper(),
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java
new file mode 100644
index 00000000000..1b1cdb51c63
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java
@@ -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;
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java
index ed0264f0249..ac7e30eec8d 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java
@@ -1,10 +1,14 @@
package org.prebid.server.hooks.modules.optable.targeting.model;
+import io.vertx.core.Future;
import lombok.Data;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
import java.util.List;
+import java.util.Set;
@Data
public class ModuleContext {
@@ -19,8 +23,31 @@ public class ModuleContext {
private long optableTargetingExecutionTime;
+ private boolean isEarlyNetworkCallEnabled = false;
+
+ private Future optableTargetingCall;
+
+ private long callTargetingAPITimestamp;
+
+ private Set biddersToEnrich;
+
+ private OptableTargetingProperties optableTargetingProperties;
+
+ private boolean shouldSkipEnrichment;
+
+ private String id5Signature;
+
public static ModuleContext of(AuctionInvocationContext invocationContext) {
final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext();
return moduleContext != null ? moduleContext : new ModuleContext();
}
+
+ public void failWithExecutionTime(long executionTime) {
+ setOptableTargetingExecutionTime(executionTime);
+ setEnrichRequestStatus(EnrichmentStatus.failure());
+ }
+
+ public boolean hasOptableTargetingProperties() {
+ return optableTargetingProperties != null;
+ }
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java
index 9dacd6de322..d53d1963795 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java
@@ -23,4 +23,8 @@ public class OptableAttributes {
String userAgent;
Long timeout;
+
+ App app;
+
+ String id5Signature;
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java
index 20862050f39..9b24ed2ed9d 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java
@@ -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;
}
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java
index 7f0598da83e..314e2639cef 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java
@@ -32,6 +32,9 @@ public final class OptableTargetingProperties {
@JsonProperty("id-prefix-order")
String idPrefixOrder;
+ @JsonProperty("hid-prefixes")
+ String hidPrefixes;
+
@JsonProperty("optable-inserter-eids-merge")
Set optableInserterEidsMerge = Set.of();
@@ -42,4 +45,16 @@ public final class OptableTargetingProperties {
Set optableInserterEidsIgnore = Set.of();
CacheProperties cache = new CacheProperties();
+
+ @JsonProperty("enrichment-percentage")
+ Integer enrichmentPercentage = 100;
+
+ @JsonProperty("bidder-enrichment-percentages")
+ Map bidderEnrichmentPercentages = Map.of();
+
+ @JsonProperty("enrich-web")
+ Boolean enrichWeb = true;
+
+ @JsonProperty("enrich-app")
+ Boolean enrichApp = true;
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java
index 787b3adbf1a..a0c8f974b69 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java
@@ -17,4 +17,6 @@ public class ExtUserOptable extends FlexibleExtension {
String zip;
String vid;
+
+ String id5Signature;
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java
index 826ce2698ed..5bc11a92bd3 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java
@@ -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;
@@ -10,4 +11,6 @@ public class TargetingResult {
List audience;
Ortb2 ortb2;
+
+ ObjectNode refs;
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java
new file mode 100644
index 00000000000..317cb621fe4
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java
@@ -0,0 +1,130 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import io.vertx.core.Future;
+import org.apache.commons.collections4.CollectionUtils;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2;
+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;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
+import org.prebid.server.hooks.v1.bidder.BidderRequestHook;
+import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeoutException;
+
+public class OptableBidderRequestHook implements BidderRequestHook {
+
+ public static final String CODE = "optable-targeting-bidder-request-hook";
+
+ @Override
+ public Future> call(BidderRequestPayload bidderRequestPayload,
+ BidderInvocationContext invocationContext) {
+
+ final ModuleContext moduleContext = ModuleContext.of(invocationContext);
+ final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties();
+
+ final Set biddersToEnrich = moduleContext.getBiddersToEnrich();
+ if (CollectionUtils.isEmpty(biddersToEnrich)
+ || !biddersToEnrich.contains(invocationContext.bidder())) {
+ return noAction(moduleContext, null);
+ }
+
+ final String bidder = invocationContext.bidder();
+
+ return moduleContext.getOptableTargetingCall()
+ .compose(targetingResult ->
+ enrichedPayload(targetingResult, moduleContext, properties, bidder))
+ .recover(throwable -> failedAction(moduleContext, throwable, bidder));
+ }
+
+ private Future> enrichedPayload(TargetingResult targetingResult,
+ ModuleContext moduleContext,
+ OptableTargetingProperties properties,
+ String bidder) {
+
+ final long executionTime = calcExecutionTime(moduleContext);
+ final boolean hasData = hasEnrichmentData(targetingResult);
+
+ if (hasData) {
+ moduleContext.setTargeting(targetingResult.getAudience());
+ moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult));
+ moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());
+ }
+
+ final String outcome = hasData ? "enriched" : "no-data";
+ final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags(
+ bidder, outcome, executionTime);
+
+ return hasData
+ ? update(BidderRequestEnricher.of(targetingResult, properties), moduleContext, analyticsTags)
+ : noAction(moduleContext, analyticsTags);
+ }
+
+ private Future> failedAction(ModuleContext moduleContext,
+ Throwable throwable,
+ String bidder) {
+
+ final long executionTime = calcExecutionTime(moduleContext);
+ final String outcome = throwable instanceof TimeoutException ? "timeout" : "error";
+ final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags(
+ bidder, outcome, executionTime);
+
+ return noAction(moduleContext, analyticsTags);
+ }
+
+ private static boolean hasEnrichmentData(TargetingResult targetingResult) {
+ return Optional.ofNullable(targetingResult)
+ .map(TargetingResult::getOrtb2)
+ .map(Ortb2::getUser)
+ .isPresent();
+ }
+
+ private static long calcExecutionTime(ModuleContext moduleContext) {
+ final long startTime = moduleContext.getCallTargetingAPITimestamp();
+ return startTime > 0 ? System.currentTimeMillis() - startTime : 0;
+ }
+
+ private Future> noAction(ModuleContext moduleContext,
+ Tags analyticsTags) {
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .analyticsTags(analyticsTags)
+ .moduleContext(moduleContext)
+ .build());
+ }
+
+ private static Future> update(
+ PayloadUpdate payloadUpdate,
+ ModuleContext moduleContext,
+ Tags analyticsTags) {
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.update)
+ .analyticsTags(analyticsTags)
+ .payloadUpdate(payloadUpdate)
+ .moduleContext(moduleContext)
+ .build());
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java
new file mode 100644
index 00000000000..e5b24f3c53c
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java
@@ -0,0 +1,120 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Future;
+import org.apache.commons.collections4.CollectionUtils;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler;
+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.PropertiesValidator;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook;
+import org.prebid.server.log.ConditionalLogger;
+import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.settings.model.Account;
+
+import java.util.Objects;
+import java.util.Set;
+
+public class OptableRawAuctionRequestHook implements RawAuctionRequestHook {
+
+ private static final ConditionalLogger conditionalLogger = new ConditionalLogger(
+ LoggerFactory.getLogger(OptableRawAuctionRequestHook.class));
+
+ public static final String CODE = "optable-targeting-raw-auction-request-hook";
+
+ private final ConfigResolver configResolver;
+ private final TargetingRequestExecutor targetingRequestExecutor;
+ private final BidderEnrichmentSampler bidderEnrichmentSampler;
+ private final CompositeHookExecutionPlan hooksExecutionPlan;
+ private final double logSamplingRate;
+
+ public OptableRawAuctionRequestHook(ConfigResolver configResolver,
+ TargetingRequestExecutor targetingRequestExecutor,
+ BidderEnrichmentSampler bidderEnrichmentSampler,
+ CompositeHookExecutionPlan hooksExecutionPlan,
+ double logSamplingRate) {
+
+ this.configResolver = Objects.requireNonNull(configResolver);
+ this.targetingRequestExecutor = Objects.requireNonNull(targetingRequestExecutor);
+ this.bidderEnrichmentSampler = Objects.requireNonNull(bidderEnrichmentSampler);
+ this.hooksExecutionPlan = hooksExecutionPlan;
+ this.logSamplingRate = logSamplingRate;
+ }
+
+ @Override
+ public Future> call(AuctionRequestPayload payload,
+ AuctionInvocationContext invocationContext) {
+
+ final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig());
+ final ModuleContext moduleContext = new ModuleContext();
+ moduleContext.setEarlyNetworkCallEnabled(true);
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis());
+ moduleContext.setOptableTargetingProperties(properties);
+
+ if (!PropertiesValidator.isValid(properties)) {
+ conditionalLogger.error(
+ "Account not properly configured: tenant and/or origin is missing.", logSamplingRate);
+
+ moduleContext.failWithExecutionTime(
+ System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp());
+
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest();
+ if (!PropertiesValidator.isTrafficSourceValid(bidRequest, properties)) {
+ moduleContext.setShouldSkipEnrichment(true);
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ final Set biddersToEnrich = bidderEnrichmentSampler.sample(bidRequest, properties);
+ if (CollectionUtils.isEmpty(biddersToEnrich)) {
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ moduleContext.setBiddersToEnrich(biddersToEnrich);
+ final Account account = invocationContext.auctionContext().getAccount();
+ final long crossHookFutureTimeout =
+ hooksExecutionPlan.getOptableTargetingBidderRequestTimeout(account);
+
+ final Future optableTargetingCall = targetingRequestExecutor.makeRequest(
+ payload,
+ invocationContext,
+ properties,
+ crossHookFutureTimeout);
+
+ moduleContext.setOptableTargetingCall(optableTargetingCall);
+
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ public static Future> update(
+ PayloadUpdate payloadUpdate,
+ ModuleContext moduleContext) {
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.update)
+ .payloadUpdate(payloadUpdate)
+ .moduleContext(moduleContext)
+ .build());
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java
index 5a20f79a347..5b915618b0c 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java
@@ -2,7 +2,6 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.vertx.core.Future;
-import org.apache.commons.collections4.CollectionUtils;
import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
@@ -13,6 +12,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;
@@ -52,27 +52,39 @@ public Future> call(AuctionResponsePayl
final ModuleContext moduleContext = ModuleContext.of(invocationContext);
moduleContext.setAdserverTargetingEnabled(adserverTargeting);
- if (!adserverTargeting) {
- return success(moduleContext);
+ if (moduleContext.isShouldSkipEnrichment() || !adserverTargeting) {
+ return id5SignatureOnlyPayload(moduleContext);
}
+ final List targeting = moduleContext.getTargeting();
+
final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility(
- auctionResponsePayload.bidResponse(), moduleContext.getTargeting());
+ auctionResponsePayload.bidResponse(), targeting);
moduleContext.setEnrichResponseStatus(validationStatus);
return validationStatus.getStatus() == Status.SUCCESS
- ? enrichedPayload(moduleContext)
- : success(moduleContext);
+ ? update(fullEnrichmentChain(targeting, moduleContext.getId5Signature()), moduleContext)
+ : id5SignatureOnlyPayload(moduleContext);
}
- private Future> enrichedPayload(ModuleContext moduleContext) {
- final List targeting = moduleContext.getTargeting();
+ private PayloadUpdate fullEnrichmentChain(final List targeting,
+ final String id5Signature) {
- return CollectionUtils.isNotEmpty(targeting)
- ? update(BidResponseEnricher.of(targeting, objectMapper, jsonMerger), moduleContext)
+ return BidResponseEnricher.of(targeting, objectMapper, jsonMerger)
+ .andThen(Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger))::apply;
+ }
+
+ private Future> id5SignatureOnlyPayload(ModuleContext moduleContext) {
+ final String id5Signature = moduleContext.getId5Signature();
+ return id5Signature != null
+ ? update(id5SignatureEnrichmentChain(id5Signature), moduleContext)
: success(moduleContext);
}
+ private PayloadUpdate id5SignatureEnrichmentChain(String id5Signature) {
+ return Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger);
+ }
+
private Future> update(
PayloadUpdate payloadUpdate,
ModuleContext moduleContext) {
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java
index a5ad2559d40..fc22fdc354c 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java
@@ -1,31 +1,19 @@
package org.prebid.server.hooks.modules.optable.targeting.v1;
-import com.iab.openrtb.request.BidRequest;
-import com.iab.openrtb.request.Device;
-import com.iab.openrtb.request.User;
import io.vertx.core.Future;
-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;
-import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload;
-import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl;
-import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload;
-import org.prebid.server.auction.model.AuctionContext;
-import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
-import org.prebid.server.execution.timeout.Timeout;
import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
-import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
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.BidRequestCleaner;
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.OptableAttributesResolver;
-import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting;
+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;
@@ -35,9 +23,15 @@
import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook;
import org.prebid.server.log.ConditionalLogger;
import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.settings.model.Account;
import java.util.Objects;
+/**
+ * @deprecated This hook is deprecated and will be removed in a future release.
+ * Use {@link OptableRawAuctionRequestHook} and {@link OptableBidderRequestHook} instead.
+ */
+@Deprecated
public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook {
private static final ConditionalLogger conditionalLogger = new ConditionalLogger(
@@ -45,19 +39,23 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc
public static final String CODE = "optable-targeting-processed-auction-request-hook";
+ private static final String AUCTION_NOT_PROPERLY_CONFIGURED =
+ "Account not properly configured: tenant and/or origin is missing.";
+
private final ConfigResolver configResolver;
- private final OptableTargeting optableTargeting;
- private final UserFpdActivityMask userFpdActivityMask;
+ private final TargetingRequestExecutor targetingRequestExecutor;
private final double logSamplingRate;
+ private final CompositeHookExecutionPlan hooksExecutionPlan;
+
public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver,
- OptableTargeting optableTargeting,
- UserFpdActivityMask userFpdActivityMask,
+ TargetingRequestExecutor targetingRequestExecutor,
+ CompositeHookExecutionPlan hooksExecutionPlan,
double logSamplingRate) {
this.configResolver = Objects.requireNonNull(configResolver);
- this.optableTargeting = Objects.requireNonNull(optableTargeting);
- this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask);
+ this.targetingRequestExecutor = Objects.requireNonNull(targetingRequestExecutor);
+ this.hooksExecutionPlan = hooksExecutionPlan;
this.logSamplingRate = logSamplingRate;
}
@@ -65,94 +63,97 @@ public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver
public Future> call(AuctionRequestPayload auctionRequestPayload,
AuctionInvocationContext invocationContext) {
- final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig());
- final ModuleContext moduleContext = new ModuleContext();
- final long callTargetingAPITimestamp = System.currentTimeMillis();
+ final ModuleContext moduleContext = ModuleContext.of(invocationContext);
+ if (moduleContext.isShouldSkipEnrichment()) {
+ moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext));
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
- if (!isTargetingPropertiesValid(properties)) {
- conditionalLogger.error(
- "Account not properly configured: tenant and/or origin is missing.", logSamplingRate);
+ final Account account = invocationContext.auctionContext().getAccount();
+ final boolean hasRawAuctionRequestHook = hooksExecutionPlan.hasRawAuctionRequestHook(account);
+ final boolean hasBidderRequestHook = hooksExecutionPlan.hasBidderRequestHook(account);
- moduleContext.setOptableTargetingExecutionTime(System.currentTimeMillis() - callTargetingAPITimestamp);
- moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure());
+ if (hasRawAuctionRequestHook && hasBidderRequestHook) {
return update(BidRequestCleaner.instance(), moduleContext);
}
- final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext);
+ final OptableTargetingProperties properties =
+ resolveOptableTargetingProperties(moduleContext, invocationContext);
- final Timeout timeout = getHookTimeout(invocationContext);
- final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes(
- invocationContext.auctionContext(),
- properties.getTimeout());
+ final Future optableTargetingCall = hasRawAuctionRequestHook
+ ? resolveEarlyNetworkCall(moduleContext)
+ : resolvePreEarlyNetworkCall(auctionRequestPayload, invocationContext, moduleContext, properties);
- return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout)
+ if (optableTargetingCall == null) {
+ moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext));
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ return optableTargetingCall
.compose(targetingResult -> {
- moduleContext.setOptableTargetingExecutionTime(
- System.currentTimeMillis() - callTargetingAPITimestamp);
- return enrichedPayload(targetingResult, moduleContext, properties);
+ moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext));
+ return enrichPayload(targetingResult, moduleContext, properties);
})
.recover(throwable -> {
- moduleContext.setOptableTargetingExecutionTime(
- System.currentTimeMillis() - callTargetingAPITimestamp);
- moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure());
+ moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext));
return update(BidRequestCleaner.instance(), moduleContext);
});
}
- private boolean isTargetingPropertiesValid(OptableTargetingProperties properties) {
- return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant());
- }
+ private Future> enrichPayload(
+ TargetingResult targetingResult,
+ ModuleContext moduleContext,
+ OptableTargetingProperties properties) {
+
+ moduleContext.setTargeting(targetingResult.getAudience());
+ moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult));
+ moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());
- private BidRequest applyActivityRestrictions(BidRequest bidRequest,
- AuctionInvocationContext auctionInvocationContext) {
+ final PayloadUpdate payloadUpdate =
+ BidRequestCleaner.instance().andThen(BidRequestEnricher.of(targetingResult, properties))::apply;
- final AuctionContext auctionContext = auctionInvocationContext.auctionContext();
- final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of(
- ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE),
- bidRequest);
- final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure();
+ return update(payloadUpdate, moduleContext);
+ }
- final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed(
- Activity.TRANSMIT_UFPD, activityInvocationPayload);
- final boolean disallowTransmitEids = !activityInfrastructure.isAllowed(
- Activity.TRANSMIT_EIDS, activityInvocationPayload);
- final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed(
- Activity.TRANSMIT_GEO, activityInvocationPayload);
+ private Future resolveEarlyNetworkCall(ModuleContext moduleContext) {
+ return moduleContext.getOptableTargetingCall();
+ }
- return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo);
+ private static long calcAPICallExecutionTime(ModuleContext moduleContext) {
+ return System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp();
}
- private BidRequest maskUserPersonalInfo(BidRequest bidRequest,
- boolean disallowTransmitUfpd,
- boolean disallowTransmitEids,
- boolean disallowTransmitGeo) {
+ private OptableTargetingProperties resolveOptableTargetingProperties(ModuleContext moduleContext,
+ AuctionInvocationContext invocationContext) {
- final User maskedUser = userFpdActivityMask.maskUser(
- bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids);
- final Device maskedDevice = userFpdActivityMask.maskDevice(
- bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo);
+ final OptableTargetingProperties properties = moduleContext.hasOptableTargetingProperties()
+ ? moduleContext.getOptableTargetingProperties()
+ : configResolver.resolve(invocationContext.accountConfig());
+ moduleContext.setOptableTargetingProperties(properties);
- return bidRequest.toBuilder()
- .user(maskedUser)
- .device(maskedDevice)
- .build();
+ return properties;
}
- private Timeout getHookTimeout(AuctionInvocationContext invocationContext) {
- return invocationContext.timeout();
- }
+ private Future resolvePreEarlyNetworkCall(
+ AuctionRequestPayload payload,
+ AuctionInvocationContext invocationContext,
+ ModuleContext moduleContext,
+ OptableTargetingProperties properties) {
- private Future> enrichedPayload(TargetingResult targetingResult,
- ModuleContext moduleContext,
- OptableTargetingProperties properties) {
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis());
+ if (!PropertiesValidator.isValid(properties)) {
+ conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate);
- moduleContext.setTargeting(targetingResult.getAudience());
- moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());
- return update(
- BidRequestCleaner.instance()
- .andThen(BidRequestEnricher.of(targetingResult, properties))
- ::apply,
- moduleContext);
+ moduleContext.failWithExecutionTime(
+ System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp());
+ return Future.failedFuture(AUCTION_NOT_PROPERLY_CONFIGURED);
+ }
+
+ return targetingRequestExecutor.makeRequest(
+ payload,
+ invocationContext,
+ properties,
+ null);
}
private static Future> update(
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java
new file mode 100644
index 00000000000..f86724de969
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java
@@ -0,0 +1,27 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import lombok.AllArgsConstructor;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.util.PbsUtil;
+
+import java.util.Map;
+import java.util.Optional;
+
+@AllArgsConstructor(staticName = "of")
+public class AliasesResolver {
+
+ private final BidderCatalog bidderCatalog;
+
+ public BidderAliases resolve(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest)
+ .map(PbsUtil::extRequestPrebid)
+ .map(extRequestPrebid -> {
+ final Map aliases = extRequestPrebid.getAliases();
+ final Map aliasesGvlIds = extRequestPrebid.getAliasgvlids();
+ return BidderAliases.of(aliases, aliasesGvlIds, bidderCatalog);
+ })
+ .orElseGet(() -> BidderAliases.of(Map.of(), Map.of(), bidderCatalog));
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java
index 80905ce088c..5504ddfea26 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
@@ -21,8 +22,10 @@
public class AnalyticTagsResolver {
private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request";
+ private static final String ACTIVITY_ENRICH_BIDDER_REQUEST = "optable-enrich-bidder-request";
private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response";
private static final String STATUS_EXECUTION_TIME = "execution-time";
+ private static final String STATUS_OUTCOME = "outcome";
private static final String STATUS_REASON = "reason";
private AnalyticTagsResolver() {
@@ -35,6 +38,26 @@ public static Tags toEnrichRequestAnalyticTags(ModuleContext moduleContext) {
toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime())))));
}
+ public static Tags toBidderEnrichRequestAnalyticTags(String bidder,
+ String outcome,
+ long executionTime) {
+
+ final ObjectNode values = ObjectMapperProvider.mapper().createObjectNode()
+ .put(STATUS_OUTCOME, outcome)
+ .put(STATUS_EXECUTION_TIME, String.valueOf(executionTime));
+
+ return TagsImpl.of(Collections.singletonList(ActivityImpl.of(
+ ACTIVITY_ENRICH_BIDDER_REQUEST,
+ "enriched".equals(outcome) ? Status.SUCCESS.getValue() : Status.FAIL.getValue(),
+ Collections.singletonList(ResultImpl.of(
+ null,
+ values,
+ AppliedToImpl.builder()
+ .bidders(Collections.singletonList(bidder))
+ .request(true)
+ .build())))));
+ }
+
public static Tags toEnrichResponseAnalyticTags(ModuleContext moduleContext) {
final List activities = new ArrayList<>();
if (moduleContext.isAdserverTargetingEnabled()) {
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java
index 30652383942..d467e6df750 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java
@@ -14,7 +14,7 @@
public class BidRequestCleaner implements PayloadUpdate {
private static final String OPTABLE_FIELD = "optable";
- private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid");
+ private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid", "id5_signature");
public static BidRequestCleaner instance() {
return new BidRequestCleaner();
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java
index 2a60389802c..81c82b55580 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java
@@ -1,39 +1,15 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;
-import com.iab.openrtb.request.BidRequest;
-import com.iab.openrtb.request.Data;
-import com.iab.openrtb.request.Eid;
-import com.iab.openrtb.request.Segment;
-import com.iab.openrtb.request.Uid;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
-import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
-import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User;
import org.prebid.server.hooks.v1.PayloadUpdate;
import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-public class BidRequestEnricher implements PayloadUpdate {
-
- private static final String OPTABLE_CO_INSERTER = "optable.co";
-
- private final TargetingResult targetingResult;
- private final OptableTargetingProperties targetingProperties;
+public class BidRequestEnricher extends RequestEnricher implements PayloadUpdate {
private BidRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) {
- this.targetingResult = targetingResult;
- this.targetingProperties = targetingProperties;
+ super(targetingResult, targetingProperties);
}
public static BidRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) {
@@ -44,160 +20,4 @@ public static BidRequestEnricher of(TargetingResult targetingResult, OptableTarg
public AuctionRequestPayload apply(AuctionRequestPayload payload) {
return AuctionRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest()));
}
-
- private BidRequest enrichBidRequest(BidRequest bidRequest) {
- if (bidRequest == null || targetingResult == null) {
- return bidRequest;
- }
-
- final User optableUser = Optional.of(targetingResult)
- .map(TargetingResult::getOrtb2)
- .map(Ortb2::getUser)
- .orElse(null);
-
- if (optableUser == null) {
- return bidRequest;
- }
-
- final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser())
- .orElseGet(() -> com.iab.openrtb.request.User.builder().build());
-
- return bidRequest.toBuilder()
- .user(mergeUserData(bidRequestUser, optableUser))
- .build();
- }
-
- private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) {
- return user.toBuilder()
- .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids())))
- .data(mergeData(user.getData(), optableUser.getData()))
- .build();
- }
-
- private List mergeEids(List destination, List source) {
- if (CollectionUtils.isEmpty(destination)) {
- return source;
- }
-
- if (CollectionUtils.isEmpty(source)) {
- return destination;
- }
-
- final Map idToSourceEid = source.stream().collect(Collectors.toMap(
- BidRequestEnricher::eidIdExtractor,
- Function.identity(),
- (a, b) -> b,
- HashMap::new));
-
- final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace();
- final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge()
- .stream()
- .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet());
-
- final List mergedEid = destination.stream()
- .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid))
- && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter())
- ? resolveEidConflict(
- destinationEid,
- idToSourceEid.get(eidIdExtractor(destinationEid)),
- sourceToMerge,
- sourceToReplace)
- : destinationEid)
- .toList();
-
- return merge(mergedEid, source, BidRequestEnricher::eidIdExtractor);
- }
-
- private List filterOptableEids(List eids) {
- if (CollectionUtils.isEmpty(eids)) {
- return eids;
- }
-
- final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore();
- if (CollectionUtils.isEmpty(optableIdsToIgnore)) {
- return eids;
- }
-
- return eids.stream()
- .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter())
- || !optableIdsToIgnore.contains(eid.getSource()))
- .toList();
- }
-
- private static Eid resolveEidConflict(Eid destinationEid,
- Eid sourceEid,
- Set sourceToMerge,
- Set sourceToReplace) {
-
- final String eidSource = sourceEid.getSource();
-
- if (sourceToReplace.contains(eidSource)) {
- return sourceEid;
- }
- if (sourceToMerge.contains(eidSource)) {
- return mergeEid(destinationEid, sourceEid);
- }
-
- return destinationEid;
- }
-
- private static Eid mergeEid(Eid destinationEid, Eid sourceEid) {
- return destinationEid.toBuilder()
- .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId))
- .build();
- }
-
- private static String eidIdExtractor(Eid eid) {
- return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource());
- }
-
- private static List mergeData(List destination, List source) {
- if (CollectionUtils.isEmpty(destination)) {
- return source;
- }
-
- if (CollectionUtils.isEmpty(source)) {
- return destination;
- }
-
- final Map idToSourceData = source.stream()
- .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new));
-
- final List mergedData = destination.stream()
- .map(destinationData -> idToSourceData.containsKey(destinationData.getId())
- ? mergeData(destinationData, idToSourceData.get(destinationData.getId()))
- : destinationData)
- .toList();
-
- return merge(mergedData, source, Data::getId);
- }
-
- private static Data mergeData(Data destinationData, Data sourceData) {
- return destinationData.toBuilder()
- .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId))
- .build();
- }
-
- private static List merge(List destination,
- List source,
- Function idExtractor) {
-
- if (CollectionUtils.isEmpty(source)) {
- return destination;
- }
-
- if (CollectionUtils.isEmpty(destination)) {
- return source;
- }
-
- final Set existingIds = destination.stream()
- .map(idExtractor)
- .collect(Collectors.toSet());
-
- return Stream.concat(
- destination.stream(),
- source.stream()
- .filter(entry -> !existingIds.contains(idExtractor.apply(entry))))
- .toList();
- }
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java
new file mode 100644
index 00000000000..9e714e74a54
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java
@@ -0,0 +1,65 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import lombok.AllArgsConstructor;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.IntSupplier;
+import java.util.stream.Collectors;
+
+@AllArgsConstructor(staticName = "of")
+public class BidderEnrichmentSampler {
+
+ private final AliasesResolver aliasesResolver;
+ private final IntSupplier randomSupplier;
+
+ public static BidderEnrichmentSampler of(AliasesResolver aliasesResolver) {
+ return of(aliasesResolver, () -> ThreadLocalRandom.current().nextInt(100));
+ }
+
+ public Set sample(BidRequest bidRequest, OptableTargetingProperties optableTargetingProperties) {
+ final Integer defaultEnrichmentPercentage = optableTargetingProperties.getEnrichmentPercentage();
+ final Map bidderEnrichmentPercentage =
+ optableTargetingProperties.getBidderEnrichmentPercentages();
+
+ final BidderAliases aliases = aliasesResolver.resolve(bidRequest);
+ return extractUniqueBidders(bidRequest)
+ .stream()
+ .filter(bidder -> {
+ final int percentage =
+ resolvePercentage(aliases, bidder, defaultEnrichmentPercentage, bidderEnrichmentPercentage);
+ return percentage > 0 && randomSupplier.getAsInt() < percentage;
+ })
+ .collect(Collectors.toSet());
+ }
+
+ private static int resolvePercentage(BidderAliases aliases, String bidder,
+ Integer defaultEnrichmentPercentage,
+ Map bidderEnrichmentPercentage) {
+
+ return Optional.ofNullable(bidderEnrichmentPercentage.get(bidder))
+ .or(() -> Optional.ofNullable(bidderEnrichmentPercentage.get(aliases.resolveBidder(bidder))))
+ .orElse(defaultEnrichmentPercentage);
+ }
+
+ private static Set extractUniqueBidders(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getImp())
+ .stream()
+ .flatMap(Collection::stream)
+ .map(Imp::getExt)
+ .filter(Objects::nonNull)
+ .map(ext -> ext.at("/prebid/bidder"))
+ .filter(Objects::nonNull)
+ .flatMap(bidder -> StreamUtil.asStream(bidder.fieldNames()))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java
new file mode 100644
index 00000000000..24b59cd42a1
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java
@@ -0,0 +1,23 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
+
+public class BidderRequestEnricher extends RequestEnricher implements PayloadUpdate {
+
+ private BidderRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) {
+ super(targetingResult, targetingProperties);
+ }
+
+ public static BidderRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) {
+ return new BidderRequestEnricher(targetingResult, properties);
+ }
+
+ @Override
+ public BidderRequestPayload apply(BidderRequestPayload payload) {
+ return BidderRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest()));
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java
new file mode 100644
index 00000000000..0d5bda38be9
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java
@@ -0,0 +1,120 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
+import org.prebid.server.hooks.execution.model.ExecutionGroup;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
+import org.prebid.server.hooks.execution.model.HookHttpEndpoint;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionPlan;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook;
+import org.prebid.server.settings.model.Account;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+public class CompositeHookExecutionPlan {
+
+ private static final HookHttpEndpoint ENDPOINT_AUCTION = HookHttpEndpoint.POST_AUCTION;
+ private static final String STAGE_RAW_AUCTION_REQUEST = "raw_auction_request";
+ private static final String STAGE_BIDDER_REQUEST = "bidder_request";
+ private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = OptableRawAuctionRequestHook.CODE;
+ private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = OptableBidderRequestHook.CODE;
+
+ private final boolean hasGlobalRawAuctionRequestHook;
+
+ private final boolean hasGlobalBidderRequestHook;
+
+ private final long globalBidderRequestHookTimeout;
+
+ private final ConcurrentHashMap rawAuctionRequestHookCache = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap bidderRequestHookCache = new ConcurrentHashMap<>();
+
+ private final ConcurrentHashMap bidderRequestHookTimeoutCache = new ConcurrentHashMap<>();
+
+ private CompositeHookExecutionPlan(boolean hasGlobalRawAuctionRequestHook,
+ boolean hasGlobalBidderRequestHook,
+ long globalBidderRequestHookTimeout) {
+
+ this.hasGlobalRawAuctionRequestHook = hasGlobalRawAuctionRequestHook;
+ this.hasGlobalBidderRequestHook = hasGlobalBidderRequestHook;
+ this.globalBidderRequestHookTimeout = globalBidderRequestHookTimeout;
+ }
+
+ public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) {
+ return globalExecutionPlan == null
+ ? new CompositeHookExecutionPlan(false, false, 0)
+ : new CompositeHookExecutionPlan(
+ hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION),
+ hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST),
+ getHookTimeout(globalExecutionPlan,
+ STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST));
+ }
+
+ private T computeFromAccount(Account account,
+ ConcurrentHashMap cache,
+ T defaultValue,
+ Function compute) {
+ final String accountId = account != null ? account.getId() : null;
+ return StringUtils.isNotEmpty(accountId)
+ ? cache.computeIfAbsent(accountId, id -> compute.apply(resolveExecutionPlan(account)))
+ : defaultValue;
+ }
+
+ public boolean hasRawAuctionRequestHook(Account account) {
+ return computeFromAccount(account, rawAuctionRequestHookCache, false,
+ plan -> hasHook(plan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION)
+ || hasGlobalRawAuctionRequestHook);
+ }
+
+ public boolean hasBidderRequestHook(Account account) {
+ return computeFromAccount(account, bidderRequestHookCache, false,
+ plan -> hasHook(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)
+ || hasGlobalBidderRequestHook);
+ }
+
+ public long getOptableTargetingBidderRequestTimeout(Account account) {
+ return computeFromAccount(account, bidderRequestHookTimeoutCache, globalBidderRequestHookTimeout,
+ plan -> {
+ final long timeout = getHookTimeout(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST);
+ return timeout != 0 ? timeout : globalBidderRequestHookTimeout;
+ });
+ }
+
+ private ExecutionPlan resolveExecutionPlan(Account account) {
+ return Optional.ofNullable(account)
+ .map(org.prebid.server.settings.model.Account::getHooks)
+ .map(org.prebid.server.settings.model.AccountHooksConfiguration::getExecutionPlan)
+ .orElse(null);
+ }
+
+ private static boolean hasHook(ExecutionPlan executionPlan, String stage, String hookCode) {
+ return Optional.ofNullable(executionPlan)
+ .map(ExecutionPlan::getEndpoints)
+ .map(endpoints -> endpoints.get(ENDPOINT_AUCTION))
+ .map(EndpointExecutionPlan::getStages)
+ .map(stages -> stages.get(Stage.valueOf(stage)))
+ .map(StageExecutionPlan::getGroups)
+ .orElseGet(List::of)
+ .stream()
+ .map(ExecutionGroup::getHookSequence)
+ .flatMap(java.util.Collection::stream)
+ .anyMatch(hook -> hookCode.equals(hook.getHookImplCode()));
+ }
+
+ private static long getHookTimeout(ExecutionPlan executionPlan, String stage, String hookCode) {
+ return Optional.ofNullable(executionPlan)
+ .map(ExecutionPlan::getEndpoints)
+ .map(endpoints -> endpoints.get(ENDPOINT_AUCTION))
+ .map(EndpointExecutionPlan::getStages)
+ .map(stages -> stages.get(Stage.valueOf(stage)))
+ .map(StageExecutionPlan::getGroups)
+ .orElseGet(List::of)
+ .stream().findFirst()
+ .map(ExecutionGroup::getTimeout)
+ .orElse(0L);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java
new file mode 100644
index 00000000000..1d7e081148e
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java
@@ -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;
+ }
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java
new file mode 100644
index 00000000000..4349ad21240
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java
@@ -0,0 +1,74 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.Eid;
+import com.iab.openrtb.request.Uid;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public class Id5Resolver {
+
+ public static final String OPTABLE_INSERTER = "optable.co";
+ public static final String ID5_SOURCE = "id5-sync.com";
+ public static final String OPTABLE = "optable";
+ public static final String REF = "ref";
+ public static final String SIGNATURE = "signature";
+
+ private Id5Resolver() {
+ }
+
+ public static String resolveId5Signature(TargetingResult targetingResult) {
+ if (targetingResult == null) {
+ return null;
+ }
+
+ final List refs = Optional.of(targetingResult)
+ .map(TargetingResult::getOrtb2)
+ .map(Ortb2::getUser)
+ .map(User::getEids)
+ .orElseGet(List::of)
+ .stream()
+ .filter(it -> OPTABLE_INSERTER.equals(it.getInserter()) && ID5_SOURCE.equals(it.getSource()))
+ .map(Eid::getUids)
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .map(Uid::getExt)
+ .filter(Objects::nonNull)
+ .map(it -> it.get(OPTABLE))
+ .filter(Objects::nonNull)
+ .map(it -> it.get(REF))
+ .filter(Objects::nonNull)
+ .map(JsonNode::asText)
+ .toList();
+
+ if (CollectionUtils.isEmpty(refs)) {
+ return null;
+ }
+
+ final ObjectNode references = targetingResult.getRefs();
+ if (references == null) {
+ return null;
+ }
+
+ return refs.stream()
+ .map(references::get)
+ .filter(Objects::nonNull)
+ .map(it -> it.get(SIGNATURE))
+ .filter(Objects::nonNull)
+ .filter(JsonNode::isValueNode)
+ .filter(it -> !it.isNull())
+ .map(JsonNode::asText)
+ .filter(StringUtils::isNotBlank)
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java
new file mode 100644
index 00000000000..e94af71928d
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java
@@ -0,0 +1,83 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.response.BidResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.auction.AuctionResponsePayload;
+import org.prebid.server.json.JsonMerger;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid;
+
+import java.util.Objects;
+
+public class Id5SignatureBidResponseEnricher implements PayloadUpdate {
+
+ private final String id5Signature;
+ private final ObjectMapper mapper;
+ private final JsonMerger jsonMerger;
+
+ private Id5SignatureBidResponseEnricher(String id5Signature, ObjectMapper mapper, JsonMerger jsonMerger) {
+ this.id5Signature = id5Signature;
+ this.mapper = Objects.requireNonNull(mapper);
+ this.jsonMerger = Objects.requireNonNull(jsonMerger);
+ }
+
+ public static Id5SignatureBidResponseEnricher of(String id5Signature, ObjectMapper mapper, JsonMerger jsonMerger) {
+ return new Id5SignatureBidResponseEnricher(id5Signature, mapper, jsonMerger);
+ }
+
+ @Override
+ public AuctionResponsePayload apply(AuctionResponsePayload payload) {
+ return AuctionResponsePayloadImpl.of(enrichBidResponse(payload.bidResponse(), id5Signature));
+ }
+
+ private BidResponse enrichBidResponse(BidResponse bidResponse, String id5Signature) {
+ if (StringUtils.isEmpty(id5Signature)) {
+ return bidResponse;
+ }
+ final ObjectNode passthroughNode = id5SignatureToObjectNode(id5Signature);
+
+ final ExtBidResponse existingExt = bidResponse.getExt();
+ final ExtBidResponsePrebid existingPrebid = existingExt != null ? existingExt.getPrebid() : null;
+ final JsonNode existingPassthrough = existingPrebid != null ? existingPrebid.getPassthrough() : null;
+
+ final JsonNode mergedPassthrough = existingPassthrough != null
+ ? jsonMerger.merge(passthroughNode, existingPassthrough)
+ : passthroughNode;
+
+ final ExtBidResponsePrebid modifiedPrebid = existingPrebid != null
+ ? existingPrebid.toBuilder()
+ .passthrough(mergedPassthrough)
+ .build()
+ : ExtBidResponsePrebid.builder()
+ .passthrough(mergedPassthrough)
+ .build();
+
+ final ExtBidResponse modifiedExt = existingExt != null
+ ? existingExt.toBuilder()
+ .prebid(modifiedPrebid)
+ .build()
+ : ExtBidResponse.builder()
+ .prebid(modifiedPrebid)
+ .build();
+
+ return bidResponse.toBuilder()
+ .ext(modifiedExt)
+ .build();
+ }
+
+ private ObjectNode id5SignatureToObjectNode(String id5Signature) {
+ final ObjectNode node = mapper.createObjectNode();
+ node.set("id5_signature", TextNode.valueOf(id5Signature));
+
+ final ObjectNode optableNode = mapper.createObjectNode();
+ optableNode.set("optable", node);
+
+ return optableNode;
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java
index 7214f6ce6a3..2ea248334b3 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java
@@ -1,8 +1,5 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Device;
import com.iab.openrtb.request.Eid;
@@ -14,8 +11,6 @@
import org.prebid.server.hooks.modules.optable.targeting.model.Id;
import org.prebid.server.hooks.modules.optable.targeting.model.OS;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable;
-import org.prebid.server.log.ConditionalLogger;
-import org.prebid.server.log.LoggerFactory;
import java.util.Collections;
import java.util.HashMap;
@@ -26,19 +21,14 @@
public class IdsMapper {
- private static final ConditionalLogger conditionalLogger =
- new ConditionalLogger(LoggerFactory.getLogger(IdsMapper.class));
-
private static final Map STATIC_PPID_MAPPING = Map.of(
"id5-sync.com", Id.ID5,
"utiq.com", Id.UTIQ,
"netid.de", Id.NET_ID);
- private final ObjectMapper objectMapper;
private final double logSamplingRate;
- public IdsMapper(ObjectMapper objectMapper, double logSamplingRate) {
- this.objectMapper = Objects.requireNonNull(objectMapper);
+ public IdsMapper(double logSamplingRate) {
this.logSamplingRate = logSamplingRate;
}
@@ -60,7 +50,7 @@ private void addOptableIds(Map ids, User user) {
final Optional extUserOptable = Optional.ofNullable(user)
.map(User::getExt)
.map(ext -> ext.getProperty("optable"))
- .map(this::parseExtUserOptable);
+ .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate));
extUserOptable.map(ExtUserOptable::getEmail).ifPresent(it -> ids.put(Id.EMAIL, it));
extUserOptable.map(ExtUserOptable::getPhone).ifPresent(it -> ids.put(Id.PHONE, it));
@@ -68,19 +58,15 @@ private void addOptableIds(Map ids, User user) {
extUserOptable.map(ExtUserOptable::getVid).ifPresent(it -> ids.put(Id.OPTABLE_VID, it));
}
- private ExtUserOptable parseExtUserOptable(JsonNode node) {
- try {
- return objectMapper.treeToValue(node, ExtUserOptable.class);
- } catch (JsonProcessingException e) {
- conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate);
- return null;
- }
- }
-
private static void addDeviceIds(Map ids, Device device) {
final String ifa = device != null ? device.getIfa() : null;
final String os = device != null ? StringUtils.toRootLowerCase(device.getOs()) : null;
final int lmt = Optional.ofNullable(device).map(Device::getLmt).orElse(0);
+ final String ip6 = device != null ? device.getIpv6() : null;
+
+ if (ip6 != null) {
+ ids.put(Id.DEVICE_IP_V_6, ip6);
+ }
if (ifa == null || StringUtils.isEmpty(os) || lmt == 1) {
return;
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java
index 7f2aad0657d..0828adea34d 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java
@@ -1,11 +1,18 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Regs;
+import com.iab.openrtb.request.User;
import org.apache.commons.collections4.SetUtils;
+import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.gpp.model.GppContext;
import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.App;
import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
-import org.prebid.server.privacy.gdpr.model.TcfContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable;
+import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
+import org.prebid.server.proto.openrtb.ext.request.ExtUser;
import java.util.ArrayList;
import java.util.List;
@@ -16,30 +23,62 @@ public class OptableAttributesResolver {
private OptableAttributesResolver() {
}
- public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) {
- final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext();
+ public static OptableAttributes resolveAttributes(AuctionContext auctionContext,
+ Long timeout,
+ double logSamplingRate) {
+
final GppContext.Scope gppScope = auctionContext.getGppContext().scope();
+ final BidRequest bidRequest = auctionContext.getBidRequest();
+ final Optional regs = Optional.ofNullable(bidRequest.getRegs());
+ final Integer gdpr = regs
+ .map(Regs::getGdpr)
+ .orElseGet(() -> regs.map(Regs::getExt)
+ .map(ExtRegs::getGdpr)
+ .orElse(null));
+
final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder()
.ips(resolveIp(auctionContext))
.userAgent(resolveUserAgent(auctionContext))
+ .app(resolveApp(auctionContext))
.timeout(timeout);
- if (tcfContext.isConsentValid()) {
- builder
- .gdprApplies(tcfContext.isInGdprScope())
- .gdprConsent(tcfContext.getConsentString());
+ if (gdpr != null && gdpr > 0) {
+ final Optional user = Optional.ofNullable(bidRequest.getUser());
+ final String consent = user.map(User::getConsent)
+ .orElseGet(() -> user.map(User::getExt)
+ .map(ExtUser::getConsent)
+ .orElse(null));
+
+ if (StringUtils.isNotEmpty(consent)) {
+ builder
+ .gdprApplies(true)
+ .gdprConsent(consent);
+ }
}
- if (gppScope.getGppModel() != null) {
+ if (gppScope != null && gppScope.getGppModel() != null) {
builder
.gpp(gppScope.getGppModel().encode())
.gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds()));
}
+ Optional.ofNullable(bidRequest.getUser())
+ .map(User::getExt)
+ .map(ext -> ext.getProperty("optable"))
+ .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate))
+ .map(ExtUserOptable::getId5Signature)
+ .filter(StringUtils::isNotBlank)
+ .ifPresent(builder::id5Signature);
+
return builder.build();
}
+ private static App resolveApp(AuctionContext auctionContext) {
+ final com.iab.openrtb.request.App app = auctionContext.getBidRequest().getApp();
+ return app != null ? App.of(app.getBundle(), app.getVer()) : null;
+ }
+
public static String resolveUserAgent(AuctionContext auctionContext) {
final Device device = auctionContext.getBidRequest().getDevice();
return device != null ? device.getUa() : null;
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java
index 0cb16d9c456..b79ca2790f2 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java
@@ -29,7 +29,7 @@ public Future getTargeting(OptableTargetingProperties propertie
Timeout timeout) {
final List ids = idsMapper.toIds(bidRequest, properties.getPpidMapping());
- final Query query = QueryBuilder.build(ids, attributes, properties.getIdPrefixOrder());
+ final Query query = QueryBuilder.build(ids, attributes, properties);
if (query == null) {
return Future.failedFuture("Can't get targeting");
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java
new file mode 100644
index 00000000000..391590057ff
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java
@@ -0,0 +1,20 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+
+public class PropertiesValidator {
+
+ private PropertiesValidator() {
+ }
+
+ public static boolean isValid(OptableTargetingProperties properties) {
+ return StringUtils.isNotEmpty(properties.getOrigin()) && StringUtils.isNotEmpty(properties.getTenant());
+ }
+
+ public static boolean isTrafficSourceValid(BidRequest bidRequest, OptableTargetingProperties properties) {
+ return (Boolean.TRUE.equals(properties.getEnrichWeb()) && bidRequest.getSite() != null)
+ || (Boolean.TRUE.equals(properties.getEnrichApp()) && bidRequest.getApp() != null);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java
index 613286f4b92..78d362d9fec 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java
@@ -5,14 +5,17 @@
import org.prebid.server.hooks.modules.optable.targeting.model.Id;
import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
import org.prebid.server.hooks.modules.optable.targeting.model.Query;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -26,12 +29,42 @@ public class QueryBuilder {
private QueryBuilder() {
}
- public static Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) {
+ public static Query build(List ids, OptableAttributes optableAttributes,
+ OptableTargetingProperties properties) {
+
if (CollectionUtils.isEmpty(ids) && CollectionUtils.isEmpty(optableAttributes.getIps())) {
return null;
}
- return Query.of(buildIdsString(ids, idPrefixOrder), buildAttributesString(optableAttributes));
+ final String idPrefixOrder = properties.getIdPrefixOrder();
+ final String hidPrefixes = properties.getHidPrefixes();
+ return Query.of(
+ buildIdsString(ids, idPrefixOrder),
+ buildHidString(ids, hidPrefixes),
+ buildAttributesString(optableAttributes),
+ buildHidAttributesString(optableAttributes));
+ }
+
+ private static String buildHidString(List ids, String hidPrefixesString) {
+ if (CollectionUtils.isEmpty(ids) || StringUtils.isEmpty(hidPrefixesString)) {
+ return StringUtils.EMPTY;
+ }
+ final Map prefixToIdValue = ids.stream()
+ .collect(Collectors.toMap(Id::getName, it -> it, (a, b) -> b));
+
+ final String hidParameters = Arrays.stream(hidPrefixesString.split(","))
+ .map(String::trim)
+ .filter(StringUtils::isNotEmpty)
+ .map(prefixToIdValue::get)
+ .filter(Objects::nonNull)
+ .map(it -> String.format(
+ "hid=%s:%s", it.getName(), URLEncoder.encode(it.getValue(), StandardCharsets.UTF_8)))
+ .collect(Collectors.joining("&"));
+ if (StringUtils.isEmpty(hidParameters)) {
+ return StringUtils.EMPTY;
+ }
+
+ return "&" + hidParameters;
}
private static String buildIdsString(List ids, String idPrefixOrder) {
@@ -39,14 +72,15 @@ private static String buildIdsString(List ids, String idPrefixOrder) {
return StringUtils.EMPTY;
}
- final List reorderedIds = reorderIds(ids, idPrefixOrder);
+ final List reorderedIds = reorderIds(ids, idPrefixOrder)
+ .stream()
+ .filter(id -> !Id.DEVICE_IP_V_6.equals(id.getName()))
+ .toList();
final StringBuilder sb = new StringBuilder();
for (Id id : reorderedIds) {
sb.append("&id=");
- sb.append(URLEncoder.encode(
- "%s:%s".formatted(id.getName(), id.getValue()),
- StandardCharsets.UTF_8));
+ sb.append(URLEncoder.encode(String.format("%s:%s", id.getName(), id.getValue()), StandardCharsets.UTF_8));
}
return sb.toString();
@@ -78,7 +112,8 @@ private static String buildAttributesString(OptableAttributes optableAttributes)
.ifPresent(gpp -> sb.append("&gpp=").append(gpp));
Optional.ofNullable(optableAttributes.getGppSid())
.filter(Predicate.not(Collection::isEmpty))
- .ifPresent(gppSids -> sb.append("&gpp_sid=").append(gppSids.stream().findFirst()));
+ .ifPresent(gppSids -> sb.append("&gpp_sid=").append(
+ gppSids.stream().limit(2).map(String::valueOf).collect(Collectors.joining(","))));
Optional.ofNullable(optableAttributes.getTimeout())
.ifPresent(timeout -> sb.append("&timeout=").append(timeout).append("ms"));
@@ -87,4 +122,31 @@ private static String buildAttributesString(OptableAttributes optableAttributes)
return sb.toString();
}
+
+ /**
+ * Kept apart from buildAttributesString because these are the only attributes that
+ * discriminate one user from another, so they have to take part in the cache key.
+ */
+ private static String buildHidAttributesString(OptableAttributes optableAttributes) {
+ final StringBuilder sb = new StringBuilder();
+
+ Optional.ofNullable(optableAttributes.getApp())
+ .ifPresent(app -> {
+ final String bundle = app.getBundle();
+ if (StringUtils.isNotEmpty(bundle)) {
+ sb.append("&bundle=").append(URLEncoder.encode(bundle, StandardCharsets.UTF_8));
+
+ final String ver = app.getVer();
+ if (StringUtils.isNotEmpty(ver)) {
+ sb.append("&ver=").append(URLEncoder.encode(ver, StandardCharsets.UTF_8));
+ }
+ }
+ });
+
+ Optional.ofNullable(optableAttributes.getId5Signature())
+ .ifPresent(id5Signature -> sb.append("&id5_signature=")
+ .append(URLEncoder.encode(id5Signature, StandardCharsets.UTF_8)));
+
+ return sb.toString();
+ }
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java
new file mode 100644
index 00000000000..f2b9a169a49
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java
@@ -0,0 +1,194 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Data;
+import com.iab.openrtb.request.Eid;
+import com.iab.openrtb.request.Segment;
+import com.iab.openrtb.request.Uid;
+import com.iab.openrtb.request.User;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public abstract class RequestEnricher {
+
+ private static final String OPTABLE_CO_INSERTER = "optable.co";
+
+ private final TargetingResult targetingResult;
+ private final OptableTargetingProperties targetingProperties;
+
+ protected RequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) {
+ this.targetingResult = targetingResult;
+ this.targetingProperties = targetingProperties;
+ }
+
+ protected BidRequest enrichBidRequest(BidRequest bidRequest) {
+ if (bidRequest == null || targetingResult == null) {
+ return bidRequest;
+ }
+
+ final var optableUser = Optional.of(targetingResult)
+ .map(TargetingResult::getOrtb2)
+ .map(Ortb2::getUser)
+ .orElse(null);
+
+ if (optableUser == null) {
+ return bidRequest;
+ }
+
+ final User bidRequestUser = Optional.ofNullable(bidRequest.getUser())
+ .orElseGet(() -> User.builder().build());
+
+ return bidRequest.toBuilder()
+ .user(mergeUserData(bidRequestUser, optableUser))
+ .build();
+ }
+
+ private User mergeUserData(
+ User user,
+ org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User optableUser) {
+
+ return user.toBuilder()
+ .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids())))
+ .data(mergeData(user.getData(), optableUser.getData()))
+ .build();
+ }
+
+ private List mergeEids(List destination, List source) {
+ if (CollectionUtils.isEmpty(destination)) {
+ return source;
+ }
+
+ if (CollectionUtils.isEmpty(source)) {
+ return destination;
+ }
+
+ final Map idToSourceEid = source.stream().collect(Collectors.toMap(
+ RequestEnricher::eidIdExtractor,
+ Function.identity(),
+ (a, b) -> b,
+ HashMap::new));
+
+ final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace();
+ final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge()
+ .stream()
+ .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet());
+
+ final List mergedEid = destination.stream()
+ .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid))
+ && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter())
+ ? resolveEidConflict(
+ destinationEid,
+ idToSourceEid.get(eidIdExtractor(destinationEid)),
+ sourceToMerge,
+ sourceToReplace)
+ : destinationEid)
+ .toList();
+
+ return merge(mergedEid, source, RequestEnricher::eidIdExtractor);
+ }
+
+ private List filterOptableEids(List eids) {
+ if (CollectionUtils.isEmpty(eids)) {
+ return eids;
+ }
+
+ final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore();
+ if (CollectionUtils.isEmpty(optableIdsToIgnore)) {
+ return eids;
+ }
+
+ return eids.stream()
+ .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter())
+ || !optableIdsToIgnore.contains(eid.getSource()))
+ .toList();
+ }
+
+ private static Eid resolveEidConflict(Eid destinationEid,
+ Eid sourceEid,
+ Set sourceToMerge,
+ Set sourceToReplace) {
+
+ final String eidSource = sourceEid.getSource();
+
+ if (sourceToReplace.contains(eidSource)) {
+ return sourceEid;
+ }
+ if (sourceToMerge.contains(eidSource)) {
+ return mergeEid(destinationEid, sourceEid);
+ }
+
+ return destinationEid;
+ }
+
+ private static Eid mergeEid(Eid destinationEid, Eid sourceEid) {
+ return destinationEid.toBuilder()
+ .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId))
+ .build();
+ }
+
+ private static String eidIdExtractor(Eid eid) {
+ return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource());
+ }
+
+ private static List mergeData(List destination, List source) {
+ if (CollectionUtils.isEmpty(destination)) {
+ return source;
+ }
+
+ if (CollectionUtils.isEmpty(source)) {
+ return destination;
+ }
+
+ final Map idToSourceData = source.stream()
+ .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new));
+
+ final List mergedData = destination.stream()
+ .map(destinationData -> idToSourceData.containsKey(destinationData.getId())
+ ? mergeData(destinationData, idToSourceData.get(destinationData.getId()))
+ : destinationData)
+ .toList();
+
+ return merge(mergedData, source, Data::getId);
+ }
+
+ private static Data mergeData(Data destinationData, Data sourceData) {
+ return destinationData.toBuilder()
+ .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId))
+ .build();
+ }
+
+ private static List merge(List destination,
+ List source,
+ Function idExtractor) {
+
+ if (CollectionUtils.isEmpty(source)) {
+ return destination;
+ }
+
+ if (CollectionUtils.isEmpty(destination)) {
+ return source;
+ }
+
+ final Set existingIds = destination.stream()
+ .map(idExtractor)
+ .collect(Collectors.toSet());
+
+ return Stream.concat(
+ destination.stream(),
+ source.stream()
+ .filter(entry -> !existingIds.contains(idExtractor.apply(entry))))
+ .toList();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java
new file mode 100644
index 00000000000..8a93df9fbdf
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java
@@ -0,0 +1,101 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.User;
+import io.vertx.core.Future;
+import org.apache.commons.lang3.ObjectUtils;
+import org.prebid.server.activity.Activity;
+import org.prebid.server.activity.ComponentType;
+import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
+import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload;
+import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl;
+import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+
+import java.util.Objects;
+
+public class TargetingRequestExecutor {
+
+ private final OptableTargeting optableTargeting;
+ private final UserFpdActivityMask userFpdActivityMask;
+ private final TimeoutFactory timeoutFactory;
+ private final double logSamplingRate;
+
+ public TargetingRequestExecutor(OptableTargeting optableTargeting,
+ UserFpdActivityMask userFpdActivityMask,
+ TimeoutFactory timeoutFactory,
+ double logSamplingRate) {
+
+ this.optableTargeting = Objects.requireNonNull(optableTargeting);
+ this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask);
+ this.timeoutFactory = ObjectUtils.requireNonEmpty(timeoutFactory);
+ this.logSamplingRate = logSamplingRate;
+ }
+
+ public Future makeRequest(AuctionRequestPayload payload,
+ AuctionInvocationContext invocationContext,
+ OptableTargetingProperties properties,
+ Long apiTimeout) {
+
+ final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext);
+
+ final Timeout timeout = apiTimeout == null
+ ? getHookTimeout(invocationContext)
+ : timeoutFactory.create(getHookTimeout(invocationContext).remaining() + apiTimeout);
+ final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes(
+ invocationContext.auctionContext(),
+ properties.getTimeout(),
+ logSamplingRate);
+
+ return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout);
+ }
+
+ private static Timeout getHookTimeout(AuctionInvocationContext invocationContext) {
+ return invocationContext.timeout();
+ }
+
+ private BidRequest applyActivityRestrictions(BidRequest bidRequest,
+ AuctionInvocationContext auctionInvocationContext) {
+
+ final AuctionContext auctionContext = auctionInvocationContext.auctionContext();
+ final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of(
+ ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE),
+ bidRequest);
+ final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure();
+
+ final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed(
+ Activity.TRANSMIT_UFPD, activityInvocationPayload);
+ final boolean disallowTransmitEids = !activityInfrastructure.isAllowed(
+ Activity.TRANSMIT_EIDS, activityInvocationPayload);
+ final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed(
+ Activity.TRANSMIT_GEO, activityInvocationPayload);
+
+ return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo);
+ }
+
+ private BidRequest maskUserPersonalInfo(BidRequest bidRequest,
+ boolean disallowTransmitUfpd,
+ boolean disallowTransmitEids,
+ boolean disallowTransmitGeo) {
+
+ final User maskedUser = userFpdActivityMask.maskUser(
+ bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids);
+ final Device maskedDevice = userFpdActivityMask.maskDevice(
+ bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo);
+
+ return bidRequest.toBuilder()
+ .user(maskedUser)
+ .device(maskedDevice)
+ .build();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java
index e7e8bc3e452..ada03ab1528 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java
@@ -42,7 +42,7 @@ public Future getTargeting(OptableTargetingProperties propertie
return cache.get(createCachingKey(tenant, origin, ips, query, true))
.recover(ignore -> apiClient.getTargeting(properties, query, ips, userAgent, timeout)
.recover(throwable -> isCircuitBreakerEnabled
- ? Future.succeededFuture(new TargetingResult(null, null))
+ ? Future.succeededFuture(new TargetingResult(null, null, null))
: Future.failedFuture(throwable))
.compose(result -> cache.put(
createCachingKey(tenant, origin, ips, query, false),
@@ -53,12 +53,19 @@ public Future getTargeting(OptableTargetingProperties propertie
}
private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) {
- return "%s:%s:%s:%s".formatted(
+ return "%s:%s:%s:%s:%s:%s".formatted(
tenant,
origin,
ips.getFirst(),
encodeQuery
? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8)
- : query.getIds());
+ : query.getIds(),
+ encodeQuery && query.getHid() != null
+ ? URLEncoder.encode(query.getHid(), StandardCharsets.UTF_8)
+ : query.getHid(),
+ encodeQuery && query.getHidAttributes() != null
+ ? URLEncoder.encode(query.getHidAttributes(), StandardCharsets.UTF_8)
+ : query.getHidAttributes()
+ );
}
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java
index a3ab28d098f..77ccb5be730 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java
@@ -10,12 +10,14 @@
import com.iab.openrtb.request.Eid;
import com.iab.openrtb.request.Geo;
import com.iab.openrtb.request.Segment;
+import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.Uid;
import com.iab.openrtb.request.User;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import org.apache.commons.io.IOUtils;
@@ -40,6 +42,7 @@
import org.prebid.server.privacy.model.Privacy;
import org.prebid.server.privacy.model.PrivacyContext;
import org.prebid.server.proto.openrtb.ext.request.ExtUser;
+import org.prebid.server.settings.model.Account;
import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
import java.io.IOException;
@@ -71,7 +74,31 @@ protected ModuleContext givenModuleContext(List audiences) {
return moduleContext;
}
- protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) {
+ protected ModuleContext givenModuleContext(List audiences, Future optableTargetingCall) {
+ final ModuleContext moduleContext = givenModuleContext(audiences);
+ moduleContext.setOptableTargetingCall(optableTargetingCall);
+ return moduleContext;
+ }
+
+ protected Eid givenId5Eid(String refValue) {
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode().set("ref", TextNode.valueOf(refValue)));
+ return Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ }
+
+ protected ObjectNode givenRefsObject(String refValue, String signature) {
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set(refValue, mapper.createObjectNode().set("signature", TextNode.valueOf(signature)));
+ return refs;
+ }
+
+ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure,
+ Timeout timeout,
+ Account account) {
final GppModel gppModel = new GppModel();
final TcfContext tcfContext = TcfContext.builder().build();
final GppContext gppContext = new GppContext(
@@ -80,6 +107,7 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr
return AuctionContext.builder()
.bidRequest(givenBidRequest())
+ .account(account)
.activityInfrastructure(activityInfrastructure)
.privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8"))
.gppContext(gppContext)
@@ -87,18 +115,32 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr
.build();
}
+ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) {
+ return givenAuctionContext(activityInfrastructure, timeout, null);
+ }
+
protected BidRequest givenBidRequest() {
return givenBidRequestWithUserEids(null);
}
protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) {
- return bidRequestCustomizer.apply(BidRequest.builder().id("requestId")).build();
+ return bidRequestCustomizer.apply(BidRequest.builder().id("requestId").site(Site.builder().build())).build();
}
protected BidRequest givenBidRequestWithUserEids(List eids) {
return BidRequest.builder()
.user(givenUser(eids))
.device(givenDevice())
+ .site(Site.builder().build())
+ .cur(List.of("USD"))
+ .build();
+ }
+
+ protected BidRequest givenBidRequestWithUser(User user) {
+ return BidRequest.builder()
+ .user(user)
+ .device(givenDevice())
+ .site(Site.builder().build())
.cur(List.of("USD"))
.build();
}
@@ -107,6 +149,7 @@ protected BidRequest givenBidRequestWithUserData(List data) {
return BidRequest.builder()
.user(givenUserWithData(data))
.device(givenDevice())
+ .site(Site.builder().build())
.cur(List.of("USD"))
.build();
}
@@ -148,17 +191,22 @@ protected TargetingResult givenTargetingResult() {
}
protected TargetingResult givenTargetingResult(List eids, List data) {
+ return givenTargetingResult(eids, data, null);
+ }
+
+ protected TargetingResult givenTargetingResult(List eids, List data, ObjectNode refs) {
return new TargetingResult(
List.of(new Audience(
"provider",
List.of(new AudienceId("id")),
"keyspace",
1)),
- new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data)));
+ new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data)),
+ refs);
}
protected TargetingResult givenEmptyTargetingResult() {
- return new TargetingResult(Collections.emptyList(), new Ortb2(null));
+ return new TargetingResult(Collections.emptyList(), new Ortb2(null), null);
}
protected User givenUser() {
@@ -245,6 +293,7 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key,
optableTargetingProperties.setApiKey(key);
optableTargetingProperties.setPpidMapping(Map.of("c", "id"));
optableTargetingProperties.setAdserverTargeting(true);
+ optableTargetingProperties.setEnrichWeb(true);
optableTargetingProperties.setTimeout(100L);
optableTargetingProperties.setCache(cacheProperties);
@@ -252,6 +301,10 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key,
}
protected Query givenQuery() {
- return Query.of("?que", "ry");
+ return Query.of("?que", "r", "y", "");
+ }
+
+ protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) {
+ return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled));
}
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java
new file mode 100644
index 00000000000..5778ac41289
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java
@@ -0,0 +1,398 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Eid;
+import io.vertx.core.Future;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.Status;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
+import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeoutException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class OptableBidderRequestHookTest extends BaseOptableTest {
+
+ @Mock
+ private BidderInvocationContext invocationContext;
+
+ @Mock
+ private BidderRequestPayload bidderRequestPayload;
+
+ private OptableBidderRequestHook target;
+
+ @BeforeEach
+ public void setUp() {
+ target = new OptableBidderRequestHook();
+ when(bidderRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(invocationContext.bidder()).thenReturn("bidder1");
+ }
+
+ @Test
+ public void shouldHaveRightCode() {
+ // given and when and then
+ assertThat(target.code()).isEqualTo("optable-targeting-bidder-request-hook");
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabled() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenOptableTargetingProperties(false));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.moduleContext()).isSameAs(moduleContext);
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabledAndTargetingCallFailed() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenOptableTargetingProperties(false));
+ moduleContext.setOptableTargetingCall(Future.failedFuture(new RuntimeException("timeout")));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(moduleContext.getId5Signature()).isNull();
+ }
+
+ @Test
+ public void shouldNotSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisabledAndTargetingCallIsPresent() {
+ // given
+ final String refValue = "refValue";
+ final String signature = "id5Signature";
+ final Eid id5Eid = givenId5Eid(refValue);
+ final ObjectNode refs = givenRefsObject(refValue, signature);
+ final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs);
+
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenOptableTargetingProperties(false));
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(moduleContext.getId5Signature()).isNull();
+ }
+
+ @Test
+ public void shouldNotSetId5SignatureOnModuleContextWhenBidderNotInEnrichmentSetAndTargetingCallIsPresent() {
+ // given
+ final String refValue = "refValue";
+ final String signature = "id5Signature";
+ final Eid id5Eid = givenId5Eid(refValue);
+ final ObjectNode refs = givenRefsObject(refValue, signature);
+ final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs);
+
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("otherBidder"));
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(moduleContext.getId5Signature()).isNull();
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenBiddersToEnrichIsEmpty() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Collections.emptySet());
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenBiddersToEnrichIsNull() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ }
+
+ @Test
+ public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100);
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(invocationContext.bidder()).thenReturn("bidder1");
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+
+ final BidRequest enrichedRequest = result
+ .payloadUpdate()
+ .apply(BidderRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(enrichedRequest.getUser().getEids().getFirst().getUids().getFirst().getId())
+ .isEqualTo("id");
+ assertThat(enrichedRequest.getUser().getData().getFirst().getSegment().getFirst().getId())
+ .isEqualTo("id");
+
+ assertAnalyticsTags(result.analyticsTags(), "bidder1", "enriched", "success");
+ }
+
+ @Test
+ public void shouldUpdateModuleContextWithTargetingOnSuccess() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100);
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(invocationContext.bidder()).thenReturn("bidder1");
+
+ // when
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(moduleContext.getTargeting()).isNotNull().isNotEmpty();
+ assertThat(moduleContext.getEnrichRequestStatus()).isNotNull()
+ .extracting(EnrichmentStatus::getStatus)
+ .extracting(Status::getValue)
+ .isEqualTo("success");
+ }
+
+ @Test
+ public void shouldSetId5SignatureOnModuleContextWhenTargetingResultHasId5Signature() {
+ // given
+ final String refValue = "refValue";
+ final String signature = "id5Signature";
+ final Eid id5Eid = givenId5Eid(refValue);
+ final ObjectNode refs = givenRefsObject(refValue, signature);
+ final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs);
+
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100);
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(invocationContext.bidder()).thenReturn("bidder1");
+
+ // when
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(moduleContext.getId5Signature()).isEqualTo(signature);
+ }
+
+ @Test
+ public void shouldNotSetId5SignatureOnModuleContextWhenTargetingResultHasNoId5Signature() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100);
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(invocationContext.bidder()).thenReturn("bidder1");
+
+ // when
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(moduleContext.getId5Signature()).isNull();
+ }
+
+ @Test
+ public void shouldReturnNoActionWithNoDataOutcomeWhenTargetingResultHasNoUser() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 50);
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(givenEmptyTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(invocationContext.bidder()).thenReturn("bidder1");
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+
+ assertThat(moduleContext.getTargeting()).isNull();
+ assertAnalyticsTags(result.analyticsTags(), "bidder1", "no-data", "fail");
+ }
+
+ @Test
+ public void shouldReturnNoActionWithErrorOutcomeWhenTargetingCallFails() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 50);
+ moduleContext.setOptableTargetingCall(
+ Future.failedFuture(new RuntimeException("targeting service error")));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(invocationContext.bidder()).thenReturn("bidder1");
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+
+ assertAnalyticsTags(result.analyticsTags(), "bidder1", "error", "fail");
+ }
+
+ @Test
+ public void shouldReturnNoActionWithTimeoutOutcomeWhenTargetingCallTimesOut() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 500);
+ moduleContext.setOptableTargetingCall(
+ Future.failedFuture(new TimeoutException("Timeout has been exceeded")));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(invocationContext.bidder()).thenReturn("bidder1");
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+
+ assertAnalyticsTags(result.analyticsTags(), "bidder1", "timeout", "fail");
+ }
+
+ private static ModuleContext givenModuleContextWithProperties(OptableTargetingProperties properties) {
+ final ModuleContext moduleContext = new ModuleContext();
+ moduleContext.setOptableTargetingProperties(properties);
+ return moduleContext;
+ }
+
+ private OptableTargetingProperties givenPropertiesWithPerBidderEnrichmentEnabled() {
+ final OptableTargetingProperties properties = givenOptableTargetingProperties(false);
+ properties.setEnrichmentPercentage(50);
+ return properties;
+ }
+
+ private void assertAnalyticsTags(Tags tags, String expectedBidder, String expectedOutcome, String expectedStatus) {
+ assertThat(tags).isNotNull();
+ assertThat(tags.activities()).hasSize(1);
+ assertThat(tags.activities().getFirst().name()).isEqualTo("optable-enrich-bidder-request");
+ assertThat(tags.activities().getFirst().status()).isEqualTo(expectedStatus);
+
+ assertThat(tags.activities().getFirst().results()).hasSize(1);
+ final JsonNode values = tags.activities().getFirst().results().getFirst().values();
+ assertThat(values.get("outcome").asText()).isEqualTo(expectedOutcome);
+ assertThat(values.has("execution-time")).isTrue();
+
+ assertThat(tags.activities().getFirst().results().getFirst().appliedTo()).isNotNull();
+ assertThat(tags.activities().getFirst().results().getFirst().appliedTo().bidders())
+ .containsExactly(expectedBidder);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java
new file mode 100644
index 00000000000..4e1a5b3e5f1
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java
@@ -0,0 +1,181 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Future;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
+import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler;
+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.OptableTargeting;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.when;
+
+@MockitoSettings(strictness = Strictness.LENIENT)
+@ExtendWith(VertxExtension.class)
+public class OptableRawAuctionRequestHookTest extends BaseOptableTest {
+
+ @Mock
+ private OptableTargeting optableTargeting;
+ @Mock
+ private UserFpdActivityMask userFpdActivityMask;
+ @Mock
+ private AuctionRequestPayload auctionRequestPayload;
+ @Mock
+ private ActivityInfrastructure activityInfrastructure;
+ @Mock
+ private AuctionInvocationContext invocationContext;
+ @Mock
+ private Timeout timeout;
+ @Mock
+ private TimeoutFactory timeoutFactory;
+ @Mock
+ private BidderEnrichmentSampler bidderEnrichmentSampler;
+
+ private ConfigResolver configResolver;
+ private TargetingRequestExecutor targetingRequestExecutor;
+ private OptableRawAuctionRequestHook target;
+
+ @BeforeEach
+ public void setUp() {
+ when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean()))
+ .thenAnswer(answer -> answer.getArgument(0));
+ configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false));
+ targetingRequestExecutor = new TargetingRequestExecutor(
+ optableTargeting, userFpdActivityMask, timeoutFactory, 0.01);
+ target = new OptableRawAuctionRequestHook(
+ configResolver, targetingRequestExecutor, bidderEnrichmentSampler,
+ CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
+ when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout));
+ when(invocationContext.timeout()).thenReturn(timeout);
+ when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true);
+ when(timeout.remaining()).thenReturn(1000L);
+ }
+
+ @Test
+ public void shouldHaveRightCode() {
+ // when and then
+ assertThat(target.code()).isEqualTo("optable-targeting-raw-auction-request-hook");
+ }
+
+ @SneakyThrows
+ @Test
+ public void shouldInjectEarlyNetworkCallToModuleContext(VertxTestContext vertxTestContext) {
+ // given
+ when(invocationContext.accountConfig())
+ .thenReturn(givenAccountConfig("key", "tenant", "origin", true));
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+ when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of("bidder"));
+
+ // when
+ final Future> result =
+ target.call(auctionRequestPayload, invocationContext);
+
+ // then
+ assertThat(result).isNotNull();
+ result.map(res -> (ModuleContext) res.moduleContext())
+ .compose(ModuleContext::getOptableTargetingCall)
+ .onComplete(call -> {
+ vertxTestContext.verify(() -> {
+ assertThat(call.result()).isNotNull();
+ });
+ vertxTestContext.completeNow();
+ });
+ }
+
+ @SneakyThrows
+ @Test
+ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAccountConfiguration(
+ VertxTestContext vertxTestContext) {
+
+ // given
+ when(invocationContext.accountConfig())
+ .thenReturn(givenAccountConfig("key", "tenant", null, true));
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ configResolver = new ConfigResolver(
+ mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true));
+ target = new OptableRawAuctionRequestHook(
+ configResolver, targetingRequestExecutor, bidderEnrichmentSampler,
+ CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
+
+ // when
+ final Future> result =
+ target.call(auctionRequestPayload, invocationContext);
+
+ // then
+ assertThat(result).isNotNull();
+ result.map(res -> (ModuleContext) res.moduleContext())
+ .onComplete(cxt -> {
+ vertxTestContext.verify(() -> {
+ final ModuleContext moduleContext = cxt.result();
+ assertThat(moduleContext.getOptableTargetingCall()).isNull();
+ assertThat(moduleContext.isEarlyNetworkCallEnabled()).isTrue();
+ });
+ vertxTestContext.completeNow();
+ });
+ }
+
+ @SneakyThrows
+ @Test
+ public void shouldNotInjectEarlyNetworkCallWhenTrafficSourceIsInvalid(VertxTestContext vertxTestContext) {
+ // given
+ when(invocationContext.accountConfig())
+ .thenReturn(givenAccountConfig("key", "tenant", "origin", true));
+ final BidRequest bidRequestWithoutTrafficSource = givenBidRequest(bidRequestCustomizer ->
+ bidRequestCustomizer.site(null).app(null));
+ when(auctionRequestPayload.bidRequest()).thenReturn(bidRequestWithoutTrafficSource);
+ when(invocationContext.auctionContext()).thenReturn(
+ givenAuctionContext(activityInfrastructure, timeout)
+ .toBuilder()
+ .bidRequest(bidRequestWithoutTrafficSource)
+ .build());
+
+ configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false));
+ target = new OptableRawAuctionRequestHook(
+ configResolver, targetingRequestExecutor, bidderEnrichmentSampler,
+ CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
+
+ // when
+ final Future> result =
+ target.call(auctionRequestPayload, invocationContext);
+
+ // then
+ assertThat(result).isNotNull();
+ result.map(res -> (ModuleContext) res.moduleContext())
+ .onComplete(cxt -> {
+ vertxTestContext.verify(() -> {
+ final ModuleContext moduleContext = cxt.result();
+ assertThat(moduleContext.isShouldSkipEnrichment()).isTrue();
+ assertThat(moduleContext.getOptableTargetingCall()).isNull();
+ });
+ vertxTestContext.completeNow();
+ });
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java
index af4a809df78..08c1d4d3997 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java
@@ -1,5 +1,6 @@
package org.prebid.server.hooks.modules.optable.targeting.v1;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.response.BidResponse;
import io.vertx.core.Future;
@@ -9,12 +10,17 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.analytics.Activity;
+import org.prebid.server.hooks.v1.analytics.Result;
+import org.prebid.server.hooks.v1.analytics.Tags;
import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
import org.prebid.server.hooks.v1.auction.AuctionResponseHook;
import org.prebid.server.hooks.v1.auction.AuctionResponsePayload;
@@ -54,9 +60,10 @@ public void shouldHaveCode() {
}
@Test
- public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() {
+ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTagsWhenTargetingIsEmptyAndNoId5Signature() {
// given
- when(invocationContext.moduleContext()).thenReturn(givenModuleContext());
+ when(invocationContext.moduleContext()).thenReturn(
+ givenModuleContext(null, Future.failedFuture(new RuntimeException("error"))));
// when
final Future> future =
@@ -67,14 +74,51 @@ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.no_action);
- assertThat(result.analyticsTags().activities().getFirst()
- .results().getFirst().values().get("reason")).isNotNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.payloadUpdate()).isNull();
+ assertThat(result.analyticsTags())
+ .extracting(Tags::activities)
+ .extracting(List::getFirst)
+ .extracting(Activity::results)
+ .extracting(List::getFirst)
+ .extracting(Result::values)
+ .extracting(it -> it.get("reason"))
+ .isNotNull();
assertThat(result.errors()).isNull();
}
+ @Test
+ public void shouldReturnResultWithUpdateActionAndId5SignatureWhenTargetingIsEmptyButId5SignatureIsPresent() {
+ // given
+ final String signature = "id5Signature";
+ final ModuleContext moduleContext =
+ givenModuleContext(null, Future.failedFuture(new RuntimeException("error")));
+ moduleContext.setId5Signature(signature);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse());
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(givenBidResponse()))
+ .bidResponse();
+ final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature);
+ }
+
@Test
public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn() {
// given
@@ -83,7 +127,8 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn(
"provider",
List.of(new AudienceId("audienceId")),
"keyspace",
- 1))));
+ 1)),
+ Future.succeededFuture(givenEmptyTargetingResult())));
when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse());
// when
@@ -117,14 +162,241 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn(
}
@Test
- public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() {
+ public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() {
+ // given
+ final String signature = "id5Signature";
+ final ModuleContext moduleContext = givenModuleContext(List.of(
+ new Audience(
+ "provider",
+ List.of(new AudienceId("audienceId")),
+ "keyspace",
+ 1)));
+ moduleContext.setId5Signature(signature);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse());
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(givenBidResponse()))
+ .bidResponse();
+ final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid()
+ .getFirst()
+ .getBid()
+ .getFirst()
+ .getExt()
+ .get("prebid")
+ .get("targeting");
+ final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+
+ assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId");
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature);
+ }
+
+ @Test
+ public void shouldEnrichBidResponseWithId5SignatureOnlyWhenNoTargeting() {
+ // given
+ final String signature = "id5Signature";
+ final ModuleContext moduleContext =
+ givenModuleContext(null, Future.succeededFuture(givenEmptyTargetingResult()));
+ moduleContext.setId5Signature(signature);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse());
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(givenBidResponse()))
+ .bidResponse();
+ final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature);
+ }
+
+ @Test
+ public void shouldReturnUpdateActionWhenTargetingIsPresentEvenIfOptableTargetingCallFails() {
+ // given
+ when(invocationContext.moduleContext()).thenReturn(givenModuleContext(
+ List.of(new Audience(
+ "provider",
+ List.of(new AudienceId("audienceId")),
+ "keyspace",
+ 1)),
+ Future.failedFuture(new RuntimeException("error"))));
+ when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse());
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(givenBidResponse()))
+ .bidResponse();
+ final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid()
+ .getFirst()
+ .getBid()
+ .getFirst()
+ .getExt()
+ .get("prebid")
+ .get("targeting");
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+ assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId");
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmptyAndNoId5Signature() {
+ // given
+ when(invocationContext.moduleContext()).thenReturn(
+ givenModuleContext(null, Future.failedFuture(new RuntimeException("error"))));
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.payloadUpdate()).isNull();
+ }
+
+ @Test
+ public void shouldReturnUpdateActionWithId5SignatureWhenOptableTargetingCallFailsAndTargetingIsEmpty() {
+ // given
+ final String signature = "id5Signature";
+ final ModuleContext moduleContext =
+ givenModuleContext(null, Future.failedFuture(new RuntimeException("error")));
+ moduleContext.setId5Signature(signature);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse());
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(givenBidResponse()))
+ .bidResponse();
+ final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature);
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenTargetingCallFailsAndNoBidsAndNoId5Signature() {
+ // given
+ when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(
+ new Audience(
+ "provider",
+ List.of(new AudienceId("audienceId")),
+ "keyspace",
+ 1)),
+ Future.failedFuture(new RuntimeException("error"))));
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.payloadUpdate()).isNull();
+ }
+
+ @Test
+ public void shouldReturnUpdateActionWithId5SignatureWhenTargetingCallFailsAndNoBids() {
+ // given
+ final String signature = "id5Signature";
+ final ModuleContext moduleContext = givenModuleContext(List.of(
+ new Audience(
+ "provider",
+ List.of(new AudienceId("audienceId")),
+ "keyspace",
+ 1)),
+ Future.failedFuture(new RuntimeException("error")));
+ moduleContext.setId5Signature(signature);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ final BidResponse bidlessResponse = BidResponse.builder().build();
+ when(auctionResponsePayload.bidResponse()).thenReturn(bidlessResponse);
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(bidlessResponse))
+ .bidResponse();
+ final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature);
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenAdserverTargetingIsDisabledAndNoId5Signature() {
// given
+ final OptableTargetingProperties properties = givenOptableTargetingProperties(false);
+ properties.setAdserverTargeting(false);
+ configResolver = new ConfigResolver(mapper, jsonMerger, properties);
+ target = new OptableTargetingAuctionResponseHook(configResolver, mapper, jsonMerger);
+ when(invocationContext.accountConfig()).thenReturn(mapper.valueToTree(properties));
when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(
new Audience(
"provider",
List.of(new AudienceId("audienceId")),
"keyspace",
- 1))));
+ 1)),
+ Future.failedFuture(new RuntimeException("error"))));
// when
final Future> future =
@@ -137,6 +409,96 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() {
assertThat(result).isNotNull()
.returns(InvocationStatus.success, InvocationResult::status)
.returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.payloadUpdate()).isNull();
+ }
+
+ @Test
+ public void shouldReturnUpdateActionWithId5SignatureWhenAdserverTargetingIsDisabled() {
+ // given
+ final String signature = "id5Signature";
+ final OptableTargetingProperties properties = givenOptableTargetingProperties(false);
+ properties.setAdserverTargeting(false);
+ configResolver = new ConfigResolver(mapper, jsonMerger, properties);
+ target = new OptableTargetingAuctionResponseHook(configResolver, mapper, jsonMerger);
+ when(invocationContext.accountConfig()).thenReturn(mapper.valueToTree(properties));
+ final ModuleContext moduleContext = givenModuleContext(List.of(
+ new Audience(
+ "provider",
+ List.of(new AudienceId("audienceId")),
+ "keyspace",
+ 1)),
+ Future.failedFuture(new RuntimeException("error")));
+ moduleContext.setId5Signature(signature);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(givenBidResponse()))
+ .bidResponse();
+ final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature);
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenSkipEnrichmentIsTrueAndNoId5Signature() {
+ // given
+ final ModuleContext moduleContext = givenModuleContext();
+ moduleContext.setShouldSkipEnrichment(true);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.payloadUpdate()).isNull();
+ }
+
+ @Test
+ public void shouldReturnUpdateActionWithId5SignatureWhenSkipEnrichmentIsTrue() {
+ // given
+ final String signature = "id5Signature";
+ final ModuleContext moduleContext = givenModuleContext();
+ moduleContext.setShouldSkipEnrichment(true);
+ moduleContext.setId5Signature(signature);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+ final BidResponse bidResponse = result
+ .payloadUpdate()
+ .apply(AuctionResponsePayloadImpl.of(givenBidResponse()))
+ .bidResponse();
+ final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature);
}
private ObjectNode givenAccountConfig(boolean cacheEnabled) {
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java
index 008262b8a3e..c804be0b508 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java
@@ -2,27 +2,43 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Data;
+import com.iab.openrtb.request.Eid;
+import com.iab.openrtb.request.Segment;
+import com.iab.openrtb.request.Uid;
import io.vertx.core.Future;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.mockito.junit.jupiter.MockitoSettings;
-import org.mockito.quality.Strictness;
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
+import org.prebid.server.hooks.execution.model.ExecutionGroup;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
+import org.prebid.server.hooks.execution.model.HookHttpEndpoint;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionPlan;
import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
import org.prebid.server.hooks.modules.optable.targeting.model.Status;
+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.OptableTargeting;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.settings.model.Account;
+
+import java.util.List;
+import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -30,57 +46,61 @@
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
-@MockitoSettings(strictness = Strictness.LENIENT)
-public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest {
+class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest {
private ConfigResolver configResolver;
@Mock
private OptableTargeting optableTargeting;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private UserFpdActivityMask userFpdActivityMask;
- private OptableTargetingProcessedAuctionRequestHook target;
-
@Mock
private AuctionRequestPayload auctionRequestPayload;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private AuctionInvocationContext invocationContext;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private ActivityInfrastructure activityInfrastructure;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private Timeout timeout;
+ @Mock(strictness = Mock.Strictness.LENIENT)
+ private TimeoutFactory timeoutFactory;
+
+ private TargetingRequestExecutor targetingRequestExecutor;
+
+ private OptableTargetingProcessedAuctionRequestHook target;
+
@BeforeEach
- public void setUp() {
+ void setUp() {
when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean()))
.thenAnswer(answer -> answer.getArgument(0));
configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false));
+ targetingRequestExecutor = new TargetingRequestExecutor(
+ optableTargeting, userFpdActivityMask, timeoutFactory, 0.01);
target = new OptableTargetingProcessedAuctionRequestHook(
- configResolver,
- optableTargeting,
- userFpdActivityMask,
- 0.01);
+ configResolver, targetingRequestExecutor, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true));
- when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout));
+ when(invocationContext.auctionContext()).thenReturn(
+ givenAuctionContext(activityInfrastructure, timeout, Account.builder().id("accountId").build()));
when(invocationContext.timeout()).thenReturn(timeout);
when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true);
when(timeout.remaining()).thenReturn(1000L);
}
@Test
- public void shouldHaveRightCode() {
+ void codeShouldReturnRightCode() {
// when and then
assertThat(target.code()).isEqualTo("optable-targeting-processed-auction-request-hook");
}
@Test
- public void shouldReturnResultWithPBSAnalyticsTags() {
+ void callShouldReturnResultWithPBSAnalyticsTags() {
// given
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any()))
@@ -95,16 +115,16 @@ public void shouldReturnResultWithPBSAnalyticsTags() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
assertThat(result.analyticsTags().activities().getFirst()
.results().getFirst().values().get("execution-time")).isNotNull();
}
@Test
- public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() {
+ void callShouldReturnResultWithUpdateActionWhenOptableTargetingReturnsTargeting() {
// given
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any()))
@@ -119,30 +139,233 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
final BidRequest bidRequest = result
.payloadUpdate()
.apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
.bidRequest();
- assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id");
- assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id");
+ assertThat(bidRequest.getUser().getEids())
+ .flatExtracting(Eid::getUids)
+ .extracting(Uid::getId)
+ .containsExactly("id");
+ assertThat(bidRequest.getUser().getData())
+ .flatExtracting(Data::getSegment)
+ .extracting(Segment::getId)
+ .containsExactly("id");
+ }
+
+ @Test
+ void callShouldSetId5SignatureOnModuleContextWhenTargetingResultContainsId5Signature() {
+ // given
+ final String refValue = "refValue";
+ final String signature = "id5Signature";
+ final Eid id5Eid = givenId5Eid(refValue);
+ final ObjectNode refs = givenRefsObject(refValue, signature);
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult(List.of(id5Eid), null, refs)));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat((ModuleContext) future.result().moduleContext())
+ .isNotNull()
+ .extracting(ModuleContext::getId5Signature)
+ .isEqualTo(signature);
}
@Test
- public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() {
+ void callShouldLeaveId5SignatureNullWhenTargetingResultHasNoId5Signature() {
+ // given
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat((ModuleContext) future.result().moduleContext())
+ .isNotNull()
+ .extracting(ModuleContext::getId5Signature)
+ .isNull();
+ }
+
+ @Test
+ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() {
+ // given
+ final ModuleContext moduleContext = new ModuleContext();
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver,
+ targetingRequestExecutor,
+ CompositeHookExecutionPlan.of(givenExecutionPlan(true, false)),
+ 0.01);
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ moduleContext.setOptableTargetingCall(
+ targetingRequestExecutor.makeRequest(
+ auctionRequestPayload,
+ invocationContext,
+ givenOptableTargetingProperties("key", "tenant", "origin", false),
+ null));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids())
+ .flatExtracting(Eid::getUids)
+ .extracting(Uid::getId)
+ .containsExactly("id");
+ assertThat(bidRequest.getUser().getData())
+ .flatExtracting(Data::getSegment)
+ .extracting(Segment::getId)
+ .containsExactly("id");
+ }
+
+ @Test
+ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() {
+ // given
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver,
+ targetingRequestExecutor,
+ CompositeHookExecutionPlan.of(givenExecutionPlan(false, false)),
+ 0.01);
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids())
+ .flatExtracting(Eid::getUids)
+ .extracting(Uid::getId)
+ .containsExactly("id");
+ assertThat(bidRequest.getUser().getData())
+ .flatExtracting(Data::getSegment)
+ .extracting(Segment::getId)
+ .containsExactly("id");
+ }
+
+ @Test
+ void callShouldReturnResultWithEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() {
+ // given
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver,
+ targetingRequestExecutor,
+ CompositeHookExecutionPlan.of(givenExecutionPlan(false, true)),
+ 0.01);
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids())
+ .flatExtracting(Eid::getUids)
+ .extracting(Uid::getId)
+ .containsExactly("id");
+ assertThat(bidRequest.getUser().getData())
+ .flatExtracting(Data::getSegment)
+ .extracting(Segment::getId)
+ .containsExactly("id");
+ }
+
+ @Test
+ void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() {
+ // given
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver,
+ targetingRequestExecutor,
+ CompositeHookExecutionPlan.of(givenExecutionPlan(true, true)),
+ 0.01);
+ when(invocationContext.moduleContext()).thenReturn(new ModuleContext());
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids()).isNull();
+ assertThat(bidRequest.getUser().getData()).isNull();
+ }
+
+ @Test
+ void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() {
// given
configResolver = new ConfigResolver(
mapper,
jsonMerger,
givenOptableTargetingProperties("key", "tenant", null, false));
target = new OptableTargetingProcessedAuctionRequestHook(
- configResolver,
- optableTargeting,
- userFpdActivityMask,
- 0.01);
+ configResolver, targetingRequestExecutor, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
when(invocationContext.accountConfig())
.thenReturn(givenAccountConfig("key", "tenant", null, true));
@@ -155,26 +378,23 @@ public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
assertThat((ModuleContext) result.moduleContext())
.extracting(it -> it.getEnrichRequestStatus().getStatus())
.isEqualTo(Status.FAIL);
}
@Test
- public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() {
+ void callShouldReturnFailWhenTenantIsAbsentInAccountConfiguration() {
// given
configResolver = new ConfigResolver(
mapper,
jsonMerger,
givenOptableTargetingProperties("key", null, "origin", false));
target = new OptableTargetingProcessedAuctionRequestHook(
- configResolver,
- optableTargeting,
- userFpdActivityMask,
- 0.01);
+ configResolver, targetingRequestExecutor, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
when(invocationContext.accountConfig())
.thenReturn(givenAccountConfig("key", null, null, true));
@@ -187,18 +407,17 @@ public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
assertThat((ModuleContext) result.moduleContext())
.extracting(it -> it.getEnrichRequestStatus().getStatus())
.isEqualTo(Status.FAIL);
}
@Test
- public void shouldReturnResultWithCleanedUpUserExtOptableTag() {
+ void callShouldReturnResultWithCleanedUpUserExtOptableTag() {
// given
- when(invocationContext.timeout()).thenReturn(timeout);
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any()))
.thenReturn(Future.succeededFuture(givenTargetingResult()));
@@ -212,10 +431,10 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
final ObjectNode optable = (ObjectNode) result
.payloadUpdate()
.apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
@@ -226,7 +445,7 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() {
}
@Test
- public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() {
+ void callShouldReturnResultWithUpdateWhenOptableTargetingDoesNotReturnResult() {
// given
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any())).thenReturn(Future.succeededFuture(null));
@@ -240,17 +459,54 @@ public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult()
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ }
+
+ @Test
+ void callShouldReturnUpdateWhenTrafficSourceIsInvalid() {
+ // given
+ final ModuleContext moduleContext = new ModuleContext();
+ moduleContext.setShouldSkipEnrichment(true);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
}
private ObjectNode givenAccountConfig(boolean cacheEnabled) {
return givenAccountConfig("key", "tenant", "origin", cacheEnabled);
}
- private ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) {
- return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled));
+ private ExecutionPlan givenExecutionPlan(boolean hasRawAuctionRequestHook, boolean hasBidderRequestHook) {
+ final HookId rawAuctionHook = HookId.of("optable-targeting", "optable-targeting-raw-auction-request-hook");
+ final HookId bidderRequestHook = HookId.of("optable-targeting", "optable-targeting-bidder-request-hook");
+
+ final StageExecutionPlan rawAuctionStage = StageExecutionPlan.of(List.of(
+ ExecutionGroup.of(null, hasRawAuctionRequestHook ? List.of(rawAuctionHook) : List.of())
+ ));
+ final StageExecutionPlan bidderRequestStage = StageExecutionPlan.of(List.of(
+ ExecutionGroup.of(null, hasBidderRequestHook ? List.of(bidderRequestHook) : List.of())
+ ));
+
+ final EndpointExecutionPlan endpointExecutionPlan = EndpointExecutionPlan.of(Map.of(
+ Stage.raw_auction_request, rawAuctionStage,
+ Stage.bidder_request, bidderRequestStage
+ ));
+
+ return ExecutionPlan.of(null, Map.of(HookHttpEndpoint.POST_AUCTION, endpointExecutionPlan));
}
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java
new file mode 100644
index 00000000000..b8b7c986636
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java
@@ -0,0 +1,88 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+public class AliasesResolverTest {
+
+ @Mock
+ private BidderCatalog bidderCatalog;
+
+ private AliasesResolver target;
+
+ @BeforeEach
+ public void setUp() {
+ target = AliasesResolver.of(bidderCatalog);
+ }
+
+ @Test
+ public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestIsNull() {
+ // when
+ final BidderAliases result = target.resolve(null);
+
+ // then
+ assertThat(result.isAliasDefined("anyAlias")).isFalse();
+ }
+
+ @Test
+ public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestHasNoExt() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder().build();
+
+ // when
+ final BidderAliases result = target.resolve(bidRequest);
+
+ // then
+ assertThat(result.isAliasDefined("anyAlias")).isFalse();
+ }
+
+ @Test
+ public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestHasNoExtPrebid() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .ext(ExtRequest.empty())
+ .build();
+
+ // when
+ final BidderAliases result = target.resolve(bidRequest);
+
+ // then
+ assertThat(result.isAliasDefined("anyAlias")).isFalse();
+ }
+
+ @Test
+ public void resolveShouldReturnBidderAliasesWithValuesWhenBidRequestHasAliases() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .ext(ExtRequest.of(ExtRequestPrebid.builder()
+ .aliases(Map.of("alias", "bidder"))
+ .aliasgvlids(Map.of("alias", 123))
+ .build()))
+ .build();
+
+ given(bidderCatalog.isValidName(anyString())).willReturn(false);
+
+ // when
+ final BidderAliases result = target.resolve(bidRequest);
+
+ // then
+ assertThat(result.isAliasDefined("alias")).isTrue();
+ assertThat(result.resolveBidder("alias")).isEqualTo("bidder");
+ assertThat(result.resolveAliasVendorId("alias")).isEqualTo(123);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java
index b6d72748fbd..606c89ee102 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java
@@ -27,4 +27,31 @@ public void shouldRemoveUserExtOptableTag() {
.extracting(it -> it.getProperty("optable"))
.isEqualTo(null);
}
+
+ @Test
+ public void shouldKeepOtherUserExtOptableTags() {
+ // given
+ final User user = givenUser();
+ ((com.fasterxml.jackson.databind.node.ObjectNode) user.getExt().getProperty("optable"))
+ .put("other", "value")
+ .put("id5_signature", "signature");
+
+ final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest ->
+ bidRequest.user(user)));
+
+ // when
+ final AuctionRequestPayload result = BidRequestCleaner.instance().apply(auctionRequestPayload);
+
+ // then
+ assertThat(result).extracting(AuctionRequestPayload::bidRequest)
+ .extracting(BidRequest::getUser)
+ .extracting(User::getExt)
+ .extracting(it -> (com.fasterxml.jackson.databind.node.ObjectNode) it.getProperty("optable"))
+ .isNotNull()
+ .satisfies(optable -> {
+ assertThat(optable.has("other")).isTrue();
+ assertThat(optable.has("email")).isFalse();
+ assertThat(optable.has("id5_signature")).isFalse();
+ });
+ }
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java
new file mode 100644
index 00000000000..914079178e7
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java
@@ -0,0 +1,266 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.IntSupplier;
+import java.util.function.UnaryOperator;
+
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+public class BidderEnrichmentSamplerTest extends BaseOptableTest {
+
+ @Mock
+ private AliasesResolver aliasesResolver;
+
+ @Mock
+ private BidderAliases bidderAliases;
+
+ @Mock
+ private IntSupplier randomSupplier;
+
+ private BidderEnrichmentSampler target;
+
+ @BeforeEach
+ public void setUp() {
+ target = BidderEnrichmentSampler.of(aliasesResolver, randomSupplier);
+ given(aliasesResolver.resolve(any())).willReturn(bidderAliases);
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenRequestHasNoImpressions() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(identity());
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenImpHasNoExt() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(identity()))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenImpExtHasNoPrebidBidderNode() {
+ // given
+ final ObjectNode ext = mapper.createObjectNode();
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(ext)))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenBidderNodeIsEmpty() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt())))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldIncludeAllBiddersWhenDefaultPercentageIs100() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(99);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("bidderA", "bidderB");
+ }
+
+ @Test
+ public void sampleShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(-1, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldExcludeBidderWhenRandomValueEqualsPercentage() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(50);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldIncludeBidderWhenRandomValueIsBelowPercentage() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(49);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ @Test
+ public void sampleShouldExcludeBidderWhenRandomValueExceedsPercentage() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(51);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldExcludeBidderWhenPercentageIsZeroAndRandomIsZero() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(0, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldUseBidderSpecificPercentageWhenAvailable() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(99);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(-1, Map.of("bidderA", 100)));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ @Test
+ public void sampleShouldUseAliasSpecificPercentageWhenBidderResolvesToAlias() {
+ // given
+ given(bidderAliases.resolveBidder("bidderA")).willReturn("bidderA");
+ given(bidderAliases.resolveBidder("bidderB")).willReturn("aliasB");
+ given(randomSupplier.getAsInt()).willReturn(99);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(-1, Map.of("aliasB", 100)));
+
+ // then
+ assertThat(result).containsExactly("bidderB");
+ }
+
+ @Test
+ public void sampleShouldDeduplicateBiddersAppearingInMultipleImps() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(0);
+
+ final ObjectNode ext = givenBidderExt("bidderA");
+ final BidRequest bidRequest = givenBidRequest(request -> request.imp(List.of(
+ givenImp(imp -> imp.ext(ext)),
+ givenImp(imp -> imp.ext(ext)))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ private OptableTargetingProperties givenSampleProperties(int defaultPct, Map bidderPcts) {
+ final OptableTargetingProperties props = new OptableTargetingProperties();
+ props.setEnrichmentPercentage(defaultPct);
+ props.setBidderEnrichmentPercentages(bidderPcts);
+ return props;
+ }
+
+ private ObjectNode givenBidderExt(String... bidders) {
+ final ObjectNode bidderNode = mapper.createObjectNode();
+ for (String bidder : bidders) {
+ bidderNode.put(bidder, "value");
+ }
+ final ObjectNode prebidNode = mapper.createObjectNode();
+ prebidNode.set("bidder", bidderNode);
+ final ObjectNode ext = mapper.createObjectNode();
+ ext.set("prebid", prebidNode);
+ return ext;
+ }
+
+ private static Imp givenImp(UnaryOperator impCustomizer) {
+ return impCustomizer.apply(Imp.builder()).build();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java
index 1917fd6ca27..e0a723bbfa3 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java
@@ -109,6 +109,7 @@ private TargetingResult givenTargetingResult() {
List.of(new AudienceId("1")),
"keyspace",
0)),
- new Ortb2(new User(null, null)));
+ new Ortb2(new User(null, null)),
+ null);
}
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java
new file mode 100644
index 00000000000..c836de388a9
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java
@@ -0,0 +1,360 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
+import org.prebid.server.hooks.execution.model.ExecutionGroup;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
+import org.prebid.server.hooks.execution.model.HookHttpEndpoint;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionPlan;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountHooksConfiguration;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CompositeHookExecutionPlanTest {
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnTrueWhenGlobalPlanHasHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnTrueWhenAccountPlanHasHook() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnTrueWhenBothPlansHaveHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenNeitherPlanHasHook() {
+ // given
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenAccountIsNull() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(null)).isFalse();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenAccountIdIsEmpty() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnGlobalFlagWhenAccountHasNoHooksConfig() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnSameResultOnRepeatedCallsForSameAccount() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnTrueWhenGlobalPlanHasHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnTrueWhenAccountPlanHasHook() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnTrueWhenBothPlansHaveHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenNeitherPlanHasHook() {
+ // given
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenAccountIsNull() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(null)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenAccountIdIsEmpty() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnGlobalFlagWhenAccountHasNoHooksConfig() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnSameResultOnRepeatedCallsForSameAccount() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenOnlyBidderRequestHookIsInGlobalPlan() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenOnlyRawAuctionRequestHookIsInGlobalPlan() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenConfigured() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout(
+ "bidder_request",
+ "optable-targeting-bidder-request-hook",
+ 500L);
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(500L);
+ }
+
+ @Test
+ public void getBidderRequestTimeoutShouldReturnAccountTimeoutWhenAccountPlanOverrides() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout(
+ "bidder_request",
+ "optable-targeting-bidder-request-hook",
+ 500L);
+ final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout(
+ "bidder_request",
+ "optable-targeting-bidder-request-hook",
+ 200L);
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(200L);
+ }
+
+ @Test
+ public void getBidderRequestTimeoutShouldFallbackToGlobalWhenAccountPlanHasNoTimeout() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout(
+ "bidder_request",
+ "optable-targeting-bidder-request-hook",
+ 300L);
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(300L);
+ }
+
+ @Test
+ public void getBidderRequestTimeoutShouldReturnZeroWhenNoPlanIsConfigured() {
+ // given
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(0L);
+ }
+
+ @Test
+ public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIsNull() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout(
+ "bidder_request",
+ "optable-targeting-bidder-request-hook",
+ 400L);
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+
+ // when and then
+ assertThat(target.getOptableTargetingBidderRequestTimeout(null)).isEqualTo(400L);
+ }
+
+ @Test
+ public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIdIsEmpty() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout(
+ "bidder_request",
+ "optable-targeting-bidder-request-hook",
+ 150L);
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("").build();
+
+ // when and then
+ assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(150L);
+ }
+
+ @Test
+ public void getBidderRequestTimeoutShouldReturnSameResultOnRepeatedCallsForSameAccount() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout(
+ "bidder_request",
+ "optable-targeting-bidder-request-hook",
+ 250L);
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(250L);
+ assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(250L);
+ }
+
+ private ExecutionPlan givenExecutionPlan(String stage, String hookCode) {
+ final HookId hookId = HookId.of("optable-targeting", hookCode);
+ final ExecutionGroup group = ExecutionGroup.of(null, List.of(hookId));
+ final StageExecutionPlan stagePlan = StageExecutionPlan.of(List.of(group));
+ final EndpointExecutionPlan endpointPlan = EndpointExecutionPlan.of(Map.of(Stage.valueOf(stage), stagePlan));
+ return ExecutionPlan.of(null, Map.of(HookHttpEndpoint.POST_AUCTION, endpointPlan));
+ }
+
+ private ExecutionPlan givenExecutionPlanWithTimeout(String stage, String hookCode, long timeout) {
+ final HookId hookId = HookId.of("optable-targeting", hookCode);
+ final ExecutionGroup group = ExecutionGroup.of(timeout, List.of(hookId));
+ final StageExecutionPlan stagePlan = StageExecutionPlan.of(List.of(group));
+ final EndpointExecutionPlan endpointPlan = EndpointExecutionPlan.of(Map.of(Stage.valueOf(stage), stagePlan));
+ return ExecutionPlan.of(null, Map.of(HookHttpEndpoint.POST_AUCTION, endpointPlan));
+ }
+
+ private Account givenAccount(String accountId, ExecutionPlan executionPlan) {
+ return Account.builder()
+ .id(accountId)
+ .hooks(AccountHooksConfiguration.of(executionPlan, null, null))
+ .build();
+ }
+}
+
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java
new file mode 100644
index 00000000000..b3142c173f2
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java
@@ -0,0 +1,64 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable;
+import org.prebid.server.json.ObjectMapperProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExtUserOptableResolverTest {
+
+ @Test
+ public void shouldResolveExtUserOptableWhenNodeIsValid() {
+ // given
+ final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode();
+ node.set("email", TextNode.valueOf("user@example.com"));
+ node.set("phone", TextNode.valueOf("123"));
+ node.set("zip", TextNode.valueOf("321"));
+ node.set("vid", TextNode.valueOf("vid"));
+ node.set("id5_signature", TextNode.valueOf("signature"));
+
+ // when
+ final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0);
+
+ // then
+ assertThat(result).isNotNull()
+ .returns("user@example.com", ExtUserOptable::getEmail)
+ .returns("123", ExtUserOptable::getPhone)
+ .returns("321", ExtUserOptable::getZip)
+ .returns("vid", ExtUserOptable::getVid)
+ .returns("signature", ExtUserOptable::getId5Signature);
+ }
+
+ @Test
+ public void shouldReturnNullWhenNodeIsNotParseable() {
+ // given
+ final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode();
+ node.set("email", ObjectMapperProvider.mapper().createArrayNode().add(1).add(2));
+
+ // when
+ final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldResolveExtUserOptableWhenNodeIsEmpty() {
+ // given
+ final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode();
+
+ // when
+ final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0);
+
+ // then
+ assertThat(result).isNotNull()
+ .returns(null, ExtUserOptable::getEmail)
+ .returns(null, ExtUserOptable::getPhone)
+ .returns(null, ExtUserOptable::getZip)
+ .returns(null, ExtUserOptable::getVid)
+ .returns(null, ExtUserOptable::getId5Signature);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java
new file mode 100644
index 00000000000..da0e5bb4161
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java
@@ -0,0 +1,339 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.request.Eid;
+import com.iab.openrtb.request.Uid;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User;
+import org.prebid.server.json.ObjectMapperProvider;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class Id5ResolverTest {
+
+ private ObjectMapper mapper;
+
+ @BeforeEach
+ public void setUp() {
+ mapper = ObjectMapperProvider.mapper();
+ }
+
+ @Test
+ public void shouldReturnNullWhenSignatureIsJsonNull() {
+ // given
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("refValue")));
+ final Eid eid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("refValue", mapper.createObjectNode().putNull("signature"));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ refs);
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenTargetingResultIsNull() {
+ // when
+ final String result = Id5Resolver.resolveId5Signature(null);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenOrtb2IsNull() {
+ // given
+ final TargetingResult targetingResult = new TargetingResult(List.of(), null, null);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenUserHasNoEids() {
+ // given
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(null, null)),
+ null);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenEidsDoNotMatchOptableAndId5Source() {
+ // given
+ final Eid eid = Eid.builder()
+ .source("other-source")
+ .inserter("other-inserter")
+ .uids(List.of(Uid.builder().id("id").build()))
+ .build();
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ null);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenMatchingEidButRefsAreAbsent() {
+ // given
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("refValue")));
+
+ final Eid eid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ null);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenRefsDoNotContainResolvedRef() {
+ // given
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("refValue")));
+
+ final Eid eid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("otherRef", mapper.createObjectNode()
+ .set("signature", TextNode.valueOf("signatureValue")));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ refs);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnSignatureWhenAllConditionsAreMet() {
+ // given
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("refValue")));
+
+ final Eid eid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("refValue", mapper.createObjectNode()
+ .set("signature", TextNode.valueOf("signatureValue")));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ refs);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isEqualTo("signatureValue");
+ }
+
+ @Test
+ public void shouldReturnSignatureWhenMultipleMatchingEidsExist() {
+ // given
+ final ObjectNode firstUidExt = mapper.createObjectNode();
+ firstUidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("firstRef")));
+
+ final ObjectNode secondUidExt = mapper.createObjectNode();
+ secondUidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("secondRef")));
+
+ final Eid firstEid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id1").ext(firstUidExt).build()))
+ .build();
+ final Eid secondEid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("firstRef", mapper.createObjectNode()
+ .set("signature", TextNode.valueOf("firstSignature")));
+ refs.set("secondRef", mapper.createObjectNode()
+ .set("signature", TextNode.valueOf("secondSignature")));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(firstEid, secondEid), null)),
+ refs);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isEqualTo("firstSignature");
+ }
+
+ @Test
+ public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() {
+ // given
+ final ObjectNode firstUidExt = mapper.createObjectNode();
+ firstUidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("firstRef")));
+
+ final ObjectNode secondUidExt = mapper.createObjectNode();
+ secondUidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("secondRef")));
+
+ final Eid firstEid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id1").ext(firstUidExt).build()))
+ .build();
+ final Eid secondEid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("secondRef", mapper.createObjectNode()
+ .set("signature", TextNode.valueOf("secondSignature")));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(firstEid, secondEid), null)),
+ refs);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isEqualTo("secondSignature");
+ }
+
+ @Test
+ public void shouldReturnNullWhenRefEntrySignatureIsBlank() {
+ // given
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("refValue")));
+
+ final Eid eid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("refValue", mapper.createObjectNode()
+ .set("signature", TextNode.valueOf(" ")));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ refs);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() {
+ // given
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("refValue")));
+
+ final Eid eid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("refValue", mapper.createObjectNode()
+ .set("signature", mapper.createObjectNode()));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ refs);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void shouldReturnNullWhenRefEntryHasNoSignature() {
+ // given
+ final ObjectNode uidExt = mapper.createObjectNode();
+ uidExt.set("optable", mapper.createObjectNode()
+ .set("ref", TextNode.valueOf("refValue")));
+
+ final Eid eid = Eid.builder()
+ .source("id5-sync.com")
+ .inserter("optable.co")
+ .uids(List.of(Uid.builder().id("id").ext(uidExt).build()))
+ .build();
+ final ObjectNode refs = mapper.createObjectNode();
+ refs.set("refValue", mapper.createObjectNode()
+ .set("other", TextNode.valueOf("value")));
+ final TargetingResult targetingResult = new TargetingResult(
+ List.of(),
+ new Ortb2(new User(List.of(eid), null)),
+ refs);
+
+ // when
+ final String result = Id5Resolver.resolveId5Signature(targetingResult);
+
+ // then
+ assertThat(result).isNull();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java
new file mode 100644
index 00000000000..d9eb8bd1798
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java
@@ -0,0 +1,122 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.response.BidResponse;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
+import org.prebid.server.hooks.v1.auction.AuctionResponsePayload;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.json.JsonMerger;
+import org.prebid.server.json.ObjectMapperProvider;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class Id5SignatureBidResponseEnricherTest {
+
+ private final JacksonMapper jacksonMapper = new JacksonMapper(ObjectMapperProvider.mapper());
+ private final JsonMerger jsonMerger = new JsonMerger(jacksonMapper);
+
+ @Test
+ public void shouldReturnOriginBidResponseWhenId5SignatureIsNull() {
+ // given
+ final BidResponse bidResponse = BidResponse.builder().build();
+ final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse);
+ final Id5SignatureBidResponseEnricher enricher =
+ Id5SignatureBidResponseEnricher.of(null, ObjectMapperProvider.mapper(), jsonMerger);
+
+ // when
+ final AuctionResponsePayload result = enricher.apply(payload);
+
+ // then
+ assertThat(result.bidResponse()).isSameAs(bidResponse);
+ }
+
+ @Test
+ public void shouldReturnOriginBidResponseWhenId5SignatureIsEmpty() {
+ // given
+ final BidResponse bidResponse = BidResponse.builder().build();
+ final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse);
+ final Id5SignatureBidResponseEnricher enricher =
+ Id5SignatureBidResponseEnricher.of("", ObjectMapperProvider.mapper(), jsonMerger);
+
+ // when
+ final AuctionResponsePayload result = enricher.apply(payload);
+
+ // then
+ assertThat(result.bidResponse()).isSameAs(bidResponse);
+ }
+
+ @Test
+ public void shouldAddId5SignatureToPassthroughWhenExtIsAbsent() {
+ // given
+ final BidResponse bidResponse = BidResponse.builder().build();
+ final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse);
+ final Id5SignatureBidResponseEnricher enricher =
+ Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger);
+
+ // when
+ final AuctionResponsePayload result = enricher.apply(payload);
+
+ // then
+ final BidResponse enriched = result.bidResponse();
+ assertThat(enriched.getExt()).isNotNull();
+ assertThat(enriched.getExt().getPrebid()).isNotNull();
+ final JsonNode passthrough = enriched.getExt().getPrebid().getPassthrough();
+ assertThat(passthrough).isNotNull();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo("signature");
+ }
+
+ @Test
+ public void shouldMergeId5SignatureWithExistingPassthrough() {
+ // given
+ final ObjectNode existingPassthrough = ObjectMapperProvider.mapper().createObjectNode();
+ existingPassthrough.set("other", ObjectMapperProvider.mapper().createObjectNode()
+ .set("value", TextNode.valueOf("otherValue")));
+ existingPassthrough.set("optable", ObjectMapperProvider.mapper().createObjectNode()
+ .set("existing", TextNode.valueOf("preserved")));
+
+ final ExtBidResponse ext = ExtBidResponse.builder()
+ .prebid(ExtBidResponsePrebid.builder()
+ .passthrough(existingPassthrough)
+ .build())
+ .build();
+ final BidResponse bidResponse = BidResponse.builder().ext(ext).build();
+ final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse);
+ final Id5SignatureBidResponseEnricher enricher =
+ Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger);
+
+ // when
+ final AuctionResponsePayload result = enricher.apply(payload);
+
+ // then
+ final JsonNode passthrough = result.bidResponse().getExt().getPrebid().getPassthrough();
+ assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo("signature");
+ assertThat(passthrough.get("optable").get("existing").asText()).isEqualTo("preserved");
+ assertThat(passthrough.get("other").get("value").asText()).isEqualTo("otherValue");
+ }
+
+ @Test
+ public void shouldPreserveExistingPrebidFieldsWhenAddingPassthrough() {
+ // given
+ final ExtBidResponse ext = ExtBidResponse.builder()
+ .prebid(ExtBidResponsePrebid.builder().build())
+ .build();
+ final BidResponse bidResponse = BidResponse.builder().ext(ext).build();
+ final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse);
+ final Id5SignatureBidResponseEnricher enricher =
+ Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger);
+
+ // when
+ final AuctionResponsePayload result = enricher.apply(payload);
+
+ // then
+ final ExtBidResponsePrebid prebid = result.bidResponse().getExt().getPrebid();
+ assertThat(prebid).isNotNull();
+ assertThat(prebid.getPassthrough().get("optable").get("id5_signature").asText())
+ .isEqualTo("signature");
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java
index 39693661629..ee273761f04 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java
@@ -32,7 +32,7 @@ public class IdsMapperTest {
@BeforeEach
public void setUp() {
- target = new IdsMapper(objectMapper, 0.01);
+ target = new IdsMapper(0.01);
}
@Test
@@ -56,7 +56,8 @@ public void shouldMapBidRequestToAllPossibleIds() {
.doesNotContain(Id.of(Id.APPLE_IDFA, "ifa"))
.contains(Id.of(Id.ID5, "id5_id"))
.contains(Id.of(Id.UTIQ, "utiq_id"))
- .contains(Id.of("c", "test_id"));
+ .contains(Id.of("c", "test_id"))
+ .contains(Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1"));
}
@Test
@@ -71,6 +72,17 @@ public void shouldMapNothing() {
assertThat(ids).isNotNull();
}
+ @Test
+ public void shouldMapIpv6WhenPresent() {
+ final BidRequest bidRequest = givenBidRequestWithEids(Map.of());
+
+ final List ids = target.toIds(bidRequest, Map.of());
+
+ assertThat(ids).isNotNull()
+ .contains(Id.of(Id.EMAIL, "email"))
+ .contains(Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1"));
+ }
+
private BidRequest givenBidRequestWithEids(Map eids) {
final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class);
final ExtUser extUser = ExtUser.builder().build();
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java
index de2c01948fb..51adfda9952 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java
@@ -1,7 +1,12 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
import com.iab.gpp.encoder.GppModel;
import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Regs;
+import com.iab.openrtb.request.User;
+import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -9,12 +14,15 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.prebid.server.auction.gpp.model.GppContext;
import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.App;
import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest;
import org.prebid.server.privacy.gdpr.model.TcfContext;
import org.prebid.server.privacy.model.Privacy;
import org.prebid.server.privacy.model.PrivacyContext;
+import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
+import org.prebid.server.proto.openrtb.ext.request.ExtUser;
import java.util.List;
import java.util.Set;
@@ -42,19 +50,36 @@ public void setUp() {
}
@Test
- public void shouldResolveTcfAttributesWhenConsentIsValid() {
+ public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() {
// given
final GppModel gppModel = mock();
- when(tcfContext.isConsentValid()).thenReturn(true);
- when(tcfContext.isInGdprScope()).thenReturn(true);
- when(tcfContext.getConsentString()).thenReturn("consent");
when(gppModel.encode()).thenReturn("consent");
when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1)));
- final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext);
+ final AuctionContext auctionContext =
+ givenAuctionContext(givenBidRequestWithGdprORTB26(true, "consent"), tcfContext, gppContext);
// when
final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
- auctionContext, properties.getTimeout());
+ auctionContext, properties.getTimeout(), 0.01);
+
+ // then
+ assertThat(result).isNotNull()
+ .returns(true, OptableAttributes::isGdprApplies)
+ .returns("consent", OptableAttributes::getGdprConsent);
+ }
+
+ @Test
+ public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() {
+ // given
+ final GppModel gppModel = mock();
+ when(gppModel.encode()).thenReturn("consent");
+ when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1)));
+ final AuctionContext auctionContext =
+ givenAuctionContext(givenBidRequestWithGdprORTB25(true, "consent"), tcfContext, gppContext);
+
+ // when
+ final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
+ auctionContext, properties.getTimeout(), 0.01);
// then
assertThat(result).isNotNull()
@@ -75,7 +100,7 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() {
// when
final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
- auctionContext, properties.getTimeout());
+ auctionContext, properties.getTimeout(), 0.01);
// then
assertThat(result).isNotNull()
@@ -96,7 +121,7 @@ public void shouldResolveGppAttributes() {
// when
final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
- auctionContext, properties.getTimeout());
+ auctionContext, properties.getTimeout(), 0.01);
// then
assertThat(result).isNotNull()
@@ -105,6 +130,113 @@ public void shouldResolveGppAttributes() {
.returns(Set.of(1), OptableAttributes::getGppSid);
}
+ @Test
+ public void shouldResolveAppWhenAppIsPresent() {
+ // given
+ final com.iab.openrtb.request.App ortbApp = com.iab.openrtb.request.App.builder()
+ .bundle("com.example.app")
+ .ver("1.2.3")
+ .build();
+ final BidRequest bidRequest = BidRequest.builder()
+ .app(ortbApp)
+ .build();
+ final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext);
+
+ // when
+ final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
+ auctionContext, properties.getTimeout(), 0.01);
+
+ // then
+ assertThat(result).isNotNull()
+ .returns(App.of("com.example.app", "1.2.3"), OptableAttributes::getApp);
+ }
+
+ @Test
+ public void shouldNotResolveAppWhenAppIsAbsent() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder().build();
+ final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext);
+
+ // when
+ final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
+ auctionContext, properties.getTimeout(), 0.01);
+
+ // then
+ assertThat(result).isNotNull()
+ .returns(null, OptableAttributes::getApp);
+ }
+
+ @Test
+ public void shouldResolveId5SignatureWhenPresentInUserExtOptable() {
+ // given
+ final BidRequest bidRequest = givenBidRequestWithId5Signature("signature");
+ final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext);
+
+ // when
+ final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
+ auctionContext, properties.getTimeout(), 0.01);
+
+ // then
+ assertThat(result).isNotNull()
+ .returns("signature", OptableAttributes::getId5Signature);
+ }
+
+ @Test
+ public void shouldNotResolveId5SignatureWhenAbsentInUserExtOptable() {
+ // given
+ final BidRequest bidRequest = givenBidRequestWithId5Signature(null);
+ final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext);
+
+ // when
+ final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
+ auctionContext, properties.getTimeout(), 0.01);
+
+ // then
+ assertThat(result).isNotNull()
+ .returns(null, OptableAttributes::getId5Signature);
+ }
+
+ private BidRequest givenBidRequestWithId5Signature(String signature) {
+ final ObjectNode optable = mapper.createObjectNode();
+ if (StringUtils.isNotEmpty(signature)) {
+ optable.set("id5_signature", TextNode.valueOf(signature));
+ }
+
+ final ExtUser extUser = ExtUser.builder().build();
+ extUser.addProperty("optable", optable);
+ final User user = User.builder().ext(extUser).build();
+
+ return BidRequest.builder().user(user).build();
+ }
+
+ private BidRequest givenBidRequestWithGdprORTB26(boolean isGdprEnabled, String consent) {
+ final User user = User.builder()
+ .consent(consent)
+ .build();
+
+ return BidRequest.builder()
+ .user(user)
+ .regs(Regs.builder()
+ .gdpr(isGdprEnabled ? 1 : 0)
+ .build())
+ .build();
+ }
+
+ private BidRequest givenBidRequestWithGdprORTB25(boolean isGdprEnabled, String consent) {
+ final User user = User.builder()
+ .ext(ExtUser.builder()
+ .consent(consent)
+ .build())
+ .build();
+
+ return BidRequest.builder()
+ .user(user)
+ .regs(Regs.builder()
+ .ext(ExtRegs.of(isGdprEnabled ? 1 : 0, null, null, null))
+ .build())
+ .build();
+ }
+
public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) {
return AuctionContext.builder()
.bidRequest(bidRequest)
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java
new file mode 100644
index 00000000000..0f496051b65
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java
@@ -0,0 +1,126 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Site;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PropertiesValidatorTest {
+
+ @Test
+ public void isValidShouldReturnTrueWhenTenantAndOriginArePresent() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setTenant("tenant");
+ properties.setOrigin("origin");
+
+ // when
+ final boolean result = PropertiesValidator.isValid(properties);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void isValidShouldReturnFalseWhenTenantIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setOrigin("origin");
+
+ // when
+ final boolean result = PropertiesValidator.isValid(properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isValidShouldReturnFalseWhenOriginIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setTenant("tenant");
+
+ // when
+ final boolean result = PropertiesValidator.isValid(properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnTrueWhenEnrichWebIsTrueAndSiteIsPresent() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichWeb(true);
+ final BidRequest bidRequest = BidRequest.builder().site(Site.builder().build()).build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnFalseWhenEnrichWebIsTrueAndSiteIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichWeb(true);
+ final BidRequest bidRequest = BidRequest.builder().build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnTrueWhenEnrichAppIsTrueAndAppIsPresent() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichApp(true);
+ final BidRequest bidRequest = BidRequest.builder().app(App.builder().build()).build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnFalseWhenEnrichAppIsTrueAndAppIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichApp(true);
+ final BidRequest bidRequest = BidRequest.builder().build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnFalseWhenBothEnrichWebAndEnrichAppAreFalseOrNull() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichWeb(false);
+ properties.setEnrichApp(false);
+ final BidRequest bidRequest = BidRequest.builder()
+ .site(Site.builder().build())
+ .app(App.builder().build())
+ .build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java
index 0359578ee75..f8eb3a99c36 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java
@@ -1,11 +1,14 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;
import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.modules.optable.targeting.model.App;
import org.prebid.server.hooks.modules.optable.targeting.model.Id;
import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
import org.prebid.server.hooks.modules.optable.targeting.model.Query;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
import java.util.List;
+import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -15,16 +18,21 @@ public class QueryBuilderTest {
private final String idPrefixOrder = "c,c1";
+ private OptableTargetingProperties properties() {
+ return givenProperties(idPrefixOrder, null);
+ }
+
@Test
public void shouldSeparateAttributesFromIds() {
// given
final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123"));
// when
- final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder);
+ final Query query = QueryBuilder.build(ids, optableAttributes, properties());
// then
assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123");
+ assertThat(query.getHid()).isEqualTo("");
assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server");
}
@@ -34,10 +42,11 @@ public void shouldBuildFullQueryString() {
final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123"));
// when
- final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder);
+ final Query query = QueryBuilder.build(ids, optableAttributes, properties());
// then
assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123");
+ assertThat(query.getHid()).isEqualTo("");
assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server");
assertThat(query.toQueryString())
.isEqualTo("&id=e%3Aemail&id=p%3A123&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server");
@@ -49,7 +58,7 @@ public void shouldBuildQueryStringWhenHaveIds() {
final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123"));
// when
- final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString();
+ final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString();
// then
assertThat(query).contains("e%3Aemail", "p%3A123");
@@ -61,7 +70,7 @@ public void shouldBuildQueryStringWithExtraAttributes() {
final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123"));
// when
- final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString();
+ final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString();
// then
assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms");
@@ -77,7 +86,7 @@ public void shouldBuildQueryStringWithRightOrder() {
Id.of("c", "234"));
// when
- final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString();
+ final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString();
// then
assertThat(query).startsWith("&id=c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail");
@@ -92,7 +101,7 @@ public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() {
.build();
// when
- final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder);
+ final Query query = QueryBuilder.build(ids, attributes, properties());
// then
assertThat(query).isNotNull();
@@ -106,12 +115,405 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() {
final OptableAttributes attributes = OptableAttributes.builder().build();
// when
- final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder);
+ final Query query = QueryBuilder.build(ids, attributes, properties());
// then
assertThat(query).isNull();
}
+ @Test
+ public void shouldBuildQueryStringWithGppSid() {
+ // given
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .ips(List.of("8.8.8.8"))
+ .gpp("DBABzw~1YNY~BVQqAAAAAgA")
+ .gppSid(Set.of(7, 22))
+ .build();
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).contains("&gpp=DBABzw~1YNY~BVQqAAAAAgA");
+ final String gppSidValue = query.split("gpp_sid=")[1].split("&")[0];
+ assertThat(gppSidValue.split(",")).containsExactlyInAnyOrder("7", "22");
+ assertThat(query).doesNotContain("Optional");
+ }
+
+ @Test
+ public void shouldBuildQueryStringWithSingleGppSid() {
+ // given
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .ips(List.of("8.8.8.8"))
+ .gpp("DBABzw~1YNY")
+ .gppSid(Set.of(7))
+ .build();
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).contains("&gpp_sid=7");
+ assertThat(query).doesNotContain("Optional");
+ }
+
+ @Test
+ public void shouldLimitGppSidToTwoValues() {
+ // given
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .ips(List.of("8.8.8.8"))
+ .gpp("DBABzw~1YNY~BVQqAAAAAgA")
+ .gppSid(Set.of(5, 7, 22))
+ .build();
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ final String gppSidValue = query.split("gpp_sid=")[1].split("&")[0];
+ assertThat(gppSidValue.split(",")).hasSize(2);
+ assertThat(query).doesNotContain("Optional");
+ }
+
+ @Test
+ public void shouldNotIncludeGppSidWhenEmpty() {
+ // given
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .ips(List.of("8.8.8.8"))
+ .gpp("DBABzw~1YNY")
+ .gppSid(Set.of())
+ .build();
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).doesNotContain("gpp_sid");
+ }
+
+ @Test
+ public void shouldBuildHidWhenHidPrefixesMatchIds() {
+ // given
+ final OptableTargetingProperties props = givenProperties(null, "c,i6");
+ final List ids = List.of(
+ Id.of("c", "234"),
+ Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, optableAttributes, props);
+
+ // then
+ assertThat(query.getHid()).isEqualTo("&hid=c:234&hid=i6:0%3A0%3A0%3A0%3A0%3A0%3A0%3A1");
+ }
+
+ @Test
+ public void shouldExcludeDeviceIpV6FromIdsString() {
+ // given
+ final OptableTargetingProperties props = givenProperties(null, "i6");
+ final List ids = List.of(
+ Id.of(Id.EMAIL, "email"),
+ Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, optableAttributes, props);
+
+ // then
+ assertThat(query.getIds()).doesNotContain(Id.DEVICE_IP_V_6);
+ assertThat(query.getHid()).isEqualTo("&hid=i6:0%3A0%3A0%3A0%3A0%3A0%3A0%3A1");
+ }
+
+ @Test
+ public void shouldUrlEncodeHidValueWithSpecialCharacters() {
+ // given
+ final OptableTargetingProperties props = givenProperties(null, "c");
+ final List ids = List.of(Id.of("c", "a b&c=d+e/f"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, optableAttributes, props);
+
+ // then
+ assertThat(query.getHid()).isEqualTo("&hid=c:a+b%26c%3Dd%2Be%2Ff");
+ }
+
+ @Test
+ public void shouldLeaveHidValueUnchangedForAlphanumericValue() {
+ // given
+ final OptableTargetingProperties props = givenProperties(null, "c");
+ final List ids = List.of(Id.of("c", "abc123"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, optableAttributes, props);
+
+ // then
+ assertThat(query.getHid()).isEqualTo("&hid=c:abc123");
+ }
+
+ @Test
+ public void shouldNotBuildHidWhenNoMatch() {
+ // given
+ final OptableTargetingProperties props = givenProperties(null, "nonexistent");
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, optableAttributes, props);
+
+ // then
+ assertThat(query.getHid()).isEqualTo("");
+ }
+
+ @Test
+ public void shouldNotBuildHidWhenHidPrefixesNotConfigured() {
+ // given
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, optableAttributes, properties());
+
+ // then
+ assertThat(query.getHid()).isEqualTo("");
+ }
+
+ @Test
+ public void shouldAppendBundleAndVerWhenAppHasBoth() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("com.example.app", "1.2.3"))
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).contains("&bundle=com.example.app", "&ver=1.2.3");
+ }
+
+ @Test
+ public void shouldAppendBundleOnlyWhenVerIsEmpty() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("com.example.app", ""))
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).contains("&bundle=com.example.app");
+ assertThat(query).doesNotContain("&ver=");
+ }
+
+ @Test
+ public void shouldNotAppendBundleAndVerWhenBundleIsEmpty() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("", "1.2.3"))
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).doesNotContain("&bundle=", "&ver=");
+ }
+
+ @Test
+ public void shouldNotAppendBundleAndVerWhenAppIsNull() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder().build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).doesNotContain("&bundle=", "&ver=");
+ }
+
+ @Test
+ public void shouldBuildHidAttributesWithBundleAndVerWhenAppIsPresent() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("com.example.app", "1.2.3"))
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes())
+ .isEqualTo("&bundle=com.example.app&ver=1.2.3");
+ }
+
+ @Test
+ public void shouldBuildHidAttributesWithBundleOnlyWhenVerIsEmpty() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("com.example.app", ""))
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes()).isEqualTo("&bundle=com.example.app");
+ }
+
+ @Test
+ public void shouldNotBuildHidAttributesWithBundleWhenBundleIsEmpty() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("", "1.2.3"))
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes()).doesNotContain("&bundle=", "&ver=");
+ }
+
+ @Test
+ public void shouldNotBuildHidAttributesWithBundleWhenAppIsNull() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder().build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes()).isEmpty();
+ }
+
+ @Test
+ public void shouldBuildHidAttributesWithId5SignatureWhenPresent() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .id5Signature("signature")
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes()).isEqualTo("&id5_signature=signature");
+ }
+
+ @Test
+ public void shouldNotBuildHidAttributesWithId5SignatureWhenNull() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder().build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes()).doesNotContain("&id5_signature=");
+ }
+
+ @Test
+ public void shouldIncludeHidAttributesInToQueryString() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("com.example.app", "1.2.3"))
+ .id5Signature("signature")
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final String queryString = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(queryString).endsWith("&bundle=com.example.app&ver=1.2.3&id5_signature=signature");
+ // they live in hidAttributes so that they take part in the cache key, and must not
+ // also be emitted from buildAttributesString
+ assertThat(queryString.split("&bundle=", -1)).hasSize(2);
+ assertThat(queryString.split("&ver=", -1)).hasSize(2);
+ assertThat(queryString.split("&id5_signature=", -1)).hasSize(2);
+ }
+
+ @Test
+ public void shouldTrimWhitespaceAroundHidPrefixes() {
+ // given
+ final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "phone"));
+ // when
+ final Query query = QueryBuilder.build(
+ ids, OptableAttributes.builder().build(), givenProperties(idPrefixOrder, " e , p "));
+ // then
+ assertThat(query.getHid()).isEqualTo("&hid=e:email&hid=p:phone");
+ }
+
+ @Test
+ public void shouldAppendBundleAndVerToHidAttributesWithSpecialCharacters() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .app(App.of("com.example app", "1.2 3"))
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes())
+ .isEqualTo("&bundle=com.example+app&ver=1.2+3");
+ }
+
+ @Test
+ public void shouldAppendId5SignatureToHidAttributesWithSpecialCharacters() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .id5Signature("a b&c=d")
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final Query query = QueryBuilder.build(ids, attributes, properties());
+
+ // then
+ assertThat(query.getHidAttributes()).isEqualTo("&id5_signature=a+b%26c%3Dd");
+ }
+
+ @Test
+ public void shouldAppendId5SignatureWhenPresent() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder()
+ .id5Signature("signature")
+ .build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).contains("&id5_signature=signature");
+ }
+
+ @Test
+ public void shouldNotAppendId5SignatureWhenNull() {
+ // given
+ final OptableAttributes attributes = OptableAttributes.builder().build();
+ final List ids = List.of(Id.of(Id.EMAIL, "email"));
+
+ // when
+ final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString();
+
+ // then
+ assertThat(query).doesNotContain("&id5_signature=");
+ }
+
private OptableAttributes givenOptableAttributes() {
return OptableAttributes.builder()
.timeout(100L)
@@ -119,4 +521,11 @@ private OptableAttributes givenOptableAttributes() {
.gdprConsent("tcf")
.build();
}
+
+ private static OptableTargetingProperties givenProperties(String idPrefixOrder, String hidPrefixes) {
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setIdPrefixOrder(idPrefixOrder);
+ properties.setHidPrefixes(hidPrefixes);
+ return properties;
+ }
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java
index e624fa56a8c..db7542c3b32 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java
@@ -4,6 +4,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.prebid.server.execution.timeout.Timeout;
@@ -13,6 +14,8 @@
import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -170,4 +173,90 @@ public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() {
assertThat(result.getAudience()).isNull();
verify(cache, times(1)).put(any(), eq(targetingResult.result()), anyInt());
}
+
+ @Test
+ public void shouldIncludeHidInCacheKey() {
+ // given
+ final Query query = Query.of("&id=e%3Aemail", "&hid=c:234", "&gdpr=1", "");
+ final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class);
+ when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ target.getTargeting(
+ givenOptableTargetingProperties(true),
+ query,
+ List.of("8.8.8.8"),
+ "user agent",
+ timeout);
+
+ // then
+ final String key = keyCaptor.getValue();
+ assertThat(key).contains(URLEncoder.encode("&hid=c:234", StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void shouldIncludeHidAttributesInCacheKey() {
+ // given
+ final Query query = Query.of("&id=e%3Aemail", "", "&gdpr=1", "&bundle=com.example.app");
+ final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class);
+ when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ target.getTargeting(
+ givenOptableTargetingProperties(true),
+ query,
+ List.of("8.8.8.8"),
+ "user agent",
+ timeout);
+
+ // then
+ final String key = keyCaptor.getValue();
+ assertThat(key).contains(URLEncoder.encode("&bundle=com.example.app", StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void shouldBuildCacheKeyWithAllComponents() {
+ // given
+ final Query query = Query.of("&id=e%3Aemail", "&hid=c:234", "&gdpr=1", "&id5_signature=sig");
+ final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class);
+ when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ target.getTargeting(
+ givenOptableTargetingProperties("key", "accountId", "origin", true),
+ query,
+ List.of("8.8.8.8"),
+ "user agent",
+ timeout);
+
+ // then
+ final String key = keyCaptor.getValue();
+ assertThat(key).isEqualTo(
+ "accountId:origin:8.8.8.8:"
+ + URLEncoder.encode("&id=e%3Aemail", StandardCharsets.UTF_8)
+ + ":"
+ + URLEncoder.encode("&hid=c:234", StandardCharsets.UTF_8)
+ + ":"
+ + URLEncoder.encode("&id5_signature=sig", StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void shouldUseNullInCacheKeyWhenHidIsNull() {
+ // given
+ final Query query = Query.of("&id=e%3Aemail", null, "&gdpr=1", null);
+ final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class);
+ when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ target.getTargeting(
+ givenOptableTargetingProperties(true),
+ query,
+ List.of("8.8.8.8"),
+ "user agent",
+ timeout);
+
+ // then
+ final String key = keyCaptor.getValue();
+ assertThat(key).endsWith(":null:null");
+ }
}
diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml
index d5a938f47ac..93ebd387348 100644
--- a/sample/configs/prebid-config-with-optable.yaml
+++ b/sample/configs/prebid-config-with-optable.yaml
@@ -13,9 +13,21 @@ adapters:
improvedigital:
enabled: true
colossus:
- enabled: true
+ enabled: true
triplelift:
enabled: true
+ adf:
+ enabled: true
+ yieldmo:
+ enabled: true
+ grid:
+ enabled: true
+ unruly:
+ enabled: true
+ epsilon:
+ enabled: true
+ thirtythreeacross:
+ enabled: true
metrics:
prefix: prebid
cache:
@@ -52,4 +64,4 @@ hooks:
enabled: true
modules:
optable-targeting:
- api-endpoint: https://na.edge.optable.co/v2/targeting?t={{TENANT}}&o={{ORIGIN}}
+ api-endpoint: https://na.edge.optable.co/v2/targeting?t={TENANT}&o={ORIGIN}
diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml
index 7a533da3697..58238287f58 100644
--- a/sample/configs/sample-app-settings-optable.yaml
+++ b/sample/configs/sample-app-settings-optable.yaml
@@ -19,8 +19,18 @@ accounts:
modules:
optable-targeting:
api-key: key
- tenant: optable
- origin: web-sdk-demo
+ tenant: prebidtest
+ origin: js-sdk
+ enrichment-percentage: 100
+ bidder-enrichment-percentages:
+ appnexus: 75
+ rubicon: 75
+ pubmatic: 0
+ improvedigital: 50
+ enrich-web: true
+ enrich-app: true
+ id-prefix-order: "e,v,c"
+ hid-prefixes: "a,c,i6"
ppid-mapping: { "pubcid.org": "c" }
adserver-targeting: true
cache:
@@ -31,14 +41,27 @@ accounts:
"endpoints": {
"/openrtb2/auction": {
"stages": {
- "processed-auction-request": {
+ "raw-auction-request": {
"groups": [
{
- "timeout": 600,
+ "timeout": 50,
"hook-sequence": [
{
"module-code": "optable-targeting",
- "hook-impl-code": "optable-targeting-processed-auction-request-hook"
+ "hook-impl-code": "optable-targeting-raw-auction-request-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "bidder-request": {
+ "groups": [
+ {
+ "timeout": 500,
+ "hook-sequence": [
+ {
+ "module-code": "optable-targeting",
+ "hook-impl-code": "optable-targeting-bidder-request-hook"
}
]
}