diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index 893c4ba..71080e4 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, origin.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false { + 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..0d5a783 100644 --- a/Source/Public/OptableConfig.swift +++ b/Source/Public/OptableConfig.swift @@ -40,6 +40,12 @@ public class OptableConfig: NSObject { @objc public var customUserAgent: String? + /// 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? + /// Boolean flag to skip the detection of advertising IDs. Default is false. @objc public var skipAdvertisingIdDetection: Bool = false @@ -103,6 +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, 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( @@ -113,6 +120,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 +130,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..c916d83 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,84 @@ 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) + } + + /** + 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. + */ + 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/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..818f8b7 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 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://www.acmeco.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..925ab68 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 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://www.acmeco.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: