From 067e7d2fa09566697bba16fb6d932700a92b136e Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Mon, 27 Jul 2026 12:52:38 +0300 Subject: [PATCH 1/6] 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 f276e7c..8d9ca73 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -178,6 +178,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 d3fb4b8..0f2d95c 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) + override func tearDown() { sdk.api.storage.clearTargeting() super.tearDown() @@ -146,6 +155,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 5dcf2cb..14ab091 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 `customUserAgent` 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 3eccfb1..2d7e327 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 `customUserAgent` 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 49c4093c3c5cb192414010a7e723a49021c1e32b Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 15:55:16 +0300 Subject: [PATCH 2/6] 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 8d9ca73..4d62e22 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -178,7 +178,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 0f2d95c..5bb46ed 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -184,6 +184,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 d7b6fb0b6bd6cf558415ad8f5fac4388a7d56f5e Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 16:00:52 +0300 Subject: [PATCH 3/6] 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 b470a9b924157932da6fca5872f98e3a45655db1 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 16:05:34 +0300 Subject: [PATCH 4/6] 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 7c2731a39ef4c8aaedd5d251419e596222d42cb5 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 31 Jul 2026 16:16:47 +0300 Subject: [PATCH 5/6] 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 14ab091..9ca87a9 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 `customUserAgent` 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 2d7e327..a66a225 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 `customUserAgent` 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) ``` From e5de14a48a14725b07540fd8313727f099075e22 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 4 Sep 2026 14:19:16 +0300 Subject: [PATCH 6/6] fix: update test_origin_header_on_all_endpoints --- Tests/Unit/EdgeAPITests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index 5bb46ed..afe040c 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -22,7 +22,7 @@ class EdgeAPITests: XCTestCase { originSlug: T.api.slug.iosSDK, apiKey: T.api.apiKey, customUserAgent: T.api.userAgent, - origin: T.api.origin, + origin: T.api.origin ) lazy var originSDK = OptableSDK(config: originConfig) @@ -221,7 +221,7 @@ class EdgeAPITests: XCTestCase { let factories: [RequestFactory] = [ { try $0.identify(ids: [.postalCode("1234567890")]) }, - { try $0.targeting(ids: [.emailAddress("12345")]) }, + { try $0.targeting(ids: [.emailAddress("12345")], hids: []) }, { try $0.profile(traits: ["test-key": "test-value"]) }, { try $0.witness(event: "test-event", properties: ["test-key": "test-value"]) }, ]