From 95948b47c872fbcd812084e5ef730fe41bb028fe Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Mon, 27 Jul 2026 12:52:38 +0300 Subject: [PATCH 1/5] feat: add optional origin header --- Source/Core/EdgeAPI.swift | 4 ++ Source/Public/OptableConfig.swift | 8 +++ Tests/Misc/Constants.swift | 4 +- Tests/Unit/EdgeAPITests.swift | 69 +++++++++++++++++++ demo-ios-objc/Podfile.lock | 12 ++-- demo-ios-objc/demo-ios-objc/AppDelegate.m | 3 +- .../demo-ios-swift/AppDelegate.swift | 1 + docs/usage-objc.md | 6 ++ docs/usage-swift.md | 7 ++ 9 files changed, 106 insertions(+), 8 deletions(-) diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 893c4ba..409463e 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -154,6 +154,10 @@ extension EdgeAPI { headers[.userAgent] = userAgent } + if let origin = config.origin { + headers[.origin] = origin + } + if let apiKey = config.apiKey { headers[.authorization] = "Bearer \(apiKey)" } diff --git a/Source/Public/OptableConfig.swift b/Source/Public/OptableConfig.swift index 1a7e23f..62274ba 100644 --- a/Source/Public/OptableConfig.swift +++ b/Source/Public/OptableConfig.swift @@ -40,6 +40,11 @@ public class OptableConfig: NSObject { @objc public var customUserAgent: String? + /// An optional value sent as the `Origin` HTTP header on every Optable API request. + /// When `nil` (the default), no `Origin` header is sent. + @objc + public var origin: String? + /// Boolean flag to skip the detection of advertising IDs. Default is false. @objc public var skipAdvertisingIdDetection: Bool = false @@ -103,6 +108,7 @@ public class OptableConfig: NSObject { - insecure: Boolean flag that determines if insecure HTTP should be used instead of HTTPS. Default is false. - apiKey: An optional API key for authentication. If the API Endpoint is enabled as private, a Service Account API key will be required. - customUserAgent: An optional custom user agent string for network requests. + - origin: An optional value sent as the `Origin` HTTP header on every Optable API request. No header is sent when nil. - skipAdvertisingIdDetection: Boolean flag to skip the detection of advertising IDs. Default is false. */ public init( @@ -113,6 +119,7 @@ public class OptableConfig: NSObject { insecure: Bool = false, apiKey: String? = nil, customUserAgent: String? = nil, + origin: String? = nil, skipAdvertisingIdDetection: Bool = false ) { self.tenant = tenant @@ -122,6 +129,7 @@ public class OptableConfig: NSObject { self.insecure = insecure self.apiKey = apiKey self.customUserAgent = customUserAgent + self.origin = origin self.skipAdvertisingIdDetection = skipAdvertisingIdDetection } } diff --git a/Tests/Misc/Constants.swift b/Tests/Misc/Constants.swift index 6e51fae..75a38d6 100644 --- a/Tests/Misc/Constants.swift +++ b/Tests/Misc/Constants.swift @@ -47,7 +47,9 @@ enum T { } static let userAgent: String = "ios-integration-tests" - + + static let origin: String = "https://ios-integration-tests.optable.co" + static let apiKey: String = "test-api-key" static let apiKeyBearer: String = "Bearer \(apiKey)" } diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index 602ec0a..c4d33a5 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -17,6 +17,15 @@ class EdgeAPITests: XCTestCase { ) lazy var sdk = OptableSDK(config: config) + lazy var originConfig = OptableConfig( + tenant: T.api.tenant.prebidtest, + originSlug: T.api.slug.iosSDK, + apiKey: T.api.apiKey, + customUserAgent: T.api.userAgent, + origin: T.api.origin, + ) + lazy var originSDK = OptableSDK(config: originConfig) + // MARK: URL-s /** Expected output: @@ -141,6 +150,66 @@ class EdgeAPITests: XCTestCase { XCTAssertEqual(generatedHeaders["User-Agent"], T.api.userAgent) XCTAssertEqual(generatedHeaders["Authorization"], T.api.apiKeyBearer) + XCTAssertNil(generatedHeaders["Origin"]) + } + + /** + When `origin` is configured, it is sent as the `Origin` header. + */ + func test_header_generation_with_origin() throws { + let generatedHeaders = originSDK.api.resolveHeaders().asDict + + XCTAssertEqual(generatedHeaders["User-Agent"], T.api.userAgent) + XCTAssertEqual(generatedHeaders["Authorization"], T.api.apiKeyBearer) + XCTAssertEqual(generatedHeaders["Origin"], T.api.origin) + } + + /** + `origin` is optional, and mutable after the config has been created. + */ + func test_header_generation_origin_is_optional() throws { + let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) + let edgeAPI = EdgeAPI(config) + + XCTAssertNil(config.origin) + XCTAssertNil(edgeAPI.resolveHeaders().asDict["Origin"]) + + config.origin = T.api.origin + + XCTAssertEqual(edgeAPI.resolveHeaders().asDict["Origin"], T.api.origin) + } + + /** + The `Origin` header is unrelated to `originSlug`, which is sent as the `o` query parameter. + */ + func test_origin_does_not_affect_url_generation() throws { + let generatedURL = originSDK.api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) + let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! + + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "o" })!.value, T.api.slug.iosSDK) + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "origin" })) + } + + /** + Every endpoint carries the configured `Origin` header, and none of them carry one when it is unset. + */ + func test_origin_header_on_all_endpoints() throws { + typealias RequestFactory = (EdgeAPI) throws -> URLRequest? + + let factories: [RequestFactory] = [ + { try $0.identify(ids: [.postalCode("1234567890")]) }, + { try $0.targeting(ids: [.emailAddress("12345")]) }, + { try $0.profile(traits: ["test-key": "test-value"]) }, + { try $0.witness(event: "test-event", properties: ["test-key": "test-value"]) }, + ] + + try factories.forEach({ makeRequest in + let withoutOrigin = try makeRequest(sdk.api) + XCTAssertNil(withoutOrigin?.value(forHTTPHeaderField: "Origin")) + + let withOrigin = try makeRequest(originSDK.api) + XCTAssertEqual(withOrigin?.value(forHTTPHeaderField: "Origin"), T.api.origin) + }) } // MARK: URLRequest-s diff --git a/demo-ios-objc/Podfile.lock b/demo-ios-objc/Podfile.lock index 4bbe98b..a4b4b7d 100644 --- a/demo-ios-objc/Podfile.lock +++ b/demo-ios-objc/Podfile.lock @@ -1,11 +1,11 @@ PODS: - - Google-Mobile-Ads-SDK (12.14.0): + - Google-Mobile-Ads-SDK (13.7.0): - GoogleUserMessagingPlatform (>= 1.1) - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (1.0.0) - - PrebidMobile (3.1.0): - - PrebidMobile/core (= 3.1.0) - - PrebidMobile/core (3.1.0) + - PrebidMobile (3.3.2): + - PrebidMobile/core (= 3.3.2) + - PrebidMobile/core (3.3.2) DEPENDENCIES: - Google-Mobile-Ads-SDK @@ -23,10 +23,10 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2 + Google-Mobile-Ads-SDK: dd0ded6717d122c6a32c69dc4afcace7d276e4e4 GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c OptableSDK: d7cb5f7a211f1bac2a0c25677f4c4b31ded8ed4e - PrebidMobile: 046bb6220157c7332dc6c6e19a99397bb481ac3a + PrebidMobile: 309815211aeed6353a5742ad2f7a05194980f6bc PODFILE CHECKSUM: 84321d4bbdf19f72ce3dfa6f4cb2f0a9869574ad diff --git a/demo-ios-objc/demo-ios-objc/AppDelegate.m b/demo-ios-objc/demo-ios-objc/AppDelegate.m index 14297bc..6528397 100644 --- a/demo-ios-objc/demo-ios-objc/AppDelegate.m +++ b/demo-ios-objc/demo-ios-objc/AppDelegate.m @@ -31,7 +31,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( OptableConfig *config = [[OptableConfig alloc] initWithTenant: @"prebidtest" originSlug: @"ios-sdk"]; config.host = @"na.cloud.optable.co"; - + config.origin = @"https://demo-ios-objc.optable.co"; + OPTABLE = [[OptableSDK alloc] initWithConfig: config]; OPTABLE.delegate = delegate; diff --git a/demo-ios-swift/demo-ios-swift/AppDelegate.swift b/demo-ios-swift/demo-ios-swift/AppDelegate.swift index 5dbaabc..c255a45 100644 --- a/demo-ios-swift/demo-ios-swift/AppDelegate.swift +++ b/demo-ios-swift/demo-ios-swift/AppDelegate.swift @@ -32,6 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { tenant: "prebidtest", originSlug: "ios-sdk", host: "ca.edge.optable.co", + origin: "https://demo-ios-swift.optable.co", skipAdvertisingIdDetection: false ) OPTABLE = OptableSDK(config: config) diff --git a/docs/usage-objc.md b/docs/usage-objc.md index 5ed211b..7c32aa6 100644 --- a/docs/usage-objc.md +++ b/docs/usage-objc.md @@ -75,6 +75,12 @@ You can call various SDK APIs on the instance as shown in the examples below. It You can disable user agent `WKWebView` based auto-detection and provide your own value by setting the `useragent` parameter to a string value, similar to the Swift example. +By default the SDK does not send an `Origin` HTTP header. If your DCN expects one, you can set the optional `origin` parameter, and its value will be sent as the `Origin` header on every Optable API request (`identify`, `targeting`, `profile`, `witness`): + +```objective-c +config.origin = @"https://dcn.customer.com"; +``` + ### Identify API To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Apple ID for Advertising (IDFA), or even your own vendor or app level `PPID`, you can call the `identify` API as follows: diff --git a/docs/usage-swift.md b/docs/usage-swift.md index 715e10f..bc9e7f2 100644 --- a/docs/usage-swift.md +++ b/docs/usage-swift.md @@ -51,6 +51,13 @@ OPTABLE = OptableSDK(config: config) The default value of `nil` for the `useragent` parameter enables the `WKWebView` auto-detection behavior. +By default the SDK does not send an `Origin` HTTP header. If your DCN expects one, you can set the optional `origin` parameter, and its value will be sent as the `Origin` header on every Optable API request (`identify`, `targeting`, `profile`, `witness`): + +```swift +let config = OptableConfig(..., origin: "https://dcn.customer.com") +OPTABLE = OptableSDK(config: config) +``` + ### Identify API To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Apple ID for Advertising (IDFA), or even your own vendor or app level `PPID`, you can call the `identify` API as follows: From 88e062650e496ac2c9f6f40ac22f9f843d6f26f5 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 15:55:16 +0300 Subject: [PATCH 2/5] fix: add check for empty origin value --- Source/Core/EdgeAPI.swift | 2 +- Tests/Unit/EdgeAPITests.swift | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 409463e..71080e4 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -154,7 +154,7 @@ extension EdgeAPI { headers[.userAgent] = userAgent } - if let origin = config.origin { + if let origin = config.origin, origin.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false { headers[.origin] = origin } diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index c4d33a5..c916d83 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -179,6 +179,24 @@ class EdgeAPITests: XCTestCase { XCTAssertEqual(edgeAPI.resolveHeaders().asDict["Origin"], T.api.origin) } + /** + A blank `origin` (empty or whitespace-only) is suppressed rather than sent as an empty `Origin` header, + matching the Android SDK behavior. + */ + func test_header_generation_origin_is_not_blank() throws { + let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) + let edgeAPI = EdgeAPI(config) + + config.origin = "" + XCTAssertNil(edgeAPI.resolveHeaders().asDict["Origin"]) + + config.origin = " " + XCTAssertNil(edgeAPI.resolveHeaders().asDict["Origin"]) + + config.origin = T.api.origin + XCTAssertEqual(edgeAPI.resolveHeaders().asDict["Origin"], T.api.origin) + } + /** The `Origin` header is unrelated to `originSlug`, which is sent as the `o` query parameter. */ From 2a2b43cce28dc7a4f7b5d6a0f8a703b84317f9d9 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 16:00:52 +0300 Subject: [PATCH 3/5] revert: dependency bump --- demo-ios-objc/Podfile.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/demo-ios-objc/Podfile.lock b/demo-ios-objc/Podfile.lock index a4b4b7d..4c9ed45 100644 --- a/demo-ios-objc/Podfile.lock +++ b/demo-ios-objc/Podfile.lock @@ -1,11 +1,11 @@ PODS: - - Google-Mobile-Ads-SDK (13.7.0): + - Google-Mobile-Ads-SDK (12.14.0): - GoogleUserMessagingPlatform (>= 1.1) - GoogleUserMessagingPlatform (3.1.0) - OptableSDK (1.0.0) - - PrebidMobile (3.3.2): - - PrebidMobile/core (= 3.3.2) - - PrebidMobile/core (3.3.2) + - PrebidMobile (3.1.0): + - PrebidMobile/core (= 3.1.0) + - PrebidMobile/core (3.1.0) DEPENDENCIES: - Google-Mobile-Ads-SDK @@ -23,11 +23,11 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Google-Mobile-Ads-SDK: dd0ded6717d122c6a32c69dc4afcace7d276e4e4 + Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2 GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c OptableSDK: d7cb5f7a211f1bac2a0c25677f4c4b31ded8ed4e - PrebidMobile: 309815211aeed6353a5742ad2f7a05194980f6bc + PrebidMobile: 046bb6220157c7332dc6c6e19a99397bb481ac3a PODFILE CHECKSUM: 84321d4bbdf19f72ce3dfa6f4cb2f0a9869574ad -COCOAPODS: 1.16.2 +COCOAPODS: 1.16.2 \ No newline at end of file From 84b407816049b98f772742a32e7f55046ac1cb7f Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 16:05:34 +0300 Subject: [PATCH 4/5] revert: podfile.lock change --- demo-ios-objc/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-ios-objc/Podfile.lock b/demo-ios-objc/Podfile.lock index 4c9ed45..4bbe98b 100644 --- a/demo-ios-objc/Podfile.lock +++ b/demo-ios-objc/Podfile.lock @@ -30,4 +30,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 84321d4bbdf19f72ce3dfa6f4cb2f0a9869574ad -COCOAPODS: 1.16.2 \ No newline at end of file +COCOAPODS: 1.16.2 From 3a52bcf7b7bee640318a028af96863f5d19473f1 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 16:16:47 +0300 Subject: [PATCH 5/5] fix: docs --- Source/Public/OptableConfig.swift | 7 ++++--- docs/usage-objc.md | 4 ++-- docs/usage-swift.md | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Source/Public/OptableConfig.swift b/Source/Public/OptableConfig.swift index 62274ba..0d5a783 100644 --- a/Source/Public/OptableConfig.swift +++ b/Source/Public/OptableConfig.swift @@ -40,8 +40,9 @@ public class OptableConfig: NSObject { @objc public var customUserAgent: String? - /// An optional value sent as the `Origin` HTTP header on every Optable API request. - /// When `nil` (the default), no `Origin` header is sent. + /// An optional value sent as the `Origin` HTTP header on every Optable API request, + /// identifying the origin you want your mobile traffic attributed to. E.g. `https://www.acmeco.com`. + /// When `nil` (the default), no `Origin` header is sent. Unrelated to `originSlug`. @objc public var origin: String? @@ -108,7 +109,7 @@ public class OptableConfig: NSObject { - insecure: Boolean flag that determines if insecure HTTP should be used instead of HTTPS. Default is false. - apiKey: An optional API key for authentication. If the API Endpoint is enabled as private, a Service Account API key will be required. - customUserAgent: An optional custom user agent string for network requests. - - origin: An optional value sent as the `Origin` HTTP header on every Optable API request. No header is sent when nil. + - origin: An optional value sent as the `Origin` HTTP header on every Optable API request, identifying the origin you want your mobile traffic attributed to. E.g. `https://www.acmeco.com`. No header is sent when nil. Unrelated to `originSlug`. - skipAdvertisingIdDetection: Boolean flag to skip the detection of advertising IDs. Default is false. */ public init( diff --git a/docs/usage-objc.md b/docs/usage-objc.md index 7c32aa6..818f8b7 100644 --- a/docs/usage-objc.md +++ b/docs/usage-objc.md @@ -75,10 +75,10 @@ You can call various SDK APIs on the instance as shown in the examples below. It You can disable user agent `WKWebView` based auto-detection and provide your own value by setting the `useragent` parameter to a string value, similar to the Swift example. -By default the SDK does not send an `Origin` HTTP header. If your DCN expects one, you can set the optional `origin` parameter, and its value will be sent as the `Origin` header on every Optable API request (`identify`, `targeting`, `profile`, `witness`): +By default the SDK does not send an `Origin` HTTP header. If your DCN expects one, you can set the optional `origin` parameter to the origin you want your mobile traffic attributed to, and its value will be sent as the `Origin` header on every Optable API request (`identify`, `targeting`, `profile`, `witness`): ```objective-c -config.origin = @"https://dcn.customer.com"; +config.origin = @"https://www.acmeco.com"; ``` ### Identify API diff --git a/docs/usage-swift.md b/docs/usage-swift.md index bc9e7f2..925ab68 100644 --- a/docs/usage-swift.md +++ b/docs/usage-swift.md @@ -51,10 +51,10 @@ OPTABLE = OptableSDK(config: config) The default value of `nil` for the `useragent` parameter enables the `WKWebView` auto-detection behavior. -By default the SDK does not send an `Origin` HTTP header. If your DCN expects one, you can set the optional `origin` parameter, and its value will be sent as the `Origin` header on every Optable API request (`identify`, `targeting`, `profile`, `witness`): +By default the SDK does not send an `Origin` HTTP header. If your DCN expects one, you can set the optional `origin` parameter to the origin you want your mobile traffic attributed to, and its value will be sent as the `Origin` header on every Optable API request (`identify`, `targeting`, `profile`, `witness`): ```swift -let config = OptableConfig(..., origin: "https://dcn.customer.com") +let config = OptableConfig(..., origin: "https://www.acmeco.com") OPTABLE = OptableSDK(config: config) ```