Skip to content

Commit

Permalink
Merge pull request #179 from launchdarkly/gw/add-user-attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
gwhelanLD authored Mar 11, 2022
2 parents a4231b0 + 096ad3e commit a98286f
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 93 deletions.
10 changes: 10 additions & 0 deletions LaunchDarkly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; };
29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; };
29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.swift */; };
29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A4C47427DA6266005B8D34 /* UserAttribute.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 */; };
Expand Down Expand Up @@ -346,6 +350,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
29A4C47427DA6266005B8D34 /* UserAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAttribute.swift; sourceTree = "<group>"; };
830BF932202D188E006DF9B1 /* HTTPURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLRequest.swift; sourceTree = "<group>"; };
830DB3AB22380A3E00D65D25 /* HTTPHeadersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersSpec.swift; sourceTree = "<group>"; };
830DB3AD2239B54900D65D25 /* URLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponse.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -671,6 +676,7 @@
83EBCB9D20D9A0A1003A7142 /* FeatureFlag */,
8354EFDD1F26380700C05156 /* LDConfig.swift */,
83A2D6231F51CD7A00EA3BD4 /* LDUser.swift */,
29A4C47427DA6266005B8D34 /* UserAttribute.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -1216,6 +1222,7 @@
8311885A2113AE1500D77CB5 /* Log.swift in Sources */,
8311884B2113ADDA00D77CB5 /* LDChangedFlag.swift in Sources */,
8311885E2113AE2900D77CB5 /* HTTPURLResponse.swift in Sources */,
29A4C47827DA6266005B8D34 /* UserAttribute.swift in Sources */,
8347BB0F21F147E100E56BCD /* LDTimer.swift in Sources */,
B4C9D43B2489E20A004A9B03 /* DiagnosticReporter.swift in Sources */,
C443A40523145FBF00145710 /* ConnectionInformation.swift in Sources */,
Expand Down Expand Up @@ -1275,6 +1282,7 @@
831EF35A20655E730001C643 /* HTTPHeaders.swift in Sources */,
831EF35B20655E730001C643 /* DarklyService.swift in Sources */,
831EF35C20655E730001C643 /* HTTPURLResponse.swift in Sources */,
29A4C47727DA6266005B8D34 /* UserAttribute.swift in Sources */,
8354AC6B22418C0600CDE602 /* CacheableUserEnvironmentFlags.swift in Sources */,
C443A40723145FEE00145710 /* ConnectionInformationStore.swift in Sources */,
831EF35D20655E730001C643 /* HTTPURLRequest.swift in Sources */,
Expand Down Expand Up @@ -1339,6 +1347,7 @@
831D2AAF2061AAA000B4AC3C /* Thread.swift in Sources */,
83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */,
8354EFE01F26380700C05156 /* LDClient.swift in Sources */,
29A4C47527DA6266005B8D34 /* UserAttribute.swift in Sources */,
831425B1206B030100F2EF36 /* EnvironmentReporter.swift in Sources */,
C408884723033B3600420721 /* ConnectionInformationStore.swift in Sources */,
83B6C4B61F4DE7630055351C /* LDCommon.swift in Sources */,
Expand Down Expand Up @@ -1451,6 +1460,7 @@
83D9EC8E2062DEAB004D7FA6 /* HTTPURLResponse.swift in Sources */,
83D9EC8F2062DEAB004D7FA6 /* HTTPURLRequest.swift in Sources */,
83D9EC902062DEAB004D7FA6 /* Dictionary.swift in Sources */,
29A4C47627DA6266005B8D34 /* UserAttribute.swift in Sources */,
831425B2206B030100F2EF36 /* EnvironmentReporter.swift in Sources */,
83D9EC922062DEAB004D7FA6 /* Data.swift in Sources */,
8347BB0D21F147E100E56BCD /* LDTimer.swift in Sources */,
Expand Down
7 changes: 3 additions & 4 deletions LaunchDarkly/LaunchDarkly/Models/LDConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public struct LDConfig {
/// The default setting for private user attributes. (false)
static let allUserAttributesPrivate = false
/// The default private user attribute list (nil)
static let privateUserAttributes: [String]? = nil
static let privateUserAttributes: [UserAttribute] = []

/// The default HTTP request method for stream connections and feature flag requests. When true, these requests will use the non-standard verb `REPORT`. When false, these requests will use the standard verb `GET`. (false)
static let useReport = false
Expand Down Expand Up @@ -213,7 +213,7 @@ public struct LDConfig {

See Also: `allUserAttributesPrivate`, `LDUser.privatizableAttributes`, and `LDUser.privateAttributes`.
*/
public var privateUserAttributes: [String]? = Defaults.privateUserAttributes
public var privateUserAttributes: [UserAttribute] = Defaults.privateUserAttributes

/**
Directs the SDK to use REPORT for HTTP requests for feature flag data. (Default: `false`)
Expand Down Expand Up @@ -368,8 +368,7 @@ extension LDConfig: Equatable {
&& lhs.enableBackgroundUpdates == rhs.enableBackgroundUpdates
&& lhs.startOnline == rhs.startOnline
&& lhs.allUserAttributesPrivate == rhs.allUserAttributesPrivate
&& (lhs.privateUserAttributes == nil && rhs.privateUserAttributes == nil
|| (lhs.privateUserAttributes != nil && rhs.privateUserAttributes != nil && lhs.privateUserAttributes! == rhs.privateUserAttributes!))
&& Set(lhs.privateUserAttributes) == Set(rhs.privateUserAttributes)
&& lhs.useReport == rhs.useReport
&& lhs.inlineUserInEvents == rhs.inlineUserInEvents
&& lhs.isDebugMode == rhs.isDebugMode
Expand Down
70 changes: 19 additions & 51 deletions LaunchDarkly/LaunchDarkly/Models/LDUser.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

typealias UserKey = String // use for identifying semantics for strings, particularly in dictionaries

/**
LDUser allows clients to collect information about users in order to refine the feature flag values sent to the SDK. For example, the client app may launch with the SDK defined anonymous user. As the user works with the client app, information may be collected as needed and sent to LaunchDarkly. The client app controls the information collected, which LaunchDarkly does not use except as the client directs to refine feature flags. Client apps should follow [Apple's Privacy Policy](apple.com/legal/privacy) when collecting user information.
The SDK caches last known feature flags for use on app startup to provide continuity with the last app run. Provided the LDClient is online and can establish a connection with LaunchDarkly servers, cached information will only be used a very short time. Once the latest feature flags arrive at the SDK, the SDK no longer uses cached feature flags. The SDK retains feature flags on the last 5 client defined users. The SDK will retain feature flags until they are overwritten by a different user's feature flags, or until the user removes the app from the device.
Expand All @@ -14,21 +15,7 @@ public struct LDUser {
case key, name, firstName, lastName, country, ipAddress = "ip", email, avatar, custom, isAnonymous = "anonymous", device, operatingSystem = "os", config, privateAttributes = "privateAttrs", secondary
}

/**
LDUser attributes that can be marked private.
The SDK will not include private attribute values in analytics events, but private attribute names will be sent.
See Also: `LDConfig.allUserAttributesPrivate`, `LDConfig.privateUserAttributes`, and `privateAttributes`.
*/
public static var privatizableAttributes: [String] { optionalAttributes }

static let optionalAttributes = [CodingKeys.name.rawValue, CodingKeys.firstName.rawValue,
CodingKeys.lastName.rawValue, CodingKeys.country.rawValue,
CodingKeys.ipAddress.rawValue, CodingKeys.email.rawValue,
CodingKeys.avatar.rawValue, CodingKeys.secondary.rawValue]

static var sdkSetAttributes: [String] {
[CodingKeys.device.rawValue, CodingKeys.operatingSystem.rawValue]
}
static let optionalAttributes = UserAttribute.BuiltIn.allBuiltIns.filter { $0.name != "key" && $0.name != "anonymous"}

static let storedIdKey: String = "ldDeviceIdentifier"

Expand Down Expand Up @@ -61,7 +48,7 @@ public struct LDUser {
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 attributes found in `privatizableAttributes` and top level `custom` dictionary keys here. (Default: nil)
See Also: `LDConfig.allUserAttributesPrivate` and `LDConfig.privateUserAttributes`.
*/
public var privateAttributes: [String]?
public var privateAttributes: [UserAttribute]

/// An NSObject wrapper for the Swift LDUser struct. Intended for use in mixed apps when Swift code needs to pass a user into an Objective-C method.
public var objcLdUser: ObjcLDUser { ObjcLDUser(self) }
Expand Down Expand Up @@ -93,7 +80,7 @@ public struct LDUser {
avatar: String? = nil,
custom: [String: Any]? = nil,
isAnonymous: Bool? = nil,
privateAttributes: [String]? = nil,
privateAttributes: [UserAttribute]? = nil,
secondary: String? = nil) {
let environmentReporter = EnvironmentReporter()
let selectedKey = key ?? LDUser.defaultKey(environmentReporter: environmentReporter)
Expand All @@ -110,12 +97,12 @@ public struct LDUser {
self.custom = custom ?? [:]
self.custom.merge([CodingKeys.device.rawValue: environmentReporter.deviceModel,
CodingKeys.operatingSystem.rawValue: environmentReporter.systemVersion]) { lhs, _ in lhs }
self.privateAttributes = privateAttributes
self.privateAttributes = privateAttributes ?? []
Log.debug(typeName(and: #function) + "user: \(self)")
}

/**
Initializer that takes a [String: Any] and creates a LDUser from the contents. Uses any keys present to define corresponding attribute values. Initializes attributes not present in the dictionary to their default value. Attempts to set `device` and `operatingSystem` from corresponding values embedded in `custom`. DEPRECATED: Attempts to set feature flags from values set in `config`.
Initializer that takes a [String: Any] and creates a LDUser from the contents. Uses any keys present to define corresponding attribute values. Initializes attributes not present in the dictionary to their default value. Attempts to set `device` and `operatingSystem` from corresponding values embedded in `custom`.
- parameter userDictionary: Dictionary with LDUser attribute keys and values.
*/
public init(userDictionary: [String: Any]) {
Expand All @@ -130,7 +117,11 @@ public struct LDUser {
ipAddress = userDictionary[CodingKeys.ipAddress.rawValue] as? String
email = userDictionary[CodingKeys.email.rawValue] as? String
avatar = userDictionary[CodingKeys.avatar.rawValue] as? String
privateAttributes = userDictionary[CodingKeys.privateAttributes.rawValue] as? [String]
if let privateAttrs = (userDictionary[CodingKeys.privateAttributes.rawValue] as? [String]) {
privateAttributes = privateAttrs.map { UserAttribute.forName($0) }
} else {
privateAttributes = []
}
custom = userDictionary[CodingKeys.custom.rawValue] as? [String: Any] ?? [:]

Log.debug(typeName(and: #function) + "user: \(self)")
Expand All @@ -146,37 +137,19 @@ public struct LDUser {
isAnonymous: true)
}

// swiftlint:disable:next cyclomatic_complexity
private func value(for attribute: String) -> Any? {
switch attribute {
case CodingKeys.key.rawValue: return key
case CodingKeys.secondary.rawValue: return secondary
case CodingKeys.isAnonymous.rawValue: return isAnonymous
case CodingKeys.name.rawValue: return name
case CodingKeys.firstName.rawValue: return firstName
case CodingKeys.lastName.rawValue: return lastName
case CodingKeys.country.rawValue: return country
case CodingKeys.ipAddress.rawValue: return ipAddress
case CodingKeys.email.rawValue: return email
case CodingKeys.avatar.rawValue: return avatar
case CodingKeys.custom.rawValue: return custom
case CodingKeys.device.rawValue: return custom[CodingKeys.device.rawValue]
case CodingKeys.operatingSystem.rawValue: return custom[CodingKeys.operatingSystem.rawValue]
case CodingKeys.privateAttributes.rawValue: return privateAttributes
default: return nil
private func value(for attribute: UserAttribute) -> Any? {
if let builtInGetter = attribute.builtInGetter {
return builtInGetter(self)
}
}
/// Returns the custom dictionary without the SDK set device and operatingSystem attributes
var customWithoutSdkSetAttributes: [String: Any] {
custom.filter { key, _ in !LDUser.sdkSetAttributes.contains(key) }
return custom[attribute.name]
}

/// Dictionary with LDUser attribute keys and values, with options to include feature flags and private attributes. LDConfig object used to help resolving what attributes should be private.
/// - parameter includePrivateAttributes: Controls whether the resulting dictionary includes private attributes
/// - parameter config: Provides supporting information for defining private attributes
func dictionaryValue(includePrivateAttributes includePrivate: Bool, config: LDConfig) -> [String: Any] {
let allPrivate = !includePrivate && config.allUserAttributesPrivate
let privateAttributeNames = includePrivate ? [] : (privateAttributes ?? []) + (config.privateUserAttributes ?? [])
let privateAttributeNames = includePrivate ? [] : (privateAttributes + config.privateUserAttributes).map { $0.name }

var dictionary: [String: Any] = [:]
var redactedAttributes: [String] = []
Expand All @@ -186,10 +159,10 @@ public struct LDUser {

LDUser.optionalAttributes.forEach { attribute in
if let value = self.value(for: attribute) {
if allPrivate || privateAttributeNames.contains(attribute) {
redactedAttributes.append(attribute)
if allPrivate || privateAttributeNames.contains(attribute.name) {
redactedAttributes.append(attribute.name)
} else {
dictionary[attribute] = value
dictionary[attribute.name] = value
}
}
}
Expand Down Expand Up @@ -255,11 +228,6 @@ extension LDUser: TypeIdentifying { }

#if DEBUG
extension LDUser {
/// Testing method to get the user attribute value from a LDUser struct
func value(forAttribute attribute: String) -> Any? {
value(for: attribute)
}

// Compares all user properties. Excludes the composed FlagStore, which contains the users feature flags
func isEqual(to otherUser: LDUser) -> Bool {
key == otherUser.key
Expand Down
50 changes: 50 additions & 0 deletions LaunchDarkly/LaunchDarkly/Models/UserAttribute.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation

public class UserAttribute: Equatable, Hashable {

public struct BuiltIn {
public static let key = UserAttribute("key") { $0.key }
public static let secondaryKey = UserAttribute("secondary") { $0.secondary }
// swiftlint:disable:next identifier_name
public static let ip = UserAttribute("ip") { $0.ipAddress }
public static let email = UserAttribute("email") { $0.email }
public static let name = UserAttribute("name") { $0.name }
public static let avatar = UserAttribute("avatar") { $0.avatar }
public static let firstName = UserAttribute("firstName") { $0.firstName }
public static let lastName = UserAttribute("lastName") { $0.lastName }
public static let country = UserAttribute("country") { $0.country }
public static let anonymous = UserAttribute("anonymous") { $0.isAnonymous }

static let allBuiltIns = [key, secondaryKey, ip, email, name, avatar, firstName, lastName, country, anonymous]
}

static var builtInMap = { return BuiltIn.allBuiltIns.reduce(into: [:]) { $0[$1.name] = $1 } }()

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
}

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)
}
}
6 changes: 3 additions & 3 deletions LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ public final class ObjcLDConfig: NSObject {

See Also: `allUserAttributesPrivate`, `LDUser.privatizableAttributes` (`ObjcLDUser.privatizableAttributes`), and `LDUser.privateAttributes` (`ObjcLDUser.privateAttributes`).
*/
@objc public var privateUserAttributes: [String]? {
get { config.privateUserAttributes }
set { config.privateUserAttributes = newValue }
@objc public var privateUserAttributes: [String] {
get { config.privateUserAttributes.map { $0.name } }
set { config.privateUserAttributes = newValue.map { UserAttribute.forName($0) } }
}

/**
Expand Down
16 changes: 3 additions & 13 deletions LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@ import Foundation
public final class ObjcLDUser: NSObject {
var user: LDUser

/**
LDUser attributes that can be marked private.

The SDK will not include private attribute values in analytics events, but private attribute names will be sent.

See Also: `ObjcLDConfig.allUserAttributesPrivate`, `ObjcLDConfig.privateUserAttributes`, and `privateAttributes`.
*/
@objc public class var privatizableAttributes: [String] {
LDUser.privatizableAttributes
}
/// LDUser secondary attribute used to make `secondary` private
@objc public class var attributeSecondary: String {
LDUser.CodingKeys.secondary.rawValue
Expand Down Expand Up @@ -123,9 +113,9 @@ public final class ObjcLDUser: NSObject {
See Also: `ObjcLDConfig.allUserAttributesPrivate` and `ObjcLDConfig.privateUserAttributes`.

*/
@objc public var privateAttributes: [String]? {
get { user.privateAttributes }
set { user.privateAttributes = newValue }
@objc public var privateAttributes: [String] {
get { user.privateAttributes.map { $0.name } }
set { user.privateAttributes = newValue.map { UserAttribute.forName($0) } }
}

/**
Expand Down
Loading

0 comments on commit a98286f

Please sign in to comment.