From 683a547c5f5707c3d66e95a68f5b399d7b2ce259 Mon Sep 17 00:00:00 2001 From: Riabkov <22525345+Riabkov@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:43:46 +0300 Subject: [PATCH 1/3] - Add OptableConfig.origin - Append 'Origin' header in RequestInterceptor.kt if config.origin is not null or empty - Added 'Origin' header tests to MockWebServerTest for green/empty/null config.origin - Add origin to Kotlin and Java demo apps' Applications - Update README.md --- .../optable/demoappjava/TheApplication.java | 15 ++++++++- .../optable/androidsdkdemo/TheApplication.kt | 8 ++++- README.md | 17 +++++++++- .../main/java/co/optable/sdk/OptableConfig.kt | 2 ++ .../sdk/core/network/RequestInterceptor.kt | 5 +++ .../java/co/optable/sdk/OptableConfigTest.kt | 5 ++- .../co/optable/sdk/core/MockWebServerTest.kt | 31 +++++++++++++++++++ 7 files changed, 79 insertions(+), 4 deletions(-) diff --git a/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java b/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java index 37239ba..6275dc9 100644 --- a/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java +++ b/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java @@ -2,6 +2,7 @@ import android.app.Application; import co.optable.sdk.OptableConfig; +import co.optable.sdk.OptableConsents; import co.optable.sdk.OptableSDK; public class TheApplication extends Application { @@ -12,7 +13,19 @@ public class TheApplication extends Application { public void onCreate() { super.onCreate(); - OptableConfig config = new OptableConfig(this, "prebidtest", "android-sdk"); + OptableConfig config = new OptableConfig( + this, + "prebidtest", + "android-sdk", + "na.edge.optable.co", + "v2", + false, + null, + null, + false, + new OptableConsents(), + "https://www.optable.co" + ); optable = new OptableSDK(config); } diff --git a/DemoApp/DemoAppKotlin/app/src/main/java/co/optable/androidsdkdemo/TheApplication.kt b/DemoApp/DemoAppKotlin/app/src/main/java/co/optable/androidsdkdemo/TheApplication.kt index 8467fb2..b7038e0 100644 --- a/DemoApp/DemoAppKotlin/app/src/main/java/co/optable/androidsdkdemo/TheApplication.kt +++ b/DemoApp/DemoAppKotlin/app/src/main/java/co/optable/androidsdkdemo/TheApplication.kt @@ -13,7 +13,13 @@ class TheApplication : Application() { override fun onCreate() { super.onCreate() - val config = OptableConfig(this, "prebidtest", "android-sdk", "ca.edge.optable.co") + val config = OptableConfig( + providedContext = this, + tenant = "prebidtest", + originSlug = "android-sdk", + host = "ca.edge.optable.co", + origin = "https://www.optable.co", + ) optable = OptableSDK(config) } diff --git a/README.md b/README.md index c2cb654..15eb62a 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ developers running the DCN locally for testing. - `customUserAgent` An optional custom user agent string for network requests. - `skipAdvertisingIdDetection` Boolean flag to skip the detection of advertising IDs. Default is false. - `consents` Optional `OptableConsents` object for providing custom consent information. If not provided, default values will be used. +- `origin` An optional value for the HTTP `Origin` header sent with Optable API requests. Unrelated to `originSlug`. ```kotlin val config = OptableConfig( @@ -116,6 +117,7 @@ val config = OptableConfig( customUserAgent = "", skipAdvertisingIdDetection = false, consents = OptableConsents(), + origin = "https://www.acmeco.com", ) ``` @@ -135,7 +137,7 @@ val config = OptableConfig( ) ``` -Finally, an optional sixth boolean parameter `skipAdvertisingIdDetection` can be used to skip any ID info detection from +An optional boolean parameter `skipAdvertisingIdDetection` can be used to skip any ID info detection from `AdvertisingIdClient` which by default runs in a background co-routine. Disabling ad ID detection means that the SDK will not be able to automatically obtain the Google Advertising ID. For example, to disable ad ID detection, in Kotlin: @@ -146,6 +148,19 @@ val config = OptableConfig( ) ``` +Finally, an optional string parameter `origin` can be used to set the value of the HTTP `Origin` header sent with every +Optable API request. Mobile HTTP stacks do not send an `Origin` header by +default, and your DCN uses the request origin for several server-side features: some EID resolvers require it to be +present, and it also feeds site attribution and origin validation. Set it to the origin you want your mobile traffic +attributed to, for example: + +```kotlin +val config = OptableConfig( + ... + origin = "https://www.acmeco.com" +) +``` + ### OptableIdentifiers There are many fields to identify the users. You can provide any user information using `OptableIdentifiers` class. diff --git a/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt b/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt index ba0ed4c..b0208cd 100644 --- a/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt +++ b/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt @@ -15,6 +15,7 @@ import android.content.Context * @param customUserAgent An optional custom user agent string for network requests. * @param skipAdvertisingIdDetection Boolean flag to skip the detection of advertising IDs. Default is false. * @param consents Optional `OptableConsents` object for providing custom consent information. If not provided, default values will be used. + * @param origin An optional value for the HTTP `Origin` header sent with Optable API requests. Unrelated to `originSlug`. */ class OptableConfig @JvmOverloads constructor( providedContext: Context, @@ -27,6 +28,7 @@ class OptableConfig @JvmOverloads constructor( internal val customUserAgent: String? = null, internal val skipAdvertisingIdDetection: Boolean = false, var consents: OptableConsents = OptableConsents(), + internal val origin: String? = null, ) { internal val context = providedContext.applicationContext diff --git a/android_sdk/src/main/java/co/optable/sdk/core/network/RequestInterceptor.kt b/android_sdk/src/main/java/co/optable/sdk/core/network/RequestInterceptor.kt index af92c70..0ebf493 100644 --- a/android_sdk/src/main/java/co/optable/sdk/core/network/RequestInterceptor.kt +++ b/android_sdk/src/main/java/co/optable/sdk/core/network/RequestInterceptor.kt @@ -57,6 +57,11 @@ internal class RequestInterceptor( modifiedRequest.addHeader("Authorization", "Bearer $apiKey") } + val origin = config.origin + if (!origin.isNullOrBlank()) { + modifiedRequest.addHeader("Origin", origin) + } + val userAgent = userAgentHolder.getUserAgent() if (userAgent != null) { modifiedRequest.addHeader("User-Agent", userAgent) diff --git a/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt b/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt index c467883..97d6be2 100644 --- a/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt @@ -39,6 +39,7 @@ class OptableConfigTest { assertNull(config.apiKey) assertNull(config.customUserAgent) assertFalse(config.skipAdvertisingIdDetection) + assertNull(config.origin) assertEquals(mockApplicationContext, config.context) } @@ -55,7 +56,8 @@ class OptableConfigTest { apiKey = "test-api-key", customUserAgent = "TestAgent/1.0", skipAdvertisingIdDetection = true, - consents = customConsents + consents = customConsents, + origin = "https://www.optable.co" ) assertEquals("custom-tenant", config.tenant) @@ -67,6 +69,7 @@ class OptableConfigTest { assertEquals("TestAgent/1.0", config.customUserAgent) assertTrue(config.skipAdvertisingIdDetection) assertEquals(customConsents, config.consents) + assertEquals("https://www.optable.co", config.origin) } @Test diff --git a/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt b/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt index e3b4df8..6f4b0b8 100644 --- a/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt @@ -69,6 +69,7 @@ class MockWebServerTest { assertNull(request.headers["Authorization"]) assertNull(request.headers["X-Optable-Visitor"]) assertNull(request.headers["User-Agent"]) + assertNull(request.headers["Origin"]) assertEquals("application/json", request.headers["Accept"]) assertEquals( @@ -97,6 +98,33 @@ class MockWebServerTest { assertEquals("passport", request.headers["X-Optable-Visitor"]) } + @Test + fun `optional header, origin`() { + whenever(config.origin).thenReturn("https://www.optable.co") + + val request = makeRequest().request + + assertEquals("https://www.optable.co", request.headers["Origin"]) + } + + @Test + fun `no origin header when origin is blank`() { + whenever(config.origin).thenReturn(" ") + + val request = makeRequest().request + + assertNull(request.headers["Origin"]) + } + + @Test + fun `no origin header when origin is null`() { + whenever(config.origin).thenReturn(null) + + val request = makeRequest().request + + assertNull(request.headers["Origin"]) + } + @Test fun `complete request`() { whenever(config.tenant).thenReturn("tenant") @@ -104,12 +132,15 @@ class MockWebServerTest { whenever(config.apiKey).thenReturn("apiKey") whenever(storage.getPassport()).thenReturn("passport") whenever(userAgentHolder.getUserAgent()).thenReturn("userAgent") + whenever(config.origin).thenReturn("https://www.optable.co") val request = makeRequest().request assertEquals("application/json", request.headers["Accept"]) assertEquals("Bearer apiKey", request.headers["Authorization"]) verify(config, times(1)).apiKey + assertEquals("https://www.optable.co", request.headers["Origin"]) + verify(config, times(1)).origin assertEquals("passport", request.headers["X-Optable-Visitor"]) verify(storage, times(1)).getPassport() assertEquals("userAgent", request.headers["User-Agent"]) From 74150bdc4d6646741f5886b7683eb4bb0dea2e99 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 31 Jul 2026 11:11:29 +0200 Subject: [PATCH 2/3] Make OptableConfig.origin settable after construction Java callers could only reach the origin parameter through the full 11-argument constructor, which forced the Java demo to restate every default. A public var matches consents, and matches the iOS SDK where origin is a settable property. --- .../co/optable/demoappjava/TheApplication.java | 16 ++-------------- README.md | 7 +++++++ .../main/java/co/optable/sdk/OptableConfig.kt | 3 ++- .../java/co/optable/sdk/OptableConfigTest.kt | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java b/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java index 6275dc9..2f826a4 100644 --- a/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java +++ b/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/TheApplication.java @@ -2,7 +2,6 @@ import android.app.Application; import co.optable.sdk.OptableConfig; -import co.optable.sdk.OptableConsents; import co.optable.sdk.OptableSDK; public class TheApplication extends Application { @@ -13,19 +12,8 @@ public class TheApplication extends Application { public void onCreate() { super.onCreate(); - OptableConfig config = new OptableConfig( - this, - "prebidtest", - "android-sdk", - "na.edge.optable.co", - "v2", - false, - null, - null, - false, - new OptableConsents(), - "https://www.optable.co" - ); + OptableConfig config = new OptableConfig(this, "prebidtest", "android-sdk"); + config.setOrigin("https://www.optable.co"); optable = new OptableSDK(config); } diff --git a/README.md b/README.md index 15eb62a..ee269cc 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,13 @@ val config = OptableConfig( ) ``` +`origin` can also be set after the config has been created, which is the convenient form from Java: + +```java +OptableConfig config = new OptableConfig(this, "prebidtest", "android-sdk"); +config.setOrigin("https://www.acmeco.com"); +``` + ### OptableIdentifiers There are many fields to identify the users. You can provide any user information using `OptableIdentifiers` class. diff --git a/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt b/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt index b0208cd..52dc92b 100644 --- a/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt +++ b/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt @@ -16,6 +16,7 @@ import android.content.Context * @param skipAdvertisingIdDetection Boolean flag to skip the detection of advertising IDs. Default is false. * @param consents Optional `OptableConsents` object for providing custom consent information. If not provided, default values will be used. * @param origin An optional value for the HTTP `Origin` header sent with Optable API requests. Unrelated to `originSlug`. + * Settable after construction, which is how Java callers set it without passing every preceding parameter. */ class OptableConfig @JvmOverloads constructor( providedContext: Context, @@ -28,7 +29,7 @@ class OptableConfig @JvmOverloads constructor( internal val customUserAgent: String? = null, internal val skipAdvertisingIdDetection: Boolean = false, var consents: OptableConsents = OptableConsents(), - internal val origin: String? = null, + var origin: String? = null, ) { internal val context = providedContext.applicationContext diff --git a/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt b/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt index 97d6be2..c712136 100644 --- a/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt @@ -102,6 +102,21 @@ class OptableConfigTest { assertEquals(expectedUrl, config.getBaseUrl()) } + @Test + fun `origin property should be updatable`() { + val config = OptableConfig( + providedContext = mockContext, + tenant = "test-tenant", + originSlug = "test-slug" + ) + + assertNull(config.origin) + + config.origin = "https://www.acmeco.com" + + assertEquals("https://www.acmeco.com", config.origin) + } + @Test fun `consents property should be updatable`() { val config = OptableConfig( From cf05250b10f8bcc28d92bbde46185a4379c6c541 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 31 Jul 2026 12:03:13 +0200 Subject: [PATCH 3/3] Test that an empty origin sends no header The blank case covered whitespace only, so the empty string the PR description claims to cover went untested. --- .../test/java/co/optable/sdk/core/MockWebServerTest.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt b/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt index 6f4b0b8..4cd3a3d 100644 --- a/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/core/MockWebServerTest.kt @@ -116,6 +116,15 @@ class MockWebServerTest { assertNull(request.headers["Origin"]) } + @Test + fun `no origin header when origin is empty`() { + whenever(config.origin).thenReturn("") + + val request = makeRequest().request + + assertNull(request.headers["Origin"]) + } + @Test fun `no origin header when origin is null`() { whenever(config.origin).thenReturn(null)