Skip to content

Commit

Permalink
feat: client identification headers (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Jan 27, 2025
1 parent 0b19cde commit bc418c3
Show file tree
Hide file tree
Showing 10 changed files with 57 additions and 17 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ The ready event is fired once the client has received it's first set of feature

Note: To release the package you'll need to have [CocoaPods](https://cocoapods.org/) installed.

First, you'll need to add a tag. Releasing the tag is enough for the Swift package manager, but it's polite to also ensure CocoaPods users can also consume the code.
Update `Sources/Version/Version.swift` with the new version number. It will be used in `x-unleash-sdk` header as a version reported to Unleash server.

Then, you'll need to add a tag with the same version number as the previous step. Releasing the tag is enough for the Swift package manager, but it's polite to also ensure CocoaPods users can also consume the code.

```sh
git tag -a 0.0.4 -m "v0.0.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class UnleashClientBase {
var timer: Timer?
var poller: Poller
var metrics: Metrics
var connectionId: UUID

public init(
unleashUrl: String,
Expand All @@ -26,6 +27,7 @@ public class UnleashClientBase {
fatalError("Invalid Unleash URL: \(unleashUrl)")
}

self.connectionId = UUID()
self.timer = nil
if let poller = poller {
self.poller = poller
Expand All @@ -35,7 +37,9 @@ public class UnleashClientBase {
unleashUrl: url,
apiKey: clientKey,
customHeaders: customHeaders,
bootstrap: bootstrap
bootstrap: bootstrap,
appName: appName,
connectionId: connectionId
)
}
if let metrics = metrics {
Expand All @@ -51,7 +55,7 @@ public class UnleashClientBase {
}
task.resume()
}
self.metrics = Metrics(appName: appName, metricsInterval: Double(metricsInterval), clock: { return Date() }, disableMetrics: disableMetrics, poster: urlSessionPoster, url: url, clientKey: clientKey, customHeaders: customHeaders)
self.metrics = Metrics(appName: appName, metricsInterval: Double(metricsInterval), clock: { return Date() }, disableMetrics: disableMetrics, poster: urlSessionPoster, url: url, clientKey: clientKey, customHeaders: customHeaders, connectionId: connectionId)
}

self.context = Context(appName: appName, environment: environment)
Expand Down
8 changes: 7 additions & 1 deletion Sources/UnleashProxyClientSwift/Metrics/Metrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class Metrics {
var bucket: Bucket
let url: URL
let customHeaders: [String: String]
let connectionId: UUID

init(appName: String,
metricsInterval: TimeInterval,
Expand All @@ -21,7 +22,8 @@ public class Metrics {
poster: @escaping PosterHandler,
url: URL,
clientKey: String,
customHeaders: [String: String] = [:]) {
customHeaders: [String: String] = [:],
connectionId: UUID) {
self.appName = appName
self.metricsInterval = metricsInterval
self.clock = clock
Expand All @@ -31,6 +33,7 @@ public class Metrics {
self.clientKey = clientKey
self.bucket = Bucket(clock: clock)
self.customHeaders = customHeaders
self.connectionId = connectionId
}

func start() {
Expand Down Expand Up @@ -105,6 +108,9 @@ public class Metrics {
request.addValue("no-cache", forHTTPHeaderField: "Cache")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(clientKey, forHTTPHeaderField: "Authorization")
request.addValue(appName, forHTTPHeaderField: "x-unleash-appname")
request.addValue(connectionId.uuidString, forHTTPHeaderField: "x-unleash-connection-id")
request.setValue("unleash-client-swift:\(LibraryInfo.version)", forHTTPHeaderField: "x-unleash-sdk")
if !self.customHeaders.isEmpty {
for (key, value) in self.customHeaders {
request.setValue(value, forHTTPHeaderField: key)
Expand Down
13 changes: 11 additions & 2 deletions Sources/UnleashProxyClientSwift/Poller/Poller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public class Poller {
var ready: Bool
var apiKey: String;
var etag: String;

var appName: String;
var connectionId: UUID;

private let session: PollerSession
var storageProvider: StorageProvider
let customHeaders: [String: String]
Expand All @@ -21,11 +23,15 @@ public class Poller {
session: PollerSession = URLSession.shared,
storageProvider: StorageProvider = DictionaryStorageProvider(),
customHeaders: [String: String] = [:],
bootstrap: Bootstrap = .toggles([])
bootstrap: Bootstrap = .toggles([]),
appName: String,
connectionId: UUID
) {
self.refreshInterval = refreshInterval
self.unleashUrl = unleashUrl
self.apiKey = apiKey
self.appName = appName
self.connectionId = connectionId
self.timer = nil
self.ready = false
self.etag = ""
Expand Down Expand Up @@ -107,6 +113,9 @@ public class Poller {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(self.apiKey, forHTTPHeaderField: "Authorization")
request.setValue(self.etag, forHTTPHeaderField: "If-None-Match")
request.setValue(self.appName, forHTTPHeaderField: "x-unleash-appname")
request.setValue(self.connectionId.uuidString, forHTTPHeaderField: "x-unleash-connection-id")
request.setValue("unleash-client-swift:\(LibraryInfo.version)", forHTTPHeaderField: "x-unleash-sdk")
if !self.customHeaders.isEmpty {
for (key, value) in self.customHeaders {
request.setValue(value, forHTTPHeaderField: key)
Expand Down
3 changes: 3 additions & 0 deletions Sources/UnleashProxyClientSwift/Version/Version.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public struct LibraryInfo {
public static let version = "1.6.0" // Update this string with each new release
}
9 changes: 6 additions & 3 deletions Tests/UnleashProxyClientSwiftTests/MetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ final class MetricsTests: XCTestCase {
clock: fixedClock,
poster: poster,
url: URL(string: "https://unleashinstance.com")!,
clientKey: "testKey")
clientKey: "testKey",
connectionId: UUID())
metrics.start()

metrics.count(name: "testToggle", enabled: true)
Expand Down Expand Up @@ -84,7 +85,8 @@ final class MetricsTests: XCTestCase {
clock: fixedClock,
poster: poster,
url: URL(string: "https://unleashinstance.com")!,
clientKey: "testKey")
clientKey: "testKey",
connectionId: UUID())
metrics.start()

metrics.count(name: "irrelevant", enabled: true)
Expand All @@ -105,7 +107,8 @@ final class MetricsTests: XCTestCase {
disableMetrics: true,
poster: poster,
url: URL(string: "https://unleashinstance.com")!,
clientKey: "testKey")
clientKey: "testKey",
connectionId: UUID())
metrics.start()

metrics.count(name: "irrelevant", enabled: true)
Expand Down
2 changes: 1 addition & 1 deletion Tests/UnleashProxyClientSwiftTests/MockMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ class MockMetrics: Metrics {
let response = HTTPURLResponse(url: URL(string: "https://irrelevant.com")!, statusCode: 200, httpVersion: nil, headerFields: nil)
completionHandler(.success((Data(), response!)))
}
super.init(appName: appName, metricsInterval: Double(15), clock: { return Date() }, disableMetrics: false, poster: noOpPoster, url: URL(string: "https://irrelevant.com")!, clientKey: "irrelevant")
super.init(appName: appName, metricsInterval: Double(15), clock: { return Date() }, disableMetrics: false, poster: noOpPoster, url: URL(string: "https://irrelevant.com")!, clientKey: "irrelevant", connectionId: UUID())
}
}
4 changes: 2 additions & 2 deletions Tests/UnleashProxyClientSwiftTests/MockPoller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ class MockPoller: Poller {
var dataGenerator: () -> [String: Toggle];
var stubCompletionError: PollerError?

init(callback: @escaping () -> [String: Toggle], unleashUrl: URL, apiKey: String, session: PollerSession) {
init(callback: @escaping () -> [String: Toggle], unleashUrl: URL, apiKey: String, session: PollerSession, appName: String, connectionId: UUID) {
self.dataGenerator = callback
super.init(refreshInterval: 15, unleashUrl: unleashUrl, apiKey: apiKey, session: session)
super.init(refreshInterval: 15, unleashUrl: unleashUrl, apiKey: apiKey, session: session, appName: appName, connectionId: connectionId)
}

override func getFeatures(context: Context, completionHandler: ((PollerError?) -> Void)? = nil) -> Void {
Expand Down
11 changes: 9 additions & 2 deletions Tests/UnleashProxyClientSwiftTests/PollerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ final class PollerTests: XCTestCase {

private let unleashUrl = URL(string: "https://app.unleash-hosted.com/hosted/api/proxy")!
private let apiKey = "SECRET"
private let appName = "APPNAME"
private let connectionId = UUID(uuidString: "123E4567-E89B-12d3-A456-426614174000")!
private let timeout = 1.0

func testWhenInitWithBootstrapTogglesThenAddToStore() {
Expand Down Expand Up @@ -190,13 +192,16 @@ final class PollerTests: XCTestCase {
let response = mockResponse()
let data = stubData()
let session = MockPollerSession(data: data, response: response)
let poller = Poller(refreshInterval: nil, unleashUrl: unleashUrl, apiKey: apiKey, session: session, customHeaders: customHeaders)
let poller = Poller(refreshInterval: nil, unleashUrl: unleashUrl, apiKey: apiKey, session: session, customHeaders: customHeaders, appName: appName, connectionId: connectionId)

let expectation = XCTestExpectation(description: "Expect custom headers to be set in the request.")

session.performRequestHandler = { request in
XCTAssertEqual(request.value(forHTTPHeaderField: "X-Custom-Header"), "CustomValue")
XCTAssertEqual(request.value(forHTTPHeaderField: "X-Another-Header"), "AnotherValue")
XCTAssertEqual(request.value(forHTTPHeaderField: "x-unleash-appname"), "APPNAME")
XCTAssertEqual(request.value(forHTTPHeaderField: "x-unleash-connection-id"), "123E4567-E89B-12D3-A456-426614174000")
XCTAssertTrue(request.value(forHTTPHeaderField: "x-unleash-sdk")!.range(of: #"^unleash-client-swift:\d+\.\d+\.\d+$"#, options: .regularExpression) != nil, "x-unleash-sdk header sdk:semver format does not match")
expectation.fulfill()
}

Expand Down Expand Up @@ -251,7 +256,9 @@ final class PollerTests: XCTestCase {
unleashUrl: url ?? unleashUrl,
apiKey: apiKey,
session: session,
bootstrap: bootstrap
bootstrap: bootstrap,
appName: appName,
connectionId: connectionId
)
}

Expand Down
12 changes: 9 additions & 3 deletions Tests/UnleashProxyClientSwiftTests/testUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ func setup(
callback: dataGenerator,
unleashUrl: URL(string: "https://app.unleash-hosted.com/hosted/api/proxy")!,
apiKey: "SECRET",
session: session
session: session,
appName: "APPNAME",
connectionId: UUID()
)
let metrics = MockMetrics(appName: "test")

Expand Down Expand Up @@ -80,7 +82,9 @@ func setup(
callback: dataGenerator,
unleashUrl: URL(string: "https://app.unleash-hosted.com/hosted/api/proxy")!,
apiKey: "SECRET",
session: session
session: session,
appName: "APPNAME",
connectionId: UUID()
)
let metrics = MockMetrics(appName: "test")

Expand All @@ -107,7 +111,9 @@ func setupBase(
callback: dataGenerator,
unleashUrl: URL(string: "https://app.unleash-hosted.com/hosted/api/proxy")!,
apiKey: "SECRET",
session: session
session: session,
appName: "APPNAME",
connectionId: UUID()
)
let metrics = MockMetrics(appName: "test")

Expand Down

0 comments on commit bc418c3

Please sign in to comment.