Skip to content

Commit

Permalink
Add support for application tags (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 authored May 19, 2022
1 parent a212925 commit 5bd1b2f
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 24 deletions.
18 changes: 14 additions & 4 deletions ContractTests/Source/Controllers/SdkController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ final class SdkController {
}

// TODO(mmk) Need to hook up initialRetryDelayMs
}

if let polling = createInstance.configuration.polling {
} else if let polling = createInstance.configuration.polling {
config.streamingMode = .polling
if let baseUri = polling.baseUri {
config.baseUrl = URL(string: baseUri)!
}
Expand Down Expand Up @@ -68,7 +67,18 @@ final class SdkController {
}
}

// TODO(mmk) Handle tag parameters
if let tags = createInstance.configuration.tags {
var applicationInfo = ApplicationInfo()
if let id = tags.applicationId {
applicationInfo.applicationIdentifier(id)
}

if let verision = tags.applicationVersion {
applicationInfo.applicationVersion(verision)
}

config.applicationInfo = applicationInfo
}

let clientSide = createInstance.configuration.clientSide

Expand Down
20 changes: 0 additions & 20 deletions ContractTests/testharness-suppressions.txt
Original file line number Diff line number Diff line change
@@ -1,24 +1,4 @@
events/requests/method and headers
tags/stream requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/stream requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null}
tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""}
tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/poll requests/{"applicationId":null,"applicationVersion":null}
tags/poll requests/{"applicationId":null,"applicationVersion":""}
tags/poll requests/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/poll requests/{"applicationId":"","applicationVersion":null}
tags/poll requests/{"applicationId":"","applicationVersion":""}
tags/poll requests/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null}
tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""}
tags/poll requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/event posts/{"applicationId":null,"applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/event posts/{"applicationId":"","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":null}
tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":""}
tags/event posts/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}
tags/disallowed characters
evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate flag with detail
evaluation/parameterized/evaluationReasons=false/basic values - bool/flag1-bool/evaluate all flags
evaluation/parameterized/evaluationReasons=false/basic values - bool/flag2-bool/evaluate flag without detail
Expand Down
73 changes: 73 additions & 0 deletions LaunchDarkly/LaunchDarkly/Models/LDConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,76 @@ typealias MobileKey = String
*/
public typealias RequestHeaderTransform = (_ url: URL, _ headers: [String: String]) -> [String: String]

/// Defines application metadata.
///
/// These properties are optional and informational. They may be used in LaunchDarkly
/// analytics or other product features, but they do not affect feature flag evaluations.
public struct ApplicationInfo: Equatable {
private var applicationId: String
private var applicationVersion: String

public init() {
applicationId = ""
applicationVersion = ""
}

/// A unique identifier representing the application where the LaunchDarkly SDK is running.
///
/// This can be specified as any string value as long as it only uses the following characters:
/// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
/// characters will be ignored.
public mutating func applicationIdentifier(_ applicationId: String) {
if let error = validate(applicationId) {
Log.debug("applicationIdentifier \(error)")
return
}

self.applicationId = applicationId
}

/// A unique identifier representing the version of the application where the LaunchDarkly SDK
/// is running.
///
/// This can be specified as any string value as long as it only uses the following characters:
/// ASCII letters, ASCII digits, period, hyphen, underscore. A string containing any other
/// characters will be ignored.
public mutating func applicationVersion(_ applicationVersion: String) {
if let error = validate(applicationVersion) {
Log.debug("applicationVersion \(error)")
return
}

self.applicationVersion = applicationVersion
}

func buildTag() -> String {
var tags: [String] = []

if !applicationId.isEmpty {
tags.append("application-id/\(applicationId)")
}

if !applicationVersion.isEmpty {
tags.append("application-version/\(applicationVersion)")
}

return tags.lazy.joined(separator: " ")
}

private func validate(_ value: String) -> String? {
let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-")
if value.rangeOfCharacter(from: allowed.inverted) != nil {
return "contained invalid characters"
}

if value.count > 64 {
return "longer than 64 characters and was discarded"
}

return nil
}
}

/**
Use LDConfig to configure the LDClient. When initialized, a LDConfig contains the default values which can be changed as needed.
*/
Expand Down Expand Up @@ -169,6 +239,8 @@ public struct LDConfig {
public var flagPollingInterval: TimeInterval = Defaults.flagPollingInterval
/// The time interval between feature flag requests while running in the background. Used only for polling mode. (Default: 60 minutes)
public var backgroundFlagPollingInterval: TimeInterval = Defaults.backgroundFlagPollingInterval
/// The configuration for application metadata.
public var applicationInfo: ApplicationInfo? = nil

/**
Controls the method the SDK uses to keep feature flags updated. (Default: `.streaming`)
Expand Down Expand Up @@ -364,6 +436,7 @@ extension LDConfig: Equatable {
&& lhs.eventFlushInterval == rhs.eventFlushInterval
&& lhs.flagPollingInterval == rhs.flagPollingInterval
&& lhs.backgroundFlagPollingInterval == rhs.backgroundFlagPollingInterval
&& lhs.applicationInfo == rhs.applicationInfo
&& lhs.streamingMode == rhs.streamingMode
&& lhs.enableBackgroundUpdates == rhs.enableBackgroundUpdates
&& lhs.startOnline == rhs.startOnline
Expand Down
7 changes: 7 additions & 0 deletions LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct HTTPHeaders {
static let ifNoneMatch = "If-None-Match"
static let eventPayloadIDHeader = "X-LaunchDarkly-Payload-ID"
static let sdkWrapper = "X-LaunchDarkly-Wrapper"
static let tags = "X-LaunchDarkly-Tags"
}

struct HeaderValue {
Expand All @@ -24,12 +25,14 @@ struct HTTPHeaders {
private let authKey: String
private let userAgent: String
private let wrapperHeaderVal: String?
private let applicationTag: String

init(config: LDConfig, environmentReporter: EnvironmentReporting) {
self.mobileKey = config.mobileKey
self.additionalHeaders = config.additionalHeaders
self.userAgent = "\(environmentReporter.systemName)/\(environmentReporter.sdkVersion)"
self.authKey = "\(HeaderValue.apiKey) \(config.mobileKey)"
self.applicationTag = config.applicationInfo?.buildTag() ?? ""

if let wrapperName = config.wrapperName {
if let wrapperVersion = config.wrapperVersion {
Expand All @@ -50,6 +53,10 @@ struct HTTPHeaders {
headers[HeaderKey.sdkWrapper] = wrapperHeader
}

if !self.applicationTag.isEmpty {
headers[HeaderKey.tags] = self.applicationTag
}

return headers
}

Expand Down
23 changes: 23 additions & 0 deletions LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,27 @@ final class LDConfigSpec: XCTestCase {
XCTAssertEqual(config.enableBackgroundUpdates, operatingSystem.isBackgroundEnabled)
}
}

func testApplicationInfoGeneratesTagCorrectly() {
var applicationInfo = ApplicationInfo()
XCTAssertEqual("", applicationInfo.buildTag())

applicationInfo.applicationVersion("example-version")
XCTAssertEqual("application-version/example-version", applicationInfo.buildTag())

applicationInfo.applicationIdentifier("example-id")
XCTAssertEqual("application-id/example-id application-version/example-version", applicationInfo.buildTag())
}

func testApplicationInfoRejectsInvalidConfigurations() {
let values = ["", " ", "/", ":", "🐦", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890._-"]
var info = ApplicationInfo()

for value in values {
info.applicationIdentifier(value)
info.applicationVersion(value)

XCTAssertEqual("", info.buildTag())
}
}
}

0 comments on commit 5bd1b2f

Please sign in to comment.