OpenFeature Dart Server SDK
Warning: this repository is still an in-progress implementation of the
dart-server-sdk.
OpenFeature is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool.
Quick start
Requirements
Dart language version: 3.11.4
Note
The OpenFeature Dart Server SDK only supports the latest currently maintained Dart language versions.
Install
dependencies:
openfeature_dart_server_sdk: ^0.0.21
Then run
dart pub get
Usage
import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
void main() async {
final api = OpenFeatureAPI();
await api.setProviderAndWait(
InMemoryProvider({
'new-feature': true,
'welcome-message': 'Hello, OpenFeature!',
}),
);
final client = api.getClient('my-app');
final newFeatureEnabled = await client.getBooleanFlag(
'new-feature',
defaultValue: false,
);
final details = await client.getBooleanDetails(
'new-feature',
defaultValue: false,
);
print('Reason: ${details.reason}, Variant: ${details.variant}');
if (newFeatureEnabled) {
final welcomeMessage = await client.getStringFlag(
'welcome-message',
defaultValue: 'Welcome!',
);
print(welcomeMessage);
}
}
API reference
See pub.dev/documentation/openfeature_dart_server_sdk/latest for the complete API documentation.
Features
| Status | Feature | Description |
|---|---|---|
| ✅ | Providers | Integrate with a commercial, open source, or in-house feature management tool. |
| ✅ | Targeting | Contextually-aware flag evaluation using evaluation context. |
| ✅ | Hooks | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | Tracking | Associate user actions with feature flag evaluations for experimentation. |
| ✅ | Logging | Integrate with popular logging packages. |
| ✅ | Domains | Logically bind clients with providers. |
| ⚠️ | Multi-Provider | Compose multiple providers behind one SDK-level provider with a selection strategy. |
| ✅ | Eventing | React to state changes in the provider or flag management system. |
| ✅ | Shutdown | Gracefully clean up a provider during application shutdown. |
| ✅ | Transaction Context Propagation | Set a specific evaluation context for a transaction such as an HTTP request or thread. |
| ✅ | Extending | Extend OpenFeature with custom providers and hooks. |
Implemented: ✅ | Experimental: ⚠️ | Not implemented yet: ❌
Providers
Providers are an abstraction between a flag management system and the OpenFeature SDK. Look here for a complete list of available providers. If the provider you need does not exist yet, see Develop a provider.
final api = OpenFeatureAPI();
api.setProvider(MyProvider());
Targeting
Sometimes the value of a flag must consider dynamic criteria about the application or user, such as location, IP, or organization. In OpenFeature this is called targeting. If your flag management system supports targeting, you can provide the input data using the evaluation context.
final api = OpenFeatureAPI();
api.setGlobalContext(
OpenFeatureEvaluationContext({
'region': 'us-east-1-iah-1a',
}),
);
final client = FeatureClient(
metadata: ClientMetadata(name: 'my-app'),
hookManager: HookManager(),
defaultContext: const EvaluationContext(
attributes: {
'version': '1.4.6',
},
),
provider: api.provider,
);
final result = await client.getBooleanFlag(
'feature-flag',
defaultValue: false,
context: const EvaluationContext(
attributes: {
'user': 'user-123',
'company': 'Initech',
},
),
);
Hooks
Hooks allow custom logic to be added at well-defined points of the flag evaluation life-cycle. Look here for a complete list of available hooks.
Once you have added a hook dependency, it can be registered at the global or client level.
final api = OpenFeatureAPI();
api.addHooks([MyGlobalHook()]);
final client = api.getClient('my-app');
client.addHook(MyClientHook());
Note
Invocation-level hooks are not yet supported. Hooks can currently be registered at the global or client level.
Before hooks may return context updates. Any returned attributes are merged into the evaluation context before the provider is called.
Note
Invocation-level hooks are not yet supported. Hooks can currently be registered at the global or client level.
Tracking
The tracking API allows you to associate user actions with feature flag evaluations. This is useful for experimentation and downstream analytics.
import 'package:openfeature_dart_server_sdk/evaluation_context.dart';
import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
final api = OpenFeatureAPI();
final client = api.getClient('my-app');
await client.track(
'checkout-completed',
context: const EvaluationContext(
attributes: {
'user': 'user-123',
},
),
trackingDetails: const TrackingEventDetails(
value: 99.99,
attributes: {'currency': 'USD'},
),
);
Note that some providers may not support tracking. Check the provider documentation for details.
Logging
In accordance with the OpenFeature specification, the SDK does not generally log messages during flag evaluation.
The SDK uses the package:logging structured logging API internally. You can configure log levels and listeners to capture SDK output for troubleshooting and debugging.
For lifecycle-level logging, use the built-in LoggingHook or
OpenTelemetryHook. LoggingHook redacts evaluation-context values by default
and only emits context keys unless includeContext: true is set explicitly.
Domains
Clients can be assigned to a domain. A domain is a logical identifier that can be used to associate clients with a particular provider. If a domain has no associated provider, the default provider is used.
import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
final api = OpenFeatureAPI();
api.setProvider(InMemoryProvider({'default-flag': true}));
api.registerProvider(CustomCacheProvider());
api.bindClientToProvider('cache-domain', 'CustomCacheProvider');
final defaultClient = api.getClient('default-client');
await defaultClient.getBooleanFlag('default-flag', defaultValue: false);
final cacheClient = api.getClient('cache-client', domain: 'cache-domain');
await cacheClient.getBooleanFlag('cached-flag', defaultValue: false);
Multi-Provider (experimental)
OpenFeature treats Multi-Provider as an SDK-level utility rather than behavior owned by any one provider. It is useful for migrations and fallback compositions where one client should consult more than one underlying provider.
This SDK includes an experimental MultiProvider implementation with a default
FirstMatchStrategy. Providers are evaluated in order. A FLAG_NOT_FOUND
result is treated as a miss and evaluation continues with the next provider.
Initialization and connection fan out across all configured providers, and
tracking is treated as best-effort fan-out.
import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/multi_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
final api = OpenFeatureAPI();
final multiProvider = MultiProvider([
InMemoryProvider({'new-feature': true}),
InMemoryProvider({'fallback-feature': true}),
]);
await api.setProviderAndWait(multiProvider);
final client = api.getClient('my-app');
final enabled = await client.getBooleanFlag(
'new-feature',
defaultValue: false,
);
Treat the current Multi-Provider surface as experimental until the strategy API and documentation settle further.
Eventing
Events allow you to react to state changes in the provider or underlying flag
management system, such as flag definition changes, provider readiness, or error
conditions. Initialization events (PROVIDER_READY on success,
PROVIDER_ERROR on failure) are dispatched for every provider. Some providers
support additional events such as PROVIDER_CONFIGURATION_CHANGED.
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
import 'package:openfeature_dart_server_sdk/open_feature_event.dart';
final api = OpenFeatureAPI();
api.events.listen((event) {
if (event.type == OpenFeatureEventType.PROVIDER_CONFIGURATION_CHANGED) {
print('Provider configuration changed: ${event.message}');
}
});
Client-scoped handlers are also available. A client only receives events for its associated provider.
final client = api.getClient('my-app');
final sub = client.addHandler((event) {
print('Client received event: ${event.type}');
});
await client.removeHandler(sub);
The SDK also exposes a global event bus for SDK-specific flag evaluation events.
import 'package:openfeature_dart_server_sdk/event_system.dart';
OpenFeatureEvents.instance.subscribe(
(event) {
print('Flag evaluated: ${event.data['flagKey']} = ${event.data['result']}');
},
filter: EventFilter(
types: {OpenFeatureEventType.flagEvaluated},
),
);
Shutdown
The OpenFeature API provides mechanisms to perform cleanup of registered providers. This should only be called when your application is shutting down.
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
import 'package:openfeature_dart_server_sdk/shutdown_manager.dart';
final api = OpenFeatureAPI();
final shutdownManager = ShutdownManager();
shutdownManager.registerHook(
ShutdownHook(
name: 'provider-cleanup',
phase: ShutdownPhase.PROVIDER_SHUTDOWN,
execute: () async {
await api.dispose();
},
),
);
await shutdownManager.shutdown();
Transaction Context Propagation
Transaction context is a container for transaction-specific evaluation context, such as user id, user agent, or IP. Transaction context can be set where request-specific data is available and then automatically applied to flag evaluations inside that transaction.
import 'package:openfeature_dart_server_sdk/transaction_context.dart';
final transactionManager = TransactionContextManager();
final context = TransactionContext(
transactionId: 'request-123',
attributes: {
'user': 'user-456',
'region': 'us-west-1',
},
);
transactionManager.pushContext(context);
await client.getBooleanFlag('my-flag', defaultValue: false);
await transactionManager.withContext(
'transaction-id',
{'user': 'user-123'},
() async {
await client.getBooleanFlag('my-flag', defaultValue: false);
},
);
transactionManager.popContext();
Extending
Develop a provider
To develop a provider, create a new project and include the OpenFeature SDK as a dependency. This can live in a new repository or in the existing contrib repository.
import 'package:openfeature_dart_server_sdk/feature_provider.dart';
class MyCustomProvider implements FeatureProvider {
@override
String get name => 'MyCustomProvider';
@override
ProviderMetadata get metadata => ProviderMetadata(name: name);
@override
ProviderState get state => ProviderState.READY;
@override
ProviderConfig get config => ProviderConfig();
@override
Future<void> initialize([Map<String, dynamic>? config]) async {}
@override
Future<void> connect() async {}
@override
Future<void> shutdown() async {}
@override
Future<void> track(
String trackingEventName, {
Map<String, dynamic>? evaluationContext,
TrackingEventDetails? trackingDetails,
}) async {}
@override
Future<FlagEvaluationResult<bool>> getBooleanFlag(
String flagKey,
bool defaultValue, {
Map<String, dynamic>? context,
}) async {
return FlagEvaluationResult(
flagKey: flagKey,
value: true,
evaluatedAt: DateTime.now(),
evaluatorId: name,
);
}
@override
Future<FlagEvaluationResult<String>> getStringFlag(
String flagKey,
String defaultValue, {
Map<String, dynamic>? context,
}) async {
return FlagEvaluationResult(
flagKey: flagKey,
value: 'value',
evaluatedAt: DateTime.now(),
evaluatorId: name,
);
}
@override
Future<FlagEvaluationResult<int>> getIntegerFlag(
String flagKey,
int defaultValue, {
Map<String, dynamic>? context,
}) async {
return FlagEvaluationResult(
flagKey: flagKey,
value: 42,
evaluatedAt: DateTime.now(),
evaluatorId: name,
);
}
@override
Future<FlagEvaluationResult<double>> getDoubleFlag(
String flagKey,
double defaultValue, {
Map<String, dynamic>? context,
}) async {
return FlagEvaluationResult(
flagKey: flagKey,
value: 3.14,
evaluatedAt: DateTime.now(),
evaluatorId: name,
);
}
@override
Future<FlagEvaluationResult<Map<String, dynamic>>> getObjectFlag(
String flagKey,
Map<String, dynamic> defaultValue, {
Map<String, dynamic>? context,
}) async {
return FlagEvaluationResult(
flagKey: flagKey,
value: {'key': 'value'},
evaluatedAt: DateTime.now(),
evaluatorId: name,
);
}
}
Built a new provider? Let us know so we can add it to the docs.
Develop a hook
To develop a hook, create a new project and include the OpenFeature SDK as a dependency. This can live in a new repository or in the existing contrib repository. Implement your own hook by using the hook interfaces exported by the SDK.
import 'package:openfeature_dart_server_sdk/hooks.dart';
class MyCustomHook extends BaseHook {
MyCustomHook()
: super(metadata: HookMetadata(name: 'MyCustomHook'));
@override
Future<Map<String, dynamic>?> before(HookContext context) async {
print('Before evaluating flag: ${context.flagKey}');
return {
'requestSource': 'my-hook',
};
}
@override
Future<void> after(HookContext context) async {
print('After evaluating flag: ${context.flagKey}, result: ${context.result}');
}
@override
Future<void> error(HookContext context) async {
print('Error evaluating flag: ${context.flagKey}, error: ${context.error}');
}
@override
Future<void> finally_(
HookContext context,
EvaluationDetails? evaluationDetails, [
HookHints? hints,
]) async {
print('Finished evaluating flag: ${context.flagKey}');
}
}
Built a new hook? Let us know so we can add it to the docs.
Testing
Use the InMemoryProvider to set flags for the scope of a test. Use
OpenFeatureAPI.resetInstance() in tearDown to clean up between tests.
import 'package:openfeature_dart_server_sdk/feature_provider.dart';
import 'package:openfeature_dart_server_sdk/open_feature_api.dart';
import 'package:test/test.dart';
void main() {
late OpenFeatureAPI api;
late InMemoryProvider testProvider;
setUp(() async {
api = OpenFeatureAPI();
testProvider = InMemoryProvider({
'test-flag': true,
'string-flag': 'test-value',
});
await api.setProviderAndWait(testProvider);
});
tearDown(() {
OpenFeatureAPI.resetInstance();
});
test('evaluates boolean flag correctly', () async {
final client = api.getClient('test-client');
final result = await client.getBooleanFlag(
'test-flag',
defaultValue: false,
);
expect(result, isTrue);
});
test('evaluates string flag correctly', () async {
final client = api.getClient('test-client');
final result = await client.getStringFlag(
'string-flag',
defaultValue: 'default',
);
expect(result, equals('test-value'));
});
}
Support the project
- Give this repo a star.
- Follow us on social media:
- Twitter: @openfeature
- LinkedIn: OpenFeature
- Join us on Slack
- For more, check out our community page
Contributing
Interested in contributing? Take a look at the CONTRIBUTING guide.
Thanks to everyone that has already contributed
Made with contrib.rocks.