Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ Thumbs.db
build

example-1

monorepo
specs
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.PHONY: build test clean setup-monorepo update-monorepo

build:
mkdir -p build
go build -o build/featurevisor-go cmd/main.go
Expand All @@ -7,3 +9,15 @@ test:

clean:
rm -rf build

setup-monorepo:
mkdir -p monorepo
if [ ! -d "monorepo/.git" ]; then \
git clone git@github.com:featurevisor/featurevisor.git monorepo; \
else \
(cd monorepo && git fetch origin main && git checkout main && git pull origin main); \
fi
(cd monorepo && make install && make build)

update-monorepo:
(cd monorepo && git pull origin main)
78 changes: 59 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ f.GetVariableObject(featureKey, variableKey, context)
f.GetVariableJSON(featureKey, variableKey, context)
```

For typed arrays/objects, use `Into` methods with pointer outputs:

```go
var items []string
_ = f.GetVariableArrayInto(featureKey, variableKey, context, &items)

var cfg MyConfig
_ = f.GetVariableObjectInto(featureKey, variableKey, context, &cfg)
```

`context` and `OverrideOptions` are optional and can be passed before the output pointer.

## Getting all evaluations

You can get evaluations of all features available in the SDK instance:
Expand Down Expand Up @@ -305,16 +317,19 @@ import (
)

f := featurevisor.CreateInstance(featurevisor.Options{
Sticky: &StickyFeatures{
"myFeatureKey": featurevisor.StickyFeature{
Sticky: &featurevisor.StickyFeatures{
"myFeatureKey": {
Enabled: true,
// optional
Variation: &featurevisor.VariationValue{Value: "treatment"},
Variation: func() *featurevisor.VariationValue {
v := featurevisor.VariationValue("treatment")
return &v
}(),
Variables: map[string]interface{}{
"myVariableKey": "myVariableValue",
},
},
"anotherFeatureKey": featurevisor.StickyFeature{
"anotherFeatureKey": {
Enabled: false,
},
},
Expand All @@ -329,14 +344,17 @@ You can also set sticky features after the SDK is initialized:

```go
f.SetSticky(featurevisor.StickyFeatures{
"myFeatureKey": featurevisor.StickyFeature{
"myFeatureKey": {
Enabled: true,
Variation: &featurevisor.VariationValue{Value: "treatment"},
Variation: func() *featurevisor.VariationValue {
v := featurevisor.VariationValue("treatment")
return &v
}(),
Variables: map[string]interface{}{
"myVariableKey": "myVariableValue",
},
},
"anotherFeatureKey": featurevisor.StickyFeature{
"anotherFeatureKey": {
Enabled: false,
},
}, true) // replace existing sticky features (false by default)
Expand All @@ -350,6 +368,8 @@ You may also initialize the SDK without passing `datafile`, and set it later on:
f.SetDatafile(datafileContent)
```

`SetDatafile` accepts either parsed `featurevisor.DatafileContent` or a raw JSON string.

### Updating datafile

You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile-set) event that you can listen and react to accordingly.
Expand Down Expand Up @@ -480,14 +500,14 @@ You can listen to these events that can occur at various stages in your applicat
### `datafile_set`

```go
unsubscribe := f.On(featurevisor.EventNameDatafileSet, func(event featurevisor.Event) {
revision := event.Revision // new revision
previousRevision := event.PreviousRevision
revisionChanged := event.RevisionChanged // true if revision has changed
unsubscribe := f.On(featurevisor.EventNameDatafileSet, func(details featurevisor.EventDetails) {
revision := details["revision"] // new revision
previousRevision := details["previousRevision"]
revisionChanged := details["revisionChanged"] // true if revision has changed

// list of feature keys that have new updates,
// and you should re-evaluate them
features := event.Features
features := details["features"]

// handle here
})
Expand All @@ -507,9 +527,9 @@ compared to the previous datafile content that existed in the SDK instance.
### `context_set`

```go
unsubscribe := f.On(featurevisor.EventNameContextSet, func(event featurevisor.Event) {
replaced := event.Replaced // true if context was replaced
context := event.Context // the new context
unsubscribe := f.On(featurevisor.EventNameContextSet, func(details featurevisor.EventDetails) {
replaced := details["replaced"] // true if context was replaced
context := details["context"] // the new context

fmt.Println("Context set")
})
Expand All @@ -518,9 +538,9 @@ unsubscribe := f.On(featurevisor.EventNameContextSet, func(event featurevisor.Ev
### `sticky_set`

```go
unsubscribe := f.On(featurevisor.EventNameStickySet, func(event featurevisor.Event) {
replaced := event.Replaced // true if sticky features got replaced
features := event.Features // list of all affected feature keys
unsubscribe := f.On(featurevisor.EventNameStickySet, func(details featurevisor.EventDetails) {
replaced := details["replaced"] // true if sticky features got replaced
features := details["features"] // list of all affected feature keys

fmt.Println("Sticky features set")
})
Expand Down Expand Up @@ -628,7 +648,8 @@ f := featurevisor.CreateInstance(featurevisor.Options{
Or after initialization:

```go
f.AddHook(myCustomHook)
removeHook := f.AddHook(myCustomHook)
removeHook()
```

## Child instance
Expand Down Expand Up @@ -666,7 +687,9 @@ Similar to parent SDK, child instances also support several additional methods:
- `GetVariableInteger`
- `GetVariableDouble`
- `GetVariableArray`
- `GetVariableArrayInto`
- `GetVariableObject`
- `GetVariableObjectInto`
- `GetVariableJSON`
- `GetAllEvaluations`
- `On`
Expand Down Expand Up @@ -699,10 +722,27 @@ go run cmd/main.go test \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--quiet|verbose \
--onlyFailures \
--with-scopes \
--with-tags \
--keyPattern="myFeatureKey" \
--assertionPattern="#1"
```

`--with-scopes` and `--with-tags` match Featurevisor CLI behavior by generating and testing against scoped/tagged datafiles via `npx featurevisor build`.

If you want to validate parity locally against the JavaScript SDK runner, you can use the bundled example project:

```bash
cd monorepo/examples/example-1
npx featurevisor test --with-scopes --with-tags

# from repository root:
go run cmd/main.go test \
--projectDirectoryPath="/absolute/path/to/featurevisor-go/monorepo/examples/example-1" \
--with-scopes \
--with-tags
```

### Benchmark

Learn more about benchmarking [here](https://featurevisor.com/docs/cmd/#benchmarking).
Expand Down
102 changes: 75 additions & 27 deletions child.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package featurevisor

import "fmt"

// ChildOptions contains options for creating a child instance
type ChildOptions struct {
Parent *Featurevisor
Expand All @@ -12,6 +14,7 @@ type FeaturevisorChild struct {
parent *Featurevisor
context Context
sticky *StickyFeatures
emitter *Emitter
}

// NewFeaturevisorChild creates a new child instance
Expand All @@ -20,9 +23,24 @@ func NewFeaturevisorChild(options ChildOptions) *FeaturevisorChild {
parent: options.Parent,
context: options.Context,
sticky: options.Sticky,
emitter: NewEmitter(),
}
}

// On adds an event listener
func (c *FeaturevisorChild) On(eventName EventName, callback EventCallback) Unsubscribe {
if eventName == EventNameContextSet || eventName == EventNameStickySet {
return c.emitter.On(eventName, callback)
}

return c.parent.On(eventName, callback)
}

// Close closes child instance listeners
func (c *FeaturevisorChild) Close() {
c.emitter.ClearAll()
}

// SetContext sets the context
func (c *FeaturevisorChild) SetContext(context Context, replace ...bool) {
replaceValue := false
Expand All @@ -38,24 +56,24 @@ func (c *FeaturevisorChild) SetContext(context Context, replace ...bool) {
c.context[key] = value
}
}

c.emitter.Trigger(EventNameContextSet, EventDetails{
"context": c.context,
"replaced": replaceValue,
})
}

// GetContext returns the context
func (c *FeaturevisorChild) GetContext(context Context) Context {
if context == nil {
return c.context
}

// Merge contexts
result := Context{}
merged := Context{}
for key, value := range c.context {
result[key] = value
merged[key] = value
}
for key, value := range context {
result[key] = value
merged[key] = value
}

return result
return c.parent.GetContext(merged)
}

// SetSticky sets sticky features
Expand Down Expand Up @@ -86,8 +104,7 @@ func (c *FeaturevisorChild) SetSticky(sticky StickyFeatures, replace ...bool) {

params := getParamsForStickySetEvent(previousStickyFeatures, *c.sticky, replaceValue)

c.parent.logger.Info("sticky features set", params)
c.parent.emitter.Trigger(EventNameStickySet, EventDetails(params))
c.emitter.Trigger(EventNameStickySet, EventDetails(params))
}

// getEvaluationDependencies gets evaluation dependencies
Expand Down Expand Up @@ -334,18 +351,7 @@ func (c *FeaturevisorChild) GetVariableArray(featureKey string, variableKey stri
return nil
}

typedValue := GetValueByType(value, "array")
if arrayValue, ok := typedValue.([]interface{}); ok {
result := make([]string, len(arrayValue))
for i, item := range arrayValue {
if strItem, ok := item.(string); ok {
result[i] = strItem
}
}
return result
}

return nil
return ToTypedArray[string](GetValueByType(value, "array"))
}

// GetVariableObject gets an object variable
Expand All @@ -355,12 +361,12 @@ func (c *FeaturevisorChild) GetVariableObject(featureKey string, variableKey str
return nil
}

typedValue := GetValueByType(value, "object")
if objectValue, ok := typedValue.(map[string]interface{}); ok {
return objectValue
typedValue := ToTypedObject[map[string]interface{}](GetValueByType(value, "object"))
if typedValue == nil {
return nil
}

return nil
return *typedValue
}

// GetVariableJSON gets a JSON variable
Expand All @@ -373,6 +379,48 @@ func (c *FeaturevisorChild) GetVariableJSON(featureKey string, variableKey strin
return value
}

// GetVariableArrayInto decodes an array variable into the provided pointer output.
// Supported argument order (after featureKey, variableKey): out OR context, out OR context, options, out.
func (c *FeaturevisorChild) GetVariableArrayInto(featureKey string, variableKey string, args ...interface{}) error {
context, options, out, err := parseVariableIntoArgs(args...)
if err != nil {
return err
}

value := c.GetVariable(featureKey, variableKey, context, options)
if value == nil {
return decodeInto(nil, out)
}

arrayValue := GetValueByType(value, "array")
if arrayValue == nil {
return fmt.Errorf("variable %q is not an array", variableKey)
}

return decodeInto(arrayValue, out)
}

// GetVariableObjectInto decodes an object variable into the provided pointer output.
// Supported argument order (after featureKey, variableKey): out OR context, out OR context, options, out.
func (c *FeaturevisorChild) GetVariableObjectInto(featureKey string, variableKey string, args ...interface{}) error {
context, options, out, err := parseVariableIntoArgs(args...)
if err != nil {
return err
}

value := c.GetVariable(featureKey, variableKey, context, options)
if value == nil {
return decodeInto(nil, out)
}

objectValue := GetValueByType(value, "object")
if objectValue == nil {
return fmt.Errorf("variable %q is not an object", variableKey)
}

return decodeInto(objectValue, out)
}

// GetAllEvaluations gets all evaluations for features
func (c *FeaturevisorChild) GetAllEvaluations(context Context, featureKeys []string, options OverrideOptions) EvaluatedFeatures {
result := EvaluatedFeatures{}
Expand Down
Loading
Loading