From 9d77e528a3379e936175e77ad5aed3344728bbba Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 31 Mar 2025 13:55:43 +0200 Subject: [PATCH 01/29] add feature flag models --- dart/lib/src/protocol.dart | 2 + dart/lib/src/protocol/contexts.dart | 21 ++++++++ .../lib/src/protocol/sentry_feature_flag.dart | 50 ++++++++++++++++++ .../src/protocol/sentry_feature_flags.dart | 52 +++++++++++++++++++ dart/test/contexts_test.dart | 36 +++++++++++-- .../protocol/sentry_feature_flag_tests.dart | 38 ++++++++++++++ .../protocol/sentry_feature_flags_tests.dart | 42 +++++++++++++++ 7 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 dart/lib/src/protocol/sentry_feature_flag.dart create mode 100644 dart/lib/src/protocol/sentry_feature_flags.dart create mode 100644 dart/test/protocol/sentry_feature_flag_tests.dart create mode 100644 dart/test/protocol/sentry_feature_flags_tests.dart diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index e8bfb8d460..4127259d77 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -39,3 +39,5 @@ export 'protocol/sentry_view_hierarchy_element.dart'; export 'protocol/span_id.dart'; export 'protocol/span_status.dart'; export 'sentry_event_like.dart'; +export 'protocol/sentry_feature_flag.dart'; +export 'protocol/sentry_feature_flags.dart'; diff --git a/dart/lib/src/protocol/contexts.dart b/dart/lib/src/protocol/contexts.dart index 24e5093d9c..72d3247bff 100644 --- a/dart/lib/src/protocol/contexts.dart +++ b/dart/lib/src/protocol/contexts.dart @@ -21,6 +21,7 @@ class Contexts extends MapView { SentryTraceContext? trace, SentryResponse? response, SentryFeedback? feedback, + SentryFeatureFlags? flags, }) : super({ SentryDevice.type: device, SentryOperatingSystem.type: operatingSystem, @@ -32,6 +33,7 @@ class Contexts extends MapView { SentryTraceContext.type: trace, SentryResponse.type: response, SentryFeedback.type: feedback, + SentryFeatureFlags.type: flags, }); /// Deserializes [Contexts] from JSON [Map]. @@ -68,6 +70,9 @@ class Contexts extends MapView { feedback: data[SentryFeedback.type] != null ? SentryFeedback.fromJson(Map.from(data[SentryFeedback.type])) : null, + flags: data[SentryFeatureFlags.type] != null + ? SentryFeatureFlags.fromJson(Map.from(data[SentryFeatureFlags.type])) + : null, ); data.keys @@ -151,6 +156,11 @@ class Contexts extends MapView { set feedback(SentryFeedback? value) => this[SentryFeedback.type] = value; + /// Feature flags context for a feature flag event. + SentryFeatureFlags? get flags => this[SentryFeatureFlags.type]; + + set flags(SentryFeatureFlags? value) => this[SentryFeatureFlags.type] = value; + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; @@ -250,6 +260,13 @@ class Contexts extends MapView { break; + case SentryFeatureFlags.type: + final flagsMap = flags?.toJson(); + if (flagsMap?.isNotEmpty ?? false) { + json[SentryFeatureFlags.type] = flagsMap; + } + break; + default: if (value != null) { json[key] = value; @@ -272,6 +289,7 @@ class Contexts extends MapView { response: response?.clone(), runtimes: runtimes.map((runtime) => runtime.clone()).toList(), feedback: feedback?.clone(), + flags: flags?.clone(), )..addEntries( entries.where((element) => !_defaultFields.contains(element.key)), ); @@ -290,6 +308,7 @@ class Contexts extends MapView { SentryTraceContext? trace, SentryResponse? response, SentryFeedback? feedback, + SentryFeatureFlags? flags, }) => Contexts( device: device ?? this.device, @@ -303,6 +322,7 @@ class Contexts extends MapView { trace: trace ?? this.trace, response: response ?? this.response, feedback: feedback ?? this.feedback, + flags: flags ?? this.flags, )..addEntries( entries.where((element) => !_defaultFields.contains(element.key)), ); @@ -319,5 +339,6 @@ class Contexts extends MapView { SentryTraceContext.type, SentryResponse.type, SentryFeedback.type, + SentryFeatureFlags.type, ]; } diff --git a/dart/lib/src/protocol/sentry_feature_flag.dart b/dart/lib/src/protocol/sentry_feature_flag.dart new file mode 100644 index 0000000000..1bd172ca58 --- /dev/null +++ b/dart/lib/src/protocol/sentry_feature_flag.dart @@ -0,0 +1,50 @@ +import 'package:meta/meta.dart'; + +import 'access_aware_map.dart'; + +@immutable +class SentryFeatureFlag { + final String name; + final String value; + + @internal + final Map? unknown; + + SentryFeatureFlag({ + required this.name, + required this.value, + this.unknown, + }); + + factory SentryFeatureFlag.fromJson(Map data) { + final json = AccessAwareMap(data); + + return SentryFeatureFlag( + name: json['name'], + value: json['value'], + unknown: json.notAccessed(), + ); + } + + Map toJson() { + return { + ...?unknown, + 'name': name, + 'value': value, + }; + } + + SentryFeatureFlag copyWith({ + String? name, + String? value, + Map? unknown, + }) { + return SentryFeatureFlag( + name: name ?? this.name, + value: value ?? this.value, + unknown: unknown ?? this.unknown, + ); + } + + SentryFeatureFlag clone() => copyWith(); +} diff --git a/dart/lib/src/protocol/sentry_feature_flags.dart b/dart/lib/src/protocol/sentry_feature_flags.dart new file mode 100644 index 0000000000..8aa1eae24e --- /dev/null +++ b/dart/lib/src/protocol/sentry_feature_flags.dart @@ -0,0 +1,52 @@ +import 'package:meta/meta.dart'; +import 'sentry_feature_flag.dart'; +import 'access_aware_map.dart'; + +@immutable +class SentryFeatureFlags { + static const type = 'flags'; + + final List values; + + @internal + final Map? unknown; + + SentryFeatureFlags({ + required this.values, + this.unknown, + }); + + factory SentryFeatureFlags.fromJson(Map data) { + final json = AccessAwareMap(data); + + final valuesValues = json['values'] as List?; + final values = valuesValues + ?.map((e) => SentryFeatureFlag.fromJson(e)) + .toList(growable: false); + + return SentryFeatureFlags( + values: values ?? [], + unknown: json.notAccessed(), + ); + } + + Map toJson() { + return { + ...?unknown, + 'values': values.map((e) => e.toJson()).toList(growable: false), + }; + } + + SentryFeatureFlags copyWith({ + List? values, + Map? unknown, + }) { + return SentryFeatureFlags( + values: values ?? + this.values.map((e) => e.copyWith()).toList(growable: false), + unknown: unknown ?? this.unknown, + ); + } + + SentryFeatureFlags clone() => copyWith(); +} diff --git a/dart/test/contexts_test.dart b/dart/test/contexts_test.dart index 6287572c8c..d4899cc212 100644 --- a/dart/test/contexts_test.dart +++ b/dart/test/contexts_test.dart @@ -47,6 +47,13 @@ void main() { final gpu = SentryGpu(name: 'Radeon', version: '1'); + final flags = SentryFeatureFlags( + values: [ + SentryFeatureFlag(name: 'feature_flag_1', value: 'value_1'), + SentryFeatureFlag(name: 'feature_flag_2', value: 'value_2'), + ], + ); + final contexts = Contexts( device: testDevice, operatingSystem: testOS, @@ -54,6 +61,7 @@ void main() { app: testApp, browser: testBrowser, gpu: gpu, + flags: flags, ) ..['theme'] = {'value': 'material'} ..['version'] = {'value': 9}; @@ -94,6 +102,12 @@ void main() { 'testrt2': {'name': 'testRT2', 'type': 'runtime', 'version': '2.3.1'}, 'theme': {'value': 'material'}, 'version': {'value': 9}, + 'flags': { + 'values': [ + {'name': 'feature_flag_1', 'value': 'value_1'}, + {'name': 'feature_flag_2', 'value': 'value_2'}, + ] + }, }; test('serializes to JSON', () { @@ -121,7 +135,7 @@ void main() { expect( clone.operatingSystem!.toJson(), contexts.operatingSystem!.toJson()); expect(clone.gpu!.toJson(), contexts.gpu!.toJson()); - + expect(clone.flags!.toJson(), contexts.flags!.toJson()); for (final element in contexts.runtimes) { expect( clone.runtimes.where( @@ -183,6 +197,17 @@ void main() { expect(contexts.runtimes.length, 2); expect(contexts.runtimes.last.name, 'testRT2'); }); + + test('set flags', () { + final contexts = Contexts(); + contexts.flags = SentryFeatureFlags( + values: [ + SentryFeatureFlag(name: 'feature_flag_1', value: 'value_1'), + SentryFeatureFlag(name: 'feature_flag_2', value: 'value_2'), + ], + ); + expect(contexts.flags!.toJson(), flags.toJson()); + }); }); group('parse contexts', () { @@ -293,7 +318,12 @@ const jsonContexts = ''' "raw_description":"runtime description RT1 1.0" }, "browser": {"version": "12.3.4"}, - "gpu": {"name": "Radeon", "version": "1"} - + "gpu": {"name": "Radeon", "version": "1"}, + "flags": { + "values": [ + {"name": "feature_flag_1", "value": "value_1"}, + {"name": "feature_flag_2", "value": "value_2"} + ] + } } '''; diff --git a/dart/test/protocol/sentry_feature_flag_tests.dart b/dart/test/protocol/sentry_feature_flag_tests.dart new file mode 100644 index 0000000000..50f022c342 --- /dev/null +++ b/dart/test/protocol/sentry_feature_flag_tests.dart @@ -0,0 +1,38 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + final featureFlag = SentryFeatureFlag( + name: 'feature_flag_1', + value: 'value_1', + unknown: testUnknown, + ); + final featureFlagJson = { + ...testUnknown, + 'name': 'feature_flag_1', + 'value': 'value_1', + }; + + group('json', () { + test('toJson', () { + final json = featureFlag.toJson(); + expect( + DeepCollectionEquality().equals(featureFlagJson, json), + true, + ); + }); + + test('fromJson', () { + final featureFlag = SentryFeatureFlag.fromJson(featureFlagJson); + final json = featureFlag.toJson(); + + expect( + DeepCollectionEquality().equals(featureFlagJson, json), + true, + ); + }); + }); +} diff --git a/dart/test/protocol/sentry_feature_flags_tests.dart b/dart/test/protocol/sentry_feature_flags_tests.dart new file mode 100644 index 0000000000..b661589215 --- /dev/null +++ b/dart/test/protocol/sentry_feature_flags_tests.dart @@ -0,0 +1,42 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + final featureFlags = SentryFeatureFlags( + values: [ + SentryFeatureFlag(name: 'feature_flag_1', value: 'value_1'), + SentryFeatureFlag(name: 'feature_flag_2', value: 'value_2'), + ], + unknown: testUnknown, + ); + final featureFlagsJson = { + ...testUnknown, + 'values': [ + {'name': 'feature_flag_1', 'value': 'value_1'}, + {'name': 'feature_flag_2', 'value': 'value_2'}, + ], + }; + + group('json', () { + test('toJson', () { + final json = featureFlags.toJson(); + expect( + DeepCollectionEquality().equals(featureFlagsJson, json), + true, + ); + }); + + test('fromJson', () { + final featureFlags = SentryFeatureFlags.fromJson(featureFlagsJson); + final json = featureFlags.toJson(); + + expect( + DeepCollectionEquality().equals(featureFlagsJson, json), + true, + ); + }); + }); +} From 16ee1e61d0d4192cc1b8d24e1d69fe0a49fe270a Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 31 Mar 2025 15:12:37 +0200 Subject: [PATCH 02/29] add FeatureFlagsIntegration --- dart/lib/src/feature_flags_integration.dart | 50 ++++++++ dart/lib/src/sentry.dart | 19 +++ dart/test/feature_flags_integration_test.dart | 114 ++++++++++++++++++ dart/test/mocks/mock_hub.dart | 11 +- dart/test/sentry_test.dart | 27 +++++ 5 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 dart/lib/src/feature_flags_integration.dart create mode 100644 dart/test/feature_flags_integration_test.dart diff --git a/dart/lib/src/feature_flags_integration.dart b/dart/lib/src/feature_flags_integration.dart new file mode 100644 index 0000000000..10f7aa4e8e --- /dev/null +++ b/dart/lib/src/feature_flags_integration.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'hub.dart'; +import 'integration.dart'; +import 'sentry_options.dart'; +import 'protocol/sentry_feature_flags.dart'; +import 'protocol/sentry_feature_flag.dart'; + +/// Integration which handles adding feature flags to the scope. +class FeatureFlagsIntegration extends Integration { + Hub? _hub; + + @override + void call(Hub hub, SentryOptions options) { + _hub = hub; + options.sdk.addIntegration('featureFlagsIntegration'); + } + + FutureOr addFeatureFlag(String name, String value) async { + // LRU cache of 100 feature flags on scope + // if the cache is full, remove the oldest feature flag + // if the feature flag is already in the cache, update the value + // if the feature flag is not in the cache, add it + + final flags = + _hub?.scope.contexts[SentryFeatureFlags.type] as SentryFeatureFlags? ?? + SentryFeatureFlags(values: []); + final values = flags.values; + + if (values.length >= 100) { + values.removeAt(0); + } + + final index = values.indexWhere((element) => element.name == name); + if (index != -1) { + values[index] = SentryFeatureFlag(name: name, value: value); + } else { + values.add(SentryFeatureFlag(name: name, value: value)); + } + + final newFlags = flags.copyWith(values: values); + + await _hub?.scope.setContexts(SentryFeatureFlags.type, newFlags); + } + + @override + FutureOr close() { + _hub = null; + } +} diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index d89d26c46c..99722f6701 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -25,6 +25,7 @@ import 'sentry_run_zoned_guarded.dart'; import 'tracing.dart'; import 'transport/data_category.dart'; import 'transport/task_queue.dart'; +import 'feature_flags_integration.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -105,6 +106,8 @@ class Sentry { options.addIntegration(LoadDartDebugImagesIntegration()); } + options.addIntegration(FeatureFlagsIntegration()); + options.addEventProcessor(EnricherEventProcessor(options)); options.addEventProcessor(ExceptionEventProcessor(options)); options.addEventProcessor(DeduplicationEventProcessor(options)); @@ -357,6 +360,22 @@ class Sentry { /// Gets the current active transaction or span bound to the scope. static ISentrySpan? getSpan() => _hub.getSpan(); + static Future addFeatureFlag(String name, String value) async { + final featureFlagsIntegration = currentHub.options.integrations + .whereType() + .firstOrNull; + + if (featureFlagsIntegration == null) { + currentHub.options.logger( + SentryLevel.debug, + 'FeatureFlagsIntegration not found. Make sure Sentry is initialized before accessing the addFeatureFlag API.', + ); + return; + } + + await featureFlagsIntegration.addFeatureFlag(name, value); + } + @internal static Hub get currentHub => _hub; diff --git a/dart/test/feature_flags_integration_test.dart b/dart/test/feature_flags_integration_test.dart new file mode 100644 index 0000000000..8546c31cef --- /dev/null +++ b/dart/test/feature_flags_integration_test.dart @@ -0,0 +1,114 @@ +@TestOn('vm') +library; + +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; +import 'package:sentry/src/feature_flags_integration.dart'; + +import 'test_utils.dart'; +import 'mocks/mock_hub.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('adds itself to sdk.integrations', () { + final sut = fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + + expect(fixture.options.sdk.integrations.contains('featureFlagsIntegration'), + isTrue); + }); + + test('adds feature flag to scope', () async { + final sut = fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + + await sut.addFeatureFlag('foo', 'bar'); + + expect(fixture.hub.scope.contexts[SentryFeatureFlags.type], isNotNull); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.name, + equals('foo')); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, + equals('bar')); + }); + + test('replaces existing feature flag', () async { + final sut = fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + + await sut.addFeatureFlag('foo', 'bar'); + await sut.addFeatureFlag('foo', 'baz'); + + expect(fixture.hub.scope.contexts[SentryFeatureFlags.type], isNotNull); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.name, + equals('foo')); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, + equals('baz')); + }); + + test('removes oldest feature flag when there are more than 100', () async { + final sut = fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + + for (var i = 0; i < 100; i++) { + await sut.addFeatureFlag('foo_$i', 'bar_$i'); + } + + expect(fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.length, + equals(100)); + + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.name, + equals('foo_0')); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, + equals('bar_0')); + + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.name, + equals('foo_99')); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.value, + equals('bar_99')); + + await sut.addFeatureFlag('foo_100', 'bar_100'); + + expect(fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.length, + equals(100)); + + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.name, + equals('foo_1')); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, + equals('bar_1')); + + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.name, + equals('foo_100')); + expect( + fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.value, + equals('bar_100')); + }); +} + +class Fixture { + final hub = MockHub(); + final options = defaultTestOptions(); + + FeatureFlagsIntegration getSut() { + return FeatureFlagsIntegration(); + } +} diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index 2ba6a43774..6b5b280830 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; @@ -21,10 +23,16 @@ class MockHub with NoSuchMethodProvider implements Hub { final _options = defaultTestOptions(); + late Scope _scope; + @override @internal SentryOptions get options => _options; + MockHub() { + _scope = Scope(_options); + } + /// Useful for tests. void reset() { captureEventCalls = []; @@ -37,6 +45,7 @@ class MockHub with NoSuchMethodProvider implements Hub { spanContextCals = 0; captureTransactionCalls = []; getSpanCalls = 0; + _scope = Scope(_options); } @override @@ -130,7 +139,7 @@ class MockHub with NoSuchMethodProvider implements Hub { } @override - Scope get scope => Scope(_options); + Scope get scope => _scope; } class CaptureEventCall { diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 6d519b4948..96ce6fcfad 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/dart_exception_type_identifier.dart'; import 'package:sentry/src/event_processor/deduplication_event_processor.dart'; +import 'package:sentry/src/feature_flags_integration.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -262,6 +263,12 @@ void main() { }, appRunner: appRunner, ); + expect( + optionsReference.integrations + .whereType() + .length, + 1, + ); expect( optionsReference.integrations .whereType() @@ -290,6 +297,26 @@ void main() { ); }, onPlatform: {'vm': Skip()}); + test('should add feature flagg FeatureFlagsIntegration', () async { + await Sentry.init( + options: defaultTestOptions(), + (options) => options.dsn = fakeDsn, + ); + + await Sentry.addFeatureFlag('foo', 'bar'); + + expect( + Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first + .name, + equals('foo'), + ); + expect( + Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first + .value, + equals('bar'), + ); + }); + test('should close integrations', () async { final integration = MockIntegration(); From 956e3880f1257ad73cc8a3d25164b115d3a087da Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 31 Mar 2025 15:46:27 +0200 Subject: [PATCH 03/29] remove comments, add test --- dart/lib/sentry.dart | 2 ++ dart/lib/src/feature_flags_integration.dart | 5 ----- dart/test/scope_test.dart | 10 ++++++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 259a7cf10d..1c6ed0a3ba 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -24,6 +24,8 @@ export 'src/noop_isolate_error_integration.dart' export 'src/observers.dart'; export 'src/performance_collector.dart'; export 'src/protocol.dart'; +export 'src/protocol/sentry_feature_flags.dart'; +export 'src/protocol/sentry_feature_flag.dart'; export 'src/protocol/sentry_feedback.dart'; export 'src/protocol/sentry_proxy.dart'; export 'src/run_zoned_guarded_integration.dart'; diff --git a/dart/lib/src/feature_flags_integration.dart b/dart/lib/src/feature_flags_integration.dart index 10f7aa4e8e..47c05eccc1 100644 --- a/dart/lib/src/feature_flags_integration.dart +++ b/dart/lib/src/feature_flags_integration.dart @@ -17,11 +17,6 @@ class FeatureFlagsIntegration extends Integration { } FutureOr addFeatureFlag(String name, String value) async { - // LRU cache of 100 feature flags on scope - // if the cache is full, remove the oldest feature flag - // if the feature flag is already in the cache, update the value - // if the feature flag is not in the cache, add it - final flags = _hub?.scope.contexts[SentryFeatureFlags.type] as SentryFeatureFlags? ?? SentryFeatureFlags(values: []); diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 59b42e4227..7f2614f08d 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -437,6 +437,11 @@ void main() { await scope.setTag('build', '579'); await scope.setExtra('company-name', 'Dart Inc'); await scope.setContexts('theme', 'material'); + await scope.setContexts( + SentryFeatureFlags.type, + SentryFeatureFlags( + values: [SentryFeatureFlag(name: 'foo', value: 'bar')], + )); await scope.setUser(scopeUser); final updatedEvent = await scope.applyToEvent(event, Hint()); @@ -451,6 +456,11 @@ void main() { expect( updatedEvent?.extra, {'e-infos': 'abc', 'company-name': 'Dart Inc'}); expect(updatedEvent?.contexts['theme'], {'value': 'material'}); + expect(updatedEvent?.contexts[SentryFeatureFlags.type]?.values.first.name, + 'foo'); + expect( + updatedEvent?.contexts[SentryFeatureFlags.type]?.values.first.value, + 'bar'); }); test('apply trace context to event', () async { From 1559285210a650c0323c306c3679db968df33dc5 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 31 Mar 2025 16:08:20 +0200 Subject: [PATCH 04/29] use bool type for value --- CHANGELOG.md | 10 +++++++++ dart/lib/src/feature_flags_integration.dart | 2 +- .../lib/src/protocol/sentry_feature_flag.dart | 4 ++-- dart/lib/src/sentry.dart | 2 +- dart/test/contexts_test.dart | 16 +++++++------- dart/test/feature_flags_integration_test.dart | 22 +++++++++---------- .../protocol/sentry_feature_flag_tests.dart | 4 ++-- .../protocol/sentry_feature_flags_tests.dart | 8 +++---- dart/test/scope_test.dart | 4 ++-- dart/test/sentry_test.dart | 4 ++-- 10 files changed, 43 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c9c4026e..6da6e9da95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### Features + +- Add `FeatureFlagIntegration` ([#2825](https://github.com/getsentry/sentry-dart/pull/2825)) +```dart +// Manually track a feature flag +Sentry.addFeatureFlag('my-feature', true); +``` + ## 9.0.0-alpha.2 ### Features diff --git a/dart/lib/src/feature_flags_integration.dart b/dart/lib/src/feature_flags_integration.dart index 47c05eccc1..7d40f2ecdf 100644 --- a/dart/lib/src/feature_flags_integration.dart +++ b/dart/lib/src/feature_flags_integration.dart @@ -16,7 +16,7 @@ class FeatureFlagsIntegration extends Integration { options.sdk.addIntegration('featureFlagsIntegration'); } - FutureOr addFeatureFlag(String name, String value) async { + FutureOr addFeatureFlag(String name, bool value) async { final flags = _hub?.scope.contexts[SentryFeatureFlags.type] as SentryFeatureFlags? ?? SentryFeatureFlags(values: []); diff --git a/dart/lib/src/protocol/sentry_feature_flag.dart b/dart/lib/src/protocol/sentry_feature_flag.dart index 1bd172ca58..6d73f3c56d 100644 --- a/dart/lib/src/protocol/sentry_feature_flag.dart +++ b/dart/lib/src/protocol/sentry_feature_flag.dart @@ -5,7 +5,7 @@ import 'access_aware_map.dart'; @immutable class SentryFeatureFlag { final String name; - final String value; + final bool value; @internal final Map? unknown; @@ -36,7 +36,7 @@ class SentryFeatureFlag { SentryFeatureFlag copyWith({ String? name, - String? value, + bool? value, Map? unknown, }) { return SentryFeatureFlag( diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 99722f6701..1055740cb8 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -360,7 +360,7 @@ class Sentry { /// Gets the current active transaction or span bound to the scope. static ISentrySpan? getSpan() => _hub.getSpan(); - static Future addFeatureFlag(String name, String value) async { + static Future addFeatureFlag(String name, bool value) async { final featureFlagsIntegration = currentHub.options.integrations .whereType() .firstOrNull; diff --git a/dart/test/contexts_test.dart b/dart/test/contexts_test.dart index d4899cc212..934969270a 100644 --- a/dart/test/contexts_test.dart +++ b/dart/test/contexts_test.dart @@ -49,8 +49,8 @@ void main() { final flags = SentryFeatureFlags( values: [ - SentryFeatureFlag(name: 'feature_flag_1', value: 'value_1'), - SentryFeatureFlag(name: 'feature_flag_2', value: 'value_2'), + SentryFeatureFlag(name: 'feature_flag_1', value: true), + SentryFeatureFlag(name: 'feature_flag_2', value: false), ], ); @@ -104,8 +104,8 @@ void main() { 'version': {'value': 9}, 'flags': { 'values': [ - {'name': 'feature_flag_1', 'value': 'value_1'}, - {'name': 'feature_flag_2', 'value': 'value_2'}, + {'name': 'feature_flag_1', 'value': true}, + {'name': 'feature_flag_2', 'value': false}, ] }, }; @@ -202,8 +202,8 @@ void main() { final contexts = Contexts(); contexts.flags = SentryFeatureFlags( values: [ - SentryFeatureFlag(name: 'feature_flag_1', value: 'value_1'), - SentryFeatureFlag(name: 'feature_flag_2', value: 'value_2'), + SentryFeatureFlag(name: 'feature_flag_1', value: true), + SentryFeatureFlag(name: 'feature_flag_2', value: false), ], ); expect(contexts.flags!.toJson(), flags.toJson()); @@ -321,8 +321,8 @@ const jsonContexts = ''' "gpu": {"name": "Radeon", "version": "1"}, "flags": { "values": [ - {"name": "feature_flag_1", "value": "value_1"}, - {"name": "feature_flag_2", "value": "value_2"} + {"name": "feature_flag_1", "value": true}, + {"name": "feature_flag_2", "value": false} ] } } diff --git a/dart/test/feature_flags_integration_test.dart b/dart/test/feature_flags_integration_test.dart index 8546c31cef..5a8e4b9d17 100644 --- a/dart/test/feature_flags_integration_test.dart +++ b/dart/test/feature_flags_integration_test.dart @@ -29,7 +29,7 @@ void main() { sut.call(fixture.hub, fixture.options); - await sut.addFeatureFlag('foo', 'bar'); + await sut.addFeatureFlag('foo', true); expect(fixture.hub.scope.contexts[SentryFeatureFlags.type], isNotNull); expect( @@ -37,7 +37,7 @@ void main() { equals('foo')); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, - equals('bar')); + equals(true)); }); test('replaces existing feature flag', () async { @@ -45,8 +45,8 @@ void main() { sut.call(fixture.hub, fixture.options); - await sut.addFeatureFlag('foo', 'bar'); - await sut.addFeatureFlag('foo', 'baz'); + await sut.addFeatureFlag('foo', true); + await sut.addFeatureFlag('foo', false); expect(fixture.hub.scope.contexts[SentryFeatureFlags.type], isNotNull); expect( @@ -54,7 +54,7 @@ void main() { equals('foo')); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, - equals('baz')); + equals(false)); }); test('removes oldest feature flag when there are more than 100', () async { @@ -63,7 +63,7 @@ void main() { sut.call(fixture.hub, fixture.options); for (var i = 0; i < 100; i++) { - await sut.addFeatureFlag('foo_$i', 'bar_$i'); + await sut.addFeatureFlag('foo_$i', i % 2 == 0 ? true : false); } expect(fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.length, @@ -74,16 +74,16 @@ void main() { equals('foo_0')); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, - equals('bar_0')); + equals(true)); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.name, equals('foo_99')); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.value, - equals('bar_99')); + equals(false)); - await sut.addFeatureFlag('foo_100', 'bar_100'); + await sut.addFeatureFlag('foo_100', true); expect(fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.length, equals(100)); @@ -93,14 +93,14 @@ void main() { equals('foo_1')); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.first.value, - equals('bar_1')); + equals(false)); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.name, equals('foo_100')); expect( fixture.hub.scope.contexts[SentryFeatureFlags.type]?.values.last.value, - equals('bar_100')); + equals(true)); }); } diff --git a/dart/test/protocol/sentry_feature_flag_tests.dart b/dart/test/protocol/sentry_feature_flag_tests.dart index 50f022c342..2480f07ef2 100644 --- a/dart/test/protocol/sentry_feature_flag_tests.dart +++ b/dart/test/protocol/sentry_feature_flag_tests.dart @@ -7,13 +7,13 @@ import '../mocks.dart'; void main() { final featureFlag = SentryFeatureFlag( name: 'feature_flag_1', - value: 'value_1', + value: true, unknown: testUnknown, ); final featureFlagJson = { ...testUnknown, 'name': 'feature_flag_1', - 'value': 'value_1', + 'value': true, }; group('json', () { diff --git a/dart/test/protocol/sentry_feature_flags_tests.dart b/dart/test/protocol/sentry_feature_flags_tests.dart index b661589215..df7d4bde6c 100644 --- a/dart/test/protocol/sentry_feature_flags_tests.dart +++ b/dart/test/protocol/sentry_feature_flags_tests.dart @@ -7,16 +7,16 @@ import '../mocks.dart'; void main() { final featureFlags = SentryFeatureFlags( values: [ - SentryFeatureFlag(name: 'feature_flag_1', value: 'value_1'), - SentryFeatureFlag(name: 'feature_flag_2', value: 'value_2'), + SentryFeatureFlag(name: 'feature_flag_1', value: true), + SentryFeatureFlag(name: 'feature_flag_2', value: false), ], unknown: testUnknown, ); final featureFlagsJson = { ...testUnknown, 'values': [ - {'name': 'feature_flag_1', 'value': 'value_1'}, - {'name': 'feature_flag_2', 'value': 'value_2'}, + {'name': 'feature_flag_1', 'value': true}, + {'name': 'feature_flag_2', 'value': false}, ], }; diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 7f2614f08d..47000c0a68 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -440,7 +440,7 @@ void main() { await scope.setContexts( SentryFeatureFlags.type, SentryFeatureFlags( - values: [SentryFeatureFlag(name: 'foo', value: 'bar')], + values: [SentryFeatureFlag(name: 'foo', value: true)], )); await scope.setUser(scopeUser); @@ -460,7 +460,7 @@ void main() { 'foo'); expect( updatedEvent?.contexts[SentryFeatureFlags.type]?.values.first.value, - 'bar'); + true); }); test('apply trace context to event', () async { diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 96ce6fcfad..b8131b99a3 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -303,7 +303,7 @@ void main() { (options) => options.dsn = fakeDsn, ); - await Sentry.addFeatureFlag('foo', 'bar'); + await Sentry.addFeatureFlag('foo', true); expect( Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first @@ -313,7 +313,7 @@ void main() { expect( Sentry.currentHub.scope.contexts[SentryFeatureFlags.type]?.values.first .value, - equals('bar'), + equals(true), ); }); From 3944ea1533c9ca5e0e2920014db9a4dc35dac980 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 1 Apr 2025 11:02:58 +0200 Subject: [PATCH 05/29] add sentry_firebase package --- firebase/.gitignore | 14 + firebase/.metadata | 10 + firebase/CHANGELOG.md | 1 + firebase/LICENSE | 21 + firebase/README.md | 9 + firebase/analysis_options.yaml | 32 + firebase/dartdoc_options.yaml | 1 + firebase/lib/sentry_firebase.dart | 3 + .../lib/src/sentry_firebase_integration.dart | 26 + firebase/pubspec.yaml | 32 + firebase/pubspec_overrides.yaml | 3 + firebase/test/mocks/mocks.dart | 9 + firebase/test/mocks/mocks.mocks.dart | 583 ++++++++++++++++++ .../src/sentry_firebase_integration_test.dart | 46 ++ 14 files changed, 790 insertions(+) create mode 100644 firebase/.gitignore create mode 100644 firebase/.metadata create mode 120000 firebase/CHANGELOG.md create mode 100644 firebase/LICENSE create mode 100644 firebase/README.md create mode 100644 firebase/analysis_options.yaml create mode 120000 firebase/dartdoc_options.yaml create mode 100644 firebase/lib/sentry_firebase.dart create mode 100644 firebase/lib/src/sentry_firebase_integration.dart create mode 100644 firebase/pubspec.yaml create mode 100644 firebase/pubspec_overrides.yaml create mode 100644 firebase/test/mocks/mocks.dart create mode 100644 firebase/test/mocks/mocks.mocks.dart create mode 100644 firebase/test/src/sentry_firebase_integration_test.dart diff --git a/firebase/.gitignore b/firebase/.gitignore new file mode 100644 index 0000000000..ba521d5a39 --- /dev/null +++ b/firebase/.gitignore @@ -0,0 +1,14 @@ +# Omit committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ diff --git a/firebase/.metadata b/firebase/.metadata new file mode 100644 index 0000000000..07eecc71cd --- /dev/null +++ b/firebase/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "09de023485e95e6d1225c2baa44b8feb85e0d45f" + channel: "stable" + +project_type: package diff --git a/firebase/CHANGELOG.md b/firebase/CHANGELOG.md new file mode 120000 index 0000000000..04c99a55ca --- /dev/null +++ b/firebase/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/firebase/LICENSE b/firebase/LICENSE new file mode 100644 index 0000000000..2a6964d84d --- /dev/null +++ b/firebase/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/firebase/README.md b/firebase/README.md new file mode 100644 index 0000000000..0b9b31a02e --- /dev/null +++ b/firebase/README.md @@ -0,0 +1,9 @@ +

+ + + +
+

+ +Sentry integration for `firebase_remote_config` package +=========== diff --git a/firebase/analysis_options.yaml b/firebase/analysis_options.yaml new file mode 100644 index 0000000000..7119dc352d --- /dev/null +++ b/firebase/analysis_options.yaml @@ -0,0 +1,32 @@ +include: package:lints/recommended.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: error + # treat missing returns as a warning (not a hint) + missing_return: error + # allow having TODOs in the code + todo: ignore + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: warning + # ignore sentry/path on pubspec as we change it on deployment + invalid_dependency: ignore + exclude: + - example/** + - test/mocks/mocks.mocks.dart + +linter: + rules: + - prefer_final_locals + - prefer_single_quotes + - prefer_relative_imports + - unnecessary_brace_in_string_interps + - implementation_imports + - require_trailing_commas + - unawaited_futures diff --git a/firebase/dartdoc_options.yaml b/firebase/dartdoc_options.yaml new file mode 120000 index 0000000000..7cbb8c0d74 --- /dev/null +++ b/firebase/dartdoc_options.yaml @@ -0,0 +1 @@ +../dart/dartdoc_options.yaml \ No newline at end of file diff --git a/firebase/lib/sentry_firebase.dart b/firebase/lib/sentry_firebase.dart new file mode 100644 index 0000000000..25ddd03f57 --- /dev/null +++ b/firebase/lib/sentry_firebase.dart @@ -0,0 +1,3 @@ +library; + +export 'src/sentry_firebase_integration.dart'; diff --git a/firebase/lib/src/sentry_firebase_integration.dart b/firebase/lib/src/sentry_firebase_integration.dart new file mode 100644 index 0000000000..e8fb2e60c4 --- /dev/null +++ b/firebase/lib/src/sentry_firebase_integration.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:sentry/sentry.dart'; + +class SentryFirebaseIntegration extends Integration { + SentryFirebaseIntegration(this._firebaseRemoteConfig); + + final FirebaseRemoteConfig _firebaseRemoteConfig; + + StreamSubscription? _subscription; + + @override + FutureOr call(Hub hub, SentryOptions options) { + _subscription = _firebaseRemoteConfig.onConfigUpdated.listen((event) { + print(event); + }); + options.sdk.addIntegration('sentryFirebaseIntegration'); + } + + @override + FutureOr close() async { + await _subscription?.cancel(); + _subscription = null; + } +} diff --git a/firebase/pubspec.yaml b/firebase/pubspec.yaml new file mode 100644 index 0000000000..52965e3d68 --- /dev/null +++ b/firebase/pubspec.yaml @@ -0,0 +1,32 @@ +name: sentry_firebase +description: "Sentry integration to use feature flags from Firebase Remote Config." +version: 9.0.0-alpha.2 +homepage: https://docs.sentry.io/platforms/flutter/ +repository: https://github.com/getsentry/sentry-dart +issue_tracker: https://github.com/getsentry/sentry-dart/issues + +environment: + sdk: '>=3.5.0 <4.0.0' + flutter: '>=3.24.0' + +platforms: + android: + ios: + macos: + linux: + windows: + web: + +dependencies: + flutter: + sdk: flutter + firebase_remote_config: ^5.4.3 + sentry: ^9.0.0-alpha.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + coverage: ^1.3.0 + mockito: ^5.1.0 + build_runner: ^2.4.6 diff --git a/firebase/pubspec_overrides.yaml b/firebase/pubspec_overrides.yaml new file mode 100644 index 0000000000..16e71d16f0 --- /dev/null +++ b/firebase/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + sentry: + path: ../dart diff --git a/firebase/test/mocks/mocks.dart b/firebase/test/mocks/mocks.dart new file mode 100644 index 0000000000..581122064f --- /dev/null +++ b/firebase/test/mocks/mocks.dart @@ -0,0 +1,9 @@ +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:mockito/annotations.dart'; +import 'package:sentry/sentry.dart'; + +@GenerateMocks([ + Hub, + FirebaseRemoteConfig, +]) +void main() {} diff --git a/firebase/test/mocks/mocks.mocks.dart b/firebase/test/mocks/mocks.mocks.dart new file mode 100644 index 0000000000..281b90f406 --- /dev/null +++ b/firebase/test/mocks/mocks.mocks.dart @@ -0,0 +1,583 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in sentry_firebase/test/mocks/mocks.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; + +import 'package:firebase_core/firebase_core.dart' as _i3; +import 'package:firebase_remote_config/firebase_remote_config.dart' as _i7; +import 'package:firebase_remote_config_platform_interface/firebase_remote_config_platform_interface.dart' + as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i8; +import 'package:sentry/sentry.dart' as _i2; +import 'package:sentry/src/profiling.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeSentryOptions_0 extends _i1.SmartFake implements _i2.SentryOptions { + _FakeSentryOptions_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSentryId_1 extends _i1.SmartFake implements _i2.SentryId { + _FakeSentryId_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeScope_2 extends _i1.SmartFake implements _i2.Scope { + _FakeScope_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeHub_3 extends _i1.SmartFake implements _i2.Hub { + _FakeHub_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeISentrySpan_4 extends _i1.SmartFake implements _i2.ISentrySpan { + _FakeISentrySpan_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeFirebaseApp_5 extends _i1.SmartFake implements _i3.FirebaseApp { + _FakeFirebaseApp_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeDateTime_6 extends _i1.SmartFake implements DateTime { + _FakeDateTime_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeRemoteConfigSettings_7 extends _i1.SmartFake + implements _i4.RemoteConfigSettings { + _FakeRemoteConfigSettings_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeRemoteConfigValue_8 extends _i1.SmartFake + implements _i4.RemoteConfigValue { + _FakeRemoteConfigValue_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Hub]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHub extends _i1.Mock implements _i2.Hub { + MockHub() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.SentryOptions get options => + (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeSentryOptions_0( + this, + Invocation.getter(#options), + ), + ) + as _i2.SentryOptions); + + @override + bool get isEnabled => + (super.noSuchMethod(Invocation.getter(#isEnabled), returnValue: false) + as bool); + + @override + _i2.SentryId get lastEventId => + (super.noSuchMethod( + Invocation.getter(#lastEventId), + returnValue: _FakeSentryId_1(this, Invocation.getter(#lastEventId)), + ) + as _i2.SentryId); + + @override + _i2.Scope get scope => + (super.noSuchMethod( + Invocation.getter(#scope), + returnValue: _FakeScope_2(this, Invocation.getter(#scope)), + ) + as _i2.Scope); + + @override + // ignore: invalid_use_of_internal_member + set profilerFactory(_i5.SentryProfilerFactory? value) => super.noSuchMethod( + Invocation.setter(#profilerFactory, value), + returnValueForMissingStub: null, + ); + + @override + _i6.Future<_i2.SentryId> captureEvent( + _i2.SentryEvent? event, { + dynamic stackTrace, + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureEvent, + [event], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureEvent, + [event], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + ), + ), + ) + as _i6.Future<_i2.SentryId>); + + @override + _i6.Future<_i2.SentryId> captureException( + dynamic throwable, { + dynamic stackTrace, + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureException, + [throwable], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureException, + [throwable], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + ), + ), + ) + as _i6.Future<_i2.SentryId>); + + @override + _i6.Future<_i2.SentryId> captureMessage( + String? message, { + _i2.SentryLevel? level, + String? template, + List? params, + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureMessage, + [message], + { + #level: level, + #template: template, + #params: params, + #hint: hint, + #withScope: withScope, + }, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureMessage, + [message], + { + #level: level, + #template: template, + #params: params, + #hint: hint, + #withScope: withScope, + }, + ), + ), + ), + ) + as _i6.Future<_i2.SentryId>); + + @override + _i6.Future<_i2.SentryId> captureFeedback( + _i2.SentryFeedback? feedback, { + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureFeedback, + [feedback], + {#hint: hint, #withScope: withScope}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureFeedback, + [feedback], + {#hint: hint, #withScope: withScope}, + ), + ), + ), + ) + as _i6.Future<_i2.SentryId>); + + @override + _i6.Future addBreadcrumb(_i2.Breadcrumb? crumb, {_i2.Hint? hint}) => + (super.noSuchMethod( + Invocation.method(#addBreadcrumb, [crumb], {#hint: hint}), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + void bindClient(_i2.SentryClient? client) => super.noSuchMethod( + Invocation.method(#bindClient, [client]), + returnValueForMissingStub: null, + ); + + @override + _i2.Hub clone() => + (super.noSuchMethod( + Invocation.method(#clone, []), + returnValue: _FakeHub_3(this, Invocation.method(#clone, [])), + ) + as _i2.Hub); + + @override + _i6.Future close() => + (super.noSuchMethod( + Invocation.method(#close, []), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + _i6.FutureOr configureScope(_i2.ScopeCallback? callback) => + (super.noSuchMethod(Invocation.method(#configureScope, [callback])) + as _i6.FutureOr); + + @override + _i2.ISentrySpan startTransaction( + String? name, + String? operation, { + String? description, + DateTime? startTimestamp, + bool? bindToScope, + bool? waitForChildren, + Duration? autoFinishAfter, + bool? trimEnd, + _i2.OnTransactionFinish? onFinish, + Map? customSamplingContext, + }) => + (super.noSuchMethod( + Invocation.method( + #startTransaction, + [name, operation], + { + #description: description, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + #customSamplingContext: customSamplingContext, + }, + ), + returnValue: _FakeISentrySpan_4( + this, + Invocation.method( + #startTransaction, + [name, operation], + { + #description: description, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + #customSamplingContext: customSamplingContext, + }, + ), + ), + ) + as _i2.ISentrySpan); + + @override + _i2.ISentrySpan startTransactionWithContext( + _i2.SentryTransactionContext? transactionContext, { + Map? customSamplingContext, + DateTime? startTimestamp, + bool? bindToScope, + bool? waitForChildren, + Duration? autoFinishAfter, + bool? trimEnd, + _i2.OnTransactionFinish? onFinish, + }) => + (super.noSuchMethod( + Invocation.method( + #startTransactionWithContext, + [transactionContext], + { + #customSamplingContext: customSamplingContext, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + }, + ), + returnValue: _FakeISentrySpan_4( + this, + Invocation.method( + #startTransactionWithContext, + [transactionContext], + { + #customSamplingContext: customSamplingContext, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + }, + ), + ), + ) + as _i2.ISentrySpan); + + @override + _i6.Future<_i2.SentryId> captureTransaction( + _i2.SentryTransaction? transaction, { + _i2.SentryTraceContextHeader? traceContext, + _i2.Hint? hint, + }) => + (super.noSuchMethod( + Invocation.method( + #captureTransaction, + [transaction], + {#traceContext: traceContext, #hint: hint}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureTransaction, + [transaction], + {#traceContext: traceContext, #hint: hint}, + ), + ), + ), + ) + as _i6.Future<_i2.SentryId>); + + @override + void setSpanContext( + dynamic throwable, + _i2.ISentrySpan? span, + String? transaction, + ) => super.noSuchMethod( + Invocation.method(#setSpanContext, [throwable, span, transaction]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [FirebaseRemoteConfig]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseRemoteConfig extends _i1.Mock + implements _i7.FirebaseRemoteConfig { + MockFirebaseRemoteConfig() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.FirebaseApp get app => + (super.noSuchMethod( + Invocation.getter(#app), + returnValue: _FakeFirebaseApp_5(this, Invocation.getter(#app)), + ) + as _i3.FirebaseApp); + + @override + DateTime get lastFetchTime => + (super.noSuchMethod( + Invocation.getter(#lastFetchTime), + returnValue: _FakeDateTime_6( + this, + Invocation.getter(#lastFetchTime), + ), + ) + as DateTime); + + @override + _i4.RemoteConfigFetchStatus get lastFetchStatus => + (super.noSuchMethod( + Invocation.getter(#lastFetchStatus), + returnValue: _i4.RemoteConfigFetchStatus.noFetchYet, + ) + as _i4.RemoteConfigFetchStatus); + + @override + _i4.RemoteConfigSettings get settings => + (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeRemoteConfigSettings_7( + this, + Invocation.getter(#settings), + ), + ) + as _i4.RemoteConfigSettings); + + @override + _i6.Stream<_i4.RemoteConfigUpdate> get onConfigUpdated => + (super.noSuchMethod( + Invocation.getter(#onConfigUpdated), + returnValue: _i6.Stream<_i4.RemoteConfigUpdate>.empty(), + ) + as _i6.Stream<_i4.RemoteConfigUpdate>); + + @override + Map get pluginConstants => + (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + ) + as Map); + + @override + _i6.Future activate() => + (super.noSuchMethod( + Invocation.method(#activate, []), + returnValue: _i6.Future.value(false), + ) + as _i6.Future); + + @override + _i6.Future ensureInitialized() => + (super.noSuchMethod( + Invocation.method(#ensureInitialized, []), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + _i6.Future fetch() => + (super.noSuchMethod( + Invocation.method(#fetch, []), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + _i6.Future fetchAndActivate() => + (super.noSuchMethod( + Invocation.method(#fetchAndActivate, []), + returnValue: _i6.Future.value(false), + ) + as _i6.Future); + + @override + Map getAll() => + (super.noSuchMethod( + Invocation.method(#getAll, []), + returnValue: {}, + ) + as Map); + + @override + bool getBool(String? key) => + (super.noSuchMethod( + Invocation.method(#getBool, [key]), + returnValue: false, + ) + as bool); + + @override + int getInt(String? key) => + (super.noSuchMethod(Invocation.method(#getInt, [key]), returnValue: 0) + as int); + + @override + double getDouble(String? key) => + (super.noSuchMethod( + Invocation.method(#getDouble, [key]), + returnValue: 0.0, + ) + as double); + + @override + String getString(String? key) => + (super.noSuchMethod( + Invocation.method(#getString, [key]), + returnValue: _i8.dummyValue( + this, + Invocation.method(#getString, [key]), + ), + ) + as String); + + @override + _i4.RemoteConfigValue getValue(String? key) => + (super.noSuchMethod( + Invocation.method(#getValue, [key]), + returnValue: _FakeRemoteConfigValue_8( + this, + Invocation.method(#getValue, [key]), + ), + ) + as _i4.RemoteConfigValue); + + @override + _i6.Future setConfigSettings( + _i4.RemoteConfigSettings? remoteConfigSettings, + ) => + (super.noSuchMethod( + Invocation.method(#setConfigSettings, [remoteConfigSettings]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + _i6.Future setDefaults(Map? defaultParameters) => + (super.noSuchMethod( + Invocation.method(#setDefaults, [defaultParameters]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + _i6.Future setCustomSignals(Map? customSignals) => + (super.noSuchMethod( + Invocation.method(#setCustomSignals, [customSignals]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); +} diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart new file mode 100644 index 0000000000..5da2c96340 --- /dev/null +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_firebase/src/sentry_firebase_integration.dart'; +import 'package:sentry/sentry.dart'; + +import 'package:mockito/mockito.dart'; +import '../mocks/mocks.mocks.dart'; + +import 'package:firebase_remote_config/firebase_remote_config.dart'; + +void main() { + + late final Fixture fixture; + + givenRemoveConfigUpdate(RemoteConfigUpdate update) { + when(fixture.mockFirebaseRemoteConfig.onConfigUpdated).thenAnswer((_) => Stream.value(update)); + } + + setUp(() { + fixture = Fixture(); + + final update = RemoteConfigUpdate({'test'}); + givenRemoveConfigUpdate(update); + }); + + test('adds integration to options', () { + final sut = fixture.getSut(); + sut.call(fixture.mockHub, fixture.options); + + expect(fixture.options.sdk.integrations.contains("sentryFirebaseIntegration"), isTrue); + }); +} + +class Fixture { + + final mockHub = MockHub(); + final options = SentryOptions( + dsn: 'https://example.com/sentry-dsn', + ); + + final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); + + SentryFirebaseIntegration getSut() { + return SentryFirebaseIntegration(mockFirebaseRemoteConfig); + } + +} From e9ce800469068ab73cfe072679ec7c8057ab80cb Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 1 Apr 2025 12:23:47 +0200 Subject: [PATCH 06/29] add feature flag if provided key is updated from remote config --- .../lib/src/sentry_firebase_integration.dart | 22 +- firebase/test/mocks/mocks.mocks.dart | 564 ++++++++---------- .../src/sentry_firebase_integration_test.dart | 100 +++- 3 files changed, 353 insertions(+), 333 deletions(-) diff --git a/firebase/lib/src/sentry_firebase_integration.dart b/firebase/lib/src/sentry_firebase_integration.dart index e8fb2e60c4..a8befb8052 100644 --- a/firebase/lib/src/sentry_firebase_integration.dart +++ b/firebase/lib/src/sentry_firebase_integration.dart @@ -4,16 +4,26 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:sentry/sentry.dart'; class SentryFirebaseIntegration extends Integration { - SentryFirebaseIntegration(this._firebaseRemoteConfig); + SentryFirebaseIntegration(this._firebaseRemoteConfig, this._keys); final FirebaseRemoteConfig _firebaseRemoteConfig; + final Set _keys; - StreamSubscription? _subscription; + StreamSubscription? _subscription; @override - FutureOr call(Hub hub, SentryOptions options) { - _subscription = _firebaseRemoteConfig.onConfigUpdated.listen((event) { - print(event); + FutureOr call(Hub hub, SentryOptions options) async { + if (_keys.isEmpty) { + // TODO: log warning + return; + } + _subscription = _firebaseRemoteConfig.onConfigUpdated.listen((event) async { + for (final updatedKey in event.updatedKeys) { + if (_keys.contains(updatedKey)) { + final value = _firebaseRemoteConfig.getBool(updatedKey); + await Sentry.addFeatureFlag(updatedKey, value); + } + } }); options.sdk.addIntegration('sentryFirebaseIntegration'); } @@ -22,5 +32,5 @@ class SentryFirebaseIntegration extends Integration { FutureOr close() async { await _subscription?.cancel(); _subscription = null; - } + } } diff --git a/firebase/test/mocks/mocks.mocks.dart b/firebase/test/mocks/mocks.mocks.dart index 281b90f406..37f4436d01 100644 --- a/firebase/test/mocks/mocks.mocks.dart +++ b/firebase/test/mocks/mocks.mocks.dart @@ -30,49 +30,49 @@ import 'package:sentry/src/profiling.dart' as _i5; class _FakeSentryOptions_0 extends _i1.SmartFake implements _i2.SentryOptions { _FakeSentryOptions_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeSentryId_1 extends _i1.SmartFake implements _i2.SentryId { _FakeSentryId_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeScope_2 extends _i1.SmartFake implements _i2.Scope { _FakeScope_2(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeHub_3 extends _i1.SmartFake implements _i2.Hub { _FakeHub_3(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeISentrySpan_4 extends _i1.SmartFake implements _i2.ISentrySpan { _FakeISentrySpan_4(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeFirebaseApp_5 extends _i1.SmartFake implements _i3.FirebaseApp { _FakeFirebaseApp_5(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeDateTime_6 extends _i1.SmartFake implements DateTime { _FakeDateTime_6(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeRemoteConfigSettings_7 extends _i1.SmartFake implements _i4.RemoteConfigSettings { _FakeRemoteConfigSettings_7(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } class _FakeRemoteConfigValue_8 extends _i1.SmartFake implements _i4.RemoteConfigValue { _FakeRemoteConfigValue_8(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + : super(parent, parentInvocation); } /// A class which mocks [Hub]. @@ -84,15 +84,13 @@ class MockHub extends _i1.Mock implements _i2.Hub { } @override - _i2.SentryOptions get options => - (super.noSuchMethod( - Invocation.getter(#options), - returnValue: _FakeSentryOptions_0( - this, - Invocation.getter(#options), - ), - ) - as _i2.SentryOptions); + _i2.SentryOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeSentryOptions_0( + this, + Invocation.getter(#options), + ), + ) as _i2.SentryOptions); @override bool get isEnabled => @@ -100,27 +98,23 @@ class MockHub extends _i1.Mock implements _i2.Hub { as bool); @override - _i2.SentryId get lastEventId => - (super.noSuchMethod( - Invocation.getter(#lastEventId), - returnValue: _FakeSentryId_1(this, Invocation.getter(#lastEventId)), - ) - as _i2.SentryId); + _i2.SentryId get lastEventId => (super.noSuchMethod( + Invocation.getter(#lastEventId), + returnValue: _FakeSentryId_1(this, Invocation.getter(#lastEventId)), + ) as _i2.SentryId); @override - _i2.Scope get scope => - (super.noSuchMethod( - Invocation.getter(#scope), - returnValue: _FakeScope_2(this, Invocation.getter(#scope)), - ) - as _i2.Scope); + _i2.Scope get scope => (super.noSuchMethod( + Invocation.getter(#scope), + returnValue: _FakeScope_2(this, Invocation.getter(#scope)), + ) as _i2.Scope); @override // ignore: invalid_use_of_internal_member set profilerFactory(_i5.SentryProfilerFactory? value) => super.noSuchMethod( - Invocation.setter(#profilerFactory, value), - returnValueForMissingStub: null, - ); + Invocation.setter(#profilerFactory, value), + returnValueForMissingStub: null, + ); @override _i6.Future<_i2.SentryId> captureEvent( @@ -130,23 +124,22 @@ class MockHub extends _i1.Mock implements _i2.Hub { _i2.ScopeCallback? withScope, }) => (super.noSuchMethod( + Invocation.method( + #captureEvent, + [event], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, Invocation.method( #captureEvent, [event], {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, ), - returnValue: _i6.Future<_i2.SentryId>.value( - _FakeSentryId_1( - this, - Invocation.method( - #captureEvent, - [event], - {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, - ), - ), - ), - ) - as _i6.Future<_i2.SentryId>); + ), + ), + ) as _i6.Future<_i2.SentryId>); @override _i6.Future<_i2.SentryId> captureException( @@ -156,23 +149,22 @@ class MockHub extends _i1.Mock implements _i2.Hub { _i2.ScopeCallback? withScope, }) => (super.noSuchMethod( + Invocation.method( + #captureException, + [throwable], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, Invocation.method( #captureException, [throwable], {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, ), - returnValue: _i6.Future<_i2.SentryId>.value( - _FakeSentryId_1( - this, - Invocation.method( - #captureException, - [throwable], - {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, - ), - ), - ), - ) - as _i6.Future<_i2.SentryId>); + ), + ), + ) as _i6.Future<_i2.SentryId>); @override _i6.Future<_i2.SentryId> captureMessage( @@ -184,6 +176,20 @@ class MockHub extends _i1.Mock implements _i2.Hub { _i2.ScopeCallback? withScope, }) => (super.noSuchMethod( + Invocation.method( + #captureMessage, + [message], + { + #level: level, + #template: template, + #params: params, + #hint: hint, + #withScope: withScope, + }, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, Invocation.method( #captureMessage, [message], @@ -195,24 +201,9 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i6.Future<_i2.SentryId>.value( - _FakeSentryId_1( - this, - Invocation.method( - #captureMessage, - [message], - { - #level: level, - #template: template, - #params: params, - #hint: hint, - #withScope: withScope, - }, - ), - ), - ), - ) - as _i6.Future<_i2.SentryId>); + ), + ), + ) as _i6.Future<_i2.SentryId>); @override _i6.Future<_i2.SentryId> captureFeedback( @@ -221,55 +212,49 @@ class MockHub extends _i1.Mock implements _i2.Hub { _i2.ScopeCallback? withScope, }) => (super.noSuchMethod( + Invocation.method( + #captureFeedback, + [feedback], + {#hint: hint, #withScope: withScope}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, Invocation.method( #captureFeedback, [feedback], {#hint: hint, #withScope: withScope}, ), - returnValue: _i6.Future<_i2.SentryId>.value( - _FakeSentryId_1( - this, - Invocation.method( - #captureFeedback, - [feedback], - {#hint: hint, #withScope: withScope}, - ), - ), - ), - ) - as _i6.Future<_i2.SentryId>); + ), + ), + ) as _i6.Future<_i2.SentryId>); @override _i6.Future addBreadcrumb(_i2.Breadcrumb? crumb, {_i2.Hint? hint}) => (super.noSuchMethod( - Invocation.method(#addBreadcrumb, [crumb], {#hint: hint}), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + Invocation.method(#addBreadcrumb, [crumb], {#hint: hint}), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override void bindClient(_i2.SentryClient? client) => super.noSuchMethod( - Invocation.method(#bindClient, [client]), - returnValueForMissingStub: null, - ); + Invocation.method(#bindClient, [client]), + returnValueForMissingStub: null, + ); @override - _i2.Hub clone() => - (super.noSuchMethod( - Invocation.method(#clone, []), - returnValue: _FakeHub_3(this, Invocation.method(#clone, [])), - ) - as _i2.Hub); + _i2.Hub clone() => (super.noSuchMethod( + Invocation.method(#clone, []), + returnValue: _FakeHub_3(this, Invocation.method(#clone, [])), + ) as _i2.Hub); @override - _i6.Future close() => - (super.noSuchMethod( - Invocation.method(#close, []), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + _i6.Future close() => (super.noSuchMethod( + Invocation.method(#close, []), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override _i6.FutureOr configureScope(_i2.ScopeCallback? callback) => @@ -290,39 +275,38 @@ class MockHub extends _i1.Mock implements _i2.Hub { Map? customSamplingContext, }) => (super.noSuchMethod( - Invocation.method( - #startTransaction, - [name, operation], - { - #description: description, - #startTimestamp: startTimestamp, - #bindToScope: bindToScope, - #waitForChildren: waitForChildren, - #autoFinishAfter: autoFinishAfter, - #trimEnd: trimEnd, - #onFinish: onFinish, - #customSamplingContext: customSamplingContext, - }, - ), - returnValue: _FakeISentrySpan_4( - this, - Invocation.method( - #startTransaction, - [name, operation], - { - #description: description, - #startTimestamp: startTimestamp, - #bindToScope: bindToScope, - #waitForChildren: waitForChildren, - #autoFinishAfter: autoFinishAfter, - #trimEnd: trimEnd, - #onFinish: onFinish, - #customSamplingContext: customSamplingContext, - }, - ), - ), - ) - as _i2.ISentrySpan); + Invocation.method( + #startTransaction, + [name, operation], + { + #description: description, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + #customSamplingContext: customSamplingContext, + }, + ), + returnValue: _FakeISentrySpan_4( + this, + Invocation.method( + #startTransaction, + [name, operation], + { + #description: description, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + #customSamplingContext: customSamplingContext, + }, + ), + ), + ) as _i2.ISentrySpan); @override _i2.ISentrySpan startTransactionWithContext( @@ -336,37 +320,36 @@ class MockHub extends _i1.Mock implements _i2.Hub { _i2.OnTransactionFinish? onFinish, }) => (super.noSuchMethod( - Invocation.method( - #startTransactionWithContext, - [transactionContext], - { - #customSamplingContext: customSamplingContext, - #startTimestamp: startTimestamp, - #bindToScope: bindToScope, - #waitForChildren: waitForChildren, - #autoFinishAfter: autoFinishAfter, - #trimEnd: trimEnd, - #onFinish: onFinish, - }, - ), - returnValue: _FakeISentrySpan_4( - this, - Invocation.method( - #startTransactionWithContext, - [transactionContext], - { - #customSamplingContext: customSamplingContext, - #startTimestamp: startTimestamp, - #bindToScope: bindToScope, - #waitForChildren: waitForChildren, - #autoFinishAfter: autoFinishAfter, - #trimEnd: trimEnd, - #onFinish: onFinish, - }, - ), - ), - ) - as _i2.ISentrySpan); + Invocation.method( + #startTransactionWithContext, + [transactionContext], + { + #customSamplingContext: customSamplingContext, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + }, + ), + returnValue: _FakeISentrySpan_4( + this, + Invocation.method( + #startTransactionWithContext, + [transactionContext], + { + #customSamplingContext: customSamplingContext, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + }, + ), + ), + ) as _i2.ISentrySpan); @override _i6.Future<_i2.SentryId> captureTransaction( @@ -375,33 +358,33 @@ class MockHub extends _i1.Mock implements _i2.Hub { _i2.Hint? hint, }) => (super.noSuchMethod( + Invocation.method( + #captureTransaction, + [transaction], + {#traceContext: traceContext, #hint: hint}, + ), + returnValue: _i6.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, Invocation.method( #captureTransaction, [transaction], {#traceContext: traceContext, #hint: hint}, ), - returnValue: _i6.Future<_i2.SentryId>.value( - _FakeSentryId_1( - this, - Invocation.method( - #captureTransaction, - [transaction], - {#traceContext: traceContext, #hint: hint}, - ), - ), - ), - ) - as _i6.Future<_i2.SentryId>); + ), + ), + ) as _i6.Future<_i2.SentryId>); @override void setSpanContext( dynamic throwable, _i2.ISentrySpan? span, String? transaction, - ) => super.noSuchMethod( - Invocation.method(#setSpanContext, [throwable, span, transaction]), - returnValueForMissingStub: null, - ); + ) => + super.noSuchMethod( + Invocation.method(#setSpanContext, [throwable, span, transaction]), + returnValueForMissingStub: null, + ); } /// A class which mocks [FirebaseRemoteConfig]. @@ -414,108 +397,84 @@ class MockFirebaseRemoteConfig extends _i1.Mock } @override - _i3.FirebaseApp get app => - (super.noSuchMethod( - Invocation.getter(#app), - returnValue: _FakeFirebaseApp_5(this, Invocation.getter(#app)), - ) - as _i3.FirebaseApp); + _i3.FirebaseApp get app => (super.noSuchMethod( + Invocation.getter(#app), + returnValue: _FakeFirebaseApp_5(this, Invocation.getter(#app)), + ) as _i3.FirebaseApp); @override - DateTime get lastFetchTime => - (super.noSuchMethod( - Invocation.getter(#lastFetchTime), - returnValue: _FakeDateTime_6( - this, - Invocation.getter(#lastFetchTime), - ), - ) - as DateTime); + DateTime get lastFetchTime => (super.noSuchMethod( + Invocation.getter(#lastFetchTime), + returnValue: _FakeDateTime_6( + this, + Invocation.getter(#lastFetchTime), + ), + ) as DateTime); @override - _i4.RemoteConfigFetchStatus get lastFetchStatus => - (super.noSuchMethod( - Invocation.getter(#lastFetchStatus), - returnValue: _i4.RemoteConfigFetchStatus.noFetchYet, - ) - as _i4.RemoteConfigFetchStatus); + _i4.RemoteConfigFetchStatus get lastFetchStatus => (super.noSuchMethod( + Invocation.getter(#lastFetchStatus), + returnValue: _i4.RemoteConfigFetchStatus.noFetchYet, + ) as _i4.RemoteConfigFetchStatus); @override - _i4.RemoteConfigSettings get settings => - (super.noSuchMethod( - Invocation.getter(#settings), - returnValue: _FakeRemoteConfigSettings_7( - this, - Invocation.getter(#settings), - ), - ) - as _i4.RemoteConfigSettings); + _i4.RemoteConfigSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeRemoteConfigSettings_7( + this, + Invocation.getter(#settings), + ), + ) as _i4.RemoteConfigSettings); @override - _i6.Stream<_i4.RemoteConfigUpdate> get onConfigUpdated => - (super.noSuchMethod( - Invocation.getter(#onConfigUpdated), - returnValue: _i6.Stream<_i4.RemoteConfigUpdate>.empty(), - ) - as _i6.Stream<_i4.RemoteConfigUpdate>); + _i6.Stream<_i4.RemoteConfigUpdate> get onConfigUpdated => (super.noSuchMethod( + Invocation.getter(#onConfigUpdated), + returnValue: _i6.Stream<_i4.RemoteConfigUpdate>.empty(), + ) as _i6.Stream<_i4.RemoteConfigUpdate>); @override - Map get pluginConstants => - (super.noSuchMethod( - Invocation.getter(#pluginConstants), - returnValue: {}, - ) - as Map); + Map get pluginConstants => (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + ) as Map); @override - _i6.Future activate() => - (super.noSuchMethod( - Invocation.method(#activate, []), - returnValue: _i6.Future.value(false), - ) - as _i6.Future); + _i6.Future activate() => (super.noSuchMethod( + Invocation.method(#activate, []), + returnValue: _i6.Future.value(false), + ) as _i6.Future); @override - _i6.Future ensureInitialized() => - (super.noSuchMethod( - Invocation.method(#ensureInitialized, []), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + _i6.Future ensureInitialized() => (super.noSuchMethod( + Invocation.method(#ensureInitialized, []), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i6.Future fetch() => - (super.noSuchMethod( - Invocation.method(#fetch, []), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + _i6.Future fetch() => (super.noSuchMethod( + Invocation.method(#fetch, []), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i6.Future fetchAndActivate() => - (super.noSuchMethod( - Invocation.method(#fetchAndActivate, []), - returnValue: _i6.Future.value(false), - ) - as _i6.Future); + _i6.Future fetchAndActivate() => (super.noSuchMethod( + Invocation.method(#fetchAndActivate, []), + returnValue: _i6.Future.value(false), + ) as _i6.Future); @override - Map getAll() => - (super.noSuchMethod( - Invocation.method(#getAll, []), - returnValue: {}, - ) - as Map); + Map getAll() => (super.noSuchMethod( + Invocation.method(#getAll, []), + returnValue: {}, + ) as Map); @override - bool getBool(String? key) => - (super.noSuchMethod( - Invocation.method(#getBool, [key]), - returnValue: false, - ) - as bool); + bool getBool(String? key) => (super.noSuchMethod( + Invocation.method(#getBool, [key]), + returnValue: false, + ) as bool); @override int getInt(String? key) => @@ -523,61 +482,52 @@ class MockFirebaseRemoteConfig extends _i1.Mock as int); @override - double getDouble(String? key) => - (super.noSuchMethod( - Invocation.method(#getDouble, [key]), - returnValue: 0.0, - ) - as double); + double getDouble(String? key) => (super.noSuchMethod( + Invocation.method(#getDouble, [key]), + returnValue: 0.0, + ) as double); @override - String getString(String? key) => - (super.noSuchMethod( - Invocation.method(#getString, [key]), - returnValue: _i8.dummyValue( - this, - Invocation.method(#getString, [key]), - ), - ) - as String); + String getString(String? key) => (super.noSuchMethod( + Invocation.method(#getString, [key]), + returnValue: _i8.dummyValue( + this, + Invocation.method(#getString, [key]), + ), + ) as String); @override - _i4.RemoteConfigValue getValue(String? key) => - (super.noSuchMethod( - Invocation.method(#getValue, [key]), - returnValue: _FakeRemoteConfigValue_8( - this, - Invocation.method(#getValue, [key]), - ), - ) - as _i4.RemoteConfigValue); + _i4.RemoteConfigValue getValue(String? key) => (super.noSuchMethod( + Invocation.method(#getValue, [key]), + returnValue: _FakeRemoteConfigValue_8( + this, + Invocation.method(#getValue, [key]), + ), + ) as _i4.RemoteConfigValue); @override _i6.Future setConfigSettings( _i4.RemoteConfigSettings? remoteConfigSettings, ) => (super.noSuchMethod( - Invocation.method(#setConfigSettings, [remoteConfigSettings]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + Invocation.method(#setConfigSettings, [remoteConfigSettings]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override _i6.Future setDefaults(Map? defaultParameters) => (super.noSuchMethod( - Invocation.method(#setDefaults, [defaultParameters]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + Invocation.method(#setDefaults, [defaultParameters]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override _i6.Future setCustomSignals(Map? customSignals) => (super.noSuchMethod( - Invocation.method(#setCustomSignals, [customSignals]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) - as _i6.Future); + Invocation.method(#setCustomSignals, [customSignals]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); } diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart index 5da2c96340..1e5d5c2d84 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -8,39 +8,99 @@ import '../mocks/mocks.mocks.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; void main() { - late final Fixture fixture; - givenRemoveConfigUpdate(RemoteConfigUpdate update) { - when(fixture.mockFirebaseRemoteConfig.onConfigUpdated).thenAnswer((_) => Stream.value(update)); + givenRemoveConfigUpdate() { + final update = RemoteConfigUpdate({'test'}); + when(fixture.mockFirebaseRemoteConfig.onConfigUpdated) + .thenAnswer((_) => Stream.value(update)); + when(fixture.mockFirebaseRemoteConfig.getBool('test')).thenReturn(true); } - setUp(() { + setUp(() async { fixture = Fixture(); - final update = RemoteConfigUpdate({'test'}); - givenRemoveConfigUpdate(update); + 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; }); - test('adds integration to options', () { - final sut = fixture.getSut(); - sut.call(fixture.mockHub, fixture.options); + tearDown(() { + Sentry.close(); + }); + + test('adds integration to options', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut({'test'}); + + sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations.contains('sentryFirebaseIntegration'), + isTrue, + ); + }); + + test('does not add integration to options if no keys are provided', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut({}); + + sut.call(fixture.hub, fixture.options); - expect(fixture.options.sdk.integrations.contains("sentryFirebaseIntegration"), isTrue); - }); + expect( + fixture.options.sdk.integrations.contains('sentryFirebaseIntegration'), + isFalse, + ); + }); + + test('adds update to feature flags', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut({'test'}); + sut.call(fixture.hub, fixture.options); + await Future.delayed(const Duration( + milliseconds: 100)); // wait for the subscription to be called + + // ignore: invalid_use_of_internal_member + final featureFlags = fixture.hub.scope.contexts[SentryFeatureFlags.type] + as SentryFeatureFlags?; + + expect(featureFlags, isNotNull); + expect(featureFlags?.values.first.name, 'test'); + expect(featureFlags?.values.first.value, true); + }); + + test('doesn`t add update to feature flags if key is not in the list', + () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut({'test2'}); + sut.call(fixture.hub, fixture.options); + await Future.delayed(const Duration( + milliseconds: 100)); // wait for the subscription to be called + + // ignore: invalid_use_of_internal_member + final featureFlags = fixture.hub.scope.contexts[SentryFeatureFlags.type] + as SentryFeatureFlags?; + + expect(featureFlags, isNull); + }); } class Fixture { - - final mockHub = MockHub(); - final options = SentryOptions( - dsn: 'https://example.com/sentry-dsn', - ); + late Hub hub; + late SentryOptions options; final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - - SentryFirebaseIntegration getSut() { - return SentryFirebaseIntegration(mockFirebaseRemoteConfig); + + Future getSut(Set keys) async { + return SentryFirebaseIntegration(mockFirebaseRemoteConfig, keys); } - } From e5fdace5b4aee2c5d93f75b0844ceb9f9fd82721 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 1 Apr 2025 14:01:38 +0200 Subject: [PATCH 07/29] format --- .../test/src/sentry_firebase_integration_test.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart index 1e5d5c2d84..c9182aaa07 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -65,8 +65,11 @@ void main() { final sut = await fixture.getSut({'test'}); sut.call(fixture.hub, fixture.options); - await Future.delayed(const Duration( - milliseconds: 100)); // wait for the subscription to be called + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); // wait for the subscription to be called // ignore: invalid_use_of_internal_member final featureFlags = fixture.hub.scope.contexts[SentryFeatureFlags.type] @@ -83,8 +86,11 @@ void main() { final sut = await fixture.getSut({'test2'}); sut.call(fixture.hub, fixture.options); - await Future.delayed(const Duration( - milliseconds: 100)); // wait for the subscription to be called + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); // wait for the subscription to be called // ignore: invalid_use_of_internal_member final featureFlags = fixture.hub.scope.contexts[SentryFeatureFlags.type] From 4e6bd4c51c48a042c22d6d1c73a68966a67ca9fb Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 1 Apr 2025 14:16:17 +0200 Subject: [PATCH 08/29] test wether subscription is canceld on close --- firebase/test/mocks/mocks.dart | 3 + firebase/test/mocks/mocks.mocks.dart | 555 ++++++++++++++++-- .../src/sentry_firebase_integration_test.dart | 19 +- 3 files changed, 518 insertions(+), 59 deletions(-) diff --git a/firebase/test/mocks/mocks.dart b/firebase/test/mocks/mocks.dart index 581122064f..579fc1638d 100644 --- a/firebase/test/mocks/mocks.dart +++ b/firebase/test/mocks/mocks.dart @@ -1,9 +1,12 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:mockito/annotations.dart'; import 'package:sentry/sentry.dart'; +import 'dart:async'; @GenerateMocks([ Hub, FirebaseRemoteConfig, + Stream, + StreamSubscription, ]) void main() {} diff --git a/firebase/test/mocks/mocks.mocks.dart b/firebase/test/mocks/mocks.mocks.dart index 37f4436d01..6f9eebffda 100644 --- a/firebase/test/mocks/mocks.mocks.dart +++ b/firebase/test/mocks/mocks.mocks.dart @@ -3,7 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; +import 'dart:async' as _i5; import 'package:firebase_core/firebase_core.dart' as _i3; import 'package:firebase_remote_config/firebase_remote_config.dart' as _i7; @@ -12,7 +12,7 @@ import 'package:firebase_remote_config_platform_interface/firebase_remote_config import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i8; import 'package:sentry/sentry.dart' as _i2; -import 'package:sentry/src/profiling.dart' as _i5; +import 'package:sentry/src/profiling.dart' as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -75,6 +75,17 @@ class _FakeRemoteConfigValue_8 extends _i1.SmartFake : super(parent, parentInvocation); } +class _FakeFuture_9 extends _i1.SmartFake implements _i5.Future { + _FakeFuture_9(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeStreamSubscription_10 extends _i1.SmartFake + implements _i5.StreamSubscription { + _FakeStreamSubscription_10(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [Hub]. /// /// See the documentation for Mockito's code generation for more information. @@ -110,14 +121,13 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Scope); @override - // ignore: invalid_use_of_internal_member - set profilerFactory(_i5.SentryProfilerFactory? value) => super.noSuchMethod( + set profilerFactory(_i6.SentryProfilerFactory? value) => super.noSuchMethod( Invocation.setter(#profilerFactory, value), returnValueForMissingStub: null, ); @override - _i6.Future<_i2.SentryId> captureEvent( + _i5.Future<_i2.SentryId> captureEvent( _i2.SentryEvent? event, { dynamic stackTrace, _i2.Hint? hint, @@ -129,7 +139,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { [event], {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, ), - returnValue: _i6.Future<_i2.SentryId>.value( + returnValue: _i5.Future<_i2.SentryId>.value( _FakeSentryId_1( this, Invocation.method( @@ -139,10 +149,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ), ), - ) as _i6.Future<_i2.SentryId>); + ) as _i5.Future<_i2.SentryId>); @override - _i6.Future<_i2.SentryId> captureException( + _i5.Future<_i2.SentryId> captureException( dynamic throwable, { dynamic stackTrace, _i2.Hint? hint, @@ -154,7 +164,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { [throwable], {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, ), - returnValue: _i6.Future<_i2.SentryId>.value( + returnValue: _i5.Future<_i2.SentryId>.value( _FakeSentryId_1( this, Invocation.method( @@ -164,10 +174,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ), ), - ) as _i6.Future<_i2.SentryId>); + ) as _i5.Future<_i2.SentryId>); @override - _i6.Future<_i2.SentryId> captureMessage( + _i5.Future<_i2.SentryId> captureMessage( String? message, { _i2.SentryLevel? level, String? template, @@ -187,7 +197,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i6.Future<_i2.SentryId>.value( + returnValue: _i5.Future<_i2.SentryId>.value( _FakeSentryId_1( this, Invocation.method( @@ -203,10 +213,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ), ), - ) as _i6.Future<_i2.SentryId>); + ) as _i5.Future<_i2.SentryId>); @override - _i6.Future<_i2.SentryId> captureFeedback( + _i5.Future<_i2.SentryId> captureFeedback( _i2.SentryFeedback? feedback, { _i2.Hint? hint, _i2.ScopeCallback? withScope, @@ -217,7 +227,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { [feedback], {#hint: hint, #withScope: withScope}, ), - returnValue: _i6.Future<_i2.SentryId>.value( + returnValue: _i5.Future<_i2.SentryId>.value( _FakeSentryId_1( this, Invocation.method( @@ -227,15 +237,15 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ), ), - ) as _i6.Future<_i2.SentryId>); + ) as _i5.Future<_i2.SentryId>); @override - _i6.Future addBreadcrumb(_i2.Breadcrumb? crumb, {_i2.Hint? hint}) => + _i5.Future addBreadcrumb(_i2.Breadcrumb? crumb, {_i2.Hint? hint}) => (super.noSuchMethod( Invocation.method(#addBreadcrumb, [crumb], {#hint: hint}), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override void bindClient(_i2.SentryClient? client) => super.noSuchMethod( @@ -250,16 +260,16 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Hub); @override - _i6.Future close() => (super.noSuchMethod( + _i5.Future close() => (super.noSuchMethod( Invocation.method(#close, []), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.FutureOr configureScope(_i2.ScopeCallback? callback) => + _i5.FutureOr configureScope(_i2.ScopeCallback? callback) => (super.noSuchMethod(Invocation.method(#configureScope, [callback])) - as _i6.FutureOr); + as _i5.FutureOr); @override _i2.ISentrySpan startTransaction( @@ -352,7 +362,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.ISentrySpan); @override - _i6.Future<_i2.SentryId> captureTransaction( + _i5.Future<_i2.SentryId> captureTransaction( _i2.SentryTransaction? transaction, { _i2.SentryTraceContextHeader? traceContext, _i2.Hint? hint, @@ -363,7 +373,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { [transaction], {#traceContext: traceContext, #hint: hint}, ), - returnValue: _i6.Future<_i2.SentryId>.value( + returnValue: _i5.Future<_i2.SentryId>.value( _FakeSentryId_1( this, Invocation.method( @@ -373,7 +383,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ), ), - ) as _i6.Future<_i2.SentryId>); + ) as _i5.Future<_i2.SentryId>); @override void setSpanContext( @@ -427,10 +437,10 @@ class MockFirebaseRemoteConfig extends _i1.Mock ) as _i4.RemoteConfigSettings); @override - _i6.Stream<_i4.RemoteConfigUpdate> get onConfigUpdated => (super.noSuchMethod( + _i5.Stream<_i4.RemoteConfigUpdate> get onConfigUpdated => (super.noSuchMethod( Invocation.getter(#onConfigUpdated), - returnValue: _i6.Stream<_i4.RemoteConfigUpdate>.empty(), - ) as _i6.Stream<_i4.RemoteConfigUpdate>); + returnValue: _i5.Stream<_i4.RemoteConfigUpdate>.empty(), + ) as _i5.Stream<_i4.RemoteConfigUpdate>); @override Map get pluginConstants => (super.noSuchMethod( @@ -439,30 +449,30 @@ class MockFirebaseRemoteConfig extends _i1.Mock ) as Map); @override - _i6.Future activate() => (super.noSuchMethod( + _i5.Future activate() => (super.noSuchMethod( Invocation.method(#activate, []), - returnValue: _i6.Future.value(false), - ) as _i6.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i6.Future ensureInitialized() => (super.noSuchMethod( + _i5.Future ensureInitialized() => (super.noSuchMethod( Invocation.method(#ensureInitialized, []), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future fetch() => (super.noSuchMethod( + _i5.Future fetch() => (super.noSuchMethod( Invocation.method(#fetch, []), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future fetchAndActivate() => (super.noSuchMethod( + _i5.Future fetchAndActivate() => (super.noSuchMethod( Invocation.method(#fetchAndActivate, []), - returnValue: _i6.Future.value(false), - ) as _i6.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override Map getAll() => (super.noSuchMethod( @@ -506,28 +516,457 @@ class MockFirebaseRemoteConfig extends _i1.Mock ) as _i4.RemoteConfigValue); @override - _i6.Future setConfigSettings( + _i5.Future setConfigSettings( _i4.RemoteConfigSettings? remoteConfigSettings, ) => (super.noSuchMethod( Invocation.method(#setConfigSettings, [remoteConfigSettings]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setDefaults(Map? defaultParameters) => + _i5.Future setDefaults(Map? defaultParameters) => (super.noSuchMethod( Invocation.method(#setDefaults, [defaultParameters]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i6.Future setCustomSignals(Map? customSignals) => + _i5.Future setCustomSignals(Map? customSignals) => (super.noSuchMethod( Invocation.method(#setCustomSignals, [customSignals]), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [Stream]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockStream extends _i1.Mock implements _i5.Stream { + MockStream() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isBroadcast => + (super.noSuchMethod(Invocation.getter(#isBroadcast), returnValue: false) + as bool); + + @override + _i5.Future get length => (super.noSuchMethod( + Invocation.getter(#length), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + + @override + _i5.Future get isEmpty => (super.noSuchMethod( + Invocation.getter(#isEmpty), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future get first => (super.noSuchMethod( + Invocation.getter(#first), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull(this, Invocation.getter(#first)), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9(this, Invocation.getter(#first)), + ) as _i5.Future); + + @override + _i5.Future get last => (super.noSuchMethod( + Invocation.getter(#last), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull(this, Invocation.getter(#last)), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9(this, Invocation.getter(#last)), + ) as _i5.Future); + + @override + _i5.Future get single => (super.noSuchMethod( + Invocation.getter(#single), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull(this, Invocation.getter(#single)), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9(this, Invocation.getter(#single)), + ) as _i5.Future); + + @override + _i5.Stream asBroadcastStream({ + void Function(_i5.StreamSubscription)? onListen, + void Function(_i5.StreamSubscription)? onCancel, + }) => + (super.noSuchMethod( + Invocation.method(#asBroadcastStream, [], { + #onListen: onListen, + #onCancel: onCancel, + }), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.StreamSubscription listen( + void Function(T)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) => + (super.noSuchMethod( + Invocation.method( + #listen, + [onData], + { + #onError: onError, + #onDone: onDone, + #cancelOnError: cancelOnError, + }, + ), + returnValue: _FakeStreamSubscription_10( + this, + Invocation.method( + #listen, + [onData], + { + #onError: onError, + #onDone: onDone, + #cancelOnError: cancelOnError, + }, + ), + ), + ) as _i5.StreamSubscription); + + @override + _i5.Stream where(bool Function(T)? test) => (super.noSuchMethod( + Invocation.method(#where, [test]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream map(S Function(T)? convert) => (super.noSuchMethod( + Invocation.method(#map, [convert]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream asyncMap(_i5.FutureOr Function(T)? convert) => + (super.noSuchMethod( + Invocation.method(#asyncMap, [convert]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream asyncExpand(_i5.Stream? Function(T)? convert) => + (super.noSuchMethod( + Invocation.method(#asyncExpand, [convert]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream handleError( + Function? onError, { + bool Function(dynamic)? test, + }) => + (super.noSuchMethod( + Invocation.method(#handleError, [onError], {#test: test}), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream expand(Iterable Function(T)? convert) => + (super.noSuchMethod( + Invocation.method(#expand, [convert]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Future pipe(_i5.StreamConsumer? streamConsumer) => + (super.noSuchMethod( + Invocation.method(#pipe, [streamConsumer]), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Stream transform(_i5.StreamTransformer? streamTransformer) => + (super.noSuchMethod( + Invocation.method(#transform, [streamTransformer]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Future reduce(T Function(T, T)? combine) => (super.noSuchMethod( + Invocation.method(#reduce, [combine]), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#reduce, [combine]), + ), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9(this, Invocation.method(#reduce, [combine])), + ) as _i5.Future); + + @override + _i5.Future fold(S? initialValue, S Function(S, T)? combine) => + (super.noSuchMethod( + Invocation.method(#fold, [initialValue, combine]), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#fold, [initialValue, combine]), + ), + (S v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9( + this, + Invocation.method(#fold, [initialValue, combine]), + ), + ) as _i5.Future); + + @override + _i5.Future join([String? separator = '']) => (super.noSuchMethod( + Invocation.method(#join, [separator]), + returnValue: _i5.Future.value( + _i8.dummyValue( + this, + Invocation.method(#join, [separator]), + ), + ), + ) as _i5.Future); + + @override + _i5.Future contains(Object? needle) => (super.noSuchMethod( + Invocation.method(#contains, [needle]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future forEach(void Function(T)? action) => (super.noSuchMethod( + Invocation.method(#forEach, [action]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future every(bool Function(T)? test) => (super.noSuchMethod( + Invocation.method(#every, [test]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future any(bool Function(T)? test) => (super.noSuchMethod( + Invocation.method(#any, [test]), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Stream cast() => (super.noSuchMethod( + Invocation.method(#cast, []), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Future> toList() => (super.noSuchMethod( + Invocation.method(#toList, []), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + + @override + _i5.Future> toSet() => (super.noSuchMethod( + Invocation.method(#toSet, []), + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); + + @override + _i5.Future drain([E? futureValue]) => (super.noSuchMethod( + Invocation.method(#drain, [futureValue]), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#drain, [futureValue]), + ), + (E v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9( + this, + Invocation.method(#drain, [futureValue]), + ), + ) as _i5.Future); + + @override + _i5.Stream take(int? count) => (super.noSuchMethod( + Invocation.method(#take, [count]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream takeWhile(bool Function(T)? test) => (super.noSuchMethod( + Invocation.method(#takeWhile, [test]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream skip(int? count) => (super.noSuchMethod( + Invocation.method(#skip, [count]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream skipWhile(bool Function(T)? test) => (super.noSuchMethod( + Invocation.method(#skipWhile, [test]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Stream distinct([bool Function(T, T)? equals]) => (super.noSuchMethod( + Invocation.method(#distinct, [equals]), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); + + @override + _i5.Future firstWhere(bool Function(T)? test, {T Function()? orElse}) => + (super.noSuchMethod( + Invocation.method(#firstWhere, [test], {#orElse: orElse}), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#firstWhere, [test], {#orElse: orElse}), + ), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9( + this, + Invocation.method(#firstWhere, [test], {#orElse: orElse}), + ), + ) as _i5.Future); + + @override + _i5.Future lastWhere(bool Function(T)? test, {T Function()? orElse}) => + (super.noSuchMethod( + Invocation.method(#lastWhere, [test], {#orElse: orElse}), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#lastWhere, [test], {#orElse: orElse}), + ), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9( + this, + Invocation.method(#lastWhere, [test], {#orElse: orElse}), + ), + ) as _i5.Future); + + @override + _i5.Future singleWhere(bool Function(T)? test, {T Function()? orElse}) => + (super.noSuchMethod( + Invocation.method(#singleWhere, [test], {#orElse: orElse}), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#singleWhere, [test], {#orElse: orElse}), + ), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9( + this, + Invocation.method(#singleWhere, [test], {#orElse: orElse}), + ), + ) as _i5.Future); + + @override + _i5.Future elementAt(int? index) => (super.noSuchMethod( + Invocation.method(#elementAt, [index]), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#elementAt, [index]), + ), + (T v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9(this, Invocation.method(#elementAt, [index])), + ) as _i5.Future); + + @override + _i5.Stream timeout( + Duration? timeLimit, { + void Function(_i5.EventSink)? onTimeout, + }) => + (super.noSuchMethod( + Invocation.method(#timeout, [timeLimit], {#onTimeout: onTimeout}), + returnValue: _i5.Stream.empty(), + ) as _i5.Stream); +} + +/// A class which mocks [StreamSubscription]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockStreamSubscription extends _i1.Mock + implements _i5.StreamSubscription { + MockStreamSubscription() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isPaused => + (super.noSuchMethod(Invocation.getter(#isPaused), returnValue: false) + as bool); + + @override + _i5.Future cancel() => (super.noSuchMethod( + Invocation.method(#cancel, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + void onData(void Function(T)? handleData) => super.noSuchMethod( + Invocation.method(#onData, [handleData]), + returnValueForMissingStub: null, + ); + + @override + void onError(Function? handleError) => super.noSuchMethod( + Invocation.method(#onError, [handleError]), + returnValueForMissingStub: null, + ); + + @override + void onDone(void Function()? handleDone) => super.noSuchMethod( + Invocation.method(#onDone, [handleDone]), + returnValueForMissingStub: null, + ); + + @override + void pause([_i5.Future? resumeSignal]) => super.noSuchMethod( + Invocation.method(#pause, [resumeSignal]), + returnValueForMissingStub: null, + ); + + @override + void resume() => super.noSuchMethod( + Invocation.method(#resume, []), + returnValueForMissingStub: null, + ); + + @override + _i5.Future asFuture([E? futureValue]) => (super.noSuchMethod( + Invocation.method(#asFuture, [futureValue]), + returnValue: _i8.ifNotNull( + _i8.dummyValueOrNull( + this, + Invocation.method(#asFuture, [futureValue]), + ), + (E v) => _i5.Future.value(v), + ) ?? + _FakeFuture_9( + this, + Invocation.method(#asFuture, [futureValue]), + ), + ) as _i5.Future); } diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart index c9182aaa07..a931f13e87 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -8,7 +8,7 @@ import '../mocks/mocks.mocks.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; void main() { - late final Fixture fixture; + late Fixture fixture; givenRemoveConfigUpdate() { final update = RemoteConfigUpdate({'test'}); @@ -80,6 +80,23 @@ void main() { expect(featureFlags?.values.first.value, true); }); + test('stream canceld on close', () async { + final streamSubscription = MockStreamSubscription(); + when(streamSubscription.cancel()).thenAnswer((_) => Future.value()); + + final stream = MockStream(); + when(stream.listen(any)).thenAnswer((_) => streamSubscription); + + when(fixture.mockFirebaseRemoteConfig.onConfigUpdated) + .thenAnswer((_) => stream); + + final sut = await fixture.getSut({'test'}); + await sut.call(fixture.hub, fixture.options); + await sut.close(); + + verify(streamSubscription.cancel()).called(1); + }); + test('doesn`t add update to feature flags if key is not in the list', () async { givenRemoveConfigUpdate(); From 5bfac70df5ee4d86d9056882186a1109312feeb5 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 1 Apr 2025 14:50:10 +0200 Subject: [PATCH 09/29] add option to activate remote config --- firebase/example/example.dart | 53 ++++++++++++++++++ .../lib/src/sentry_firebase_integration.dart | 9 +++- .../src/sentry_firebase_integration_test.dart | 54 ++++++++++++++++++- 3 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 firebase/example/example.dart diff --git a/firebase/example/example.dart b/firebase/example/example.dart new file mode 100644 index 0000000000..4ec19ccc5b --- /dev/null +++ b/firebase/example/example.dart @@ -0,0 +1,53 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_remote_config_example/home_page.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_firebase/sentry_firebase.dart'; + +import 'firebase_options.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + final remoteConfig = FirebaseRemoteConfig.instance; + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(minutes: 1), + minimumFetchInterval: const Duration(hours: 1), + )); + + await SentryFlutter.init( + (options) { + options.dsn = + 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; + + final sentryFirebaseIntegration = SentryFirebaseIntegration( + remoteConfig, + ['firebase-feature-flag-a', 'firebase-feature-flag-b'], + // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. + activateOnConfigUpdated: false, + ); + options.sdk.integrations.add(sentryFirebaseIntegration); + }, + ); + + runApp(const RemoteConfigApp()); +} + +class RemoteConfigApp extends StatelessWidget { + const RemoteConfigApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Remote Config Example', + home: const HomePage(), + theme: ThemeData( + useMaterial3: true, + primarySwatch: Colors.blue, + ), + ); + } +} diff --git a/firebase/lib/src/sentry_firebase_integration.dart b/firebase/lib/src/sentry_firebase_integration.dart index a8befb8052..94934736ce 100644 --- a/firebase/lib/src/sentry_firebase_integration.dart +++ b/firebase/lib/src/sentry_firebase_integration.dart @@ -4,11 +4,13 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:sentry/sentry.dart'; class SentryFirebaseIntegration extends Integration { - SentryFirebaseIntegration(this._firebaseRemoteConfig, this._keys); + SentryFirebaseIntegration(this._firebaseRemoteConfig, this._keys, + {bool activateOnConfigUpdated = true}) + : _activateOnConfigUpdated = activateOnConfigUpdated; final FirebaseRemoteConfig _firebaseRemoteConfig; final Set _keys; - + final bool _activateOnConfigUpdated; StreamSubscription? _subscription; @override @@ -18,6 +20,9 @@ class SentryFirebaseIntegration extends Integration { return; } _subscription = _firebaseRemoteConfig.onConfigUpdated.listen((event) async { + if (_activateOnConfigUpdated) { + await _firebaseRemoteConfig.activate(); + } for (final updatedKey in event.updatedKeys) { if (_keys.contains(updatedKey)) { final value = _firebaseRemoteConfig.getBool(updatedKey); diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart index a931f13e87..ab22b9d35a 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -15,6 +15,9 @@ void main() { when(fixture.mockFirebaseRemoteConfig.onConfigUpdated) .thenAnswer((_) => Stream.value(update)); when(fixture.mockFirebaseRemoteConfig.getBool('test')).thenReturn(true); + + when(fixture.mockFirebaseRemoteConfig.activate()) + .thenAnswer((_) => Future.value(true)); } setUp(() async { @@ -115,6 +118,48 @@ void main() { expect(featureFlags, isNull); }); + + test('activate called by default', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut({'test'}); + sut.call(fixture.hub, fixture.options); + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); + + verify(fixture.mockFirebaseRemoteConfig.activate()).called(1); + }); + + test('activate not called if activateOnConfigUpdated is false', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut({'test'}, activateOnConfigUpdated: false); + sut.call(fixture.hub, fixture.options); + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); + + verifyNever(fixture.mockFirebaseRemoteConfig.activate()); + }); + + test('activate called if activateOnConfigUpdated is true', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut({'test'}, activateOnConfigUpdated: true); + sut.call(fixture.hub, fixture.options); + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); + + verify(fixture.mockFirebaseRemoteConfig.activate()).called(1); + }); } class Fixture { @@ -123,7 +168,12 @@ class Fixture { final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - Future getSut(Set keys) async { - return SentryFirebaseIntegration(mockFirebaseRemoteConfig, keys); + Future getSut(Set keys, + {bool activateOnConfigUpdated = false}) async { + return SentryFirebaseIntegration( + mockFirebaseRemoteConfig, + keys, + activateOnConfigUpdated: activateOnConfigUpdated, + ); } } From 6dc16c83515deb24b4d1fb57ff5dcdae7d8d288d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 2 Apr 2025 10:57:13 +0200 Subject: [PATCH 10/29] review feedback --- CHANGELOG.md | 1 + dart/lib/src/feature_flags_integration.dart | 2 +- dart/lib/src/sentry.dart | 2 +- dart/test/feature_flags_integration_test.dart | 3 +-- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8386df4dd7..7a49bfd018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ // Manually track a feature flag Sentry.addFeatureFlag('my-feature', true); ``` + ### Dependencies - Bump Android SDK from v8.2.0 to v8.6.0 ([#2819](https://github.com/getsentry/sentry-dart/pull/2819), [#2831](https://github.com/getsentry/sentry-dart/pull/2831)) diff --git a/dart/lib/src/feature_flags_integration.dart b/dart/lib/src/feature_flags_integration.dart index 7d40f2ecdf..0e7dc9148a 100644 --- a/dart/lib/src/feature_flags_integration.dart +++ b/dart/lib/src/feature_flags_integration.dart @@ -13,7 +13,7 @@ class FeatureFlagsIntegration extends Integration { @override void call(Hub hub, SentryOptions options) { _hub = hub; - options.sdk.addIntegration('featureFlagsIntegration'); + options.sdk.addIntegration('FeatureFlagsIntegration'); } FutureOr addFeatureFlag(String name, bool value) async { diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 1055740cb8..463dea1cd6 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -367,7 +367,7 @@ class Sentry { if (featureFlagsIntegration == null) { currentHub.options.logger( - SentryLevel.debug, + SentryLevel.warning, 'FeatureFlagsIntegration not found. Make sure Sentry is initialized before accessing the addFeatureFlag API.', ); return; diff --git a/dart/test/feature_flags_integration_test.dart b/dart/test/feature_flags_integration_test.dart index 5a8e4b9d17..abcb3b4417 100644 --- a/dart/test/feature_flags_integration_test.dart +++ b/dart/test/feature_flags_integration_test.dart @@ -1,4 +1,3 @@ -@TestOn('vm') library; import 'package:sentry/sentry.dart'; @@ -20,7 +19,7 @@ void main() { sut.call(fixture.hub, fixture.options); - expect(fixture.options.sdk.integrations.contains('featureFlagsIntegration'), + expect(fixture.options.sdk.integrations.contains('FeatureFlagsIntegration'), isTrue); }); From bdfaf448a412afa9e25baeab891bdda4c1eb2b7d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 11:12:17 +0200 Subject: [PATCH 11/29] format cl --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f79891bc6..cb8679b36e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ // Manually track a feature flag Sentry.addFeatureFlag('my-feature', true); ``` + ### Behavioral changes - Set log level to `warning` by default when `debug = true` ([#2836](https://github.com/getsentry/sentry-dart/pull/2836)) From 4f247a531b23092644a3791cd546a377696aa8a3 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 11:21:27 +0200 Subject: [PATCH 12/29] updates --- dart/lib/src/feature_flags_integration.dart | 4 ++-- dart/lib/src/protocol/sentry_feature_flag.dart | 3 ++- dart/lib/src/protocol/sentry_feature_flags.dart | 7 ++++--- dart/lib/src/sentry.dart | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/dart/lib/src/feature_flags_integration.dart b/dart/lib/src/feature_flags_integration.dart index 0e7dc9148a..75bb80120c 100644 --- a/dart/lib/src/feature_flags_integration.dart +++ b/dart/lib/src/feature_flags_integration.dart @@ -33,9 +33,9 @@ class FeatureFlagsIntegration extends Integration { values.add(SentryFeatureFlag(name: name, value: value)); } - final newFlags = flags.copyWith(values: values); + flags.values = values; - await _hub?.scope.setContexts(SentryFeatureFlags.type, newFlags); + await _hub?.scope.setContexts(SentryFeatureFlags.type, flags); } @override diff --git a/dart/lib/src/protocol/sentry_feature_flag.dart b/dart/lib/src/protocol/sentry_feature_flag.dart index 6d73f3c56d..94944a5370 100644 --- a/dart/lib/src/protocol/sentry_feature_flag.dart +++ b/dart/lib/src/protocol/sentry_feature_flag.dart @@ -2,7 +2,6 @@ import 'package:meta/meta.dart'; import 'access_aware_map.dart'; -@immutable class SentryFeatureFlag { final String name; final bool value; @@ -34,6 +33,7 @@ class SentryFeatureFlag { }; } + @Deprecated('Assign values directly to the instance.') SentryFeatureFlag copyWith({ String? name, bool? value, @@ -46,5 +46,6 @@ class SentryFeatureFlag { ); } + @Deprecated('Will be removed in a future version.') SentryFeatureFlag clone() => copyWith(); } diff --git a/dart/lib/src/protocol/sentry_feature_flags.dart b/dart/lib/src/protocol/sentry_feature_flags.dart index 8aa1eae24e..96f8170237 100644 --- a/dart/lib/src/protocol/sentry_feature_flags.dart +++ b/dart/lib/src/protocol/sentry_feature_flags.dart @@ -2,14 +2,13 @@ import 'package:meta/meta.dart'; import 'sentry_feature_flag.dart'; import 'access_aware_map.dart'; -@immutable class SentryFeatureFlags { static const type = 'flags'; - final List values; + List values; @internal - final Map? unknown; + Map? unknown; SentryFeatureFlags({ required this.values, @@ -37,6 +36,7 @@ class SentryFeatureFlags { }; } + @Deprecated('Assign values directly to the instance.') SentryFeatureFlags copyWith({ List? values, Map? unknown, @@ -48,5 +48,6 @@ class SentryFeatureFlags { ); } + @Deprecated('Will be removed in a future version.') SentryFeatureFlags clone() => copyWith(); } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 463dea1cd6..eed7c3abbe 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -368,7 +368,7 @@ class Sentry { if (featureFlagsIntegration == null) { currentHub.options.logger( SentryLevel.warning, - 'FeatureFlagsIntegration not found. Make sure Sentry is initialized before accessing the addFeatureFlag API.', + '$FeatureFlagsIntegration not found. Make sure Sentry is initialized before accessing the addFeatureFlag API.', ); return; } From 112f2119e31f8efcbf75739e141c18965d25a4dc Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 15:47:43 +0200 Subject: [PATCH 13/29] add cl entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb8679b36e..2a23cfd85e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ // Manually track a feature flag Sentry.addFeatureFlag('my-feature', true); ``` +- Firebase Remote Config Integration ([#2837](https://github.com/getsentry/sentry-dart/pull/2837)) ### Behavioral changes From 969e12d23f4983f4c6393fabb73be7db986df9d2 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 15:54:09 +0200 Subject: [PATCH 14/29] log message --- firebase/lib/src/sentry_firebase_integration.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/firebase/lib/src/sentry_firebase_integration.dart b/firebase/lib/src/sentry_firebase_integration.dart index 94934736ce..a2c9df5371 100644 --- a/firebase/lib/src/sentry_firebase_integration.dart +++ b/firebase/lib/src/sentry_firebase_integration.dart @@ -16,7 +16,10 @@ class SentryFirebaseIntegration extends Integration { @override FutureOr call(Hub hub, SentryOptions options) async { if (_keys.isEmpty) { - // TODO: log warning + options.logger( + SentryLevel.warning, + 'No keys provided to $SentryFirebaseIntegration. Will not track feature flags.', + ); return; } _subscription = _firebaseRemoteConfig.onConfigUpdated.listen((event) async { From 17fd9c125c79822bf683377eec8a23132057ba24 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 16:05:02 +0200 Subject: [PATCH 15/29] update readme --- .github/workflows/firebase.yml | 58 ++++++++++++++++++++++ firebase/README.md | 90 ++++++++++++++++++++++++++++++++++ firebase/example/example.dart | 3 +- 3 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/firebase.yml diff --git a/.github/workflows/firebase.yml b/.github/workflows/firebase.yml new file mode 100644 index 0000000000..f2d3fd4281 --- /dev/null +++ b/.github/workflows/firebase.yml @@ -0,0 +1,58 @@ +name: sentry-firebase +on: + push: + branches: + - main + - release/** + pull_request: + paths: + - '!**/*.md' + - '!**/class-diagram.svg' + - '.github/workflows/firebase.yml' + - '.github/workflows/analyze.yml' + - '.github/actions/dart-test/**' + - '.github/actions/coverage/**' + - 'dart/**' + - 'flutter/**' + - 'firebase/**' + +jobs: + cancel-previous-workflow: + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # pin@0.12.1 + with: + access_token: ${{ github.token }} + + build: + name: '${{ matrix.os }} | ${{ matrix.sdk }}' + runs-on: ${{ matrix.os }}-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [macos, ubuntu, windows] + sdk: [stable, beta] + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/dart-test + with: + directory: firebase + web: false + +# TODO: don't set coverage for now to finish publishing it +# - uses: ./.github/actions/coverage +# if: runner.os == 'Linux' && matrix.sdk == 'stable' +# with: +# token: ${{ secrets.CODECOV_TOKEN }} +# directory: firebase +# coverage: sentry_firebase +# min-coverage: 55 + + analyze: + uses: ./.github/workflows/analyze.yml + with: + package: firebase diff --git a/firebase/README.md b/firebase/README.md index 0b9b31a02e..9902a393c4 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -5,5 +5,95 @@

+ +=========== + +

+ + + +
+

+ Sentry integration for `firebase_remote_config` package =========== + +| package | build | pub | likes | popularity | pub points | +|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| ------- | +| sentry_firebase | [![build](https://github.com/getsentry/sentry-dart/actions/workflows/firebase.yml/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-firebase) | [![pub package](https://img.shields.io/pub/v/sentry_firebase.svg)](https://pub.dev/packages/sentry_firebase) | [![likes](https://img.shields.io/pub/likes/sentry_firebase)](https://pub.dev/packages/sentry_firebase/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_firebase)](https://pub.dev/packages/sentry_firebase/score) | [![pub points](https://img.shields.io/pub/points/sentry_firebase)](https://pub.dev/packages/sentry_firebase/score) + +Integration for [`firebase_remote_config`](https://pub.dev/packages/firebase_remote_config) package. Track changes to firebase boolean values as feature flags in Sentry.io + +#### Usage + +- Sign up for a Sentry.io account and get a DSN at https://sentry.io. + +- Follow the installing instructions on [pub.dev](https://pub.dev/packages/sentry/install). + +- Initialize the Sentry SDK using the DSN issued by Sentry.io. + +- Call... + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_remote_config_example/home_page.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_firebase/sentry_firebase.dart'; + +import 'firebase_options.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + final remoteConfig = FirebaseRemoteConfig.instance; + await remoteConfig.setConfigSettings(RemoteConfigSettings( + fetchTimeout: const Duration(minutes: 1), + minimumFetchInterval: const Duration(hours: 1), + )); + + await SentryFlutter.init( + (options) { + options.dsn = 'https://example@sentry.io/add-your-dsn-here'; + + final sentryFirebaseIntegration = SentryFirebaseIntegration( + remoteConfig, + ['firebase-feature-flag-a', 'firebase-feature-flag-b'], + // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. + activateOnConfigUpdated: false, + ); + options.sdk.integrations.add(sentryFirebaseIntegration); + }, + ); + + runApp(const RemoteConfigApp()); +} + +class RemoteConfigApp extends StatelessWidget { + const RemoteConfigApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Remote Config Example', + home: const HomePage(), + theme: ThemeData( + useMaterial3: true, + primarySwatch: Colors.blue, + ), + ); + } +} +``` + +#### Resources + +* [![Flutter docs](https://img.shields.io/badge/documentation-sentry.io-green.svg?label=flutter%20docs)](https://docs.sentry.io/platforms/flutter/) +* [![Dart docs](https://img.shields.io/badge/documentation-sentry.io-green.svg?label=dart%20docs)](https://docs.sentry.io/platforms/dart/) +* [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-dart.svg)](https://github.com/getsentry/sentry-dart/discussions) +* [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K) +* [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry) +* [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) diff --git a/firebase/example/example.dart b/firebase/example/example.dart index 4ec19ccc5b..c95959c97d 100644 --- a/firebase/example/example.dart +++ b/firebase/example/example.dart @@ -20,8 +20,7 @@ Future main() async { await SentryFlutter.init( (options) { - options.dsn = - 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; + options.dsn = 'https://example@sentry.io/add-your-dsn-here'; final sentryFirebaseIntegration = SentryFirebaseIntegration( remoteConfig, From 63c08b0010f72af736e378b1b4ae09dcadb227cd Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 16:14:25 +0200 Subject: [PATCH 16/29] run flutter test --- .github/workflows/firebase.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/firebase.yml b/.github/workflows/firebase.yml index f2d3fd4281..988333e48e 100644 --- a/.github/workflows/firebase.yml +++ b/.github/workflows/firebase.yml @@ -38,7 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/dart-test + - uses: ./.github/actions/flutter-test with: directory: firebase web: false From 0b2b321871062b1d04f84c7fe99aec5477d44c11 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 16:21:01 +0200 Subject: [PATCH 17/29] use flutter sdk --- .github/workflows/firebase.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/firebase.yml b/.github/workflows/firebase.yml index 988333e48e..3d76aed73d 100644 --- a/.github/workflows/firebase.yml +++ b/.github/workflows/firebase.yml @@ -56,3 +56,4 @@ jobs: uses: ./.github/workflows/analyze.yml with: package: firebase + sdk: flutter From 2a415c2efab8ff5ae27b78df6cb0dc7f77c62b2e Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 16:23:32 +0200 Subject: [PATCH 18/29] fix & format --- firebase/lib/src/sentry_firebase_integration.dart | 8 +++++--- firebase/test/src/sentry_firebase_integration_test.dart | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/firebase/lib/src/sentry_firebase_integration.dart b/firebase/lib/src/sentry_firebase_integration.dart index a2c9df5371..a4cccbf5e9 100644 --- a/firebase/lib/src/sentry_firebase_integration.dart +++ b/firebase/lib/src/sentry_firebase_integration.dart @@ -4,9 +4,11 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:sentry/sentry.dart'; class SentryFirebaseIntegration extends Integration { - SentryFirebaseIntegration(this._firebaseRemoteConfig, this._keys, - {bool activateOnConfigUpdated = true}) - : _activateOnConfigUpdated = activateOnConfigUpdated; + SentryFirebaseIntegration( + this._firebaseRemoteConfig, + this._keys, { + bool activateOnConfigUpdated = true, + }) : _activateOnConfigUpdated = activateOnConfigUpdated; final FirebaseRemoteConfig _firebaseRemoteConfig; final Set _keys; diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart index ab22b9d35a..7f22c46292 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -168,8 +168,10 @@ class Fixture { final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - Future getSut(Set keys, - {bool activateOnConfigUpdated = false}) async { + Future getSut( + Set keys, { + bool activateOnConfigUpdated = false, + }) async { return SentryFirebaseIntegration( mockFirebaseRemoteConfig, keys, From 52a5a16ea7435da011f514af07899ebf798a6d25 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 16:34:01 +0200 Subject: [PATCH 19/29] fix test expectations --- .../src/sentry_firebase_integration_test.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart index 7f22c46292..c1a2db6e2f 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -170,12 +170,19 @@ class Fixture { Future getSut( Set keys, { - bool activateOnConfigUpdated = false, + bool? activateOnConfigUpdated, }) async { - return SentryFirebaseIntegration( - mockFirebaseRemoteConfig, - keys, - activateOnConfigUpdated: activateOnConfigUpdated, - ); + if (activateOnConfigUpdated == null) { + return SentryFirebaseIntegration( + mockFirebaseRemoteConfig, + keys, + ); + } else { + return SentryFirebaseIntegration( + mockFirebaseRemoteConfig, + keys, + activateOnConfigUpdated: activateOnConfigUpdated, + ); + } } } From b64acc2dc3fd548be68bfc5b22ea57ae9947a54e Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 16:46:39 +0200 Subject: [PATCH 20/29] update example values --- firebase/README.md | 2 +- firebase/example/example.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase/README.md b/firebase/README.md index 9902a393c4..8c7becf642 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -61,7 +61,7 @@ Future main() async { final sentryFirebaseIntegration = SentryFirebaseIntegration( remoteConfig, - ['firebase-feature-flag-a', 'firebase-feature-flag-b'], + ['firebase_feature_flag_a', 'firebase_feature_flag_b'], // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); diff --git a/firebase/example/example.dart b/firebase/example/example.dart index c95959c97d..6b4cc756db 100644 --- a/firebase/example/example.dart +++ b/firebase/example/example.dart @@ -24,7 +24,7 @@ Future main() async { final sentryFirebaseIntegration = SentryFirebaseIntegration( remoteConfig, - ['firebase-feature-flag-a', 'firebase-feature-flag-b'], + ['firebase_feature_flag_a', 'firebase_feature_flag_b'], // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); From 213c627dee6609f5d06a9ffea7bdceea9e7968ed Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 8 Apr 2025 10:47:30 +0200 Subject: [PATCH 21/29] fix example --- firebase/README.md | 6 +++--- firebase/example/example.dart | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase/README.md b/firebase/README.md index 8c7becf642..b0b35a51ec 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -58,14 +58,14 @@ Future main() async { await SentryFlutter.init( (options) { options.dsn = 'https://example@sentry.io/add-your-dsn-here'; - + final sentryFirebaseIntegration = SentryFirebaseIntegration( remoteConfig, - ['firebase_feature_flag_a', 'firebase_feature_flag_b'], + {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); - options.sdk.integrations.add(sentryFirebaseIntegration); + options.addIntegration(sentryFirebaseIntegration); }, ); diff --git a/firebase/example/example.dart b/firebase/example/example.dart index 6b4cc756db..ec78b6ef61 100644 --- a/firebase/example/example.dart +++ b/firebase/example/example.dart @@ -24,11 +24,11 @@ Future main() async { final sentryFirebaseIntegration = SentryFirebaseIntegration( remoteConfig, - ['firebase_feature_flag_a', 'firebase_feature_flag_b'], + {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); - options.sdk.integrations.add(sentryFirebaseIntegration); + options.addIntegration(sentryFirebaseIntegration); }, ); From 28fd2316de8ce8905bc9b221e694dd02e816843d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 8 Apr 2025 10:49:24 +0200 Subject: [PATCH 22/29] add firebase to version bump --- scripts/bump-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 8920aa395f..e642013c17 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -10,7 +10,7 @@ NEW_VERSION="${2}" echo "Current version: ${OLD_VERSION}" echo "Bumping version: ${NEW_VERSION}" -for pkg in {dart,flutter,logging,dio,file,sqflite,drift,hive,isar,link}; do +for pkg in {dart,flutter,logging,dio,file,sqflite,drift,hive,isar,link,firebase}; do # Bump version in pubspec.yaml perl -pi -e "s/^version: .*/version: $NEW_VERSION/" $pkg/pubspec.yaml # Bump sentry dependency version in pubspec.yaml From 012fd334ca99027bec64e32c329ff4575fc32025 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 8 Apr 2025 10:51:38 +0200 Subject: [PATCH 23/29] update yaml files --- .craft.yml | 5 ++++- .github/workflows/diagrams.yml | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.craft.yml b/.craft.yml index daf9c4106c..494ee8251f 100644 --- a/.craft.yml +++ b/.craft.yml @@ -18,6 +18,7 @@ targets: drift: isar: link: + # firebase: TODO: Uncomment after we published sentry_firebase - name: github - name: registry sdks: @@ -31,4 +32,6 @@ targets: pub:sentry_hive: pub:sentry_isar: # TODO: after we published link we need to add it to the registry repo and then uncomment here - # pub:sentry_link: \ No newline at end of file + # pub:sentry_link: + # TODO: after we published firebase we need to add it to the registry repo and then uncomment here + # pub:sentry_firebase: \ No newline at end of file diff --git a/.github/workflows/diagrams.yml b/.github/workflows/diagrams.yml index d909808b72..818e45e258 100644 --- a/.github/workflows/diagrams.yml +++ b/.github/workflows/diagrams.yml @@ -51,6 +51,14 @@ jobs: working-directory: ./isar run: lakos . -i "{test/**,example/**}" | dot -Tsvg -o class-diagram.svg + - name: link + working-directory: ./link + run: lakos . -i "{test/**,example/**}" | dot -Tsvg -o class-diagram.svg + + - name: firebase + working-directory: ./firebase + run: lakos . -i "{test/**,example/**}" | dot -Tsvg -o class-diagram.svg + # Source: https://stackoverflow.com/a/58035262 - name: Extract branch name shell: bash From 03016baad1abaea7fce0120297f2e195ba8079dc Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 8 Apr 2025 10:55:09 +0200 Subject: [PATCH 24/29] make params named --- firebase/README.md | 6 +++--- firebase/example/example.dart | 4 ++-- .../lib/src/sentry_firebase_integration.dart | 16 +++++++++------- .../src/sentry_firebase_integration_test.dart | 10 +++++----- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/firebase/README.md b/firebase/README.md index b0b35a51ec..3b2cea164d 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -58,10 +58,10 @@ Future main() async { await SentryFlutter.init( (options) { options.dsn = 'https://example@sentry.io/add-your-dsn-here'; - + final sentryFirebaseIntegration = SentryFirebaseIntegration( - remoteConfig, - {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, + firebaseRemoteConfig: remoteConfig, + featureFlagKeys: {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); diff --git a/firebase/example/example.dart b/firebase/example/example.dart index ec78b6ef61..b1a14ff390 100644 --- a/firebase/example/example.dart +++ b/firebase/example/example.dart @@ -23,8 +23,8 @@ Future main() async { options.dsn = 'https://example@sentry.io/add-your-dsn-here'; final sentryFirebaseIntegration = SentryFirebaseIntegration( - remoteConfig, - {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, + firebaseRemoteConfig: remoteConfig, + featureFlagKeys: {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); diff --git a/firebase/lib/src/sentry_firebase_integration.dart b/firebase/lib/src/sentry_firebase_integration.dart index a4cccbf5e9..3359f6053e 100644 --- a/firebase/lib/src/sentry_firebase_integration.dart +++ b/firebase/lib/src/sentry_firebase_integration.dart @@ -4,20 +4,22 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:sentry/sentry.dart'; class SentryFirebaseIntegration extends Integration { - SentryFirebaseIntegration( - this._firebaseRemoteConfig, - this._keys, { + SentryFirebaseIntegration({ + required FirebaseRemoteConfig firebaseRemoteConfig, + required Set featureFlagKeys, bool activateOnConfigUpdated = true, - }) : _activateOnConfigUpdated = activateOnConfigUpdated; + }) : _firebaseRemoteConfig = firebaseRemoteConfig, + _featureFlagKeys = featureFlagKeys, + _activateOnConfigUpdated = activateOnConfigUpdated; final FirebaseRemoteConfig _firebaseRemoteConfig; - final Set _keys; + final Set _featureFlagKeys; final bool _activateOnConfigUpdated; StreamSubscription? _subscription; @override FutureOr call(Hub hub, SentryOptions options) async { - if (_keys.isEmpty) { + if (_featureFlagKeys.isEmpty) { options.logger( SentryLevel.warning, 'No keys provided to $SentryFirebaseIntegration. Will not track feature flags.', @@ -29,7 +31,7 @@ class SentryFirebaseIntegration extends Integration { await _firebaseRemoteConfig.activate(); } for (final updatedKey in event.updatedKeys) { - if (_keys.contains(updatedKey)) { + if (_featureFlagKeys.contains(updatedKey)) { final value = _firebaseRemoteConfig.getBool(updatedKey); await Sentry.addFeatureFlag(updatedKey, value); } diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_integration_test.dart index c1a2db6e2f..6cc5932f7e 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_integration_test.dart @@ -169,18 +169,18 @@ class Fixture { final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); Future getSut( - Set keys, { + Set featureFlagKeys, { bool? activateOnConfigUpdated, }) async { if (activateOnConfigUpdated == null) { return SentryFirebaseIntegration( - mockFirebaseRemoteConfig, - keys, + firebaseRemoteConfig: mockFirebaseRemoteConfig, + featureFlagKeys: featureFlagKeys, ); } else { return SentryFirebaseIntegration( - mockFirebaseRemoteConfig, - keys, + firebaseRemoteConfig: mockFirebaseRemoteConfig, + featureFlagKeys: featureFlagKeys, activateOnConfigUpdated: activateOnConfigUpdated, ); } From 8c85832064ef3d6bfa9f3ac8a84540ef9832e7b3 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 8 Apr 2025 13:51:09 +0200 Subject: [PATCH 25/29] change name to firebase_remote_config --- .craft.yml | 4 ++-- .github/workflows/firebase.yml | 15 ++++++--------- firebase/README.md | 8 ++++---- firebase/example/example.dart | 6 +++--- firebase/lib/sentry_firebase.dart | 3 --- firebase/lib/sentry_firebase_remote_config.dart | 3 +++ ...entry_firebase_remote_config_integration.dart} | 8 ++++---- firebase/pubspec.yaml | 2 +- ..._firebase_remote_config_integration_test.dart} | 11 +++++------ 9 files changed, 28 insertions(+), 32 deletions(-) delete mode 100644 firebase/lib/sentry_firebase.dart create mode 100644 firebase/lib/sentry_firebase_remote_config.dart rename firebase/lib/src/{sentry_firebase_integration.dart => sentry_firebase_remote_config_integration.dart} (81%) rename firebase/test/src/{sentry_firebase_integration_test.dart => sentry_firebase_remote_config_integration_test.dart} (93%) diff --git a/.craft.yml b/.craft.yml index 494ee8251f..05c9b52635 100644 --- a/.craft.yml +++ b/.craft.yml @@ -18,7 +18,7 @@ targets: drift: isar: link: - # firebase: TODO: Uncomment after we published sentry_firebase + firebase: - name: github - name: registry sdks: @@ -34,4 +34,4 @@ targets: # TODO: after we published link we need to add it to the registry repo and then uncomment here # pub:sentry_link: # TODO: after we published firebase we need to add it to the registry repo and then uncomment here - # pub:sentry_firebase: \ No newline at end of file + # pub:sentry_firebase_remote_config: \ No newline at end of file diff --git a/.github/workflows/firebase.yml b/.github/workflows/firebase.yml index 3d76aed73d..126264b489 100644 --- a/.github/workflows/firebase.yml +++ b/.github/workflows/firebase.yml @@ -16,15 +16,12 @@ on: - 'flutter/**' - 'firebase/**' -jobs: - cancel-previous-workflow: - runs-on: ubuntu-latest - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # pin@0.12.1 - with: - access_token: ${{ github.token }} +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +jobs: build: name: '${{ matrix.os }} | ${{ matrix.sdk }}' runs-on: ${{ matrix.os }}-latest @@ -49,7 +46,7 @@ jobs: # with: # token: ${{ secrets.CODECOV_TOKEN }} # directory: firebase -# coverage: sentry_firebase +# coverage: sentry_firebase_remote_config # min-coverage: 55 analyze: diff --git a/firebase/README.md b/firebase/README.md index 3b2cea164d..8e503d8ea6 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -20,7 +20,7 @@ Sentry integration for `firebase_remote_config` package | package | build | pub | likes | popularity | pub points | |-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| ------- | -| sentry_firebase | [![build](https://github.com/getsentry/sentry-dart/actions/workflows/firebase.yml/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-firebase) | [![pub package](https://img.shields.io/pub/v/sentry_firebase.svg)](https://pub.dev/packages/sentry_firebase) | [![likes](https://img.shields.io/pub/likes/sentry_firebase)](https://pub.dev/packages/sentry_firebase/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_firebase)](https://pub.dev/packages/sentry_firebase/score) | [![pub points](https://img.shields.io/pub/points/sentry_firebase)](https://pub.dev/packages/sentry_firebase/score) +| sentry_firebase_remote_config | [![build](https://github.com/getsentry/sentry-dart/actions/workflows/firebase.yml/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-firebase) | [![pub package](https://img.shields.io/pub/v/sentry_firebase_remote_config.svg)](https://pub.dev/packages/sentry_firebase_remote_config) | [![likes](https://img.shields.io/pub/likes/sentry_firebase_remote_config)](https://pub.dev/packages/sentry_firebase_remote_config/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_firebase_remote_config)](https://pub.dev/packages/sentry_firebase_remote_config/score) | [![pub points](https://img.shields.io/pub/points/sentry_firebase_remote_config)](https://pub.dev/packages/sentry_firebase_remote_config/score) Integration for [`firebase_remote_config`](https://pub.dev/packages/firebase_remote_config) package. Track changes to firebase boolean values as feature flags in Sentry.io @@ -39,7 +39,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_remote_config_example/home_page.dart'; import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_firebase/sentry_firebase.dart'; +import 'package:sentry_firebase_remote_config/sentry_firebase_remote_config.dart'; import 'firebase_options.dart'; @@ -59,13 +59,13 @@ Future main() async { (options) { options.dsn = 'https://example@sentry.io/add-your-dsn-here'; - final sentryFirebaseIntegration = SentryFirebaseIntegration( + final sentryFirebaseRemoteConfigIntegration = SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: remoteConfig, featureFlagKeys: {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); - options.addIntegration(sentryFirebaseIntegration); + options.addIntegration(sentryFirebaseRemoteConfigIntegration); }, ); diff --git a/firebase/example/example.dart b/firebase/example/example.dart index b1a14ff390..5c6e25e0aa 100644 --- a/firebase/example/example.dart +++ b/firebase/example/example.dart @@ -2,7 +2,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_remote_config_example/home_page.dart'; import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_firebase/sentry_firebase.dart'; +import 'package:sentry_firebase_remote_config/sentry_firebase_remote_config.dart'; import 'firebase_options.dart'; @@ -22,13 +22,13 @@ Future main() async { (options) { options.dsn = 'https://example@sentry.io/add-your-dsn-here'; - final sentryFirebaseIntegration = SentryFirebaseIntegration( + final sentryFirebaseRemoteConfigIntegration = SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: remoteConfig, featureFlagKeys: {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); - options.addIntegration(sentryFirebaseIntegration); + options.addIntegration(sentryFirebaseRemoteConfigIntegration); }, ); diff --git a/firebase/lib/sentry_firebase.dart b/firebase/lib/sentry_firebase.dart deleted file mode 100644 index 25ddd03f57..0000000000 --- a/firebase/lib/sentry_firebase.dart +++ /dev/null @@ -1,3 +0,0 @@ -library; - -export 'src/sentry_firebase_integration.dart'; diff --git a/firebase/lib/sentry_firebase_remote_config.dart b/firebase/lib/sentry_firebase_remote_config.dart new file mode 100644 index 0000000000..39222fe090 --- /dev/null +++ b/firebase/lib/sentry_firebase_remote_config.dart @@ -0,0 +1,3 @@ +library; + +export 'src/sentry_firebase_remote_config_integration.dart'; diff --git a/firebase/lib/src/sentry_firebase_integration.dart b/firebase/lib/src/sentry_firebase_remote_config_integration.dart similarity index 81% rename from firebase/lib/src/sentry_firebase_integration.dart rename to firebase/lib/src/sentry_firebase_remote_config_integration.dart index 3359f6053e..bb5cc0f132 100644 --- a/firebase/lib/src/sentry_firebase_integration.dart +++ b/firebase/lib/src/sentry_firebase_remote_config_integration.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:sentry/sentry.dart'; -class SentryFirebaseIntegration extends Integration { - SentryFirebaseIntegration({ +class SentryFirebaseRemoteConfigIntegration extends Integration { + SentryFirebaseRemoteConfigIntegration({ required FirebaseRemoteConfig firebaseRemoteConfig, required Set featureFlagKeys, bool activateOnConfigUpdated = true, @@ -22,7 +22,7 @@ class SentryFirebaseIntegration extends Integration { if (_featureFlagKeys.isEmpty) { options.logger( SentryLevel.warning, - 'No keys provided to $SentryFirebaseIntegration. Will not track feature flags.', + 'No keys provided to $SentryFirebaseRemoteConfigIntegration. Will not track feature flags.', ); return; } @@ -37,7 +37,7 @@ class SentryFirebaseIntegration extends Integration { } } }); - options.sdk.addIntegration('sentryFirebaseIntegration'); + options.sdk.addIntegration('$SentryFirebaseRemoteConfigIntegration'); } @override diff --git a/firebase/pubspec.yaml b/firebase/pubspec.yaml index 52965e3d68..c6dbf430cf 100644 --- a/firebase/pubspec.yaml +++ b/firebase/pubspec.yaml @@ -1,4 +1,4 @@ -name: sentry_firebase +name: sentry_firebase_remote_config description: "Sentry integration to use feature flags from Firebase Remote Config." version: 9.0.0-alpha.2 homepage: https://docs.sentry.io/platforms/flutter/ diff --git a/firebase/test/src/sentry_firebase_integration_test.dart b/firebase/test/src/sentry_firebase_remote_config_integration_test.dart similarity index 93% rename from firebase/test/src/sentry_firebase_integration_test.dart rename to firebase/test/src/sentry_firebase_remote_config_integration_test.dart index 6cc5932f7e..f9857dc778 100644 --- a/firebase/test/src/sentry_firebase_integration_test.dart +++ b/firebase/test/src/sentry_firebase_remote_config_integration_test.dart @@ -1,5 +1,4 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_firebase/src/sentry_firebase_integration.dart'; import 'package:sentry/sentry.dart'; import 'package:mockito/mockito.dart'; @@ -45,7 +44,7 @@ void main() { sut.call(fixture.hub, fixture.options); expect( - fixture.options.sdk.integrations.contains('sentryFirebaseIntegration'), + fixture.options.sdk.integrations.contains('$SentryFirebaseRemoteConfigIntegration'), isTrue, ); }); @@ -58,7 +57,7 @@ void main() { sut.call(fixture.hub, fixture.options); expect( - fixture.options.sdk.integrations.contains('sentryFirebaseIntegration'), + fixture.options.sdk.integrations.contains('$SentryFirebaseRemoteConfigIntegration'), isFalse, ); }); @@ -168,17 +167,17 @@ class Fixture { final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - Future getSut( + Future getSut( Set featureFlagKeys, { bool? activateOnConfigUpdated, }) async { if (activateOnConfigUpdated == null) { - return SentryFirebaseIntegration( + return SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: mockFirebaseRemoteConfig, featureFlagKeys: featureFlagKeys, ); } else { - return SentryFirebaseIntegration( + return SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: mockFirebaseRemoteConfig, featureFlagKeys: featureFlagKeys, activateOnConfigUpdated: activateOnConfigUpdated, From 40a5b237e1838d47184cbb5f067927802d3b21e8 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 8 Apr 2025 15:20:54 +0200 Subject: [PATCH 26/29] fix test and added integration --- firebase/example/example.dart | 3 ++- .../lib/src/sentry_firebase_remote_config_integration.dart | 4 ++-- .../sentry_firebase_remote_config_integration_test.dart | 7 +++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/firebase/example/example.dart b/firebase/example/example.dart index 5c6e25e0aa..b7d6b71a1d 100644 --- a/firebase/example/example.dart +++ b/firebase/example/example.dart @@ -22,7 +22,8 @@ Future main() async { (options) { options.dsn = 'https://example@sentry.io/add-your-dsn-here'; - final sentryFirebaseRemoteConfigIntegration = SentryFirebaseRemoteConfigIntegration( + final sentryFirebaseRemoteConfigIntegration = + SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: remoteConfig, featureFlagKeys: {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. diff --git a/firebase/lib/src/sentry_firebase_remote_config_integration.dart b/firebase/lib/src/sentry_firebase_remote_config_integration.dart index bb5cc0f132..59f853239c 100644 --- a/firebase/lib/src/sentry_firebase_remote_config_integration.dart +++ b/firebase/lib/src/sentry_firebase_remote_config_integration.dart @@ -22,7 +22,7 @@ class SentryFirebaseRemoteConfigIntegration extends Integration { if (_featureFlagKeys.isEmpty) { options.logger( SentryLevel.warning, - 'No keys provided to $SentryFirebaseRemoteConfigIntegration. Will not track feature flags.', + 'No keys provided to SentryFirebaseRemoteConfigIntegration. Will not track feature flags.', ); return; } @@ -37,7 +37,7 @@ class SentryFirebaseRemoteConfigIntegration extends Integration { } } }); - options.sdk.addIntegration('$SentryFirebaseRemoteConfigIntegration'); + options.sdk.addIntegration('SentryFirebaseRemoteConfigIntegration'); } @override diff --git a/firebase/test/src/sentry_firebase_remote_config_integration_test.dart b/firebase/test/src/sentry_firebase_remote_config_integration_test.dart index f9857dc778..bd32465949 100644 --- a/firebase/test/src/sentry_firebase_remote_config_integration_test.dart +++ b/firebase/test/src/sentry_firebase_remote_config_integration_test.dart @@ -5,6 +5,7 @@ import 'package:mockito/mockito.dart'; import '../mocks/mocks.mocks.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:sentry_firebase_remote_config/sentry_firebase_remote_config.dart'; void main() { late Fixture fixture; @@ -44,7 +45,8 @@ void main() { sut.call(fixture.hub, fixture.options); expect( - fixture.options.sdk.integrations.contains('$SentryFirebaseRemoteConfigIntegration'), + fixture.options.sdk.integrations + .contains('SentryFirebaseRemoteConfigIntegration'), isTrue, ); }); @@ -57,7 +59,8 @@ void main() { sut.call(fixture.hub, fixture.options); expect( - fixture.options.sdk.integrations.contains('$SentryFirebaseRemoteConfigIntegration'), + fixture.options.sdk.integrations + .contains('SentryFirebaseRemoteConfigIntegration'), isFalse, ); }); From eac7227ba507da2b9dac926a548fa5f081786448 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 9 Apr 2025 10:28:41 +0200 Subject: [PATCH 27/29] rename package folder, observe all boolean keys --- .craft.yml | 2 +- .github/workflows/diagrams.yml | 4 +- ...irebase.yml => firebase_remote_config.yml} | 12 ++-- .../.gitignore | 0 .../.metadata | 0 .../CHANGELOG.md | 0 {firebase => firebase_remote_config}/LICENSE | 0 .../README.md | 1 - .../analysis_options.yaml | 0 .../dartdoc_options.yaml | 0 .../example/example.dart | 1 - .../lib/sentry_firebase_remote_config.dart | 0 ...ry_firebase_remote_config_integration.dart | 28 +++++---- .../pubspec.yaml | 0 .../pubspec_overrides.yaml | 0 .../test/mocks/mocks.dart | 0 .../test/mocks/mocks.mocks.dart | 0 ...rebase_remote_config_integration_test.dart | 59 ++++--------------- scripts/bump-version.sh | 2 +- 19 files changed, 38 insertions(+), 71 deletions(-) rename .github/workflows/{firebase.yml => firebase_remote_config.yml} (83%) rename {firebase => firebase_remote_config}/.gitignore (100%) rename {firebase => firebase_remote_config}/.metadata (100%) rename {firebase => firebase_remote_config}/CHANGELOG.md (100%) rename {firebase => firebase_remote_config}/LICENSE (100%) rename {firebase => firebase_remote_config}/README.md (98%) rename {firebase => firebase_remote_config}/analysis_options.yaml (100%) rename {firebase => firebase_remote_config}/dartdoc_options.yaml (100%) rename {firebase => firebase_remote_config}/example/example.dart (95%) rename {firebase => firebase_remote_config}/lib/sentry_firebase_remote_config.dart (100%) rename {firebase => firebase_remote_config}/lib/src/sentry_firebase_remote_config_integration.dart (72%) rename {firebase => firebase_remote_config}/pubspec.yaml (100%) rename {firebase => firebase_remote_config}/pubspec_overrides.yaml (100%) rename {firebase => firebase_remote_config}/test/mocks/mocks.dart (100%) rename {firebase => firebase_remote_config}/test/mocks/mocks.mocks.dart (100%) rename {firebase => firebase_remote_config}/test/src/sentry_firebase_remote_config_integration_test.dart (70%) diff --git a/.craft.yml b/.craft.yml index 05c9b52635..19249c5ef9 100644 --- a/.craft.yml +++ b/.craft.yml @@ -18,7 +18,7 @@ targets: drift: isar: link: - firebase: + firebase_remote_config: - name: github - name: registry sdks: diff --git a/.github/workflows/diagrams.yml b/.github/workflows/diagrams.yml index 818e45e258..06e9754282 100644 --- a/.github/workflows/diagrams.yml +++ b/.github/workflows/diagrams.yml @@ -55,8 +55,8 @@ jobs: working-directory: ./link run: lakos . -i "{test/**,example/**}" | dot -Tsvg -o class-diagram.svg - - name: firebase - working-directory: ./firebase + - name: firebase_remote_config + working-directory: ./firebase_remote_config run: lakos . -i "{test/**,example/**}" | dot -Tsvg -o class-diagram.svg # Source: https://stackoverflow.com/a/58035262 diff --git a/.github/workflows/firebase.yml b/.github/workflows/firebase_remote_config.yml similarity index 83% rename from .github/workflows/firebase.yml rename to .github/workflows/firebase_remote_config.yml index 126264b489..09ad1f6a93 100644 --- a/.github/workflows/firebase.yml +++ b/.github/workflows/firebase_remote_config.yml @@ -1,4 +1,4 @@ -name: sentry-firebase +name: sentry-firebase-remote-config on: push: branches: @@ -8,13 +8,13 @@ on: paths: - '!**/*.md' - '!**/class-diagram.svg' - - '.github/workflows/firebase.yml' + - '.github/workflows/firebase_remote_config.yml' - '.github/workflows/analyze.yml' - '.github/actions/dart-test/**' - '.github/actions/coverage/**' - 'dart/**' - 'flutter/**' - - 'firebase/**' + - 'firebase_remote_config/**' # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value concurrency: @@ -37,7 +37,7 @@ jobs: - uses: ./.github/actions/flutter-test with: - directory: firebase + directory: firebase_remote_config web: false # TODO: don't set coverage for now to finish publishing it @@ -45,12 +45,12 @@ jobs: # if: runner.os == 'Linux' && matrix.sdk == 'stable' # with: # token: ${{ secrets.CODECOV_TOKEN }} -# directory: firebase +# directory: firebase_remote_config # coverage: sentry_firebase_remote_config # min-coverage: 55 analyze: uses: ./.github/workflows/analyze.yml with: - package: firebase + package: firebase_remote_config sdk: flutter diff --git a/firebase/.gitignore b/firebase_remote_config/.gitignore similarity index 100% rename from firebase/.gitignore rename to firebase_remote_config/.gitignore diff --git a/firebase/.metadata b/firebase_remote_config/.metadata similarity index 100% rename from firebase/.metadata rename to firebase_remote_config/.metadata diff --git a/firebase/CHANGELOG.md b/firebase_remote_config/CHANGELOG.md similarity index 100% rename from firebase/CHANGELOG.md rename to firebase_remote_config/CHANGELOG.md diff --git a/firebase/LICENSE b/firebase_remote_config/LICENSE similarity index 100% rename from firebase/LICENSE rename to firebase_remote_config/LICENSE diff --git a/firebase/README.md b/firebase_remote_config/README.md similarity index 98% rename from firebase/README.md rename to firebase_remote_config/README.md index 8e503d8ea6..31fe89e293 100644 --- a/firebase/README.md +++ b/firebase_remote_config/README.md @@ -61,7 +61,6 @@ Future main() async { final sentryFirebaseRemoteConfigIntegration = SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: remoteConfig, - featureFlagKeys: {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); diff --git a/firebase/analysis_options.yaml b/firebase_remote_config/analysis_options.yaml similarity index 100% rename from firebase/analysis_options.yaml rename to firebase_remote_config/analysis_options.yaml diff --git a/firebase/dartdoc_options.yaml b/firebase_remote_config/dartdoc_options.yaml similarity index 100% rename from firebase/dartdoc_options.yaml rename to firebase_remote_config/dartdoc_options.yaml diff --git a/firebase/example/example.dart b/firebase_remote_config/example/example.dart similarity index 95% rename from firebase/example/example.dart rename to firebase_remote_config/example/example.dart index b7d6b71a1d..eba2d4c1d8 100644 --- a/firebase/example/example.dart +++ b/firebase_remote_config/example/example.dart @@ -25,7 +25,6 @@ Future main() async { final sentryFirebaseRemoteConfigIntegration = SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: remoteConfig, - featureFlagKeys: {'firebase_feature_flag_a', 'firebase_feature_flag_b'}, // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. activateOnConfigUpdated: false, ); diff --git a/firebase/lib/sentry_firebase_remote_config.dart b/firebase_remote_config/lib/sentry_firebase_remote_config.dart similarity index 100% rename from firebase/lib/sentry_firebase_remote_config.dart rename to firebase_remote_config/lib/sentry_firebase_remote_config.dart diff --git a/firebase/lib/src/sentry_firebase_remote_config_integration.dart b/firebase_remote_config/lib/src/sentry_firebase_remote_config_integration.dart similarity index 72% rename from firebase/lib/src/sentry_firebase_remote_config_integration.dart rename to firebase_remote_config/lib/src/sentry_firebase_remote_config_integration.dart index 59f853239c..c2df37330e 100644 --- a/firebase/lib/src/sentry_firebase_remote_config_integration.dart +++ b/firebase_remote_config/lib/src/sentry_firebase_remote_config_integration.dart @@ -6,33 +6,23 @@ import 'package:sentry/sentry.dart'; class SentryFirebaseRemoteConfigIntegration extends Integration { SentryFirebaseRemoteConfigIntegration({ required FirebaseRemoteConfig firebaseRemoteConfig, - required Set featureFlagKeys, bool activateOnConfigUpdated = true, }) : _firebaseRemoteConfig = firebaseRemoteConfig, - _featureFlagKeys = featureFlagKeys, _activateOnConfigUpdated = activateOnConfigUpdated; final FirebaseRemoteConfig _firebaseRemoteConfig; - final Set _featureFlagKeys; final bool _activateOnConfigUpdated; StreamSubscription? _subscription; @override FutureOr call(Hub hub, SentryOptions options) async { - if (_featureFlagKeys.isEmpty) { - options.logger( - SentryLevel.warning, - 'No keys provided to SentryFirebaseRemoteConfigIntegration. Will not track feature flags.', - ); - return; - } _subscription = _firebaseRemoteConfig.onConfigUpdated.listen((event) async { if (_activateOnConfigUpdated) { await _firebaseRemoteConfig.activate(); } for (final updatedKey in event.updatedKeys) { - if (_featureFlagKeys.contains(updatedKey)) { - final value = _firebaseRemoteConfig.getBool(updatedKey); + final value = _firebaseRemoteConfig.getBoolOrNull(updatedKey); + if (value != null) { await Sentry.addFeatureFlag(updatedKey, value); } } @@ -46,3 +36,17 @@ class SentryFirebaseRemoteConfigIntegration extends Integration { _subscription = null; } } + +extension _SentryFirebaseRemoteConfig on FirebaseRemoteConfig { + bool? getBoolOrNull(String key) { + final strValue = getString(key); + final lowerCase = strValue.toLowerCase(); + if (lowerCase == 'true' || lowerCase == '1') { + return true; + } + if (lowerCase == 'false' || lowerCase == '0') { + return false; + } + return null; + } +} diff --git a/firebase/pubspec.yaml b/firebase_remote_config/pubspec.yaml similarity index 100% rename from firebase/pubspec.yaml rename to firebase_remote_config/pubspec.yaml diff --git a/firebase/pubspec_overrides.yaml b/firebase_remote_config/pubspec_overrides.yaml similarity index 100% rename from firebase/pubspec_overrides.yaml rename to firebase_remote_config/pubspec_overrides.yaml diff --git a/firebase/test/mocks/mocks.dart b/firebase_remote_config/test/mocks/mocks.dart similarity index 100% rename from firebase/test/mocks/mocks.dart rename to firebase_remote_config/test/mocks/mocks.dart diff --git a/firebase/test/mocks/mocks.mocks.dart b/firebase_remote_config/test/mocks/mocks.mocks.dart similarity index 100% rename from firebase/test/mocks/mocks.mocks.dart rename to firebase_remote_config/test/mocks/mocks.mocks.dart diff --git a/firebase/test/src/sentry_firebase_remote_config_integration_test.dart b/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart similarity index 70% rename from firebase/test/src/sentry_firebase_remote_config_integration_test.dart rename to firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart index bd32465949..d88e182523 100644 --- a/firebase/test/src/sentry_firebase_remote_config_integration_test.dart +++ b/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart @@ -11,11 +11,12 @@ void main() { late Fixture fixture; givenRemoveConfigUpdate() { - final update = RemoteConfigUpdate({'test'}); + final update = RemoteConfigUpdate({'test', 'foo'}); when(fixture.mockFirebaseRemoteConfig.onConfigUpdated) .thenAnswer((_) => Stream.value(update)); - when(fixture.mockFirebaseRemoteConfig.getBool('test')).thenReturn(true); + when(fixture.mockFirebaseRemoteConfig.getString('test')).thenReturn('true'); + when(fixture.mockFirebaseRemoteConfig.getString('foo')).thenReturn('bar'); when(fixture.mockFirebaseRemoteConfig.activate()) .thenAnswer((_) => Future.value(true)); } @@ -40,7 +41,7 @@ void main() { test('adds integration to options', () async { givenRemoveConfigUpdate(); - final sut = await fixture.getSut({'test'}); + final sut = await fixture.getSut(); sut.call(fixture.hub, fixture.options); @@ -51,24 +52,10 @@ void main() { ); }); - test('does not add integration to options if no keys are provided', () async { + test('adds boolean update to feature flags', () async { givenRemoveConfigUpdate(); - final sut = await fixture.getSut({}); - - sut.call(fixture.hub, fixture.options); - - expect( - fixture.options.sdk.integrations - .contains('SentryFirebaseRemoteConfigIntegration'), - isFalse, - ); - }); - - test('adds update to feature flags', () async { - givenRemoveConfigUpdate(); - - final sut = await fixture.getSut({'test'}); + final sut = await fixture.getSut(); sut.call(fixture.hub, fixture.options); await Future.delayed( const Duration( @@ -81,6 +68,7 @@ void main() { as SentryFeatureFlags?; expect(featureFlags, isNotNull); + expect(featureFlags?.values.length, 1); expect(featureFlags?.values.first.name, 'test'); expect(featureFlags?.values.first.value, true); }); @@ -95,36 +83,17 @@ void main() { when(fixture.mockFirebaseRemoteConfig.onConfigUpdated) .thenAnswer((_) => stream); - final sut = await fixture.getSut({'test'}); + final sut = await fixture.getSut(); await sut.call(fixture.hub, fixture.options); await sut.close(); verify(streamSubscription.cancel()).called(1); }); - test('doesn`t add update to feature flags if key is not in the list', - () async { - givenRemoveConfigUpdate(); - - final sut = await fixture.getSut({'test2'}); - sut.call(fixture.hub, fixture.options); - await Future.delayed( - const Duration( - milliseconds: 100, - ), - ); // wait for the subscription to be called - - // ignore: invalid_use_of_internal_member - final featureFlags = fixture.hub.scope.contexts[SentryFeatureFlags.type] - as SentryFeatureFlags?; - - expect(featureFlags, isNull); - }); - test('activate called by default', () async { givenRemoveConfigUpdate(); - final sut = await fixture.getSut({'test'}); + final sut = await fixture.getSut(); sut.call(fixture.hub, fixture.options); await Future.delayed( const Duration( @@ -138,7 +107,7 @@ void main() { test('activate not called if activateOnConfigUpdated is false', () async { givenRemoveConfigUpdate(); - final sut = await fixture.getSut({'test'}, activateOnConfigUpdated: false); + final sut = await fixture.getSut(activateOnConfigUpdated: false); sut.call(fixture.hub, fixture.options); await Future.delayed( const Duration( @@ -152,7 +121,7 @@ void main() { test('activate called if activateOnConfigUpdated is true', () async { givenRemoveConfigUpdate(); - final sut = await fixture.getSut({'test'}, activateOnConfigUpdated: true); + final sut = await fixture.getSut(activateOnConfigUpdated: true); sut.call(fixture.hub, fixture.options); await Future.delayed( const Duration( @@ -171,18 +140,14 @@ class Fixture { final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); Future getSut( - Set featureFlagKeys, { - bool? activateOnConfigUpdated, - }) async { + {bool? activateOnConfigUpdated}) async { if (activateOnConfigUpdated == null) { return SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: mockFirebaseRemoteConfig, - featureFlagKeys: featureFlagKeys, ); } else { return SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: mockFirebaseRemoteConfig, - featureFlagKeys: featureFlagKeys, activateOnConfigUpdated: activateOnConfigUpdated, ); } diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index e642013c17..4b495014ed 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -10,7 +10,7 @@ NEW_VERSION="${2}" echo "Current version: ${OLD_VERSION}" echo "Bumping version: ${NEW_VERSION}" -for pkg in {dart,flutter,logging,dio,file,sqflite,drift,hive,isar,link,firebase}; do +for pkg in {dart,flutter,logging,dio,file,sqflite,drift,hive,isar,link,firebase_remote_config}; do # Bump version in pubspec.yaml perl -pi -e "s/^version: .*/version: $NEW_VERSION/" $pkg/pubspec.yaml # Bump sentry dependency version in pubspec.yaml From e147a005393a7cbe7f9a6cfc23fbbb9a6cfdbe64 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 9 Apr 2025 10:35:58 +0200 Subject: [PATCH 28/29] fix analyze warning --- .../src/sentry_firebase_remote_config_integration_test.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart b/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart index d88e182523..8c8eae56dd 100644 --- a/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart +++ b/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart @@ -139,8 +139,9 @@ class Fixture { final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); - Future getSut( - {bool? activateOnConfigUpdated}) async { + Future getSut({ + bool? activateOnConfigUpdated, + }) async { if (activateOnConfigUpdated == null) { return SentryFirebaseRemoteConfigIntegration( firebaseRemoteConfig: mockFirebaseRemoteConfig, From e980c8dee424395b8e88d58a01d06dba7f4ee3ca Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 14 Apr 2025 15:39:34 +0200 Subject: [PATCH 29/29] add sample in readme --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df68e6612..54cd0b958c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ Sentry.addFeatureFlag('my-feature', true); ``` - Firebase Remote Config Integration ([#2837](https://github.com/getsentry/sentry-dart/pull/2837)) +```dart +// Add the integration to automatically track feature flags from firebase remote config. +await SentryFlutter.init( + (options) { + options.dsn = 'https://example@sentry.io/add-your-dsn-here'; + options.addIntegration( + SentryFirebaseRemoteConfigIntegration( + firebaseRemoteConfig: yourRirebaseRemoteConfig, + ), + ); + }, +); +``` ### Behavioral changes