Skip to content

featurevisor/featurevisor-swift2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Featurevisor Swift SDK

This is a port of Featurevisor Javascript SDK v3.x to Swift, providing a way to evaluate feature flags, variations, and variables in your Swift applications.

This SDK is compatible with Featurevisor v3.0 projects and v2 datafiles.

Table of contents

Installation

In your Swift application, add this package using Swift Package Manager:

.package(url: "https://github.com/featurevisor/featurevisor-swift2.git", from: "0.1.0")

Then add the product dependency:

.product(name: "Featurevisor", package: "featurevisor-swift2")

Public API

The main runtime API is createFeaturevisor():

let f: Featurevisor = createFeaturevisor(
    FeaturevisorOptions(datafile: datafileContent)
)

Most applications only need createFeaturevisor, Featurevisor, and FeaturevisorOptions. Public extension and observability types include FeaturevisorModule, FeaturevisorDiagnostic, and the datafile model types.

Initialization

The SDK can be initialized by passing datafile content directly:

import Foundation
import Featurevisor

let datafileURL = URL(string: "https://cdn.yoursite.com/datafile.json")!
let data = try Data(contentsOf: datafileURL)
let datafileContent = try DatafileContent.fromData(data)

let f = createFeaturevisor(
    FeaturevisorOptions(
        datafile: datafileContent
    )
)

Evaluation types

We can evaluate 3 types of values against a particular feature:

  • Flag (Bool): whether the feature is enabled or not
  • Variation (String): the variation of the feature (if any)
  • Variables: variable values of the feature (if any)

These evaluations are run against the provided context.

Context

Contexts are attribute values that we pass to SDK for evaluating features against.

Think of the conditions that you define in your segments, which are used in your feature's rules.

They are plain dictionaries:

let context: Context = [
    "userId": .string("123"),
    "country": .string("nl"),
]

Setting initial context

You can set context at the time of initialization:

let f = createFeaturevisor(
    FeaturevisorOptions(
        context: [
            "deviceId": .string("123"),
            "country": .string("nl"),
        ]
    )
)

Setting after initialization

You can also set more context after the SDK has been initialized:

f.setContext([
    "userId": .string("234"),
])

This will merge the new context with the existing one (if already set).

Replacing existing context

If you wish to fully replace the existing context, you can pass true in second argument:

f.setContext(
    [
        "deviceId": .string("123"),
        "userId": .string("234"),
        "country": .string("nl"),
        "browser": .string("chrome"),
    ],
    replace: true
)

Manually passing context

You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations:

let context: Context = [
    "userId": .string("123"),
    "country": .string("nl"),
]

let isEnabled = f.isEnabled("my_feature", context)
let variation = f.getVariation("my_feature", context)
let variableValue = f.getVariable("my_feature", "my_variable", context)

When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.

Check if enabled

Once the SDK is initialized, you can check if a feature is enabled or not:

let featureKey = "my_feature"

let isEnabled = f.isEnabled(featureKey)

if isEnabled {
    // do something
}

You can also pass additional context per evaluation:

let isEnabled = f.isEnabled(featureKey, [
    // ...additional context
])

Getting variation

If your feature has any variations defined, you can evaluate them as follows:

let featureKey = "my_feature"

let variation = f.getVariation(featureKey)

if variation == "treatment" {
    // do something for treatment variation
} else {
    // handle default/control variation
}

Additional context per evaluation can also be passed:

let variation = f.getVariation(featureKey, [
    // ...additional context
])

Getting variables

Your features may also include variables, which can be evaluated as follows:

let variableKey = "bgColor"
let bgColorValue = f.getVariable("my_feature", variableKey)

Additional context per evaluation can also be passed:

let bgColorValue = f.getVariable("my_feature", variableKey, [
    // ...additional context
])

Type specific methods

Next to generic getVariable() methods, there are also type specific methods available for convenience:

f.getVariableBoolean(featureKey, variableKey, context)
f.getVariableString(featureKey, variableKey, context)
f.getVariableInteger(featureKey, variableKey, context)
f.getVariableDouble(featureKey, variableKey, context)
f.getVariableArray(featureKey, variableKey, context)
f.getVariableObject(featureKey, variableKey, context)
f.getVariableJSON(featureKey, variableKey, context)

Type specific methods do not coerce strings or booleans into numbers. They return nil when the value does not match the requested type.

Getting all evaluations

You can get evaluations of all features available in the SDK instance:

let allEvaluations = f.getAllEvaluations([:])
print(allEvaluations)

This is handy especially when you want to pass all evaluations from a backend application to the frontend.

Sticky

For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched datafile:

Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use SpawnOptions(sticky: ...) when a child needs its own sticky state.

Initialize with sticky

let f = createFeaturevisor(
    FeaturevisorOptions(
        sticky: [
            "myFeatureKey": EvaluatedFeature(
                enabled: true,
                variation: "treatment",
                variables: ["myVariableKey": .string("myVariableValue")]
            ),
            "anotherFeatureKey": EvaluatedFeature(enabled: false),
        ]
    )
)

Set sticky afterwards

f.setSticky([
    "myFeatureKey": EvaluatedFeature(
        enabled: true,
        variation: "treatment",
        variables: ["myVariableKey": .string("myVariableValue")]
    ),
    "anotherFeatureKey": EvaluatedFeature(enabled: false),
], replace: true)

Setting datafile

You may also initialize the SDK without passing datafile, and set it later on:

f.setDatafile(datafileContent)

You can also set using raw JSON string:

f.setDatafile(json: jsonString)

Merging by default

By default, setDatafile merges the incoming datafile with the SDK instance's existing datafile:

  • incoming features and segments override matching keys
  • existing features and segments that are missing from the incoming datafile are kept
  • revision, schemaVersion, and featurevisorVersion are taken from the incoming datafile

This means you can call setDatafile more than once with different datafiles, and the SDK instance accumulates their features and segments together.

Replacing

Pass replace: true to replace the stored datafile entirely:

f.setDatafile(datafileContent, replace: true)
f.setDatafile(json: jsonString, replace: true)

Loading datafiles on demand

Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.

This pairs well with targets, where each target produces a smaller datafile for a specific part of your application:

let f = createFeaturevisor(FeaturevisorOptions())

func loadDatafile(target: String) {
    let url = URL(string: "https://cdn.yoursite.com/production/featurevisor-\(target).json")!
    if let data = try? Data(contentsOf: url),
       let datafile = try? DatafileContent.fromData(data) {
        // merges into whatever was loaded before
        f.setDatafile(datafile)
    }
}

loadDatafile(target: "products")

// later, when the user reaches checkout
loadDatafile(target: "checkout")

Updating datafile

You can set the datafile as many times as you want in your application, which will result in emitting a datafile_set event that you can listen and react to accordingly.

Interval-based update

import Foundation

let interval: TimeInterval = 5 * 60
Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
    if let data = try? Data(contentsOf: datafileURL),
       let datafile = try? DatafileContent.fromData(data) {
        f.setDatafile(datafile)
    }
}

Diagnostics

By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.

Levels

Available diagnostic levels are fatal, error, warn, info, and debug.

Set the level during initialization or update it afterwards:

let f = createFeaturevisor(
    FeaturevisorOptions(logLevel: .debug)
)

f.setLogLevel(.info)

Handler

Use onDiagnostic to send structured diagnostics to your observability system:

let f = createFeaturevisor(
    FeaturevisorOptions(
        logLevel: .info,
        onDiagnostic: { diagnostic in
            print(diagnostic.level, diagnostic.code, diagnostic.message)
        }
    )
)

Modules can also subscribe to diagnostics or report their own diagnostics from setup using the provided module API.

Every diagnostic has level, code, message, and an object-shaped details dictionary. Optional module, moduleName, and originalError fields describe provenance. Evaluation metadata belongs in details.

Events

Featurevisor SDK implements a simple event emitter that allows you to listen to runtime events.

datafile_set

let unsubscribe = f.on(.datafileSet) { payload in
    print(payload.params)
}

unsubscribe()

context_set

let unsubscribe = f.on(.contextSet) { _ in
    // handle context updates
}

unsubscribe()

sticky_set

let unsubscribe = f.on(.stickySet) { _ in
    // handle sticky updates
}

unsubscribe()

error

let unsubscribe = f.on(.error) { payload in
    if case .object(let diagnostic)? = payload.params["diagnostic"] {
        print(diagnostic["message"] ?? "")
    }
}

unsubscribe()

The error event is emitted for diagnostics whose level is error.

Evaluation details

If you need evaluation metadata, use:

let flagDetails = f.evaluateFlag("my_feature")
let variationDetails = f.evaluateVariation("my_feature")
let variableDetails = f.evaluateVariable("my_feature", "my_variable")

Modules

Modules allow you to intercept evaluation inputs and outputs.

Defining a module

let module = FeaturevisorModule(
    name: "my-module",
    setup: { api in
        api.reportDiagnostic(
            FeaturevisorDiagnostic(
                level: .info,
                code: "module_ready",
                message: "Module is ready"
            )
        )
    },
    before: { options in
        var updated = options
        updated.dependencies.context["someAdditionalAttribute"] = .string("value")
        return updated
    },
    bucketKey: { options in
        options.bucketKey
    },
    bucketValue: { options in
        options.bucketValue
    },
    after: { evaluation, _ in
        evaluation
    },
    close: {
        // clean up module resources
    }
)

Registering modules

let f = createFeaturevisor(
    FeaturevisorOptions(
        modules: [module]
    )
)

let removeModule = f.addModule(module)
removeModule?()

Child instance

You can spawn child instances with inherited context:

let child = f.spawn([
    "userId": .string("123"),
])

let enabled = child.isEnabled("my_feature")

Close

To clear listeners and close resources:

f.close()

CLI usage

The package also ships an executable named featurevisor.

All three commands accept repeatable --target=<target> options. test builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. benchmark and assess-distribution run independently against every selected Target datafile. Without --target, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.

Test

swift run featurevisor test \
  --projectDirectoryPath=/path/to/featurevisor-project

Benchmark

swift run featurevisor benchmark \
  --projectDirectoryPath=/path/to/featurevisor-project \
  --environment=production \
  --feature=my_feature \
  --context='{"userId":"123"}' \
  --n=1000

Assess distribution

swift run featurevisor assess-distribution \
  --projectDirectoryPath=/path/to/featurevisor-project \
  --environment=production \
  --feature=my_feature \
  --populateUuid=userId \
  --n=1000

Development of this package

Running tests

swift test

To verify against the local Featurevisor example-1 project:

make test-example-1

License

MIT © Fahad Heylaal

About

Swift SDK for Featurevisor v2 and above

Resources

License

Stars

1 star

Watchers

1 watching

Forks

Packages

 
 
 

Contributors