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 188a4c93115..bd4c24dc3cc 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -13,10 +13,22 @@ Targeting API endpoint is configurable per publisher. ### Execution Plan -This module runs at two stages: +The module provides five hooks: -* Processed Auction Request: to enrich `user.eids` and `user.data`. -* Auction Response: to inject ad server targeting. +* Raw Auction Request: starts the Optable API call early, without blocking the auction (async flow). +* Async Processed Auction Request: starts the call for requests that rely on stored requests (async flow, see below). +* Bidder Request: awaits the API response and enriches each bidder request with `user.eids` and `user.data` (async flow). +* Auction Response: injects ad server targeting. +* Processed Auction Request (legacy): makes the API call and enriches the whole request synchronously, blocking the + auction (legacy flow). + +The async flow requires the Raw Auction Request, Async Processed Auction Request, Bidder Request and Auction Response +hooks. The legacy flow uses only the legacy Processed Auction Request hook (with the Auction Response hook) and needs +no other hooks in the plan. + +Stored requests and stored imps are merged after the Raw Auction Request stage. When the bidders or `site`/`app` are +not known at that stage (f.e. Prebid Mobile SDK traffic, where they live in the stored request), the Async Processed +Auction Request hook starts the call on the merged request instead. Without it such requests are not enriched. We recommend defining the execution plan in the account config so the module is only invoked for specific accounts. See below for an example. @@ -27,10 +39,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: @@ -41,14 +51,40 @@ hooks: "endpoints": { "/openrtb2/auction": { "stages": { + "raw-auction-request": { + "groups": [ + { + "timeout": 50, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-raw-auction-request-hook" + } + ] + } + ] + }, "processed-auction-request": { "groups": [ { - "timeout": 100, + "timeout": 50, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-async-processed-auction-request-hook" + } + ] + } + ] + }, + "bidder-request": { + "groups": [ + { + "timeout": 50, "hook-sequence": [ { "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-processed-auction-request-hook" + "hook-impl-code": "optable-targeting-bidder-request-hook" } ] } @@ -84,6 +120,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" }, @@ -99,24 +144,44 @@ 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 used only the `processed-auction-request` hook (with `auction-response`), which made the API call +and enriched the whole request synchronously, blocking the auction. That hook keeps the legacy synchronous behavior +and requires no configuration changes. + +To migrate to the non-blocking async flow, replace the `processed-auction-request` hook in the execution plan with +the `async-processed-auction-request` hook and add the `raw-auction-request` and `bidder-request` hooks as shown +above. Without the async processed hook, requests that rely on stored requests are not enriched. + ### 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 the budget for the API call started in the `raw-auction-request` or +`async-processed-auction-request` stage. The call runs in parallel with the rest of the auction, so the actual wait +at the `bidder-request` stage is usually much shorter than the full roundtrip. The `raw-auction-request` and +`async-processed-auction-request` hook timeouts only cover validation and 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 +198,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 @@ -172,8 +242,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 @@ -196,6 +266,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 0a3931d8481..0ef3cfb5a59 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 791c6a0d2b0..b59400cc6d4 100644 --- a/extra/modules/optable-targeting/sample-requests/data.json +++ b/extra/modules/optable-targeting/sample-requests/data.json @@ -1,110 +1,269 @@ { - "test": 1, - "id": "1", "imp": [ { - "id": "1", - "banner": { - "w": 300, - "h": 250 - }, "ext": { "prebid": { - "storedauctionresponse": { - "id": "optable-stored-response" - }, "bidder": { - "appnexus": { - "placementId": 0 + "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" } - } + }, + "id": "imp_id", + "banner": { + "topframe": 1, + "format": [ + { + "w": 320, + "h": 50 + } + ] + }, + "bidfloor": 1.0, + "bidfloorcur": "USD" } ], + "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" + ] + }, + "browsers": [ + { + "brand": "Chromium", + "version": [ + "120", + "0", + "0", + "0" + ] + }, + { + "brand": "Not-A.Brand", + "version": [ + "24", + "0", + "0", + "0" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "120", + "0", + "0", + "0" + ] + } + ], + "mobile": 0, + "model": "", + "architecture": "arm" + } + }, "site": { - "domain": "test.com", + "domain": "example-site.com", "publisher": { - "domain": "test.com", + "domain": "example-publisher.com", "id": "1" }, - "page": "https://www.test.com/" - }, - "device": { - "ip": "8.8.8.8" + "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": "growthcode.io", - "uids": [ - { - "id": "fb58593e-7ac6-48bd-b2de-89a758726362", - "atype": 1 - } - ] - }, - { - "source": "pubcid.org", - "uids": [ - { - "id": "test", - "atype": 1 - } - ] - }, - { - "source": "crwdcntrl.net", - "uids": [ - { - "id": "dd1b31e65f5e45548c11a0275ba3a8072c00e3a2a0493e8f5a8f54f8067e8b00", - "atype": 1 - } - ] - }, - { - "source": "amxdt.net", + "source": "id5-sync.com", "uids": [ { - "id": "amx*3*a583802a-e6fe-48d7-87c6-7db1b6a4a73a*70f06cdcf8ab0b4ac07a56860ed0e0b6ef0388dc0b0ab5a1dd725999d3b339cf", - "atype": 1 + "id": "ID5*a7fF5d6f1GZgAUunYu5C3Edu5H4_nlp9aN34PiHotjzCtKWRh-vXq84ijeusCYHD", + "atype": 1, + "ext": { + "linkType": 2, + "pba": "EGg0hDGKtMSoVfdiLRmUPZc+x7Hm8I37s1sAPcxYmWA=" + } } ] }, { - "source": "audigent.com", + "source": "pubmatic.com", "uids": [ { - "id": "f84456cd3c72296d7898f62e1c46dd964206ff4d47e64b690c3c5a1d6b1bd286", - "atype": 1 - } - ] - }, - { - "source": "adnxs.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": { "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..7d6daf992dd 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,28 @@ 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.IpAddressHelper; 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.OptableTargetingAsyncProcessedAuctionRequestHook; 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.OptableTargetingFlowResolver; 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 +51,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 +59,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,22 +99,66 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties); } + @Bean + TargetingRequestExecutor targetingRequestExecutor(OptableTargeting optableTargeting, + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory, + IpAddressHelper ipAddressHelper, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new TargetingRequestExecutor( + optableTargeting, userFpdActivityMask, timeoutFactory, ipAddressHelper, logSamplingRate); + } + @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, - OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, JsonMerger jsonMerger, + OptableTargetingFlowResolver earlyOptableCallResolver, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { return new OptableTargetingModule(List.of( - new OptableTargetingProcessedAuctionRequestHook( + new OptableRawAuctionRequestHook( configResolver, - optableTargeting, - userFpdActivityMask, + earlyOptableCallResolver, logSamplingRate), + new OptableTargetingProcessedAuctionRequestHook( + configResolver, + earlyOptableCallResolver), + new OptableTargetingAsyncProcessedAuctionRequestHook( + configResolver, + earlyOptableCallResolver), + new OptableBidderRequestHook(), new OptableTargetingAuctionResponseHook( configResolver, ObjectMapperProvider.mapper(), jsonMerger))); } + + @Bean + BidderEnrichmentSampler bidderEnrichmentSampler(BidderCatalog bidderCatalog) { + return BidderEnrichmentSampler.of(AliasesResolver.of(bidderCatalog)); + } + + @Bean + OptableTargetingFlowResolver earlyOptableCallResolver( + BidderEnrichmentSampler bidderEnrichmentSampler, + TargetingRequestExecutor targetingRequestExecutor, + @Value("${hooks.host-execution-plan:}") + String hostExecutionPlan, + @Value("${hooks.default-account-execution-plan:}") + String defaultAccountExecutionPlan, + JacksonMapper mapper, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + final CompositeHookExecutionPlan hooksExecutionPlan = CompositeHookExecutionPlan.of( + StringUtils.isNoneEmpty(hostExecutionPlan) + ? mapper.decodeValue(hostExecutionPlan, ExecutionPlan.class) + : null, + StringUtils.isNoneEmpty(defaultAccountExecutionPlan) + ? mapper.decodeValue(defaultAccountExecutionPlan, ExecutionPlan.class) + : null); + + return new OptableTargetingFlowResolver( + bidderEnrichmentSampler, targetingRequestExecutor, hooksExecutionPlan, logSamplingRate); + } } 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..639cd20bc59 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,35 @@ public class ModuleContext { private long optableTargetingExecutionTime; + private long optableTargetingStartTimestamp; + + private Future optableTargetingCall; + + private Set biddersToEnrich; + + private OptableTargetingProperties optableTargetingProperties; + + private boolean shouldSkipEnrichment; + + private boolean isEarlyCallInitializationCompleted = true; + + private String id5Signature; + public static ModuleContext of(AuctionInvocationContext invocationContext) { final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext(); return moduleContext != null ? moduleContext : new ModuleContext(); } + + public void recordTargetingExecutionTime() { + optableTargetingExecutionTime = System.currentTimeMillis() - optableTargetingStartTimestamp; + } + + public void failAndRecordExecutionTime() { + recordTargetingExecutionTime(); + 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..7f03999ad97 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java @@ -0,0 +1,133 @@ +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.BidRequestCleaner; +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(BidRequestCleaner.instance() + .andThen(BidderRequestEnricher.of(targetingResult, properties))::apply, 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.getOptableTargetingStartTimestamp(); + return startTime > 0 ? System.currentTimeMillis() - startTime : 0; + } + + private Future> noAction(ModuleContext moduleContext, + Tags analyticsTags) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(BidRequestCleaner.instance()) + .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..c4ce982aba5 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -0,0 +1,126 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import io.vertx.core.Future; +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.v1.core.AuctionRequestCleaner; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargetingFlowResolver; +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.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; + +public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(OptableRawAuctionRequestHook.class)); + + private static final String PREBID_STORED_REQUEST_PATH = "/prebid/storedrequest"; + + public static final String CODE = "optable-targeting-raw-auction-request-hook"; + + private final ConfigResolver configResolver; + private final OptableTargetingFlowResolver optableTargetingFlowResolver; + private final double logSamplingRate; + + public OptableRawAuctionRequestHook(ConfigResolver configResolver, + OptableTargetingFlowResolver optableTargetingFlowResolver, + double logSamplingRate) { + + this.configResolver = Objects.requireNonNull(configResolver); + this.optableTargetingFlowResolver = optableTargetingFlowResolver; + this.logSamplingRate = logSamplingRate; + } + + @Override + public Future> call(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext) { + + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setOptableTargetingStartTimestamp(System.currentTimeMillis()); + moduleContext.setOptableTargetingProperties(properties); + + if (!PropertiesValidator.isValid(properties)) { + conditionalLogger.error( + "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); + + moduleContext.failAndRecordExecutionTime(); + + return update(AuctionRequestCleaner.instance(), moduleContext); + } + + final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest(); + if (!PropertiesValidator.isTrafficSourceValid(bidRequest, properties) + || hasStoredRequest(bidRequest) + || hasStoredImps(bidRequest)) { + + moduleContext.setEarlyCallInitializationCompleted(false); + + return success(moduleContext); + } + + return optableTargetingFlowResolver.resolveAsyncOptableTargetingFlow( + moduleContext, payload, invocationContext, properties); + } + + private static boolean hasStoredImps(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getImp()) + .stream() + .flatMap(Collection::stream) + .map(Imp::getExt) + .filter(Objects::nonNull) + .anyMatch(impExt -> impExt.at(PREBID_STORED_REQUEST_PATH).isObject()); + } + + private static boolean hasStoredRequest(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getStoredrequest) + .isPresent(); + } + + public static Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } + + public static Future> success(ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .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/OptableTargetingAsyncProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAsyncProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..a8b871a9dbf --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAsyncProcessedAuctionRequestHook.java @@ -0,0 +1,86 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +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.v1.core.AuctionRequestCleaner; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargetingFlowResolver; +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.ProcessedAuctionRequestHook; + +import java.util.Objects; + +public class OptableTargetingAsyncProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + public static final String CODE = "optable-targeting-async-processed-auction-request-hook"; + + private final ConfigResolver configResolver; + + private final OptableTargetingFlowResolver optableTargetingFlowResolver; + + public OptableTargetingAsyncProcessedAuctionRequestHook(ConfigResolver configResolver, + OptableTargetingFlowResolver earlyOptableCallResolver) { + + this.configResolver = Objects.requireNonNull(configResolver); + this.optableTargetingFlowResolver = Objects.requireNonNull(earlyOptableCallResolver); + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final ModuleContext moduleContext = ModuleContext.of(invocationContext); + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + + if (moduleContext.isEarlyCallInitializationCompleted()) { + return success(moduleContext); + } + + if (!PropertiesValidator.isTrafficSourceValid( + invocationContext.auctionContext().getBidRequest(), properties)) { + + moduleContext.setShouldSkipEnrichment(true); + moduleContext.setEarlyCallInitializationCompleted(false); + + return update(AuctionRequestCleaner.instance(), moduleContext); + } + + return optableTargetingFlowResolver.resolveAsyncOptableTargetingFlow( + moduleContext, auctionRequestPayload, invocationContext, properties); + } + + public static Future> success(ModuleContext moduleContext) { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .moduleContext(moduleContext) + .build()); + } + + 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..e650a19affb 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,172 +1,46 @@ 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.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.v1.InvocationAction; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargetingFlowResolver; 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.ProcessedAuctionRequestHook; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.log.LoggerFactory; import java.util.Objects; +/** + * @deprecated This hook is deprecated and will be removed in a future release. + * Use {@link OptableTargetingAsyncProcessedAuctionRequestHook} instead. + */ +@Deprecated public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { - private static final ConditionalLogger conditionalLogger = new ConditionalLogger( - LoggerFactory.getLogger(OptableTargetingProcessedAuctionRequestHook.class)); - public static final String CODE = "optable-targeting-processed-auction-request-hook"; private final ConfigResolver configResolver; - private final OptableTargeting optableTargeting; - private final UserFpdActivityMask userFpdActivityMask; - private final double logSamplingRate; + + private final OptableTargetingFlowResolver optableTargetingFlowResolver; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, - OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, - double logSamplingRate) { + OptableTargetingFlowResolver earlyOptableCallResolver) { this.configResolver = Objects.requireNonNull(configResolver); - this.optableTargeting = Objects.requireNonNull(optableTargeting); - this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); - this.logSamplingRate = logSamplingRate; + this.optableTargetingFlowResolver = Objects.requireNonNull(earlyOptableCallResolver); } @Override public Future> call(AuctionRequestPayload auctionRequestPayload, - AuctionInvocationContext invocationContext) { + AuctionInvocationContext invocationContext) { + final ModuleContext moduleContext = ModuleContext.of(invocationContext); final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); - final ModuleContext moduleContext = new ModuleContext(); - final long callTargetingAPITimestamp = System.currentTimeMillis(); - - if (!isTargetingPropertiesValid(properties)) { - conditionalLogger.error( - "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); - - moduleContext.setOptableTargetingExecutionTime(System.currentTimeMillis() - callTargetingAPITimestamp); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); - return update(BidRequestCleaner.instance(), moduleContext); - } - - final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext); - - final Timeout timeout = getHookTimeout(invocationContext); - final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( - invocationContext.auctionContext(), - properties.getTimeout()); - - return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout) - .compose(targetingResult -> { - moduleContext.setOptableTargetingExecutionTime( - System.currentTimeMillis() - callTargetingAPITimestamp); - return enrichedPayload(targetingResult, moduleContext, properties); - }) - .recover(throwable -> { - moduleContext.setOptableTargetingExecutionTime( - System.currentTimeMillis() - callTargetingAPITimestamp); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); - return update(BidRequestCleaner.instance(), moduleContext); - }); - } - - private boolean isTargetingPropertiesValid(OptableTargetingProperties properties) { - return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); - } - - 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(); - } - - private Timeout getHookTimeout(AuctionInvocationContext invocationContext) { - return invocationContext.timeout(); - } - - private Future> enrichedPayload(TargetingResult targetingResult, - ModuleContext moduleContext, - OptableTargetingProperties properties) { - - moduleContext.setTargeting(targetingResult.getAudience()); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); - return update( - BidRequestCleaner.instance() - .andThen(BidRequestEnricher.of(targetingResult, properties)) - ::apply, - moduleContext); - } - - private static Future> update( - PayloadUpdate payloadUpdate, - ModuleContext moduleContext) { - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.update) - .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) - .payloadUpdate(payloadUpdate) - .moduleContext(moduleContext) - .build()); + return optableTargetingFlowResolver.resolveOptableTargetingFlow( + auctionRequestPayload, invocationContext, moduleContext, properties); } @Override 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/AuctionRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionRequestCleaner.java new file mode 100644 index 00000000000..b49c504e261 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionRequestCleaner.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +public class AuctionRequestCleaner extends BaseRequestCleaner implements PayloadUpdate { + + public static AuctionRequestCleaner instance() { + return new AuctionRequestCleaner(); + } + + @Override + public AuctionRequestPayload apply(AuctionRequestPayload payload) { + return AuctionRequestPayloadImpl.of(clearExtUserOptable(payload.bidRequest())); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BaseRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BaseRequestCleaner.java new file mode 100644 index 00000000000..44ffdbcd553 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BaseRequestCleaner.java @@ -0,0 +1,37 @@ +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.BidRequest; +import com.iab.openrtb.request.User; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.List; + +public abstract class BaseRequestCleaner { + + private static final String OPTABLE_FIELD = "optable"; + private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid", "id5_signature"); + + protected static BidRequest clearExtUserOptable(BidRequest bidRequest) { + final User user = bidRequest.getUser(); + final ExtUser extUser = user != null ? user.getExt() : null; + final JsonNode optable = extUser != null ? extUser.getProperty(OPTABLE_FIELD) : null; + if (optable == null || !optable.isObject() || optable.isEmpty()) { + return bidRequest; + } + + final ObjectNode cleanedOptable = cleanOptable((ObjectNode) optable); + if (cleanedOptable.isEmpty()) { + extUser.addProperty(OPTABLE_FIELD, null); + } else { + extUser.addProperty(OPTABLE_FIELD, cleanedOptable); + } + + return bidRequest; + } + + public static ObjectNode cleanOptable(ObjectNode optable) { + return optable.deepCopy().remove(FIELDS_TO_REMOVE); + } +} 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..f8e85cc4562 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 @@ -1,49 +1,17 @@ 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.BidRequest; -import com.iab.openrtb.request.User; -import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; -import java.util.List; - -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"); +public class BidRequestCleaner extends BaseRequestCleaner implements PayloadUpdate { public static BidRequestCleaner instance() { return new BidRequestCleaner(); } @Override - public AuctionRequestPayload apply(AuctionRequestPayload payload) { - return AuctionRequestPayloadImpl.of(clearExtUserOptable(payload.bidRequest())); - } - - private static BidRequest clearExtUserOptable(BidRequest bidRequest) { - final User user = bidRequest.getUser(); - final ExtUser extUser = user != null ? user.getExt() : null; - final JsonNode optable = extUser != null ? extUser.getProperty(OPTABLE_FIELD) : null; - if (optable == null || !optable.isObject() || optable.isEmpty()) { - return bidRequest; - } - - final ObjectNode cleanedOptable = cleanOptable((ObjectNode) optable); - if (cleanedOptable.isEmpty()) { - extUser.addProperty(OPTABLE_FIELD, null); - } else { - extUser.addProperty(OPTABLE_FIELD, cleanedOptable); - } - - return bidRequest; - } - - public static ObjectNode cleanOptable(ObjectNode optable) { - return optable.deepCopy().remove(FIELDS_TO_REMOVE); + public BidderRequestPayload apply(BidderRequestPayload payload) { + return BidderRequestPayloadImpl.of(clearExtUserOptable(payload.bidRequest())); } } 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..504c590f859 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java @@ -0,0 +1,93 @@ +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.BidRequest; +import com.iab.openrtb.request.Imp; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; +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; +import java.util.stream.Stream; + +@AllArgsConstructor(staticName = "of") +public class BidderEnrichmentSampler { + + private static final String PREBID_BIDDER_PATH = "/prebid/bidder"; + private static final int DEFAULT_ENRICHMENT_PERCENTAGE = 100; + + 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) { + // an explicit null in the account config overrides the defaults of the properties class + final int defaultEnrichmentPercentage = ObjectUtils.defaultIfNull( + optableTargetingProperties.getEnrichmentPercentage(), DEFAULT_ENRICHMENT_PERCENTAGE); + final Map bidderEnrichmentPercentage = MapUtils.emptyIfNull( + 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, + int 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) + .flatMap(BidderEnrichmentSampler::extractImpBidders) + .collect(Collectors.toSet()); + } + + /** + * Follows the core, which at a later stage moves old-style imp.ext.BIDDER params into imp.ext.prebid.bidder + * for the imps that have no bidders there yet. + */ + private static Stream extractImpBidders(ObjectNode impExt) { + final JsonNode prebidBidders = impExt.at(PREBID_BIDDER_PATH); + if (prebidBidders.isObject() && !prebidBidders.isEmpty()) { + return StreamUtil.asStream(prebidBidders.fieldNames()); + } + + return StreamUtil.asStream(impExt.fieldNames()) + .filter(Ortb2ImplicitParametersResolver::isImpExtBidder) + .filter(field -> impExt.get(field).isObject()); + } + + public boolean hasBidders(BidRequest bidRequest) { + return !extractUniqueBidders(bidRequest).isEmpty(); + } +} 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..cd8019a3120 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java @@ -0,0 +1,112 @@ +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.settings.model.Account; +import org.prebid.server.settings.model.AccountHooksConfiguration; + +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_BIDDER_REQUEST = "bidder_request"; + private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = OptableBidderRequestHook.CODE; + + private final boolean hasHostBidderRequestHook; + + private final long hostBidderRequestHookTimeout; + + private final ExecutionPlan defaultAccountExecutionPlan; + + private final ConcurrentHashMap bidderRequestHookCache = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap bidderRequestHookTimeoutCache = new ConcurrentHashMap<>(); + + private CompositeHookExecutionPlan(boolean hasHostBidderRequestHook, + long hostBidderRequestHookTimeout, + ExecutionPlan defaultAccountExecutionPlan) { + + this.hasHostBidderRequestHook = hasHostBidderRequestHook; + this.hostBidderRequestHookTimeout = hostBidderRequestHookTimeout; + this.defaultAccountExecutionPlan = defaultAccountExecutionPlan; + } + + public static CompositeHookExecutionPlan of(ExecutionPlan hostExecutionPlan, + ExecutionPlan defaultAccountExecutionPlan) { + + return new CompositeHookExecutionPlan( + hasHook(hostExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST), + getHookTimeout(hostExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST), + defaultAccountExecutionPlan); + } + + 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 hasBidderRequestHook(Account account) { + return computeFromAccount(account, bidderRequestHookCache, false, + plan -> hasHook(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST) + || hasHostBidderRequestHook); + } + + public long getOptableTargetingBidderRequestTimeout(Account account) { + return computeFromAccount(account, bidderRequestHookTimeoutCache, hostBidderRequestHookTimeout, + plan -> { + final long timeout = getHookTimeout(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST); + return timeout != 0 ? timeout : hostBidderRequestHookTimeout; + }); + } + + private ExecutionPlan resolveExecutionPlan(Account account) { + final ExecutionPlan accountExecutionPlan = Optional.ofNullable(account) + .map(Account::getHooks) + .map(AccountHooksConfiguration::getExecutionPlan) + .orElse(null); + + return accountExecutionPlan != null ? accountExecutionPlan : defaultAccountExecutionPlan; + } + + 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/ConfigResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java index 219f37af2ab..a5ea6bdf0ff 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java @@ -24,6 +24,11 @@ public ConfigResolver(ObjectMapper mapper, JsonMerger jsonMerger, OptableTargeti } public OptableTargetingProperties resolve(ObjectNode configNode) { + // an account may have the hooks in its execution plan without any module config of its own + if (configNode == null) { + return globalProperties; + } + final JsonNode mergedNode = jsonMerger.merge(configNode, globalPropertiesObjectNode); return parse(mergedNode).orElse(globalProperties); } 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..9244ca0c519 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,28 @@ 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 com.networknt.org.apache.commons.validator.routines.InetAddressValidator; import org.apache.commons.collections4.SetUtils; +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.IpAddressHelper; 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.hooks.modules.optable.targeting.v1.OptableTargetingModule; +import org.prebid.server.model.HttpRequestContext; +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; @@ -13,50 +30,126 @@ public class OptableAttributesResolver { + private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For"; + private static final String X_FORWARDED_FOR_HEADER_DELIMITER = ","; + private OptableAttributesResolver() { } - public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { - final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext(); - final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); + public static OptableAttributes resolveAttributes(AuctionContext auctionContext, + BidRequest maskedBidRequest, + Long timeout, + double logSamplingRate, + IpAddressHelper ipAddressHelper) { + + final GppContext.Scope gppScope = Optional.ofNullable(auctionContext.getGppContext()) + .map(GppContext::scope) + .orElse(null); + + final Optional regs = Optional.ofNullable(maskedBidRequest.getRegs()); + final Integer gdpr = regs + .map(Regs::getGdpr) + .orElseGet(() -> regs.map(Regs::getExt) + .map(ExtRegs::getGdpr) + .orElse(null)); + + final Optional user = Optional.ofNullable(maskedBidRequest.getUser()); final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() - .ips(resolveIp(auctionContext)) - .userAgent(resolveUserAgent(auctionContext)) + .ips(resolveIp(auctionContext, maskedBidRequest.getDevice(), ipAddressHelper)) + .userAgent(resolveUserAgent(maskedBidRequest.getDevice())) + .app(resolveApp(maskedBidRequest.getApp())) .timeout(timeout); - if (tcfContext.isConsentValid()) { - builder - .gdprApplies(tcfContext.isInGdprScope()) - .gdprConsent(tcfContext.getConsentString()); + if (gdpr != null && gdpr > 0) { + 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())); } + user.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(); } - public static String resolveUserAgent(AuctionContext auctionContext) { - final Device device = auctionContext.getBidRequest().getDevice(); + public static String resolveUserAgent(Device device) { return device != null ? device.getUa() : null; } - private static List resolveIp(AuctionContext auctionContext) { + private static App resolveApp(com.iab.openrtb.request.App app) { + return app != null ? App.of(app.getBundle(), app.getVer()) : null; + } + + private static List resolveIp(AuctionContext auctionContext, + Device device, IpAddressHelper ipAddressHelper) { + final List result = new ArrayList<>(); - final Optional deviceOpt = Optional.ofNullable(auctionContext.getBidRequest().getDevice()); + final Optional deviceOpt = Optional.ofNullable(device); deviceOpt.map(Device::getIp).ifPresent(result::add); deviceOpt.map(Device::getIpv6).ifPresent(result::add); if (result.isEmpty()) { - Optional.ofNullable(auctionContext.getPrivacyContext().getIpAddress()) - .ifPresent(result::add); + Optional.ofNullable(auctionContext.getHttpRequest()) + .map(HttpRequestContext::getHeaders) + .map(it -> it.get(X_FORWARDED_FOR_HEADER)) + .map(it -> it.split(X_FORWARDED_FOR_HEADER_DELIMITER)) + .filter(it -> it.length > 0) + .map(it -> it[0]) + .ifPresent(ip -> result.add( + maskIp(auctionContext.getBidRequest(), + ip, + ipAddressHelper, + auctionContext.getActivityInfrastructure()))); } return result; } + + private static String maskIp(BidRequest bidRequest, String ip, + IpAddressHelper ipAddressHelper, ActivityInfrastructure activityInfrastructure) { + + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), + bidRequest); + + final boolean disallowTransmitUfpd = + !activityInfrastructure.isAllowed(Activity.TRANSMIT_UFPD, activityInvocationPayload); + if (disallowTransmitUfpd) { + return maskIp(ip, ipAddressHelper); + } + + return ip; + } + + private static String maskIp(String ip, IpAddressHelper ipAddressHelper) { + final boolean isIpv4 = InetAddressValidator.getInstance().isValidInet4Address(ip); + if (isIpv4) { + return ipAddressHelper.maskIpv4(ip); + } + final boolean isIpv6 = InetAddressValidator.getInstance().isValidInet6Address(ip); + if (isIpv6) { + return ipAddressHelper.anonymizeIpv6(ip); + } + + return 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/OptableTargetingFlowResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolver.java new file mode 100644 index 00000000000..a19109a1ad4 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolver.java @@ -0,0 +1,163 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +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.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.TargetingResult; +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.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 OptableTargetingFlowResolver { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(OptableTargetingFlowResolver.class)); + + private static final String AUCTION_NOT_PROPERLY_CONFIGURED = + "Account not properly configured: tenant and/or origin is missing."; + + private final BidderEnrichmentSampler bidderEnrichmentSampler; + private final TargetingRequestExecutor targetingRequestExecutor; + private final CompositeHookExecutionPlan hooksExecutionPlan; + private final double logSamplingRate; + + public OptableTargetingFlowResolver(BidderEnrichmentSampler bidderEnrichmentSampler, + TargetingRequestExecutor targetingRequestExecutor, + CompositeHookExecutionPlan hooksExecutionPlan, + double logSamplingRate) { + + this.bidderEnrichmentSampler = Objects.requireNonNull(bidderEnrichmentSampler); + this.targetingRequestExecutor = Objects.requireNonNull(targetingRequestExecutor); + this.hooksExecutionPlan = hooksExecutionPlan; + this.logSamplingRate = logSamplingRate; + } + + public Future> resolveAsyncOptableTargetingFlow( + ModuleContext moduleContext, + AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + OptableTargetingProperties properties) { + + final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest(); + final Set biddersToEnrich = bidderEnrichmentSampler.sample(bidRequest, properties); + if (CollectionUtils.isEmpty(biddersToEnrich)) { + return update(AuctionRequestCleaner.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(AuctionRequestCleaner.instance(), moduleContext); + } + + /** + * @deprecated This call is deprecated and will be removed in a future release. + */ + @Deprecated + public Future> resolveOptableTargetingFlow( + AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + + final Account account = invocationContext.auctionContext().getAccount(); + if (hooksExecutionPlan.hasBidderRequestHook(account)) { + return updateWithAnalytics(AuctionRequestCleaner.instance(), moduleContext); + } + + return resolvePreEarlyNetworkCall(auctionRequestPayload, invocationContext, moduleContext, properties) + .compose(targetingResult -> { + moduleContext.recordTargetingExecutionTime(); + return enrichPayload(targetingResult, moduleContext, properties); + }) + .recover(throwable -> { + moduleContext.failAndRecordExecutionTime(); + return updateWithAnalytics(AuctionRequestCleaner.instance(), moduleContext); + }); + } + + private Future> enrichPayload( + TargetingResult targetingResult, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + + final PayloadUpdate payloadUpdate = + AuctionRequestCleaner.instance().andThen(BidRequestEnricher.of(targetingResult, properties))::apply; + + return updateWithAnalytics(payloadUpdate, moduleContext); + } + + private Future resolvePreEarlyNetworkCall( + AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + + moduleContext.setOptableTargetingStartTimestamp(System.currentTimeMillis()); + if (!PropertiesValidator.isValid(properties)) { + conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate); + + moduleContext.failAndRecordExecutionTime(); + return Future.failedFuture(AUCTION_NOT_PROPERLY_CONFIGURED); + } + + return targetingRequestExecutor.makeRequest( + payload, + invocationContext, + properties, + null); + } + + private static Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } + + private static Future> updateWithAnalytics( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } +} 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..eb6a9c28bc2 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java @@ -0,0 +1,103 @@ +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.IpAddressHelper; +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 IpAddressHelper ipAddressHelper; + private final double logSamplingRate; + + public TargetingRequestExecutor(OptableTargeting optableTargeting, + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory, + IpAddressHelper ipAddressHelper, + double logSamplingRate) { + + this.optableTargeting = Objects.requireNonNull(optableTargeting); + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + this.timeoutFactory = ObjectUtils.requireNonEmpty(timeoutFactory); + this.logSamplingRate = logSamplingRate; + this.ipAddressHelper = ipAddressHelper; + } + + public Future makeRequest(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + OptableTargetingProperties properties, + Long apiTimeout) { + + final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext); + + final Timeout timeout = apiTimeout == null + ? invocationContext.timeout() + : timeoutFactory.create(invocationContext.timeout().remaining() + apiTimeout); + final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( + invocationContext.auctionContext(), + bidRequest, + properties.getTimeout(), + logSamplingRate, + ipAddressHelper); + + return optableTargeting.getTargeting(properties, bidRequest, attributes, 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..cdd7c24fb72 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 @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; @@ -42,7 +43,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 +54,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(), + CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : null, 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 8e3a057f66b..b4cdcf73d5c 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 @@ -9,15 +9,19 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; 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.collections4.MapUtils; import org.apache.commons.io.IOUtils; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.gpp.model.GppContext; @@ -36,10 +40,13 @@ import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import org.prebid.server.model.HttpRequestContext; 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.ExtUser; +import org.prebid.server.settings.model.Account; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.io.IOException; @@ -71,7 +78,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( @@ -79,7 +110,9 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr GppContext.Regions.builder().build()); return AuctionContext.builder() + .httpRequest(givenHttpRequestContext()) .bidRequest(givenBidRequest()) + .account(account) .activityInfrastructure(activityInfrastructure) .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) .gppContext(gppContext) @@ -87,18 +120,55 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr .build(); } + protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) { + return givenAuctionContext(activityInfrastructure, timeout, null); + } + + protected HttpRequestContext givenHttpRequestContext() { + return givenHttpRequestContext(null); + } + + protected HttpRequestContext givenHttpRequestContext(Map headers) { + return HttpRequestContext.builder() + .headers(givenHeaders(headers)) + .build(); + } + + protected CaseInsensitiveMultiMap givenHeaders(Map headers) { + if (MapUtils.isEmpty(headers)) { + return CaseInsensitiveMultiMap.empty(); + } + + final CaseInsensitiveMultiMap.Builder builder = CaseInsensitiveMultiMap.builder(); + for (Map.Entry entry: headers.entrySet()) { + builder.add(entry.getKey(), entry.getValue()); + } + + return builder.build(); + } + 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,10 +177,48 @@ protected BidRequest givenBidRequestWithUserData(List data) { return BidRequest.builder() .user(givenUserWithData(data)) .device(givenDevice()) + .site(Site.builder().build()) .cur(List.of("USD")) .build(); } + protected static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder()).build(); + } + + protected ObjectNode givenPrebidBidderExt(String... bidders) { + final ObjectNode prebidNode = mapper.createObjectNode(); + prebidNode.set("bidder", givenBidderNode(bidders)); + final ObjectNode ext = mapper.createObjectNode(); + ext.set("prebid", prebidNode); + return ext; + } + + protected ObjectNode givenStoredImpExt() { + final ObjectNode prebidNode = mapper.createObjectNode(); + prebidNode.putObject("storedrequest").put("id", "storedImpId"); + final ObjectNode ext = mapper.createObjectNode(); + ext.set("prebid", prebidNode); + return ext; + } + + protected ObjectNode givenResolvedStoredImpExt(String... bidders) { + final ObjectNode prebidNode = mapper.createObjectNode(); + prebidNode.set("bidder", givenBidderNode(bidders)); + prebidNode.putObject("storedrequest").put("id", "storedImpId"); + final ObjectNode ext = mapper.createObjectNode(); + ext.set("prebid", prebidNode); + return ext; + } + + protected ObjectNode givenBidderNode(String... bidders) { + final ObjectNode bidderNode = mapper.createObjectNode(); + for (String bidder : bidders) { + bidderNode.put(bidder, "value"); + } + return bidderNode; + } + protected BidResponse givenBidResponse() { final ObjectNode targetingNode = mapper.createObjectNode(); targetingNode.set("attribute1", TextNode.valueOf("value1")); @@ -148,17 +256,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() { @@ -235,6 +348,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); @@ -242,6 +356,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..43f0e025b88 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java @@ -0,0 +1,415 @@ +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 shouldCleanBidRequestWhenPerBidderEnrichmentIsDisabled() { + // 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.update, InvocationResult::action); + assertThat(result.moduleContext()).isSameAs(moduleContext); + assertCleanedBidRequest(result); + } + + @Test + public void shouldCleanBidRequestWhenPerBidderEnrichmentIsDisabledAndTargetingCallFailed() { + // 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.update, InvocationResult::action); + assertCleanedBidRequest(result); + 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.update, InvocationResult::action); + assertCleanedBidRequest(result); + 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.update, InvocationResult::action); + assertCleanedBidRequest(result); + assertThat(moduleContext.getId5Signature()).isNull(); + } + + @Test + public void shouldCleanBidRequestWhenBiddersToEnrichIsEmpty() { + // 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.update, InvocationResult::action); + assertCleanedBidRequest(result); + } + + @Test + public void shouldCleanBidRequestWhenBiddersToEnrichIsNull() { + // 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.update, InvocationResult::action); + assertCleanedBidRequest(result); + } + + @Test + public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingStartTimestamp(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.setOptableTargetingStartTimestamp(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.setOptableTargetingStartTimestamp(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.setOptableTargetingStartTimestamp(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 shouldCleanBidRequestWithNoDataOutcomeWhenTargetingResultHasNoUser() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingStartTimestamp(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.update, InvocationResult::action); + assertCleanedBidRequest(result); + + assertThat(moduleContext.getTargeting()).isNull(); + assertAnalyticsTags(result.analyticsTags(), "bidder1", "no-data", "fail"); + } + + @Test + public void shouldCleanBidRequestWithErrorOutcomeWhenTargetingCallFails() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingStartTimestamp(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.update, InvocationResult::action); + assertCleanedBidRequest(result); + + assertAnalyticsTags(result.analyticsTags(), "bidder1", "error", "fail"); + } + + @Test + public void shouldCleanBidRequestWithTimeoutOutcomeWhenTargetingCallTimesOut() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingStartTimestamp(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.update, InvocationResult::action); + assertCleanedBidRequest(result); + + assertAnalyticsTags(result.analyticsTags(), "bidder1", "timeout", "fail"); + } + + private void assertCleanedBidRequest(InvocationResult result) { + final BidRequest cleanedRequest = result + .payloadUpdate() + .apply(BidderRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(cleanedRequest.getUser().getExt().getProperty("optable")).isNull(); + } + + 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..6fb010ad923 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -0,0 +1,331 @@ +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.IpAddressHelper; +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.execution.v1.auction.AuctionRequestPayloadImpl; +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.OptableTargetingFlowResolver; +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.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; + +import java.util.List; +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 IpAddressHelper ipAddressHelper; + @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.maskUser(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + targetingRequestExecutor = new TargetingRequestExecutor( + optableTargeting, userFpdActivityMask, timeoutFactory, ipAddressHelper, 0.01); + target = new OptableRawAuctionRequestHook( + configResolver, givenEarlyOptableCallResolver(), 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); + } + + private OptableTargetingFlowResolver givenEarlyOptableCallResolver() { + return new OptableTargetingFlowResolver( + bidderEnrichmentSampler, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(ExecutionPlan.empty(), null), + 0.01); + } + + @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()) + .onComplete(cxt -> { + vertxTestContext.verify(() -> { + final ModuleContext moduleContext = cxt.result(); + assertThat(moduleContext.getOptableTargetingCall()).isNotNull(); + assertThat(moduleContext.getOptableTargetingCall().result()).isNotNull(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isTrue(); + }); + 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, givenEarlyOptableCallResolver(), 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.isEarlyCallInitializationCompleted()).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, givenEarlyOptableCallResolver(), 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()).isFalse(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + }); + vertxTestContext.completeNow(); + }); + } + + @SneakyThrows + @Test + public void shouldCleanRequestWhenNoBiddersToEnrich(VertxTestContext vertxTestContext) { + + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", "origin", true)); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of()); + + // 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()).isFalse(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.getBiddersToEnrich()).isNull(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isTrue(); + + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + final BidRequest bidRequest = invocationResult.payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getExt().getProperty("optable")).isNull(); + }); + vertxTestContext.completeNow(); + }); + } + + @SneakyThrows + @Test + public void shouldNotInjectEarlyNetworkCallToModuleContextWhenAnyImpIsStoredImp( + VertxTestContext vertxTestContext) { + + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", "origin", true)); + final BidRequest bidRequestWithStoredImp = givenBidRequest(request -> request.imp(List.of( + givenImp(imp -> imp.ext(givenPrebidBidderExt("bidderA"))), + givenImp(imp -> imp.ext(givenStoredImpExt()))))); + when(invocationContext.auctionContext()).thenReturn( + givenAuctionContext(activityInfrastructure, timeout) + .toBuilder() + .bidRequest(bidRequestWithStoredImp) + .build()); + when(auctionRequestPayload.bidRequest()).thenReturn(bidRequestWithStoredImp); + 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()) + .onComplete(cxt -> { + vertxTestContext.verify(() -> { + final ModuleContext moduleContext = cxt.result(); + assertThat(moduleContext.isShouldSkipEnrichment()).isFalse(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.getBiddersToEnrich()).isNull(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + }); + vertxTestContext.completeNow(); + }); + } + + @SneakyThrows + @Test + public void shouldNotInjectEarlyNetworkCallToModuleContextWhenBidRequestReferencesStoredRequest( + VertxTestContext vertxTestContext) { + + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", "origin", true)); + final BidRequest bidRequestWithStoredRequest = givenBidRequest(request -> request + .imp(List.of(givenImp(imp -> imp.ext(givenPrebidBidderExt("bidderA"))))) + .ext(givenStoredRequestExt())); + when(invocationContext.auctionContext()).thenReturn( + givenAuctionContext(activityInfrastructure, timeout) + .toBuilder() + .bidRequest(bidRequestWithStoredRequest) + .build()); + when(auctionRequestPayload.bidRequest()).thenReturn(bidRequestWithStoredRequest); + 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()) + .onComplete(cxt -> { + vertxTestContext.verify(() -> { + final ModuleContext moduleContext = cxt.result(); + assertThat(moduleContext.isShouldSkipEnrichment()).isFalse(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.getBiddersToEnrich()).isNull(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); + assertThat(invocationResult.payloadUpdate()).isNull(); + }); + vertxTestContext.completeNow(); + }); + } + + private ExtRequest givenStoredRequestExt() { + return ExtRequest.of(ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of("storedRequestId")) + .build()); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAsyncProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAsyncProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..654904f91e1 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAsyncProcessedAuctionRequestHookTest.java @@ -0,0 +1,320 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +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.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.IpAddressHelper; +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.execution.v1.auction.AuctionRequestPayloadImpl; +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.OptableTargetingFlowResolver; +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.model.openrtb.TargetingResult; +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.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; + +@ExtendWith(MockitoExtension.class) +class OptableTargetingAsyncProcessedAuctionRequestHookTest extends BaseOptableTest { + + private ConfigResolver configResolver; + + @Mock + private OptableTargeting optableTargeting; + + @Mock(strictness = Mock.Strictness.LENIENT) + private UserFpdActivityMask userFpdActivityMask; + + @Mock(strictness = Mock.Strictness.LENIENT) + private IpAddressHelper ipAddressHelper; + + @Mock + private AuctionRequestPayload auctionRequestPayload; + + @Mock(strictness = Mock.Strictness.LENIENT) + private AuctionInvocationContext invocationContext; + + @Mock(strictness = Mock.Strictness.LENIENT) + private ActivityInfrastructure activityInfrastructure; + + @Mock(strictness = Mock.Strictness.LENIENT) + private Timeout timeout; + + @Mock(strictness = Mock.Strictness.LENIENT) + private TimeoutFactory timeoutFactory; + + @Mock(strictness = Mock.Strictness.LENIENT) + private BidderEnrichmentSampler bidderEnrichmentSampler; + + private TargetingRequestExecutor targetingRequestExecutor; + + private OptableTargetingAsyncProcessedAuctionRequestHook target; + + @BeforeEach + void setUp() { + when(userFpdActivityMask.maskUser(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + targetingRequestExecutor = new TargetingRequestExecutor( + optableTargeting, userFpdActivityMask, timeoutFactory, ipAddressHelper, 0.01); + target = new OptableTargetingAsyncProcessedAuctionRequestHook( + configResolver, givenFlowResolver()); + + when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); + 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); + } + + private OptableTargetingFlowResolver givenFlowResolver() { + return new OptableTargetingFlowResolver( + bidderEnrichmentSampler, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(ExecutionPlan.empty(), null), + 0.01); + } + + @Test + void codeShouldReturnRightCode() { + // when and then + assertThat(target.code()).isEqualTo("optable-targeting-async-processed-auction-request-hook"); + } + + @Test + void callShouldReturnResultWithNoActionWhenModuleContextIsAbsent() { + // given + when(invocationContext.moduleContext()).thenReturn(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.no_action, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); + assertThat(result.payloadUpdate()).isNull(); + } + + @Test + void callShouldReturnResultWithNoActionWhenEarlyOptableCallIsInitialized() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyCallInitializationCompleted(true); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + 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.no_action, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); + assertThat(result.payloadUpdate()).isNull(); + assertThat(moduleContext.getOptableTargetingCall()).isNotNull(); + assertThat(moduleContext.getOptableTargetingCall().succeeded()).isTrue(); + } + + @Test + void callShouldRetryEarlyNetworkCallInitializationWhenItWasNotCompletedOnRawAuctionRequestHookStep() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyCallInitializationCompleted(false); + + final TargetingResult targetingResult = givenTargetingResult(); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(targetingResult)); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of("bidder")); + + // 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(); + assertThat(result.analyticsTags()).isNull(); + assertThat(moduleContext.getBiddersToEnrich()).containsExactly("bidder"); + assertThat(moduleContext.getOptableTargetingCall()).isNotNull(); + assertThat(moduleContext.getOptableTargetingCall().succeeded()).isTrue(); + assertThat(moduleContext.getOptableTargetingCall().result()).isSameAs(targetingResult); + + final BidRequest bidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getEids()).isNull(); + assertThat(bidRequest.getUser().getData()).isNull(); + } + + @Test + void callShouldCleanRequestWhenNoBiddersToEnrich() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyCallInitializationCompleted(false); + + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of()); + + // 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(); + assertThat(result.payloadUpdate()).isNotNull(); + final ObjectNode optable = (ObjectNode) result.payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest() + .getUser().getExt().getProperty("optable"); + assertThat(optable).isNull(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + } + + @Test + void callShouldSkipEnrichmentAndCleanRequestWhenTrafficSourceIsInvalid() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyCallInitializationCompleted(false); + + final BidRequest bidRequestWithoutTrafficSource = givenBidRequest( + bidRequestCustomizer -> bidRequestCustomizer.site(null).app(null)); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.auctionContext()).thenReturn( + givenAuctionContext(activityInfrastructure, timeout, Account.builder().id("accountId").build()) + .toBuilder() + .bidRequest(bidRequestWithoutTrafficSource) + .build()); + + // 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(); + assertThat(result.payloadUpdate()).isNotNull(); + assertThat(moduleContext.isShouldSkipEnrichment()).isTrue(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + assertThat(moduleContext.getBiddersToEnrich()).isNull(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + + final BidRequest bidRequest = result.payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getExt().getProperty("optable")).isNull(); + } + + @Test + void callShouldRetryEarlyNetworkCallInitializationWhenResolvedRequestStillHasStoredImps() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyCallInitializationCompleted(false); + + final TargetingResult targetingResult = givenTargetingResult(); + final BidRequest bidRequestWithResolvedStoredImp = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenResolvedStoredImpExt("bidderA")))))); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.auctionContext()).thenReturn( + givenAuctionContext(activityInfrastructure, timeout, Account.builder().id("accountId").build()) + .toBuilder() + .bidRequest(bidRequestWithResolvedStoredImp) + .build()); + when(auctionRequestPayload.bidRequest()).thenReturn(bidRequestWithResolvedStoredImp); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(targetingResult)); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of("bidder")); + + // 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(); + assertThat(moduleContext.getBiddersToEnrich()).containsExactly("bidder"); + assertThat(moduleContext.getOptableTargetingCall()).isNotNull(); + assertThat(moduleContext.getOptableTargetingCall().succeeded()).isTrue(); + assertThat(moduleContext.getOptableTargetingCall().result()).isSameAs(targetingResult); + } + + private ObjectNode givenAccountConfig(boolean cacheEnabled) { + return givenAccountConfig("key", "tenant", "origin", cacheEnabled); + } +} 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 8d4d688b16c..a73c0f3fa10 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; @@ -53,9 +59,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 = @@ -66,14 +73,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 @@ -82,7 +126,8 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)))); + 1)), + Future.succeededFuture(givenEmptyTargetingResult()))); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -116,14 +161,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); + final ConfigResolver 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 = @@ -136,6 +408,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); + final ConfigResolver 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..5982fef25cd 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,85 +2,125 @@ 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.IpAddressHelper; 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.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.OptableTargetingFlowResolver; +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; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.when; +@SuppressWarnings("deprecation") @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(strictness = Mock.Strictness.LENIENT) + private IpAddressHelper ipAddressHelper; @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; + + @Mock(strictness = Mock.Strictness.LENIENT) + private BidderEnrichmentSampler bidderEnrichmentSampler; + + private TargetingRequestExecutor targetingRequestExecutor; + + private OptableTargetingProcessedAuctionRequestHook target; + @BeforeEach - public void setUp() { + void setUp() { + when(userFpdActivityMask.maskUser(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + targetingRequestExecutor = new TargetingRequestExecutor( + optableTargeting, userFpdActivityMask, timeoutFactory, ipAddressHelper, 0.01); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + configResolver, givenFlowResolver(ExecutionPlan.empty())); 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); } + private OptableTargetingFlowResolver givenFlowResolver(ExecutionPlan executionPlan) { + return new OptableTargetingFlowResolver( + bidderEnrichmentSampler, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(executionPlan, null), + 0.01); + } + @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,21 +135,100 @@ 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())) + .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 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 + 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 callShouldReturnResultWithoutEnrichedBidRequestWhenBidderRequestHookIsPresent() { + // given + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, + givenFlowResolver(givenExecutionPlan(true))); + when(invocationContext.moduleContext()).thenReturn(new ModuleContext()); + // when final Future> future = target.call(auctionRequestPayload, invocationContext); @@ -119,30 +238,27 @@ 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()).isNull(); + assertThat(bidRequest.getUser().getData()).isNull(); } @Test - public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { + void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { // given configResolver = new ConfigResolver( mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, false)); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + configResolver, givenFlowResolver(ExecutionPlan.empty())); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", "tenant", null, true)); @@ -155,26 +271,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, givenFlowResolver(ExecutionPlan.empty())); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", null, null, true)); @@ -187,18 +300,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 +324,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 +338,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 +352,27 @@ 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(); } 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 hasBidderRequestHook) { + final HookId bidderRequestHook = HookId.of("optable-targeting", "optable-targeting-bidder-request-hook"); + + final StageExecutionPlan bidderRequestStage = StageExecutionPlan.of(List.of( + ExecutionGroup.of(null, hasBidderRequestHook ? List.of(bidderRequestHook) : List.of()) + )); + + final EndpointExecutionPlan endpointExecutionPlan = EndpointExecutionPlan.of(Map.of( + 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/AuctionRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionRequestCleanerTest.java new file mode 100644 index 00000000000..5e053f4ec94 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionRequestCleanerTest.java @@ -0,0 +1,57 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AuctionRequestCleanerTest extends BaseOptableTest { + + @Test + public void shouldRemoveUserExtOptableTag() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> + bidRequest.user(givenUser()))); + + // when + final AuctionRequestPayload result = AuctionRequestCleaner.instance().apply(auctionRequestPayload); + + // then + assertThat(result).extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getExt) + .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 = AuctionRequestCleaner.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/BidRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java deleted file mode 100644 index b6d72748fbd..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.User; -import org.junit.jupiter.api.Test; -import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; -import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; -import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; - -import static org.assertj.core.api.Assertions.assertThat; - -public class BidRequestCleanerTest extends BaseOptableTest { - - @Test - public void shouldRemoveUserExtOptableTag() { - // given - final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> - bidRequest.user(givenUser()))); - - // when - final AuctionRequestPayload result = BidRequestCleaner.instance().apply(auctionRequestPayload); - - // then - assertThat(result).extracting(AuctionRequestPayload::bidRequest) - .extracting(BidRequest::getUser) - .extracting(User::getExt) - .extracting(it -> it.getProperty("optable")) - .isEqualTo(null); - } -} 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..42c1090ae47 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java @@ -0,0 +1,365 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +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.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 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 sampleShouldReturnEmptySetWhenImpExtHasNoBidderNodes() { + // 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(givenPrebidBidderExt()))))); + + // 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(givenPrebidBidderExt("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(givenPrebidBidderExt("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(givenPrebidBidderExt("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(givenPrebidBidderExt("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(givenPrebidBidderExt("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(givenPrebidBidderExt("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(givenPrebidBidderExt("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(givenPrebidBidderExt("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 = givenPrebidBidderExt("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"); + } + + @Test + public void sampleShouldReturnOldStyleImpExtBiddersWhenPrebidBidderNodeIsAbsent() { + // 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(givenOldStyleExt("bidderA", "bidderB")))))); + + // when + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); + + // then + assertThat(result).containsExactlyInAnyOrder("bidderA", "bidderB"); + } + + @Test + public void sampleShouldIgnoreOldStyleImpExtBiddersWhenImpHasPrebidBidderNode() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(99); + + final ObjectNode ext = givenPrebidBidderExt("bidderA"); + ext.putObject("bidderB").put("param", "value"); + 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).containsExactly("bidderA"); + } + + @Test + public void sampleShouldFallBackToOldStyleImpExtBiddersWhenPrebidBidderNodeIsEmpty() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(99); + + final ObjectNode ext = givenPrebidBidderExt(); + ext.putObject("bidderB").put("param", "value"); + 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).containsExactly("bidderB"); + } + + @Test + public void sampleShouldCombinePrebidAndOldStyleBiddersOfDifferentImps() { + // 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(givenPrebidBidderExt("bidderA"))), + givenImp(imp -> imp.ext(givenOldStyleExt("bidderB")))))); + + // when + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); + + // then + assertThat(result).containsExactlyInAnyOrder("bidderA", "bidderB"); + } + + @Test + public void sampleShouldNotTreatReservedOrNonObjectImpExtFieldsAsBidders() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(99); + + final ObjectNode ext = givenOldStyleExt("bidderA", "context", "data", "skadn", "all", "general"); + ext.put("gpid", "gpid"); + ext.put("tid", "tid"); + ext.putObject("prebid").putObject("storedrequest").put("id", "id"); + 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).containsExactly("bidderA"); + assertThat(target.hasBidders(bidRequest)).isTrue(); + } + + @Test + public void sampleShouldEnrichAllBiddersWhenPercentagesAreNull() { + // 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(givenPrebidBidderExt("bidderA")))))); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setEnrichmentPercentage(null); + properties.setBidderEnrichmentPercentages(null); + + // when + final Set result = target.sample(bidRequest, properties); + + // 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 givenOldStyleExt(String... bidders) { + final ObjectNode ext = mapper.createObjectNode(); + for (String bidder : bidders) { + ext.putObject(bidder).put("param", "value"); + } + return ext; + } +} 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..c8b70f47a23 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java @@ -0,0 +1,325 @@ +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 hasBidderRequestHookShouldReturnTrueWhenHostPlanHasHook() { + // given + final ExecutionPlan hostPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + 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, null); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isTrue(); + } + + @Test + public void hasBidderRequestHookShouldReturnTrueWhenDefaultAccountPlanHasHookAndAccountHasNoOwnPlan() { + // given + final ExecutionPlan defaultAccountPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, defaultAccountPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isTrue(); + } + + @Test + public void hasBidderRequestHookShouldNotFallBackToDefaultAccountPlanWhenAccountHasOwnPlan() { + // given + final ExecutionPlan defaultAccountPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final ExecutionPlan accountPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, defaultAccountPlan); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnTrueWhenBothPlansHaveHook() { + // given + final ExecutionPlan hostPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final ExecutionPlan accountPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + 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, null); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnFalseWhenAccountIsNull() { + // given + final ExecutionPlan hostPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + + // when and then + assertThat(target.hasBidderRequestHook(null)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnFalseWhenAccountIdIsEmpty() { + // given + final ExecutionPlan hostPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + final Account account = Account.builder().id("").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnHostFlagWhenAccountHasNoHooksConfig() { + // given + final ExecutionPlan hostPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + 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, null); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isTrue(); + assertThat(target.hasBidderRequestHook(account)).isTrue(); + } + + @Test + public void hasBidderRequestHookShouldReturnFalseWhenOnlyRawAuctionRequestHookIsInHostPlan() { + // given + final ExecutionPlan hostPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isFalse(); + } + + @Test + public void getBidderRequestTimeoutShouldReturnHostTimeoutWhenConfigured() { + // given + final ExecutionPlan hostPlan = givenExecutionPlanWithTimeout( + "bidder_request", + "optable-targeting-bidder-request-hook", + 500L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(500L); + } + + @Test + public void getBidderRequestTimeoutShouldReturnAccountTimeoutWhenAccountPlanOverrides() { + // given + final ExecutionPlan hostPlan = 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(hostPlan, null); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(200L); + } + + @Test + public void getBidderRequestTimeoutShouldReturnDefaultAccountPlanTimeoutWhenAccountHasNoOwnPlan() { + // given + final ExecutionPlan defaultAccountPlan = givenExecutionPlanWithTimeout( + "bidder_request", + "optable-targeting-bidder-request-hook", + 500L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, defaultAccountPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(500L); + } + + @Test + public void getBidderRequestTimeoutShouldReturnAccountTimeoutWhenBothAccountAndDefaultAccountPlansHaveTimeout() { + // given + final ExecutionPlan defaultAccountPlan = 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(null, defaultAccountPlan); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(200L); + } + + @Test + public void getBidderRequestTimeoutShouldFallbackToHostWhenDefaultAccountPlanHasNoTimeout() { + // given + final ExecutionPlan hostPlan = givenExecutionPlanWithTimeout( + "bidder_request", + "optable-targeting-bidder-request-hook", + 300L); + final ExecutionPlan defaultAccountPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, defaultAccountPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(300L); + } + + @Test + public void getBidderRequestTimeoutShouldFallbackToHostWhenAccountPlanHasNoTimeout() { + // given + final ExecutionPlan hostPlan = 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(hostPlan, null); + 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, null); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(0L); + } + + @Test + public void getBidderRequestTimeoutShouldReturnHostTimeoutWhenAccountIsNull() { + // given + final ExecutionPlan hostPlan = givenExecutionPlanWithTimeout( + "bidder_request", + "optable-targeting-bidder-request-hook", + 400L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + + // when and then + assertThat(target.getOptableTargetingBidderRequestTimeout(null)).isEqualTo(400L); + } + + @Test + public void getBidderRequestTimeoutShouldReturnHostTimeoutWhenAccountIdIsEmpty() { + // given + final ExecutionPlan hostPlan = givenExecutionPlanWithTimeout( + "bidder_request", + "optable-targeting-bidder-request-hook", + 150L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(hostPlan, null); + 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, 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/ConfigResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolverTest.java new file mode 100644 index 00000000000..ca32067fb50 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolverTest.java @@ -0,0 +1,41 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConfigResolverTest extends BaseOptableTest { + + private OptableTargetingProperties globalProperties; + + private ConfigResolver target; + + @BeforeEach + public void setUp() { + globalProperties = givenOptableTargetingProperties(false); + target = new ConfigResolver(mapper, jsonMerger, globalProperties); + } + + @Test + public void resolveShouldReturnGlobalPropertiesWhenAccountHasNoModuleConfig() { + // when and then + assertThat(target.resolve(null)).isSameAs(globalProperties); + } + + @Test + public void resolveShouldPreferAccountPropertiesOverGlobalOnes() { + // given + final ObjectNode accountConfig = mapper.createObjectNode().put("tenant", "accountTenant"); + + // when + final OptableTargetingProperties result = target.resolve(accountConfig); + + // then + assertThat(result.getTenant()).isEqualTo("accountTenant"); + assertThat(result.getOrigin()).isEqualTo(globalProperties.getOrigin()); + } +} 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..653792fb57a 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,25 +1,40 @@ 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.Device; +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; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.IpAddressHelper; 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.Map; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -27,34 +42,60 @@ @ExtendWith(MockitoExtension.class) public class OptableAttributesResolverTest extends BaseOptableTest { + private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For"; + @Mock(strictness = LENIENT) private TcfContext tcfContext; @Mock(strictness = LENIENT) private GppContext gppContext; + @Mock(strictness = LENIENT) + private ActivityInfrastructure activityInfrastructure; + + @Mock(strictness = LENIENT) + private IpAddressHelper ipAddressHelper; + @Mock private OptableTargetingProperties properties; @BeforeEach public void setUp() { when(properties.getTimeout()).thenReturn(100L); + when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); } @Test - public void shouldResolveTcfAttributesWhenConsentIsValid() { + public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() { + // given + final GppModel gppModel = mock(); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final BidRequest bidRequest = givenBidRequestWithGdprORTB26(true, "consent"); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(true, OptableAttributes::isGdprApplies) + .returns("consent", OptableAttributes::getGdprConsent); + } + + @Test + public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { // 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 BidRequest bidRequest = givenBidRequestWithGdprORTB25(true, "consent"); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); // then assertThat(result).isNotNull() @@ -68,20 +109,19 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { final GppModel gppModel = mock(); when(tcfContext.isConsentValid()).thenReturn(false); when(tcfContext.getConsentString()).thenReturn("consent"); - when(tcfContext.getIpAddress()).thenReturn("8.8.8.8"); when(gppModel.encode()).thenReturn("consent"); when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); - final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); + final BidRequest bidRequest = givenBidRequest(); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); // then assertThat(result).isNotNull() .returns(false, OptableAttributes::isGdprApplies) - .returns(null, OptableAttributes::getGdprConsent) - .returns(List.of("8.8.8.8"), OptableAttributes::getIps); + .returns(null, OptableAttributes::getGdprConsent); } @Test @@ -92,11 +132,12 @@ public void shouldResolveGppAttributes() { 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 BidRequest bidRequest = givenBidRequest(); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); // then assertThat(result).isNotNull() @@ -105,11 +146,269 @@ public void shouldResolveGppAttributes() { .returns(Set.of(1), OptableAttributes::getGppSid); } - public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { + @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, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // 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, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // 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, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // 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, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(null, OptableAttributes::getId5Signature); + } + + @Test + public void shouldResolveIpsFromDeviceWhenDeviceIpsArePresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ip("1.2.3.4").ipv6("2001:db8::1").build()) + .build(); + final AuctionContext auctionContext = + givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(List.of("1.2.3.4", "2001:db8::1"), OptableAttributes::getIps); + } + + @Test + public void shouldResolveIpFromXForwardedForHeaderWhenDeviceIpsAreAbsent() { + // given + final BidRequest bidRequest = givenBidRequest(); + final AuctionContext auctionContext = + givenAuctionContextWithHeaders(bidRequest, Map.of(X_FORWARDED_FOR_HEADER, "1.2.3.4")); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(List.of("1.2.3.4"), OptableAttributes::getIps); + } + + @Test + public void shouldResolveFirstIpFromXForwardedForHeaderWhenItContainsMultipleIps() { + // given + final BidRequest bidRequest = givenBidRequest(); + final AuctionContext auctionContext = + givenAuctionContextWithHeaders(bidRequest, Map.of(X_FORWARDED_FOR_HEADER, "1.2.3.4, 5.6.7.8")); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(List.of("1.2.3.4"), OptableAttributes::getIps); + } + + @Test + public void shouldNotMaskIpFromXForwardedForHeaderWhenTransmitUfpdIsAllowed() { + // given + final BidRequest bidRequest = givenBidRequest(); + final AuctionContext auctionContext = + givenAuctionContextWithHeaders(bidRequest, Map.of(X_FORWARDED_FOR_HEADER, "1.2.3.4")); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(List.of("1.2.3.4"), OptableAttributes::getIps); + } + + @Test + public void shouldMaskIpv4FromXForwardedForHeaderWhenTransmitUfpdIsNotAllowed() { + // given + when(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).thenReturn(false); + when(ipAddressHelper.maskIpv4("1.2.3.4")).thenReturn("1.2.3.0"); + final BidRequest bidRequest = givenBidRequest(); + final AuctionContext auctionContext = + givenAuctionContextWithHeaders(bidRequest, Map.of(X_FORWARDED_FOR_HEADER, "1.2.3.4")); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(List.of("1.2.3.0"), OptableAttributes::getIps); + } + + @Test + public void shouldAnonymizeIpv6FromXForwardedForHeaderWhenTransmitUfpdIsNotAllowed() { + // given + when(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).thenReturn(false); + when(ipAddressHelper.anonymizeIpv6("2001:db8::1")).thenReturn("2001:db8::"); + final BidRequest bidRequest = givenBidRequest(); + final AuctionContext auctionContext = + givenAuctionContextWithHeaders(bidRequest, Map.of(X_FORWARDED_FOR_HEADER, "2001:db8::1")); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, bidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(List.of("2001:db8::"), OptableAttributes::getIps); + } + + @Test + public void shouldResolveIpsFromMaskedDeviceRatherThanFromAuctionContextBidRequest() { + // given + final BidRequest rawBidRequest = BidRequest.builder() + .device(Device.builder().ip("1.2.3.4").ipv6("2001:db8::1").ua("userAgent").build()) + .build(); + final AuctionContext auctionContext = givenAuctionContext(rawBidRequest, tcfContext, gppContext); + final BidRequest maskedBidRequest = rawBidRequest.toBuilder() + .device(Device.builder().ip("1.2.3.0").ua("userAgent").build()) + .build(); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, maskedBidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // then + assertThat(result).isNotNull() + .returns(List.of("1.2.3.0"), OptableAttributes::getIps) + .returns("userAgent", OptableAttributes::getUserAgent); + } + + @Test + public void shouldNotResolveId5SignatureWhenMaskedUserExtIsDropped() { + // given + final BidRequest rawBidRequest = givenBidRequestWithId5Signature("signature"); + final AuctionContext auctionContext = givenAuctionContext(rawBidRequest, tcfContext, gppContext); + final BidRequest maskedBidRequest = rawBidRequest.toBuilder() + .user(null) + .build(); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, maskedBidRequest, properties.getTimeout(), 0.01, ipAddressHelper); + + // 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) .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) .gppContext(gppContext) .build(); } + + public AuctionContext givenAuctionContextWithHeaders(BidRequest bidRequest, Map headers) { + return AuctionContext.builder() + .bidRequest(bidRequest) + .httpRequest(givenHttpRequestContext(headers)) + .activityInfrastructure(activityInfrastructure) + .build(); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolverTest.java new file mode 100644 index 00000000000..08653253f9c --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolverTest.java @@ -0,0 +1,123 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +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.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.model.ExecutionPlan; +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.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +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.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableTargetingFlowResolverTest extends BaseOptableTest { + + @Mock + private BidderEnrichmentSampler bidderEnrichmentSampler; + + @Mock + private TargetingRequestExecutor targetingRequestExecutor; + + @Mock + private AuctionRequestPayload auctionRequestPayload; + + @Mock + private AuctionInvocationContext invocationContext; + + private OptableTargetingFlowResolver target; + + @BeforeEach + public void setUp() { + target = new OptableTargetingFlowResolver( + bidderEnrichmentSampler, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(ExecutionPlan.empty(), null), + 0.01); + } + + @Test + public void resolveAsyncOptableTargetingFlowShouldProceedWhenBidRequestIsValid() { + // given + final BidRequest bidRequest = givenBidRequest(request -> request.imp(List.of( + givenImp(imp -> imp.ext(givenPrebidBidderExt("bidderA")))))); + givenInvocationContext(bidRequest); + final Future targetingResultFuture = Future.succeededFuture(givenTargetingResult()); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of("bidder")); + when(targetingRequestExecutor.makeRequest(any(), any(), any(), any())).thenReturn(targetingResultFuture); + final ModuleContext moduleContext = new ModuleContext(); + + // when + final Future> future = target.resolveAsyncOptableTargetingFlow( + moduleContext, auctionRequestPayload, invocationContext, givenOptableTargetingProperties(false)); + + // 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(); + assertThat(result.payloadUpdate()).isNotNull(); + assertThat(moduleContext.getBiddersToEnrich()).containsExactly("bidder"); + assertThat(moduleContext.getOptableTargetingCall()).isSameAs(targetingResultFuture); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isTrue(); + } + + @Test + public void resolveAsyncOptableTargetingFlowShouldCleanRequestWhenNoBiddersToEnrich() { + // given + final BidRequest bidRequest = givenBidRequest(request -> request.imp(List.of( + givenImp(imp -> imp.ext(givenPrebidBidderExt("bidderA")))))); + givenInvocationContext(bidRequest); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of()); + final ModuleContext moduleContext = new ModuleContext(); + + // when + final Future> future = target.resolveAsyncOptableTargetingFlow( + moduleContext, auctionRequestPayload, invocationContext, givenOptableTargetingProperties(false)); + + // 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(); + assertThat(result.payloadUpdate()).isNotNull(); + assertThat(moduleContext.getBiddersToEnrich()).isNull(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isTrue(); + + final AuctionRequestPayload cleanedPayload = result.payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequestWithUser(givenUser()))); + assertThat(cleanedPayload.bidRequest().getUser().getExt().getProperty("optable")).isNull(); + } + + private void givenInvocationContext(BidRequest bidRequest) { + when(invocationContext.auctionContext()).thenReturn( + AuctionContext.builder() + .bidRequest(bidRequest) + .account(Account.builder().id("accountId").build()) + .build()); + } +} 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..9af83a04d31 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://ca.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..c7d1f9d381e 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,6 +41,19 @@ accounts: "endpoints": { "/openrtb2/auction": { "stages": { + "raw-auction-request": { + "groups": [ + { + "timeout": 50, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-raw-auction-request-hook" + } + ] + } + ] + }, "processed-auction-request": { "groups": [ { @@ -38,7 +61,20 @@ accounts: "hook-sequence": [ { "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-processed-auction-request-hook" + "hook-impl-code": "optable-targeting-async-processed-auction-request-hook" + } + ] + } + ] + }, + "bidder-request": { + "groups": [ + { + "timeout": 500, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-bidder-request-hook" } ] }