Skip to content

feat(flags): add retained first-flags callbacks to Dart and Flutter clients - #1204

Open
typotter wants to merge 1 commit into
developfrom
codex/flutter-first-flags-retained
Open

typotter wants to merge 1 commit into
developfrom
codex/flutter-first-flags-retained

Conversation

@typotter

@typotter typotter commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

What and why?

Applications need to react when the first usable flag configuration is installed, including cached flags available before network initialization finishes. Add onFirstFlags directly to the Dart and Flutter flags clients:

void Function() onFirstFlags(void Function(FlagsClientEvent event) listener);

Each registration receives the first accepted configuration's immutable key inventory once, including an empty list for an empty configuration. Late registrations receive the retained event. If no usable configuration is installed, the registration can remain pending until cancelled. Evaluations in the callback read current flag values.

This is a source-breaking interface addition: custom DatadogFlagsClient implementations must implement the method. The core and Flutter package release versions and minimum dependency must be coordinated before publication.

How?

Core retains the first event, binds listeners to their registration zones and schedules delivery in a later microtask. The returned unregister function is idempotent and suppresses delivery until the callback starts. Callback references are cleared on cancellation or delivery. Synchronous listener errors are isolated; the application owns errors from asynchronous work started by its callback.

Flutter resolves its underlying client and forwards registration, delivery and cancellation. Cancellation also works while that resolution is pending. FlagsClientEvent keeps its constructor annotated @internal and defensively copies its key list.

The CLI example prints the installed keys and evaluates its selected flag. The Flutter example registers in initState, displays a callback evaluation and unregisters in dispose; it uses the repository's .env convention.

The Flutter app registers and cleans up as follows (exact excerpt):

@override
void initState() {
  super.initState();
  final client = DatadogSdk.instance.flags?.sharedClient();
  if (widget.isConfigured && client != null) {
    _unregisterFirstFlags = client.onFirstFlags((event) {
      debugPrint('First installed flags: ${event.flagsChanged}');
      final details = client.getBooleanDetails(
        key: _flagKey,
        defaultValue: false,
      );
      if (mounted) setState(() => _firstDetails = details);
    });
  }
  unawaited(_evaluate());
}

@override
void dispose() {
  _unregisterFirstFlags?.call();
  super.dispose();
}

Local validation: 125 core tests and 23 Flutter tests passed, including controlled microtask/resolver tests and real-core Flutter RUM integration. Both packages and examples analyse cleanly; 49 Dart files pass formatting checks. The CLI executable and Flutter debug bundle build successfully. These checks use companion source packages and do not establish native-device or published-package compatibility.

Review checklist

  • This pull request has appropriate unit and / or integration tests
  • This pull request references a Github or JIRA issue

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

DataDog/dd-sdk-flutter | ios-integration-test — 🔄 Retry may pass, looks flaky

View more details · View in GitLab

ℹ️ Info

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 852b843 | Docs | View more details | Give us feedback!

@typotter
typotter marked this pull request as ready for review October 5, 2026 16:53
@typotter
typotter requested review from a team as code owners October 5, 2026 16:53
@typotter
typotter requested review from sameerank and vjfridge and removed request for a team October 5, 2026 16:53
@drichards-87 drichards-87 self-assigned this Oct 5, 2026
@typotter
typotter requested review from leoromanovsky and removed request for vjfridge October 5, 2026 18:17
drichards-87
drichards-87 previously approved these changes Oct 5, 2026
@drichards-87 drichards-87 removed their assignment Oct 5, 2026
@typotter
typotter requested a balanced review from Copilot October 5, 2026 18:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Cross-package asynchronous lifecycle, weak-reference, and cancellation semantics warrant final human validation despite extensive coverage.

Review effort: Balanced
Findings: None

What changed in this PR

Adds retained first-install flag callbacks across Dart core and Flutter, including cancellation, documentation, examples, and comprehensive tests.

Changes:

  • Adds immutable flag events and an event facade backed by weak client associations.
  • Implements one-shot asynchronous delivery and cancellation in core and Flutter.
  • Updates examples, documentation, and lifecycle/GC test coverage.
File Description
packages/​datadog_flags/​lib/​datadog_flags.dart Exports event APIs.
packages/​datadog_flags/​lib/​datadog_flags_internal.dart Exports the companion-package bridge.
packages/​datadog_flags/​lib/​src/​default_flags_client.dart Registers core event delivery.
packages/​datadog_flags/​lib/​src/​flags_client.dart Clarifies initialization documentation.
packages/​datadog_flags/​lib/​src/​flags_client_event.dart Defines immutable event data.
packages/​datadog_flags/​lib/​src/​flags_event_registry.dart Adds weak event-source associations.
packages/​datadog_flags/​lib/​src/​flags_events.dart Adds the public facade and extension.
packages/​datadog_flags/​lib/​src/​flags_repository.dart Retains and delivers the first event.
packages/​datadog_flags/​lib/​src/​no_op_flags_client.dart Supports no-op registration.
packages/​datadog_flags/​test/​flags_events_compatibility_test.dart Tests legacy-client compatibility.
packages/​datadog_flags/​test/​flags_client_event_test.dart Tests event immutability and type mapping.
packages/​datadog_flags/​test/​first_flags_registration_test.dart Tests registration and cancellation.
packages/​datadog_flags/​test/​first_flags_callback_test.dart Tests installation callback behavior.
packages/​datadog_flags/​test_vm/​helpers/​force_gc.dart Adds VM garbage-collection support.
packages/​datadog_flags/​test_vm/​first_flags_capture_test.dart Tests capture release and facade lifetime.
packages/​datadog_flags/​example/​bin/​typed_evaluation.dart Demonstrates first-install callbacks.
packages/​datadog_flags/​example/​test/​typed_evaluation_test.dart Tests the CLI example.
packages/​datadog_flags/​example/​pubspec.yaml Adds example test dependencies.
packages/​datadog_flags/​README.md Documents the core API.
packages/​datadog_flags/​CHANGELOG.md Records the core feature.
packages/​datadog_flags_flutter/​lib/​datadog_flags_flutter.dart Re-exports event APIs.
packages/​datadog_flags_flutter/​lib/​src/​datadog_flags_plugin.dart Forwards registrations to core.
packages/​datadog_flags_flutter/​test/​helpers/​first_flags_test_client.dart Adds a wrapper test factory.
packages/​datadog_flags_flutter/​test/​flags_events_compatibility_test.dart Tests Flutter compatibility.
packages/​datadog_flags_flutter/​test/​first_flags_registration_test.dart Tests forwarding and cancellation.
packages/​datadog_flags_flutter/​test_vm/​helpers/​force_gc.dart Adds Flutter VM GC support.
packages/​datadog_flags_flutter/​test_vm/​first_flags_capture_test.dart Tests wrapper capture release.
packages/​datadog_flags_flutter/​example/​lib/​main.dart Demonstrates callback lifecycle handling.
packages/​datadog_flags_flutter/​example/​test/​first_flags_example_test.dart Tests the Flutter example.
packages/​datadog_flags_flutter/​example/​pubspec.yaml Adds example test dependencies.
packages/​datadog_flags_flutter/​README.md Documents Flutter behavior and release coordination.
packages/​datadog_flags_flutter/​CHANGELOG.md Records the Flutter feature.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

sameerank
sameerank previously approved these changes Oct 5, 2026

@sameerank sameerank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex found one correctness issue but I assume it's non-blocking. Doesn't sound common to register and initialize in different Dart zones

Comment thread packages/datadog_flags/lib/src/flags_repository.dart
Base automatically changed from typo/flutter-cached-reason to develop October 5, 2026 20:09
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot dismissed stale reviews from sameerank and drichards-87 October 5, 2026 20:09

The base branch was changed.

@typotter typotter changed the title feat: retain first flags events for Dart and Flutter client callbacks feat: add retained first flags callbacks directly to Dart and Flutter clients Oct 6, 2026
@typotter
typotter requested a balanced review from Copilot October 6, 2026 20:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The source-breaking cross-package API and deferred release-version coordination require final human validation.

Review effort: Balanced
Findings: None

leoromanovsky
leoromanovsky previously approved these changes Oct 6, 2026

@leoromanovsky leoromanovsky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't find any actionable defects.

@fuzzybinary fuzzybinary left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two very big things here that I'm concerned about:

  • Calling the GC directly, even in tests. Unless I'm mistaken, that is not publicly documented, and we have no control over what Dart does with that call, or even what type of GC it might trigger.
  • The additions both README are way too detailed for public facing documentation and in some cases are specifically instructions to us as maintainers. If we want to retain that information, it needs to go somewhere else.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What type of errors would this catch that wouldn't be caught with other tests? I feel like unit testing the example adds more maintenance without much benefit...


@override
void Function() onFirstFlags(
void Function(FlagsClientEvent event) listener) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth creating a typedef for this Function type.

Comment on lines +19 to +41
/// Registers [listener] for this client's first accepted cache or network
/// installation, including an empty configuration. Every registration receives
/// the retained first event once, even if registered after later updates.
/// The event contains every key in that accepted configuration, not one event
/// per flag or a catalog of every server-side flag. Valid empty configurations
/// notify with an empty key list. Missing/rejected cache entries and failed or
/// undecodable responses do not consume the notification.
///
/// Delivery always runs in a microtask in the registration zone, including
/// late registrations. It does not wait for initialization or persistence to
/// complete. Evaluations in the callback read current assignments, not a
/// snapshot pinned to the event. Registration and replay perform no SDK I/O;
/// callback code may itself evaluate flags or start other work.
/// Synchronous thrown objects (including Exception and Error) are isolated;
/// asynchronous work and errors started by the callback belong to the
/// application.
///
/// Returns an idempotent unregister function. It releases the callback and
/// suppresses delivery that has not started, including an already queued
/// microtask. It cannot interrupt a running callback, clear the retained event
/// or cancel initialization. Reset does not rearm or erase the first event.
/// Pending callbacks remain until installation or explicit unregistration.
/// Reacquire a shared client after SDK re-enable; registrations do not migrate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this whole comment could be simplified, or at least more understandable. Things like "not one event per flag or a (etc..)" feel like they could be omitted, explaining only what the callback does, not what it doesn't do, unless the user has some reasonable expectation that it would / should do that.

The wording of "do not consume the notification" is odd to me. I'm guessing that means we don't get the callback in the case of an error?

The second paragraph also feels overly verbose. Just knowing it is safe to throw exceptions / errors in the callback, and that it is safe for the callback to start other async work should be enough, if that's indeed what it's saying?

Anyway, please take another read over this and see if we can't simplify it.


import 'package:meta/meta.dart';

/// Implemented flag client event types. Declaring a value does not emit it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?

return owner;
}

Future<void> _flush() => Future<void>.delayed(Duration.zero);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to be more specific about what this is flushing.

Comment thread packages/datadog_flags/CHANGELOG.md Outdated
Comment on lines +3 to +11
## Unreleased

### Features

* Mark `FlagsClientEvent` construction as SDK-internal; applications receive events through `onFirstFlags`.

* Add `onFirstFlags` directly to `DatadogFlagsClient` for retained first-install callbacks with a registration-local unregister function. Custom implementations and test doubles must implement the new member. Coordinate the core and Flutter integration release before publishing; this source change does not assign a release version.

* Retain the first accepted flag-installation event for early and late registrations. Every active registration is delivered once in a microtask; its unregister function suppresses delivery not yet started.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog items are generated from the release process.

Comment thread packages/datadog_flags/README.md Outdated
Comment on lines +398 to +407
`FlagsClientEvent` has only `type` and nullable `flagsChanged`. The implemented
type is `FlagsClientEventType.configurationChanged` (`CONFIGURATION_CHANGED`).
Key lists are defensively copied and unmodifiable; null and empty remain distinct.
The event constructor is SDK-internal (`@internal`) and is not a supported
application API; applications receive events through `onFirstFlags`.
The retained event keeps the first installation's keys, even after updates or a
failed refresh. It is not an assignment snapshot: evaluations read current values.
Existing fetch fallback rules still apply; without matching stored assignments,
a failed update can leave evaluations returning defaults while the first event
remains available for replay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this all necessary to be in the README? This seems like it's going into way too much detail on things that aren't really relevant for a user of this functionality.

This whole new block would likely be better as a simple summary, leaving the detailed explanation in the document comments which are pushed to the package documenation.

Comment thread packages/datadog_flags/README.md Outdated
Comment on lines +426 to +428
VM-only capture-release checks use actual garbage collection through the local
VM service: `dart test test_vm/first_flags_capture_test.dart`, or
`flutter test --enable-vmservice test_vm/first_flags_capture_test.dart`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definitely doesn't belong in the README IMO. Remember this README becomes the front page documentation of the package.

Comment on lines +21 to +22
expect(const String.fromEnvironment('DD_CLIENT_TOKEN'), isNotEmpty,
reason: 'Run with --dart-define=DD_CLIENT_TOKEN=test-token');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does datadog_flags_flutter not use the .env file the rest of the packages setup through melos?

Comment on lines +174 to +189
### Coordinated release requirement

`onFirstFlags` is a member of `DatadogFlagsClient` and
`DatadogFlutterFlagsClient`. Custom implementations, decorators, and test doubles
must implement it; decorators can forward directly to their delegate. This is a
source-breaking interface addition. The SDK no-op client accepts registrations
without emitting events. The wrapper's asynchronous delegate resolution remains
best effort.

The repository releaser assigns an explicit release version; this change
does not select one. Before publishing the integration, publish the coordinated
core containing this API and ensure the minimum dependency selects that release.
The wrapper requires `datadog_flags: ^1.2.0` for CACHED support; the published
core selected for this integration must also contain `onFirstFlags`. Local path
overrides validate the companion source only and are not registry compatibility
proof. No package publication is part of this change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is internal instructions for the team and does not belong here.

@barboraplasovska
barboraplasovska removed their request for review October 9, 2026 13:38
Notify applications when the first usable flag configuration is installed. Core retains the immutable key inventory and delivers callbacks asynchronously in their registration zones; Flutter bridges registration and cancellation to core.

Validate with 148 core and Flutter tests, static analysis, formatting, and compiled CLI and Flutter examples.

BREAKING CHANGE: Custom DatadogFlagsClient implementations must implement onFirstFlags.
@typotter
typotter force-pushed the codex/flutter-first-flags-retained branch from 7a70427 to 852b843 Compare October 9, 2026 20:49
@typotter typotter changed the title feat: add retained first flags callbacks directly to Dart and Flutter clients feat(flags): add retained first-flags callbacks to Dart and Flutter clients Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants