| Crates.io | spotify-confidence-openfeature-provider-local |
| lib.rs | spotify-confidence-openfeature-provider-local |
| version | 0.2.0 |
| created_at | 2026-01-19 13:58:07.321007+00 |
| updated_at | 2026-01-22 10:34:45.858095+00 |
| description | OpenFeature provider for Confidence using native Rust resolver |
| homepage | https://confidence.spotify.com/ |
| repository | https://github.com/spotify/confidence-resolver |
| max_upload_size | |
| id | 2054655 |
| size | 198,411 |
A high-performance OpenFeature provider for Confidence feature flags that evaluates flags locally for minimal latency.
Add this dependency to your Cargo.toml:
[dependencies]
spotify-confidence-openfeature-provider-local = "0.1.0"
You'll need a client secret from Confidence to use this provider.
See the Integration Guide: Getting Your Credentials for step-by-step instructions on:
use open_feature::{EvaluationContext, OpenFeature};
use spotify_confidence_openfeature_provider_local::{ConfidenceProvider, ProviderOptions};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create provider options with your client secret
let options = ProviderOptions::new("your-client-secret"); // Get from Confidence dashboard
// Create the Confidence provider
let provider = ConfidenceProvider::new(options)?;
// Set the provider on the OpenFeature singleton
OpenFeature::singleton_mut()
.await
.set_provider(provider)
.await;
// Create an OpenFeature client
let client = OpenFeature::singleton().await.create_client();
// Create evaluation context with user attributes for targeting
let context = EvaluationContext::default()
.with_targeting_key("user-123")
.with_custom_field("country", "US")
.with_custom_field("plan", "premium");
// Evaluate a boolean flag
let enabled = client
.get_bool_value("test-flag.enabled", Some(&context), None)
.await
.unwrap_or(false);
println!("Flag value: {}", enabled);
Ok(())
}
The evaluation context contains information about the user/session being evaluated for targeting and A/B testing.
use open_feature::EvaluationContext;
// Simple attributes
let context = EvaluationContext::default()
.with_targeting_key("user-123")
.with_custom_field("country", "US")
.with_custom_field("plan", "premium")
.with_custom_field("age", 25);
The provider uses a default value fallback pattern - when evaluation fails, it returns an error that you must handle with .unwrap_or() to apply your default value.
See the Integration Guide: Error Handling for:
// Using unwrap_or for default values
let enabled = client
.get_bool_value("my-flag.enabled", Some(&context), None)
.await
.unwrap_or(false);
// enabled will be 'false' if evaluation failed
// For detailed error information, use get_bool_details()
let details = client
.get_bool_details("my-flag.enabled", Some(&context), None)
.await;
match details {
Ok(result) => {
println!("Value: {}", result.value);
println!("Variant: {:?}", result.variant);
println!("Reason: {:?}", result.reason);
}
Err(e) => {
eprintln!("Flag evaluation error: {:?}", e);
}
}
The ProviderOptions struct contains all configuration options for the provider:
use spotify_confidence_openfeature_provider_local::ProviderOptions;
let options = ProviderOptions::new("your-client-secret")
.with_initialize_timeout(10_000) // Max ms to wait for initial state fetch
.with_state_poll_interval(30_000) // Interval in ms for polling state updates
.with_confidence_materialization_store(); // Enable remote materialization
client_secret (String): The client secret used for authentication and flag evaluationinitialize_timeout_ms: Max milliseconds to wait for initial state fetch (default: 30,000)state_poll_interval_ms: Interval in milliseconds for polling state updates (default: 30,000)flush_interval_ms: Interval in milliseconds for flushing logs (default: 10,000)assign_flush_interval_ms: Interval in milliseconds for flushing assign logs (default: 100)materialization_store: Storage for sticky variant assignments and materialized segmentsThe provider supports all OpenFeature value types:
// Boolean flags
let enabled = client
.get_bool_value("feature.enabled", Some(&context), None)
.await
.unwrap_or(false);
// String flags
let color = client
.get_string_value("feature.button_color", Some(&context), None)
.await
.unwrap_or_else(|_| "blue".to_string());
// Integer flags
let timeout = client
.get_int_value("feature.timeout-ms", Some(&context), None)
.await
.unwrap_or(5000);
// Float flags
let ratio = client
.get_float_value("feature.sampling_ratio", Some(&context), None)
.await
.unwrap_or(0.5);
// Object/structured flags
use open_feature::StructValue;
let config = client
.get_struct_value::<StructValue>("feature", Some(&context), None)
.await
.unwrap_or_default();
The provider uses the tracing crate for structured logging. Enable logging by initializing a tracing subscriber:
// Add to your Cargo.toml:
// tracing-subscriber = "0.3"
fn main() {
// Initialize tracing with default settings
tracing_subscriber::fmt::init();
// Or with custom configuration
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
}
The provider logs at different levels:
DEBUG: Flag resolution details, state updatesINFO: Provider initialization, configurationWARN: Non-critical issues, fallbacksERROR: Failures, network errorsImportant: To ensure proper cleanup and flushing of exposure logs, you must explicitly shut down the provider rather than relying on the OpenFeature API shutdown function.
// Get the provider and shut it down explicitly
// Do NOT rely on OpenFeature::singleton_mut().await.shutdown().await
Why? Due to an upstream issue in the OpenFeature Rust SDK, calling the OpenFeature shutdown may not properly wait for the provider shutdown to complete. This can result in loss of exposure logs and other telemetry data. Shutting down the provider directly ensures proper cleanup.
Flushes pending logs to Confidence (exposure events, resolve analytics)
Closes HTTP connections and releases network resources
Stops background tasks (state polling, log batching)
Materialization stores provide persistent storage for sticky variant assignments and custom targeting segments. This enables two key use cases:
Sticky Assignments: Maintain consistent variant assignments across evaluations even when targeting attributes change. This enables pausing intake (stopping new users from entering an experiment) while keeping existing users in their assigned variants.
Custom Targeting via Materialized Segments: Precomputed sets of identifiers from datasets that should be targeted. Instead of evaluating complex targeting rules at runtime, materializations allow efficient lookup of whether a unit (user, session, etc.) is included in a target segment.
If your flags rely on sticky assignments or materialized segments, the default SDK behavior will prevent those rules from being applied and your evaluations will fall back to default values. For production workloads that need sticky behavior or segment lookups, configure a MaterializationStore to avoid unexpected fallbacks and ensure consistent variant assignment.
For quick setup without managing your own storage infrastructure, enable the built-in remote materialization store:
let options = ProviderOptions::new("your-client-secret")
.with_confidence_materialization_store();
When to use:
Trade-offs:
For improved latency and reduced network calls, implement the MaterializationStore trait to store materialization data in your infrastructure:
use async_trait::async_trait;
use std::sync::Arc;
use spotify_confidence_openfeature_provider_local::{
MaterializationStore, ReadOpType, ReadResultType, WriteOp,
ProviderOptions,
};
struct MyRedisStore {
// your implementation
}
#[async_trait]
impl MaterializationStore for MyRedisStore {
async fn read_materializations(
&self,
read_ops: Vec<ReadOpType>,
) -> Result<Vec<ReadResultType>, spotify_confidence_openfeature_provider_local::Error> {
// Load materialization data from Redis
todo!()
}
async fn write_materializations(
&self,
write_ops: Vec<WriteOp>,
) -> Result<(), spotify_confidence_openfeature_provider_local::Error> {
// Store materialization data to Redis
todo!()
}
}
// Use your custom store
let my_store = Arc::new(MyRedisStore { /* ... */ });
let options = ProviderOptions::new("your-client-secret")
.with_materialization_store(my_store);
Consider implementing a materialization store if:
If you don't use sticky assignments or materialized segments, the default behavior is sufficient.
See the root LICENSE file.