Skip to content
Closed
29 changes: 29 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!--
Provide a description of your changes below and a general summary in the title.
Please also provide some test recommendations if necessary to ensure we don't have regressions.
Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Description

<!--- Describe your changes in detail -->

## Regression Test Recommendations

<!--- Functionality that could be affected by the change and any other concerns -->

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

## Estimated time to fix the ticket(s) or epic(s) refernced by the PR in days

- #FD343 - 2 days
Comment thread
gazreese marked this conversation as resolved.
Outdated
31 changes: 31 additions & 0 deletions FlagsmithClient/Classes/Flagsmith.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@ typealias CompletionHandler<T> = @Sendable (Result<T, any Error>) -> Void
public final class Flagsmith: @unchecked Sendable {
/// Shared singleton client object
public static let shared: Flagsmith = .init()

/// User-Agent header value for HTTP requests
/// Format: flagsmith-swift-ios-sdk/<version>
/// Falls back to "unknown" if version is not discoverable at runtime
internal static var userAgent: String {
let version = getSDKVersion()
return "flagsmith-swift-ios-sdk/\(version)"
}

/// Get the SDK version from the bundle at runtime
/// Falls back to "unknown" if version is not discoverable
private static func getSDKVersion() -> String {
// Try to get version from the main bundle first
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, !version.isEmpty {
return version
}

// Try to get version from the FlagsmithClient bundle (CocoaPods)
if let bundle = Bundle(identifier: "org.cocoapods.FlagsmithClient"),
let version = bundle.infoDictionary?["CFBundleShortVersionString"] as? String, !version.isEmpty {
return version
}

// Try to get version from the current bundle (for SPM or direct integration)
if let version = Bundle(for: Flagsmith.self).infoDictionary?["CFBundleShortVersionString"] as? String, !version.isEmpty {
return version
}

// Fallback to "unknown" if version is not discoverable
return "unknown"
}
private let apiManager: APIManager
private let sseManager: SSEManager
private let analytics: FlagsmithAnalytics
Expand Down
1 change: 1 addition & 0 deletions FlagsmithClient/Classes/Internal/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ enum Router: Sendable {
}
request.addValue(apiKey, forHTTPHeaderField: "X-Environment-Key")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(Flagsmith.userAgent, forHTTPHeaderField: "User-Agent")

return request
}
Expand Down
1 change: 1 addition & 0 deletions FlagsmithClient/Classes/Internal/SSEManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ final class SSEManager: NSObject, URLSessionDataDelegate, @unchecked Sendable {
request.setValue("text/event-stream, application/json; charset=utf-8", forHTTPHeaderField: "Accept")
request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
request.setValue("keep-alive", forHTTPHeaderField: "Connection")
request.setValue(Flagsmith.userAgent, forHTTPHeaderField: "User-Agent")

completionHandler = completion
dataTask = session.dataTask(with: request)
Expand Down
32 changes: 32 additions & 0 deletions FlagsmithClient/Tests/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@ final class RouterTests: FlagsmithClientTestCase {
XCTAssertTrue(request.allHTTPHeaderFields?.contains(where: { $0.key == "X-Environment-Key" }) ?? false)
XCTAssertNil(request.httpBody)
}

func testUserAgentHeader() throws {
let url = try XCTUnwrap(baseUrl)
let route = Router.getFlags
let request = try route.request(baseUrl: url, apiKey: apiKey)

// Verify User-Agent header is present
XCTAssertTrue(request.allHTTPHeaderFields?.contains(where: { $0.key == "User-Agent" }) ?? false)

// Verify User-Agent header format
let userAgent = request.allHTTPHeaderFields?["User-Agent"]
XCTAssertNotNil(userAgent)
XCTAssertTrue(userAgent?.hasPrefix("flagsmith-swift-ios-sdk/") ?? false)

// Verify the format is correct (should end with either a version number or "unknown")
let expectedPattern = "^flagsmith-swift-ios-sdk/([0-9]+\\.[0-9]+\\.[0-9]+|unknown)$"
let regex = try NSRegularExpression(pattern: expectedPattern)
let range = NSRange(location: 0, length: userAgent?.count ?? 0)
XCTAssertTrue(regex.firstMatch(in: userAgent ?? "", options: [], range: range) != nil,
"User-Agent should match pattern 'flagsmith-swift-ios-sdk/<version>' or 'flagsmith-swift-ios-sdk/unknown', got: \(userAgent ?? "nil")")
}

func testUserAgentHeaderFormat() {
// Test that the User-Agent format is correct
let userAgent = Flagsmith.userAgent
XCTAssertTrue(userAgent.hasPrefix("flagsmith-swift-ios-sdk/"))

// Should either have a version number or "unknown"
let versionPart = String(userAgent.dropFirst("flagsmith-swift-ios-sdk/".count))
XCTAssertTrue(versionPart == "unknown" || versionPart.range(of: #"^\d+\.\d+\.\d+$"#, options: .regularExpression) != nil,
"Version part should be 'unknown' or a semantic version number, got: \(versionPart)")
}
Comment on lines +30 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two tests seem to be crossing wires. This suggestion replaces them both with a simple assertion, though I haven't tested it. Let me know if you agree with it.

Suggested change
// Verify User-Agent header is present
XCTAssertTrue(request.allHTTPHeaderFields?.contains(where: { $0.key == "User-Agent" }) ?? false)
// Verify User-Agent header format
let userAgent = request.allHTTPHeaderFields?["User-Agent"]
XCTAssertNotNil(userAgent)
XCTAssertTrue(userAgent?.hasPrefix("flagsmith-swift-ios-sdk/") ?? false)
// Verify the format is correct (should end with a semantic version number)
let expectedPattern = "^flagsmith-swift-ios-sdk/[0-9]+\\.[0-9]+\\.[0-9]+$"
let regex = try NSRegularExpression(pattern: expectedPattern)
let range = NSRange(location: 0, length: userAgent?.count ?? 0)
let match = regex.firstMatch(in: userAgent ?? "", options: [], range: range)
let message = "User-Agent should match pattern 'flagsmith-swift-ios-sdk/<version>', got: \(userAgent ?? "nil")"
XCTAssertTrue(match != nil, message)
}
func testUserAgentHeaderFormat() {
// Test that the User-Agent format is correct
let userAgent = Flagsmith.userAgent
XCTAssertTrue(userAgent.hasPrefix("flagsmith-swift-ios-sdk/"))
// Should have a semantic version number (e.g., 3.8.4)
let versionPart = String(userAgent.dropFirst("flagsmith-swift-ios-sdk/".count))
XCTAssertTrue(versionPart.range(of: #"^\d+\.\d+\.\d+$"#, options: NSString.CompareOptions.regularExpression) != nil,
"Version part should be a semantic version number (e.g., 3.8.4), got: \(versionPart)")
// Should be the expected SDK version
// x-release-please-start-version
XCTAssertEqual(versionPart, "3.8.4", "Expected SDK version 3.8.4, got: \(versionPart)")
// x-release-please-end
}
// x-release-please-start-version
XCTAssertTrue(request.allHTTPHeaderFields?["User-Agent"], "flagsmith-swift-ios-sdk/3.8.4")
// x-release-please-end
}


func testGetIdentityRequest() throws {
let url = try XCTUnwrap(baseUrl)
Expand Down
Loading