From cb42f92df22a3d17fb165f5c0241e8f75f6c81a6 Mon Sep 17 00:00:00 2001 From: tanderson-ld <127344469+tanderson-ld@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:37:17 -0500 Subject: [PATCH] prepare 9.0.0 release (#298) ## [9.0.0] - 2023-08-02 ### Added: - Added Automatic Mobile Environment Attributes functionality which makes it simpler to target your mobile customers based on application name or version, or on device characteristics including manufacturer, model, operating system, locale, and so on. To learn more, read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes). ### Removed - Removed LDUser and related functionality. Use LDContext instead. To learn more, read https://docs.launchdarkly.com/home/contexts. --------- Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: torchhound <5600929+torchhound@users.noreply.github.com> Co-authored-by: Gavin Whelan Co-authored-by: Louis Chan Co-authored-by: Matthew Keeler Co-authored-by: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Co-authored-by: Ember Stevens Co-authored-by: Ember Stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: ld-repository-standards[bot] <113625520+ld-repository-standards[bot]@users.noreply.github.com> Co-authored-by: Kane Parkinson <93555788+kparkinson-ld@users.noreply.github.com> --- .jazzy.yaml | 2 - CHANGELOG.md | 7 + .../Source/Controllers/SdkController.swift | 38 ++- ContractTests/Source/Models/client.swift | 6 +- ContractTests/Source/Models/command.swift | 3 +- ContractTests/Source/Models/user.swift | 29 --- LaunchDarkly.podspec | 2 +- LaunchDarkly.xcodeproj/project.pbxproj | 200 ++++++++++----- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../GeneratedCode/mocks.generated.swift | 84 +++---- LaunchDarkly/LaunchDarkly/LDClient.swift | 69 ++---- .../Models/ConnectionInformation.swift | 2 +- .../Models/Context/LDContext.swift | 13 +- .../Models/Context/Modifier.swift | 145 +++++++++++ .../LaunchDarkly/Models/DiagnosticEvent.swift | 18 +- .../LaunchDarkly/Models/LDConfig.swift | 109 ++++++-- LaunchDarkly/LaunchDarkly/Models/LDUser.swift | 232 ------------------ .../LaunchDarkly/Models/UserAttribute.swift | 80 ------ .../Networking/DarklyService.swift | 4 +- .../LaunchDarkly/Networking/HTTPHeaders.swift | 6 +- .../ObjectiveC/ObjcLDApplicationInfo.swift | 22 +- .../ObjectiveC/ObjcLDClient.swift | 40 --- .../ObjectiveC/ObjcLDConfig.swift | 4 +- .../LaunchDarkly/ObjectiveC/ObjcLDUser.swift | 131 ---------- .../ServiceObjects/ClientServiceFactory.swift | 30 ++- .../ServiceObjects/DiagnosticReporter.swift | 5 +- .../ServiceObjects/EnvironmentReporter.swift | 156 +++--------- .../ApplicationInfoEnvironmentReporter.swift | 11 + .../EnvironmentReporterBuilder.swift | 53 ++++ .../EnvironmentReporterChainBase.swift | 37 +++ .../IOSEnvironmentReporter.swift | 9 + .../MacOSEnvironmentReporter.swift | 48 ++++ .../ReportingConsts.swift | 6 + .../SDKEnvironmentReporter.swift | 26 ++ .../SystemCapabilities.swift | 33 +++ .../TVOSEnvironmentReporter.swift | 9 + .../WatchOSEnvironmentReporter.swift | 9 + .../ServiceObjects/Throttler.swift | 4 +- .../LaunchDarklyTests/LDClientSpec.swift | 137 +++++++---- .../Mocks/ClientServiceMockFactory.swift | 6 +- .../Mocks/EnvironmentReportingMock.swift | 8 +- .../Mocks/LDConfigStub.swift | 6 +- .../LaunchDarklyTests/Mocks/LDUserStub.swift | 49 ---- .../Models/Context/LDContextCodableSpec.swift | 2 +- .../Models/Context/LDContextSpec.swift | 19 +- .../Models/Context/ModifierSpec.swift | 161 ++++++++++++ .../Models/DiagnosticEventSpec.swift | 44 ++-- .../Models/LDConfigSpec.swift | 86 ++++--- .../Models/User/LDUserSpec.swift | 109 -------- .../Models/User/LDUserToContextSpec.swift | 64 ----- .../Networking/DarklyServiceSpec.swift | 7 +- .../Networking/HTTPHeadersSpec.swift | 30 ++- .../Networking/URLRequestSpec.swift | 2 +- .../DiagnosticReporterSpec.swift | 2 +- .../EnvironmentReporterSpec.swift | 19 -- ...plicationInfoEnvironmentReporterSpec.swift | 20 ++ .../EnvironmentReporterChainBaseSpec.swift | 26 ++ .../IOSEnvironmentReporterSpec.swift | 19 ++ .../SDKEnvironmentReporterSpec.swift | 16 ++ .../WatchOSEnvironmentReporterSpec.swift | 37 +++ .../ServiceObjects/ThrottlerSpec.swift | 8 +- LaunchDarkly/LaunchDarklyTests/UtilSpec.swift | 14 ++ README.md | 6 +- 63 files changed, 1323 insertions(+), 1264 deletions(-) delete mode 100644 ContractTests/Source/Models/user.swift create mode 100644 LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 LaunchDarkly/LaunchDarkly/Models/Context/Modifier.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/LDUser.swift delete mode 100644 LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift delete mode 100644 LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporter.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBase.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporter.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/MacOSEnvironmentReporter.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ReportingConsts.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporter.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/TVOSEnvironmentReporter.swift create mode 100644 LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporter.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/Models/Context/ModifierSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift delete mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporterSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBaseSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporterSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporterSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporterSpec.swift create mode 100644 LaunchDarkly/LaunchDarklyTests/UtilSpec.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index e055ff36..a9b0ba52 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -19,7 +19,6 @@ custom_categories: - LDConfig - LDContext - LDContextBuilder - - LDUser - Reference - LDMultiContextBuilder - LDEvaluationDetail @@ -52,7 +51,6 @@ custom_categories: - ObjcLDReference - ObjcLDContext - ObjcLDChangedFlag - - ObjcLDUser - ObjcLDValue - ObjcLDValueType diff --git a/CHANGELOG.md b/CHANGELOG.md index 01d96b22..0b36348e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [9.0.0] - 2023-08-02 +### Added: +- Added Automatic Mobile Environment Attributes functionality which makes it simpler to target your mobile customers based on application name or version, or on device characteristics including manufacturer, model, operating system, locale, and so on. To learn more, read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes). + +### Removed +- Removed LDUser and related functionality. Use LDContext instead. To learn more, read https://docs.launchdarkly.com/home/contexts. + ## [8.2.0] - 2023-08-02 ### Changed: - Deprecated LDUser and related functionality. Use LDContext instead. To learn more, read https://docs.launchdarkly.com/home/contexts. diff --git a/ContractTests/Source/Controllers/SdkController.swift b/ContractTests/Source/Controllers/SdkController.swift index f3d5e394..ef01640e 100644 --- a/ContractTests/Source/Controllers/SdkController.swift +++ b/ContractTests/Source/Controllers/SdkController.swift @@ -22,7 +22,7 @@ final class SdkController: RouteCollection { "service-endpoints", "strongly-typed", "tags", - "user-type" + "auto-env-attributes" ] return StatusResponse( @@ -32,7 +32,9 @@ final class SdkController: RouteCollection { func createClient(_ req: Request) throws -> Response { let createInstance = try req.content.decode(CreateInstance.self) - var config = LDConfig(mobileKey: createInstance.configuration.credential) + let mobileKey = createInstance.configuration.credential + let autoEnvAttributes: AutoEnvAttributes = createInstance.configuration.clientSide.includeEnvironmentAttributes == true ? .enabled : .disabled + var config = LDConfig(mobileKey: mobileKey, autoEnvAttributes: autoEnvAttributes) config.enableBackgroundUpdates = true config.isDebugMode = true @@ -81,8 +83,16 @@ final class SdkController: RouteCollection { applicationInfo.applicationIdentifier(id) } - if let verision = tags.applicationVersion { - applicationInfo.applicationVersion(verision) + if let name = tags.applicationName { + applicationInfo.applicationName(name) + } + + if let version = tags.applicationVersion { + applicationInfo.applicationVersion(version) + } + + if let versionName = tags.applicationVersionName { + applicationInfo.applicationVersionName(versionName) } config.applicationInfo = applicationInfo @@ -101,14 +111,8 @@ final class SdkController: RouteCollection { let dispatchSemaphore = DispatchSemaphore(value: 0) let startWaitSeconds = (createInstance.configuration.startWaitTimeMs ?? 5_000) / 1_000 - if let context = clientSide.initialContext { - LDClient.start(config: config, context: context, startWaitSeconds: startWaitSeconds) { _ in - dispatchSemaphore.signal() - } - } else if let user = clientSide.initialUser { - LDClient.start(config: config, user: user, startWaitSeconds: startWaitSeconds) { _ in - dispatchSemaphore.signal() - } + LDClient.start(config: config, context: clientSide.initialContext, startWaitSeconds: startWaitSeconds) { _ in + dispatchSemaphore.signal() } dispatchSemaphore.wait() @@ -159,14 +163,8 @@ final class SdkController: RouteCollection { return CommandResponse.evaluateAll(result) case "identifyEvent": let semaphore = DispatchSemaphore(value: 0) - if let context = commandParameters.identifyEvent!.context { - client.identify(context: context) { - semaphore.signal() - } - } else if let user = commandParameters.identifyEvent!.user { - client.identify(user: user) { - semaphore.signal() - } + client.identify(context: commandParameters.identifyEvent!.context) { + semaphore.signal() } semaphore.wait() case "customEvent": diff --git a/ContractTests/Source/Models/client.swift b/ContractTests/Source/Models/client.swift index a072c4e7..4dedb957 100644 --- a/ContractTests/Source/Models/client.swift +++ b/ContractTests/Source/Models/client.swift @@ -39,12 +39,14 @@ struct EventParameters: Content { struct TagParameters: Content { var applicationId: String? + var applicationName: String? var applicationVersion: String? + var applicationVersionName: String? } struct ClientSideParameters: Content { - var initialContext: LDContext? - var initialUser: LDUser? + var initialContext: LDContext var evaluationReasons: Bool? var useReport: Bool? + var includeEnvironmentAttributes: Bool? } diff --git a/ContractTests/Source/Models/command.swift b/ContractTests/Source/Models/command.swift index 567f784c..86c91ae4 100644 --- a/ContractTests/Source/Models/command.swift +++ b/ContractTests/Source/Models/command.swift @@ -70,8 +70,7 @@ struct CustomEventParameters: Content { } struct IdentifyEventParameters: Content, Decodable { - var context: LDContext? - var user: LDUser? + var context: LDContext } struct ContextBuildParameters: Content, Decodable { diff --git a/ContractTests/Source/Models/user.swift b/ContractTests/Source/Models/user.swift deleted file mode 100644 index 51542c93..00000000 --- a/ContractTests/Source/Models/user.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import LaunchDarkly - -extension LDUser: Decodable { - - /// String keys associated with LDUser properties. - public enum CodingKeys: String, CodingKey { - /// Key names match the corresponding LDUser property - case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributes = "privateAttributeNames" - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - self.init() - - key = try values.decodeIfPresent(String.self, forKey: .key) ?? "" - name = try values.decodeIfPresent(String.self, forKey: .name) - firstName = try values.decodeIfPresent(String.self, forKey: .firstName) - lastName = try values.decodeIfPresent(String.self, forKey: .lastName) - country = try values.decodeIfPresent(String.self, forKey: .country) - ipAddress = try values.decodeIfPresent(String.self, forKey: .ipAddress) - email = try values.decodeIfPresent(String.self, forKey: .email) - avatar = try values.decodeIfPresent(String.self, forKey: .avatar) - custom = try values.decodeIfPresent([String: LDValue].self, forKey: .custom) ?? [:] - isAnonymous = try values.decodeIfPresent(Bool.self, forKey: .isAnonymous) ?? false - _ = try values.decodeIfPresent([String].self, forKey: .privateAttributes) - privateAttributes = (try values.decodeIfPresent([String].self, forKey: .privateAttributes) ?? []).map { UserAttribute.forName($0) } - } -} diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 57460160..20cf2c2e 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "8.2.0" + ld.version = "9.0.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index 2ba43d3e..d83e954d 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -15,6 +15,12 @@ 29FE1299280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; 29FE129A280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; 29FE129B280413D4008CC918 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FE1297280413D4008CC918 /* Util.swift */; }; + 3D3AB9432A4F16FE003AECF1 /* ReportingConsts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3AB9422A4F16FE003AECF1 /* ReportingConsts.swift */; }; + 3D3AB9442A4F16FE003AECF1 /* ReportingConsts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3AB9422A4F16FE003AECF1 /* ReportingConsts.swift */; }; + 3D3AB9452A4F16FE003AECF1 /* ReportingConsts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3AB9422A4F16FE003AECF1 /* ReportingConsts.swift */; }; + 3D3AB9462A4F16FE003AECF1 /* ReportingConsts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3AB9422A4F16FE003AECF1 /* ReportingConsts.swift */; }; + 3D3AB9482A570F3A003AECF1 /* ModifierSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3AB9472A570F3A003AECF1 /* ModifierSpec.swift */; }; + 3D9A12582A73236800698B8D /* UtilSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9A12572A73236800698B8D /* UtilSpec.swift */; }; 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */; }; 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */; }; 830DB3AE2239B54900D65D25 /* URLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830DB3AD2239B54900D65D25 /* URLResponse.swift */; }; @@ -126,7 +132,6 @@ 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 8372668E20D4439600BD1088 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8372668B20D4439600BD1088 /* DateFormatter.swift */; }; 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837406D321F760640087B22B /* LDTimerSpec.swift */; }; - 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */; }; 837EF3742059C237009D628A /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837EF3732059C237009D628A /* Log.swift */; }; 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831D8B761F72A4B400ED65E8 /* FlagSynchronizerSpec.swift */; }; 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838F96731FB9F024009CFC45 /* LDClientSpec.swift */; }; @@ -183,6 +188,10 @@ 83F0A5641FB5F33800550A95 /* LDConfigSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */; }; 83FEF8DD1F266742001CF12C /* FlagSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */; }; 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */; }; + A3047D642A606B6000F568E0 /* SDKEnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3047D5D2A606B6000F568E0 /* SDKEnvironmentReporterSpec.swift */; }; + A3047D652A606B6000F568E0 /* IOSEnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3047D5E2A606B6000F568E0 /* IOSEnvironmentReporterSpec.swift */; }; + A3047D662A606B6000F568E0 /* EnvironmentReporterChainBaseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3047D5F2A606B6000F568E0 /* EnvironmentReporterChainBaseSpec.swift */; }; + A3047D692A606B6000F568E0 /* ApplicationInfoEnvironmentReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3047D622A606B6000F568E0 /* ApplicationInfoEnvironmentReporterSpec.swift */; }; A30EF4ED28C24A9A00CD220E /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A30EF4EC28C24A9A00CD220E /* LDSwiftEventSource */; }; A30EF4EF28C24AA400CD220E /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A30EF4EE28C24AA400CD220E /* LDSwiftEventSource */; }; A30EF4F128C24AAD00CD220E /* LDSwiftEventSource in Frameworks */ = {isa = PBXBuildFile; productRef = A30EF4F028C24AAD00CD220E /* LDSwiftEventSource */; }; @@ -203,22 +212,35 @@ A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; }; - A349D0332926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; - A349D0342926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; - A349D0352926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; - A349D0362926CA0600DD5DE9 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0312926CA0600DD5DE9 /* UserAttribute.swift */; }; - A349D0372926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; - A349D0382926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; - A349D0392926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; - A349D03A2926CA0600DD5DE9 /* LDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D0322926CA0600DD5DE9 /* LDUser.swift */; }; - A349D03C2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; - A349D03D2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; - A349D03E2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; - A349D03F2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */; }; - A355845529281CD70023D8EE /* LDUserSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */; }; - A355845729281CF00023D8EE /* LDUserStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A355845629281CF00023D8EE /* LDUserStub.swift */; }; - A355845929281E610023D8EE /* LDUserToContextSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A355845829281E610023D8EE /* LDUserToContextSpec.swift */; }; A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */; }; + A358D6D12A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; }; + A358D6D22A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; }; + A358D6D32A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; }; + A358D6D42A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; }; + A358D6D72A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */; }; + A358D6D82A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */; }; + A358D6D92A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */; }; + A358D6DA2A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */; }; + A358D6DD2A4DE7D600270C60 /* IOSEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6DC2A4DE7D600270C60 /* IOSEnvironmentReporter.swift */; }; + A358D6E42A4DE98300270C60 /* MacOSEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6E12A4DE98300270C60 /* MacOSEnvironmentReporter.swift */; }; + A358D6EF2A4DE9A600270C60 /* TVOSEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6EB2A4DE9A600270C60 /* TVOSEnvironmentReporter.swift */; }; + A358D6F02A4DE9EB00270C60 /* WatchOSEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6E62A4DE99B00270C60 /* WatchOSEnvironmentReporter.swift */; }; + A358D6F22A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F12A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift */; }; + A358D6F32A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F12A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift */; }; + A358D6F42A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F12A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift */; }; + A358D6F52A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F12A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift */; }; + A358D6F72A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F62A4DF1D500270C60 /* SDKEnvironmentReporter.swift */; }; + A358D6F82A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F62A4DF1D500270C60 /* SDKEnvironmentReporter.swift */; }; + A358D6F92A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F62A4DF1D500270C60 /* SDKEnvironmentReporter.swift */; }; + A358D6FA2A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6F62A4DF1D500270C60 /* SDKEnvironmentReporter.swift */; }; + A3599E882A4B4AD400DB5C67 /* Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3599E872A4B4AD400DB5C67 /* Modifier.swift */; }; + A3599E892A4B4AD400DB5C67 /* Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3599E872A4B4AD400DB5C67 /* Modifier.swift */; }; + A3599E8A2A4B4AD400DB5C67 /* Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3599E872A4B4AD400DB5C67 /* Modifier.swift */; }; + A3599E8B2A4B4AD400DB5C67 /* Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3599E872A4B4AD400DB5C67 /* Modifier.swift */; }; + A35AD4602A619E45005A8DCB /* SystemCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35AD45F2A619E45005A8DCB /* SystemCapabilities.swift */; }; + A35AD4612A619E45005A8DCB /* SystemCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35AD45F2A619E45005A8DCB /* SystemCapabilities.swift */; }; + A35AD4622A619E45005A8DCB /* SystemCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35AD45F2A619E45005A8DCB /* SystemCapabilities.swift */; }; + A35AD4632A619E45005A8DCB /* SystemCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35AD45F2A619E45005A8DCB /* SystemCapabilities.swift */; }; A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; A36EDFCA2853883400D91B05 /* ObjcLDReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36EDFC72853883400D91B05 /* ObjcLDReference.swift */; }; @@ -346,6 +368,9 @@ /* Begin PBXFileReference section */ 29F9D19D2812E005008D12C0 /* ObjcLDValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDValue.swift; sourceTree = ""; }; 29FE1297280413D4008CC918 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; + 3D3AB9422A4F16FE003AECF1 /* ReportingConsts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportingConsts.swift; sourceTree = ""; }; + 3D3AB9472A570F3A003AECF1 /* ModifierSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierSpec.swift; sourceTree = ""; }; + 3D9A12572A73236800698B8D /* UtilSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilSpec.swift; sourceTree = ""; }; 830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = ""; }; 830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = ""; }; 830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = ""; }; @@ -389,7 +414,6 @@ 835E1D421F685AC900184DB4 /* ObjcLDChangedFlag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDChangedFlag.swift; sourceTree = ""; }; 8372668B20D4439600BD1088 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 837406D321F760640087B22B /* LDTimerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDTimerSpec.swift; sourceTree = ""; }; - 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterSpec.swift; sourceTree = ""; }; 837EF3732059C237009D628A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 838F96731FB9F024009CFC45 /* LDClientSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDClientSpec.swift; sourceTree = ""; }; 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientServiceFactory.swift; sourceTree = ""; }; @@ -418,6 +442,10 @@ 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigSpec.swift; sourceTree = ""; }; 83FEF8DC1F266742001CF12C /* FlagSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagSynchronizer.swift; sourceTree = ""; }; 83FEF8DE1F2667E4001CF12C /* EventReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventReporter.swift; sourceTree = ""; }; + A3047D5D2A606B6000F568E0 /* SDKEnvironmentReporterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDKEnvironmentReporterSpec.swift; sourceTree = ""; }; + A3047D5E2A606B6000F568E0 /* IOSEnvironmentReporterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IOSEnvironmentReporterSpec.swift; sourceTree = ""; }; + A3047D5F2A606B6000F568E0 /* EnvironmentReporterChainBaseSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterChainBaseSpec.swift; sourceTree = ""; }; + A3047D622A606B6000F568E0 /* ApplicationInfoEnvironmentReporterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationInfoEnvironmentReporterSpec.swift; sourceTree = ""; }; A31088142837DC0400184942 /* Reference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reference.swift; sourceTree = ""; }; A31088152837DC0400184942 /* Kind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Kind.swift; sourceTree = ""; }; A31088162837DC0400184942 /* LDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDContext.swift; sourceTree = ""; }; @@ -425,13 +453,17 @@ A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = ""; }; A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.swift; sourceTree = ""; }; - A349D0312926CA0600DD5DE9 /* UserAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = ""; }; - A349D0322926CA0600DD5DE9 /* LDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUser.swift; sourceTree = ""; }; - A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDUser.swift; sourceTree = ""; }; - A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserSpec.swift; sourceTree = ""; }; - A355845629281CF00023D8EE /* LDUserStub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserStub.swift; sourceTree = ""; }; - A355845829281E610023D8EE /* LDUserToContextSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LDUserToContextSpec.swift; sourceTree = ""; }; A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextCodableSpec.swift; sourceTree = ""; }; + A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterChainBase.swift; sourceTree = ""; }; + A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationInfoEnvironmentReporter.swift; sourceTree = ""; }; + A358D6DC2A4DE7D600270C60 /* IOSEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSEnvironmentReporter.swift; sourceTree = ""; }; + A358D6E12A4DE98300270C60 /* MacOSEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacOSEnvironmentReporter.swift; sourceTree = ""; }; + A358D6E62A4DE99B00270C60 /* WatchOSEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchOSEnvironmentReporter.swift; sourceTree = ""; }; + A358D6EB2A4DE9A600270C60 /* TVOSEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVOSEnvironmentReporter.swift; sourceTree = ""; }; + A358D6F12A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterBuilder.swift; sourceTree = ""; }; + A358D6F62A4DF1D500270C60 /* SDKEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKEnvironmentReporter.swift; sourceTree = ""; }; + A3599E872A4B4AD400DB5C67 /* Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modifier.swift; sourceTree = ""; }; + A35AD45F2A619E45005A8DCB /* SystemCapabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemCapabilities.swift; sourceTree = ""; }; A36EDFC72853883400D91B05 /* ObjcLDReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDReference.swift; sourceTree = ""; }; A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = ""; }; A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDApplicationInfo.swift; sourceTree = ""; }; @@ -521,8 +553,8 @@ 831D8B751F72A48900ED65E8 /* ServiceObjects */ = { isa = PBXGroup; children = ( + A3047D5B2A606A0000F568E0 /* EnvironmentReporting */, B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */, - 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */, 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */, 83B8C2441FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift */, 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */, @@ -622,6 +654,7 @@ isa = PBXGroup; children = ( 838F96731FB9F024009CFC45 /* LDClientSpec.swift */, + 3D9A12572A73236800698B8D /* UtilSpec.swift */, 83EF67911F9945CE00403126 /* Models */, 83CFE7CF1F7AD89D0010544E /* Mocks */, 831D8B751F72A48900ED65E8 /* ServiceObjects */, @@ -638,8 +671,6 @@ 8354EFE61F263E4200C05156 /* Models */ = { isa = PBXGroup; children = ( - A349D0322926CA0600DD5DE9 /* LDUser.swift */, - A349D0312926CA0600DD5DE9 /* UserAttribute.swift */, A31088132837DC0400184942 /* Context */, C408884823033B7500420721 /* ConnectionInformation.swift */, B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */, @@ -653,7 +684,6 @@ 835E1D341F63332C00184DB4 /* ObjectiveC */ = { isa = PBXGroup; children = ( - A349D03B2926CA1800DD5DE9 /* ObjcLDUser.swift */, A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */, 835E1D3C1F63450A00184DB4 /* ObjcLDClient.swift */, 835E1D3D1F63450A00184DB4 /* ObjcLDConfig.swift */, @@ -678,7 +708,6 @@ 83CFE7CF1F7AD89D0010544E /* Mocks */ = { isa = PBXGroup; children = ( - A355845629281CF00023D8EE /* LDUserStub.swift */, 83CFE7D01F7AD8DC0010544E /* DarklyServiceMock.swift */, 832307A71F7DA61B0029815A /* LDEventSourceMock.swift */, 832307A91F7ECA630029815A /* LDConfigStub.swift */, @@ -761,7 +790,6 @@ 83EF67911F9945CE00403126 /* Models */ = { isa = PBXGroup; children = ( - A349D06A2926CB1100DD5DE9 /* User */, A31088232837DCA900184942 /* Context */, 83F0A5631FB5F33800550A95 /* LDConfigSpec.swift */, 83EBCBA720D9A251003A7142 /* FeatureFlag */, @@ -774,6 +802,7 @@ 83FEF8D91F2666BF001CF12C /* ServiceObjects */ = { isa = PBXGroup; children = ( + A358D6CF2A4DD45000270C60 /* EnvironmentReporting */, 8354AC742243168800CDE602 /* Cache */, 838F96771FBA504A009CFC45 /* ClientServiceFactory.swift */, 83B1D7C82073F354006D1B1C /* CwlSysctl.swift */, @@ -791,12 +820,24 @@ path = ServiceObjects; sourceTree = ""; }; + A3047D5B2A606A0000F568E0 /* EnvironmentReporting */ = { + isa = PBXGroup; + children = ( + A3047D622A606B6000F568E0 /* ApplicationInfoEnvironmentReporterSpec.swift */, + A3047D5F2A606B6000F568E0 /* EnvironmentReporterChainBaseSpec.swift */, + A3047D5E2A606B6000F568E0 /* IOSEnvironmentReporterSpec.swift */, + A3047D5D2A606B6000F568E0 /* SDKEnvironmentReporterSpec.swift */, + ); + path = EnvironmentReporting; + sourceTree = ""; + }; A31088132837DC0400184942 /* Context */ = { isa = PBXGroup; children = ( A31088142837DC0400184942 /* Reference.swift */, A31088152837DC0400184942 /* Kind.swift */, A31088162837DC0400184942 /* LDContext.swift */, + A3599E872A4B4AD400DB5C67 /* Modifier.swift */, ); path = Context; sourceTree = ""; @@ -808,17 +849,26 @@ A31088252837DCA900184942 /* ReferenceSpec.swift */, A31088262837DCA900184942 /* KindSpec.swift */, A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */, + 3D3AB9472A570F3A003AECF1 /* ModifierSpec.swift */, ); path = Context; sourceTree = ""; }; - A349D06A2926CB1100DD5DE9 /* User */ = { + A358D6CF2A4DD45000270C60 /* EnvironmentReporting */ = { isa = PBXGroup; children = ( - A355845829281E610023D8EE /* LDUserToContextSpec.swift */, - A349D06B2926CB2600DD5DE9 /* LDUserSpec.swift */, - ); - path = User; + 3D3AB9422A4F16FE003AECF1 /* ReportingConsts.swift */, + A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */, + A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */, + A358D6DC2A4DE7D600270C60 /* IOSEnvironmentReporter.swift */, + A358D6E12A4DE98300270C60 /* MacOSEnvironmentReporter.swift */, + A358D6E62A4DE99B00270C60 /* WatchOSEnvironmentReporter.swift */, + A358D6EB2A4DE9A600270C60 /* TVOSEnvironmentReporter.swift */, + A358D6F12A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift */, + A358D6F62A4DF1D500270C60 /* SDKEnvironmentReporter.swift */, + A35AD45F2A619E45005A8DCB /* SystemCapabilities.swift */, + ); + path = EnvironmentReporting; sourceTree = ""; }; B467790E24D8AECA00897F00 /* Frameworks */ = { @@ -1181,13 +1231,14 @@ 8311885F2113AE2D00D77CB5 /* HTTPURLRequest.swift in Sources */, A36EDFD02853C50B00D91B05 /* ObjcLDContext.swift in Sources */, B4C9D4362489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, - A349D03A2926CA0600DD5DE9 /* LDUser.swift in Sources */, + A358D6DA2A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */, 831188452113ADC500D77CB5 /* LDClient.swift in Sources */, - A349D03F2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, A310881E2837DC0400184942 /* Kind.swift in Sources */, A310881A2837DC0400184942 /* Reference.swift in Sources */, + 3D3AB9462A4F16FE003AECF1 /* ReportingConsts.swift in Sources */, 831188522113ADF700D77CB5 /* KeyedValueCache.swift in Sources */, 831188582113AE0F00D77CB5 /* EventReporter.swift in Sources */, + A358D6F52A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */, 8311885D2113AE2500D77CB5 /* DarklyService.swift in Sources */, 831188692113AE5900D77CB5 /* ObjcLDConfig.swift in Sources */, A31088222837DC0400184942 /* LDContext.swift in Sources */, @@ -1197,11 +1248,13 @@ C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831188592113AE1200D77CB5 /* FlagStore.swift in Sources */, C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */, + A358D6EF2A4DE9A600270C60 /* TVOSEnvironmentReporter.swift in Sources */, 29FE129B280413D4008CC918 /* Util.swift in Sources */, 831188652113AE4600D77CB5 /* Date.swift in Sources */, 831188672113AE4D00D77CB5 /* Thread.swift in Sources */, C443A40823145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 29F9D1A12812E005008D12C0 /* ObjcLDValue.swift in Sources */, + A3599E8B2A4B4AD400DB5C67 /* Modifier.swift in Sources */, 8311885C2113AE2200D77CB5 /* HTTPHeaders.swift in Sources */, 831188562113AE0800D77CB5 /* FlagSynchronizer.swift in Sources */, 8311884A2113ADD700D77CB5 /* FeatureFlag.swift in Sources */, @@ -1216,9 +1269,11 @@ 8311885B2113AE1D00D77CB5 /* Throttler.swift in Sources */, 8311884E2113ADE500D77CB5 /* Event.swift in Sources */, A36EDFCB2853883400D91B05 /* ObjcLDReference.swift in Sources */, - A349D0362926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 832D68A5224A38FC005F052A /* CacheConverter.swift in Sources */, + A35AD4632A619E45005A8DCB /* SystemCapabilities.swift in Sources */, + A358D6FA2A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */, 831188432113ADBE00D77CB5 /* LDCommon.swift in Sources */, + A358D6D42A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */, B4C9D4312489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 830DB3B12239B54900D65D25 /* URLResponse.swift in Sources */, 831188512113ADF400D77CB5 /* ClientServiceFactory.swift in Sources */, @@ -1242,38 +1297,42 @@ 831EF34520655E730001C643 /* LDClient.swift in Sources */, 830DB3B02239B54900D65D25 /* URLResponse.swift in Sources */, B4C9D4352489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, + A358D6F92A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */, 831EF34A20655E730001C643 /* FeatureFlag.swift in Sources */, C443A40C2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831EF34B20655E730001C643 /* LDChangedFlag.swift in Sources */, A36EDFCA2853883400D91B05 /* ObjcLDReference.swift in Sources */, 8354AC722243166900CDE602 /* FeatureFlagCache.swift in Sources */, A310881D2837DC0400184942 /* Kind.swift in Sources */, + A35AD4622A619E45005A8DCB /* SystemCapabilities.swift in Sources */, C443A40423145FBE00145710 /* ConnectionInformation.swift in Sources */, 832D68A4224A38FC005F052A /* CacheConverter.swift in Sources */, 831EF34C20655E730001C643 /* FlagChangeObserver.swift in Sources */, 831EF34D20655E730001C643 /* FlagsUnchangedObserver.swift in Sources */, A31088192837DC0400184942 /* Reference.swift in Sources */, 831EF34E20655E730001C643 /* Event.swift in Sources */, - A349D03E2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */, 831EF35120655E730001C643 /* KeyedValueCache.swift in Sources */, + A358D6D32A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */, 831AAE2E20A9E4F600B46DBA /* Throttler.swift in Sources */, 831EF35520655E730001C643 /* FlagSynchronizer.swift in Sources */, - A349D0392926CA0600DD5DE9 /* LDUser.swift in Sources */, + A358D6F42A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */, B4C9D4302489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 831EF35620655E730001C643 /* FlagChangeNotifier.swift in Sources */, + A358D6D92A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */, 831EF35720655E730001C643 /* EventReporter.swift in Sources */, 831EF35820655E730001C643 /* FlagStore.swift in Sources */, 831EF35920655E730001C643 /* Log.swift in Sources */, - A349D0352926CA0600DD5DE9 /* UserAttribute.swift in Sources */, + A358D6E42A4DE98300270C60 /* MacOSEnvironmentReporter.swift in Sources */, 831EF35A20655E730001C643 /* HTTPHeaders.swift in Sources */, 831EF35B20655E730001C643 /* DarklyService.swift in Sources */, 831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */, C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */, 29FE129A280413D4008CC918 /* Util.swift in Sources */, 831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */, + 3D3AB9452A4F16FE003AECF1 /* ReportingConsts.swift in Sources */, 835E4C54206BDF8D004C6E6C /* EnvironmentReporter.swift in Sources */, 29F9D1A02812E005008D12C0 /* ObjcLDValue.swift in Sources */, 8372668E20D4439600BD1088 /* DateFormatter.swift in Sources */, @@ -1282,6 +1341,7 @@ 83EBCBB520DABE1B003A7142 /* FlagRequestTracker.swift in Sources */, 8347BB0E21F147E100E56BCD /* LDTimer.swift in Sources */, B495A8A42787762C0051977C /* LDClientVariation.swift in Sources */, + A3599E8A2A4B4AD400DB5C67 /* Modifier.swift in Sources */, 831EF36320655E730001C643 /* Date.swift in Sources */, 831EF36520655E730001C643 /* Thread.swift in Sources */, 83B1D7C92073F354006D1B1C /* CwlSysctl.swift in Sources */, @@ -1304,13 +1364,14 @@ 830BF933202D188E006DF9B1 /* HTTPURLRequest.swift in Sources */, B4C9D4332489C8FD004A9B03 /* DiagnosticCache.swift in Sources */, A36EDFCD2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, - A349D0372926CA0600DD5DE9 /* LDUser.swift in Sources */, + A358D6D72A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */, 8354EFE51F263DAC00C05156 /* FeatureFlag.swift in Sources */, - A349D03C2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, 8372668C20D4439600BD1088 /* DateFormatter.swift in Sources */, A310881B2837DC0400184942 /* Kind.swift in Sources */, + 3D3AB9432A4F16FE003AECF1 /* ReportingConsts.swift in Sources */, A31088172837DC0400184942 /* Reference.swift in Sources */, 8354EFE21F26380700C05156 /* Event.swift in Sources */, + A358D6F22A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */, C408884923033B7500420721 /* ConnectionInformation.swift in Sources */, 831D8B721F71D3E700ED65E8 /* DarklyService.swift in Sources */, A310881F2837DC0400184942 /* LDContext.swift in Sources */, @@ -1325,9 +1386,11 @@ C443A40A2315AA4D00145710 /* NetworkReporter.swift in Sources */, 831D8B741F72994600ED65E8 /* FlagStore.swift in Sources */, 29F9D19E2812E005008D12C0 /* ObjcLDValue.swift in Sources */, + A3599E882A4B4AD400DB5C67 /* Modifier.swift in Sources */, 8358F2601F476AD800ECE1AF /* FlagChangeNotifier.swift in Sources */, 83FEF8DF1F2667E4001CF12C /* EventReporter.swift in Sources */, 831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */, + A358D6DD2A4DE7D600270C60 /* IOSEnvironmentReporter.swift in Sources */, 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */, 8354EFE01F26380700C05156 /* LDClient.swift in Sources */, 831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */, @@ -1339,9 +1402,11 @@ 8354AC702243166900CDE602 /* FeatureFlagCache.swift in Sources */, A36EDFC82853883400D91B05 /* ObjcLDReference.swift in Sources */, 8358F2621F47747F00ECE1AF /* FlagChangeObserver.swift in Sources */, - A349D0332926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 832D68A2224A38FC005F052A /* CacheConverter.swift in Sources */, + A35AD4602A619E45005A8DCB /* SystemCapabilities.swift in Sources */, + A358D6F72A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */, 835E1D401F63450A00184DB4 /* ObjcLDConfig.swift in Sources */, + A358D6D12A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */, 83DDBEFE1FA24F9600E428B6 /* Date.swift in Sources */, B4C9D42E2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, 83B8C2471FE4071F0082B8A9 /* HTTPURLResponse.swift in Sources */, @@ -1357,6 +1422,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A3047D662A606B6000F568E0 /* EnvironmentReporterChainBaseSpec.swift in Sources */, A31088272837DCA900184942 /* LDContextSpec.swift in Sources */, 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */, 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */, @@ -1365,10 +1431,9 @@ 83DDBF001FA2589900E428B6 /* FlagStoreSpec.swift in Sources */, B4F689142497B2FC004D3CE0 /* DiagnosticEventSpec.swift in Sources */, 83396BC91F7C3711000E256E /* DarklyServiceSpec.swift in Sources */, + 3D9A12582A73236800698B8D /* UtilSpec.swift in Sources */, 83EF67931F9945E800403126 /* EventSpec.swift in Sources */, - 837E38C921E804ED0008A50C /* EnvironmentReporterSpec.swift in Sources */, 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */, - A355845729281CF00023D8EE /* LDUserStub.swift in Sources */, 831AAE3020A9E75D00B46DBA /* ThrottlerSpec.swift in Sources */, 832D68AC224B3321005F052A /* CacheConverterSpec.swift in Sources */, 838F96741FB9F024009CFC45 /* LDClientSpec.swift in Sources */, @@ -1383,11 +1448,14 @@ 832307AA1F7ECA630029815A /* LDConfigStub.swift in Sources */, A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */, 8354AC77224316F800CDE602 /* FeatureFlagCacheSpec.swift in Sources */, + A3047D652A606B6000F568E0 /* IOSEnvironmentReporterSpec.swift in Sources */, 83EBCBB120D9C7B5003A7142 /* FlagCounterSpec.swift in Sources */, + A3047D642A606B6000F568E0 /* SDKEnvironmentReporterSpec.swift in Sources */, 83B8C2451FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift in Sources */, 8335299E1FC37727001166F8 /* FlagMaintainingMock.swift in Sources */, + A3047D692A606B6000F568E0 /* ApplicationInfoEnvironmentReporterSpec.swift in Sources */, + 3D3AB9482A570F3A003AECF1 /* ModifierSpec.swift in Sources */, 83383A5120460DD30024D975 /* SynchronizingErrorSpec.swift in Sources */, - A355845929281E610023D8EE /* LDUserToContextSpec.swift in Sources */, 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */, 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, @@ -1396,7 +1464,6 @@ 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, 832307A81F7DA61B0029815A /* LDEventSourceMock.swift in Sources */, - A355845529281CD70023D8EE /* LDUserSpec.swift in Sources */, 838F967A1FBA551A009CFC45 /* ClientServiceMockFactory.swift in Sources */, A31088292837DCA900184942 /* KindSpec.swift in Sources */, ); @@ -1414,17 +1481,19 @@ 83D9EC7C2062DEAB004D7FA6 /* FeatureFlag.swift in Sources */, A36EDFCE2853C50B00D91B05 /* ObjcLDContext.swift in Sources */, 8372668D20D4439600BD1088 /* DateFormatter.swift in Sources */, - A349D0382926CA0600DD5DE9 /* LDUser.swift in Sources */, + A358D6D82A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift in Sources */, 83D9EC7D2062DEAB004D7FA6 /* LDChangedFlag.swift in Sources */, - A349D03D2926CA1800DD5DE9 /* ObjcLDUser.swift in Sources */, A310881C2837DC0400184942 /* Kind.swift in Sources */, A31088182837DC0400184942 /* Reference.swift in Sources */, + 3D3AB9442A4F16FE003AECF1 /* ReportingConsts.swift in Sources */, 83D9EC7E2062DEAB004D7FA6 /* FlagChangeObserver.swift in Sources */, 83D9EC7F2062DEAB004D7FA6 /* FlagsUnchangedObserver.swift in Sources */, + A358D6F32A4DEB4C00270C60 /* EnvironmentReporterBuilder.swift in Sources */, 83D9EC802062DEAB004D7FA6 /* Event.swift in Sources */, 83D9EC822062DEAB004D7FA6 /* ClientServiceFactory.swift in Sources */, A31088202837DC0400184942 /* LDContext.swift in Sources */, 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, + A358D6F02A4DE9EB00270C60 /* WatchOSEnvironmentReporter.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, 83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */, @@ -1435,6 +1504,7 @@ 83D9EC892062DEAB004D7FA6 /* EventReporter.swift in Sources */, 83D9EC8A2062DEAB004D7FA6 /* FlagStore.swift in Sources */, 29F9D19F2812E005008D12C0 /* ObjcLDValue.swift in Sources */, + A3599E892A4B4AD400DB5C67 /* Modifier.swift in Sources */, 83D9EC8B2062DEAB004D7FA6 /* Log.swift in Sources */, 83D9EC8C2062DEAB004D7FA6 /* HTTPHeaders.swift in Sources */, C443A40623145FED00145710 /* ConnectionInformationStore.swift in Sources */, @@ -1449,9 +1519,11 @@ B4C9D4392489E20A004A9B03 /* DiagnosticReporter.swift in Sources */, B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */, A36EDFC92853883400D91B05 /* ObjcLDReference.swift in Sources */, - A349D0342926CA0600DD5DE9 /* UserAttribute.swift in Sources */, 83D9EC952062DEAB004D7FA6 /* Date.swift in Sources */, + A35AD4612A619E45005A8DCB /* SystemCapabilities.swift in Sources */, + A358D6F82A4DF1D500270C60 /* SDKEnvironmentReporter.swift in Sources */, 832D68A3224A38FC005F052A /* CacheConverter.swift in Sources */, + A358D6D22A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */, 83D9EC972062DEAB004D7FA6 /* Thread.swift in Sources */, 83D9EC982062DEAB004D7FA6 /* ObjcLDClient.swift in Sources */, B4C9D42F2489B5FF004A9B03 /* DiagnosticEvent.swift in Sources */, @@ -1503,7 +1575,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1526,7 +1598,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1549,7 +1621,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1570,7 +1642,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1613,11 +1685,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DYLIB_COMPATIBILITY_VERSION = 8.0.0; - DYLIB_CURRENT_VERSION = 8.2.0; + DYLIB_COMPATIBILITY_VERSION = 9.0.0; + DYLIB_CURRENT_VERSION = 9.0.0; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - FRAMEWORK_VERSION = E; + FRAMEWORK_VERSION = F; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1684,11 +1756,11 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_COMPATIBILITY_VERSION = 8.0.0; - DYLIB_CURRENT_VERSION = 8.2.0; + DYLIB_COMPATIBILITY_VERSION = 9.0.0; + DYLIB_CURRENT_VERSION = 9.0.0; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_VERSION = E; + FRAMEWORK_VERSION = F; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1724,7 +1796,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1744,7 +1816,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1786,7 +1858,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; @@ -1808,7 +1880,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 8.2.0; + MARKETING_VERSION = 9.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; diff --git a/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/LaunchDarkly.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 4a0ba753..3413895e 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -110,6 +110,15 @@ final class DiagnosticReportingMock: DiagnosticReporting { // MARK: - EnvironmentReportingMock final class EnvironmentReportingMock: EnvironmentReporting { + var applicationInfoSetCount = 0 + var setApplicationInfoCallback: (() throws -> Void)? + var applicationInfo: ApplicationInfo = Constants.applicationInfo { + didSet { + applicationInfoSetCount += 1 + try! setApplicationInfoCallback?() + } + } + var isDebugBuildSetCount = 0 var setIsDebugBuildCallback: (() throws -> Void)? var isDebugBuild: Bool = true { @@ -119,12 +128,12 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - var deviceTypeSetCount = 0 - var setDeviceTypeCallback: (() throws -> Void)? - var deviceType: String = Constants.deviceType { + var deviceModelSetCount = 0 + var setDeviceModelCallback: (() throws -> Void)? + var deviceModel: String = Constants.deviceModel { didSet { - deviceTypeSetCount += 1 - try! setDeviceTypeCallback?() + deviceModelSetCount += 1 + try! setDeviceModelCallback?() } } @@ -137,42 +146,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - var systemNameSetCount = 0 - var setSystemNameCallback: (() throws -> Void)? - var systemName: String = Constants.systemName { - didSet { - systemNameSetCount += 1 - try! setSystemNameCallback?() - } - } - - var operatingSystemSetCount = 0 - var setOperatingSystemCallback: (() throws -> Void)? - var operatingSystem: OperatingSystem = .iOS { - didSet { - operatingSystemSetCount += 1 - try! setOperatingSystemCallback?() - } - } - - var backgroundNotificationSetCount = 0 - var setBackgroundNotificationCallback: (() throws -> Void)? - var backgroundNotification: Notification.Name? = EnvironmentReporter().backgroundNotification { - didSet { - backgroundNotificationSetCount += 1 - try! setBackgroundNotificationCallback?() - } - } - - var foregroundNotificationSetCount = 0 - var setForegroundNotificationCallback: (() throws -> Void)? - var foregroundNotification: Notification.Name? = EnvironmentReporter().foregroundNotification { - didSet { - foregroundNotificationSetCount += 1 - try! setForegroundNotificationCallback?() - } - } - var vendorUUIDSetCount = 0 var setVendorUUIDCallback: (() throws -> Void)? var vendorUUID: String? = Constants.vendorUUID { @@ -182,21 +155,30 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - var sdkVersionSetCount = 0 - var setSdkVersionCallback: (() throws -> Void)? - var sdkVersion: String = Constants.sdkVersion { + var manufacturerSetCount = 0 + var setManufacturerCallback: (() throws -> Void)? + var manufacturer: String = Constants.manufacturer { + didSet { + manufacturerSetCount += 1 + try! setManufacturerCallback?() + } + } + + var localeSetCount = 0 + var setLocaleCallback: (() throws -> Void)? + var locale: String = Constants.locale { didSet { - sdkVersionSetCount += 1 - try! setSdkVersionCallback?() + localeSetCount += 1 + try! setLocaleCallback?() } } - var shouldThrottleOnlineCallsSetCount = 0 - var setShouldThrottleOnlineCallsCallback: (() throws -> Void)? - var shouldThrottleOnlineCalls: Bool = true { + var osFamilySetCount = 0 + var setOsFamilyCallback: (() throws -> Void)? + var osFamily: String = Constants.osFamily { didSet { - shouldThrottleOnlineCallsSetCount += 1 - try! setShouldThrottleOnlineCallsCallback?() + osFamilySetCount += 1 + try! setOsFamilyCallback?() } } } diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index d9a2505f..9f4c2dcf 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -286,24 +286,14 @@ public class LDClient { } } - /** - Deprecated identify method which accepts a legacy LDUser instead of an LDContext. - - This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. - */ - @available(*, deprecated, message: "Use identify(context:completion:)") - public func identify(user: LDUser, completion: (() -> Void)? = nil) { - switch user.toContext() { - case .failure(let error): - Log.debug(self.typeName(and: #function) + "user created an invalid context: SDK identified context WILL NOT CHANGE: " + error.localizedDescription ) - case .success(let context): - identify(context: context, completion: completion) + func internalIdentify(newContext: LDContext, completion: (() -> Void)? = nil) { + var updatedContext = newContext + if config.autoEnvAttributes { + updatedContext = AutoEnvContextModifier(environmentReporter: environmentReporter).modifyContext(updatedContext) } - } - func internalIdentify(newContext: LDContext, completion: (() -> Void)? = nil) { internalIdentifyQueue.sync { - self.context = newContext + self.context = updatedContext Log.debug(self.typeName(and: #function) + "new context set with key: " + self.context.fullyQualifiedKey() ) let wasOnline = self.isOnline self.internalSetOnline(false) @@ -597,23 +587,6 @@ public class LDClient { start(serviceFactory: nil, config: config, context: context, completion: completion) } - /** - Deprecated start method which accepts a legacy LDUser instead of an LDContext. - - This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:completion:)` for details. - */ - @available(*, deprecated, message: "Use start(config:context:completion:)") - public static func start(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { - switch user?.toContext() { - case nil: - start(serviceFactory: nil, config: config, context: nil, completion: completion) - case .failure(let error): - Log.debug(self.typeName(and: #function) + "user created an invalid context: " + error.localizedDescription ) - case .success(let context): - start(serviceFactory: nil, config: config, context: context, completion: completion) - } - } - static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, completion: (() -> Void)? = nil) { Log.debug("LDClient starting") if serviceFactory != nil { @@ -661,23 +634,6 @@ public class LDClient { start(serviceFactory: nil, config: config, context: context, startWaitSeconds: startWaitSeconds, completion: completion) } - /** - Deprecated start method which accepts a legacy LDUser instead of an LDContext. - - This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(config:context:startWaitSeconds:completion:)` for details. - */ - @available(*, deprecated, message: "Use start(config:context:startWithSeconds:completion:)") - public static func start(config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { - switch user?.toContext() { - case nil: - start(serviceFactory: nil, config: config, context: nil, startWaitSeconds: startWaitSeconds, completion: completion) - case .failure(let error): - Log.debug(self.typeName(and: #function) + "user created an invalid context: " + error.localizedDescription ) - case .success(let context): - start(serviceFactory: nil, config: config, context: context, startWaitSeconds: startWaitSeconds, completion: completion) - } - } - static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, context: LDContext? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { var completed = true let internalCompletedQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue") @@ -743,7 +699,7 @@ public class LDClient { private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startContext: LDContext?, completion: (() -> Void)? = nil) { self.serviceFactory = serviceFactory - environmentReporter = self.serviceFactory.makeEnvironmentReporter() + environmentReporter = self.serviceFactory.makeEnvironmentReporter(config: configuration) flagCache = self.serviceFactory.makeFeatureFlagCache(mobileKey: configuration.mobileKey, maxCachedContexts: configuration.maxCachedContexts) flagStore = self.serviceFactory.makeFlagStore() flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() @@ -752,8 +708,13 @@ public class LDClient { config = configuration let anonymousContext = LDContext() context = startContext ?? anonymousContext - service = self.serviceFactory.makeDarklyServiceProvider(config: config, context: context) - diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service) + + if config.autoEnvAttributes { + context = AutoEnvContextModifier(environmentReporter: environmentReporter).modifyContext(context) + } + + service = self.serviceFactory.makeDarklyServiceProvider(config: config, context: context, envReporter: environmentReporter) + diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service, environmentReporter: environmentReporter) eventReporter = self.serviceFactory.makeEventReporter(service: service) connectionInformation = self.serviceFactory.makeConnectionInformation() flagSynchronizer = self.serviceFactory.makeFlagSynchronizer(streamingMode: config.allowStreamingMode ? config.streamingMode : .polling, @@ -761,10 +722,10 @@ public class LDClient { useReport: config.useReport, service: service) - if let backgroundNotification = environmentReporter.backgroundNotification { + if let backgroundNotification = SystemCapabilities.backgroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: backgroundNotification, object: nil) } - if let foregroundNotification = environmentReporter.foregroundNotification { + if let foregroundNotification = SystemCapabilities.foregroundNotification { NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: foregroundNotification, object: nil) } diff --git a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift index dfcf2883..1aa72cf5 100644 --- a/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift +++ b/LaunchDarkly/LaunchDarkly/Models/ConnectionInformation.swift @@ -131,7 +131,7 @@ public struct ConnectionInformation: Codable, CustomStringConvertible { } if reason.isEmpty && config.streamingMode == .streaming && !config.allowStreamingMode { reason = " LDConfig disallowed streaming mode. " - reason += !ldClient.environmentReporter.operatingSystem.isStreamingEnabled ? "Streaming is not allowed on \(ldClient.environmentReporter.operatingSystem)." : "Unknown reason." + reason += !SystemCapabilities.operatingSystem.isStreamingEnabled ? "Streaming is not allowed on \(SystemCapabilities.operatingSystem)." : "Unknown reason." } Log.debug(ldClient.typeName(and: #function, appending: ": ") + "\(streamingMode)\(reason)") return streamingMode diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift index 03fc6f9e..5bd37d24 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Context/LDContext.swift @@ -26,7 +26,7 @@ public struct LDContext: Encodable, Equatable { static let storedIdKey: String = "ldDeviceIdentifier" internal var kind: Kind = .user - fileprivate var contexts: [LDContext] = [] + internal var contexts: [LDContext] = [] // Meta attributes fileprivate var name: String? @@ -855,7 +855,14 @@ public struct LDMultiContextBuilder { /// /// It is invalid to add more than one context with the same Kind. This error is detected when /// you call `LDMultiContextBuilder.build()`. + /// + /// Adding a multi-kind context behaves the same as if each single-kind context was added individually. public mutating func addContext(_ context: LDContext) { + if context.isMulti() { + contexts.append(contentsOf: context.contexts) + return + } + contexts.append(context) } @@ -874,10 +881,6 @@ public struct LDMultiContextBuilder { return Result.failure(.emptyMultiKind) } - if contexts.contains(where: { $0.isMulti() }) { - return Result.failure(.nestedMultiKind) - } - if contexts.count == 1 { return Result.success(contexts[0]) } diff --git a/LaunchDarkly/LaunchDarkly/Models/Context/Modifier.swift b/LaunchDarkly/LaunchDarkly/Models/Context/Modifier.swift new file mode 100644 index 00000000..c28394ca --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/Models/Context/Modifier.swift @@ -0,0 +1,145 @@ +import Foundation + +protocol ContextModifier { + func modifyContext(_ context: LDContext) -> LDContext +} + +class AutoEnvContextModifier { + static let specVersion = "1.0" + private let environmentReporter: EnvironmentReporting + + init(environmentReporter: EnvironmentReporting) { + self.environmentReporter = environmentReporter + } + + private func makeRecipeList() -> [ContextRecipe] { + return [ + applicationRecipe(), + deviceRecipe() + ] + } + + private func makeContextFromRecipe(_ recipe: ContextRecipe) -> Result { + var builder = LDContextBuilder() + builder.kind(recipe.kind) + builder.key(recipe.keyCallable()) + + recipe.attributeCallables.forEach { (key, callable) in + let succeess = builder.trySetValue(key, callable()) + if !succeess { + Log.debug(self.typeName(and: #function) + " Failed setting value for key \(key)") + } + } + + return builder.build() + } + + // + // Begin recipe definition for ld_application kind + // + static let ldApplicationKind = "ld_application" + static let attrId = "id" + static let attrName = "name" + static let attrVersion = "version" + static let attrVersionName = "versionName" + static let envAttributesVersion = "envAttributesVersion" + + private func applicationRecipe() -> ContextRecipe { + let keyCallable: () -> (String) = { + Util.sha256base64(self.environmentReporter.applicationInfo.applicationId + ":" + self.environmentReporter.applicationInfo.applicationVersion) + } + + var callables: [String : () -> LDValue] = [:] + callables[AutoEnvContextModifier.envAttributesVersion] = { () -> LDValue in AutoEnvContextModifier.specVersion.toLDValue() } + callables[AutoEnvContextModifier.attrId] = { () -> LDValue in self.environmentReporter.applicationInfo.applicationId.toLDValue() } + callables[AutoEnvContextModifier.attrName] = { () -> LDValue in self.environmentReporter.applicationInfo.applicationName.toLDValue() } + callables[AutoEnvContextModifier.attrVersion] = { () -> LDValue in self.environmentReporter.applicationInfo.applicationVersion.toLDValue() } + callables[AutoEnvContextModifier.attrVersionName] = { () -> LDValue in self.environmentReporter.applicationInfo.applicationVersionName.toLDValue() } + callables[AutoEnvContextModifier.attrLocale] = { () -> LDValue in self.environmentReporter.locale.toLDValue() } + + return ContextRecipe( + kind: AutoEnvContextModifier.ldApplicationKind, + keyCallable: keyCallable, + attributeCallables: callables + ) + } + + // + // Begin recipe definition for ld_device kind + // + static var ldDeviceKind = "ld_device" + static var attrManufacturer = "manufacturer" + static var attrModel = "model" + static var attrLocale = "locale" + static var attrOs = "os" + static var attrFamily = "family" + + private func deviceRecipe() -> ContextRecipe { + let keyCallable: () -> (String) = { + LDContext.defaultKey(kind: Kind(AutoEnvContextModifier.ldDeviceKind)!) + } + + var callables: [String : () -> LDValue] = [:] + callables[AutoEnvContextModifier.envAttributesVersion] = { () -> LDValue in AutoEnvContextModifier.specVersion.toLDValue() } + callables[AutoEnvContextModifier.attrManufacturer] = { () -> LDValue in self.environmentReporter.manufacturer.toLDValue() } + callables[AutoEnvContextModifier.attrModel] = { () -> LDValue in self.environmentReporter.deviceModel.toLDValue() } + callables[AutoEnvContextModifier.attrOs] = {() -> LDValue in LDValue(dictionaryLiteral: + (AutoEnvContextModifier.attrFamily, self.environmentReporter.osFamily.toLDValue()), + (AutoEnvContextModifier.attrName, SystemCapabilities.systemName.toLDValue()), + (AutoEnvContextModifier.attrVersion, self.environmentReporter.systemVersion.toLDValue()) + )} + + return ContextRecipe( + kind: AutoEnvContextModifier.ldDeviceKind, + keyCallable: keyCallable, + attributeCallables: callables + ) + } +} + +extension AutoEnvContextModifier: TypeIdentifying { } + +extension AutoEnvContextModifier: ContextModifier { + func modifyContext(_ context: LDContext) -> LDContext { + var builder = LDMultiContextBuilder() + builder.addContext(context) + + let contextKeys = context.contextKeys() + for recipe in makeRecipeList() { + if contextKeys[recipe.kind.description] != nil { + Log.debug(self.typeName(and: #function) + " Unable to automatically add environment attributes for kind \(recipe.kind). It already exists.") + continue + } + + switch makeContextFromRecipe(recipe) { + case .success(let ctx): + builder.addContext(ctx) + case .failure(let err): + Log.debug(self.typeName(and: #function) + " Failed adding context of kind \(recipe.kind) with error \(err)") + } + } + + switch builder.build() { + case .success(let newContext): + return newContext + case .failure(let err): + Log.debug(self.typeName(and: #function) + " Failed adding telemetry context information with error \(err). Using customer context instead.") + return context + } + } +} + +// A ContextRecipe is a set of callables that will be executed for a given kind +// to generate the associated `LDContext`. The reason this class exists is to not make +// platform API calls until the context kind is needed. +final class ContextRecipe { + fileprivate let kind: String + fileprivate let keyCallable: () -> String + fileprivate let attributeCallables: [String: () -> LDValue] + + init(kind: String, keyCallable: @escaping () -> String, attributeCallables: [String : () -> LDValue]) { + self.kind = kind + self.keyCallable = keyCallable + self.attributeCallables = attributeCallables + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 7b51e86f..41218fdb 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -20,13 +20,13 @@ struct DiagnosticInit: DiagnosticEvent, Encodable { let configuration: DiagnosticConfig let platform: DiagnosticPlatform - init(config: LDConfig, diagnosticId: DiagnosticId, creationDate: Int64) { + init(config: LDConfig, environmentReporting: EnvironmentReporting, diagnosticId: DiagnosticId, creationDate: Int64) { self.id = diagnosticId self.creationDate = creationDate self.sdk = DiagnosticSdk(config: config) self.configuration = DiagnosticConfig(config: config) - self.platform = DiagnosticPlatform(config: config) + self.platform = DiagnosticPlatform(environmentReporting: environmentReporting) } } @@ -68,12 +68,12 @@ struct DiagnosticPlatform: Encodable { // Very general device model such as "iPad", "iPhone Simulator", or "Apple Watch" let deviceType: String - init(config: LDConfig) { - systemName = config.environmentReporter.operatingSystem.rawValue - systemVersion = config.environmentReporter.systemVersion - backgroundEnabled = config.environmentReporter.operatingSystem.isBackgroundEnabled - streamingEnabled = config.environmentReporter.operatingSystem.isStreamingEnabled - deviceType = config.environmentReporter.deviceType + init(environmentReporting: EnvironmentReporting) { + systemName = SystemCapabilities.operatingSystem.rawValue + systemVersion = environmentReporting.systemVersion + backgroundEnabled = SystemCapabilities.operatingSystem.isBackgroundEnabled + streamingEnabled = SystemCapabilities.operatingSystem.isStreamingEnabled + deviceType = environmentReporting.deviceModel } } @@ -84,7 +84,7 @@ struct DiagnosticSdk: Encodable { let wrapperVersion: String? init(config: LDConfig) { - version = config.environmentReporter.sdkVersion + version = ReportingConsts.sdkVersion wrapperName = config.wrapperName wrapperVersion = config.wrapperVersion } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index bbd40808..ec22a366 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -18,6 +18,26 @@ public enum LDStreamingMode { case polling } +/** + Enable / disable options for Auto Environment Attributes functionality. When enabled, the SDK will automatically + provide data about the mobile environment where the application is running. This data makes it simpler to target + your mobile customers based on application name or version, or on device characteristics including manufacturer, + model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. To learn more, + read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes). + + For example, consider a “dark mode” feature being added to an app. Versions 10 through 14 contain early, + incomplete versions of the feature. These versions are available to all customers, but the “dark mode” feature is only + enabled for testers. With version 15, the feature is considered complete. With Auto Environment Attributes enabled, + you can use targeting rules to enable "dark mode" for all customers who are using version 15 or greater, and ensure + that customers on previous versions don't use the earlier, unfinished version of the feature. + */ +@objc public enum AutoEnvAttributes: Int { + /// Enables the Auto EnvironmentAttributes functionality. + case enabled + /// Disables the Auto EnvironmentAttributes functionality. + case disabled +} + typealias MobileKey = String /** @@ -35,14 +55,18 @@ public typealias RequestHeaderTransform = (_ url: URL, _ headers: [String: Strin /// 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. +/// analytics or other product features. public struct ApplicationInfo: Equatable { - private var applicationId: String - private var applicationVersion: String + internal var applicationId: String + internal var applicationName: String + internal var applicationVersion: String + internal var applicationVersionName: String public init() { applicationId = "" + applicationName = "" applicationVersion = "" + applicationVersionName = "" } /// A unique identifier representing the application where the LaunchDarkly SDK is running. @@ -59,6 +83,20 @@ public struct ApplicationInfo: Equatable { self.applicationId = applicationId } + /// A human-friendly application name 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 applicationName(_ applicationName: String) { + if let error = validate(applicationName) { + Log.debug("applicationName \(error)") + return + } + + self.applicationName = applicationName + } + /// A unique identifier representing the version of the application where the LaunchDarkly SDK /// is running. /// @@ -74,6 +112,21 @@ public struct ApplicationInfo: Equatable { self.applicationVersion = applicationVersion } + /// A human-friendly name 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 applicationVersionName(_ applicationVersionName: String) { + if let error = validate(applicationVersionName) { + Log.debug("applicationVersionName \(error)") + return + } + + self.applicationVersionName = applicationVersionName + } + func buildTag() -> String { var tags: [String] = [] @@ -81,10 +134,18 @@ public struct ApplicationInfo: Equatable { tags.append("application-id/\(applicationId)") } + if (!applicationName.isEmpty) { + tags.append("application-name/\(applicationName)") + } + if !applicationVersion.isEmpty { tags.append("application-version/\(applicationVersion)") } + if !applicationVersionName.isEmpty { + tags.append("application-version-name/\(applicationVersionName)") + } + return tags.lazy.joined(separator: " ") } @@ -172,6 +233,9 @@ public struct LDConfig { /// a closure to allow dynamic changes of headers on connect & reconnect static let headerDelegate: RequestHeaderTransform? = nil + + /// The default behavior for environment attributes is to not modify any provided context UNLESS the developer specifically opts-in. + static let autoEnvAttributes: Bool = false } /// Constants relevant to setting up an `LDConfig` @@ -204,8 +268,8 @@ public struct LDConfig { /// The minimum time interval between sending periodic diagnostic data. (5 minutes) public let diagnosticRecordingInterval: TimeInterval - init(environmentReporter: EnvironmentReporting = EnvironmentReporter()) { - let isDebug = environmentReporter.isDebugBuild + init(isDebugBuild: Bool = false) { + let isDebug = isDebugBuild self.flagPollingInterval = isDebug ? Debug.flagPollingInterval : Production.flagPollingInterval self.backgroundFlagPollingInterval = isDebug ? Debug.backgroundFlagPollingInterval : Production.backgroundFlagPollingInterval self.diagnosticRecordingInterval = isDebug ? Debug.diagnosticRecordingInterval : Production.diagnosticRecordingInterval @@ -328,14 +392,16 @@ public struct LDConfig { /// a closure to allow dynamic changes of headers on connect & reconnect public var headerDelegate: RequestHeaderTransform? + /// Set to true to opt in to automatically sending mobile environment attributes. This data makes it simpler to target mobile customers + /// based on application name or version, or on device characteristics including manufacturer, model, operating system, locale, and so on. + public var autoEnvAttributes: Bool = Defaults.autoEnvAttributes + /// LaunchDarkly defined minima for selected configurable items public let minima: Minima /// An NSObject wrapper for the Swift LDConfig struct. Intended for use in mixed apps when Swift code needs to pass a config into an Objective-C method. public var objcLdConfig: ObjcLDConfig { ObjcLDConfig(self) } - let environmentReporter: EnvironmentReporting - /// A Dictionary of identifying names to unique mobile keys for all environments private var mobileKeys: [String: String] { var internalMobileKeys = getSecondaryMobileKeys() @@ -378,21 +444,33 @@ public struct LDConfig { private var _secondaryMobileKeys: [String: String] // Internal constructor to enable automated testing - init(mobileKey: String, environmentReporter: EnvironmentReporting) { + init(mobileKey: String, autoEnvAttributes: AutoEnvAttributes, isDebugBuild: Bool) { self.mobileKey = mobileKey - self.environmentReporter = environmentReporter - minima = Minima(environmentReporter: environmentReporter) - allowStreamingMode = environmentReporter.operatingSystem.isStreamingEnabled - allowBackgroundUpdates = environmentReporter.operatingSystem.isBackgroundEnabled + self.autoEnvAttributes = (autoEnvAttributes == .enabled) // mapping API enum to bool + minima = Minima(isDebugBuild: isDebugBuild) + allowStreamingMode = SystemCapabilities.operatingSystem.isStreamingEnabled + allowBackgroundUpdates = SystemCapabilities.operatingSystem.isBackgroundEnabled _secondaryMobileKeys = Defaults.secondaryMobileKeys if mobileKey.isEmpty { Log.debug(typeName(and: #function, appending: ": ") + "mobileKey is empty. The SDK will not operate correctly without a valid mobile key.") } } - /// LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. Note that client app developers may prefer to get the LDConfig from `LDClient.config` in order to retain previously set values. - public init(mobileKey: String) { - self.init(mobileKey: mobileKey, environmentReporter: EnvironmentReporter()) + /** + LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. + Note that client app developers may prefer to get the LDConfig from `LDClient.config` in order to retain previously set values. + + - Parameters: + - mobileKey: The mobile key for the LaunchDarkly environment. This can be found on the LaunchDarkly dashboard once logged in. + - autoEnvAttributes: Enable / disable Auto Environment Attributes functionality. When enabled, the SDK will automatically + provide data about the mobile environment where the application is running. This data makes it simpler to target + your mobile customers based on application name or version, or on device characteristics including manufacturer, + model, operating system, locale, and so on. We recommend enabling this when you configure the SDK. To learn more, + read [Automatic environment attributes](https://docs.launchdarkly.com/sdk/features/environment-attributes). + for more documentation. + */ + public init(mobileKey: String, autoEnvAttributes: AutoEnvAttributes) { + self.init(mobileKey: mobileKey, autoEnvAttributes: autoEnvAttributes, isDebugBuild: false) } // Determine the effective flag polling interval based on runMode, configured foreground & background polling interval, and minimum foreground & background polling interval. @@ -437,6 +515,7 @@ extension LDConfig: Equatable { && lhs.wrapperName == rhs.wrapperName && lhs.wrapperVersion == rhs.wrapperVersion && lhs.additionalHeaders == rhs.additionalHeaders + && lhs.autoEnvAttributes == rhs.autoEnvAttributes } } diff --git a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/LDUser.swift deleted file mode 100644 index 659fe07e..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/LDUser.swift +++ /dev/null @@ -1,232 +0,0 @@ -import Foundation - -/** - LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. - - The usage of LDUser is no longer recommended and is retained only to ease the adoption of the `LDContext` class. New - code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. - */ -@available(*, deprecated, message: "Use LDContextBuilder to construct a context of kind 'user'") -public struct LDUser: Encodable, Equatable { - - static let optionalAttributes = UserAttribute.BuiltIn.allBuiltIns.filter { $0.name != "key" && $0.name != "anonymous"} - - static let storedIdKey: String = "ldDeviceIdentifier" - - /// Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. - public var key: String - /// Client app defined name for the user. (Default: nil) - public var name: String? - /// Client app defined first name for the user. (Default: nil) - public var firstName: String? - /// Client app defined last name for the user. (Default: nil) - public var lastName: String? - /// Client app defined country for the user. (Default: nil) - public var country: String? - /// Client app defined ipAddress for the user. (Default: nil) - public var ipAddress: String? - /// Client app defined email address for the user. (Default: nil) - public var email: String? - /// Client app defined avatar for the user. (Default: nil) - public var avatar: String? - /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private, see `privateAttributes` for details. (Default: [:]) - public var custom: [String: LDValue] - /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: false) - public var isAnonymous: Bool - - /** - Client app defined privateAttributes for the user. - The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - This attribute is ignored if `LDConfig.allUserAttributesPrivate` is true. Combined with `LDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define most built-in attributes and all top level `custom` dictionary keys here. (Default: []]) - See Also: `LDConfig.allUserAttributesPrivate` and `LDConfig.privateUserAttributes`. - */ - public var privateAttributes: [UserAttribute] - - var contextKind: String { isAnonymous ? "anonymousUser" : "user" } - - /** - Initializer to create a LDUser. Client configurable attributes each have an optional parameter to facilitate setting user information into the LDUser. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes if the client does not provide them. The SDK embeds `device` and `operatingSystem` into the `custom` dictionary for transmission to LaunchDarkly. - - parameter key: String that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. - - parameter name: Client app defined name for the user. (Default: nil) - - parameter firstName: Client app defined first name for the user. (Default: nil) - - parameter lastName: Client app defined last name for the user. (Default: nil) - - parameter country: Client app defined country for the user. (Default: nil) - - parameter ipAddress: Client app defined ipAddress for the user. (Default: nil) - - parameter email: Client app defined email address for the user. (Default: nil) - - parameter avatar: Client app defined avatar for the user. (Default: nil) - - parameter custom: Client app defined dictionary for the user. The client app may declare top level dictionary items as private. If the client app defines custom as private, the SDK considers the dictionary private except for device & operatingSystem (which cannot be made private). See `privateAttributes` for details. (Default: nil) - - parameter isAnonymous: Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. (Default: nil) - - parameter privateAttributes: Client app defined privateAttributes for the user. (Default: nil) - */ - public init(key: String? = nil, - name: String? = nil, - firstName: String? = nil, - lastName: String? = nil, - country: String? = nil, - ipAddress: String? = nil, - email: String? = nil, - avatar: String? = nil, - custom: [String: LDValue]? = nil, - isAnonymous: Bool? = nil, - privateAttributes: [UserAttribute]? = nil) { - let environmentReporter = EnvironmentReporter() - let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter) - self.key = selectedKey - self.name = name - self.firstName = firstName - self.lastName = lastName - self.country = country - self.ipAddress = ipAddress - self.email = email - self.avatar = avatar - if isAnonymous == nil && selectedKey == LDUser.defaultKey(environmentReporter: environmentReporter) { - self.isAnonymous = true - } else { - // If not nil, use the value, otherwise false. - self.isAnonymous = isAnonymous ?? false; - } - self.custom = custom ?? [:] - self.privateAttributes = privateAttributes ?? [] - Log.debug(typeName(and: #function) + "user: \(self)") - } - - /** - Internal initializer that accepts an environment reporter, used for testing - */ - init(environmentReporter: EnvironmentReporting) { - self.init(key: LDUser.defaultKey(environmentReporter: environmentReporter), isAnonymous: true) - } - - private func value(for attribute: UserAttribute) -> Any? { - if let builtInGetter = attribute.builtInGetter { - return builtInGetter(self) - } - return custom[attribute.name] - } - - struct UserInfoKeys { - static let includePrivateAttributes = CodingUserInfoKey(rawValue: "LD_includePrivateAttributes")! - static let allAttributesPrivate = CodingUserInfoKey(rawValue: "LD_allAttributesPrivate")! - static let globalPrivateAttributes = CodingUserInfoKey(rawValue: "LD_globalPrivateAttributes")! - } - - /** - Internal helper method to convert an LDUser to an LDContext. - - Ideally we would do this as the LDUser was being built. However, the LDUser properties are publicly accessible, which makes that approach problematic. - */ - internal func toContext() -> Result { - var contextBuilder = LDContextBuilder(key: key) - - // Custom attributes must be processed first in case built-in attributes - // need to override those values - custom.forEach { (key, value) in - contextBuilder.trySetValue(key, value) - } - - if let name = name { - contextBuilder.name(name) - } - - contextBuilder.anonymous(isAnonymous) - - if let firstName = firstName { - contextBuilder.trySetValue("firstName", firstName.toLDValue()) - } - if let lastName = lastName { - contextBuilder.trySetValue("lastName", lastName.toLDValue()) - } - if let country = country { - contextBuilder.trySetValue("country", country.toLDValue()) - } - if let ipAddress = ipAddress { - contextBuilder.trySetValue("ipAddress", ipAddress.toLDValue()) - } - if let email = email { - contextBuilder.trySetValue("email", email.toLDValue()) - } - if let avatar = avatar { - contextBuilder.trySetValue("avatar", avatar.toLDValue()) - } - - privateAttributes.forEach { privateAttribute in - contextBuilder.addPrivateAttribute(Reference(literal: privateAttribute.name)) - } - - return contextBuilder.build() - } - - public func encode(to encoder: Encoder) throws { - let includePrivateAttributes = encoder.userInfo[UserInfoKeys.includePrivateAttributes] as? Bool ?? false - let allAttributesPrivate = encoder.userInfo[UserInfoKeys.allAttributesPrivate] as? Bool ?? false - let globalPrivateAttributes = encoder.userInfo[UserInfoKeys.globalPrivateAttributes] as? [String] ?? [] - - let allPrivate = !includePrivateAttributes && allAttributesPrivate - let privateAttributeNames = includePrivateAttributes ? [] : (privateAttributes.map { $0.name } + globalPrivateAttributes) - - var redactedAttributes: [String] = [] - - var container = encoder.container(keyedBy: DynamicKey.self) - try container.encode(key, forKey: DynamicKey(stringValue: "key")!) - - if isAnonymous { - try container.encode(true, forKey: DynamicKey(stringValue: "anonymous")!) - } - - try LDUser.optionalAttributes.forEach { attribute in - if let value = self.value(for: attribute) as? String { - if allPrivate || privateAttributeNames.contains(attribute.name) { - redactedAttributes.append(attribute.name) - } else { - try container.encode(value, forKey: DynamicKey(stringValue: attribute.name)!) - } - } - } - - var nestedContainer: KeyedEncodingContainer? - try custom.forEach { attrName, attrVal in - if allPrivate || privateAttributeNames.contains(attrName) { - redactedAttributes.append(attrName) - } else { - if nestedContainer == nil { - nestedContainer = container.nestedContainer(keyedBy: DynamicKey.self, forKey: DynamicKey(stringValue: "custom")!) - } - try nestedContainer!.encode(attrVal, forKey: DynamicKey(stringValue: attrName)!) - } - } - - if !redactedAttributes.isEmpty { - try container.encode(Set(redactedAttributes).sorted(), forKey: DynamicKey(stringValue: "privateAttrs")!) - } - } - - /// Default key is the LDUser.key the SDK provides when any intializer is called without defining the key. The key should be constant with respect to the client app installation on a specific device. (The key may change if the client app is uninstalled and then reinstalled on the same device.) - /// - parameter environmentReporter: The environmentReporter provides selected information that varies between OS regarding how it's determined - static func defaultKey(environmentReporter: EnvironmentReporting) -> String { - // For iOS & tvOS, this should be UIDevice.current.identifierForVendor.UUIDString - // For macOS & watchOS, this should be a UUID that the sdk creates and stores so that the value returned here should be always the same - if let vendorUUID = environmentReporter.vendorUUID { - return vendorUUID - } - if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { - return storedId - } - let key = UUID().uuidString - UserDefaults.standard.set(key, forKey: storedIdKey) - return key - } -} - -/// Class providing ObjC interoperability with the LDUser struct -@available(*, deprecated) -@objc final class LDUserWrapper: NSObject { - let wrapped: LDUser - - init(user: LDUser) { - wrapped = user - super.init() - } -} - -@available(*, deprecated) -extension LDUser: TypeIdentifying { } diff --git a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift b/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift deleted file mode 100644 index f59a3761..00000000 --- a/LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation - -/** - Represents a built-in or custom attribute name supported by `LDUser`. - - This abstraction helps to distinguish attribute names from other `String` values. - - For a more complete description of user attributes and how they can be referenced in feature flag rules, see the - reference guides [Setting user attributes](https://docs.launchdarkly.com/home/users/attributes) and - [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users). - */ -@available(*, deprecated) -public class UserAttribute: Equatable, Hashable { - - /** - Instances for built in attributes. - */ - public struct BuiltIn { - /// Represents the user key attribute. - public static let key = UserAttribute("key") { $0.key } - /// Represents the IP address attribute. - public static let ip = UserAttribute("ip") { $0.ipAddress } // swiftlint:disable:this identifier_name - /// Represents the email address attribute. - public static let email = UserAttribute("email") { $0.email } - /// Represents the full name attribute. - public static let name = UserAttribute("name") { $0.name } - /// Represents the avatar attribute. - public static let avatar = UserAttribute("avatar") { $0.avatar } - /// Represents the first name attribute. - public static let firstName = UserAttribute("firstName") { $0.firstName } - /// Represents the last name attribute. - public static let lastName = UserAttribute("lastName") { $0.lastName } - /// Represents the country attribute. - public static let country = UserAttribute("country") { $0.country } - /// Represents the anonymous attribute. - public static let anonymous = UserAttribute("anonymous") { $0.isAnonymous } - - static let allBuiltIns = [key, ip, email, name, avatar, firstName, lastName, country, anonymous] - } - - static var builtInMap = { return BuiltIn.allBuiltIns.reduce(into: [:]) { $0[$1.name] = $1 } }() - - /** - Returns a `UserAttribute` instance for the specified atttribute name. - - For built-in attributes, the same instances are always reused and `isBuiltIn` will be `true`. For custom - attributes, a new instance is created and `isBuiltIn` will be `false`. - - - parameter name: the attribute name - - returns: a `UserAttribute` - */ - public static func forName(_ name: String) -> UserAttribute { - if let builtIn = builtInMap[name] { - return builtIn - } - return UserAttribute(name) - } - - let name: String - let builtInGetter: ((LDUser) -> Any?)? - - init(_ name: String, builtInGetter: ((LDUser) -> Any?)? = nil) { - self.name = name - self.builtInGetter = builtInGetter - } - - /// Whether the attribute is built-in rather than custom. - public var isBuiltIn: Bool { builtInGetter != nil } - - public static func == (lhs: UserAttribute, rhs: UserAttribute) -> Bool { - if lhs.isBuiltIn || rhs.isBuiltIn { - return lhs === rhs - } - return lhs.name == rhs.name - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(name) - } -} diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 846d37ad..23c22e2a 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -53,7 +53,7 @@ final class DarklyService: DarklyServiceProvider { private var session: URLSession var flagRequestEtag: String? - init(config: LDConfig, context: LDContext, serviceFactory: ClientServiceCreating) { + init(config: LDConfig, context: LDContext, envReporter: EnvironmentReporting, serviceFactory: ClientServiceCreating) { self.config = config self.context = context self.serviceFactory = serviceFactory @@ -64,7 +64,7 @@ final class DarklyService: DarklyServiceProvider { self.diagnosticCache = nil } - self.httpHeaders = HTTPHeaders(config: config, environmentReporter: serviceFactory.makeEnvironmentReporter()) + self.httpHeaders = HTTPHeaders(config: config, environmentReporter: envReporter) // URLSessionConfiguration is a class, but `.default` creates a new instance. This does not effect other session configuration. let sessionConfig = URLSessionConfiguration.default diff --git a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift index abbd3a9a..9ee51f9d 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/HTTPHeaders.swift @@ -30,11 +30,11 @@ struct HTTPHeaders { init(config: LDConfig, environmentReporter: EnvironmentReporting) { self.mobileKey = config.mobileKey self.additionalHeaders = config.additionalHeaders - self.userAgent = "\(environmentReporter.systemName)/\(environmentReporter.sdkVersion)" + self.userAgent = "\(SystemCapabilities.systemName)/\(ReportingConsts.sdkVersion)" self.authKey = "\(HeaderValue.apiKey) \(config.mobileKey)" - self.applicationTag = config.applicationInfo?.buildTag() ?? "" + self.applicationTag = environmentReporter.applicationInfo.buildTag() - if let wrapperName = config.wrapperName { + if let wrapperName = config.wrapperName { if let wrapperVersion = config.wrapperVersion { wrapperHeaderVal = "\(wrapperName)/\(wrapperVersion)" } else { diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift index da71a69b..d305e820 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDApplicationInfo.swift @@ -3,8 +3,7 @@ import Foundation /** Use LDApplicationInfo to define 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. + These properties are optional and informational. They may be used in LaunchDarkly analytics or other product features. */ @objc(LDApplicationInfo) public final class ObjcLDApplicationInfo: NSObject { @@ -31,6 +30,15 @@ public final class ObjcLDApplicationInfo: NSObject { applicationInfo.applicationIdentifier(applicationId) } + /// A human-friendly application name 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. + @objc public func applicationName(_ applicationName: String) { + applicationInfo.applicationName(applicationName) + } + /// A unique identifier representing the version of the application where the LaunchDarkly SDK /// is running. /// @@ -40,4 +48,14 @@ public final class ObjcLDApplicationInfo: NSObject { @objc public func applicationVersion(_ applicationVersion: String) { applicationInfo.applicationVersion(applicationVersion) } + + /// A human-friendly name 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. + @objc public func applicationVersionName(_ applicationVersionName: String) { + applicationInfo.applicationVersionName(applicationVersionName) + } } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index cdcd9dd2..239ebcf6 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -114,16 +114,6 @@ public final class ObjcLDClient: NSObject { ldClient.identify(context: context.context, completion: nil) } - /** - Deprecated identify method which accepts a legacy LDUser instead of an LDContext. - - This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context)` for details. - */ - @available(*, deprecated, message: "Use identify(context)") - @objc public func identify(user: ObjcLDUser) { - ldClient.identify(user: user.user, completion: nil) - } - /** The LDContext set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the context. See `LDContext` for details about what information can be retained. @@ -140,16 +130,6 @@ public final class ObjcLDClient: NSObject { ldClient.identify(context: context.context, completion: completion) } - /** - Deprecated identify method which accepts a legacy LDUser instead of an LDContext. - - This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `identify(context:completion:)` for details. - */ - @available(*, deprecated, message: "Use identify(context:completion:)") - @objc public func identify(user: ObjcLDUser, completion: (() -> Void)? = nil) { - ldClient.identify(user: user.user, completion: completion) - } - /** Stops the LDClient. Stopping the client means the LDClient goes offline and stops recording events. LDClient will no longer provide feature flag values, only returning default values. @@ -574,16 +554,6 @@ public final class ObjcLDClient: NSObject { LDClient.start(config: configuration.config, context: context.context, completion: completion) } - /** - Deprecated start method which accepts a legacy LDUser instead of an LDContext. - - This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:completion:)` for details. - */ - @available(*, deprecated, message: "Use start(configuration:context:completion:)") - @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, completion: (() -> Void)? = nil) { - LDClient.start(config: configuration.config, user: user.user, completion: completion) - } - /** See [start](x-source-tag://start) for more information on starting the SDK. @@ -596,16 +566,6 @@ public final class ObjcLDClient: NSObject { LDClient.start(config: configuration.config, context: context.context, startWaitSeconds: startWaitSeconds, completion: completion) } - /** - Deprecated start method which accepts a legacy LDUser instead of an LDContext. - - This LDUser will be converted into an LDContext, and the context specific version of this method will be called. See `start(configuration:context:startWaitSeconds:completion:)` for details. - */ - @available(*, deprecated, message: "Use start(configuration:context:startWaitSeconds:completion:)") - @objc public static func start(configuration: ObjcLDConfig, user: ObjcLDUser, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { - LDClient.start(config: configuration.config, user: user.user, startWaitSeconds: startWaitSeconds, completion: completion) - } - private init(client: LDClient) { ldClient = client } diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift index 7966535e..1cc08671 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift @@ -188,8 +188,8 @@ public final class ObjcLDConfig: NSObject { } /// LDConfig constructor. Configurable values are all set to their default values. The client app can modify these values as desired. Note that client app developers may prefer to get the LDConfig from `LDClient.config` (`ObjcLDClient.config`) in order to retain previously set values. - @objc public init(mobileKey: String) { - config = LDConfig(mobileKey: mobileKey) + @objc public init(mobileKey: String, autoEnvAttributes: AutoEnvAttributes) { + config = LDConfig(mobileKey: mobileKey, autoEnvAttributes: autoEnvAttributes) } // Initializer to wrap the Swift LDConfig into ObjcLDConfig for use in Objective-C apps. diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift deleted file mode 100644 index 7c2166ff..00000000 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation - -/** - LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. - - The usage of LDUser is no longer recommended and is retained only to ease the adoption of the `LDContext` class. New - code using this SDK should make use of the `LDContextBuilder` to construct an equivalent `Kind.user` kind context. - */ -@objc (LDUser) -@available(*, deprecated, message: "Use ObjcLDContextBuilder to construct a context of kind 'user'") -public final class ObjcLDUser: NSObject { - var user: LDUser - - /// LDUser name attribute used to make `name` private - @objc public class var attributeName: String { "name" } - /// LDUser firstName attribute used to make `firstName` private - @objc public class var attributeFirstName: String { "firstName" } - /// LDUser lastName attribute used to make `lastName` private - @objc public class var attributeLastName: String { "lastName" } - /// LDUser country attribute used to make `country` private - @objc public class var attributeCountry: String { "country" } - /// LDUser ipAddress attribute used to make `ipAddress` private - @objc public class var attributeIPAddress: String { "ip" } - /// LDUser email attribute used to make `email` private - @objc public class var attributeEmail: String { "email" } - /// LDUser avatar attribute used to make `avatar` private - @objc public class var attributeAvatar: String { "avatar" } - - /// Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. - @objc public var key: String { - return user.key - } - /// Client app defined name for the user. (Default: nil) - @objc public var name: String? { - get { user.name } - set { user.name = newValue } - } - /// Client app defined first name for the user. (Default: nil) - @objc public var firstName: String? { - get { user.firstName } - set { user.firstName = newValue } - } - /// Client app defined last name for the user. (Default: nil) - @objc public var lastName: String? { - get { user.lastName } - set { user.lastName = newValue } - } - /// Client app defined country for the user. (Default: nil) - @objc public var country: String? { - get { user.country } - set { user.country = newValue } - } - /// Client app defined ipAddress for the user. (Default: nil) - @objc public var ipAddress: String? { - get { user.ipAddress } - set { user.ipAddress = newValue } - } - /// Client app defined email address for the user. (Default: nil) - @objc public var email: String? { - get { user.email } - set { user.email = newValue } - } - /// Client app defined avatar for the user. (Default: nil) - @objc public var avatar: String? { - get { user.avatar } - set { user.avatar = newValue } - } - /// Client app defined dictionary for the user. The client app may declare top level dictionary items as private. See `privateAttributes` for details. - @objc public var custom: [String: ObjcLDValue] { - get { user.custom.mapValues { ObjcLDValue(wrappedValue: $0) } } - set { user.custom = newValue.mapValues { $0.wrappedValue } } - } - /// Client app defined isAnonymous for the user. If the client app does not define isAnonymous, the SDK will use the `key` to set this attribute. isAnonymous cannot be made private. (Default: YES) - @objc public var isAnonymous: Bool { - get { user.isAnonymous } - set { user.isAnonymous = newValue } - } - - /** - Client app defined privateAttributes for the user. - - The SDK will not include private attribute values in analytics events, but private attribute names will be sent. - - This attribute is ignored if `ObjcLDConfig.allUserAttributesPrivate` is YES. Combined with `ObjcLDConfig.privateUserAttributes`. The SDK considers attributes appearing in either list as private. Client apps may define most built-in attributes and all top level `custom` dictionary keys here. (Default: `[]`]) - - See Also: `ObjcLDConfig.allUserAttributesPrivate` and `ObjcLDConfig.privateUserAttributes`. - - */ - @objc public var privateAttributes: [String] { - get { user.privateAttributes.map { $0.name } } - set { user.privateAttributes = newValue.map { UserAttribute.forName($0) } } - } - - /** - Initializer to create a LDUser. Client configurable attributes are set to their default value. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes. The SDK embeds `device` and `operatingSystem` into the `custom` dictionary for transmission to LaunchDarkly. - */ - @objc override public init() { - user = LDUser() - } - - /** - Initializer to create a LDUser with a specific key. Other client configurable attributes are set to their default value. The SDK will automatically set `key`, `device`, `operatingSystem`, and `isAnonymous` attributes. The SDK embeds `device` and `operatingSystem` into the `custom` dictionary for transmission to LaunchDarkly. - - - parameter key: String that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. - */ - @objc public init(key: String) { - user = LDUser(key: key) - } - - // Initializer to wrap the Swift LDUser into ObjcLDUser for use in Objective-C apps. - init(_ user: LDUser) { - self.user = user - } - - /// Compares users by comparing their user keys only, to allow the client app to collect user information over time - @objc public func isEqual(object: Any) -> Bool { - guard let otherUser = object as? ObjcLDUser - else { return false } - return user == otherUser.user - } - - /// Convert a legacy LDUser to the newer LDContext - @objc public func toContext() -> ContextBuilderResult { - switch self.user.toContext() { - case .success(let context): - return ContextBuilderResult.fromSuccess(context) - case .failure(let error): - return ContextBuilderResult.fromError(error) - } - } -} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index baf0784f..74f0817b 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -5,7 +5,7 @@ protocol ClientServiceCreating { func makeKeyedValueCache(cacheKey: String?) -> KeyedValueCaching func makeFeatureFlagCache(mobileKey: String, maxCachedContexts: Int) -> FeatureFlagCaching func makeCacheConverter() -> CacheConverting - func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider + func makeDarklyServiceProvider(config: LDConfig, context: LDContext, envReporter: EnvironmentReporting) -> DarklyServiceProvider func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, @@ -16,11 +16,11 @@ protocol ClientServiceCreating { func makeEventReporter(service: DarklyServiceProvider) -> EventReporting func makeEventReporter(service: DarklyServiceProvider, onSyncComplete: EventSyncCompleteClosure?) -> EventReporting func makeStreamingProvider(url: URL, httpHeaders: [String: String], connectMethod: String, connectBody: Data?, handler: EventHandler, delegate: RequestHeaderTransform?, errorHandler: ConnectionErrorHandler?) -> DarklyStreamingProvider - func makeEnvironmentReporter() -> EnvironmentReporting + func makeEnvironmentReporter(config: LDConfig) -> EnvironmentReporting func makeThrottler(environmentReporter: EnvironmentReporting) -> Throttling func makeConnectionInformation() -> ConnectionInformation func makeDiagnosticCache(sdkKey: String) -> DiagnosticCaching - func makeDiagnosticReporter(service: DarklyServiceProvider) -> DiagnosticReporting + func makeDiagnosticReporter(service: DarklyServiceProvider, environmentReporter: EnvironmentReporting) -> DiagnosticReporting func makeFlagStore() -> FlagMaintaining } @@ -37,8 +37,8 @@ final class ClientServiceFactory: ClientServiceCreating { CacheConverter() } - func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider { - DarklyService(config: config, context: context, serviceFactory: self) + func makeDarklyServiceProvider(config: LDConfig, context: LDContext, envReporter: EnvironmentReporting) -> DarklyServiceProvider { + DarklyService(config: config, context: context, envReporter: envReporter, serviceFactory: self) } func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing { @@ -85,12 +85,22 @@ final class ClientServiceFactory: ClientServiceCreating { return EventSource(config: config) } - func makeEnvironmentReporter() -> EnvironmentReporting { - EnvironmentReporter() + func makeEnvironmentReporter(config: LDConfig) -> EnvironmentReporting { + let builder = EnvironmentReporterBuilder() + + if let info = config.applicationInfo { + builder.applicationInfo(info) + } + + if config.autoEnvAttributes { + builder.enableCollectionFromPlatform() + } + + return builder.build() } func makeThrottler(environmentReporter: EnvironmentReporting) -> Throttling { - Throttler(environmentReporter: environmentReporter) + Throttler() } func makeConnectionInformation() -> ConnectionInformation { @@ -101,8 +111,8 @@ final class ClientServiceFactory: ClientServiceCreating { DiagnosticCache(sdkKey: sdkKey) } - func makeDiagnosticReporter(service: DarklyServiceProvider) -> DiagnosticReporting { - DiagnosticReporter(service: service) + func makeDiagnosticReporter(service: DarklyServiceProvider, environmentReporter: EnvironmentReporting) -> DiagnosticReporting { + DiagnosticReporter(service: service, environmentReporting: environmentReporter) } func makeFlagStore() -> FlagMaintaining { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift index 9cb188e5..890b4724 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift @@ -7,13 +7,15 @@ protocol DiagnosticReporting { class DiagnosticReporter: DiagnosticReporting { private let service: DarklyServiceProvider + private let environmentReporting: EnvironmentReporting private var timer: TimeResponding? private var sentInit: Bool private let stateQueue = DispatchQueue(label: "com.launchdarkly.diagnosticReporter.state", qos: .background) private let workQueue = DispatchQueue(label: "com.launchdarkly.diagnosticReporter.work", qos: .background) - init(service: DarklyServiceProvider) { + init(service: DarklyServiceProvider, environmentReporting: EnvironmentReporting) { self.service = service + self.environmentReporting = environmentReporting self.sentInit = false } @@ -35,6 +37,7 @@ class DiagnosticReporter: DiagnosticReporting { sendDiagnosticEventAsync(diagnosticEvent: lastStats) } let initEvent = DiagnosticInit(config: service.config, + environmentReporting: environmentReporting, diagnosticId: cache.getDiagnosticId(), creationDate: Date().millisSince1970) sendDiagnosticEventAsync(diagnosticEvent: initEvent) diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index 10f4cdf6..a72a79d0 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -11,141 +11,43 @@ import UIKit #endif enum OperatingSystem: String { - case iOS, watchOS, macOS, tvOS - - static var allOperatingSystems: [OperatingSystem] { - [.iOS, .watchOS, .macOS, .tvOS] - } - - var isBackgroundEnabled: Bool { - OperatingSystem.backgroundEnabledOperatingSystems.contains(self) - } - static var backgroundEnabledOperatingSystems: [OperatingSystem] { - [.macOS] - } - - var isStreamingEnabled: Bool { - OperatingSystem.streamingEnabledOperatingSystems.contains(self) - } - static var streamingEnabledOperatingSystems: [OperatingSystem] { - [.iOS, .macOS, .tvOS] - } + case iOS, watchOS, macOS, tvOS, unknown + + static var allOperatingSystems: [OperatingSystem] { + [.iOS, .watchOS, .macOS, .tvOS] + } + + var isBackgroundEnabled: Bool { + OperatingSystem.backgroundEnabledOperatingSystems.contains(self) + } + static var backgroundEnabledOperatingSystems: [OperatingSystem] { + [.macOS] + } + + var isStreamingEnabled: Bool { + OperatingSystem.streamingEnabledOperatingSystems.contains(self) + } + static var streamingEnabledOperatingSystems: [OperatingSystem] { + [.iOS, .macOS, .tvOS] + } } // sourcery: autoMockable protocol EnvironmentReporting { + // sourcery: defaultMockValue = Constants.applicationInfo + var applicationInfo: ApplicationInfo { get } // sourcery: defaultMockValue = true var isDebugBuild: Bool { get } - // sourcery: defaultMockValue = Constants.deviceType - var deviceType: String { get } + // sourcery: defaultMockValue = Constants.deviceModel + var deviceModel: String { get } // sourcery: defaultMockValue = Constants.systemVersion var systemVersion: String { get } - // sourcery: defaultMockValue = Constants.systemName - var systemName: String { get } - // sourcery: defaultMockValue = .iOS - var operatingSystem: OperatingSystem { get } - // sourcery: defaultMockValue = EnvironmentReporter().backgroundNotification - var backgroundNotification: Notification.Name? { get } - // sourcery: defaultMockValue = EnvironmentReporter().foregroundNotification - var foregroundNotification: Notification.Name? { get } // sourcery: defaultMockValue = Constants.vendorUUID var vendorUUID: String? { get } - // sourcery: defaultMockValue = Constants.sdkVersion - var sdkVersion: String { get } - // sourcery: defaultMockValue = true - var shouldThrottleOnlineCalls: Bool { get } -} - -struct EnvironmentReporter: EnvironmentReporting { - #if DEBUG - var isDebugBuild: Bool { true } - #else - var isDebugBuild: Bool { false } - #endif - - struct Constants { - fileprivate static let simulatorModelIdentifier = "SIMULATOR_MODEL_IDENTIFIER" - } - - #if os(iOS) - var deviceType: String { UIDevice.current.model } - var systemVersion: String { UIDevice.current.systemVersion } - var systemName: String { UIDevice.current.systemName } - var operatingSystem: OperatingSystem { .iOS } - var backgroundNotification: Notification.Name? { UIApplication.didEnterBackgroundNotification } - var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } - var vendorUUID: String? { UIDevice.current.identifierForVendor?.uuidString } - #elseif os(watchOS) - var deviceType: String { WKInterfaceDevice.current().model } - var systemVersion: String { WKInterfaceDevice.current().systemVersion } - var systemName: String { WKInterfaceDevice.current().systemName } - var operatingSystem: OperatingSystem { .watchOS } - var backgroundNotification: Notification.Name? { nil } - var foregroundNotification: Notification.Name? { nil } - var vendorUUID: String? { nil } - #elseif os(OSX) - var deviceType: String { Sysctl.modelWithoutVersion } - var systemVersion: String { ProcessInfo.processInfo.operatingSystemVersion.compactVersionString } - var systemName: String { "macOS" } - var operatingSystem: OperatingSystem { .macOS } - var backgroundNotification: Notification.Name? { NSApplication.willResignActiveNotification } - var foregroundNotification: Notification.Name? { NSApplication.didBecomeActiveNotification } - var vendorUUID: String? { nil } - #elseif os(tvOS) - var deviceType: String { UIDevice.current.model } - var systemVersion: String { UIDevice.current.systemVersion } - var systemName: String { UIDevice.current.systemName } - var operatingSystem: OperatingSystem { .tvOS } - var backgroundNotification: Notification.Name? { UIApplication.didEnterBackgroundNotification } - var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } - var vendorUUID: String? { UIDevice.current.identifierForVendor?.uuidString } - #endif - - var shouldThrottleOnlineCalls: Bool { !isDebugBuild } - let sdkVersion = "8.2.0" - // Unfortunately, the following does not function in certain configurations, such as when included through SPM -// var sdkVersion: String { -// Bundle(for: LDClient.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? "5.x" -// } + // sourcery: defaultMockValue = Constants.manufacturer + var manufacturer: String { get } + // sourcery: defaultMockValue = Constants.locale + var locale: String { get } + // sourcery: defaultMockValue = Constants.osFamily + var osFamily: String { get } } - -#if os(OSX) -extension OperatingSystemVersion { - var compactVersionString: String { - "\(majorVersion).\(minorVersion).\(patchVersion)" - } -} - -extension Sysctl { - static var modelWithoutVersion: String { - // swiftlint:disable:next force_try - let modelRegex = try! NSRegularExpression(pattern: "([A-Za-z]+)\\d{1,2},\\d") - let model = Sysctl.model // e.g. "MacPro4,1" - return modelRegex.firstCaptureGroup(in: model, options: [], range: model.range) ?? "mac" - } -} - -private extension String { - func substring(_ range: NSRange) -> String? { - guard range.location >= 0 && range.location < self.count, - range.location + range.length >= 0 && range.location + range.length < self.count - else { return nil } - let startIndex = index(self.startIndex, offsetBy: range.location) - let endIndex = index(self.startIndex, offsetBy: range.length) - return String(self[startIndex.. String? { - guard let match = self.firstMatch(in: string, options: [], range: string.range), - let group = string.substring(match.range(at: 1)) - else { return nil } - return group - } -} -#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporter.swift new file mode 100644 index 00000000..74a0dd18 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporter.swift @@ -0,0 +1,11 @@ +import Foundation + +class ApplicationInfoEnvironmentReporter: EnvironmentReporterChainBase { + private var info: ApplicationInfo + + public init(_ applicationInfo: ApplicationInfo) { + self.info = applicationInfo + } + + override var applicationInfo: ApplicationInfo { return info } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift new file mode 100644 index 00000000..583ebb44 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterBuilder.swift @@ -0,0 +1,53 @@ +import Foundation + +class EnvironmentReporterBuilder { + private var applicationInfo: ApplicationInfo? + private var collectPlatformTelemetry: Bool = false + + /// Sets the application info that this environment reporter will report when asked in the future, overriding the automatically sourced {@link ApplicationInfo} + public func applicationInfo(_ applicationInfo: ApplicationInfo) { + self.applicationInfo = applicationInfo + } + + /// Enables automatically collecting attributes from the platform. + public func enableCollectionFromPlatform() { + collectPlatformTelemetry = true + } + + func build() -> EnvironmentReporting { + /** + * Create chain of responsibility with the following priority order + * 1. {@link ApplicationInfoEnvironmentReporter} - holds customer override + * 2. {@link AndroidEnvironmentReporter} - Android platform API next + * 3. {@link SDKEnvironmentReporter} - Fallback is SDK constants + */ + var reporters: [EnvironmentReporterChainBase] = [] + + if let info = applicationInfo { + reporters.append(ApplicationInfoEnvironmentReporter(info)) + } + + if collectPlatformTelemetry { + #if os(iOS) + reporters.append(IOSEnvironmentReporter()) + #elseif os(watchOS) + reporters.append(WatchOSEnvironmentReporter()) + #elseif os(OSX) + reporters.append(MacOSEnvironmentReporter()) + #elseif os(tvOS) + reporters.append(TVOSEnvironmentReporter()) + #endif + } + + // always add fallback reporter + reporters.append(SDKEnvironmentReporter()) + + // build chain of responsibility by iterating on all but last element + for i in reporters.indices.dropLast() { + reporters[i].setNext(reporters[i + 1]) + } + + // guaranteed non-empty since fallback reporter is always added + return reporters[0] + } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBase.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBase.swift new file mode 100644 index 00000000..e03b665a --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBase.swift @@ -0,0 +1,37 @@ +import Foundation + +class EnvironmentReporterChainBase: EnvironmentReporting { + + private static let UNKNOWN: String = "unknown" + + // the next reporter in the chain if there is one + private var next: EnvironmentReporterChainBase? + + public func setNext(_ next: EnvironmentReporterChainBase) { + self.next = next + } + + var applicationInfo: ApplicationInfo { + if let n = next { + return n.applicationInfo + } + + var info = ApplicationInfo() + info.applicationIdentifier(EnvironmentReporterChainBase.UNKNOWN) + info.applicationVersion(EnvironmentReporterChainBase.UNKNOWN) + info.applicationName(EnvironmentReporterChainBase.UNKNOWN) + info.applicationVersionName(EnvironmentReporterChainBase.UNKNOWN) + + return info + } + + var isDebugBuild: Bool { next?.isDebugBuild ?? false } + var deviceModel: String { next?.deviceModel ?? EnvironmentReporterChainBase.UNKNOWN } + var systemVersion: String { next?.systemVersion ?? EnvironmentReporterChainBase.UNKNOWN } + + var vendorUUID: String? { next?.vendorUUID } + + var manufacturer: String { next?.manufacturer ?? "Apple" } + var locale: String { next?.locale ?? Locale.autoupdatingCurrent.identifier } + var osFamily: String { next?.osFamily ?? "Apple" } +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporter.swift new file mode 100644 index 00000000..caff5d8a --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporter.swift @@ -0,0 +1,9 @@ +#if os(iOS) +import Foundation +import UIKit + +class IOSEnvironmentReporter: EnvironmentReporterChainBase { + override var deviceModel: String { UIDevice.current.model } + override var systemVersion: String { UIDevice.current.systemVersion } +} +#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/MacOSEnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/MacOSEnvironmentReporter.swift new file mode 100644 index 00000000..1a7bbc5f --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/MacOSEnvironmentReporter.swift @@ -0,0 +1,48 @@ +#if os(OSX) +import Foundation +import AppKit + +class MacOSEnvironmentReporter: EnvironmentReporterChainBase { + override var deviceModel: String { Sysctl.modelWithoutVersion } + override var systemVersion: String { ProcessInfo.processInfo.operatingSystemVersion.compactVersionString } +} + +extension OperatingSystemVersion { + var compactVersionString: String { + "\(majorVersion).\(minorVersion).\(patchVersion)" + } +} + +extension Sysctl { + static var modelWithoutVersion: String { + // swiftlint:disable:next force_try + let modelRegex = try! NSRegularExpression(pattern: "([A-Za-z]+)\\d{1,2},\\d") + let model = Sysctl.model // e.g. "MacPro4,1" + return modelRegex.firstCaptureGroup(in: model, options: [], range: model.range) ?? "mac" + } +} + +private extension String { + func substring(_ range: NSRange) -> String? { + guard range.location >= 0 && range.location < self.count, + range.location + range.length >= 0 && range.location + range.length < self.count + else { return nil } + let startIndex = index(self.startIndex, offsetBy: range.location) + let endIndex = index(self.startIndex, offsetBy: range.length) + return String(self[startIndex.. String? { + guard let match = self.firstMatch(in: string, options: [], range: string.range), + let group = string.substring(match.range(at: 1)) + else { return nil } + return group + } +} +#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ReportingConsts.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ReportingConsts.swift new file mode 100644 index 00000000..e73dab38 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/ReportingConsts.swift @@ -0,0 +1,6 @@ +import Foundation + +struct ReportingConsts { + static let sdkVersion = "9.0.0" + static let sdkName = "ios-client-sdk" +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporter.swift new file mode 100644 index 00000000..46e705ab --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporter.swift @@ -0,0 +1,26 @@ +import Foundation + +#if os(iOS) || os(tvOS) +import UIKit +#endif + +class SDKEnvironmentReporter: EnvironmentReporterChainBase { + override var applicationInfo: ApplicationInfo { + var info = ApplicationInfo() + info.applicationIdentifier(ReportingConsts.sdkName) + info.applicationVersion(ReportingConsts.sdkVersion) + info.applicationName(ReportingConsts.sdkName) + info.applicationVersionName(ReportingConsts.sdkVersion) + return info + } + + #if os(iOS) + override var vendorUUID: String? { UIDevice.current.identifierForVendor?.uuidString } + #elseif os(watchOS) + override var vendorUUID: String? { nil } + #elseif os(OSX) + override var vendorUUID: String? { nil } + #elseif os(tvOS) + override var vendorUUID: String? { UIDevice.current.identifierForVendor?.uuidString } + #endif +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift new file mode 100644 index 00000000..61a4e97c --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/SystemCapabilities.swift @@ -0,0 +1,33 @@ +import Foundation + +#if os(iOS) || os(tvOS) +import UIKit +#elseif os(OSX) +import AppKit +#elseif os(watchOS) +import WatchKit +#endif + +class SystemCapabilities { + #if os(iOS) + static var backgroundNotification: Notification.Name? { UIApplication.didEnterBackgroundNotification } + static var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } + static var systemName: String { UIDevice.current.systemName } + static var operatingSystem: OperatingSystem { .iOS } + #elseif os(watchOS) + static var backgroundNotification: Notification.Name? { nil } + static var foregroundNotification: Notification.Name? { nil } + static var systemName: String { WKInterfaceDevice.current().systemName } + static var operatingSystem: OperatingSystem { .watchOS } + #elseif os(OSX) + static var backgroundNotification: Notification.Name? { NSApplication.willResignActiveNotification } + static var foregroundNotification: Notification.Name? { NSApplication.didBecomeActiveNotification } + static var systemName: String { "macOS" } + static var operatingSystem: OperatingSystem { .macOS } + #elseif os(tvOS) + static var backgroundNotification: Notification.Name? { UIApplication.didEnterBackgroundNotification } + static var foregroundNotification: Notification.Name? { UIApplication.willEnterForegroundNotification } + static var systemName: String { UIDevice.current.systemName } + static var operatingSystem: OperatingSystem { .tvOS } + #endif +} diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/TVOSEnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/TVOSEnvironmentReporter.swift new file mode 100644 index 00000000..b484473b --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/TVOSEnvironmentReporter.swift @@ -0,0 +1,9 @@ +#if os(tvOS) +import Foundation +import UIKit + +class TVOSEnvironmentReporter: EnvironmentReporterChainBase { + override var deviceModel: String { UIDevice.current.model } + override var systemVersion: String { UIDevice.current.systemVersion } +} +#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporter.swift new file mode 100644 index 00000000..b51e1760 --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporter.swift @@ -0,0 +1,9 @@ +#if os(watchOS) +import Foundation +import WatchKit + +class WatchOSEnvironmentReporter: EnvironmentReporterChainBase { + override var deviceModel: String { WKInterfaceDevice.current().model } + override var systemVersion: String { WKInterfaceDevice.current().systemVersion } +} +#endif diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift index 16b71b78..5a0564e4 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/Throttler.swift @@ -24,9 +24,9 @@ final class Throttler: Throttling { private (set) var workItem: DispatchWorkItem? init(maxDelay: TimeInterval = Constants.defaultDelay, - environmentReporter: EnvironmentReporting = EnvironmentReporter(), + isDebugBuild: Bool = false, dispatcher: ((@escaping RunClosure) -> Void)? = nil) { - self.throttlingEnabled = environmentReporter.shouldThrottleOnlineCalls + self.throttlingEnabled = !isDebugBuild self.maxDelay = maxDelay self.dispatcher = dispatcher ?? { DispatchQueue.global(qos: .userInitiated).async(execute: $0) } } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 4b8a5d94..7a7497ee 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -72,12 +72,7 @@ final class LDClientSpec: QuickSpec { init(newConfig: LDConfig? = nil, startOnline: Bool = false, streamingMode: LDStreamingMode = .streaming, - enableBackgroundUpdates: Bool = true, - operatingSystem: OperatingSystem? = nil) { - - if let operatingSystem = operatingSystem { - serviceFactoryMock.makeEnvironmentReporterReturnValue.operatingSystem = operatingSystem - } + enableBackgroundUpdates: Bool = true) { serviceFactoryMock.makeFlagChangeNotifierReturnValue = FlagChangeNotifier() serviceFactoryMock.makeFeatureFlagCacheCallback = { @@ -89,7 +84,7 @@ final class LDClientSpec: QuickSpec { self.serviceFactoryMock.makeFeatureFlagCacheReturnValue = mockCache } - config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: serviceFactoryMock.makeEnvironmentReporterReturnValue) + config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled, isDebugBuild: false) config.startOnline = startOnline config.streamingMode = streamingMode config.enableBackgroundUpdates = enableBackgroundUpdates @@ -166,6 +161,24 @@ final class LDClientSpec: QuickSpec { describe("startCompletions") { startCompletionSpec() } + + context("when we opt into auto environment attributes") { + it("modifies the initial context") { + let context = LDContext.stub() + + var testContext = TestContext(startOnline: true) + testContext.config.autoEnvAttributes = true + testContext = testContext.withContext(context) + testContext.start() + + expect(context.contextKeys().count) < testContext.subject!.context.contextKeys().count + + let kinds = testContext.subject.service.context.contextKeys().keys + + expect(kinds.contains(AutoEnvContextModifier.ldDeviceKind)) == true + expect(kinds.contains(AutoEnvContextModifier.ldApplicationKind)) == true + } + } } private func startSpec(withTimeout: Bool) { @@ -456,18 +469,25 @@ final class LDClientSpec: QuickSpec { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { beforeEach { - testContext = TestContext(startOnline: true, operatingSystem: os) + testContext = TestContext(startOnline: true) testContext.start() testContext.subject.setRunMode(.background) } it("takes the client and service objects online when background enabled") { expect(testContext.subject.isOnline) == true - expect(testContext.subject.flagSynchronizer.isOnline) == os.isBackgroundEnabled + + // TODO(os-tests): We need to expand this to the other OSs + if os == .iOS && os == SystemCapabilities.operatingSystem { + expect(testContext.subject.flagSynchronizer.isOnline) == os.isBackgroundEnabled + } expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } it("saves the config") { expect(testContext.subject.service.config) == testContext.config - expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode + // TODO(os-tests): We need to expand this to the other OSs + if os == .iOS && os == SystemCapabilities.operatingSystem { + expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode + } expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) expect(testContext.serviceFactoryMock.makeEventReporterReceivedService?.config) == testContext.config } @@ -495,7 +515,7 @@ final class LDClientSpec: QuickSpec { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { beforeEach { - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: os) + testContext = TestContext(startOnline: true, enableBackgroundUpdates: false) testContext.start() testContext.subject.setRunMode(.background) } @@ -592,6 +612,22 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 expect(testContext.flagStoreMock.replaceStoreReceivedNewFlags) == stubFlags } + it("when we have opted into auto environment attributes") { + let testContext = TestContext(startOnline: true) + testContext.config.autoEnvAttributes = true + testContext.start() + testContext.featureFlagCachingMock.reset() + + let newContext = LDContext.stub() + testContext.subject.internalIdentify(newContext: newContext) + + expect(newContext.contextKeys().count) < testContext.subject.service.context.contextKeys().count + + let kinds = testContext.subject.service.context.contextKeys().keys + + expect(kinds.contains(AutoEnvContextModifier.ldDeviceKind)) == true + expect(kinds.contains(AutoEnvContextModifier.ldApplicationKind)) == true + } } } @@ -626,19 +662,24 @@ final class LDClientSpec: QuickSpec { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { it("while configured to enable background updates") { - let testContext = TestContext(operatingSystem: os) + let testContext = TestContext() waitUntil { testContext.start(runMode: .background, completion: $0) } testContext.subject.setOnline(true) - expect(testContext.throttlerMock?.runThrottledCallCount) == (os.isBackgroundEnabled ? 1 : 0) - expect(testContext.subject.isOnline) == os.isBackgroundEnabled expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline - expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode + + // TODO(os-tests): We need to expand this to the other OSs + if os == .iOS && os == SystemCapabilities.operatingSystem { + expect(testContext.throttlerMock?.runThrottledCallCount) == (os.isBackgroundEnabled ? 1 : 0) + expect(testContext.subject.isOnline) == os.isBackgroundEnabled + expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode + } + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } it("while configured to disable background updates") { - let testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: os) + let testContext = TestContext(enableBackgroundUpdates: false) waitUntil { testContext.start(runMode: .background, completion: $0) } testContext.subject.setOnline(true) @@ -653,7 +694,7 @@ final class LDClientSpec: QuickSpec { } } it("set online when the mobile key is empty") { - let testContext = TestContext(newConfig: LDConfig(mobileKey: "")) + let testContext = TestContext(newConfig: LDConfig(mobileKey: "", autoEnvAttributes: .disabled)) waitUntil { testContext.start(completion: $0) } testContext.subject.setOnline(true) @@ -990,9 +1031,9 @@ final class LDClientSpec: QuickSpec { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { it("background updates disabled") { - let testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: os) + let testContext = TestContext(startOnline: true, enableBackgroundUpdates: false) testContext.start() - NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) + NotificationCenter.default.post(name: SystemCapabilities.backgroundNotification!, object: self) expect(testContext.subject.runMode).toEventually(equal(LDClientRunMode.background)) expect(testContext.subject.isOnline) == true @@ -1001,19 +1042,23 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagSynchronizerMock.isOnline) == false } it("background updates enabled") { - let testContext = TestContext(startOnline: true, operatingSystem: os) + let testContext = TestContext(startOnline: true) testContext.start() waitUntil { done in - NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) + NotificationCenter.default.post(name: SystemCapabilities.backgroundNotification!, object: self) DispatchQueue(label: "BackgroundUpdatesEnabled").asyncAfter(deadline: .now() + 0.2, execute: done) } expect(testContext.subject.isOnline) == true expect(testContext.subject.runMode) == LDClientRunMode.background expect(testContext.eventReporterMock.isOnline) == true - expect(testContext.flagSynchronizerMock.isOnline) == os.isBackgroundEnabled - expect(testContext.flagSynchronizerMock.streamingMode) == os.backgroundStreamingMode + + // TODO(os-tests): We need to expand this to the other OSs + if os == .iOS && os == SystemCapabilities.operatingSystem { + expect(testContext.flagSynchronizerMock.isOnline) == os.isBackgroundEnabled + expect(testContext.flagSynchronizerMock.streamingMode) == os.backgroundStreamingMode + } } } } @@ -1021,7 +1066,7 @@ final class LDClientSpec: QuickSpec { it("when offline") { let testContext = TestContext() testContext.start() - NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) + NotificationCenter.default.post(name: SystemCapabilities.backgroundNotification!, object: self) expect(testContext.subject.isOnline) == false expect(testContext.subject.runMode) == LDClientRunMode.background @@ -1036,9 +1081,9 @@ final class LDClientSpec: QuickSpec { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { it("when online at foreground notification") { - let testContext = TestContext(startOnline: true, operatingSystem: os) + let testContext = TestContext(startOnline: true) testContext.start(runMode: .background) - NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) + NotificationCenter.default.post(name: SystemCapabilities.foregroundNotification!, object: self) expect(testContext.subject.isOnline) == true expect(testContext.subject.runMode) == LDClientRunMode.foreground @@ -1046,9 +1091,9 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagSynchronizerMock.isOnline) == true } it("when offline at foreground notification") { - let testContext = TestContext(operatingSystem: os) + let testContext = TestContext() testContext.start(runMode: .background) - NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) + NotificationCenter.default.post(name: SystemCapabilities.foregroundNotification!, object: self) expect(testContext.subject.isOnline) == false expect(testContext.subject.runMode) == LDClientRunMode.foreground @@ -1060,13 +1105,15 @@ final class LDClientSpec: QuickSpec { } } + // TODO(os-tests): These tests won't actually run until we support macos targets + #if os(OSX) describe("change run mode on macOS") { context("while online") { context("and running in the foreground") { context("set background") { context("with background updates enabled") { it("streaming mode") { - let testContext = TestContext(startOnline: true, operatingSystem: .macOS) + let testContext = TestContext(startOnline: true) testContext.start() testContext.subject.setRunMode(.background) @@ -1075,7 +1122,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } it("polling mode") { - let testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) + let testContext = TestContext(startOnline: true, streamingMode: .polling) testContext.start() testContext.subject.setRunMode(.background) @@ -1086,7 +1133,7 @@ final class LDClientSpec: QuickSpec { } } it("with background updates disabled") { - let testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: .macOS) + let testContext = TestContext(startOnline: true, enableBackgroundUpdates: false) testContext.start() testContext.subject.setRunMode(.background) @@ -1097,7 +1144,7 @@ final class LDClientSpec: QuickSpec { } } it("set foreground") { - let testContext = TestContext(startOnline: true, operatingSystem: .macOS) + let testContext = TestContext(startOnline: true) testContext.start() let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount let flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount @@ -1113,7 +1160,7 @@ final class LDClientSpec: QuickSpec { } context("and running in the background") { it("set background") { - let testContext = TestContext(startOnline: true, operatingSystem: .macOS) + let testContext = TestContext(startOnline: true) testContext.start() testContext.subject.setRunMode(.background) let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount @@ -1129,7 +1176,7 @@ final class LDClientSpec: QuickSpec { } context("set foreground") { it("streaming mode") { - let testContext = TestContext(startOnline: true, operatingSystem: .macOS) + let testContext = TestContext(startOnline: true) testContext.start(runMode: .background) testContext.subject.setRunMode(.foreground) @@ -1138,7 +1185,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } it("polling mode") { - let testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) + let testContext = TestContext(startOnline: true, streamingMode: .polling) testContext.start(runMode: .background) testContext.subject.setRunMode(.foreground) @@ -1154,7 +1201,7 @@ final class LDClientSpec: QuickSpec { context("and running in the foreground") { context("set background") { it("with background updates enabled") { - let testContext = TestContext(operatingSystem: .macOS) + let testContext = TestContext() waitUntil { testContext.start(completion: $0) } testContext.subject.setRunMode(.background) @@ -1164,7 +1211,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } it("with background updates disabled") { - let testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: .macOS) + let testContext = TestContext(enableBackgroundUpdates: false) waitUntil { testContext.start(completion: $0) } testContext.subject.setRunMode(.background) @@ -1176,7 +1223,7 @@ final class LDClientSpec: QuickSpec { } } it("set foreground") { - let testContext = TestContext(operatingSystem: .macOS) + let testContext = TestContext() waitUntil { testContext.start(completion: $0) } let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount @@ -1193,7 +1240,7 @@ final class LDClientSpec: QuickSpec { } context("and running in the background") { it("set background") { - let testContext = TestContext(operatingSystem: .macOS) + let testContext = TestContext() waitUntil { testContext.start(runMode: .background, completion: $0) } let eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount @@ -1209,7 +1256,7 @@ final class LDClientSpec: QuickSpec { } context("set foreground") { it("streaming mode") { - let testContext = TestContext(operatingSystem: .macOS) + let testContext = TestContext() waitUntil { testContext.start(runMode: .background, completion: $0) } testContext.subject.setRunMode(.foreground) @@ -1219,7 +1266,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.flagSynchronizerMock.streamingMode) == LDStreamingMode.streaming } it("polling mode") { - let testContext = TestContext(streamingMode: .polling, operatingSystem: .macOS) + let testContext = TestContext(streamingMode: .polling) waitUntil { testContext.start(runMode: .background, completion: $0) } testContext.subject.setRunMode(.foreground) @@ -1233,6 +1280,7 @@ final class LDClientSpec: QuickSpec { } } } + #endif } private func streamingModeSpec() { @@ -1241,7 +1289,12 @@ final class LDClientSpec: QuickSpec { describe("flag synchronizer streaming mode") { OperatingSystem.allOperatingSystems.forEach { os in it("on \(os) sets the flag synchronizer streaming mode") { - testContext = TestContext(startOnline: true, operatingSystem: os) + // TODO(os-tests): We need to expand this to the other OSs + if os == .watchOS { + return + } + + testContext = TestContext(startOnline: true) testContext.start() expect(testContext.makeFlagSynchronizerStreamingMode) == (os.isStreamingEnabled ? LDStreamingMode.streaming : LDStreamingMode.polling) } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index d62c5761..c2813f73 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -28,7 +28,7 @@ final class ClientServiceMockFactory: ClientServiceCreating { return makeCacheConverterReturnValue } - func makeDarklyServiceProvider(config: LDConfig, context: LDContext) -> DarklyServiceProvider { + func makeDarklyServiceProvider(config: LDConfig, context: LDContext, envReporter: EnvironmentReporting) -> DarklyServiceProvider { DarklyServiceMock(config: config, context: context) } @@ -98,14 +98,14 @@ final class ClientServiceMockFactory: ClientServiceCreating { var makeDiagnosticReporterCallCount = 0 var makeDiagnosticReporterReceivedService: DarklyServiceProvider? = nil - func makeDiagnosticReporter(service: DarklyServiceProvider) -> DiagnosticReporting { + func makeDiagnosticReporter(service: DarklyServiceProvider, environmentReporter: EnvironmentReporting) -> DiagnosticReporting { makeDiagnosticReporterCallCount += 1 makeDiagnosticReporterReceivedService = service return DiagnosticReportingMock() } var makeEnvironmentReporterReturnValue: EnvironmentReportingMock = EnvironmentReportingMock() - func makeEnvironmentReporter() -> EnvironmentReporting { + func makeEnvironmentReporter(config: LDConfig) -> EnvironmentReporting { return makeEnvironmentReporterReturnValue } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift index e933cad2..a2b3a5c6 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/EnvironmentReportingMock.swift @@ -1,11 +1,17 @@ import Foundation +@testable import LaunchDarkly + extension EnvironmentReportingMock { struct Constants { - static let deviceType = "deviceTypeStub" + static let applicationInfo = LaunchDarkly.ApplicationInfo() + static let deviceModel = "deviceModelStub" static let systemVersion = "systemVersionStub" static let systemName = "systemNameStub" static let vendorUUID = "vendorUUIDStub" static let sdkVersion = "sdkVersionStub" + static let manufacturer = "manufacturerStub" + static let locale = "localeStub" + static let osFamily = "osFamilyStub" } } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift index 2c956804..f3be8b0b 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/LDConfigStub.swift @@ -9,11 +9,11 @@ extension LDConfig { } static var stub: LDConfig { - stub(mobileKey: Constants.mockMobileKey, environmentReporter: EnvironmentReportingMock()) + stub(mobileKey: Constants.mockMobileKey, autoEnvAttributes: .disabled, isDebugBuild: true) } - static func stub(mobileKey: String, environmentReporter: EnvironmentReportingMock) -> LDConfig { - var config = LDConfig(mobileKey: mobileKey, environmentReporter: environmentReporter) + static func stub(mobileKey: String, autoEnvAttributes: AutoEnvAttributes, isDebugBuild: Bool) -> LDConfig { + var config = LDConfig(mobileKey: mobileKey, autoEnvAttributes: autoEnvAttributes, isDebugBuild: isDebugBuild) config.baseUrl = DarklyServiceMock.Constants.mockBaseUrl config.eventsUrl = DarklyServiceMock.Constants.mockEventsUrl config.streamUrl = DarklyServiceMock.Constants.mockStreamUrl diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift deleted file mode 100644 index 67023d96..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/LDUserStub.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -@testable import LaunchDarkly - -extension LDUser { - struct StubConstants { - static let key = "stub.user.key" - static let userKey = "userKey" - static let name = "stub.user.name" - static let firstName = "stub.user.firstName" - static let lastName = "stub.user.lastName" - static let isAnonymous = false - static let country = "stub.user.country" - static let ipAddress = "stub.user.ipAddress" - static let email = "stub.user@email.com" - static let avatar = "stub.user.avatar" - static let device: LDValue = "stub.user.custom.device" - static let operatingSystem: LDValue = "stub.user.custom.operatingSystem" - static let custom: [String: LDValue] = ["stub.user.custom.keyA": "stub.user.custom.valueA", - "stub.user.custom.keyB": true, - "stub.user.custom.keyC": 1027, - "stub.user.custom.keyD": 2.71828, - "stub.user.custom.keyE": [0, 1, 2], - "stub.user.custom.keyF": ["1": 1, "2": 2, "3": 3]] - - static func custom(includeSystemValues: Bool) -> [String: LDValue] { - var custom = StubConstants.custom - if includeSystemValues { - custom["device"] = StubConstants.device - custom["os"] = StubConstants.operatingSystem - } - return custom - } - } - - static func stub(key: String? = nil, - environmentReporter: EnvironmentReportingMock? = nil) -> LDUser { - let user = LDUser(key: key ?? UUID().uuidString, - name: StubConstants.name, - firstName: StubConstants.firstName, - lastName: StubConstants.lastName, - country: StubConstants.country, - ipAddress: StubConstants.ipAddress, - email: StubConstants.email, - avatar: StubConstants.avatar, - custom: StubConstants.custom(includeSystemValues: true), - isAnonymous: StubConstants.isAnonymous) - return user - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift index f6a68e88..0dcda1fa 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextCodableSpec.swift @@ -45,7 +45,7 @@ final class LDContextCodableSpec: XCTestCase { for json in testCases { let context = try JSONDecoder().decode(LDContext.self, from: Data(json.utf8)) let jsonEncoder = JSONEncoder() - jsonEncoder.userInfo = [LDContext.UserInfoKeys.includePrivateAttributes:true] + jsonEncoder.userInfo = [LDContext.UserInfoKeys.includePrivateAttributes: true] let output = try jsonEncoder.encode(context) let outputJson = String(data: output, encoding: .utf8) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift index 953c8d4a..bd9a9b8d 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/LDContextSpec.swift @@ -130,9 +130,8 @@ final class LDContextSpec: XCTestCase { } } - func testMultikindCannotContainAnotherMultiKind() throws { + func testMultiBuilderFlattensMulticontext() throws { var multiBuilder = LDMultiContextBuilder() - var builder = LDContextBuilder(key: "key") multiBuilder.addContext(try builder.build().get()) @@ -142,13 +141,21 @@ final class LDContextSpec: XCTestCase { let multiContext = try multiBuilder.build().get() + // Reset to a new builder + multiBuilder = LDMultiContextBuilder() multiBuilder.addContext(multiContext) switch multiBuilder.build() { - case .success: - XCTFail("Multibuilder should have failed to build with a multi-context.") - case .failure(let error): - XCTAssertEqual(error, .nestedMultiKind) + case .success(let context): + // Ensure we didn't remove them from the existing context. + XCTAssertEqual(2, multiContext.contextKeys().count) + + XCTAssertTrue(context.isMulti()) + let contextKeys = context.contextKeys() + XCTAssertEqual(contextKeys["user"], "key") + XCTAssertEqual(contextKeys["org"], "orgKey") + case .failure: + XCTFail("Multi-kind builder should not have failed.") } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/Context/ModifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/Context/ModifierSpec.swift new file mode 100644 index 00000000..cc862832 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/Models/Context/ModifierSpec.swift @@ -0,0 +1,161 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class ModifierSpec: XCTestCase { + /** + * Requirement 1.2.2.1 - Schema adherence + * Requirement 1.2.2.3 - Adding all attributes + * Requirement 1.2.2.5 - Schema version in envAttributes + * Requirement 1.2.2.7 - Adding all context kinds + */ + func testAdheresToSchema() throws { + let underTest = AutoEnvContextModifier(environmentReporter: EnvironmentReportingMock()) + + var inputBuilder = LDContextBuilder(key: "aKey") + inputBuilder.kind("aKind") + inputBuilder.trySetValue("dontOverwriteMeBro", "really bro") + let input = try inputBuilder.build().get() + + let outputContext = underTest.modifyContext(input) + + // pull the generated keys out and set them on contexts, this is so + // we can use the built in context equality check in our assertions + var appBuilder = LDContextBuilder(key: outputContext.contextKeys()["ld_application"]!) + appBuilder.kind("ld_application") + appBuilder.trySetValue("id", LDValue.string(EnvironmentReportingMock.Constants.applicationInfo.applicationId)) + appBuilder.trySetValue("version", LDValue.string(EnvironmentReportingMock.Constants.applicationInfo.applicationVersion)) + appBuilder.trySetValue("name", LDValue.string(EnvironmentReportingMock.Constants.applicationInfo.applicationName)) + appBuilder.trySetValue("versionName", LDValue.string(EnvironmentReportingMock.Constants.applicationInfo.applicationVersionName)) + appBuilder.trySetValue("locale", LDValue.string(EnvironmentReportingMock.Constants.locale)) + appBuilder.trySetValue("envAttributesVersion", AutoEnvContextModifier.specVersion.toLDValue()) + let appContext = try appBuilder.build().get() + + var deviceBuilder = LDContextBuilder(key: outputContext.contextKeys()["ld_device"]!) + deviceBuilder.kind("ld_device") + deviceBuilder.trySetValue("manufacturer", LDValue.string(EnvironmentReportingMock.Constants.manufacturer)) + deviceBuilder.trySetValue("model", LDValue.string(EnvironmentReportingMock.Constants.deviceModel)) + deviceBuilder.trySetValue("os", LDValue(dictionaryLiteral: + ("family", EnvironmentReportingMock.Constants.osFamily.toLDValue()), + ("name", SystemCapabilities.systemName.toLDValue()), + ("version", EnvironmentReportingMock.Constants.systemVersion.toLDValue()) + )) + deviceBuilder.trySetValue("envAttributesVersion", AutoEnvContextModifier.specVersion.toLDValue()) + let deviceContext = try deviceBuilder.build().get() + + var multiBuilder = LDMultiContextBuilder() + multiBuilder.addContext(input) + multiBuilder.addContext(appContext) + multiBuilder.addContext(deviceContext) + let expectedContext = try multiBuilder.build().get() + + // Ensure we didn't remove them from the existing context. + XCTAssertEqual(3, outputContext.contextKeys().count) + XCTAssertTrue(outputContext.isMulti()) + XCTAssertEqual(expectedContext, outputContext) + } + + /** + * Requirement 1.2.2.6 - Don't add kind if already exists + * Requirement 1.2.5.1 - Doesn't change customer provided data + * Requirement 1.2.7.1 - Log warning when kind already exists + */ + func testDoesNotOverwriteCustomerData() throws { + let underTest = AutoEnvContextModifier(environmentReporter: EnvironmentReportingMock()) + + var inputBuilder = LDContextBuilder(key: "aKey") + inputBuilder.kind("ld_application") + inputBuilder.trySetValue("dontOverwriteMeBro", "really bro") + let input = try inputBuilder.build().get() + + let outputContext = underTest.modifyContext(input) + + var deviceBuilder = LDContextBuilder(key: outputContext.contextKeys()["ld_device"]!) + deviceBuilder.kind("ld_device") + deviceBuilder.trySetValue("manufacturer", EnvironmentReportingMock.Constants.manufacturer.toLDValue()) + deviceBuilder.trySetValue("model", EnvironmentReportingMock.Constants.deviceModel.toLDValue()) + deviceBuilder.trySetValue("os", LDValue(dictionaryLiteral: + ("family", EnvironmentReportingMock.Constants.osFamily.toLDValue()), + ("name", SystemCapabilities.systemName.toLDValue()), + ("version", EnvironmentReportingMock.Constants.systemVersion.toLDValue()) + )) + deviceBuilder.trySetValue("envAttributesVersion", AutoEnvContextModifier.specVersion.toLDValue()) + let deviceContext = try deviceBuilder.build().get() + + var multiBuilder = LDMultiContextBuilder() + multiBuilder.addContext(input) + multiBuilder.addContext(deviceContext) + let expectedContext = try multiBuilder.build().get() + + // Ensure we didn't remove them from the existing context. + XCTAssertEqual(2, outputContext.contextKeys().count) + XCTAssertTrue(outputContext.isMulti()) + XCTAssertEqual(expectedContext, outputContext) + } + + /** + * Requirement 1.2.5.1 - Doesn't change customer provided data + */ + func testDoesNotOverwriteCustomerDataMultiContext() throws { + let underTest = AutoEnvContextModifier(environmentReporter: EnvironmentReportingMock()) + + var inputBuilder1 = LDContextBuilder(key: "aKey") + inputBuilder1.kind("ld_application") + inputBuilder1.trySetValue("dontOverwriteMeBro", "really bro") + let input1 = try inputBuilder1.build().get() + + var inputBuilder2 = LDContextBuilder(key: "aKey") + inputBuilder2.kind("ld_device") + inputBuilder2.trySetValue("andDontOverwriteThisEither", "bro") + let input2 = try inputBuilder2.build().get() + + var inputMultiBuilder = LDMultiContextBuilder() + inputMultiBuilder.addContext(input1) + inputMultiBuilder.addContext(input2) + let inputMulti = try inputMultiBuilder.build().get() + + let outputContext = underTest.modifyContext(inputMulti) + + // Ensure we didn't remove them from the existing context. + XCTAssertEqual(2, outputContext.contextKeys().count) + XCTAssertTrue(outputContext.isMulti()) + XCTAssertEqual(inputMulti, outputContext) + } + + /** + * Requirement 1.2.6.3 - Generated keys are consistent + */ + func testGeneratesConsistentContextAcrossMultipleCalls() throws { + let underTest = AutoEnvContextModifier(environmentReporter: EnvironmentReportingMock()) + + var inputBuilder = LDContextBuilder(key: "aKey") + inputBuilder.kind("aKind") + inputBuilder.trySetValue("dontOverwriteMeBro", "really bro") + let input = try inputBuilder.build().get() + + let outputContext1 = underTest.modifyContext(input) + let outputContext2 = underTest.modifyContext(input) + + XCTAssertEqual(outputContext1, outputContext2) + } + + func testGeneratedLdApplicationKey() throws { + let underTest = AutoEnvContextModifier(environmentReporter: EnvironmentReportingMock()) + + var inputBuilder = LDContextBuilder(key: "aKey") + inputBuilder.kind("aKind") + inputBuilder.trySetValue("dontOverwriteMeBro", "really bro") + let input = try inputBuilder.build().get() + let outputContext = underTest.modifyContext(input) + let outputKey = outputContext.contexts.first(where: { $0.kind == Kind("ld_application") })!.getValue(Reference("key")) + + // expected key is the hash of the concatanation of id and version + let expectedKey = Util.sha256base64( + EnvironmentReportingMock.Constants.applicationInfo.applicationId + ":" + + EnvironmentReportingMock.Constants.applicationInfo.applicationVersion + ).toLDValue() + + XCTAssertEqual(expectedKey, outputKey) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index 2ebb4158..b76e4186 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -69,13 +69,13 @@ final class DiagnosticEventSpec: QuickSpec { let config = LDConfig.stub let diagnosticSdk = DiagnosticSdk(config: config) expect(diagnosticSdk.name) == "ios-client-sdk" - expect(diagnosticSdk.version) == config.environmentReporter.sdkVersion + expect(diagnosticSdk.version) == ReportingConsts.sdkVersion expect(diagnosticSdk.wrapperName).to(beNil()) expect(diagnosticSdk.wrapperVersion).to(beNil()) encodesToObject(diagnosticSdk) { decoded in expect(decoded.count) == 2 expect(decoded["name"]) == "ios-client-sdk" - expect(decoded["version"]) == .string(config.environmentReporter.sdkVersion) + expect(decoded["version"]) == .string(ReportingConsts.sdkVersion) } } } @@ -86,13 +86,13 @@ final class DiagnosticEventSpec: QuickSpec { config.wrapperVersion = "0.1.0" let diagnosticSdk = DiagnosticSdk(config: config) expect(diagnosticSdk.name) == "ios-client-sdk" - expect(diagnosticSdk.version) == config.environmentReporter.sdkVersion + expect(diagnosticSdk.version) == ReportingConsts.sdkVersion expect(diagnosticSdk.wrapperName) == config.wrapperName expect(diagnosticSdk.wrapperVersion) == config.wrapperVersion encodesToObject(diagnosticSdk) { decoded in expect(decoded.count) == 4 expect(decoded["name"]) == "ios-client-sdk" - expect(decoded["version"]) == .string(config.environmentReporter.sdkVersion) + expect(decoded["version"]) == .string(ReportingConsts.sdkVersion) expect(decoded["wrapperName"]) == "ReactNative" expect(decoded["wrapperVersion"]) == "0.1.0" } @@ -109,17 +109,19 @@ final class DiagnosticEventSpec: QuickSpec { context("with operating system \(String(describing: os))") { beforeEach { environmentReporter = EnvironmentReportingMock() - environmentReporter.operatingSystem = os - let config = LDConfig(mobileKey: "testKey", environmentReporter: environmentReporter) - diagnosticPlatform = DiagnosticPlatform(config: config) + diagnosticPlatform = DiagnosticPlatform(environmentReporting: environmentReporter) } it("inits with os values") { expect(diagnosticPlatform.name) == "swift" - expect(diagnosticPlatform.systemName) == environmentReporter.operatingSystem.rawValue expect(diagnosticPlatform.systemVersion) == environmentReporter.systemVersion - expect(diagnosticPlatform.backgroundEnabled) == environmentReporter.operatingSystem.isBackgroundEnabled - expect(diagnosticPlatform.streamingEnabled) == environmentReporter.operatingSystem.isStreamingEnabled - expect(diagnosticPlatform.deviceType) == environmentReporter.deviceType + expect(diagnosticPlatform.deviceType) == environmentReporter.deviceModel + + // TODO(os-tests): We need to expand this to the other OSs + if os == .iOS && os == SystemCapabilities.operatingSystem { + expect(diagnosticPlatform.systemName) == os.rawValue + expect(diagnosticPlatform.backgroundEnabled) == os.isBackgroundEnabled + expect(diagnosticPlatform.streamingEnabled) == os.isStreamingEnabled + } } it("encodes correct values to keys") { encodesToObject(diagnosticPlatform) { decoded in @@ -168,9 +170,7 @@ final class DiagnosticEventSpec: QuickSpec { } private func customizedConfig() -> LDConfig { - let environmentReporter = EnvironmentReportingMock() - environmentReporter.operatingSystem = OperatingSystem.backgroundEnabledOperatingSystems.first! - var customConfig = LDConfig(mobileKey: "foobar", environmentReporter: environmentReporter) + var customConfig = LDConfig(mobileKey: "foobar", autoEnvAttributes: .disabled, isDebugBuild: true) customConfig.baseUrl = URL(string: "https://clientstream.launchdarkly.com")! customConfig.eventsUrl = URL(string: "https://app.launchdarkly.com")! customConfig.streamUrl = URL(string: "https://mobile.launchdarkly.com")! @@ -193,9 +193,7 @@ final class DiagnosticEventSpec: QuickSpec { } private func diagnosticConfigSpec() { - let environmentReporter = EnvironmentReportingMock() - environmentReporter.operatingSystem = OperatingSystem.backgroundEnabledOperatingSystems.first! - let defaultConfig = LDConfig(mobileKey: "foobar", environmentReporter: environmentReporter) + let defaultConfig = LDConfig(mobileKey: "foobar", autoEnvAttributes: .disabled, isDebugBuild: true) let customConfig = customizedConfig() context("DiagnosticConfig") { context("init with default config") { @@ -234,7 +232,10 @@ final class DiagnosticEventSpec: QuickSpec { expect(diagnosticConfig.pollingIntervalMillis) == 360_000 expect(diagnosticConfig.backgroundPollingIntervalMillis) == 1_800_000 expect(diagnosticConfig.useReport) == true - expect(diagnosticConfig.backgroundPollingDisabled) == false + // TODO(os-tests): We need to expand this to the other OSs + if SystemCapabilities.operatingSystem == .iOS { + expect(diagnosticConfig.backgroundPollingDisabled) == true + } expect(diagnosticConfig.evaluationReasonsRequested) == true // All negative values become -1 for consistency expect(diagnosticConfig.maxCachedContexts) == -1 @@ -342,7 +343,7 @@ final class DiagnosticEventSpec: QuickSpec { beforeEach { now = Date().millisSince1970 diagnosticId = DiagnosticId(diagnosticId: UUID().uuidString, sdkKey: "foobar") - diagnosticInit = DiagnosticInit(config: customConfig, diagnosticId: diagnosticId, creationDate: now) + diagnosticInit = DiagnosticInit(config: customConfig, environmentReporting: EnvironmentReportingMock(), diagnosticId: diagnosticId, creationDate: now) } it("inits with correct values") { expect(diagnosticInit.kind) == DiagnosticKind.diagnosticInit @@ -352,7 +353,10 @@ final class DiagnosticEventSpec: QuickSpec { // Spot check sub objects, just to ensure config is being passed along expect(diagnosticInit.sdk.wrapperName) == customConfig.wrapperName expect(diagnosticInit.configuration.customBaseURI) == true - expect(diagnosticInit.platform.backgroundEnabled) == true + // TODO(os-tests): We need to expand this to the other OSs + if SystemCapabilities.operatingSystem == .iOS { + expect(diagnosticInit.platform.backgroundEnabled) == false + } } it("encodes correct values to keys") { let expectedId = encodeToLDValue(diagnosticId) diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 360bb266..08a24a99 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -42,7 +42,6 @@ final class LDConfigSpec: XCTestCase { ("poll interval", Constants.flagPollingInterval, { c, v in c.flagPollingInterval = v as! TimeInterval }), ("background poll interval", Constants.backgroundFlagPollingInterval, { c, v in c.backgroundFlagPollingInterval = v as! TimeInterval }), ("streaming mode", Constants.streamingMode, { c, v in c.streamingMode = v as! LDStreamingMode }), - ("enable background updates", Constants.enableBackgroundUpdates, { c, v in c.enableBackgroundUpdates = v as! Bool }), ("start online", Constants.startOnline, { c, v in c.startOnline = v as! Bool }), ("debug mode", Constants.debugMode, { c, v in c.isDebugMode = v as! Bool }), ("all context attributes private", Constants.allContextAttributesPrivate, { c, v in c.allContextAttributesPrivate = v as! Bool }), @@ -57,7 +56,7 @@ final class LDConfigSpec: XCTestCase { ("additional headers", Constants.additionalHeaders, { c, v in c.additionalHeaders = v as! [String: String]})] func testInitDefault() { - let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey) + let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) XCTAssertEqual(config.mobileKey, LDConfig.Constants.mockMobileKey) XCTAssertEqual(config.baseUrl, LDConfig.Defaults.baseUrl) XCTAssertEqual(config.eventsUrl, LDConfig.Defaults.eventsUrl) @@ -85,9 +84,7 @@ final class LDConfigSpec: XCTestCase { func testInitUpdate() { OperatingSystem.allOperatingSystems.forEach { os in - let environmentReporter = EnvironmentReportingMock() - environmentReporter.operatingSystem = os - var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) + var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) testFields.forEach { _, otherVal, setter in setter(&config, otherVal) } @@ -101,7 +98,10 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.flagPollingInterval, Constants.flagPollingInterval, "\(os)") XCTAssertEqual(config.backgroundFlagPollingInterval, Constants.backgroundFlagPollingInterval, "\(os)") XCTAssertEqual(config.streamingMode, Constants.streamingMode, "\(os)") - XCTAssertEqual(config.enableBackgroundUpdates, os.isBackgroundEnabled, "\(os)") + // TODO(os-tests): We need to expand this to the other OSs + if os == .iOS && os == SystemCapabilities.operatingSystem { + XCTAssertEqual(config.enableBackgroundUpdates, os.isBackgroundEnabled, "\(os)") + } XCTAssertEqual(config.startOnline, Constants.startOnline, "\(os)") XCTAssertEqual(config.allContextAttributesPrivate, Constants.allContextAttributesPrivate, "\(os)") XCTAssertEqual(config.privateContextAttributes, Constants.privateContextAttributes, "\(os)") @@ -118,18 +118,14 @@ final class LDConfigSpec: XCTestCase { } func testMinimaInitDebug() { - let environmentReporter = EnvironmentReportingMock() - environmentReporter.isDebugBuild = true - let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) + let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled, isDebugBuild: true) XCTAssertEqual(config.minima.flagPollingInterval, LDConfig.Minima.Debug.flagPollingInterval) XCTAssertEqual(config.minima.backgroundFlagPollingInterval, LDConfig.Minima.Debug.backgroundFlagPollingInterval) XCTAssertEqual(config.minima.diagnosticRecordingInterval, LDConfig.Minima.Debug.diagnosticRecordingInterval) } func testMinimaInitRelease() { - let environmentReporter = EnvironmentReportingMock() - environmentReporter.isDebugBuild = false - let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) + let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) XCTAssertEqual(config.minima.flagPollingInterval, LDConfig.Minima.Production.flagPollingInterval) XCTAssertEqual(config.minima.backgroundFlagPollingInterval, LDConfig.Minima.Production.backgroundFlagPollingInterval) XCTAssertEqual(config.minima.diagnosticRecordingInterval, LDConfig.Minima.Production.diagnosticRecordingInterval) @@ -137,9 +133,7 @@ final class LDConfigSpec: XCTestCase { func testFlagPollingInterval() { [false, true].forEach { debugMode in - let environmentReporter = EnvironmentReportingMock() - environmentReporter.isDebugBuild = debugMode - var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) + var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled, isDebugBuild: debugMode) // polling interval above minimum. var interval = config.minima.flagPollingInterval + 0.001 config.flagPollingInterval = interval @@ -168,7 +162,7 @@ final class LDConfigSpec: XCTestCase { } func testDiagnosticReportingInterval() { - var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: EnvironmentReportingMock()) + var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) // set below minimum value config.diagnosticRecordingInterval = config.minima.diagnosticRecordingInterval - 0.001 XCTAssertEqual(config.diagnosticRecordingInterval, config.minima.diagnosticRecordingInterval) @@ -178,19 +172,16 @@ final class LDConfigSpec: XCTestCase { } func testEquals() { - let environmentReporter = EnvironmentReportingMock() - // must use a background enabled OS to test inequality of background enabled - environmentReporter.operatingSystem = OperatingSystem.backgroundEnabledOperatingSystems.first! - let defaultConfig = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) + let defaultConfig = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) // same config symmetricAssertEqual(defaultConfig, defaultConfig) // equivalent config - symmetricAssertEqual(defaultConfig, LDConfig(mobileKey: LDConfig.Constants.mockMobileKey)) + symmetricAssertEqual(defaultConfig, LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled)) // different mobile key - symmetricAssertNotEqual(defaultConfig, LDConfig(mobileKey: LDConfig.Constants.alternateMobileKey)) + symmetricAssertNotEqual(defaultConfig, LDConfig(mobileKey: LDConfig.Constants.alternateMobileKey, autoEnvAttributes: .disabled)) testFields.forEach { name, otherVal, setter in - var otherConfig = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) + var otherConfig = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) setter(&otherConfig, otherVal) symmetricAssertNotEqual(defaultConfig, otherConfig, "\(name) is the same") } @@ -206,20 +197,22 @@ final class LDConfigSpec: XCTestCase { func testAllowStreamingModeSpec() { for operatingSystem in OperatingSystem.allOperatingSystems { - let environmenReporter = EnvironmentReportingMock() - environmenReporter.operatingSystem = operatingSystem - let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmenReporter) - XCTAssertEqual(config.allowStreamingMode, operatingSystem.isStreamingEnabled) + let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) + // TODO(os-tests): We need to expand this to the other OSs + if operatingSystem == .iOS && operatingSystem == SystemCapabilities.operatingSystem { + XCTAssertEqual(config.allowStreamingMode, operatingSystem.isStreamingEnabled) + } } } func testAllowBackgroundUpdatesSpec() { for operatingSystem in OperatingSystem.allOperatingSystems { - let environmenReporter = EnvironmentReportingMock() - environmenReporter.operatingSystem = operatingSystem - var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmenReporter) + var config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) config.enableBackgroundUpdates = true - XCTAssertEqual(config.enableBackgroundUpdates, operatingSystem.isBackgroundEnabled) + // TODO(os-tests): We need to expand this to the other OSs + if operatingSystem == .iOS && operatingSystem == SystemCapabilities.operatingSystem { + XCTAssertEqual(config.enableBackgroundUpdates, operatingSystem.isBackgroundEnabled) + } } } @@ -227,12 +220,25 @@ final class LDConfigSpec: XCTestCase { var applicationInfo = ApplicationInfo() XCTAssertEqual("", applicationInfo.buildTag()) + applicationInfo.applicationIdentifier("example-id") + XCTAssertEqual( + "application-id/example-id", + applicationInfo.buildTag()) + + applicationInfo.applicationName("example-name") + XCTAssertEqual( + "application-id/example-id application-name/example-name", + applicationInfo.buildTag()) + applicationInfo.applicationVersion("example-version") - XCTAssertEqual("application-version/example-version", applicationInfo.buildTag()) + XCTAssertEqual( + "application-id/example-id application-name/example-name application-version/example-version", + applicationInfo.buildTag()) - applicationInfo.applicationIdentifier("example-id") - XCTAssertEqual("application-id/example-id application-version/example-version", applicationInfo.buildTag()) - } + applicationInfo.applicationVersionName("example-version-name") + XCTAssertEqual( + "application-id/example-id application-name/example-name application-version/example-version application-version-name/example-version-name", + applicationInfo.buildTag()) } func testApplicationInfoRejectsInvalidConfigurations() { let values = ["", " ", "/", ":", "🐦", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890._-"] @@ -240,9 +246,19 @@ final class LDConfigSpec: XCTestCase { for value in values { info.applicationIdentifier(value) + info.applicationName(value) info.applicationVersion(value) + info.applicationVersionName(value) XCTAssertEqual("", info.buildTag()) } } + + func testAutoEnvAttributesParameter() { + let config1 = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .disabled) + XCTAssertFalse(config1.autoEnvAttributes) + + let config2 = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, autoEnvAttributes: .enabled) + XCTAssertTrue(config2.autoEnvAttributes) + } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift deleted file mode 100644 index f2f59e54..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserSpec.swift +++ /dev/null @@ -1,109 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class LDUserSpec: QuickSpec { - - override func spec() { - initSpec() - } - - private func initSpec() { - initSubSpec() - initWithEnvironmentReporterSpec() - } - - private func initSubSpec() { - var user: LDUser! - describe("init") { - it("with all fields and custom overriding system values") { - user = LDUser(key: LDUser.StubConstants.key, - name: LDUser.StubConstants.name, - firstName: LDUser.StubConstants.firstName, - lastName: LDUser.StubConstants.lastName, - country: LDUser.StubConstants.country, - ipAddress: LDUser.StubConstants.ipAddress, - email: LDUser.StubConstants.email, - avatar: LDUser.StubConstants.avatar, - custom: LDUser.StubConstants.custom(includeSystemValues: true), - isAnonymous: LDUser.StubConstants.isAnonymous, - privateAttributes: LDUser.optionalAttributes) - expect(user.key) == LDUser.StubConstants.key - expect(user.name) == LDUser.StubConstants.name - expect(user.firstName) == LDUser.StubConstants.firstName - expect(user.lastName) == LDUser.StubConstants.lastName - expect(user.isAnonymous) == LDUser.StubConstants.isAnonymous - expect(user.country) == LDUser.StubConstants.country - expect(user.ipAddress) == LDUser.StubConstants.ipAddress - expect(user.email) == LDUser.StubConstants.email - expect(user.avatar) == LDUser.StubConstants.avatar - expect(user.custom == LDUser.StubConstants.custom(includeSystemValues: true)).to(beTrue()) - expect(user.privateAttributes) == LDUser.optionalAttributes - } - it("without setting anonymous") { - user = LDUser(key: "abc") - } - context("called without optional elements") { - var environmentReporter: EnvironmentReporter! - beforeEach { - user = LDUser() - environmentReporter = EnvironmentReporter() - } - it("creates a LDUser without optional elements") { - expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) - expect(user.isAnonymous) == true - - expect(user.name).to(beNil()) - expect(user.firstName).to(beNil()) - expect(user.lastName).to(beNil()) - expect(user.country).to(beNil()) - expect(user.ipAddress).to(beNil()) - expect(user.email).to(beNil()) - expect(user.avatar).to(beNil()) - expect(user.privateAttributes).to(beEmpty()) - } - } - context("called without a key multiple times") { - var users = [LDUser]() - beforeEach { - while users.count < 3 { - users.append(LDUser()) - } - } - it("creates each LDUser with the default key and isAnonymous set") { - let environmentReporter = EnvironmentReporter() - users.forEach { user in - expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) - expect(user.isAnonymous) == true - } - } - } - } - } - - private func initWithEnvironmentReporterSpec() { - describe("initWithEnvironmentReporter") { - var user: LDUser! - var environmentReporter: EnvironmentReportingMock! - beforeEach { - environmentReporter = EnvironmentReportingMock() - user = LDUser(environmentReporter: environmentReporter) - } - it("creates a user with system values matching the environment reporter") { - expect(user.key) == LDUser.defaultKey(environmentReporter: environmentReporter) - expect(user.isAnonymous) == true - - expect(user.name).to(beNil()) - expect(user.firstName).to(beNil()) - expect(user.lastName).to(beNil()) - expect(user.country).to(beNil()) - expect(user.ipAddress).to(beNil()) - expect(user.email).to(beNil()) - expect(user.avatar).to(beNil()) - - expect(user.privateAttributes).to(beEmpty()) - } - } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift deleted file mode 100644 index 8faef78d..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Models/User/LDUserToContextSpec.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import XCTest - -@testable import LaunchDarkly - -final class LDUserToContextSpec: XCTestCase { - func testSimpleUserIsConvertedToSimpleContext() throws { - let user = LDUser(key: "user-key") - let builder = LDContextBuilder(key: "user-key") - let context = try builder.build().get() - let encoder = JSONEncoder() - let encodedContext = try encoder.encode(context) - let encodedUserContext = try encoder.encode(user.toContext().get()) - - XCTAssertEqual(encodedContext, encodedUserContext) - } - - func testComplexUserConversion() throws { - var user = LDUser(key: "user-key") - user.name = "Example user" - user.firstName = "Example" - user.lastName = "user" - user.country = "United States" - user.ipAddress = "192.168.1.1" - user.email = "example@test.com" - user.avatar = "profile.jpg" - user.custom = ["/nested/attribute": "here is a nested attribute"] - user.isAnonymous = true - user.privateAttributes = [UserAttribute("/nested/attribute")] - - var builder = LDContextBuilder(key: "user-key") - builder.name("Example user") - builder.trySetValue("firstName", "Example".toLDValue()) - builder.trySetValue("lastName", "user".toLDValue()) - builder.trySetValue("country", "United States".toLDValue()) - builder.trySetValue("ipAddress", "192.168.1.1".toLDValue()) - builder.trySetValue("email", "example@test.com".toLDValue()) - builder.trySetValue("avatar", "profile.jpg".toLDValue()) - builder.trySetValue("/nested/attribute", "here is a nested attribute".toLDValue()) - builder.anonymous(true) - builder.addPrivateAttribute(Reference(literal: "/nested/attribute")) - - let context = try builder.build().get() - let userContext = try user.toContext().get() - - XCTAssertEqual(context, userContext) - } - - func testUserAttributeRedactionWorksAsExpected() throws { - var user = LDUser(key: "user-key") - user.custom = [ - "a": "should be removed", - "b": "should be retained", - "/nested/attribute/path": "should be removed" - ] - user.privateAttributes = [UserAttribute("a"), UserAttribute("/nested/attribute/path")] - let context = try user.toContext().get() - let output = try JSONEncoder().encode(context) - let outputJson = String(data: output, encoding: .utf8) - - XCTAssertTrue(outputJson!.contains("should be retained")) - XCTAssertFalse(outputJson!.contains("should be removed")) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index cf826c5a..5bec5622 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -18,6 +18,7 @@ final class DarklyServiceSpec: QuickSpec { struct TestContext { let context = LDContext.stub() var config: LDConfig! + var envReporterMock = EnvironmentReportingMock() var serviceMock: DarklyServiceMock! var serviceFactoryMock: ClientServiceMockFactory = ClientServiceMockFactory() var service: DarklyService! @@ -28,12 +29,12 @@ final class DarklyServiceSpec: QuickSpec { useReport: Bool = Constants.useGetMethod, diagnosticOptOut: Bool = false) { - config = LDConfig.stub(mobileKey: mobileKey, environmentReporter: EnvironmentReportingMock()) + config = LDConfig.stub(mobileKey: mobileKey, autoEnvAttributes: .disabled, isDebugBuild: true) config.useReport = useReport config.diagnosticOptOut = diagnosticOptOut serviceMock = DarklyServiceMock(config: config) - service = DarklyService(config: config, context: context, serviceFactory: serviceFactoryMock) - httpHeaders = HTTPHeaders(config: config, environmentReporter: config.environmentReporter) + service = DarklyService(config: config, context: context, envReporter: envReporterMock, serviceFactory: serviceFactoryMock) + httpHeaders = HTTPHeaders(config: config, environmentReporter: envReporterMock) } func runStubbedGet(statusCode: Int, featureFlags: [LDFlagKey: FeatureFlag]? = nil, flagResponseEtag: String? = nil) { diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift index 8a9e536a..4d93f26c 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/HTTPHeadersSpec.swift @@ -12,7 +12,7 @@ final class HTTPHeadersSpec: XCTestCase { XCTAssertEqual(headers[HTTPHeaders.HeaderKey.authorization], "\(HTTPHeaders.HeaderValue.apiKey) \(config.mobileKey)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.userAgent], - "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)") + "\(SystemCapabilities.systemName)/\(ReportingConsts.sdkVersion)") XCTAssertNil(headers[HTTPHeaders.HeaderKey.ifNoneMatch]) } @@ -23,7 +23,7 @@ final class HTTPHeadersSpec: XCTestCase { XCTAssertEqual(headers[HTTPHeaders.HeaderKey.authorization], "\(HTTPHeaders.HeaderValue.apiKey) \(config.mobileKey)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.userAgent], - "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)") + "\(SystemCapabilities.systemName)/\(ReportingConsts.sdkVersion)") } func testEventRequestDefaultHeaders() { @@ -33,7 +33,7 @@ final class HTTPHeadersSpec: XCTestCase { XCTAssertEqual(headers[HTTPHeaders.HeaderKey.authorization], "\(HTTPHeaders.HeaderValue.apiKey) \(config.mobileKey)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.userAgent], - "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)") + "\(SystemCapabilities.systemName)/\(ReportingConsts.sdkVersion)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.contentType], HTTPHeaders.HeaderValue.applicationJson) XCTAssertEqual(headers[HTTPHeaders.HeaderKey.accept], HTTPHeaders.HeaderValue.applicationJson) XCTAssertEqual(headers[HTTPHeaders.HeaderKey.eventSchema], HTTPHeaders.HeaderValue.eventSchema4) @@ -46,7 +46,7 @@ final class HTTPHeadersSpec: XCTestCase { XCTAssertEqual(headers[HTTPHeaders.HeaderKey.authorization], "\(HTTPHeaders.HeaderValue.apiKey) \(config.mobileKey)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.userAgent], - "\(EnvironmentReportingMock.Constants.systemName)/\(EnvironmentReportingMock.Constants.sdkVersion)") + "\(SystemCapabilities.systemName)/\(ReportingConsts.sdkVersion)") XCTAssertEqual(headers[HTTPHeaders.HeaderKey.contentType], HTTPHeaders.HeaderValue.applicationJson) XCTAssertEqual(headers[HTTPHeaders.HeaderKey.accept], HTTPHeaders.HeaderValue.applicationJson) XCTAssertNil(headers[HTTPHeaders.HeaderKey.eventSchema]) @@ -92,18 +92,26 @@ final class HTTPHeadersSpec: XCTestCase { } } - func testAdditionalHeadersInConfig() { - var config = LDConfig.stub - config.additionalHeaders = ["Proxy-Authorization": "token", - HTTPHeaders.HeaderKey.authorization: "feh"] - let httpHeaders = HTTPHeaders(config: config, environmentReporter: EnvironmentReportingMock()) + func testApplicationInfoTagIsGeneratedCorrectly() { + let config = LDConfig.stub + var appInfo = ApplicationInfo() + + appInfo.applicationIdentifier("example-id") + appInfo.applicationName("example-name") + appInfo.applicationVersion("example-version") + appInfo.applicationVersionName("example-version-name") + + let environmentReporter = EnvironmentReportingMock() + environmentReporter.applicationInfo = appInfo + + let httpHeaders = HTTPHeaders(config: config, environmentReporter: environmentReporter) + let allRequestTypes = [httpHeaders.flagRequestHeaders, httpHeaders.eventSourceHeaders, httpHeaders.eventRequestHeaders, httpHeaders.diagnosticRequestHeaders] allRequestTypes.forEach { headers in - XCTAssertEqual(headers["Proxy-Authorization"], "token", "Should include additional headers") - XCTAssertEqual(headers[HTTPHeaders.HeaderKey.authorization], "feh", "Should overwrite headers") + XCTAssertEqual(headers["X-LaunchDarkly-Tags"], Optional("application-id/example-id application-name/example-name application-version/example-version application-version-name/example-version-name")) } } } diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift index 5a186dc7..64471ad6 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/URLRequestSpec.swift @@ -8,7 +8,7 @@ final class URLRequestSpec: XCTestCase { var delegateArgs: (url: URL, headers: [String: String])? let url = URL(string: "https://dummy.urlRequest.com")! - var config = LDConfig(mobileKey: "testkey") + var config = LDConfig(mobileKey: "testkey", autoEnvAttributes: .disabled) config.connectionTimeout = 15 config.headerDelegate = { url, headers in delegateArgs = (url, headers) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift index 981a616d..cd64d5ae 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift @@ -15,7 +15,7 @@ final class DiagnosticReporterSpec: XCTestCase { init() { service = DarklyServiceMock() - subject = DiagnosticReporter(service: service) + subject = DiagnosticReporter(service: service, environmentReporting: EnvironmentReportingMock()) cachingMock.getDiagnosticIdReturnValue = diagnosticId service.diagnosticCache = cachingMock diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift deleted file mode 100644 index 500057c0..00000000 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporterSpec.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import Quick -import Nimble -@testable import LaunchDarkly - -final class EnvironmentReporterSpec: QuickSpec { - - override func spec() { - integrationHarnessSpec() - } - - private func integrationHarnessSpec() { - describe("shouldRunThrottled") { - it("should throttle online calls for release build") { - expect(EnvironmentReporter().shouldThrottleOnlineCalls) == false - } - } - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporterSpec.swift new file mode 100644 index 00000000..ad560d01 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/ApplicationInfoEnvironmentReporterSpec.swift @@ -0,0 +1,20 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class ApplicationInfoEnvironmentReporterSpec: XCTest { + + func testApplicationInfoReporterSpec() { + var applicationInfo = ApplicationInfo() + applicationInfo.applicationIdentifier("example-id") + applicationInfo.applicationName("example-name") + applicationInfo.applicationVersion("example-version") + applicationInfo.applicationVersionName("example-version-name") + + let chain = EnvironmentReporterChainBase() + chain.setNext(ApplicationInfoEnvironmentReporter(applicationInfo)) + + XCTAssertEqual(chain.applicationInfo, applicationInfo) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBaseSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBaseSpec.swift new file mode 100644 index 00000000..6cebd33a --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/EnvironmentReporterChainBaseSpec.swift @@ -0,0 +1,26 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class EnvironmentReporterChainBaseSpec: XCTest { + + func testEmptyChainBase() { + let chain = EnvironmentReporterChainBase() + let appInfo = chain.applicationInfo + + XCTAssertEqual(appInfo.applicationId, "UNKNOWN") + XCTAssertEqual(appInfo.applicationName, "UNKNOWN") + XCTAssertEqual(appInfo.applicationVersion, "UNKNOWN") + XCTAssertEqual(appInfo.applicationVersionName, "UNKNOWN") + + XCTAssertFalse(chain.isDebugBuild) + + XCTAssertEqual(chain.deviceModel, "UNKNOWN") + XCTAssertEqual(chain.systemVersion, "UNKNOWN") + XCTAssertNil(chain.vendorUUID) + + XCTAssertEqual(chain.manufacturer, "Apple") + XCTAssertEqual(chain.osFamily, "Apple") + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporterSpec.swift new file mode 100644 index 00000000..47dab7a4 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/IOSEnvironmentReporterSpec.swift @@ -0,0 +1,19 @@ +#if os(iOS) +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class IOSEnvironmentReporterSpec: XCTest { + + func testIosEnvironmentReporter() { + let chain = EnvironmentReporterChainBase() + chain.setNext(IOSEnvironmentReporter()) + + XCTAssertNotEqual(chain.deviceModel, "UNKNOWN") + XCTAssertNotEqual(chain.systemVersion, "UNKNOWN") + + XCTAssertNil(chain.vendorUUID) + } +} +#endif diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporterSpec.swift new file mode 100644 index 00000000..c671ae34 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/SDKEnvironmentReporterSpec.swift @@ -0,0 +1,16 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class SDKEnvironmentReporterSpec: XCTest { + + func testSdkEnvironmentReporter() { + let reporter = SDKEnvironmentReporter() + + XCTAssertNotEqual(reporter.applicationInfo.applicationId, ReportingConsts.sdkName) + XCTAssertNotEqual(reporter.applicationInfo.applicationName, ReportingConsts.sdkName) + XCTAssertNotEqual(reporter.applicationInfo.applicationVersion, ReportingConsts.sdkVersion) + XCTAssertNotEqual(reporter.applicationInfo.applicationVersionName, ReportingConsts.sdkVersion) + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporterSpec.swift new file mode 100644 index 00000000..5522a292 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/EnvironmentReporting/WatchOSEnvironmentReporterSpec.swift @@ -0,0 +1,37 @@ +#if os(watchOS) +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class WatchOSEnvironmentReporterSpec: XCTest { + func testDefaultReporterBehavior() { + let chain = EnvironmentReporterChainBase() + chain.setNext(WatchOSEnvironmentReporter()) + + XCTAssertNotEqual(chain.deviceModel, "UNKNOWN") + XCTAssertNotEqual(chain.systemVersion, "UNKNOWN") + XCTAssertNotEqual(chain.systemName, "UNKNOWN") + + XCTAssertEqual(chain.operatingSystem, .watchOS) + + XCTAssertNil(chain.vendorUUID) + } + + func testBuilderDoesNotIncludeWatchInfoWithoutExplicitOptIn() { + let builder = EnvironmentReporterBuilder() + let reporting = builder.build() + + XCTAssertEqual(reporting.operatingSystem, .watchOS) + } + + func testEnsureBuilderUsesCorrectReporter() { + let builder = EnvironmentReporterBuilder() + builder.enableCollectionFromPlatform() + + let reporting = builder.build() + + XCTAssertEqual(reporting.operatingSystem, .watchOS) + } +} +#endif diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift index daafa73f..62c79c46 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/ThrottlerSpec.swift @@ -12,10 +12,8 @@ final class ThrottlerSpec: QuickSpec { let dispatchQueue = DispatchQueue(label: "ThrottlerSpecQueue") func testThrottler(throttlingDisabled: Bool = false) -> Throttler { - let environmentReporterMock = EnvironmentReportingMock() - environmentReporterMock.shouldThrottleOnlineCalls = !throttlingDisabled return Throttler(maxDelay: Constants.maxDelay, - environmentReporter: environmentReporterMock, + isDebugBuild: throttlingDisabled, dispatcher: { self.dispatchQueue.sync(execute: $0) }) } @@ -122,9 +120,7 @@ final class ThrottlerSpec: QuickSpec { func maxDelaySpec() { it("limits delay to maxDelay") { - let envReporter = EnvironmentReportingMock() - envReporter.shouldThrottleOnlineCalls = true - let throttler = Throttler(maxDelay: 1.0, environmentReporter: envReporter) + let throttler = Throttler(maxDelay: 1.0, isDebugBuild: false) (0..<10).forEach { _ in throttler.runThrottled { } } let callDate = Date() var runDate: Date? diff --git a/LaunchDarkly/LaunchDarklyTests/UtilSpec.swift b/LaunchDarkly/LaunchDarklyTests/UtilSpec.swift new file mode 100644 index 00000000..965fe6ef --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/UtilSpec.swift @@ -0,0 +1,14 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class UtilSpec: XCTestCase { + + func testSha256base64() throws { + let input = "hashThis!" + let expectedOutput = "sfXg3HewbCAVNQLJzPZhnFKntWYvN0nAYyUWFGy24dQ=" + let output = Util.sha256base64(input) + XCTAssertEqual(output, expectedOutput) + } +} diff --git a/README.md b/README.md index ffbbd90b..fb066e2c 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ To include LaunchDarkly in a Swift package, simply add it to the dependencies se ```swift dependencies: [ - .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "8.2.0")) + .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "9.0.0")) ] ``` @@ -60,7 +60,7 @@ To use the [CocoaPods](https://cocoapods.org) dependency manager to integrate La ```ruby use_frameworks! target 'YourTargetName' do - pod 'LaunchDarkly', '~> 8.2' + pod 'LaunchDarkly', '~> 9.0' end ``` @@ -71,7 +71,7 @@ To use the [Carthage](https://github.com/Carthage/Carthage) dependency manager t To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client-sdk" ~> 8.2 +github "launchdarkly/ios-client-sdk" ~> 9.0 ``` ### Manual installation