From 691a2d7f99b9de9b6d49b7a0f8d2e20d22610bb5 Mon Sep 17 00:00:00 2001
From: lzrf0cuz <20177103+lzrf0cuz@users.noreply.github.com>
Date: Sun, 14 Dec 2025 16:09:19 -0500
Subject: [PATCH] Complete architectural rewrite with modern SDK support,
production-grade lifecycle management, and critical bug fixes.
See CHANGELOG.md for a detailed list of changes, including:
- Breaking changes (SDK requirements, API changes)
- New features (context-aware lifecycle, event system, observability)
- Bug fixes (ObjectEvaluation, dynamic configuration)
- Concurrency and reliability improvements
- See MIGRATION.md for upgrade instructions from v1.x.
---
.gitignore | 97 +++-
.golangci.yml | 139 +++++
.mockery.yaml | 14 +
CHANGELOG.md | 125 +++++
CHANGES.txt | 4 -
CONTRIBUTING.md | 384 +++++++++++++
CONTRIBUTORS-GUIDE.md | 28 -
MIGRATION.md | 258 +++++++++
README.md | 795 +++++++++++++++++++++++---
Taskfile.yml | 366 ++++++++++++
benchmark_test.go | 166 ++++++
client.go | 26 +
concurrency_test.go | 184 ++++++
config.go | 51 ++
constants.go | 68 +++
doc.go | 42 ++
docs/images/of_banner.png | Bin 0 -> 214563 bytes
evaluation.go | 505 +++++++++++++++++
evaluation_options_test.go | 125 +++++
evaluation_test.go | 959 ++++++++++++++++++++++++++++++++
events.go | 220 ++++++++
examples/cloud/README.md | 61 ++
examples/cloud/main.go | 214 +++++++
examples/localhost/README.md | 79 +++
examples/localhost/main.go | 203 +++++++
examples/localhost/split.yaml | 46 ++
go.mod | 37 +-
go.sum | 158 ++----
helpers.go | 428 ++++++++++++++
lifecycle.go | 490 ++++++++++++++++
lifecycle_edge_cases_test.go | 915 ++++++++++++++++++++++++++++++
lifecycle_test.go | 249 +++++++++
logging.go | 105 ++++
logging_test.go | 363 ++++++++++++
mock_client_test.go | 324 +++++++++++
mock_evaluation_test.go | 574 +++++++++++++++++++
options.go | 111 ++++
options_test.go | 88 +++
provider.go | 416 +++++++-------
provider_test.go | 762 ++++++++++++-------------
split.yaml | 14 -
test/advanced/README.md | 65 +++
test/advanced/main.go | 354 ++++++++++++
test/cloud_flags.yaml | 87 +++
test/integration/README.md | 75 +++
test/integration/evaluations.go | 611 ++++++++++++++++++++
test/integration/lifecycle.go | 538 ++++++++++++++++++
test/integration/main.go | 428 ++++++++++++++
test/integration/results.go | 78 +++
test/integration/sdk.go | 353 ++++++++++++
test/integration/split.yaml | 107 ++++
testdata/split.yaml | 37 ++
track_test.go | 126 +++++
53 files changed, 12204 insertions(+), 848 deletions(-)
create mode 100644 .golangci.yml
create mode 100644 .mockery.yaml
create mode 100644 CHANGELOG.md
delete mode 100644 CHANGES.txt
create mode 100644 CONTRIBUTING.md
delete mode 100644 CONTRIBUTORS-GUIDE.md
create mode 100644 MIGRATION.md
create mode 100644 Taskfile.yml
create mode 100644 benchmark_test.go
create mode 100644 client.go
create mode 100644 concurrency_test.go
create mode 100644 config.go
create mode 100644 constants.go
create mode 100644 doc.go
create mode 100644 docs/images/of_banner.png
create mode 100644 evaluation.go
create mode 100644 evaluation_options_test.go
create mode 100644 evaluation_test.go
create mode 100644 events.go
create mode 100644 examples/cloud/README.md
create mode 100644 examples/cloud/main.go
create mode 100644 examples/localhost/README.md
create mode 100644 examples/localhost/main.go
create mode 100644 examples/localhost/split.yaml
create mode 100644 helpers.go
create mode 100644 lifecycle.go
create mode 100644 lifecycle_edge_cases_test.go
create mode 100644 lifecycle_test.go
create mode 100644 logging.go
create mode 100644 logging_test.go
create mode 100644 mock_client_test.go
create mode 100644 mock_evaluation_test.go
create mode 100644 options.go
create mode 100644 options_test.go
delete mode 100644 split.yaml
create mode 100644 test/advanced/README.md
create mode 100644 test/advanced/main.go
create mode 100644 test/cloud_flags.yaml
create mode 100644 test/integration/README.md
create mode 100644 test/integration/evaluations.go
create mode 100644 test/integration/lifecycle.go
create mode 100644 test/integration/main.go
create mode 100644 test/integration/results.go
create mode 100644 test/integration/sdk.go
create mode 100644 test/integration/split.yaml
create mode 100644 testdata/split.yaml
create mode 100644 track_test.go
diff --git a/.gitignore b/.gitignore
index b986336..35087fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,17 +1,102 @@
-# Binaries for programs and plugins
+# =============================================================================
+# Go
+# =============================================================================
+
+# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
-# Test binary, built with `go test -c`
+# Test binaries
*.test
-# Output of the go coverage tool, specifically when used with LiteIDE
+# Coverage output
*.out
+coverage.out
+
+# Go workspace
+go.work
+go.work.sum
+
+# =============================================================================
+# Project
+# =============================================================================
+
+# Environment files (may contain secrets)
+*.env
+.env.local
+
+# Task runner cache
+.task/
+
+# Split SDK local files
+.split
+.splits
+
+# =============================================================================
+# IDE - JetBrains (GoLand, IntelliJ)
+# =============================================================================
+
+.idea/*
+!.idea/codeStyles/
+!.idea/runConfigurations/
+
+*.iml
+*.ipr
+*.iws
+
+# =============================================================================
+# IDE - VS Code
+# =============================================================================
+
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+.history/
+*.vsix
+
+# =============================================================================
+# OS - macOS
+# =============================================================================
+
+.DS_Store
+.AppleDouble
+.LSOverride
+._*
+.Spotlight-V100
+.Trashes
+
+# =============================================================================
+# OS - Windows
+# =============================================================================
+
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+$RECYCLE.BIN/
+*.lnk
+
+# =============================================================================
+# OS - Linux
+# =============================================================================
+
+*~
+.directory
+.Trash-*
+.nfs*
-# Dependency directories (remove the comment below to include it)
-# vendor/
+# =============================================================================
+# Git
+# =============================================================================
-.idea/
\ No newline at end of file
+*.orig
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..71ada67
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,139 @@
+# golangci-lint configuration for Split OpenFeature Provider
+# Gold Standard Linting Configuration
+
+version: "2"
+
+run:
+ timeout: 5m
+ tests: true
+ modules-download-mode: readonly
+
+linters:
+ enable:
+ # Enabled by default
+ - errcheck # Check for unchecked errors
+ - govet # Go vet
+ - ineffassign # Detect ineffectual assignments
+ - staticcheck # Static analysis
+ - unused # Check for unused code
+
+ # Additional recommended linters
+ - misspell # Check for misspelled words
+ - unconvert # Remove unnecessary type conversions
+ - unparam # Report unused function parameters
+ - prealloc # Find slice declarations that could be preallocated
+ - goconst # Find repeated strings that could be constants
+ - gocyclo # Cyclomatic complexity
+ - gocognit # Cognitive complexity
+ - dupl # Code clone detection
+ - gocritic # Comprehensive checks
+ - revive # Fast, extensible linter
+ - gosec # Security checks
+ - bodyclose # Check HTTP response bodies are closed
+ - noctx # Detect http.Request without context.Context
+ - rowserrcheck # Check sql.Rows.Err is checked
+ - sqlclosecheck # Check sql.Rows and sql.Stmt are closed
+ - errorlint # Error wrapping checks
+ - exhaustive # Check exhaustiveness of enum switch statements
+
+ exclusions:
+ paths:
+ - examples
+ - test
+ - '.*\.pb\.go$'
+
+ rules:
+ # Exclude all linters from test files - focus on production code quality
+ - path: '(.+)_test\.go'
+ linters:
+ - errcheck
+ - gocyclo
+ - gocognit
+ - dupl
+ - gocritic
+ - gosec
+ - goconst
+ - govet
+ - revive
+ - staticcheck
+ - misspell
+ - unconvert
+ - unparam
+ - prealloc
+
+ settings:
+ errcheck:
+ check-type-assertions: true
+ check-blank: true
+
+ govet:
+ enable-all: true
+ disable:
+ - shadow # Too many false positives
+
+ gocyclo:
+ min-complexity: 15
+
+ gocognit:
+ min-complexity: 30
+
+ dupl:
+ threshold: 100
+
+ goconst:
+ min-len: 3
+ min-occurrences: 3
+
+ misspell:
+ locale: US
+
+ staticcheck:
+ checks: [ "all" ]
+
+ revive:
+ confidence: 0.8
+ rules:
+ - name: blank-imports
+ - name: context-as-argument
+ - name: context-keys-type
+ - name: dot-imports
+ - name: error-return
+ - name: error-strings
+ - name: error-naming
+ - name: exported
+ - name: if-return
+ - name: increment-decrement
+ - name: var-declaration
+ - name: package-comments
+ - name: range
+ - name: receiver-naming
+ - name: time-naming
+ - name: unexported-return
+ - name: indent-error-flow
+ - name: errorf
+ - name: empty-block
+ - name: superfluous-else
+ - name: unused-parameter
+ - name: unreachable-code
+ - name: redefines-builtin-id
+
+ gosec:
+ severity: medium
+ confidence: medium
+
+ gocritic:
+ enabled-tags:
+ - diagnostic
+ - performance
+ - style
+ disabled-checks:
+ - commentedOutCode
+ - whyNoLint
+
+ exhaustive:
+ default-signifies-exhaustive: true
+
+ prealloc:
+ simple: true
+ range-loops: true
+ for-loops: false
diff --git a/.mockery.yaml b/.mockery.yaml
new file mode 100644
index 0000000..8a18cc0
--- /dev/null
+++ b/.mockery.yaml
@@ -0,0 +1,14 @@
+# Mockery v3.6.4 configuration
+# See: https://github.com/vektra/mockery/blob/v3/docs/configuration.md
+all: false
+template: testify
+formatter: goimports
+packages:
+ github.com/splitio/split-openfeature-provider-go/v2:
+ interfaces:
+ Client:
+ config:
+ dir: "{{.InterfaceDir}}"
+ filename: "mock_client_test.go"
+ pkgname: "split"
+ structname: "MockClient"
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ae6d59e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,125 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [2.0.0] - 2025-11-24
+
+**Complete architectural rewrite** with modern SDK support, production-grade lifecycle management, and critical bug
+fixes.
+
+See [MIGRATION.md](MIGRATION.md) for upgrade instructions.
+
+### Breaking Changes
+
+#### SDK Requirements
+
+- **Split Go SDK upgraded to v6** (import: `github.com/splitio/go-client/v6`)
+- **OpenFeature Go SDK upgraded to v1** (import: `github.com/open-feature/go-sdk/openfeature`)
+
+#### API Changes
+
+- **All evaluation methods now require `context.Context` as first parameter**
+- **`Client()` renamed to `Factory()`** for Split SDK factory access
+- **`NewWithClient()` constructor removed** - use `New()` instead
+
+#### Behavioral Changes
+
+- **`ObjectEvaluation()` return structure changed**:
+ - v1: Returns treatment string only
+ - v2: Returns `FlagSetResult` (typed struct with `Treatment` and `Config` fields)
+
+### New Features
+
+#### Context-Aware Lifecycle
+
+- `InitWithContext(ctx)` - Context-aware initialization with timeout and cancellation
+- `ShutdownWithContext(ctx)` - Graceful shutdown with timeout and proper cleanup
+- Idempotent initialization with singleflight (prevents concurrent init races)
+- Provider cannot be reused after shutdown (must create new instance)
+
+#### Event System
+
+- OpenFeature event support:
+ - `PROVIDER_READY` - Provider initialized
+ - `PROVIDER_ERROR` - Initialization or runtime errors
+ - `PROVIDER_CONFIGURATION_CHANGED` - Flag definitions updated (detected via 30s polling)
+- Background monitoring (30s interval) for configuration change detection
+
+#### Event Tracking
+
+- `Track()` method implementing OpenFeature Tracker interface
+- Associates feature flag evaluations with user actions for A/B testing and experimentation
+- Supports custom traffic types via `trafficType` attribute in evaluation context
+- Supports event properties via `TrackingEventDetails.Add()`
+- Events viewable in Split Data Hub
+
+#### Per-Request Evaluation Options
+
+- `WithEvaluationMode(ctx, mode)` - Control per-request object evaluation behavior via `context.Context`
+ - `EvaluationModeIndividual` - Evaluate a single flag (useful in cloud mode to bypass flag set evaluation)
+ - `EvaluationModeSet` - Evaluate a flag set (explicit, same as cloud default; ignored in localhost mode)
+ - `EvaluationModeDefault` - Use provider's default behavior (flag set in cloud, individual in localhost)
+- `WithImpressionDisabled(ctx)` - Forward-looking API for per-evaluation impression control (logged, not yet enforced)
+- `WithEvalOptions(ctx, opts)` - Set multiple evaluation options at once
+
+#### Per-Request Track Options
+
+- `WithoutMetricValue(ctx)` - Send nil value to Split for count-only events, preventing pollution of sum/average metrics
+- `WithTrackOptions(ctx, opts)` - Set multiple tracking options at once
+- `GetTrackOptions(ctx)` - Extract tracking options from context
+
+#### Client Interface
+
+- Extracted `Client` interface for dependency injection and mock generation
+- Enables mockery-based mock generation for unit testing without Split SDK
+
+#### Observability
+
+- Structured logging with `log/slog` throughout provider and Split SDK
+- `Metrics()` method for health status and diagnostics
+- Unified logging via `WithLogger()` option
+
+### Bug Fixes
+
+#### Critical Fixes
+
+- **`ObjectEvaluation()` structure**: Now returns `FlagSetResult` with `Treatment` and `Config` fields (was: treatment
+ string only)
+- **Dynamic Configuration**: All config types (objects, primitives, arrays) consistently accessible via
+ `FlagMetadata["value"]`
+- **Dynamic Configuration JSON parsing**: Supports objects, arrays, and primitives (was: limited support)
+- **Evaluation context attributes**: Now passed to Split SDK for targeting rules (was: ignored)
+- **Shutdown resource cleanup**: Properly cleans up goroutines, channels, and SDK clients (was: resource leaks)
+
+#### Error Handling
+
+- **Shutdown timeout errors**: `ShutdownWithContext()` returns `ctx.Err()` when cleanup times out (was: no error
+ indication)
+- **JSON parse warnings**: Malformed Dynamic Configuration logged instead of silent failures
+- **Targeting key validation**: Non-string keys rejected with clear errors (was: silent failures)
+
+#### Concurrency & Reliability
+
+- **Atomic initialization**: Factory, client, and manager ready together (was: race conditions)
+- **Thread-safe health checks**: Eliminated race conditions in `Status()` and `Metrics()`
+- **Event channel lifecycle**: Properly closed during shutdown (was: potential goroutine leaks)
+- **Panic recovery**: Monitoring goroutine recovers from panics and terminates gracefully
+
+## [1.0.1] - 2022-10-14
+
+- Updated to OpenFeature spec v0.5.0 and OpenFeature Go SDK v0.6.0
+
+## [1.0.0] - 2022-10-03
+
+- Initial release
+- OpenFeature spec v0.5.0 compliance
+- OpenFeature Go SDK v0.5.0 support
+
+[2.0.0]: https://github.com/splitio/split-openfeature-provider-go/compare/v1.0.1...v2.0.0
+
+[1.0.1]: https://github.com/splitio/split-openfeature-provider-go/compare/v1.0.0...v1.0.1
+
+[1.0.0]: https://github.com/splitio/split-openfeature-provider-go/releases/tag/v1.0.0
diff --git a/CHANGES.txt b/CHANGES.txt
deleted file mode 100644
index d70bf22..0000000
--- a/CHANGES.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-1.0.0
-- 10/3/2022. Up to date with spec v0.5.0 and go sdk v0.5.0
-1.0.1
-- 10/14/2022. Up to date with spec v0.5.0 and go sdk v0.6.0
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..12aeff0
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,384 @@
+# Contributing to Split OpenFeature Go Provider
+
+We welcome contributions! This guide covers how to build, test, and submit changes.
+
+**Quick Links:**
+
+- [README.md](README.md) - Main documentation
+- [MIGRATION.md](MIGRATION.md) - v1 → v2 migration guide
+- [CHANGELOG.md](CHANGELOG.md) - Version history
+
+---
+
+## Prerequisites
+
+- **Go 1.25.4+**
+- **Task** - [taskfile.dev](https://taskfile.dev)
+- **golangci-lint** - For linting
+
+### Install Task
+
+```bash
+# macOS
+brew install go-task/tap/go-task
+
+# Linux
+sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin
+
+# Via Go
+go install github.com/go-task/task/v3/cmd/task@latest
+```
+
+### Install Development Tools
+
+```bash
+task install-tools # Install golangci-lint and other tools
+task check-tools # Verify installation
+```
+
+---
+
+## Development Workflow
+
+### 1. Fork and Clone
+
+```bash
+git clone https://github.com/YOUR_USERNAME/split-openfeature-provider-go.git
+cd split-openfeature-provider-go
+git remote add upstream https://github.com/splitio/split-openfeature-provider-go.git
+```
+
+### 2. Create Feature Branch
+
+```bash
+git fetch upstream
+git checkout -b feat/your-feature-name upstream/main
+```
+
+### 3. Make Changes
+
+**Run tests first:**
+
+```bash
+task test # Run all tests with race detector
+```
+
+**Make your changes:**
+
+- Add tests for new functionality (use testify/assert)
+- Follow Go idioms and best practices
+- Add godoc comments for exported symbols
+- Keep functions focused and small
+
+**Validate:**
+
+```bash
+task # Run lint + test + coverage
+task pre-commit # Quick pre-commit checks
+```
+
+### 4. Write Tests
+
+**Requirements:**
+
+- Use `testify/assert` or `testify/require` for assertions
+- Maintain >70% coverage (`task coverage-check`)
+- Tests must pass race detector
+- Test both success and error cases
+
+**Example:**
+
+```go
+func TestFeatureName(t *testing.T) {
+ provider, err := setupTestProvider(t)
+ require.NoError(t, err, "Setup failed")
+
+ tests := []struct {
+ name string
+ input string
+ expected string
+ wantErr bool
+ }{
+ {"valid input", "test", "expected", false},
+ {"invalid input", "", "", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := provider.YourMethod(tt.input)
+
+ if tt.wantErr {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expected, result)
+ }
+ })
+ }
+}
+```
+
+### 5. Commit and Push
+
+**Use Conventional Commits:**
+
+```bash
+git commit -m "feat: add new feature"
+git commit -m "fix: resolve bug with shutdown"
+git commit -m "docs: update README examples"
+```
+
+**Types:** `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `chore`
+
+```bash
+git push origin feat/your-feature-name
+```
+
+### 6. Create Pull Request
+
+**PR Checklist:**
+
+- [ ] All tests pass (`task test`)
+- [ ] Linter passes (`task lint`)
+- [ ] Coverage maintained at >70% (`task coverage-check`)
+- [ ] Documentation updated
+- [ ] Godoc comments added
+- [ ] No goroutine leaks
+- [ ] Concurrency safety verified
+
+---
+
+## Testing
+
+### Unit Tests
+
+```bash
+task test # All tests with race detector
+task test-short # Quick test run
+task coverage # View coverage report
+task coverage-check # Verify 70% threshold
+```
+
+### Integration Tests
+
+```bash
+task test-integration # Uses SPLIT_API_KEY if set, otherwise localhost mode
+task test-cloud # Cloud-only features (requires SPLIT_API_KEY)
+```
+
+**Integration Test (`test/integration/`)** - Automated test suite:
+
+- Localhost mode: 85 tests (no API key needed)
+- Cloud mode: 94 tests (requires SPLIT_API_KEY)
+- All evaluation types (boolean, string, int, float, object)
+- Lifecycle management and concurrent evaluations
+- Event handling and dynamic configurations
+
+**Cloud Test (`test/advanced/`)** - Cloud-only features:
+
+- Event tracking (view in Split Data Hub)
+- Configuration change detection
+- Interactive testing for cloud-specific functionality
+
+**Cloud Mode Testing Setup:**
+
+To run integration tests in cloud mode, create the required flags in your Split.io account.
+See `test/cloud_flags.yaml` for the flag definitions:
+
+1. Create 11 flags as documented in `test/cloud_flags.yaml`
+2. Create a flag set named `split_provider_test`
+3. Add `ui_theme` and `api_version` flags to the flag set
+4. Run tests:
+
+```bash
+SPLIT_API_KEY="your-key" task test-integration
+```
+
+**When Are These Tests Executed?**
+
+Neither test suite runs as part of CI (`task ci`). Run manually:
+
+```bash
+# Integration test - localhost mode (no API key)
+task test-integration
+
+# Integration test - cloud mode (requires API key and flags)
+SPLIT_API_KEY="your-key" task test-integration
+
+# Cloud test - cloud mode (requires API key)
+SPLIT_API_KEY="your-key" task test-cloud
+```
+
+**Recommendation:** Run `task test-integration` before submitting PRs that affect:
+
+- Provider initialization/shutdown
+- Flag evaluation logic
+- Event handling
+- Dynamic configuration parsing
+
+---
+
+## Code Quality
+
+### Required Standards
+
+- All exported symbols must have godoc comments
+- golangci-lint must pass
+- Coverage >70%
+- No race conditions
+- No goroutine leaks
+- Thread-safety verified for shared state
+
+### Common Commands
+
+```bash
+# Workflows
+task # Show available tasks
+task check # Run all quality checks
+task pre-commit # Quick pre-commit
+task ci # Full CI suite
+
+# Testing
+task test # Unit tests with race detector
+task test-integration # Integration tests (localhost or cloud)
+task test-cloud # Cloud-only tests (requires API key)
+task coverage # Coverage report
+
+# Code Quality
+task lint # Run linter
+task lint-fix # Auto-fix issues
+task fmt # Format code
+task vet # Run go vet
+
+# Examples
+task example-cloud # Cloud mode (requires SPLIT_API_KEY)
+task example-localhost # Localhost mode (no API key)
+
+# Tools
+task install-tools # Install dev tools
+task clean # Clean artifacts
+```
+
+---
+
+## Project Structure
+
+```
+split-openfeature-provider-go/
+├── provider.go # Core provider
+├── lifecycle.go # Init/Shutdown (context-aware)
+├── events.go # Event system
+├── evaluation.go # Flag evaluations
+├── helpers.go # Helpers and Factory()
+├── logging.go # Slog adapter
+├── constants.go # Constants
+├── provider_test.go # Unit tests
+├── lifecycle_edge_cases_test.go # Concurrency tests
+├── examples/
+│ ├── cloud/ # Cloud mode example
+│ └── localhost/ # Localhost mode example
+└── test/
+ ├── cloud_flags.yaml # Flag definitions for cloud testing
+ ├── integration/ # Integration tests (localhost + cloud)
+ └── advanced/ # Advanced tests (cloud-only features)
+```
+
+---
+
+## v2 Status: Production Ready ✅
+
+- ✅ Context-aware lifecycle with timeouts
+- ✅ Full OpenFeature event compliance
+- ✅ Optimal test coverage with race detection
+- ✅ Structured logging with slog
+- ✅ Thread-safe concurrent operations
+
+---
+
+## Known Limitations & Future Enhancements
+
+### PROVIDER_STALE Event Not Emitted
+
+**Status:** Known limitation (Split SDK dependency)
+
+The provider cannot emit `PROVIDER_STALE` events when network connectivity is lost. This is due to a limitation in the
+Split Go SDK:
+
+- `factory.IsReady()` only indicates **initial** readiness after `BlockUntilReady()` completes
+- The method does **not** change when the SDK loses network connectivity during operation
+- Internally, the SDK handles connectivity issues (switching between streaming and polling modes) but does not expose
+ this state through its public API
+
+**Impact:**
+
+- When network connectivity is lost, the SDK continues serving cached data silently
+- Applications cannot detect when they are receiving potentially stale feature flag values
+- The `PROVIDER_CONFIGURATION_CHANGED` event still works correctly when flags are updated
+
+**Potential Future Enhancement:**
+If the Split SDK exposes streaming/connectivity status in a future version, this provider could be updated to:
+
+1. Monitor the streaming status channel for `StatusUp`/`StatusDown` events
+2. Emit `PROVIDER_STALE` when streaming disconnects and polling begins
+3. Emit `PROVIDER_READY` when streaming reconnects
+
+**Workaround for Applications:**
+Applications requiring staleness awareness should implement application-level health checks, such as:
+
+- Periodic test evaluations with known flags
+- Monitoring SDK debug logs for connectivity errors
+- External health check endpoints to Split.io APIs
+
+**References:**
+
+- Split SDK sync manager: `go-split-commons/synchronizer/manager.go`
+- Push status constants: `StatusUp`, `StatusDown`, `StatusRetryableError`, `StatusNonRetryableError`
+- SSE keepAlive timeout: 70 seconds (hardcoded in SDK)
+
+### PROVIDER_CONFIGURATION_CHANGED Detected via Polling
+
+**Status:** Known limitation (Split SDK dependency)
+
+The `PROVIDER_CONFIGURATION_CHANGED` event is detected by polling, not via real-time SSE streaming. The polling interval
+is configurable via `WithMonitoringInterval` (default: 30 seconds, minimum: 5 seconds).
+
+**Why Polling?**
+
+- The Split SDK receives configuration changes instantly via SSE streaming
+- However, the SDK does **not** expose a callback or event for configuration changes
+- The only way to detect changes is by polling `manager.Splits()` and comparing `ChangeNumber` values
+
+**Impact:**
+
+- Flag evaluations reflect changes immediately (SDK updates its cache via SSE)
+- `PROVIDER_CONFIGURATION_CHANGED` events have latency up to the configured monitoring interval
+- Applications relying on this event for cache invalidation may see delayed notifications
+
+**Potential Future Enhancement:**
+If the Split SDK exposes a configuration change callback in a future version, this provider could be updated to:
+
+1. Register a callback for real-time change notifications
+2. Emit `PROVIDER_CONFIGURATION_CHANGED` immediately when changes arrive via SSE
+3. Remove the polling-based detection
+
+---
+
+## Resources
+
+**Documentation:**
+
+- [OpenFeature Specification](https://openfeature.dev/specification/sections/providers)
+- [OpenFeature Go SDK](https://openfeature.dev/docs/reference/sdks/server/go/)
+- [Split Go SDK](https://github.com/splitio/go-client)
+- [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
+
+**Help:**
+
+- [GitHub Issues](https://github.com/splitio/split-openfeature-provider-go/issues) - Bug reports and feature requests
+- [Pull Requests](https://github.com/splitio/split-openfeature-provider-go/pulls) - Contributions
+
+---
+
+## License
+
+By contributing, you agree your contributions will be licensed under Apache License 2.0.
diff --git a/CONTRIBUTORS-GUIDE.md b/CONTRIBUTORS-GUIDE.md
deleted file mode 100644
index b5653a6..0000000
--- a/CONTRIBUTORS-GUIDE.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Contributing to the Split OpenFeature Provider
-
-The Split Provider is an open source project and we welcome feedback and contribution. The information below describes how to build the project with your changes, run the tests, and send the Pull Request(PR).
-
-## Development
-
-### Development process
-
-1. Fork the repository and create a topic branch from `development` branch. Please use a descriptive name for your branch.
-2. While developing, use descriptive messages in your commits. Avoid short or meaningless sentences like "fix bug".
-3. Make sure to add tests for both positive and negative cases.
-4. Run the build script and make sure it runs with no errors.
-5. Run all tests and make sure there are no failures.
-6. `git push` your changes to GitHub within your topic branch.
-7. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository.
-8. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project.
-9. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`.
-10. Keep an eye out for any feedback or comments from the Split team.
-
-### Building the Split Provider
-- `go build`
-
-### Running tests
-- `go test`
-
-# Contact
-
-If you have any other questions or need to contact us directly in a private manner send us a note at sdks@split.io
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..a2ea07f
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,258 @@
+# Migration Guide: v1 to v2
+
+## Overview
+
+Version 2.0.0 includes critical bug fixes and SDK upgrades.
+
+### Bug Fixes
+
+- `ObjectEvaluation()` returns structured map with treatment and config fields
+- Dynamic Configuration supports any JSON type (objects, arrays, primitives)
+- Evaluation context attributes passed to Split SDK for targeting rules
+- `Shutdown()` properly cleans up all resources
+- Non-string targeting keys validated and rejected
+
+### SDK Updates
+
+- Split Go SDK updated to v6
+- OpenFeature Go SDK updated to v1
+- Go minimum version: 1.25
+
+## Breaking Changes
+
+### Import Paths
+
+```go
+// v1
+import (
+ "github.com/splitio/go-client/splitio/client"
+ "github.com/open-feature/go-sdk/pkg/openfeature"
+)
+
+// v2
+import (
+ "github.com/splitio/go-client/v6/splitio/client"
+ "github.com/open-feature/go-sdk/openfeature"
+)
+```
+
+### Provider Initialization
+
+Use `SetProviderWithContextAndWait()` for synchronous initialization with timeout:
+
+```go
+// v1
+openfeature.SetProvider(provider)
+
+// v2 - Recommended with context and timeout
+ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+defer cancel()
+
+err := openfeature.SetProviderWithContextAndWait(ctx, provider)
+if err != nil {
+ log.Fatal(err)
+}
+
+// v2 - Alternative: No timeout (uses default from BlockUntilReady config)
+err = openfeature.SetProviderAndWait(provider)
+if err != nil {
+ log.Fatal(err)
+}
+```
+
+### Context Required
+
+```go
+// v1
+result, _ := client.BooleanValue(nil, "flag-key", false, evalCtx)
+
+// v2
+ctx := context.Background()
+result, _ := client.BooleanValue(ctx, "flag-key", false, evalCtx)
+```
+
+## Migration Steps
+
+### 1. Update Dependencies
+
+```bash
+go get github.com/splitio/split-openfeature-provider-go/v2@latest
+go get github.com/splitio/go-client/v6@latest
+go get github.com/open-feature/go-sdk@latest
+go mod tidy
+```
+
+### 2. Update Imports
+
+```go
+import (
+ "context"
+
+ "github.com/open-feature/go-sdk/openfeature"
+ "github.com/splitio/split-openfeature-provider-go/v2"
+)
+```
+
+### 3. Update Initialization
+
+```go
+provider, err := split.New(apiKey)
+if err != nil {
+ log.Fatal(err)
+}
+
+// Defer shutdown with context
+defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ if err := openfeature.ShutdownWithContext(ctx); err != nil {
+ log.Printf("Shutdown error: %v", err)
+ }
+}()
+
+// Initialize with context and timeout
+ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+defer cancel()
+
+err = openfeature.SetProviderWithContextAndWait(ctx, provider)
+if err != nil {
+ log.Fatal(err)
+}
+
+client := openfeature.NewClient("my-app")
+```
+
+### 4. Add Context to Evaluations
+
+```go
+ctx := context.Background()
+evalCtx := openfeature.NewEvaluationContext("user-123", map[string]any{
+ "email": "user@example.com",
+})
+result, _ := client.BooleanValue(ctx, "my-feature", false, evalCtx)
+```
+
+## Behavioral Changes
+
+### Dynamic Configurations
+
+v1 returned treatment name. v2 returns structured map with treatment and config:
+
+```go
+result, _ := client.ObjectValue(ctx, "my-flag", split.FlagSetResult{}, evalCtx)
+// v1: "on" (treatment only)
+// v2: {"my-flag": {"treatment": "on", "config": {"feature": "enabled", "limit": 100}}}
+
+// Dynamic Configuration is accessible via FlagMetadata["value"]
+details, _ := client.StringValueDetails(ctx, "my-flag", "default", evalCtx)
+if configValue, ok := details.FlagMetadata["value"]; ok {
+ // All config types wrapped in "value" key for consistent access
+ // Object: configValue.(map[string]any)
+ // Primitive: configValue.(float64), configValue.(string), etc.
+ // Array: configValue.([]any)
+}
+```
+
+### Targeting Rules
+
+v1 ignored evaluation context attributes. v2 passes them correctly:
+
+```go
+evalCtx := openfeature.NewEvaluationContext("user-123", map[string]any{
+ "plan": "premium",
+})
+result, _ := client.BooleanValue(ctx, "premium-feature", false, evalCtx)
+// v1: attributes ignored
+// v2: targeting rules work
+```
+
+### Logging
+
+v1 used plain text logs. v2 uses structured JSON logs with `slog`.
+
+## New Features
+
+### Custom Logger
+
+```go
+logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
+ Level: slog.LevelInfo,
+}))
+slog.SetDefault(logger)
+```
+
+### Health Check
+
+```go
+metrics := provider.Metrics()
+```
+
+### Factory Access
+
+```go
+factory := provider.Factory()
+manager := factory.Manager()
+```
+
+## Compatibility
+
+| Component | v1.x | v2.x |
+|-----------------|-------|-------|
+| Go Version | 1.19+ | 1.25+ |
+| Split SDK | v5/v6 | v6 |
+| OpenFeature SDK | v0 | v1 |
+
+## Complete Example
+
+### v1
+
+```go
+import (
+ "github.com/open-feature/go-sdk/pkg/openfeature"
+ "github.com/splitio/split-openfeature-provider-go"
+)
+
+provider, _ := split.NewProviderSimple("YOUR_API_KEY")
+openfeature.SetProvider(provider)
+client := openfeature.NewClient("my-app")
+
+evalCtx := openfeature.NewEvaluationContext("user-123", nil)
+result, _ := client.BooleanValue(nil, "my-feature", false, evalCtx)
+```
+
+### v2
+
+```go
+import (
+ "context"
+ "log"
+ "time"
+
+ "github.com/open-feature/go-sdk/openfeature"
+ "github.com/splitio/split-openfeature-provider-go/v2"
+)
+
+provider, err := split.New("YOUR_API_KEY")
+if err != nil {
+ log.Fatal(err)
+}
+
+defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ openfeature.ShutdownWithContext(ctx)
+}()
+
+ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+defer cancel()
+
+err = openfeature.SetProviderWithContextAndWait(ctx, provider)
+if err != nil {
+ log.Fatal(err)
+}
+
+client := openfeature.NewClient("my-app")
+
+evalCtx := openfeature.NewEvaluationContext("user-123", nil)
+result, _ := client.BooleanValue(context.Background(), "my-feature", false, evalCtx)
+```
diff --git a/README.md b/README.md
index 25b6945..a164961 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,759 @@
-# Split OpenFeature Provider for Go
-[](https://twitter.com/intent/follow?screen_name=splitsoftware)
+
+
+

+
+# Split OpenFeature Go Provider
+
+[](https://goreportcard.com/report/github.com/splitio/split-openfeature-provider-go)
+[](https://github.com/splitio/split-openfeature-provider-go)
+[](https://pkg.go.dev/github.com/splitio/split-openfeature-provider-go/v2)
+
+**OpenFeature Go Provider for Split.io**
+
+[Installation](#installation) • [Usage](#usage) • [Examples](#examples) • [API](#api) • [Contributing](#contributing)
+
+
+
+---
## Overview
-This Provider is designed to allow the use of OpenFeature with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience.
-## Compatibility
-This SDK is compatible with Go 1.19 and higher.
+OpenFeature provider for Split.io enabling feature flag evaluation through the OpenFeature SDK with support for
+attribute-based targeting and flag metadata (JSON configurations attached to treatments).
+
+## Features
-## Getting started
-Below is a simple example that describes the instantiation of the Split Provider. Please see the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api) for details on how to use the OpenFeature SDK.
+- All OpenFeature flag types (boolean, string, number, object)
+- Event tracking for experimentation and analytics
+- Attribute-based targeting and flag metadata
+- Configuration change detection via background monitoring
+- Thread-safe concurrent evaluations
+- Structured logging via `slog`
+
+## Installation
+
+```bash
+go get github.com/splitio/split-openfeature-provider-go/v2
+go get github.com/open-feature/go-sdk
+go get github.com/splitio/go-client/v6
+```
+
+## Usage
+
+### Basic Setup
```go
import (
- "github.com/open-feature/go-sdk/pkg/openfeature"
- splitProvider "github.com/splitio/split-openfeature-provider-go"
+ "context"
+ "time"
+
+ "github.com/open-feature/go-sdk/openfeature"
+ "github.com/splitio/split-openfeature-provider-go/v2"
)
-provider, err := splitProvider.NewProviderSimple("YOUR_SDK_TYPE_API_KEY")
+provider, err := split.New("YOUR_API_KEY")
if err != nil {
- // Provider creation error
+ log.Fatal(err)
}
-openfeature.SetProvider(provider)
+defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ if err := openfeature.ShutdownWithContext(ctx); err != nil {
+ log.Printf("Shutdown error: %v", err)
+ }
+}()
+
+ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+defer cancel()
+
+if err := openfeature.SetProviderWithContextAndWait(ctx, provider); err != nil {
+ log.Fatal(err)
+}
+
+client := openfeature.NewClient("my-app")
```
-If you are more familiar with Split or want access to other initialization options, you can provide a `SplitClient` to the constructor. See the [Split Go SDK Documentation](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK#initialization) for more information.
+### Advanced Setup
+
```go
-import (
- "github.com/open-feature/go-sdk/pkg/openfeature"
- "github.com/splitio/go-client/v6/splitio/client"
- "github.com/splitio/go-client/v6/splitio/conf"
- splitProvider "github.com/splitio/split-openfeature-provider-go"
-)
+import "github.com/splitio/go-client/v6/splitio/conf"
cfg := conf.Default()
-factory, err := client.NewSplitFactory("YOUR_SDK_TYPE_API_KEY", cfg)
-if err != nil {
- // SDK initialization error
+cfg.BlockUntilReady = 15 // Default is 10 seconds
+
+provider, err := split.New("YOUR_API_KEY", split.WithSplitConfig(cfg))
+```
+
+See [examples](./examples/) for complete configuration patterns including logging setup.
+
+### Server-Side Evaluation Pattern
+
+In server-side SDKs, create client once at startup, then evaluate per-request with transaction-specific context:
+
+```go
+// Application startup - create client once
+client := openfeature.NewClient("my-app")
+
+// Per-request handler
+func handleRequest(w http.ResponseWriter, r *http.Request) {
+ // Create evaluation context with targeting key and attributes
+ evalCtx := openfeature.NewEvaluationContext("user-123", map[string]any{
+ "email": "user@example.com",
+ "plan": "premium",
+ })
+
+ // Option 1: Pass evaluation context directly to each call
+ enabled, _ := client.BooleanValue(r.Context(), "new-feature", false, evalCtx)
+
+ // Option 2: Use transaction context propagation (set once, use throughout request)
+ ctx := openfeature.WithTransactionContext(r.Context(), evalCtx)
+ enabled, _ = client.BooleanValue(ctx, "new-feature", false, openfeature.EvaluationContext{})
+ theme, _ := client.StringValue(ctx, "ui-theme", "light", openfeature.EvaluationContext{})
}
+```
-splitClient := factory.Client()
+**Required:** Targeting key in evaluation context.
-err = splitClient.BlockUntilReady(10)
-if err != nil {
- // SDK timeout error
+**Transaction context:** Use `openfeature.WithTransactionContext()` to embed evaluation context in `context.Context`
+once, then reuse across multiple evaluations.
+
+### Domain-Specific Providers
+
+Use named providers for multi-tenant or service-isolated configurations:
+
+```go
+defer func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ openfeature.ShutdownWithContext(ctx)
+}()
+
+ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+defer cancel()
+
+tenant1Provider, _ := split.New("TENANT_1_API_KEY")
+openfeature.SetNamedProviderWithContextAndWait(ctx, "tenant-1", tenant1Provider)
+
+tenant2Provider, _ := split.New("TENANT_2_API_KEY")
+openfeature.SetNamedProviderWithContextAndWait(ctx, "tenant-2", tenant2Provider)
+
+// Create clients for each named provider domain
+client1 := openfeature.NewClient("tenant-1")
+client2 := openfeature.NewClient("tenant-2")
+```
+
+### Lifecycle Management
+
+#### Context-Aware Initialization
+
+The provider supports context-aware initialization with timeout and cancellation:
+
+```go
+// Initialization with context (recommended)
+ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+defer cancel()
+
+if err := openfeature.SetProviderWithContextAndWait(ctx, provider); err != nil {
+ log.Fatal(err)
}
+```
-provider, err := splitProvider.NewProvider(*splitClient)
-if err != nil {
- // Provider creation error
+**Key Behaviors:**
+
+- Respects context deadline (returns error if timeout exceeded)
+- Cancellable via context cancellation
+- Idempotent - safe to call multiple times (fast path if already initialized)
+- Thread-safe - concurrent Init calls use singleflight (only one initialization happens)
+
+#### Graceful Shutdown with Timeout
+
+Shutdown is a graceful best-effort operation that returns an error if cleanup doesn't complete within the context
+deadline:
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+defer cancel()
+
+if err := openfeature.ShutdownWithContext(ctx); err != nil {
+ // Error means cleanup timed out, but provider is still logically shut down
+ log.Printf("Shutdown timeout: %v (cleanup continuing in background)", err)
+}
+```
+
+**Shutdown Behavior:**
+
+The provider is **immediately** marked as shut down (all new operations fail with `PROVIDER_NOT_READY`), then cleanup
+happens within the context deadline:
+
+1. **Within Deadline:** Complete cleanup, return `nil`
+2. **After Deadline:** Log warnings, return `ctx.Err()` (context.DeadlineExceeded), continue cleanup in background
+
+**Return Values:**
+
+- `nil` - shutdown completed successfully within timeout
+- `context.DeadlineExceeded` - cleanup timed out (provider still logically shut down)
+- `context.Canceled` - context was cancelled (provider still logically shut down)
+
+**Cleanup Timing:**
+
+- Event channel close: Immediate
+- Monitoring goroutine: Up to 30 seconds to terminate
+- Split SDK Destroy: Up to 1 hour in streaming mode (known SDK limitation)
+
+**Recommended Timeout:** 30 seconds minimum to allow monitoring goroutine to exit cleanly.
+
+**Important:** Even when an error is returned, the provider is logically shut down:
+
+- Provider state is atomically set to "shut down" immediately
+- All new operations (Init, evaluations) will fail with PROVIDER_NOT_READY
+- Background cleanup continues safely even after error is returned
+
+#### Provider Reusability
+
+**Important:** Once shut down, a provider instance cannot be reused. Attempting to initialize after shutdown returns an
+error:
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+defer cancel()
+_ = provider.ShutdownWithContext(ctx)
+
+// This will fail with error: "cannot initialize provider after shutdown"
+initCtx, initCancel := context.WithTimeout(context.Background(), 15*time.Second)
+defer initCancel()
+err := openfeature.SetProviderWithContextAndWait(initCtx, provider)
+```
+
+To use a provider again after shutdown, create a new instance:
+
+```go
+newProvider, _ := split.New("YOUR_API_KEY")
+```
+
+#### Thread Safety Guarantees
+
+The provider is fully thread-safe with the following guarantees:
+
+- **Concurrent Evaluations:** Multiple goroutines can safely call evaluation methods simultaneously
+- **Evaluation During Shutdown:** In-flight evaluations complete safely before client destruction
+- **Concurrent Init Calls:** Multiple Init calls use singleflight - only one initialization happens
+- **Status Consistency:** Status() and Metrics() return consistent atomic state even during transitions
+- **Factory Access:** Factory() can be called safely during concurrent operations
+
+### Provider Status
+
+The provider follows OpenFeature's state lifecycle with the following states:
+
+| State | When It Occurs | Evaluations Behavior | Status() Returns |
+|--------------|-------------------------------------------------|-----------------------------------|--------------------|
+| **NotReady** | After `New()`, before `Init()` completes | Return `PROVIDER_NOT_READY` error | `of.NotReadyState` |
+| **Ready** | After successful `Init()` / `BlockUntilReady()` | Execute normally with Split SDK | `of.ReadyState` |
+| **NotReady** | After `Shutdown()` called | Return `PROVIDER_NOT_READY` error | `of.NotReadyState` |
+
+**State Transitions:**
+
+```
+New() → NotReady
+ ↓
+Init() → Ready (if SDK becomes ready)
+ ↓
+ └─→ NotReady (if Shutdown() called)
+ ↓
+ [Terminal State - Cannot re-initialize]
+```
+
+**Important Notes:**
+
+- Once `Shutdown()` is called, the provider **cannot be re-initialized** - create a new instance instead
+- `Init()` can fail due to timeout, invalid API key, or shutdown during initialization
+- State transitions emit OpenFeature events (`PROVIDER_READY`, `PROVIDER_ERROR`, `PROVIDER_CONFIGURATION_CHANGED`)
+
+**Staleness Detection Limitation:**
+The Split SDK's `IsReady()` method only indicates initial readiness and does not change when network connectivity is
+lost. The SDK handles connectivity issues internally (switching between streaming and polling modes) but does not expose
+this state. As a result, `PROVIDER_STALE` events are not emitted. When connectivity is lost, the SDK continues serving
+cached data silently. See [CONTRIBUTING.md](CONTRIBUTING.md) for details on this limitation.
+
+**Check provider readiness:**
+
+```go
+// Check via client (works for both default and named providers)
+client := openfeature.NewClient("my-app") // or named domain like "tenant-1"
+if client.State() == openfeature.ReadyState {
+ // Provider ready for evaluations
+}
+
+// Get provider metadata
+metadata := client.Metadata()
+domain := metadata.Domain() // Client's domain name
+```
+
+**For diagnostics and monitoring:**
+
+```go
+// Provider-specific health metrics
+metrics := provider.Metrics()
+// Returns map with: provider, initialized, status, splits_count, ready
+```
+
+### Known Limitations
+
+**Context Cancellation During Evaluation**
+
+Evaluation methods (`BooleanValue`, `StringValue`, etc.) accept a `context.Context` parameter but **cannot cancel
+in-flight evaluations**. This is because the underlying Split SDK's `TreatmentWithConfig()` method does not support
+context cancellation.
+
+**Impact:**
+
+- Context cancellation/timeout is only checked **before** calling the Split SDK
+- Once evaluation starts, it runs to completion even if context expires
+- In localhost mode: evaluations are fast (~microseconds), low risk
+- In cloud mode: evaluations read from cache, typically <1ms, but network issues could cause delays
+
+**Affected operations:**
+
+- ✅ `InitWithContext` - respects context cancellation
+- ✅ `ShutdownWithContext` - respects context timeout
+- ❌ Flag evaluations - cannot cancel once started
+
+**Workarounds:**
+
+```go
+// Option 1: Use HTTP-level timeouts (recommended)
+cfg := conf.Default()
+cfg.Advanced.HTTPTimeout = 5 * time.Second
+
+// Option 2: Set aggressive evaluation context timeout
+ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+defer cancel()
+// Note: timeout only applies BEFORE evaluation starts
+value, err := client.BooleanValue(ctx, "flag", false, evalCtx)
+```
+
+**Per-Evaluation Impression Control (Forward-Looking API)**
+
+`WithImpressionDisabled()` and `EvalOptions.ImpressionDisabled` are provided as forward-looking API surface for
+per-evaluation impression control. The Split Go SDK does not currently support disabling impressions on individual
+evaluations - only SDK-level impression modes (`OPTIMIZED`/`DEBUG`/`NONE`) configured at initialization time.
+
+When `ImpressionDisabled` is set, the provider logs a one-time info message per provider instance but does not enforce
+the setting. This API will become functional when the Split Go SDK adds per-evaluation impression support.
+
+**Split SDK Destroy() Blocking (Streaming Mode)**
+
+In cloud/streaming mode, the Split SDK's `Destroy()` method can block for up to 1 hour due to SSE connection handling.
+This is a known Split SDK limitation tracked
+in [splitio/go-client#243](https://github.com/splitio/go-client/issues/243).
+
+**Impact:** During shutdown, cleanup may continue in background if context timeout expires. The provider is logically
+shut down immediately (all new operations return defaults), only cleanup may be delayed.
+
+**Mitigation:** Use appropriate shutdown timeout (30s recommended):
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+defer cancel()
+openfeature.ShutdownWithContext(ctx)
+```
+
+## Examples
+
+Complete working examples with detailed code:
+
+- **[localhost/](./examples/localhost/)** - Local development mode (YAML file, no API key required)
+- **[cloud/](./examples/cloud/)** - Cloud mode with streaming updates and all flag types
+- **[test/integration/](./test/integration/)** - Comprehensive integration test suite
+
+Run examples:
+
+```bash
+task example-localhost # No API key needed
+task example-cloud # Requires SPLIT_API_KEY
+task test-integration # Full integration tests
+```
+
+## API
+
+### Flag Evaluation
+
+All methods require targeting key in evaluation context:
+
+```go
+ctx := context.Background()
+evalCtx := openfeature.NewEvaluationContext("user-123", map[string]any{
+ "email": "user@example.com",
+ "plan": "premium",
+})
+
+// Boolean
+enabled, err := client.BooleanValue(ctx, "new-feature", false, evalCtx)
+
+// String
+theme, err := client.StringValue(ctx, "ui-theme", "light", evalCtx)
+
+// Number
+maxRetries, err := client.IntValue(ctx, "max-retries", 3, evalCtx)
+discount, err := client.FloatValue(ctx, "discount-rate", 0.0, evalCtx)
+
+// Object
+result, err := client.ObjectValue(ctx, "flag-key", split.FlagSetResult{}, evalCtx)
+```
+
+### Object Evaluation - Mode-Specific Behavior
+
+Object evaluation returns `split.FlagSetResult` in both modes:
+
+```go
+type FlagResult struct {
+ Config any // Parsed JSON config, or nil
+ Treatment string // Split treatment name (e.g., "on", "off", "v1")
+}
+
+type FlagSetResult map[string]FlagResult
+```
+
+**Cloud Mode** (default: flag set evaluation):
+
+```go
+// Default behavior: treats "my-flag-set" as a flag set name
+result, _ := client.ObjectValue(ctx, "my-flag-set", split.FlagSetResult{}, evalCtx)
+flags := result.(split.FlagSetResult)
+for name, flag := range flags {
+ fmt.Printf("%s: %s\n", name, flag.Treatment)
+}
+
+// Override: evaluate a single flag instead of a flag set
+individualCtx := split.WithEvaluationMode(ctx, split.EvaluationModeIndividual)
+result, _ = client.ObjectValue(individualCtx, "single-flag", split.FlagSetResult{}, evalCtx)
+```
+
+**Localhost Mode** (always individual flag evaluation):
+
+```go
+result, _ := client.ObjectValue(ctx, "single-flag", split.FlagSetResult{}, evalCtx)
+flags := result.(split.FlagSetResult)
+flag := flags["single-flag"]
+fmt.Println(flag.Treatment, flag.Config)
+```
+
+**Note:** Flag sets are NOT supported in localhost mode. `EvaluationModeSet` is silently ignored (always uses individual
+evaluation). See [Evaluation Options](#evaluation-options) for details.
+
+### Evaluation Options
+
+Control per-request evaluation behavior via `context.Context`:
+
+```go
+// Force individual flag evaluation in cloud mode (instead of flag set)
+individualCtx := split.WithEvaluationMode(ctx, split.EvaluationModeIndividual)
+result, _ := client.ObjectValue(individualCtx, "single-flag", split.FlagSetResult{}, evalCtx)
+
+// Force flag set evaluation (explicit, same as cloud default)
+setCtx := split.WithEvaluationMode(ctx, split.EvaluationModeSet)
+result, _ = client.ObjectValue(setCtx, "my-flag-set", split.FlagSetResult{}, evalCtx)
+
+// Set multiple evaluation options at once
+optionsCtx := split.WithEvalOptions(ctx, split.EvalOptions{
+ Mode: split.EvaluationModeIndividual,
+ ImpressionDisabled: true, // Forward-looking API (see note below)
+})
+```
+
+**Evaluation Modes:**
+
+| Mode | Cloud Behavior | Localhost Behavior |
+|-------------------------------|-------------------------|------------------------|
+| `EvaluationModeDefault` (`""`)| Flag set evaluation | Individual evaluation |
+| `EvaluationModeSet` | Flag set evaluation | Ignored (individual) |
+| `EvaluationModeIndividual` | Individual evaluation | Individual evaluation |
+
+**Note:** `EvaluationModeSet` is silently ignored in localhost mode because the Split SDK's localhost mode does not
+support flag sets. A debug-level log message is emitted when this occurs.
+
+**ImpressionDisabled** (forward-looking API): The `WithImpressionDisabled()` helper and `ImpressionDisabled` field are
+provided for future compatibility with Split SDK per-evaluation impression control. Currently logged but not enforced.
+See [Known Limitations](#known-limitations).
+
+### Extracting Configuration Metadata
+
+All `*ValueDetails` methods return evaluation metadata including flag metadata:
+
+```go
+details, err := client.StringValueDetails(ctx, "ui-theme", "light", evalCtx)
+
+// Standard fields
+value := details.Value // Evaluated value: "dark" (for strings, same as treatment)
+treatment := details.Variant // Split treatment name: "dark", "light", etc.
+reason := details.Reason // TARGETING_MATCH, DEFAULT, ERROR
+
+// Extract flag metadata (configurations attached to treatments)
+// All config types are wrapped in FlagMetadata["value"] for consistency
+if configValue, ok := details.FlagMetadata["value"]; ok {
+ // Object config: {"bgColor": "#000", "fontSize": 14}
+ if configMap, ok := configValue.(map[string]any); ok {
+ bgColor := configMap["bgColor"]
+ fontSize := configMap["fontSize"]
+ }
+ // Primitive config: 42
+ if num, ok := configValue.(float64); ok {
+ // Use primitive value
+ }
+ // Array config: ["a", "b", "c"]
+ if arr, ok := configValue.([]any); ok {
+ // Use array
+ }
}
-openfeature.SetProvider(provider)
```
-## Use of OpenFeature with Split
-After the initial setup you can use OpenFeature according to their [documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api/).
+### Evaluation Reasons
+
+| Reason | Description |
+|-------------------|---------------------------------------------------------------------------|
+| `TARGETING_MATCH` | Flag successfully evaluated |
+| `DEFAULT` | Flag not found, returned default value |
+| `ERROR` | Evaluation error (missing targeting key, provider not ready, parse error) |
+
+### Error Codes
+
+Provider implements OpenFeature error codes. All errors return default value:
+
+- `PROVIDER_NOT_READY` - Provider not initialized
+- `FLAG_NOT_FOUND` - Flag doesn't exist in Split
+- `PARSE_ERROR` - Treatment can't parse to requested type
+- `TARGETING_KEY_MISSING` - No targeting key in context
+- `INVALID_CONTEXT` - Malformed evaluation context
+- `GENERAL` - Context canceled/timeout or other errors
+
+### Default Value Behavior
+
+OpenFeature's design philosophy: **evaluations never return Go errors**. Instead, they return the default value you
+provide with resolution details indicating what happened.
+
+**When Split SDK Returns "control" Treatment:**
+
+The Split SDK returns a special `"control"` treatment to indicate evaluation failure. Our provider translates this to
+OpenFeature's default value pattern:
+
+| Condition | Split SDK Returns | Caller Receives | Resolution Details |
+|----------------------------|-------------------|-----------------|---------------------------------------------------|
+| Flag doesn't exist | `"control"` | Default value | `Reason: DEFAULT`
`Error: FLAG_NOT_FOUND` |
+| Provider not initialized | `"control"` | Default value | `Reason: ERROR`
`Error: PROVIDER_NOT_READY` |
+| Provider shut down | `"control"` | Default value | `Reason: ERROR`
`Error: PROVIDER_NOT_READY` |
+| Targeting key missing | `"control"` | Default value | `Reason: ERROR`
`Error: TARGETING_KEY_MISSING` |
+| Context canceled | `"control"` | Default value | `Reason: ERROR`
`Error: GENERAL` |
+| Network error (cloud mode) | `"control"` | Default value | `Reason: DEFAULT`
`Error: FLAG_NOT_FOUND` |
+
+**Example:**
+
+```go
+// Flag doesn't exist in Split
+enabled, err := client.BooleanValue(ctx, "nonexistent-flag", false, evalCtx)
+// Result:
+// - enabled = false (your default value)
+// - err = nil (OpenFeature doesn't return errors)
+
+// To check what happened, use *ValueDetails methods:
+details, err := client.BooleanValueDetails(ctx, "nonexistent-flag", false, evalCtx)
+// - details.Value = false
+// - details.Reason = of.DefaultReason
+// - details.ErrorCode = of.FlagNotFoundCode
+// - details.ErrorMessage = "flag not found"
+```
+
+**Key Points:**
+
+- Your application continues running normally with safe default values
+- No panic, no nil pointers, no error handling required for normal operation
+- Use `*ValueDetails` methods when you need to distinguish between success and fallback
+- This design enables graceful degradation during outages or misconfigurations
+
+### Event Tracking
+
+Track user actions for experimentation and analytics:
-One important note is that the Split Provider **requires a targeting key** to be set. Often times this should be set when evaluating the value of a flag by [setting an EvaluationContext](https://docs.openfeature.dev/docs/reference/concepts/evaluation-context) which contains the targeting key. An example flag evaluation is
```go
-client := openfeature.NewClient("CLIENT_NAME");
+evalCtx := openfeature.NewEvaluationContext("user-123", nil)
-evaluationContext := openfeature.NewEvaluationContext("TARGETING_KEY", nil)
-boolValue := client.BooleanValue(nil, "boolFlag", false, evaluationContext)
+// Basic tracking with value
+details := openfeature.NewTrackingEventDetails(99.99)
+client.Track(ctx, "purchase_completed", evalCtx, details)
+
+// Tracking with custom traffic type
+evalCtxAccount := openfeature.NewEvaluationContext("account-456", map[string]any{
+ "trafficType": "account", // Optional, defaults to "user"
+})
+client.Track(ctx, "subscription_created", evalCtxAccount, details)
+
+// Tracking with properties
+purchaseDetails := openfeature.NewTrackingEventDetails(149.99).
+ Add("currency", "USD").
+ Add("item_count", 3).
+ Add("category", "electronics")
+client.Track(ctx, "purchase", evalCtx, purchaseDetails)
```
-If the same targeting key is used repeatedly, the evaluation context may be set at the client level
+
+**Supported Property Types:**
+
+The Split SDK accepts the following property value types:
+
+| Type | Supported | Example |
+|----------------------------|-----------|----------------------------|
+| `string` | ✅ | `Add("currency", "USD")` |
+| `bool` | ✅ | `Add("is_premium", true)` |
+| `int`, `int32`, `int64` | ✅ | `Add("item_count", 3)` |
+| `uint`, `uint32`, `uint64` | ✅ | `Add("quantity", uint(5))` |
+| `float32`, `float64` | ✅ | `Add("price", 99.99)` |
+| `nil` | ✅ | `Add("optional", nil)` |
+| Arrays, maps, structs | ❌ | Silently set to `nil` |
+
+**⚠️ Important:** Unsupported types (arrays, maps, nested objects) are **silently set to `nil`** by the Split SDK - no
+error is returned. Always use primitive types for event properties.
+
+**Parameters:**
+
+- `trackingEventName`: Event name (e.g., "checkout", "signup")
+- `evaluationContext`: Contains targeting key and optional `trafficType` attribute
+- `details`: Event value and custom properties
+
+**Traffic Type:**
+
+- Defaults to `"user"` if not specified
+- Set via `trafficType` attribute in evaluation context
+- Must match a defined traffic type in Split
+
+**Localhost Mode:** Track events are accepted but not persisted (no server to send them to). Code using `Track()` runs
+unchanged in local development.
+
+**View Events:** Track events appear in Split Data Hub (Live Tail tab).
+
+### Track Options
+
+Control per-request tracking behavior via `context.Context`:
+
+```go
+// Track with metric value (standard - value sent to Split for sum/average metrics)
+details := openfeature.NewTrackingEventDetails(99.99)
+client.Track(ctx, "purchase", evalCtx, details)
+
+// Track without metric value (count-only event)
+// Sends nil to Split instead of 0, preventing pollution of sum/average metrics
+noValueCtx := split.WithoutMetricValue(ctx)
+countDetails := openfeature.NewTrackingEventDetails(0) // value ignored due to context option
+client.Track(noValueCtx, "page_view", evalCtx, countDetails)
+```
+
+**Why `WithoutMetricValue`?**
+
+OpenFeature's `NewTrackingEventDetails(value)` requires a numeric value, but Split supports count-only events (no metric
+value). Without this option, passing `0` would pollute sum/average metrics. `WithoutMetricValue` tells the provider to
+send `nil` to Split's Track API, recording only the event count.
+
+### Event Handling
+
+Subscribe to provider lifecycle events:
+
```go
-evaluationContext := openfeature.NewEvaluationContext("TARGETING_KEY", nil)
-client.SetEvaluationContext(context)
+openfeature.AddHandler(openfeature.ProviderReady, func(details openfeature.EventDetails) {
+ log.Println("Provider ready")
+})
+
+openfeature.AddHandler(openfeature.ProviderConfigChange, func(details openfeature.EventDetails) {
+ log.Println("Configuration updated")
+})
```
-or at the OpenFeatureAPI level
+
+**Events:**
+
+- `PROVIDER_READY` - Provider initialized successfully
+- `PROVIDER_CONFIG_CHANGE` - Flag configurations updated (detected via polling, default 30s, configurable via
+ `WithMonitoringInterval`)
+- `PROVIDER_ERROR` - Initialization or runtime error
+
+**Event Limitations:**
+
+- `PROVIDER_STALE` events are not emitted due to Split SDK limitations. See [Provider Status](#provider-status) for
+ details.
+- `PROVIDER_CONFIG_CHANGE` is detected by polling (default 30 seconds, configurable via `WithMonitoringInterval`,
+ minimum
+ 5 seconds), not via real-time SSE streaming. While the Split SDK receives changes instantly via SSE, it doesn't expose
+ a callback for configuration changes, so the provider polls `manager.Splits()` to detect changes. See
+ [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+
+### Direct SDK Access
+
+**⚠️ Advanced Usage Only**
+
+The provider manages the Split SDK lifecycle (initialization, shutdown, cleanup). Direct factory access should only be
+used for Split-specific features not available through OpenFeature.
+
+**Lifecycle Constraints:**
+
+- ❌ **DO NOT** call `factory.Client().Destroy()` - provider owns lifecycle
+- ❌ **DO NOT** call `factory.Client().BlockUntilReady()` - use `client.State()` instead (see [Provider Status](#provider-status))
+- ⚠️ Factory is only valid between `Init` and `Shutdown`
+- ⚠️ After `Shutdown()`, factory and client are destroyed
+
+**Example:**
+
```go
-evaluationContext := openfeature.NewEvaluationContext("TARGETING_KEY", nil)
-openfeature.SetEvaluationContext(context)
-````
-If the context was set at the client or api level, it is not required to provide it during flag evaluation.
+factory := provider.Factory()
+// Use factory for Split-specific features not available in OpenFeature
+```
+
+See [Split Go SDK documentation](https://github.com/splitio/go-client) for available methods.
+
+## Testing
+
+**Unit tests:** Use OpenFeature test provider, not Split provider.
-## Submitting issues
-
-The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/split-openfeature-provider-go/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner.
+**Integration tests:** Use localhost mode with YAML files. See [test/integration/](./test/integration/).
+
+**Provider tests:**
+
+```bash
+task test # Run all tests
+task test-race # Run with race detection
+task test-coverage # Generate coverage report
+```
+
+## Development
+
+Development workflow managed via [Taskfile](./Taskfile.yml):
+
+```bash
+task # List all tasks
+task example-localhost # Run localhost example
+task example-cloud # Run cloud example
+task test-integration # Run integration tests
+task lint # Run linters
+```
+
+## Logging
+
+Provider uses `slog` for structured logging. Configure via `slog.SetDefault()` or `split.WithLogger()` option.
+
+**Source attribution:**
+
+- `source="split-provider"` - Provider logs
+- `source="split-sdk"` - Split SDK logs
+- `source="openfeature-sdk"` - OpenFeature SDK logs (via hooks)
+
+See [examples/](./examples/) for logging configuration patterns.
## Contributing
-Please see [Contributors Guide](CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR).
+
+Contributions welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing requirements, and PR
+process.
## License
-Licensed under the Apache License, Version 2.0. See: [Apache License](http://www.apache.org/licenses/).
-
-## About Split
-
-Split is the leading Feature Delivery Platform for engineering teams that want to confidently deploy features as fast as they can develop them. Split’s fine-grained management, real-time monitoring, and data-driven experimentation ensure that new features will improve the customer experience without breaking or degrading performance. Companies like Twilio, Salesforce, GoDaddy and WePay trust Split to power their feature delivery.
-
-To learn more about Split, contact hello@split.io, or get started with feature flags for free at https://www.split.io/signup.
-
-Split has built and maintains SDKs for:
-
-* Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK)
-* Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK)
-* Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK)
-* .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK)
-* Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK)
-* PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK)
-* Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK)
-* GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK)
-* Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK)
-* iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK)
-
-For a comprehensive list of open source projects visit our [Github page](https://github.com/splitio?utf8=%E2%9C%93&query=%20only%3Apublic%20).
-
-**Learn more about Split:**
-
-Visit [split.io/product](https://www.split.io/product) for an overview of Split, or visit our documentation at [help.split.io](http://help.split.io) for more detailed information.
+Apache License 2.0. See [LICENSE](http://www.apache.org/licenses/LICENSE-2.0).
+
+## Links
+
+- [Split.io](https://www.split.io/)
+- [OpenFeature](https://openfeature.dev/)
+- [API Documentation](https://pkg.go.dev/github.com/splitio/split-openfeature-provider-go/v2)
+- [Issue Tracker](https://github.com/splitio/split-openfeature-provider-go/issues)
diff --git a/Taskfile.yml b/Taskfile.yml
new file mode 100644
index 0000000..a42c011
--- /dev/null
+++ b/Taskfile.yml
@@ -0,0 +1,366 @@
+version: '3'
+
+# Split OpenFeature Provider - Task Runner
+# Run 'task' or 'task help' for usage information
+
+# =============================================================================
+# Global Configuration
+# =============================================================================
+
+output: prefixed
+silent: true
+dotenv: ['.env.local']
+
+# =============================================================================
+# Variables
+# =============================================================================
+
+vars:
+ COVERAGE_FILE: coverage.out
+ COVERAGE_THRESHOLD: 70
+ LINT_TIMEOUT: 5m
+
+# =============================================================================
+# Default & Help
+# =============================================================================
+
+tasks:
+ default:
+ desc: Show available tasks
+ aliases: [list, ls]
+ cmds:
+ - task --list
+
+ help:
+ desc: Show help and common workflows
+ aliases: [h, '?']
+ cmds:
+ - |
+ echo "Split OpenFeature Provider - Task Runner"
+ echo "========================================"
+ echo ""
+ echo "Quick Start:"
+ echo " task install-tools Install development tools"
+ echo " task test Run tests"
+ echo ""
+ echo "Development Workflow:"
+ echo " task check Run all quality checks (lint + test + coverage)"
+ echo " task pre-commit Quick checks before committing"
+ echo " task ci Full CI checks before PR"
+ echo ""
+ echo "Code Quality:"
+ echo " task lint Run linter"
+ echo " task lint-fix Run linter with auto-fix"
+ echo " task fmt Format code"
+ echo " task vet Run go vet"
+ echo ""
+ echo "Testing:"
+ echo " task test Run unit tests with coverage"
+ echo " task test-race Run with race detection (no coverage)"
+ echo " task test-short Run unit tests (fast mode)"
+ echo " task test-coverage Show coverage report"
+ echo " task coverage-check Verify coverage >= {{.COVERAGE_THRESHOLD}}%"
+ echo ""
+ echo "Examples:"
+ echo " task example-localhost Offline mode with YAML (no account)"
+ echo " task example-cloud Cloud mode (requires SPLIT_API_KEY)"
+ echo ""
+ echo "Integration Testing:"
+ echo " task test-integration Auto-selects localhost or cloud mode"
+ echo " task test-cloud Cloud-only features"
+ echo ""
+ echo "Code Generation:"
+ echo " task generate-mocks Generate mock implementations (mockery v3)"
+ echo ""
+ echo "Run 'task --list' to see all available tasks"
+
+ # ===========================================================================
+ # Workflow Tasks
+ # ===========================================================================
+
+ check:
+ desc: Run all quality checks (lint, test, coverage)
+ aliases: [c]
+ cmds:
+ - task: lint
+ - task: test
+ - task: coverage-check
+
+ ci:
+ desc: Run full CI pipeline (use before submitting PR)
+ cmds:
+ - echo "Running CI checks..."
+ - task: fmt-check
+ - task: lint
+ - task: vet
+ - task: test
+ - task: coverage-check
+ - echo "All CI checks passed!"
+
+ pre-commit:
+ desc: Run pre-commit checks (format, lint, quick test)
+ aliases: [pc]
+ cmds:
+ - task: fmt
+ - task: lint
+ - task: test-short
+
+ pre-push:
+ desc: Run pre-push checks (full CI)
+ aliases: [pp]
+ cmds:
+ - task: ci
+
+ # ===========================================================================
+ # Build Tasks
+ # ===========================================================================
+
+ build:
+ desc: Build the provider
+ aliases: [b]
+ method: checksum
+ sources:
+ - '**/*.go'
+ - go.mod
+ - go.sum
+ cmds:
+ - go build -v ./...
+
+ clean:
+ desc: Clean build artifacts and caches
+ cmds:
+ - rm -f {{.COVERAGE_FILE}}
+ - rm -rf .task/
+ - go clean -cache -testcache
+
+ # ===========================================================================
+ # Testing Tasks
+ # ===========================================================================
+
+ test:
+ desc: Run unit tests with race detector and coverage
+ aliases: [t]
+ cmds:
+ - go test -v -race -coverprofile={{.COVERAGE_FILE}} -covermode=atomic $(go list ./... | grep -v /examples/ | grep -v /test/)
+ - task: _update-coverage-badge
+
+ test-race:
+ desc: Run unit tests with race detector (no coverage)
+ aliases: [tr]
+ cmds:
+ - go test -v -race -count=1 $(go list ./... | grep -v /examples/ | grep -v /test/)
+
+ test-short:
+ desc: Run unit tests in short mode (fast)
+ aliases: [ts]
+ cmds:
+ - go test -v -short $(go list ./... | grep -v /examples/ | grep -v /test/)
+
+ # ===========================================================================
+ # Coverage Tasks
+ # ===========================================================================
+
+ coverage:
+ desc: Generate and display coverage report
+ aliases: [cov, test-coverage]
+ deps: [test]
+ cmds:
+ - go tool cover -func={{.COVERAGE_FILE}}
+
+ coverage-check:
+ desc: Verify coverage meets {{.COVERAGE_THRESHOLD}}% threshold
+ aliases: [cc]
+ deps: [test]
+ cmds:
+ - |
+ COVERAGE=$(go tool cover -func={{.COVERAGE_FILE}} | grep total | awk '{print $3}' | sed 's/%//')
+ echo "Coverage: $COVERAGE% (threshold: {{.COVERAGE_THRESHOLD}}%)"
+ if [ $(echo "$COVERAGE < {{.COVERAGE_THRESHOLD}}" | bc) -eq 1 ]; then
+ echo "FAIL: Coverage is below threshold"
+ exit 1
+ fi
+ echo "PASS: Coverage meets threshold"
+
+ coverage-html:
+ desc: Open coverage report in browser
+ deps: [test]
+ cmds:
+ - go tool cover -html={{.COVERAGE_FILE}}
+
+ # ===========================================================================
+ # Code Quality Tasks
+ # ===========================================================================
+
+ lint:
+ desc: Run golangci-lint
+ aliases: [l]
+ method: checksum
+ sources:
+ - '**/*.go'
+ - .golangci.yml
+ cmds:
+ - golangci-lint run --timeout {{.LINT_TIMEOUT}}
+
+ lint-fix:
+ desc: Run golangci-lint with auto-fix
+ aliases: [lf]
+ cmds:
+ - golangci-lint run --fix --timeout {{.LINT_TIMEOUT}}
+
+ fmt:
+ desc: Format code with gofmt
+ aliases: [f]
+ cmds:
+ - gofmt -s -w .
+
+ fmt-check:
+ desc: Check code formatting (no changes)
+ aliases: [fc]
+ cmds:
+ - |
+ UNFORMATTED=$(gofmt -l .)
+ if [ -n "$UNFORMATTED" ]; then
+ echo "Unformatted files:"
+ echo "$UNFORMATTED"
+ exit 1
+ fi
+ echo "All files formatted correctly"
+
+ vet:
+ desc: Run go vet
+ aliases: [v]
+ method: checksum
+ sources:
+ - '**/*.go'
+ cmds:
+ - go vet ./...
+
+ # ===========================================================================
+ # Example Tasks
+ # ===========================================================================
+
+ example-localhost:
+ desc: Run localhost example (offline YAML flags, no account needed)
+ aliases: [el]
+ dir: examples/localhost
+ cmds:
+ - go run main.go
+
+ example-cloud:
+ desc: Run cloud example (requires SPLIT_API_KEY in .env.local)
+ aliases: [ec]
+ dir: examples/cloud
+ preconditions:
+ - sh: test -n "$SPLIT_API_KEY"
+ msg: "SPLIT_API_KEY not set. Create .env.local with SPLIT_API_KEY=your-key"
+ cmds:
+ - go run main.go
+
+ # ===========================================================================
+ # Integration Testing Tasks
+ # ===========================================================================
+
+ test-integration:
+ desc: Run integration tests (auto-selects localhost or cloud mode)
+ aliases: [ti]
+ dir: test/integration
+ cmds:
+ - go run .
+
+ test-cloud:
+ desc: Run cloud-only integration tests (requires SPLIT_API_KEY)
+ aliases: [tc]
+ dir: test/advanced
+ preconditions:
+ - sh: test -n "$SPLIT_API_KEY"
+ msg: "SPLIT_API_KEY not set. Create .env.local with SPLIT_API_KEY=your-key"
+ cmds:
+ - go run main.go
+
+ # ===========================================================================
+ # Dependency Management Tasks
+ # ===========================================================================
+
+ deps-tidy:
+ desc: Tidy go.mod and go.sum
+ aliases: [tidy]
+ cmds:
+ - go mod tidy
+
+ deps-update:
+ desc: Update all dependencies to latest versions
+ aliases: [update]
+ cmds:
+ - echo "Updating dependencies..."
+ - go get -u ./...
+ - go mod tidy
+ - echo "Done. Run 'task test' to verify."
+
+ # ===========================================================================
+ # Code Generation Tasks
+ # ===========================================================================
+
+ generate-mocks:
+ desc: Generate mock implementations using mockery v3
+ aliases: [mocks]
+ method: checksum
+ sources:
+ - client.go
+ generates:
+ - mock_client_test.go
+ cmds:
+ - echo "Generating mocks..."
+ - mockery
+ - echo "Done. Mock files generated."
+
+ # ===========================================================================
+ # Tool Management Tasks
+ # ===========================================================================
+
+ install-tools:
+ desc: Install required development tools
+ aliases: [tools]
+ cmds:
+ - echo "Installing development tools..."
+ - brew install golangci-lint
+ - brew install mockery
+ - echo "Done!"
+
+ check-tools:
+ desc: Verify required tools are installed
+ cmds:
+ - |
+ echo "Checking tools..."
+ MISSING=""
+ if ! command -v golangci-lint &> /dev/null; then
+ echo "[x] golangci-lint - not installed"
+ MISSING="yes"
+ else
+ echo "[ok] golangci-lint"
+ fi
+ if ! command -v mockery &> /dev/null; then
+ echo "[x] mockery - not installed"
+ MISSING="yes"
+ else
+ echo "[ok] mockery"
+ fi
+ if [ -n "$MISSING" ]; then
+ echo ""
+ echo "Run 'task install-tools' to install missing tools"
+ exit 1
+ fi
+ echo ""
+ echo "All tools installed!"
+
+ # ===========================================================================
+ # Internal Tasks
+ # ===========================================================================
+
+ _update-coverage-badge:
+ internal: true
+ cmds:
+ - |
+ COVERAGE=$(go tool cover -func={{.COVERAGE_FILE}} | grep total | awk '{print $3}' | sed 's/%//')
+ sed -i.bak "s/coverage-[0-9.]*%25/coverage-$COVERAGE%25/g" README.md
+ rm -f README.md.bak
+ echo "Coverage badge updated to $COVERAGE%"
diff --git a/benchmark_test.go b/benchmark_test.go
new file mode 100644
index 0000000..b53b9e1
--- /dev/null
+++ b/benchmark_test.go
@@ -0,0 +1,166 @@
+package split
+
+import (
+ "context"
+ "testing"
+
+ "github.com/open-feature/go-sdk/openfeature"
+ "github.com/splitio/go-client/v6/splitio/conf"
+ "github.com/splitio/go-toolkit/v5/logging"
+)
+
+// BenchmarkBooleanEvaluation benchmarks single boolean flag evaluation performance.
+func BenchmarkBooleanEvaluation(b *testing.B) {
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 10
+
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ if err != nil {
+ b.Fatalf("Failed to create provider: %v", err)
+ }
+ defer func() { _ = provider.ShutdownWithContext(context.Background()) }()
+
+ err = provider.InitWithContext(context.Background(), openfeature.NewEvaluationContext("", nil))
+ if err != nil {
+ b.Fatalf("Failed to initialize provider: %v", err)
+ }
+
+ flatCtx := openfeature.FlattenedContext{
+ openfeature.TargetingKey: "bench-user",
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _ = provider.BooleanEvaluation(context.TODO(), flagSomeOther, false, flatCtx)
+ }
+}
+
+// BenchmarkStringEvaluation benchmarks single string flag evaluation performance.
+func BenchmarkStringEvaluation(b *testing.B) {
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 10
+
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ if err != nil {
+ b.Fatalf("Failed to create provider: %v", err)
+ }
+ defer func() { _ = provider.ShutdownWithContext(context.Background()) }()
+
+ err = provider.InitWithContext(context.Background(), openfeature.NewEvaluationContext("", nil))
+ if err != nil {
+ b.Fatalf("Failed to initialize provider: %v", err)
+ }
+
+ flatCtx := openfeature.FlattenedContext{
+ openfeature.TargetingKey: "bench-user",
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _ = provider.StringEvaluation(context.TODO(), flagSomeOther, "default", flatCtx)
+ }
+}
+
+// BenchmarkConcurrentEvaluations benchmarks concurrent flag evaluations.
+func BenchmarkConcurrentEvaluations(b *testing.B) {
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 10
+
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ if err != nil {
+ b.Fatalf("Failed to create provider: %v", err)
+ }
+ defer func() { _ = provider.ShutdownWithContext(context.Background()) }()
+
+ err = provider.InitWithContext(context.Background(), openfeature.NewEvaluationContext("", nil))
+ if err != nil {
+ b.Fatalf("Failed to initialize provider: %v", err)
+ }
+
+ flatCtx := openfeature.FlattenedContext{
+ openfeature.TargetingKey: "bench-user",
+ }
+
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ _ = provider.BooleanEvaluation(context.TODO(), flagSomeOther, false, flatCtx)
+ }
+ })
+}
+
+// BenchmarkProviderInitialization measures provider initialization time.
+func BenchmarkProviderInitialization(b *testing.B) {
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 10
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ if err != nil {
+ b.Fatalf("Failed to create provider: %v", err)
+ }
+
+ err = provider.InitWithContext(context.Background(), openfeature.NewEvaluationContext("", nil))
+ if err != nil {
+ b.Fatalf("Failed to initialize provider: %v", err)
+ }
+
+ _ = provider.ShutdownWithContext(context.Background())
+ }
+}
+
+// BenchmarkAttributeHeavyEvaluation measures evaluation performance with many attributes.
+func BenchmarkAttributeHeavyEvaluation(b *testing.B) {
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 10
+
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ if err != nil {
+ b.Fatalf("Failed to create provider: %v", err)
+ }
+ defer func() { _ = provider.ShutdownWithContext(context.Background()) }()
+
+ err = provider.InitWithContext(context.Background(), openfeature.NewEvaluationContext("", nil))
+ if err != nil {
+ b.Fatalf("Failed to initialize provider: %v", err)
+ }
+
+ flatCtx := openfeature.FlattenedContext{
+ openfeature.TargetingKey: "bench-user",
+ "email": "user@example.com",
+ "plan": "enterprise",
+ "region": "us-east-1",
+ "org_id": "org-12345",
+ "user_id": "user-67890",
+ "account_type": "premium",
+ "feature_flags_enabled": true,
+ "beta_tester": true,
+ "signup_date": "2024-01-15",
+ "last_login": "2025-01-18",
+ "session_count": 42,
+ "total_spend": 1299.99,
+ "conversion_rate": 0.25,
+ "engagement_score": 87.5,
+ "device_type": "desktop",
+ "browser": "chrome",
+ "os": "macos",
+ "language": "en-US",
+ "timezone": "America/New_York",
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _ = provider.BooleanEvaluation(context.TODO(), flagSomeOther, false, flatCtx)
+ }
+}
diff --git a/client.go b/client.go
new file mode 100644
index 0000000..2fb5704
--- /dev/null
+++ b/client.go
@@ -0,0 +1,26 @@
+package split
+
+import "github.com/splitio/go-client/v6/splitio/client"
+
+// Compile-time check that *client.SplitClient satisfies Client.
+var _ Client = (*client.SplitClient)(nil)
+
+// Client defines the Split SDK client methods used by this provider.
+// This interface enables dependency injection for testing (mock generation via mockery).
+// Method signatures match *client.SplitClient exactly (verified by compile-time check above).
+type Client interface {
+ // TreatmentWithConfig evaluates a single flag and returns the treatment with optional JSON config.
+ TreatmentWithConfig(key interface{}, featureFlagName string, attributes map[string]interface{}) client.TreatmentResult
+
+ // TreatmentsWithConfigByFlagSet evaluates all flags in a flag set.
+ TreatmentsWithConfigByFlagSet(key interface{}, flagSet string, attributes map[string]interface{}) map[string]client.TreatmentResult
+
+ // Track sends a tracking event to Split for analytics.
+ Track(key string, trafficType string, eventType string, value interface{}, properties map[string]interface{}) error
+
+ // BlockUntilReady blocks until the SDK is ready or the timeout (seconds) expires.
+ BlockUntilReady(timer int) error
+
+ // Destroy shuts down the SDK client and releases resources.
+ Destroy()
+}
diff --git a/concurrency_test.go b/concurrency_test.go
new file mode 100644
index 0000000..e8962c6
--- /dev/null
+++ b/concurrency_test.go
@@ -0,0 +1,184 @@
+package split
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/open-feature/go-sdk/openfeature"
+ "github.com/splitio/go-client/v6/splitio/conf"
+ "github.com/splitio/go-toolkit/v5/logging"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestConcurrentEvaluations tests thread safety with concurrent evaluations.
+func TestConcurrentEvaluations(t *testing.T) {
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 10
+
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ require.NoError(t, err, "Failed to create provider")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+ err = openfeature.SetProviderWithContextAndWait(ctx, provider)
+ require.NoError(t, err, "Failed to set provider")
+
+ defer func() {
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer shutdownCancel()
+ _ = openfeature.ShutdownWithContext(shutdownCtx)
+ }()
+
+ const numGoroutines = 50
+ const numEvaluations = 100
+
+ var wg sync.WaitGroup
+ errors := make(chan error, numGoroutines)
+
+ for i := 0; i < numGoroutines; i++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ ofClient := openfeature.NewClient("concurrent-test")
+ evalCtx := openfeature.NewEvaluationContext("test-user", nil)
+
+ for j := 0; j < numEvaluations; j++ {
+ _, err := ofClient.BooleanValue(
+ context.TODO(),
+ flagSomeOther,
+ false,
+ evalCtx,
+ )
+ if err != nil && !strings.Contains(err.Error(), "FLAG_NOT_FOUND") {
+ errors <- fmt.Errorf("goroutine %d iteration %d: %w", id, j, err)
+ return
+ }
+
+ _, err = ofClient.StringValue(
+ context.TODO(),
+ flagSomeOther,
+ "default",
+ evalCtx,
+ )
+ if err != nil && !strings.Contains(err.Error(), "FLAG_NOT_FOUND") {
+ errors <- fmt.Errorf("goroutine %d iteration %d: %w", id, j, err)
+ return
+ }
+ }
+ }(i)
+ }
+
+ wg.Wait()
+ close(errors)
+
+ for err := range errors {
+ t.Errorf("Concurrent evaluation error: %v", err)
+ }
+}
+
+// TestConcurrentInitShutdown tests race conditions when Init and Shutdown are called concurrently.
+func TestConcurrentInitShutdown(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping in short mode")
+ }
+
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 1
+
+ const iterations = 2
+ for i := 0; i < iterations; i++ {
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ require.NoError(t, err)
+
+ var wg sync.WaitGroup
+ const concurrency = 3
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ for j := 0; j < concurrency; j++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ _ = provider.InitWithContext(ctx, openfeature.NewEvaluationContext("", nil))
+ }()
+ }
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ time.Sleep(10 * time.Millisecond)
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer shutdownCancel()
+ _ = provider.ShutdownWithContext(shutdownCtx)
+ }()
+
+ wg.Wait()
+
+ assert.Equal(t, openfeature.NotReadyState, provider.Status(), "Provider should be NotReady after shutdown")
+ }
+}
+
+// TestEventChannelOverflow tests behavior when event channel buffer is full.
+func TestEventChannelOverflow(t *testing.T) {
+ cfg := conf.Default()
+ cfg.SplitFile = testSplitFile
+ cfg.LoggerConfig.LogLevel = logging.LevelNone
+ cfg.BlockUntilReady = 1
+
+ provider, err := New("localhost", WithSplitConfig(cfg))
+ require.NoError(t, err)
+ defer func() { _ = provider.ShutdownWithContext(context.Background()) }()
+
+ err = provider.InitWithContext(context.Background(), openfeature.NewEvaluationContext("", nil))
+ require.NoError(t, err)
+
+ eventChan := provider.EventChannel()
+
+ const eventsToEmit = 150
+ const bufferSize = 100
+
+ for i := 0; i < eventsToEmit; i++ {
+ select {
+ case <-eventChan:
+ // Drain one event to make room
+ default:
+ // Channel full or empty
+ }
+ }
+
+ done := make(chan bool)
+ go func() {
+ status := provider.Status()
+ assert.Equal(t, openfeature.ReadyState, status, "Provider should still be ready")
+ done <- true
+ }()
+
+ select {
+ case <-done:
+ // Success - operation completed without blocking
+ case <-time.After(2 * time.Second):
+ t.Fatal("Event emission appears to be blocking")
+ }
+
+ drained := 0
+ for {
+ select {
+ case <-eventChan:
+ drained++
+ case <-time.After(10 * time.Millisecond):
+ goto doneLabel
+ }
+ }
+doneLabel:
+ assert.LessOrEqual(t, drained, bufferSize, "Should not have more events than buffer size")
+}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..04c1024
--- /dev/null
+++ b/config.go
@@ -0,0 +1,51 @@
+package split
+
+import "github.com/splitio/go-client/v6/splitio/conf"
+
+// TestConfig returns an optimized Split SDK configuration for tests and examples.
+// This configuration minimizes timeouts, queue sizes, and sync intervals for faster
+// execution while maintaining full functionality.
+//
+// Optimizations applied:
+// - BlockUntilReady: 5 seconds (faster initialization timeout)
+// - HTTPTimeout: 5 seconds (faster network failure detection)
+// - ImpressionsMode: debug (sends all impressions, not batched)
+// - Queue sizes: Reduced to 100 (faster event/impression flushing)
+// - Bulk sizes: Reduced to 100 (smaller batches, faster submission)
+// - Sync intervals: Set to minimums (faster updates)
+//
+// Usage:
+//
+// cfg := split.TestConfig()
+// cfg.SplitFile = "./split.yaml" // For localhost mode
+// provider, err := split.New(apiKey, split.WithSplitConfig(cfg))
+func TestConfig() *conf.SplitSdkConfig {
+ cfg := conf.Default()
+
+ // Faster initialization timeout
+ cfg.BlockUntilReady = 5
+
+ // Faster network failure detection
+ cfg.Advanced.HTTPTimeout = 5
+
+ // Use debug mode for impression tracking (sends all impressions, 60s sync)
+ // Default "optimized" batches impressions which can delay visibility
+ cfg.ImpressionsMode = "debug"
+
+ // Smaller queues for faster flushing in tests
+ cfg.Advanced.EventsQueueSize = 100
+ cfg.Advanced.ImpressionsQueueSize = 100
+
+ // Smaller batches for faster submission
+ cfg.Advanced.EventsBulkSize = 100
+ cfg.Advanced.ImpressionsBulkSize = 100
+
+ // Minimum sync intervals for faster updates
+ cfg.TaskPeriods.SplitSync = 5 // minimum: 5s
+ cfg.TaskPeriods.SegmentSync = 30 // minimum: 30s
+ cfg.TaskPeriods.ImpressionSync = 60 // minimum: 60s (debug mode)
+ cfg.TaskPeriods.EventsSync = 1 // minimum: 1s
+ cfg.TaskPeriods.TelemetrySync = 60 // reduced from 3600s
+
+ return cfg
+}
diff --git a/constants.go b/constants.go
new file mode 100644
index 0000000..d3c2b75
--- /dev/null
+++ b/constants.go
@@ -0,0 +1,68 @@
+package split
+
+import "time"
+
+const (
+ // SDK Timeouts
+
+ // defaultSDKTimeout is the default timeout in seconds for Split SDK initialization.
+ // Used as the default BlockUntilReady timeout when not configured.
+ defaultSDKTimeout = 10
+
+ // defaultInitTimeout is the default timeout for provider initialization when no BlockUntilReady is configured.
+ // Provides 5 seconds buffer beyond the defaultSDKTimeout (10s SDK + 5s buffer = 15s total).
+ defaultInitTimeout = 15 * time.Second
+
+ // initTimeoutBuffer is added to BlockUntilReady to ensure initialization has time to complete gracefully.
+ initTimeoutBuffer = 5 * time.Second
+
+ // defaultShutdownTimeout is the default timeout for provider shutdown operations.
+ // Allows time for monitoring goroutine cleanup, SDK destroy, and channel closes.
+ defaultShutdownTimeout = 30 * time.Second
+
+ // Event Handling
+
+ // eventChannelBuffer is the buffer size for the provider's event channel.
+ // Events are sent asynchronously to OpenFeature SDK handlers.
+ // Provides headroom for burst events. Overflow events are dropped (logged as warnings).
+ eventChannelBuffer = 128
+
+ // Monitoring
+
+ // defaultMonitoringInterval is the default interval for checking split definition changes.
+ defaultMonitoringInterval = 30 * time.Second
+
+ // minMonitoringInterval is the minimum allowed monitoring interval.
+ minMonitoringInterval = 5 * time.Second
+
+ // Atomic States
+
+ // shutdownStateActive indicates the provider has been shut down (atomic flag = 1).
+ shutdownStateActive = 1
+
+ // shutdownStateInactive indicates the provider is active (atomic flag = 0).
+ shutdownStateInactive = 0
+
+ // Split SDK Constants
+
+ // controlTreatment is the treatment returned by Split SDK when a flag doesn't exist
+ // or evaluation fails. Used to detect missing flags and return defaults.
+ controlTreatment = "control"
+
+ // treatmentOn is the conventional Split treatment for boolean "true".
+ treatmentOn = "on"
+
+ // treatmentOff is the conventional Split treatment for boolean "false".
+ treatmentOff = "off"
+
+ // OpenFeature Context Keys
+
+ // TrafficTypeKey is the evaluation context attribute key for Split traffic type.
+ // Used by Track() to categorize events. Not used for flag evaluations
+ // (traffic type is configured per flag in Split dashboard).
+ TrafficTypeKey = "trafficType"
+
+ // DefaultTrafficType is the default traffic type used when not specified in context.
+ // "user" is the most common traffic type for user-based targeting and tracking.
+ DefaultTrafficType = "user"
+)
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..91295e6
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,42 @@
+// Package split provides an OpenFeature provider implementation for Split.io
+// feature flags and A/B testing platform.
+//
+// # Basic Usage
+//
+// provider, err := split.New("YOUR_API_KEY")
+// if err != nil {
+// log.Fatal(err)
+// }
+//
+// ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+// defer cancel()
+// if err := openfeature.SetProviderWithContextAndWait(ctx, provider); err != nil {
+// log.Fatal(err)
+// }
+//
+// client := openfeature.NewClient("my-app")
+// evalCtx := openfeature.NewEvaluationContext("user-123", map[string]any{
+// "email": "user@example.com",
+// })
+// enabled, _ := client.BooleanValue(context.Background(), "new-feature", false, evalCtx)
+//
+// Evaluations return default values on errors. Use *ValueDetails methods to
+// distinguish success from fallback via Reason and ErrorCode fields.
+//
+// # Configuration
+//
+// cfg := conf.Default()
+// cfg.BlockUntilReady = 15
+//
+// provider, _ := split.New("YOUR_API_KEY",
+// split.WithSplitConfig(cfg),
+// split.WithLogger(logger),
+// )
+//
+// # Concurrency
+//
+// The provider is thread-safe. Multiple goroutines can evaluate flags
+// concurrently. Shutdown waits for in-flight evaluations to complete.
+//
+// See README.md for complete documentation and examples.
+package split
diff --git a/docs/images/of_banner.png b/docs/images/of_banner.png
new file mode 100644
index 0000000000000000000000000000000000000000..bf51611d4d75cb81001bc5d5b6af30eb63fd8894
GIT binary patch
literal 214563
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