diff --git a/CHANGELOG.md b/CHANGELOG.md index dc04f50b02..748b05fcc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog -### Unreleased +# Unreleased + +### Features + +- Report Flutter framework feature flags ([#2991](https://github.com/getsentry/sentry-dart/pull/2991)) + - Search for feature flags that are prefixed with `flutter:*` + - This works on Flutter builds that include [this PR](https://github.com/flutter/flutter/pull/171545) ### Fixes diff --git a/flutter/lib/src/integrations/flutter_framework_feature_flag_integration.dart b/flutter/lib/src/integrations/flutter_framework_feature_flag_integration.dart new file mode 100644 index 0000000000..101d7188b6 --- /dev/null +++ b/flutter/lib/src/integrations/flutter_framework_feature_flag_integration.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:sentry/sentry.dart'; + +const _featureFlag = 'FLUTTER_ENABLED_FEATURE_FLAGS'; + +// The Flutter framework feature flag works like this: +// An enabled (experimental) feature gets added to the `FLUTTER_ENABLED_FEATURE_FLAGS` +// dart define. Being in there means the feature is enabled, the feature is disabled +// if it's not in there. +// As a result, we also don't know the whole list of flags, but only the active ones. +// +// See +// - https://github.com/flutter/flutter/pull/168437 +// - https://github.com/flutter/flutter/pull/171545 +// +// The Flutter feature flag implementation is not meant to be public and can change in a patch release. +// See this discussion https://github.com/getsentry/sentry-dart/pull/2991/files#r2183105202 +class FlutterFrameworkFeatureFlagIntegration + extends Integration { + final String flags; + + FlutterFrameworkFeatureFlagIntegration({ + @visibleForTesting this.flags = const String.fromEnvironment(_featureFlag), + }); + + @override + FutureOr call(Hub hub, SentryOptions options) { + final enabledFeatureFlags = flags.split(','); + + for (final featureFlag in enabledFeatureFlags) { + Sentry.addFeatureFlag('flutter:$featureFlag', true); + } + options.sdk.addIntegration('FlutterFrameworkFeatureFlag'); + } +} + +extension FlutterFrameworkFeatureFlagIntegrationX + on List> { + /// For better tree-shake-ability we only add the integration if any feature flag is enabled. + void addFlutterFrameworkFeatureFlagIntegration() { + if (const bool.hasEnvironment(_featureFlag)) { + add(FlutterFrameworkFeatureFlagIntegration()); + } + } +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 73ad235108..4a82983fcb 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -18,6 +18,7 @@ import 'file_system_transport.dart'; import 'flutter_exception_type_identifier.dart'; import 'frame_callback_handler.dart'; import 'integrations/connectivity/connectivity_integration.dart'; +import 'integrations/flutter_framework_feature_flag_integration.dart'; import 'integrations/frames_tracking_integration.dart'; import 'integrations/integrations.dart'; import 'integrations/native_app_start_handler.dart'; @@ -172,6 +173,9 @@ mixin SentryFlutter { // This tracks Flutter application events, such as lifecycle events. integrations.add(WidgetsBindingIntegration()); + // Adds Flutter framework feature flags. + integrations.addFlutterFrameworkFeatureFlagIntegration(); + // The ordering here matters, as we'd like to first start the native integration. // That allow us to send events to the network and then the Flutter integrations. final native = _native; diff --git a/flutter/test/integrations/flutter_framework_feature_flag_integration_test.dart b/flutter/test/integrations/flutter_framework_feature_flag_integration_test.dart new file mode 100644 index 0000000000..e8d4016731 --- /dev/null +++ b/flutter/test/integrations/flutter_framework_feature_flag_integration_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry_flutter/src/integrations/flutter_framework_feature_flag_integration.dart'; + +void main() { + group(FlutterFrameworkFeatureFlagIntegration, () { + late Fixture fixture; + + setUp(() async { + fixture = Fixture(); + + await Sentry.init((options) { + options.dsn = 'https://example.com/sentry-dsn'; + }); + + // ignore: invalid_use_of_internal_member + fixture.hub = Sentry.currentHub; + // ignore: invalid_use_of_internal_member + fixture.options = fixture.hub.options; + }); + + tearDown(() { + Sentry.close(); + }); + + test('adds sdk integration', () { + final sut = fixture.getSut('foo,bar,baz'); + sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations + .contains('FlutterFrameworkFeatureFlag'), + true); + }); + + test('adds feature flags', () { + final sut = fixture.getSut('foo,bar,baz'); + sut.call(fixture.hub, fixture.options); + + // ignore: invalid_use_of_internal_member + final featureFlags = fixture.hub.scope.contexts[SentryFeatureFlags.type] + as SentryFeatureFlags?; + + expect(featureFlags, isNotNull); + expect(featureFlags?.values.length, 3); + expect(featureFlags?.values.first.flag, 'flutter:foo'); + expect(featureFlags?.values.first.result, true); + expect(featureFlags?.values[1].flag, 'flutter:bar'); + expect(featureFlags?.values[1].result, true); + expect(featureFlags?.values[2].flag, 'flutter:baz'); + expect(featureFlags?.values[2].result, true); + }); + }); +} + +class Fixture { + late Hub hub; + late SentryOptions options; + + FlutterFrameworkFeatureFlagIntegration getSut(String features) { + return FlutterFrameworkFeatureFlagIntegration(flags: features); + } +}