diff --git a/.craft.yml b/.craft.yml index daf9c4106c..19249c5ef9 100644 --- a/.craft.yml +++ b/.craft.yml @@ -18,6 +18,7 @@ targets: drift: isar: link: + firebase_remote_config: - 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_remote_config: \ No newline at end of file diff --git a/.github/workflows/diagrams.yml b/.github/workflows/diagrams.yml index d909808b72..06e9754282 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_remote_config + working-directory: ./firebase_remote_config + run: lakos . -i "{test/**,example/**}" | dot -Tsvg -o class-diagram.svg + # Source: https://stackoverflow.com/a/58035262 - name: Extract branch name shell: bash diff --git a/.github/workflows/firebase_remote_config.yml b/.github/workflows/firebase_remote_config.yml new file mode 100644 index 0000000000..09ad1f6a93 --- /dev/null +++ b/.github/workflows/firebase_remote_config.yml @@ -0,0 +1,56 @@ +name: sentry-firebase-remote-config +on: + push: + branches: + - main + - release/** + pull_request: + paths: + - '!**/*.md' + - '!**/class-diagram.svg' + - '.github/workflows/firebase_remote_config.yml' + - '.github/workflows/analyze.yml' + - '.github/actions/dart-test/**' + - '.github/actions/coverage/**' + - 'dart/**' + - 'flutter/**' + - 'firebase_remote_config/**' + +# 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 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [macos, ubuntu, windows] + sdk: [stable, beta] + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/flutter-test + with: + directory: firebase_remote_config + 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_remote_config +# coverage: sentry_firebase_remote_config +# min-coverage: 55 + + analyze: + uses: ./.github/workflows/analyze.yml + with: + package: firebase_remote_config + sdk: flutter diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fbaedec44..54cd0b958c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ // Manually track a feature flag 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 diff --git a/firebase_remote_config/.gitignore b/firebase_remote_config/.gitignore new file mode 100644 index 0000000000..ba521d5a39 --- /dev/null +++ b/firebase_remote_config/.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_remote_config/.metadata b/firebase_remote_config/.metadata new file mode 100644 index 0000000000..07eecc71cd --- /dev/null +++ b/firebase_remote_config/.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_remote_config/CHANGELOG.md b/firebase_remote_config/CHANGELOG.md new file mode 120000 index 0000000000..04c99a55ca --- /dev/null +++ b/firebase_remote_config/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/firebase_remote_config/LICENSE b/firebase_remote_config/LICENSE new file mode 100644 index 0000000000..2a6964d84d --- /dev/null +++ b/firebase_remote_config/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_remote_config/README.md b/firebase_remote_config/README.md new file mode 100644 index 0000000000..31fe89e293 --- /dev/null +++ b/firebase_remote_config/README.md @@ -0,0 +1,98 @@ +

+ + + +
+

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

+ + + +
+

+ +Sentry integration for `firebase_remote_config` package +=========== + +| package | build | pub | likes | popularity | pub points | +|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| ------- | +| 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 + +#### 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_remote_config/sentry_firebase_remote_config.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 sentryFirebaseRemoteConfigIntegration = SentryFirebaseRemoteConfigIntegration( + firebaseRemoteConfig: remoteConfig, + // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. + activateOnConfigUpdated: false, + ); + options.addIntegration(sentryFirebaseRemoteConfigIntegration); + }, + ); + + 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_remote_config/analysis_options.yaml b/firebase_remote_config/analysis_options.yaml new file mode 100644 index 0000000000..7119dc352d --- /dev/null +++ b/firebase_remote_config/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_remote_config/dartdoc_options.yaml b/firebase_remote_config/dartdoc_options.yaml new file mode 120000 index 0000000000..7cbb8c0d74 --- /dev/null +++ b/firebase_remote_config/dartdoc_options.yaml @@ -0,0 +1 @@ +../dart/dartdoc_options.yaml \ No newline at end of file diff --git a/firebase_remote_config/example/example.dart b/firebase_remote_config/example/example.dart new file mode 100644 index 0000000000..eba2d4c1d8 --- /dev/null +++ b/firebase_remote_config/example/example.dart @@ -0,0 +1,52 @@ +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_remote_config/sentry_firebase_remote_config.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 sentryFirebaseRemoteConfigIntegration = + SentryFirebaseRemoteConfigIntegration( + firebaseRemoteConfig: remoteConfig, + // Don't call `await remoteConfig.activate();` when firebase config is updated. Per default this is true. + activateOnConfigUpdated: false, + ); + options.addIntegration(sentryFirebaseRemoteConfigIntegration); + }, + ); + + 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_remote_config/lib/sentry_firebase_remote_config.dart b/firebase_remote_config/lib/sentry_firebase_remote_config.dart new file mode 100644 index 0000000000..39222fe090 --- /dev/null +++ b/firebase_remote_config/lib/sentry_firebase_remote_config.dart @@ -0,0 +1,3 @@ +library; + +export 'src/sentry_firebase_remote_config_integration.dart'; diff --git a/firebase_remote_config/lib/src/sentry_firebase_remote_config_integration.dart b/firebase_remote_config/lib/src/sentry_firebase_remote_config_integration.dart new file mode 100644 index 0000000000..c2df37330e --- /dev/null +++ b/firebase_remote_config/lib/src/sentry_firebase_remote_config_integration.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:sentry/sentry.dart'; + +class SentryFirebaseRemoteConfigIntegration extends Integration { + SentryFirebaseRemoteConfigIntegration({ + required FirebaseRemoteConfig firebaseRemoteConfig, + bool activateOnConfigUpdated = true, + }) : _firebaseRemoteConfig = firebaseRemoteConfig, + _activateOnConfigUpdated = activateOnConfigUpdated; + + final FirebaseRemoteConfig _firebaseRemoteConfig; + final bool _activateOnConfigUpdated; + StreamSubscription? _subscription; + + @override + FutureOr call(Hub hub, SentryOptions options) async { + _subscription = _firebaseRemoteConfig.onConfigUpdated.listen((event) async { + if (_activateOnConfigUpdated) { + await _firebaseRemoteConfig.activate(); + } + for (final updatedKey in event.updatedKeys) { + final value = _firebaseRemoteConfig.getBoolOrNull(updatedKey); + if (value != null) { + await Sentry.addFeatureFlag(updatedKey, value); + } + } + }); + options.sdk.addIntegration('SentryFirebaseRemoteConfigIntegration'); + } + + @override + FutureOr close() async { + await _subscription?.cancel(); + _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_remote_config/pubspec.yaml b/firebase_remote_config/pubspec.yaml new file mode 100644 index 0000000000..c6dbf430cf --- /dev/null +++ b/firebase_remote_config/pubspec.yaml @@ -0,0 +1,32 @@ +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/ +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_remote_config/pubspec_overrides.yaml b/firebase_remote_config/pubspec_overrides.yaml new file mode 100644 index 0000000000..16e71d16f0 --- /dev/null +++ b/firebase_remote_config/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + sentry: + path: ../dart diff --git a/firebase_remote_config/test/mocks/mocks.dart b/firebase_remote_config/test/mocks/mocks.dart new file mode 100644 index 0000000000..579fc1638d --- /dev/null +++ b/firebase_remote_config/test/mocks/mocks.dart @@ -0,0 +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_remote_config/test/mocks/mocks.mocks.dart b/firebase_remote_config/test/mocks/mocks.mocks.dart new file mode 100644 index 0000000000..6f9eebffda --- /dev/null +++ b/firebase_remote_config/test/mocks/mocks.mocks.dart @@ -0,0 +1,972 @@ +// 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 _i5; + +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 _i6; + +// 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); +} + +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. +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 + set profilerFactory(_i6.SentryProfilerFactory? value) => super.noSuchMethod( + Invocation.setter(#profilerFactory, value), + returnValueForMissingStub: null, + ); + + @override + _i5.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: _i5.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureEvent, + [event], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + ), + ), + ) as _i5.Future<_i2.SentryId>); + + @override + _i5.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: _i5.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureException, + [throwable], + {#stackTrace: stackTrace, #hint: hint, #withScope: withScope}, + ), + ), + ), + ) as _i5.Future<_i2.SentryId>); + + @override + _i5.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: _i5.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureMessage, + [message], + { + #level: level, + #template: template, + #params: params, + #hint: hint, + #withScope: withScope, + }, + ), + ), + ), + ) as _i5.Future<_i2.SentryId>); + + @override + _i5.Future<_i2.SentryId> captureFeedback( + _i2.SentryFeedback? feedback, { + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureFeedback, + [feedback], + {#hint: hint, #withScope: withScope}, + ), + returnValue: _i5.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureFeedback, + [feedback], + {#hint: hint, #withScope: withScope}, + ), + ), + ), + ) as _i5.Future<_i2.SentryId>); + + @override + _i5.Future addBreadcrumb(_i2.Breadcrumb? crumb, {_i2.Hint? hint}) => + (super.noSuchMethod( + Invocation.method(#addBreadcrumb, [crumb], {#hint: hint}), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.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 + _i5.Future close() => (super.noSuchMethod( + Invocation.method(#close, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.FutureOr configureScope(_i2.ScopeCallback? callback) => + (super.noSuchMethod(Invocation.method(#configureScope, [callback])) + as _i5.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 + _i5.Future<_i2.SentryId> captureTransaction( + _i2.SentryTransaction? transaction, { + _i2.SentryTraceContextHeader? traceContext, + _i2.Hint? hint, + }) => + (super.noSuchMethod( + Invocation.method( + #captureTransaction, + [transaction], + {#traceContext: traceContext, #hint: hint}, + ), + returnValue: _i5.Future<_i2.SentryId>.value( + _FakeSentryId_1( + this, + Invocation.method( + #captureTransaction, + [transaction], + {#traceContext: traceContext, #hint: hint}, + ), + ), + ), + ) as _i5.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 + _i5.Stream<_i4.RemoteConfigUpdate> get onConfigUpdated => (super.noSuchMethod( + Invocation.getter(#onConfigUpdated), + returnValue: _i5.Stream<_i4.RemoteConfigUpdate>.empty(), + ) as _i5.Stream<_i4.RemoteConfigUpdate>); + + @override + Map get pluginConstants => (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + ) as Map); + + @override + _i5.Future activate() => (super.noSuchMethod( + Invocation.method(#activate, []), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future ensureInitialized() => (super.noSuchMethod( + Invocation.method(#ensureInitialized, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future fetch() => (super.noSuchMethod( + Invocation.method(#fetch, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future fetchAndActivate() => (super.noSuchMethod( + Invocation.method(#fetchAndActivate, []), + returnValue: _i5.Future.value(false), + ) as _i5.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 + _i5.Future setConfigSettings( + _i4.RemoteConfigSettings? remoteConfigSettings, + ) => + (super.noSuchMethod( + Invocation.method(#setConfigSettings, [remoteConfigSettings]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setDefaults(Map? defaultParameters) => + (super.noSuchMethod( + Invocation.method(#setDefaults, [defaultParameters]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setCustomSignals(Map? customSignals) => + (super.noSuchMethod( + Invocation.method(#setCustomSignals, [customSignals]), + 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_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart b/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart new file mode 100644 index 0000000000..8c8eae56dd --- /dev/null +++ b/firebase_remote_config/test/src/sentry_firebase_remote_config_integration_test.dart @@ -0,0 +1,156 @@ +import 'package:flutter_test/flutter_test.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'; +import 'package:sentry_firebase_remote_config/sentry_firebase_remote_config.dart'; + +void main() { + late Fixture fixture; + + givenRemoveConfigUpdate() { + final update = RemoteConfigUpdate({'test', 'foo'}); + when(fixture.mockFirebaseRemoteConfig.onConfigUpdated) + .thenAnswer((_) => Stream.value(update)); + + when(fixture.mockFirebaseRemoteConfig.getString('test')).thenReturn('true'); + when(fixture.mockFirebaseRemoteConfig.getString('foo')).thenReturn('bar'); + when(fixture.mockFirebaseRemoteConfig.activate()) + .thenAnswer((_) => Future.value(true)); + } + + setUp(() async { + fixture = Fixture(); + + await Sentry.init((options) { + options.dsn = 'https://example.com/sentry-dsn'; + }); + + // ignore: invalid_use_of_internal_member + fixture.hub = Sentry.currentHub; + // ignore: invalid_use_of_internal_member + fixture.options = fixture.hub.options; + }); + + tearDown(() { + Sentry.close(); + }); + + test('adds integration to options', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations + .contains('SentryFirebaseRemoteConfigIntegration'), + isTrue, + ); + }); + + test('adds boolean update to feature flags', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut(); + 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.length, 1); + expect(featureFlags?.values.first.name, 'test'); + 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(); + await sut.call(fixture.hub, fixture.options); + await sut.close(); + + verify(streamSubscription.cancel()).called(1); + }); + + test('activate called by default', () async { + givenRemoveConfigUpdate(); + + final sut = await fixture.getSut(); + 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(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(activateOnConfigUpdated: true); + sut.call(fixture.hub, fixture.options); + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); + + verify(fixture.mockFirebaseRemoteConfig.activate()).called(1); + }); +} + +class Fixture { + late Hub hub; + late SentryOptions options; + + final mockFirebaseRemoteConfig = MockFirebaseRemoteConfig(); + + Future getSut({ + bool? activateOnConfigUpdated, + }) async { + if (activateOnConfigUpdated == null) { + return SentryFirebaseRemoteConfigIntegration( + firebaseRemoteConfig: mockFirebaseRemoteConfig, + ); + } else { + return SentryFirebaseRemoteConfigIntegration( + firebaseRemoteConfig: mockFirebaseRemoteConfig, + activateOnConfigUpdated: activateOnConfigUpdated, + ); + } + } +} diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 8920aa395f..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}; 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