Skip to content

Commit

Permalink
Support path components on custom URLs (#310)
Browse files Browse the repository at this point in the history
  • Loading branch information
andre-statsig authored Dec 3, 2024
1 parent 21e5cbb commit 44e206a
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 56 deletions.
42 changes: 19 additions & 23 deletions Sources/Statsig/NetworkService.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

fileprivate enum Endpoint: String {
internal enum Endpoint: String {
case initialize = "/v1/initialize"
case logEvent = "/v1/rgstr"
}
Expand All @@ -19,6 +19,16 @@ class NetworkService {
var store: InternalStore
var inflightRequests = AtomicDictionary<URLSessionTask>(label: "com.Statsig.InFlightRequests")

/**
Default URL used to initialize the SDK. Used for tests.
*/
internal static var defaultInitializationUrl = URL(string: "https://\(ApiHost)\(Endpoint.initialize.rawValue)")

/**
Default URL used for log_event network requests. Used for tests.
*/
internal static var defaultEventLoggingUrl = URL(string: "https://\(LogEventHost)\(Endpoint.logEvent.rawValue)")

private final let networkRetryErrorCodes = [408, 500, 502, 503, 504, 522, 524, 599]

init(sdkKey: String, options: StatsigOptions, store: InternalStore) {
Expand Down Expand Up @@ -245,6 +255,13 @@ class NetworkService {
return (nil, StatsigError.invalidJSONParam("requestBody"))
}

private func urlForEndpoint(_ endpoint: Endpoint) -> URL? {
return switch endpoint {
case .initialize: self.statsigOptions.initializationUrl ?? NetworkService.defaultInitializationUrl
case .logEvent: self.statsigOptions.eventLoggingUrl ?? NetworkService.defaultEventLoggingUrl
}
}

private func makeAndSendRequest(
_ endpoint: Endpoint,
body: Data,
Expand All @@ -253,20 +270,7 @@ class NetworkService {
taskCapture: TaskCaptureHandler = nil
)
{
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = ApiHost
urlComponents.path = endpoint.rawValue

if let override = self.statsigOptions.mainApiUrl {
urlComponents.applyOverride(override)
}

if endpoint == .logEvent, let loggingApiOverride = self.statsigOptions.logEventApiUrl {
urlComponents.applyOverride(loggingApiOverride)
}

guard let requestURL = urlComponents.url else {
guard let requestURL = urlForEndpoint(endpoint) else {
completion(nil, nil, StatsigError.invalidRequestURL("\(endpoint)"))
return
}
Expand Down Expand Up @@ -332,11 +336,3 @@ class NetworkService {
}
}
}

extension URLComponents {
mutating func applyOverride(_ url: URL) {
scheme = url.scheme
host = url.host
port = url.port
}
}
82 changes: 64 additions & 18 deletions Sources/Statsig/StatsigOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,46 @@ public class StatsigOptions {
public var shutdownOnBackground = true;

/**
The API to use for all SDK network requests. You should not need to override this (unless you have another API that implements the Statsig API endpoints)
The URL used to initialize the SDK. You should not need to override this (unless you have another endpoint that implements the Statsig initialization endpoint)
*/
public var api = "https://\(ApiHost)" {
didSet {
mainApiUrl = URL(string: api) ?? mainApiUrl
public var initializationUrl: URL? = nil

/**
The URL used for log_event network requests. You should not need to override this (unless you have another API that implements the Statsig /v1/rgstr endpoint)
*/
public var eventLoggingUrl: URL? = nil

/**
The API to use for initialization network requests. Any path will be replaced with /v1/initialize. If you need a custom path, set the full URL to initializationUrl.
You should not need to override this (unless you have another API that implements the Statsig API endpoints)
*/
public var api: String {
get {
return initializationUrl?.ignoringPath?.absoluteString ?? "https://\(ApiHost)"
}
set {
if let apiUrl = URL(string: newValue)?.ignoringPath {
self.initializationUrl = apiUrl.appendingPathComponent(Endpoint.initialize.rawValue, isDirectory: false)
} else {
print("[Statsig]: Failed to create URL with StatsigOptions.api. Please check if it's a valid URL")
}
}
}

/**
The API to use for log_event network requests. You should not need to override this (unless you have another API that implements the Statsig /v1/log_event endpoint)
The API to use for log_event network requests. Any path will be replaced with /v1/rgstr. If you need a custom path, set the full URL to eventLoggingUrl.
You should not need to override this (unless you have another API that implements the Statsig /v1/rgstr endpoint)
*/
public var eventLoggingApi = "https://\(LogEventHost)" {
didSet {
logEventApiUrl = URL(string: eventLoggingApi) ?? logEventApiUrl
public var eventLoggingApi: String {
get {
return eventLoggingUrl?.ignoringPath?.absoluteString ?? "https://\(LogEventHost)"
}
set {
if let apiUrl = URL(string: newValue)?.ignoringPath {
self.eventLoggingUrl = apiUrl.appendingPathComponent(Endpoint.logEvent.rawValue, isDirectory: false)
} else {
print("[Statsig]: Failed to create URL with StatsigOptions.eventLoggingApi. Please check if it's a valid URL")
}
}
}

Expand Down Expand Up @@ -114,9 +140,7 @@ public class StatsigOptions {
This property can be customized to utilize URLSession instances with specific configurations, including certificate pinning, for enhanced security when communicating with servers.
*/
public var urlSession: URLSession = .shared

internal var mainApiUrl: URL?
internal var logEventApiUrl: URL?

var environment: [String: String] = [:]

public init(initTimeout: Double? = 3.0,
Expand All @@ -132,6 +156,8 @@ public class StatsigOptions {
shutdownOnBackground: Bool? = true,
api: String? = nil,
eventLoggingApi: String? = nil,
initializationUrl: URL? = nil,
eventLoggingUrl: URL? = nil,
evaluationCallback: ((EvaluationCallbackData) -> Void)? = nil,
userValidationCallback: ((StatsigUser) -> StatsigUser)? = nil,
customCacheKey: ((String, StatsigUser) -> String)? = nil,
Expand Down Expand Up @@ -182,17 +208,24 @@ public class StatsigOptions {
if let storageProvider = storageProvider {
self.storageProvider = storageProvider
}

if let api = api {

if let initializationUrl = initializationUrl {
self.initializationUrl = initializationUrl
if api != nil {
print("[Statsig]: StatsigOptions.api is being ignored because StatsigOptions.initializationUrl is also being set.")
}
} else if let api = api {
self.api = api
self.mainApiUrl = URL(string: api)
}
self.mainApiUrl = URL(string: self.api) ?? URL(string: "https://\(ApiHost)")

if let eventLoggingApi = eventLoggingApi {
if let eventLoggingUrl = eventLoggingUrl {
self.eventLoggingUrl = eventLoggingUrl
if eventLoggingApi != nil {
print("[Statsig]: StatsigOptions.eventLoggingApi is being ignored because StatsigOptions.eventLoggingUrl is also being set.")
}
} else if let eventLoggingApi = eventLoggingApi {
self.eventLoggingApi = eventLoggingApi
}
self.logEventApiUrl = URL(string: self.eventLoggingApi) ?? URL(string: "https://\(LogEventHost)")

if let customCacheKey = customCacheKey {
self.customCacheKey = customCacheKey
Expand All @@ -209,3 +242,16 @@ public class StatsigOptions {
self.userValidationCallback = userValidationCallback
}
}

// NOTE: This is here to to prevent a bugfix from causing a breaking change to users of the `api` option
extension URL {
var ignoringPath: URL? {
get {
var urlComponents = URLComponents()
urlComponents.scheme = scheme
urlComponents.host = host
urlComponents.port = port
return urlComponents.url
}
}
}
70 changes: 70 additions & 0 deletions Tests/StatsigTests/ApiOverrideSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ final class ApiOverrideSpec: BaseSpec {
request = TestUtils.startWithStatusAndWait(options: opts)
}

func startWithUrls() {
let opts = StatsigOptions(initializationUrl: URL(string: "http://api.override.com/setup"), eventLoggingUrl: URL(string: "http://api.override.com/st"))
request = TestUtils.startWithStatusAndWait(options: opts)
}

afterSuite {
TestUtils.resetDefaultUrls()
}

it("calls initialize on the overridden api") {
start()
expect(request?.url?.absoluteString).to(equal("http://api.override.com/v1/initialize"))
Expand All @@ -38,6 +47,23 @@ final class ApiOverrideSpec: BaseSpec {
Statsig.shutdown()
expect(hitLog).toEventually(beTrue())
}

it("calls initialize on the overridden api using URLs") {
startWithUrls()
expect(request?.url?.absoluteString).to(equal("http://api.override.com/setup"))
}

it("calls log_event on the overridden api using URLs") {
startWithUrls()
var hitLog = false
TestUtils.captureLogs(host: "api.override.com", path: "/st") { logs in
hitLog = true
}

Statsig.logEvent("test_event")
Statsig.shutdown()
expect(hitLog).toEventually(beTrue())
}
}

describe("When Not Overridden") {
Expand Down Expand Up @@ -69,6 +95,11 @@ final class ApiOverrideSpec: BaseSpec {
request = TestUtils.startWithStatusAndWait(options: opts)
}

func startWithUrls() {
let opts = StatsigOptions(eventLoggingUrl: URL(string: "http://api.log.co.nz/st"))
request = TestUtils.startWithStatusAndWait(options: opts)
}

it("calls initialize on the statsig api") {
start()
expect(request?.url?.absoluteString).to(equal("https://featureassets.org/v1/initialize"))
Expand All @@ -85,6 +116,23 @@ final class ApiOverrideSpec: BaseSpec {
Statsig.shutdown()
expect(hitLog).toEventually(beTrue())
}

it("calls initialize on the statsig api using URLs") {
startWithUrls()
expect(request?.url?.absoluteString).to(equal("https://featureassets.org/v1/initialize"))
}

it("calls log_event on the overridden api.log.co.nz api using URLs") {
startWithUrls()
var hitLog = false
TestUtils.captureLogs(host: "api.log.co.nz", path: "/st") { logs in
hitLog = true
}

Statsig.logEvent("test_event")
Statsig.shutdown()
expect(hitLog).toEventually(beTrue())
}
}

describe("When Main and Logging API Overridden") {
Expand All @@ -93,6 +141,11 @@ final class ApiOverrideSpec: BaseSpec {
request = TestUtils.startWithStatusAndWait(options: opts)
}

func startWithUrls() {
let opts = StatsigOptions(initializationUrl: URL(string: "http://main.api/setup"), eventLoggingUrl: URL(string: "http://api.log.co.nz/st"))
request = TestUtils.startWithStatusAndWait(options: opts)
}

it("calls initialize on the overridden api") {
start()
expect(request?.url?.absoluteString).to(equal("http://main.api/v1/initialize"))
Expand All @@ -109,6 +162,23 @@ final class ApiOverrideSpec: BaseSpec {
Statsig.shutdown()
expect(hitLog).toEventually(beTrue())
}

it("calls initialize on the overridden api using URLs") {
startWithUrls()
expect(request?.url?.absoluteString).to(equal("http://main.api/setup"))
}

it("calls log_event on the overridden api.log.co.nz api using URLs") {
startWithUrls()
var hitLog = false
TestUtils.captureLogs(host: "api.log.co.nz", path: "/st") { logs in
hitLog = true
}

Statsig.logEvent("test_event")
Statsig.shutdown()
expect(hitLog).toEventually(beTrue())
}
}

}
Expand Down
11 changes: 6 additions & 5 deletions Tests/StatsigTests/AppLifecycleSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class AppLifecycleSpec: BaseSpec {

func startAndLog(shutdownOnBackground: Bool, tag: String) {
let opts = StatsigOptions(shutdownOnBackground: shutdownOnBackground)
opts.mainApiUrl = URL(string: "http://AppLifecycleSpec::\(tag)")
NetworkService.defaultInitializationUrl = URL(string: "http://AppLifecycleSpec::\(tag)/v1/initialize")

_ = TestUtils.startWithStatusAndWait(options: opts)

Expand All @@ -32,6 +32,11 @@ final class AppLifecycleSpec: BaseSpec {
Statsig.logEvent("my_event")
}

afterEach {
Statsig.shutdown()
TestUtils.resetDefaultUrls()
}


it("shuts down the logger on app background when shutdownOnBackground is true") {
startAndLog(shutdownOnBackground: true, tag: "Shutdown")
Expand All @@ -43,8 +48,6 @@ final class AppLifecycleSpec: BaseSpec {

expect(logger.timesShutdownCalled).toEventually(equal(1))
expect(logger.timesFlushCalled).to(equal(0))

Statsig.shutdown()
}


Expand All @@ -58,8 +61,6 @@ final class AppLifecycleSpec: BaseSpec {

expect(logger.timesFlushCalled).toEventually(equal(1))
expect(logger.timesShutdownCalled).to(equal(0))

Statsig.shutdown()
}
}
}
1 change: 1 addition & 0 deletions Tests/StatsigTests/BaseSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class BaseSpec: QuickSpec {

BaseSpec.resetUserDefaults()
HTTPStubs.removeAllStubs()
TestUtils.resetDefaultUrls()
}
}

Expand Down
3 changes: 2 additions & 1 deletion Tests/StatsigTests/CodableSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class CodableSpec: BaseSpec {

describe("Codable") {
let opts = StatsigOptions(disableDiagnostics: true)
opts.mainApiUrl = URL(string: "http://CodableSpec")

beforeEach {
NetworkService.defaultInitializationUrl = URL(string: "http://CodableSpec/v1/initialize")
_ = TestUtils.startWithResponseAndWait([
"feature_gates": [
"a_gate".sha256(): [
Expand All @@ -45,6 +45,7 @@ class CodableSpec: BaseSpec {
afterEach {
Statsig.client?.shutdown()
Statsig.client = nil
TestUtils.resetDefaultUrls()
}


Expand Down
3 changes: 2 additions & 1 deletion Tests/StatsigTests/ExposureLoggingSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class ExposureLoggingSpec: BaseSpec {

describe("ExposureLogging") {
let opts = StatsigOptions(disableDiagnostics: true)
opts.logEventApiUrl = URL(string: "http://ExposureLoggingSpec")

var logs: [[String: Any]] = []
beforeEach {
NetworkService.defaultEventLoggingUrl = URL(string: "http://ExposureLoggingSpec/v1/rgstr")
_ = TestUtils.startWithResponseAndWait([
"feature_gates": [
"a_gate".sha256(): [
Expand Down Expand Up @@ -54,6 +54,7 @@ class ExposureLoggingSpec: BaseSpec {
afterEach {
Statsig.client?.shutdown()
Statsig.client = nil
TestUtils.resetDefaultUrls()
}

describe("standard use") {
Expand Down
Loading

0 comments on commit 44e206a

Please sign in to comment.