Giter Club home page Giter Club logo

growthbook-swift's Introduction

GrowthBook - SDK

  • Lightweight and fast
  • Supports native Apple platforms
    • macOS version 10.15 & Above
    • iOS version 12.0 & Above
    • Apple tvOS version 12.0 & Above
    • Apple watchOS version 5.0 & Above
    • Apple visionOS version 1.0 & Above
  • Adjust variation weights and targeting without deploying new code
  • Latest spec version: 0.7.0 View Changelog

Installation

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate GrowthBook into your Xcode project using CocoaPods, specify it in your Podfile:

  • If you're using version 1.0.46 or above, it could be necessary to set "User Script Sandboxing" to "No" in your build settings.

  • Add below line in your podfile, if not there

source 'https://github.com/CocoaPods/Specs.git'
  • Add below in podfile - in respective target block
pod 'GrowthBook-IOS'
  • Execute below command in terminal
pod install
Swift Package Manager - SPM

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swiftcompiler.

Once you have your Swift package set up, adding GrowthBook as a dependency is as easy as adding it to the dependencies value of your Package.swift.

dependencies: [
    .package(url: "https://github.com/growthbook/growthbook-swift.git")
]

Integration

Integration is super easy:

  1. Create a Growth Book with a few methods: with the API key and host URL, only host URL, only JSON
  2. At the start of your app, do SDK Initialization as per below

Now you can start/stop tests, adjust coverage and variation weights, and apply a winning variation to 100% of traffic, all within the Growth Book App without deploying code changes to your site.

var sdkInstance: GrowthBookSDK = GrowthBookBuilder(apiHost: <GrowthBook/API_HOST>,
    clientKey: <GrowthBook/Client_KEY>,
    attributes: <[String: Any]>,
    trackingCallback: { experiment, experimentResult in 

    }, backroundSync: Bool?).initializer()

You must also provide the encryption key if you intend to use data encryption.

var sdkInstance: GrowthBookSDK = GrowthBookBuilder(apiHost: <GrowthBook/API_HOST>,
    clientKey: <GrowthBook/Client_KEY>,
    attributes: <[String: Any]>,
    trackingCallback: { experiment, experimentResult in 

    }).initializer()
var sdkInstance: GrowthBookSDK = GrowthBookBuilder(apiHost: <GrowthBook/API_HOST>,
    clientKey: <GrowthBook/Client_KEY>,
    attributes: <[String: Any]>,
    trackingCallback: { experiment, experimentResult in 

    }).initializer()

There are additional properties which can be setup at the time of initialization

var sdkInstance: GrowthBookSDK = GrowthBookBuilder(apiHost: <GrowthBook/API_HOST>,
    clientKey: <GrowthBook/Client_KEY>,
    attributes: <[String: Any]>,
    trackingCallback: { experiment, experimentResult in 

    })
    .setRefreshHandler { isRefreshed in
        
    } // Get Callbacks when SDK refreshed its cache
    .setNetworkDispatcher(networkDispatcher: <Network Dispatcher>) // Pass Network client to be used for API Calls
    .setEnabled(isEnabled: true) // Enable / Disable experiments
    .setQAMode(isEnabled: true) // Enable / Disable QA Mode
    .setForcedVariations(forcedVariations: <[String: Int]>) // Pass Forced Variations
    .setLogLevel(<LoggerLevel>) // Set log level for SDK Logger, by default log level is set to `info`
    .setCacheDirectory(<CacheDirectory>) // This function configures the cache directory used by the application to the designated directory type. Subsequent cache-related operations will target this directory.
    .setStickyBucketService(stickyBucketService: StickyBucketService()) // This function creates a sticky bucket service.
    .initializer()

Usage

  • Initialization returns SDK instance - GrowthBookSDK

    Use sdkInstance to consume below features -
  • The feature method takes a single string argument, which is the unique identifier for the feature and returns a FeatureResult object.

func evalFeature(id: String) -> FeatureResult
  • The run method takes an Experiment object and returns an experiment result
func run(experiment: Experiment) -> ExperimentResult
  • Manually Refresh Cache
func refreshCache()
  • Get Context
func getGBContext() -> Context
  • Get Features
func getFeatures() -> Features
  • Get the value of the feature with a fallback
func getFeatureValue(feature id: String, defaultValue: JSON) -> JSON
  • The isOn method takes a single string argument, which is the unique identifier for the feature and returns the feature state on/off
func isOn(feature id: String) -> Bool
  • The setEncryptedFeatures method takes an encrypted string with an encryption key and then decrypts it with the default method of decrypting or with a method of decrypting from the user.
func setEncryptedFeatures(encryptedString: String, encryptionKey: String, subtle: CryptoProtocol? = nil)

Models

/// Defines the GrowthBook context.
class Context {
    /// api host
    public let apiHost: String?
    /// unique client key
    public let clientKey: String?
    /// Encryption key for encrypted features.
    let encryptionKey: String?
    /// Switch to globally disable all experiments. Default true.
    let isEnabled: Bool
    /// Map of user attributes that are used to assign variations
    var attributes: JSON
    /// Force specific experiments to always assign a specific variation (used for QA)
    let forcedVariations: JSON?
    /// If true, random assignment is disabled and only explicitly forced variations are used.
    let isQaMode: Bool
    /// A function that takes experiment and result as arguments.
    let trackingCallback: (Experiment, ExperimentResult) -> Void

    // Keys are unique identifiers for the features and the values are Feature objects.
    // Feature definitions - To be pulled from API / Cache
    var features: Features
}
/// A Feature object consists of possible values plus rules for how to assign values to users.
class Feature {
    /// The default value (should use null if not specified)
    let defaultValue: JSON?
    /// Array of Rule objects that determine when and how the defaultValue gets overridden
    let rules: [FeatureRule]?
}

/// Rule object consists of various definitions to apply to calculate feature value
struct FeatureRule {
    /// Optional targeting condition
    let condition: JSON?
    /// What percent of users should be included in the experiment (between 0 and 1, inclusive)
    let coverage: Float?
    /// Immediately force a specific value (ignore every other option besides condition and coverage)
    let force: JSON?
    /// Run an experiment (A/B test) and randomly choose between these variations
    let variations: [JSON]?
    /// The globally unique tracking key for the experiment (default to the feature key)
    let key: String?
    /// How to weight traffic between variations. Must add to 1.
    let weights: [Float]?
    /// A tuple that contains the namespace identifier, plus a range of coverage for the experiment.
    let namespace: [JSON]?
    /// What user attribute should be used to assign variations (defaults to id)
    let hashAttribute: String?
    /// Hash version of hash function
    let hashVersion: Float?
    /// A more precise version of `coverage`
    let range: BucketRange?
    /// Ranges for experiment variations
    let ranges: [BucketRange]?
    /// Meta info about the experiment variations
    let meta: [VariationMeta]?
    /// Array of filters to apply to the rule
    let filters: [Filter]?
    /// Seed to use for hashing
    let seed: String?
    /// Human-readable name for the experiment
    let name: String?
    /// The phase id of the experiment
    let phase: String?
    /// Array of tracking calls to fire
    let tracks: [TrackData]?
}

/// Enum For defining feature value source
enum FeatureSource: String {
    /// Queried Feature doesn't exist in GrowthBook
    case unknownFeature
    /// Default Value for the Feature is being processed
    case defaultValue
    /// Forced Value for the Feature is being processed
    case force
    /// Experiment Value for the Feature is being processed
    case experiment
}

 /// Result for Feature
class FeatureResult {
    /// The assigned value of the feature
    let value: JSON?
    /// The assigned value cast to a boolean
    public var isOn: Bool = false
    /// The assigned value cast to a boolean and then negated
    public var isOff: Bool = true
    /// One of "unknownFeature", "defaultValue", "force", or "experiment"
    let source: String
    /// When source is "experiment", this will be the Experiment object used
    let experiment: Experiment?
    /// When source is "experiment", this will be an ExperimentResult object
    let experimentResult: ExperimentResult?
}
/// Defines a single experiment
class Experiment {
    /// The globally unique tracking key for the experiment
    let key: String
    /// The different variations to choose between
    let variations: [JSON]
    /// A tuple that contains the namespace identifier, plus a range of coverage for the experiment
    let namespace: [JSON]?
    /// All users included in the experiment will be forced into the specific variation index
    let hashAttribute: String?
    /// How to weight traffic between variations. Must add to 1.
    var weights: [Float]?
    /// If set to false, always return the control (first variation)
    var isActive: Bool
    /// What percent of users should be included in the experiment (between 0 and 1, inclusive)
    var coverage: Float?
    /// Optional targeting condition
    var condition: JSON?
    /// All users included in the experiment will be forced into the specific variation index
    var force: Int?
    /// Array of ranges, one per variation
    let ranges: [BucketRange]?
    /// Meta info about the variations
    let meta: [VariationMeta]?
    /// Array of filters to apply
    let filters: [Filter]?
    /// The hash seed to use
    let seed: String?
    /// Human-readable name for the experiment
    let name: String?
    /// Id of the current experiment phase
    let phase: String?
}

/// The result of running an Experiment given a specific Context
class ExperimentResult {
    /// Whether or not the user is part of the experiment
    let inExperiment: Bool
    /// The array index of the assigned variation
    let variationId: Int
    /// The array value of the assigned variation
    let value: JSON
    /// The user attribute used to assign a variation
    let hashAttribute: String?
    /// The value of that attribute
    let valueHash: String?
    /// The unique key for the assigned variation
    let key: String
    /// The human-readable name of the assigned variation
    let name: String?
    /// The hash value used to assign a variation (float from `0` to `1`)
    let bucket: Float?
    /// Used for holdout groups
    let passthrough: Bool?
}

/// Meta info about the variations
public struct VariationMeta {
    /// Used to implement holdout groups
    let passthrough: Bool?
    /// A unique key for this variation
    let key: String?
    /// A human-readable name for this variation
    let name: String?
}

///Used for remote feature evaluation to trigger the `TrackingCallback`
public struct TrackData {
    let experiment: Experiment
    let result: ExperimentResult
}

Streaming updates

To enable streaming updates set backgroundSync variable to "true"

var sdkInstance: GrowthBookSDK = GrowthBookBuilder(apiHost: <GrowthBook/API_KEY>, clientKey: <GrowthBook/ClientKey>, attributes: <[String: Any]>, trackingCallback: { experiment, experimentResult in }, refreshHandler: { isRefreshed in }, backgroundSync: true) .initializer()

Remote Evaluation

This mode brings the security benefits of a backend SDK to the front end by evaluating feature flags exclusively on a private server. Using Remote Evaluation ensures that any sensitive information within targeting rules or unused feature variations are never seen by the client. Note that Remote Evaluation should not be used in a backend context.

You must enable Remote Evaluation in your SDK Connection settings. Cloud customers are also required to self-host a GrowthBook Proxy Server or custom remote evaluation backend.

To use Remote Evaluation, add the remoteEval: true property to your SDK instance. A new evaluation API call will be made any time a user attribute or other dependency changes. You may optionally limit these API calls to specific attribute changes by setting the cacheKeyAttributes property (an array of attribute names that, when changed, trigger a new evaluation call).

var sdkInstance: GrowthBookSDK = GrowthBookBuilder(apiHost: <GrowthBook/API_KEY>, clientKey: <GrowthBook/ClientKey>, attributes: <[String: Any]>, trackingCallback: { experiment, experimentResult in }, refreshHandler: { isRefreshed in }, remoteEval: true) .initializer()

:::note

If you would like to implement Sticky Bucketing while using Remote Evaluation, you must configure your remote evaluation backend to support Sticky Bucketing. You will not need to provide a StickyBucketService instance to the client side SDK.

Sticky Bucketing

Sticky bucketing ensures that users see the same experiment variant, even when user session, user login status, or experiment parameters change. See the Sticky Bucketing docs for more information. If your organization and experiment supports sticky bucketing, you must implement an instance of the StickyBucketService to use Sticky Bucketing. For simple bucket persistence using the browser's LocalStorage (can be polyfilled for other environments).

License

This project uses the MIT license. The core GrowthBook app will always remain open and free, although we may add some commercial enterprise add-ons in the future.

growthbook-swift's People

Contributors

auz avatar cveer-maf avatar ericrabil avatar jdorn avatar levochkaa avatar loganblevins avatar mgratzer avatar vazarkevych avatar vinu-vanjari avatar yuryks avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

growthbook-swift's Issues

When using CocoaPods you are required to use Rosetta simulators

Right now when adding the Growthbook sdk using CocoaPods the podfile include these lines:

 spec.xcconfig                  = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }
 spec.pod_target_xcconfig       = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }
 spec.user_target_xcconfig      = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }

That forces you to use Rosetta simulators instead of the native ones when using an arm Mac.

It would be great to have native arm support in the sdk so we avoid having to use Rosetta in the simulators.

Unable to find a specification for `GrowthBook-IOS`

After adding the dependency to Pod file according as stated in the README.md in-order to install the SDK via CocoaPods, I got the following error

[!] Unable to find a specification for `GrowthBook-IOS`

You have either:
 * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
 * mistyped the name or version.
 * not added the source repo that hosts the Podspec to your Podfile.

How can I update the cache in real time without having to refreshCache?

I am using version 1.0.56, with this initializer:

 growthBook = GrowthBookBuilder(
            apiHost: configurationModel.apiHost,
            clientKey: configurationModel.clientKey,
            encryptionKey: configurationModel.encryptionKey,
            attributes: configurationModel.attributes,
            trackingCallback: { experiment, experimentResult in
                print("trackingCallback: \{ experiment):\(experimentResult)”)
            },
            refreshHandler: { isRefreshed in
                print("isRefreshed: \(isRefreshed)”)
            },
            backgroundSync: true).initializer()

I would like to know if having the app open and without ever taking out of memory growthBook could update cached parameters, this is necessary to be able to force updates in real time of our application.

No Sources Files in the Pod Folder

After I managed to install the Pod successfully I realised that the library doesn't have any source files/folder other than the README.md and the LICENSE files. So I'm unable to initialize the SDK in my swift codes/files

Which version of the SDK spec is supported?

Hi folks, I couldn't see so easily which version of the SDK spec is supposed by the latest release. If it's possible to add that information to subsequent releases it would make using your library easier (since we need to match spec versions across multiple platforms). In the meantime: which spec version is currently supported, in release 1.0.37?

Enabling/Disabling encryption on GrowthBook UI Fails

Giving some context...

We had GrowthBook v1.0.48, this version removes rosetta but had an issue related to encryption. We are handling the app force update with a GrowthBook feature flag, we have updated GrowthBook to v1.0.57 and reactivated the encryption. We noticed that our users with our app version using GrowthBook v1.0.48 were not getting the force update alert. When this was reported we realized that they were still with v1.0.48 so the encryption was not supported, then we proceeded to deactivate the encryption again, until all our users migrated to the new version.

The main problem is...

For some reason, It seems that activating the encryption in the GrowthBook web page works, but when we deactivate it again is not truly deactivated because our users with v1.0.48 are still not getting any GrowthBook information on the JSON producing the force update not to be displayed.

Is there a way to implement a realtime feature listener?

i want to be able to toggle a feature on/off for all users at once, currently iam using the refreshHandler call back in the GrowthBookBuilder initialiser to listen on changes, whenever it is called i use the isOn function on all my feature keys that i want, but after some minutes in a session, it stops sending call backs in refreshHandler, why is that and is there a way to implement what i need?

Disabling a feature in the GrowthBook UI did not also disable features in the application

Submitting this on behalf of a user:

If no features are sent then the SDK does not recognize that a change has occurred, so the feature array never gets updated. Its probably something that needs to be fixed in the SDK but we can work around it for now by always sending one enabled feature.


The original issue was disabling a feature in the GB UI did not cause the features to be disabled in the application.

When we debugged the code, we saw that even though the calls to GB were showing that features had changed, the GrowthBookBuilder refreshHandler was still saying refereshed = false. We were wondering if there was some level of cache happening that was preventing the GB SDK from getting the latest state. We gave it over 30 min but the SDK is still not getting the correct refreshed state.

Cache is empty after application update.

Problem:
After the application update from the AppStore feature cache is empty on the first launch till SDK re-fetch a new JSON from the server. If the application prepares features on launch - the user will see an invalid application state.

Why this may occur:
Currently, the cache is stored in Library/Caches/.

Documentation says:
Note that the system may delete the Caches/ directory to free up disk space, so your app must be able to re-create or download these files as needed. Link to doc

So anytime the system decides to clear the Caches directory - we have empty features cache.

Suggestion:

  • move SDK cache to another directory (add manual clear method if needed)
  • add the ability to configure cache location in SDK
Screenshot 2023-07-05 at 08 28 36

Force rule based on osVersion not returning correct Value

I have one feature flag in which I applied one force rule based on osVersion.

I have to check something specifically on OS Version 16.4.
I checked the attributes in my code, we are setting osVersion as "16.4" to growthbook SDK, and force rule also returns this in string format "16.4", but still SDK is not returning correct value.

Can you guys have a look at this on priority pls.

FeaturesViewModel is released immediately?

my code.

@IBAction func initSDK(_ sender: Any) {
    let hostURL = "url_here"
    sdkInstance = GrowthBookBuilder(hostURL: hostURL,
                                    attributes: ["platform" : "iOS",
                                                 "country":"mafegy"],
                                    trackingCallback: { experiment, experimentResult in
    }).initializer()
  }

Growthbook code

    /// Manually Refresh Cache
    @objc public func refreshCache() {
        let featureVM = FeaturesViewModel(delegate: self, dataSource: FeaturesDataSource(dispatcher: networkDispatcher))
        featureVM.fetchFeatures(apiUrl: gbContext.hostURL)
    }

Here - FeaturesViewModel gets deinit

dataSource.fetchFeatures(apiUrl: apiUrl) { [weak self] result in

// here weak self is nil as deinit is being called weak self

Can you look into it? If I make let featureVM = FeaturesViewModel( global or remove weak self, it works fine. as scope ends for FeaturesViewModel.

CleanShot 2022-08-02 at 13 34 54@2x

Encrypted features are not parsed, after latest version fix

After the fix in latest version 1.0.59 71, with the removal of !features.isEmpty the encrypted features are not parsed, because there is an empty array of features. We need to check if encrypted features also exist.

Also there is a decoding error in FeaturesViewModel in prepareFeatureData when decoding the FeaturesDataModel. The issue is that the savedGroups property is declared as String? but in fact is a dictionary.

featuresFetchFailed(error:isRemote:) is always called with isRemote == false regardless of the failure source

    func fetchFeatures(apiUrl: String?) {
        // Check for cache data
        if let json = manager.getData(fileName: Constants.featureCache) {
            let decoder = JSONDecoder()
            if let jsonPetitions = try? decoder.decode(FeaturesDataModel.self, from: json) {
                if let features = jsonPetitions.features {
                    // Call Success Delegate with mention of data available but its not remote
                    delegate?.featuresFetchedSuccessfully(features: features, isRemote: true)
                } else {
                    delegate?.featuresFetchFailed(error: .failedParsedData, isRemote: false)
                    logger.error("Failed parsed local data")
                }
            }
        } else {
            delegate?.featuresFetchFailed(error: .failedToLoadData, isRemote: false)
            logger.error("Failed load local data")
        }

        guard let apiUrl = apiUrl else { return }
        dataSource.fetchFeatures(apiUrl: apiUrl) { [weak self] result in
            switch result {
            case .success(let data):
                self?.prepareFeaturesData(data: data)
            case .failure(let error):
                self?.delegate?.featuresFetchFailed(error: .failedToLoadData, isRemote: false)
                logger.error("Failed get features: \(error.localizedDescription)")
            }
        }
    }

Strings with version format are not compared as versions

The version 0.9.0 is smaller then 0.10.0, therefore

ConditionEvaluator().isEvalOperatorCondition(operatorKey: "$vlt", attributeValue: "0.9.0", conditionValue: "0.10.0")

should return true.

Actual result: The mention method call returns false.
Expected result: The mention method call returns true.

Environment:

GrowthBook 1.0.38

Steps to reproduce:

Use the follow test to reproduce the issue:

@testable import GrowthBook
import XCTest

final class GrowthBookTests: XCTestCase {
    func test_condition_vlt() {
        let sut = ConditionEvaluator()
        XCTAssertTrue(sut.isEvalOperatorCondition(operatorKey: "$vlt", attributeValue: "0.9.0", conditionValue: "0.10.0"))
    }
}

Quick Start Documentation out of sync with latest build

Was looking to update our Swift SDK from 0.42 -> latest (0.57) and encountering an initializer error because the url argument is no longer supported. Checked the documentation and see that it still says the url argument is supported.

Looks like the argument was removed with this change: 5c1f9aa#diff-9fb9393d03752436c3248bcd9815cb5b09e5de06d63970e339799c7abbda954c

Seems like just need to rename the arguments but unclear without spelunking into all the changes. What is the correct way to initialize now?

Quick Start Documentation
image

Error:
image

response 304 Not Modified

Request api/features/sdk-laxFrU33UrMаh8NK
Response 304 Not Modified

Hi there.
Has anyone encountered such a response with GrowthBookBuilder?
Some kind of problem with the cache. There is no such situation on Android.

Thanks in advance.

Disable SSE Streaming in Mobile SDK

Is there a way to disable Server-Sent Events (SSE) streaming in the GrowthBook Mobile SDK similar to how it can be done in browser environments?

Context
The documentation mentions enabling SSE streaming in browser environments by setting streaming: true in the initialization call. However, it's unclear if there's an equivalent method to disable SSE streaming in the Mobile SDK. https://docs.growthbook.io/lib/js/#streaming-in-browser-environments

Thank you for your assistance.

[Bug] evalCondition ignores other conditions if special operator exists

If one of the four operators $or, $nor, $and, or $not exists at a particular level of the targeting conditions, all other keys at that level are ignored. This can be worked around by wrapping the level in an { $and: [...] }, but having multiple keys at the same level should be natively supported by the SDK as it's part of the Mongo query language.

See this pr for an example of fixing this behavior in the JS sdk

FeaturesViewModel.swift:47 ERROR: Failed load local data

After upgrading to the latest version 1.0.47, I have started to experience this error:

[2024-03-11 11:49:48.198] FeaturesViewModel.swift:47 ERROR: Failed load local data

This has caused that when returning certain values of features flags these values are not returned correctly.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.