Skip to content
Draft
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
113 changes: 113 additions & 0 deletions openfeature-provider/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The `ProviderConfig` struct contains all configuration options for the provider:
#### Optional Fields

- `Logger` (*slog.Logger): Custom logger for provider operations. If not provided, a default text logger is created. See [Logging](#logging) for details.
- `StickyResolveStrategy` (StickyResolveStrategy): Strategy for handling sticky resolve scenarios. Defaults to `RemoteResolverFallback`. See [Sticky Resolve](#sticky-resolve) for details.
- `ConnFactory` (func): Custom gRPC connection factory for advanced use cases (e.g., custom interceptors, TLS configuration)

#### Advanced: Testing with Custom State Provider
Expand All @@ -102,6 +103,118 @@ provider, err := confidence.NewProviderWithStateProvider(ctx,

**Important**: This configuration disables automatic state fetching and exposure logging. For production deployments, always use `NewProvider()` with `ProviderConfig`.

## Sticky Resolve

Sticky Resolve ensures users receive the same variant throughout an experiment, even if their targeting attributes change or you pause new assignments.

**Two main use cases:**
1. **Consistent experience** - User moves countries but keeps the same variant
2. **Pause intake** - Stop new assignments while maintaining existing ones

### Default: Server-Side Storage (RemoteResolverFallback)

By default, the provider uses `RemoteResolverFallback` which automatically handles sticky assignments via network calls to Confidence when needed.

**Flow:**
1. Local WASM resolver attempts to resolve
2. If sticky data needed → network call to Confidence
3. Confidence checks its sticky repository, returns variant
4. Assignment stored server-side with 90-day TTL (auto-renewed on access)

**Server-side configuration (in Confidence UI):**
- Optionally skip targeting criteria for sticky assignments
- Pause/resume new entity intake
- Automatic TTL management

### Custom: Local Storage (MaterializationRepository)

Implement `MaterializationRepository` to store assignments locally and eliminate network calls:

```go
type MaterializationRepository interface {
StickyResolveStrategy

// LoadMaterializedAssignmentsForUnit loads assignments for a unit.
// Returns a map of materialization name to MaterializationInfo.
LoadMaterializedAssignmentsForUnit(ctx context.Context, unit, materialization string) (map[string]*MaterializationInfo, error)

// StoreAssignment stores materialization assignments for a unit.
StoreAssignment(ctx context.Context, unit string, assignments map[string]*MaterializationInfo) error
}

type MaterializationInfo struct {
// UnitInMaterialization indicates if the unit exists in the materialization
UnitInMaterialization bool
// RuleToVariant maps rule IDs to their assigned variant names
RuleToVariant map[string]string
}
```

### Example: In-Memory Repository

```go
type InMemoryMaterializationRepository struct {
mu sync.RWMutex
storage map[string]map[string]*confidence.MaterializationInfo // unit -> materialization -> info
}

func (r *InMemoryMaterializationRepository) LoadMaterializedAssignmentsForUnit(
ctx context.Context, unit, materialization string,
) (map[string]*confidence.MaterializationInfo, error) {
r.mu.RLock()
defer r.mu.RUnlock()

if unitData, ok := r.storage[unit]; ok {
result := make(map[string]*confidence.MaterializationInfo)
for k, v := range unitData {
result[k] = v
}
return result, nil
}
return make(map[string]*confidence.MaterializationInfo), nil
}

func (r *InMemoryMaterializationRepository) StoreAssignment(
ctx context.Context, unit string, assignments map[string]*confidence.MaterializationInfo,
) error {
r.mu.Lock()
defer r.mu.Unlock()

if r.storage == nil {
r.storage = make(map[string]map[string]*confidence.MaterializationInfo)
}
if r.storage[unit] == nil {
r.storage[unit] = make(map[string]*confidence.MaterializationInfo)
}

for k, v := range assignments {
r.storage[unit][k] = v
}
return nil
}

func (r *InMemoryMaterializationRepository) Close() {}
```

**Usage:**

```go
provider, err := confidence.NewProvider(ctx, confidence.ProviderConfig{
APIClientID: "your-api-client-id",
APIClientSecret: "your-api-client-secret",
ClientSecret: "your-client-secret",
StickyResolveStrategy: &InMemoryMaterializationRepository{},
})
```

### Choosing a Strategy

| Strategy | Best For | Trade-offs |
|----------|----------|------------|
| **RemoteResolverFallback** (default) | Most apps | Simple, managed by Confidence. Network calls when needed. |
| **MaterializationRepository** (in-memory) | Single-instance apps, testing | Fast, no network. Lost on restart. |
| **MaterializationRepository** (Redis/DB) | Distributed/Multi instance apps | No network calls. Requires storage infra. |

## Credentials

Get your client secret from your [Confidence dashboard](https://confidence.spotify.com/):
Expand Down
2 changes: 1 addition & 1 deletion openfeature-provider/go/confidence/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,6 @@ func createProviderWithTestState(

// Create provider with the client secret from test state
// The test state includes client secret: mkjJruAATQWjeY7foFIWfVAcBWnci2YF
provider := NewLocalResolverProvider(resolverAPI, stateProvider, logger, "mkjJruAATQWjeY7foFIWfVAcBWnci2YF", slog.New(slog.NewTextHandler(os.Stderr, nil)))
provider := NewLocalResolverProvider(resolverAPI, stateProvider, logger, "mkjJruAATQWjeY7foFIWfVAcBWnci2YF", slog.New(slog.NewTextHandler(os.Stderr, nil)), nil)
return provider, nil
}
Loading