Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/datadog_flags/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,30 @@ dart run datadog_flags_example:typed_evaluation \
The repository also includes a Flutter example screen in `examples/simple_example`
that can initialize the SDK, refresh assignments, and evaluate multiple flag
types.

## First installed flags

Register after obtaining the client, before or after initializing its context:

```dart
final client = DatadogFlags.instance.sharedClient();
final unregister = client.onFirstFlags((event) {
print('First installed flags: ${event.flagsChanged}');
final details = client.getBooleanDetails(
key: 'checkout.enabled',
defaultValue: false,
);
print(details.value);
});
// Call unregister() when the application no longer needs this notification.
```

The callback receives the keys from the first accepted cache or network
configuration, including an empty list for an empty configuration. Late
registrations receive the same event. Delivery runs in a later microtask;
evaluations in the callback read current flag values. If no usable configuration
is installed, the callback remains pending until you unregister it.

Call the returned function to cancel delivery before the callback starts.
See the [typed CLI example](example/bin/typed_evaluation.dart) and
`DatadogFlagsClient.onFirstFlags` API documentation for details.
13 changes: 10 additions & 3 deletions packages/datadog_flags/example/bin/typed_evaluation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Future<void> main(List<String> arguments) async {
);

final flags = datadogFlags.sharedClient();
final unregister = flags.onFirstFlags((event) {
stdout.writeln('First installed flags: ${event.flagsChanged}');
_printDetails(_evaluate(flags, flagKey, flagType));
});
try {
await flags.initialize(
FlagsEvaluationContext(
Expand All @@ -56,14 +60,17 @@ Future<void> main(List<String> arguments) async {
stderr.writeln(error.message);
}

final details = _evaluate(flags, flagKey, flagType);
_printDetails(_evaluate(flags, flagKey, flagType));
unregister();
await datadogFlags.disable();
}

void _printDetails(FlagDetails<Object?> details) {
stdout.writeln('key: ${details.key}');
stdout.writeln('value: ${jsonEncode(details.value)}');
stdout.writeln('variant: ${details.variant ?? '(none)'}');
stdout.writeln('reason: ${details.reason ?? '(none)'}');
stdout.writeln('error: ${details.error?.name ?? '(none)'}');

await datadogFlags.disable();
}

ArgParser _argumentParser() {
Expand Down
3 changes: 3 additions & 0 deletions packages/datadog_flags/lib/datadog_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ export 'src/flags_error.dart'
show FlagEvaluationError, FlagsInitializationTimeoutException;
export 'src/flags_store.dart' show DatadogFlagsStore, FlagsData;
export 'src/evaluation_context.dart' show FlagsEvaluationContext;

export 'src/flags_client_event.dart'
show FlagsClientEvent, FlagsClientEventType, FlagsClientEventListener;
5 changes: 5 additions & 0 deletions packages/datadog_flags/lib/src/default_flags_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'evaluation_context.dart';
import 'exposure_logger.dart';
import 'flags_client.dart';
import 'flags_error.dart';
import 'flags_client_event.dart';
import 'flags_repository.dart';

class DefaultDatadogFlagsClient implements DatadogFlagsClient {
Expand All @@ -29,6 +30,10 @@ class DefaultDatadogFlagsClient implements DatadogFlagsClient {
_exposureLogger = exposureLogger,
_evaluationAggregator = evaluationAggregator;

@override
void Function() onFirstFlags(FlagsClientEventListener listener) =>
_repository.onFirstFlags(listener);

@override
Future<void> initialize(FlagsEvaluationContext context) async {
await _repository.initialize(context);
Expand Down
10 changes: 9 additions & 1 deletion packages/datadog_flags/lib/src/flags_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import 'evaluation_context.dart';
import 'flags_error.dart';
import 'flags_client_event.dart';

/// Evaluates feature flags for one current evaluation context.
///
Expand All @@ -15,6 +16,13 @@ abstract interface class DatadogFlagsClient {
/// Stable name assigned by [DatadogFlags.sharedClient].
String get name;

/// Calls [listener] once with the first accepted cached or network configuration.
///
/// Returns an unregister function.
void Function() onFirstFlags(
FlagsClientEventListener listener,
);

/// Fetches assignments for [context] and makes them available to evaluations.
///
/// The first call completes when initialization finishes or the configured
Expand All @@ -26,7 +34,7 @@ abstract interface class DatadogFlagsClient {
/// If a later call supersedes the first call, the first call remains bounded
/// by its original deadline. The later call does not use this timeout.
///
/// Evaluations made before initialization completes return their provided
/// Evaluations made before assignments are available return their provided
/// default value with a `providerNotReady` error.
Future<void> initialize(
FlagsEvaluationContext context,
Expand Down
42 changes: 42 additions & 0 deletions packages/datadog_flags/lib/src/flags_client_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0. This product includes software
// developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import 'package:meta/meta.dart';

/// Receives an event emitted by a flag client.
typedef FlagsClientEventListener = void Function(FlagsClientEvent event);

/// The kinds of notifications emitted by a flag client.
enum FlagsClientEventType {
/// A flag configuration has been installed.
configurationChanged('CONFIGURATION_CHANGED');

/// Cross-SDK event name corresponding to this Dart case.
final String code;

const FlagsClientEventType(this.code);
}

/// Immutable event data, independent of event registration and delivery.
@immutable
final class FlagsClientEvent {
/// The kind of event represented by this value.
final FlagsClientEventType type;

/// Keys in the accepted configuration represented by this event.
///
/// `null` means keys were not supplied; an empty list means an explicitly
/// supplied empty list. This value does not infer or evaluate any keys.
final List<String>? flagsChanged;

/// SDK-internal event construction; copies [flagsChanged] into an
/// unmodifiable list when supplied. Later changes to the source list cannot
/// change this value.
@internal
FlagsClientEvent({required this.type, List<String>? flagsChanged})
: flagsChanged = flagsChanged == null
? null
: List<String>.unmodifiable(flagsChanged);
}
64 changes: 64 additions & 0 deletions packages/datadog_flags/lib/src/flags_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'assignment.dart';
import 'evaluation_context.dart';
import 'flag_assignments_fetcher.dart';
import 'flags_error.dart';
import 'flags_client_event.dart';
import 'flags_store.dart';
import 'json_value.dart';

Expand All @@ -36,6 +37,9 @@ class FlagsRepository {
@visibleForTesting
final Timer Function(Duration, void Function()) scheduleInitializationTimeout;

FlagsClientEvent? _firstFlagsEvent;
final _firstFlagsRegistrations = <_FirstFlagsRegistration>{};

_InstalledFlagsState? _state;
_CancelToken? _currentToken;
bool _didStartInitialization = false;
Expand All @@ -56,6 +60,33 @@ class FlagsRepository {

bool get isRestoredFromStore => _state?.restoredFromStore ?? false;

void Function() onFirstFlags(FlagsClientEventListener callback) {
final registration = _FirstFlagsRegistration(callback);
final event = _firstFlagsEvent;
if (event != null) {
registration.schedule(event);
} else {
_firstFlagsRegistrations.add(registration);
registration.removePending =
() => _firstFlagsRegistrations.remove(registration);
}
return registration.cancel;
}

void _captureFirstFlags(FlagsData data) {
if (_firstFlagsEvent != null) return;
final event = FlagsClientEvent(
type: FlagsClientEventType.configurationChanged,
flagsChanged: data.flags.keys.toList(),
);
_firstFlagsEvent = event;
final registrations = _firstFlagsRegistrations.toList();
_firstFlagsRegistrations.clear();
for (final registration in registrations) {
registration.schedule(event);
}
}

Future<void> initialize(FlagsEvaluationContext context) {
_currentToken?.cancel();
final token = _CancelToken();
Expand Down Expand Up @@ -102,6 +133,7 @@ class FlagsRepository {
data: matchingCached,
restoredFromStore: true,
);
_captureFirstFlags(matchingCached);
}

try {
Expand All @@ -115,6 +147,7 @@ class FlagsRepository {
date: dateProvider(),
);
_state = _InstalledFlagsState(data: data, restoredFromStore: false);
_captureFirstFlags(data);
await _writeCached(data);
} catch (_) {
if (!token.isCanceled && matchingCached == null) {
Expand Down Expand Up @@ -283,3 +316,34 @@ bool _jsonValuesMatch(Object? left, Object? right) {
}
return left == right;
}

// Only this record owns the app callback. Queued work never copies it, so
// cancellation releases its captures even when a microtask is already queued.
class _FirstFlagsRegistration {
FlagsClientEventListener? _callback;
void Function()? removePending;

_FirstFlagsRegistration(FlagsClientEventListener callback)
: _callback = Zone.current.bindUnaryCallback(callback);

void cancel() {
_callback = null;
final remove = removePending;
removePending = null;
remove?.call();
}

void schedule(FlagsClientEvent event) {
removePending = null;
scheduleMicrotask(() {
final callback = _callback;
_callback = null;
if (callback == null) return;
try {
callback(event);
Comment thread
typotter marked this conversation as resolved.
} catch (_) {
// One application failure must not poison installation or other listeners.
}
});
}
}
6 changes: 5 additions & 1 deletion packages/datadog_flags/lib/src/no_op_flags_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
import 'evaluation_context.dart';
import 'flags_client.dart';
import 'flags_error.dart';
import 'flags_client_event.dart';

class NoOpDatadogFlagsClient implements DatadogFlagsClient {
@override
final String name;

const NoOpDatadogFlagsClient({required this.name});
NoOpDatadogFlagsClient({required this.name});

@override
void Function() onFirstFlags(FlagsClientEventListener listener) => () {};

@override
Future<void> initialize(FlagsEvaluationContext context) async {}
Expand Down
3 changes: 2 additions & 1 deletion packages/datadog_flags/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ environment:
dependencies:
http: ^1.0.0
json_annotation: ^4.9.0
meta: ^1.0.0
meta: ^1.3.0
uuid: ^4.0.0

dev_dependencies:
fake_async: ^1.3.1
build_runner: 2.4.15
json_serializable: 6.9.5
lints: '>=5.0.0'
Expand Down
Loading