Repository navigation
Conversation
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 1 Pipeline job failed
ℹ️ Info🔄 Datadog auto-retried 1 job - 1 passed on retry Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: 852b843 | Docs | View more details | Give us feedback! |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Codex found one correctness issue but I assume it's non-blocking. Doesn't sound common to register and initialize in different Dart zones
The base branch was changed.
leoromanovsky
left a comment
There was a problem hiding this comment.
Didn't find any actionable defects.
fuzzybinary
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
Might be worth creating a typedef for this Function type.
| /// 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. |
There was a problem hiding this comment.
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. |
| return owner; | ||
| } | ||
|
|
||
| Future<void> _flush() => Future<void>.delayed(Duration.zero); |
There was a problem hiding this comment.
It might be better to be more specific about what this is flushing.
| ## 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. |
There was a problem hiding this comment.
Changelog items are generated from the release process.
| `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. |
There was a problem hiding this comment.
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.
| 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`. |
There was a problem hiding this comment.
This definitely doesn't belong in the README IMO. Remember this README becomes the front page documentation of the package.
| expect(const String.fromEnvironment('DD_CLIENT_TOKEN'), isNotEmpty, | ||
| reason: 'Run with --dart-define=DD_CLIENT_TOKEN=test-token'); |
There was a problem hiding this comment.
Does datadog_flags_flutter not use the .env file the rest of the packages setup through melos?
| ### 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. |
There was a problem hiding this comment.
This is internal instructions for the team and does not belong here.
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.
7a70427 to
852b843
Compare
What and why?
Applications need to react when the first usable flag configuration is installed, including cached flags available before network initialization finishes. Add
onFirstFlagsdirectly to the Dart and Flutter flags clients: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
DatadogFlagsClientimplementations 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.
FlagsClientEventkeeps its constructor annotated@internaland 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 indispose; it uses the repository's.envconvention.The Flutter app registers and cleans up as follows (exact excerpt):
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