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
90 changes: 68 additions & 22 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,86 @@ name: Checks
on:
push:
branches: ["**"]
pull_request:

permissions:
contents: read

jobs:
ci:
sdk:
name: SDK (Go ${{ matrix.go }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go: ["1.21.x", "1.25.x"]

steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: "1.22"
go-version: ${{ matrix.go }}
cache-dependency-path: go.sum

- name: Install dependencies
run: go mod tidy
- name: Test SDK
run: go test -race ./...

- name: Build
- name: Build CLI
run: make build

- name: Test
run: make test
openfeature:
name: OpenFeature provider
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
cache-dependency-path: |
go.sum
openfeature/go.sum

- name: Test provider
run: make test-openfeature

- name: Verify module boundaries
run: make verify-packages

example:
name: Featurevisor example-1
runs-on: ubuntu-latest

- uses: actions/setup-node@v4
steps:
- uses: actions/checkout@v7
with:
node-version-file: ".nvmrc"
path: featurevisor-go

- name: Setup Featurevisor example-1 project
- uses: actions/checkout@v7
with:
repository: featurevisor/featurevisor
ref: main
path: featurevisor

- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
cache-dependency-path: featurevisor-go/go.sum

- uses: actions/setup-node@v7
with:
node-version-file: "featurevisor/.nvmrc"
cache: npm
cache-dependency-path: featurevisor/package-lock.json

- name: Build Featurevisor CLI
working-directory: featurevisor
run: |
mkdir example-1
(cd example-1 && npx @featurevisor/cli@2.x init --example=1)
(cd example-1 && npm install)
(cd example-1 && npx featurevisor build)
(cd example-1 && npx featurevisor test)

- name: Run Featurevisor project tests against Go SDK
run: go run ./cmd/main.go test --projectDirectoryPath=./example-1
npm ci
npm run build

- name: Run example-1 through Go SDK
working-directory: featurevisor-go
run: go run ./cmd/main.go test --projectDirectoryPath=../featurevisor/examples/example-1 --onlyFailures
38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Release validation

on:
push:
tags:
- "v*"
- "openfeature/v*"

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
cache-dependency-path: |
go.sum
openfeature/go.sum

- name: Validate tag and modules
shell: bash
run: |
if [[ "$GITHUB_REF_NAME" == openfeature/* ]]; then
[[ "$GITHUB_REF_NAME" =~ ^openfeature/v2\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]
else
[[ "$GITHUB_REF_NAME" =~ ^v2\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]
fi
make verify-packages

- name: Test SDK and provider
run: make test
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2025 Fahad Heylaal (https://fahad19.com)
Copyright (c) 2026 Fahad Heylaal (https://fahad19.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
.PHONY: build test test-example-1 clean setup-monorepo update-monorepo
.PHONY: build test test-openfeature test-example-1 verify-packages clean setup-monorepo update-monorepo

build:
mkdir -p build
go build -o build/featurevisor-go cmd/main.go

test:
go test ./...
$(MAKE) test-openfeature

test-openfeature:
(cd openfeature && GOWORK=off go test ./...)

test-example-1:
go test ./...
$(MAKE) test
go run cmd/main.go test --projectDirectoryPath=../featurevisor/examples/example-1 --onlyFailures

verify-packages:
test "$$(go list -m)" = "github.com/featurevisor/featurevisor-go/v2"
test "$$(cd openfeature && GOWORK=off go list -m)" = "github.com/featurevisor/featurevisor-go/openfeature/v2"
(cd openfeature && GOWORK=off go list -deps ./... >/dev/null)

clean:
rm -rf build

Expand Down
87 changes: 76 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ See example application [here](https://github.com/featurevisor/featurevisor-exam
- [Registering modules](#registering-modules)
- [Child instance](#child-instance)
- [Close](#close)
- [OpenFeature](#openfeature)
- [CLI usage](#cli-usage)
- [Test](#test)
- [Benchmark](#benchmark)
Expand All @@ -62,7 +63,7 @@ See example application [here](https://github.com/featurevisor/featurevisor-exam
In your Go application, install the SDK using Go modules:

```bash
go get github.com/featurevisor/featurevisor-go
go get github.com/featurevisor/featurevisor-go/v2
```

## Public API
Expand All @@ -77,6 +78,8 @@ f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{

Most applications only need `CreateFeaturevisor`, the `Featurevisor` instance type, and `FeaturevisorOptions`. Public extension and observability types include `FeaturevisorModule`, `FeaturevisorDiagnostic`, and the datafile model types.

Concurrent evaluations are safe after an instance is configured. Do not call state-changing methods such as `SetDatafile`, `SetContext`, `SetSticky`, `AddModule`, `RemoveModule`, or `Close` concurrently with evaluations or with each other. Apply those changes from a serialized update path. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.

## Initialization

The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
Expand All @@ -88,7 +91,7 @@ import (
"io"
"net/http"

"github.com/featurevisor/featurevisor-go"
"github.com/featurevisor/featurevisor-go/v2"
)

func main() {
Expand Down Expand Up @@ -150,7 +153,7 @@ You can set context at the time of initialization:

```go
import (
"github.com/featurevisor/featurevisor-go"
"github.com/featurevisor/featurevisor-go/v2"
)

f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Expand Down Expand Up @@ -335,7 +338,7 @@ Sticky values belong to an SDK or child instance. Evaluation options do not acce

```go
import (
"github.com/featurevisor/featurevisor-go"
"github.com/featurevisor/featurevisor-go/v2"
)

f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Expand Down Expand Up @@ -462,7 +465,7 @@ import (
"io"
"net/http"

"github.com/featurevisor/featurevisor-go"
"github.com/featurevisor/featurevisor-go/v2"
)

func updateDatafile(f *featurevisor.Featurevisor, datafileURL string) {
Expand Down Expand Up @@ -643,7 +646,7 @@ If `Setup` panics, the module is not registered. Featurevisor removes subscripti

```go
import (
"github.com/featurevisor/featurevisor-go"
"github.com/featurevisor/featurevisor-go/v2"
)

myCustomModule := &featurevisor.FeaturevisorModule{
Expand Down Expand Up @@ -702,7 +705,7 @@ You can register modules at the time of SDK initialization:

```go
import (
"github.com/featurevisor/featurevisor-go"
"github.com/featurevisor/featurevisor-go/v2"
)

f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Expand All @@ -721,6 +724,8 @@ removeModule()

## Child instance

A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling `Close()` removes both child-owned listeners and subscriptions delegated to the parent.

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.
Expand All @@ -746,8 +751,11 @@ Similar to parent SDK, child instances also support several additional methods:

- `SetContext`
- `SetSticky`
- `EvaluateFlag`
- `IsEnabled`
- `EvaluateVariation`
- `GetVariation`
- `EvaluateVariable`
- `GetVariable`
- `GetVariableBoolean`
- `GetVariableString`
Expand Down Expand Up @@ -798,12 +806,12 @@ go run cmd/main.go test \
If you want to validate parity locally against the JavaScript SDK runner, you can use the bundled example project:

```bash
cd /Users/fahad/Projects/featurevisor/featurevisor/examples/example-1
cd ../featurevisor/examples/example-1
npx featurevisor test

# from this Go SDK repository root:
go run cmd/main.go test \
--projectDirectoryPath="/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1" \
--projectDirectoryPath="../featurevisor/examples/example-1" \
--onlyFailures

# or:
Expand Down Expand Up @@ -839,6 +847,61 @@ go run cmd/main.go assess-distribution \
--n=1000
```

## OpenFeature

The OpenFeature provider is a separate Go module, so applications that do not use OpenFeature do not receive its dependencies:

```bash
go get github.com/featurevisor/featurevisor-go/openfeature/v2
```

```go
import (
"context"

featurevisor "github.com/featurevisor/featurevisor-go/v2"
featurevisorof "github.com/featurevisor/featurevisor-go/openfeature/v2"
of "github.com/open-feature/go-sdk/openfeature"
)

provider := featurevisorof.NewProvider(featurevisorof.Options{
FeaturevisorOptions: featurevisor.FeaturevisorOptions{
Datafile: datafileContent,
},
})

if err := of.SetProviderAndWait(provider); err != nil {
panic(err)
}

client := of.NewClient("")
enabled, err := client.BooleanValue(
context.Background(),
"checkout",
false,
of.NewEvaluationContext("user-123", map[string]any{"country": "nl"}),
)
```

Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Arrays, objects, and JSON variables use the object resolver.

OpenFeature's targeting key maps to `userId` by default. `TargetingKeyField`, `KeySeparator`, and `VariationKey` can customize the mapping. The provider's separate module follows the Go version requirement of the official OpenFeature Go SDK.

You can also reuse an existing Featurevisor instance:

```go
fv := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Datafile: datafileContent,
})
provider := featurevisorof.NewProvider(featurevisorof.Options{
Featurevisor: fv,
})
```

The caller owns an instance passed this way. Provider shutdown does not close it. Call `fv.Close()` when every consumer is finished with it. When the provider creates the instance from `FeaturevisorOptions`, the provider owns and closes it. If both fields are supplied, `Featurevisor` takes precedence over `FeaturevisorOptions`.

See the [OpenFeature provider guide](https://featurevisor.com/docs/sdks/openfeature/) for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.

<!-- FEATUREVISOR_DOCS_END -->

## Development of this package
Expand All @@ -859,8 +922,10 @@ go test ./...

### Releasing

- Manually create a new release on [GitHub](https://github.com/featurevisor/featurevisor-go/releases)
- Tag it with a prefix of `v`, like `v1.0.0`
- Tag the core SDK as `v2.x.y`.
- Tag the provider module separately as `openfeature/v2.x.y`.
- Run `make verify-packages` before creating either release.
- Create the matching releases on [GitHub](https://github.com/featurevisor/featurevisor-go/releases).

## License

Expand Down
15 changes: 15 additions & 0 deletions api_compatibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ func TestAPICompatibilityWithStringFeatureKey(t *testing.T) {
if childVariable == nil {
t.Error("Expected child variable to be returned")
}

flagEvaluation := child.EvaluateFlag("test-feature")
if flagEvaluation.Enabled == nil || !*flagEvaluation.Enabled {
t.Fatalf("expected child flag evaluation to be enabled, got %#v", flagEvaluation)
}

variationEvaluation := child.EvaluateVariation("test-feature")
if variationEvaluation.VariationValue == nil || string(*variationEvaluation.VariationValue) != *childVariation {
t.Fatalf("expected child variation evaluation to match getter, got %#v", variationEvaluation)
}

variableEvaluation := child.EvaluateVariable("test-feature", "color")
if variableEvaluation.VariableValue != "blue" {
t.Fatalf("expected child variable evaluation value blue, got %#v", variableEvaluation)
}
}

func TestAPICompatibilityWithNonExistentFeature(t *testing.T) {
Expand Down
Loading
Loading