This is a port of Featurevisor Javascript SDK v2.x to Go, providing a way to evaluate feature flags, variations, and variables in your Go applications.
This SDK is compatible with Featurevisor v2.0 projects and above.
See example application here.
- Installation
- Initialization
- Evaluation types
- Context
- Check if enabled
- Getting variation
- Getting variables
- Getting all evaluations
- Sticky
- Setting datafile
- Logging
- Events
- Evaluation details
- Hooks
- Child instance
- Close
- CLI usage
- Development of this package
- License
In your Go application, install the SDK using Go modules:
go get github.com/featurevisor/featurevisor-go/sdk
The SDK can be initialized by passing datafile content directly:
package main
import (
"io"
"net/http"
"github.com/featurevisor/featurevisor-go/sdk"
)
func main() {
datafileURL := "https://cdn.yoursite.com/datafile.json"
resp, err := http.Get(datafileURL)
if err != nil {
panic(err)
}
defer resp.Body.Close()
datafileBytes, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var datafileContent sdk.DatafileContent
if err := datafileContent.FromJSON(string(datafileBytes)); err != nil {
panic(err)
}
f := sdk.CreateInstance(sdk.InstanceOptions{
Datafile: datafileContent,
})
}
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.
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 maps:
context := sdk.Context{
"userId": "123",
"country": "nl",
// ...other attributes
}
Context can be passed to SDK instance in various different ways, depending on your needs:
You can set context at the time of initialization:
import (
"github.com/featurevisor/featurevisor-go/sdk"
)
f := sdk.CreateInstance(sdk.InstanceOptions{
Context: sdk.Context{
"deviceId": "123",
"country": "nl",
},
})
This is useful for values that don't change too frequently and available at the time of application startup.
You can also set more context after the SDK has been initialized:
f.SetContext(sdk.Context{
"userId": "234",
})
This will merge the new context with the existing one (if already set).
If you wish to fully replace the existing context, you can pass true
in second argument:
f.SetContext(sdk.Context{
"deviceId": "123",
"userId": "234",
"country": "nl",
"browser": "chrome",
}, true) // replace existing 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:
context := sdk.Context{
"userId": "123",
"country": "nl",
}
isEnabled := f.IsEnabled("my_feature", context)
variation := f.GetVariation("my_feature", context)
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.
Further details for each evaluation types are described below.
Once the SDK is initialized, you can check if a feature is enabled or not:
featureKey := "my_feature"
isEnabled := f.IsEnabled(featureKey)
if isEnabled {
// do something
}
You can also pass additional context per evaluation:
isEnabled := f.IsEnabled(featureKey, sdk.Context{
// ...additional context
})
If your feature has any variations defined, you can evaluate them as follows:
featureKey := "my_feature"
variation := f.GetVariation(featureKey)
if variation != nil && *variation == "treatment" {
// do something for treatment variation
} else {
// handle default/control variation
}
Additional context per evaluation can also be passed:
variation := f.GetVariation(featureKey, sdk.Context{
// ...additional context
})
Your features may also include variables, which can be evaluated as follows:
variableKey := "bgColor"
bgColorValue := f.GetVariable("my_feature", variableKey)
Additional context per evaluation can also be passed:
bgColorValue := f.GetVariable("my_feature", variableKey, sdk.Context{
// ...additional context
})
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)
You can get evaluations of all features available in the SDK instance:
allEvaluations := f.GetAllEvaluations(sdk.Context{})
fmt.Printf("%+v\n", allEvaluations)
// {
// myFeature: {
// enabled: true,
// variation: "control",
// variables: {
// myVariableKey: "myVariableValue",
// },
// },
//
// anotherFeature: {
// enabled: true,
// variation: "treatment",
// }
// }
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
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:
import (
"github.com/featurevisor/featurevisor-go/sdk"
)
f := sdk.CreateInstance(sdk.InstanceOptions{
Sticky: &StickyFeatures{
"myFeatureKey": sdk.StickyFeature{
Enabled: true,
// optional
Variation: &sdk.VariationValue{Value: "treatment"},
Variables: map[string]interface{}{
"myVariableKey": "myVariableValue",
},
},
"anotherFeatureKey": sdk.StickyFeature{
Enabled: false,
},
},
})
Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
You can also set sticky features after the SDK is initialized:
f.SetSticky(sdk.StickyFeatures{
"myFeatureKey": sdk.StickyFeature{
Enabled: true,
Variation: &sdk.VariationValue{Value: "treatment"},
Variables: map[string]interface{}{
"myVariableKey": "myVariableValue",
},
},
"anotherFeatureKey": sdk.StickyFeature{
Enabled: false,
},
}, true) // replace existing sticky features (false by default)
You may also initialize the SDK without passing datafile
, and set it later on:
f.SetDatafile(datafileContent)
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.
The triggers for setting the datafile again can be:
- periodic updates based on an interval (like every 5 minutes), or
- reacting to:
- a specific event in your application (like a user action), or
- an event served via websocket or server-sent events (SSE)
Here's an example of using interval-based update:
import (
"time"
"io"
"net/http"
"github.com/featurevisor/featurevisor-go/sdk"
)
func updateDatafile(f *sdk.Featurevisor, datafileURL string) {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
resp, err := http.Get(datafileURL)
if err != nil {
continue
}
defer resp.Body.Close()
datafileBytes, err := io.ReadAll(resp.Body)
if err != nil {
continue
}
var datafileContent sdk.DatafileContent
if err := datafileContent.FromJSON(string(datafileBytes)); err != nil {
continue
}
f.SetDatafile(datafileContent)
}
}
// Start the update goroutine
go updateDatafile(f, datafileURL)
By default, Featurevisor SDKs will print out logs to the console for info
level and above.
These are all the available log levels:
error
warn
info
debug
If you choose debug
level to make the logs more verbose, you can set it at the time of SDK initialization.
Setting debug
level will print out all logs, including info
, warn
, and error
levels.
import (
"github.com/featurevisor/featurevisor-go/sdk"
)
logLevel := sdk.LogLevelDebug
f := sdk.CreateInstance(sdk.InstanceOptions{
LogLevel: &logLevel,
})
Alternatively, you can also set logLevel
directly:
logLevel := sdk.LogLevelDebug
f := sdk.CreateInstance(sdk.InstanceOptions{
LogLevel: &logLevel,
})
You can also set log level from SDK instance afterwards:
f.SetLogLevel(sdk.LogLevelDebug)
You can also pass your own log handler, if you do not wish to print the logs to the console:
import (
"github.com/featurevisor/featurevisor-go/sdk"
)
logger := sdk.NewLogger(sdk.CreateLoggerOptions{
Level: &sdk.LogLevelInfo,
Handler: func(level sdk.LogLevel, message string, details interface{}) {
// do something with the log
},
})
f := sdk.CreateInstance(sdk.InstanceOptions{
Logger: logger,
})
Further log levels like info
and debug
will help you understand how the feature variations and variables are evaluated in the runtime against given context.
Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
You can listen to these events that can occur at various stages in your application:
unsubscribe := f.On(sdk.EventNameDatafileSet, func(event sdk.Event) {
revision := event.Revision // new revision
previousRevision := event.PreviousRevision
revisionChanged := event.RevisionChanged // true if revision has changed
// list of feature keys that have new updates,
// and you should re-evaluate them
features := event.Features
// handle here
})
// stop listening to the event
unsubscribe()
The features
array will contain keys of features that have either been:
- added, or
- updated, or
- removed
compared to the previous datafile content that existed in the SDK instance.
unsubscribe := f.On(sdk.EventNameContextSet, func(event sdk.Event) {
replaced := event.Replaced // true if context was replaced
context := event.Context // the new context
fmt.Println("Context set")
})
unsubscribe := f.On(sdk.EventNameStickySet, func(event sdk.Event) {
replaced := event.Replaced // true if sticky features got replaced
features := event.Features // list of all affected feature keys
fmt.Println("Sticky features set")
})
Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context:
// flag
evaluation := f.EvaluateFlag(featureKey, context)
// variation
evaluation := f.EvaluateVariation(featureKey, context)
// variable
evaluation := f.EvaluateVariable(featureKey, variableKey, context)
The returned object will always contain the following properties:
FeatureKey
: the feature keyReason
: the reason how the value was evaluated
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
BucketValue
: the bucket value between 0 and 100,000RuleKey
: the rule keyError
: the error objectEnabled
: if feature itself is enabled or notVariation
: the variation objectVariationValue
: the variation valueVariableKey
: the variable keyVariableValue
: the variable valueVariableSchema
: the variable schema
Hooks allow you to intercept the evaluation process and customize it further as per your needs.
A hook is a simple struct with a unique required Name
and optional functions:
import (
"github.com/featurevisor/featurevisor-go/sdk"
)
myCustomHook := &sdk.Hook{
// only required property
Name: "my-custom-hook",
// rest of the properties below are all optional per hook
// before evaluation
Before: func(options sdk.EvaluateOptions) sdk.EvaluateOptions {
// update context before evaluation
if options.Context == nil {
options.Context = sdk.Context{}
}
options.Context["someAdditionalAttribute"] = "value"
return options
},
// after evaluation
After: func(evaluation sdk.Evaluation, options sdk.EvaluateOptions) {
if evaluation.Reason == "error" {
// log error
return
}
},
// configure bucket key
BucketKey: func(options sdk.EvaluateOptions) string {
// return custom bucket key
return options.BucketKey
},
// configure bucket value (between 0 and 100,000)
BucketValue: func(options sdk.EvaluateOptions) int {
// return custom bucket value
return options.BucketValue
},
}
You can register hooks at the time of SDK initialization:
import (
"github.com/featurevisor/featurevisor-go/sdk"
)
f := sdk.CreateInstance(sdk.InstanceOptions{
Hooks: []*sdk.Hook{
myCustomHook,
},
})
Or after initialization:
f.AddHook(myCustomHook)
When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.
That's where child instances come in handy:
childF := f.Spawn(sdk.Context{
// user or request specific context
"userId": "123",
})
Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone:
isEnabled := childF.IsEnabled("my_feature")
variation := childF.GetVariation("my_feature")
variableValue := childF.GetVariable("my_feature", "my_variable")
Similar to parent SDK, child instances also support several additional methods:
SetContext
SetSticky
IsEnabled
GetVariation
GetVariable
GetVariableBoolean
GetVariableString
GetVariableInteger
GetVariableDouble
GetVariableArray
GetVariableObject
GetVariableJSON
GetAllEvaluations
On
Close
Both primary and child instances support a .Close()
method, that removes forgotten event listeners (via On
method) and cleans up any potential memory leaks.
f.Close()
This package also provides a CLI tool for running your Featurevisor project's test specs and benchmarking against this Go SDK:
Learn more about testing here.
go run cli/main.go test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project"
Additional options that are available:
go run cli/main.go test \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--quiet|verbose \
--onlyFailures \
--keyPattern="myFeatureKey" \
--assertionPattern="#1"
Learn more about benchmarking here.
go run cli/main.go benchmark \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--environment="production" \
--feature="myFeatureKey" \
--context='{"country": "nl"}' \
--n=1000
Learn more about assessing distribution here.
go run cli/main.go assess-distribution \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--environment=production \
--feature=foo \
--variation \
--context='{"country": "nl"}' \
--populateUuid=userId \
--populateUuid=deviceId \
--n=1000
Clone the repository, and install the dependencies using Go modules:
go mod download
go test ./...
- Manually create a new release on GitHub
- Tag it with a prefix of
v
, likev1.0.0
MIT © Fahad Heylaal