Skip to content

Commit

Permalink
refactor!: restructure automatic context decorator (#185)
Browse files Browse the repository at this point in the history
RELEASE-AS: 1.3.0
  • Loading branch information
nicklasl authored Jan 30, 2025
1 parent 848d9ae commit 3588ae8
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 118 deletions.
8 changes: 8 additions & 0 deletions ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ struct ConfidenceDemoApp: App {
context["user_id"] = ConfidenceValue.init(string: user)
}

context = ConfidenceDeviceInfoContextDecorator(
withDeviceInfo: true,
withAppInfo: true,
withOsInfo: true,
withLocale: true
).decorated(context: [:]);

confidence = Confidence
.Builder(clientSecret: secret, loggerLevel: .TRACE)
.withContext(initialContext: context)
.build()

do {
// NOTE: here we are activating all the flag values from storage, regardless of how `context` looks now
try confidence.activate()
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,35 @@ _Note: Changing the context could cause a change in the flag values._

_Note: When a context change is performed and the SDK is fetching the new values for it, the old values are still available for the Application to consume but marked with evaluation reason `STALE`._

The SDK comes with a built in helper class to decorate the Context with some static data from the device.
The class is called `ConfidenceDeviceInfoContextDecorator` and used as follows:

```swift
let context = ConfidenceDeviceInfoContextDecorator(
withDeviceInfo: true,
withAppInfo: true,
withOsInfo: true,
withLocale: true
).decorated(context: [:]); // it's also possible to pass an already prepared context here.
```
The values appended to the Context come primarily from the Bundle and the UIDevice APIs.

- `withAppInfo` includes:
- version: the value from `CFBundleShortVersionString`.
- build: the value from `CFBundleVersion`.
- namespace: the `bundleIdentifier`.
- `withDeviceInfo` includes:
- manufacturer: hard coded to Apple.
- model: the device model identifier, for example "iPhone15,4" or "iPad14,11".
- type: the value from `UIDevice.current.model`.
- `withOsInfo` includes:
- name: the system name.
- version: the system version.
- `withLocale` includes:
- locale: the selected Locale.
- preferred_languages: the user set preferred languages as set in the Locale.


When integrating the SDK in your Application, it's important to understand the implications of changing the context at runtime:
- You might want to keep the flag values unchanged within a certain session
- You might want to show a loading UI while re-fetching all flag values
Expand Down
105 changes: 0 additions & 105 deletions Sources/Confidence/ConfidenceAppLifecycleProducer.swift

This file was deleted.

114 changes: 114 additions & 0 deletions Sources/Confidence/ConfidenceDeviceInfoContextDecorator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst)
import Foundation
import UIKit
import Combine

/**
Helper class to produce device information context for the Confidence context.

The values appended to the Context come primarily from the Bundle or UiDevice API

AppInfo contains:
- version: the version name of the app.
- build: the version code of the app.
- namespace: the package name of the app.

DeviceInfo contains:
- manufacturer: the manufacturer of the device.
- brand: the brand of the device.
- model: the model of the device.
- type: the type of the device.

OsInfo contains:
- name: the name of the OS.
- version: the version of the OS.

Locale contains:
- locale: the locale of the device.
- preferred_languages: the preferred languages of the device.

The context is only updated when the class is initialized and then static.
*/
public class ConfidenceDeviceInfoContextDecorator {
private let staticContext: ConfidenceValue

public init(
withDeviceInfo: Bool = false,
withAppInfo: Bool = false,
withOsInfo: Bool = false,
withLocale: Bool = false
) {
var context: [String: ConfidenceValue] = [:]

if withDeviceInfo {
let device = UIDevice.current

context["device"] = .init(structure: [
"manufacturer": .init(string: "Apple"),
"model": .init(string: Self.getDeviceModelIdentifier()),
"type": .init(string: device.model)
])
}

if withAppInfo {
let currentVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
let currentBuild: String = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
let bundleId = Bundle.main.bundleIdentifier ?? ""

context["app"] = .init(structure: [
"version": .init(string: currentVersion),
"build": .init(string: currentBuild),
"namespace": .init(string: bundleId)
])
}

if withOsInfo {
let device = UIDevice.current

context["os"] = .init(structure: [
"name": .init(string: device.systemName),
"version": .init(string: device.systemVersion)
])
}

if withLocale {
let locale = Locale.current
let preferredLanguages = Locale.preferredLanguages

// Top level fields
context["locale"] = .init(string: locale.identifier) // Locale identifier (e.g., "en_US")
context["preferred_languages"] = .init(list: preferredLanguages.map { lang in
.init(string: lang)
})
}

self.staticContext = .init(structure: context)
}

/**
Returns a context where values are decorated (appended) according to how the ConfidenceDeviceInfoContextDecorator was setup.
The context values in the parameter context have precedence over the fields appended by this class.
*/
public func decorated(context contextToDecorate: [String: ConfidenceValue]) -> [String: ConfidenceValue] {
var result = self.staticContext.asStructure() ?? [:]
contextToDecorate.forEach { (key: String, value: ConfidenceValue) in
result[key] = value
}
return result
}


private static func getDeviceModelIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children
.compactMap { element in element.value as? Int8 }
.filter { $0 != 0 }
.map {
Character(UnicodeScalar(UInt8($0)))
}
return String(identifier)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import XCTest
@testable import Confidence

final class DeviceInfoContextDecoratorTests: XCTestCase {
func testEmptyConstructMakesNoOp() {
let result = ConfidenceDeviceInfoContextDecorator().decorated(context: [:])
XCTAssertEqual(result.count, 0)
}

func testAddDeviceInfo() {
let result = ConfidenceDeviceInfoContextDecorator(withDeviceInfo: true).decorated(context: [:])
XCTAssertEqual(result.count, 1)
XCTAssertNotNil(result["device"])
XCTAssertNotNil(result["device"]?.asStructure()?["model"])
XCTAssertNotNil(result["device"]?.asStructure()?["type"])
XCTAssertNotNil(result["device"]?.asStructure()?["manufacturer"])
}

func testAddLocale() {
let result = ConfidenceDeviceInfoContextDecorator(withLocale: true).decorated(context: [:])
XCTAssertEqual(result.count, 2)
XCTAssertNotNil(result["locale"])
XCTAssertNotNil(result["preferred_languages"])
}

func testAppendsData() {
let result = ConfidenceDeviceInfoContextDecorator(
withDeviceInfo: true
).decorated(context: ["my_key": .init(double: 42.0)])
XCTAssertEqual(result.count, 2)
XCTAssertEqual(result["my_key"]?.asDouble(), 42.0)
XCTAssertNotNil(result["device"])
}
}
18 changes: 5 additions & 13 deletions api/Confidence_public_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,15 @@
]
},
{
"className": "ConfidenceAppLifecycleProducer",
"className": "ConfidenceDeviceInfoContextDecorator",
"apiFunctions": [
{
"name": "init()",
"declaration": "public init()"
},
{
"name": "deinit",
"declaration": "deinit"
},
{
"name": "produceEvents()",
"declaration": "public func produceEvents() -> AnyPublisher<Event, Never>"
"name": "init(withDeviceInfo:withAppInfo:withOsInfo:withLocale:)",
"declaration": "public init(\n withDeviceInfo: Bool = false,\n withAppInfo: Bool = false,\n withOsInfo: Bool = false,\n withLocale: Bool = false\n)"
},
{
"name": "produceContexts()",
"declaration": "public func produceContexts() -> AnyPublisher<ConfidenceStruct, Never>"
"name": "decorated(context:)",
"declaration": "public func decorated(context contextToDecorate: [String: ConfidenceValue]) -> [String: ConfidenceValue]"
}
]
},
Expand Down

0 comments on commit 3588ae8

Please sign in to comment.