From 212f89638e34646fb10aad49583fb727f29a19d7 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 11:38:58 +0200 Subject: [PATCH 01/33] HackWeek - feature flags --- dart/lib/sentry.dart | 2 ++ dart/lib/src/feature_flags/evaluation.dart | 10 ++++++++++ dart/lib/src/feature_flags/feature_flag.dart | 16 ++++++++++++++++ dart/lib/src/transport/http_transport.dart | 8 ++++++++ dart/lib/src/transport/noop_transport.dart | 4 ++++ dart/lib/src/transport/transport.dart | 3 +++ dart/test/mocks/mock_transport.dart | 6 ++++++ dio/test/mocks/mock_transport.dart | 6 ++++++ flutter/lib/src/file_system_transport.dart | 4 ++++ 9 files changed, 59 insertions(+) create mode 100644 dart/lib/src/feature_flags/evaluation.dart create mode 100644 dart/lib/src/feature_flags/feature_flag.dart diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index bb7db1cc95..5a09125109 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -30,3 +30,5 @@ export 'src/sentry_user_feedback.dart'; // tracing export 'src/tracing.dart'; export 'src/sentry_measurement.dart'; +// feature flag +export 'src/feature_flags/feature_flag.dart'; diff --git a/dart/lib/src/feature_flags/evaluation.dart b/dart/lib/src/feature_flags/evaluation.dart new file mode 100644 index 0000000000..e02cafbed4 --- /dev/null +++ b/dart/lib/src/feature_flags/evaluation.dart @@ -0,0 +1,10 @@ +class Evaluation { + final String type; + final double? percentage; + final bool result; + final Map _tags; + + Map get tags => Map.unmodifiable(_tags); + + Evaluation(this.type, this.percentage, this.result, this._tags); +} diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart new file mode 100644 index 0000000000..df20952f26 --- /dev/null +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -0,0 +1,16 @@ +import 'package:meta/meta.dart'; + +import 'evaluation.dart'; + +@immutable +class FeatureFlag { + final String name; + final Map _tags; + final List _evaluations; + + Map get tags => Map.unmodifiable(_tags); + + List get evaluations => List.unmodifiable(_evaluations); + + FeatureFlag(this.name, this._tags, this._evaluations); +} diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 317070695a..8b2d93b846 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart'; import '../client_reports/client_report_recorder.dart'; import '../client_reports/discard_reason.dart'; +import '../feature_flags/feature_flag.dart'; import 'data_category.dart'; import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; import '../noop_client.dart'; @@ -95,6 +96,13 @@ class HttpTransport implements Transport { return SentryId.fromId(eventId); } + @override + Future?> fetchFeatureFlags() async { + + + return null; + } + Future _createStreamedRequest( SentryEnvelope envelope) async { final streamedRequest = StreamedRequest('POST', _dsn.postUri); diff --git a/dart/lib/src/transport/noop_transport.dart b/dart/lib/src/transport/noop_transport.dart index f4ae138e99..d681e56182 100644 --- a/dart/lib/src/transport/noop_transport.dart +++ b/dart/lib/src/transport/noop_transport.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import '../feature_flags/feature_flag.dart'; import '../sentry_envelope.dart'; import '../protocol.dart'; @@ -8,4 +9,7 @@ import 'transport.dart'; class NoOpTransport implements Transport { @override Future send(SentryEnvelope envelope) async => null; + + @override + Future?> fetchFeatureFlags() async => null; } diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index f0a6a2c996..56ecbe90da 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import '../feature_flags/feature_flag.dart'; import '../sentry_envelope.dart'; import '../protocol.dart'; @@ -7,4 +8,6 @@ import '../protocol.dart'; /// or caching in the disk. abstract class Transport { Future send(SentryEnvelope envelope); + + Future?> fetchFeatureFlags(); } diff --git a/dart/test/mocks/mock_transport.dart b/dart/test/mocks/mock_transport.dart index 693debe1e9..4de989cbb2 100644 --- a/dart/test/mocks/mock_transport.dart +++ b/dart/test/mocks/mock_transport.dart @@ -22,6 +22,9 @@ class MockTransport implements Transport { return envelope.header.eventId ?? SentryId.empty(); } + @override + Future?> fetchFeatureFlags() async => null; + Future _eventFromEnvelope(SentryEnvelope envelope) async { final envelopeItemData = []; envelopeItemData.addAll(await envelope.items.first.envelopeItemStream()); @@ -65,4 +68,7 @@ class ThrowingTransport implements Transport { Future send(SentryEnvelope envelope) async { throw Exception('foo bar'); } + + @override + Future?> fetchFeatureFlags() async => null; } diff --git a/dio/test/mocks/mock_transport.dart b/dio/test/mocks/mock_transport.dart index 5b9b13de03..8de005c28a 100644 --- a/dio/test/mocks/mock_transport.dart +++ b/dio/test/mocks/mock_transport.dart @@ -23,6 +23,9 @@ class MockTransport with NoSuchMethodProvider implements Transport { return envelope.header.eventId ?? SentryId.empty(); } + @override + Future?> fetchFeatureFlags() async => null; + Future _eventFromEnvelope(SentryEnvelope envelope) async { final envelopeItemData = []; envelopeItemData.addAll(await envelope.items.first.envelopeItemStream()); @@ -66,4 +69,7 @@ class ThrowingTransport implements Transport { Future send(SentryEnvelope envelope) async { throw Exception('foo bar'); } + + @override + Future?> fetchFeatureFlags() async => null; } diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index 9b1e580203..aae79e26f4 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -29,4 +29,8 @@ class FileSystemTransport implements Transport { return envelope.header.eventId; } + + // TODO: implement or fallback to http transport + @override + Future?> fetchFeatureFlags() async => null; } From 4f336aa43685951a64ea0444a73c996499446b2d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 11:42:24 +0200 Subject: [PATCH 02/33] add import --- dart/lib/sentry.dart | 2 +- dart/lib/src/feature_flags/evaluation.dart | 3 +++ dart/lib/src/transport/http_transport.dart | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 5a09125109..fe63b9a88c 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -30,5 +30,5 @@ export 'src/sentry_user_feedback.dart'; // tracing export 'src/tracing.dart'; export 'src/sentry_measurement.dart'; -// feature flag +// feature flags export 'src/feature_flags/feature_flag.dart'; diff --git a/dart/lib/src/feature_flags/evaluation.dart b/dart/lib/src/feature_flags/evaluation.dart index e02cafbed4..4fd9764ef9 100644 --- a/dart/lib/src/feature_flags/evaluation.dart +++ b/dart/lib/src/feature_flags/evaluation.dart @@ -1,3 +1,6 @@ +import 'package:meta/meta.dart'; + +@immutable class Evaluation { final String type; final double? percentage; diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 8b2d93b846..ef52931385 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -96,6 +96,7 @@ class HttpTransport implements Transport { return SentryId.fromId(eventId); } + // TODO: implement @override Future?> fetchFeatureFlags() async { From d75831fddc8018b1f42fb28fe4e694dd0e997d72 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 14:02:20 +0200 Subject: [PATCH 03/33] parsing of the spec --- dart/lib/src/feature_flags/evaluation.dart | 11 +++- dart/lib/src/feature_flags/feature_flag.dart | 13 ++++ dart/lib/src/protocol/dsn.dart | 65 ++++++++++++++------ dart/lib/src/transport/http_transport.dart | 35 ++++++++++- dart/test/transport/http_transport_test.dart | 26 ++++++++ dart/test_resources/feature_flags.json | 48 +++++++++++++++ 6 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 dart/test_resources/feature_flags.json diff --git a/dart/lib/src/feature_flags/evaluation.dart b/dart/lib/src/feature_flags/evaluation.dart index 4fd9764ef9..3582c6aa17 100644 --- a/dart/lib/src/feature_flags/evaluation.dart +++ b/dart/lib/src/feature_flags/evaluation.dart @@ -4,10 +4,19 @@ import 'package:meta/meta.dart'; class Evaluation { final String type; final double? percentage; - final bool result; + final bool? result; final Map _tags; Map get tags => Map.unmodifiable(_tags); Evaluation(this.type, this.percentage, this.result, this._tags); + + factory Evaluation.fromJson(Map json) { + return Evaluation( + json['type'] as String, + json['percentage'] as double?, + json['result'] as bool?, + Map.from(json['tags'] as Map), + ); + } } diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index df20952f26..0587b45859 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -13,4 +13,17 @@ class FeatureFlag { List get evaluations => List.unmodifiable(_evaluations); FeatureFlag(this.name, this._tags, this._evaluations); + + factory FeatureFlag.fromJson(Map json) { + final evaluationsList = json['evaluation'] as List? ?? []; + final evaluations = evaluationsList + .map((e) => Evaluation.fromJson(e)) + .toList(growable: false); + + return FeatureFlag( + json['name'] as String, + Map.from(json['tags'] as Map), + evaluations, + ); + } } diff --git a/dart/lib/src/protocol/dsn.dart b/dart/lib/src/protocol/dsn.dart index c3ec5093c8..3488003d46 100644 --- a/dart/lib/src/protocol/dsn.dart +++ b/dart/lib/src/protocol/dsn.dart @@ -24,28 +24,16 @@ class Dsn { /// The DSN URI. final Uri? uri; - Uri get postUri { - final uriCopy = uri!; - final port = uriCopy.hasPort && - ((uriCopy.scheme == 'http' && uriCopy.port != 80) || - (uriCopy.scheme == 'https' && uriCopy.port != 443)) - ? ':${uriCopy.port}' - : ''; + @Deprecated('Use [envelopeUri] instead') + Uri get postUri => envelopeUri; - final pathLength = uriCopy.pathSegments.length; + Uri get envelopeUri => _UriData.fromUri(uri!, projectId).envelopeUri; - String apiPath; - if (pathLength > 1) { - // some paths would present before the projectID in the uri - apiPath = - (uriCopy.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); - } else { - apiPath = 'api'; - } - return Uri.parse( - '${uriCopy.scheme}://${uriCopy.host}$port/$apiPath/$projectId/envelope/', - ); - } + Uri get featureFlagsUri => _UriData.fromUri(uri!, projectId).featureFlagsUri; + + // Uri get featureFlagsUri { + // return postUri.replace() + // } /// Parses a DSN String to a Dsn object factory Dsn.parse(String dsn) { @@ -66,3 +54,40 @@ class Dsn { ); } } + +class _UriData { + final String scheme; + final String host; + final String port; + final String apiPath; + final String projectId; + + _UriData(this.scheme, this.host, this.port, this.apiPath, this.projectId); + + factory _UriData.fromUri(Uri uri, String projectId) { + final port = uri.hasPort && + ((uri.scheme == 'http' && uri.port != 80) || + (uri.scheme == 'https' && uri.port != 443)) + ? ':${uri.port}' + : ''; + + final pathLength = uri.pathSegments.length; + + String apiPath; + if (pathLength > 1) { + // some paths would present before the projectID in the uri + apiPath = + (uri.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); + } else { + apiPath = 'api'; + } + + return _UriData(uri.scheme, uri.host, port, apiPath, projectId); + } + + Uri get envelopeUri => + Uri.parse('$scheme://$host}$port/$apiPath/$projectId/envelope/'); + + Uri get featureFlagsUri => + Uri.parse('$scheme://$host}$port/$apiPath/$projectId/feature_flags/'); +} diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index ef52931385..53208f7c12 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -99,14 +99,43 @@ class HttpTransport implements Transport { // TODO: implement @override Future?> fetchFeatureFlags() async { - + final response = + await _options.httpClient.get(_dsn.featureFlagsUri, headers: _headers); - return null; + if (response.statusCode != 200) { + // body guard to not log the error as it has performance impact to allocate + // the body String. + if (_options.debug) { + _options.logger( + SentryLevel.error, + 'API returned an error, statusCode = ${response.statusCode}, ' + 'body = ${response.body}', + ); + } + return null; + } + + final responseJson = json.decode(response.body); + final featureFlagsJson = responseJson['feature_flags'] as Map?; + + // for(final keys in featureFlagsJson) + if (featureFlagsJson == null || featureFlagsJson.entries.isEmpty) { + return null; + } + + List featureFlags = []; + for (final value in featureFlagsJson.entries) { + Map json = {'name': value.key, ...value.value}; + + final flag = FeatureFlag.fromJson(json); + featureFlags.add(flag); + } + return featureFlags; } Future _createStreamedRequest( SentryEnvelope envelope) async { - final streamedRequest = StreamedRequest('POST', _dsn.postUri); + final streamedRequest = StreamedRequest('POST', _dsn.envelopeUri); if (_options.compressPayload) { final compressionSink = compressInSink(streamedRequest.sink, _headers); diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 14a2fe19e7..8c5b227bbd 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -202,6 +203,31 @@ void main() { expect(fixture.clientReportRecorder.category, DataCategory.error); }); }); + + group('feature flags', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('parses the feature flag list', () async { + final featureFlagsFile = File('test_resources/feature_flags.json'); + final featureFlagsJson = await featureFlagsFile.readAsString(); + + final httpMock = MockClient((http.Request request) async { + return http.Response(featureFlagsJson, 200, headers: {}); + }); + final mockRateLimiter = MockRateLimiter(); + final sut = fixture.getSut(httpMock, mockRateLimiter); + + final flags = await sut.fetchFeatureFlags(); + + for (final flag in flags!) { + print(flag.name); + } + }); + }); } class Fixture { diff --git a/dart/test_resources/feature_flags.json b/dart/test_resources/feature_flags.json new file mode 100644 index 0000000000..330d4e4a5c --- /dev/null +++ b/dart/test_resources/feature_flags.json @@ -0,0 +1,48 @@ +{ + "feature_flags": { + "accessToProfiling": { + "tags": { + "isEarlyAdopter": "true" + }, + "evaluation": [ + { + "type": "rollout", + "percentage": 0.5, + "result": true, + "tags": { + "userSegment": "slow" + } + }, + { + "type": "match", + "result": true, + "tags": { + "isSentryDev": "true" + } + } + ] + }, + "profilingEnabled": { + "tags": { + "isEarlyAdopter": "true" + }, + "evaluation": [ + { + "type": "rollout", + "percentage": 0.05, + "result": true, + "tags": { + "isSentryDev": "true" + } + }, + { + "type": "match", + "result": true, + "tags": { + "isSentryDev": "true" + } + } + ] + } + } +} \ No newline at end of file From 14d7c9cd57b28b994e70129b5a957d2b1e290c93 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 14:26:22 +0200 Subject: [PATCH 04/33] add type enum and rename evaluation to evaluation rule --- .../{evaluation.dart => evaluation_rule.dart} | 13 +++++++------ .../lib/src/feature_flags/evaluation_type.dart | 18 ++++++++++++++++++ dart/lib/src/feature_flags/feature_flag.dart | 8 ++++---- 3 files changed, 29 insertions(+), 10 deletions(-) rename dart/lib/src/feature_flags/{evaluation.dart => evaluation_rule.dart} (52%) create mode 100644 dart/lib/src/feature_flags/evaluation_type.dart diff --git a/dart/lib/src/feature_flags/evaluation.dart b/dart/lib/src/feature_flags/evaluation_rule.dart similarity index 52% rename from dart/lib/src/feature_flags/evaluation.dart rename to dart/lib/src/feature_flags/evaluation_rule.dart index 3582c6aa17..85bdce53e7 100644 --- a/dart/lib/src/feature_flags/evaluation.dart +++ b/dart/lib/src/feature_flags/evaluation_rule.dart @@ -1,19 +1,20 @@ import 'package:meta/meta.dart'; +import 'evaluation_type.dart'; @immutable -class Evaluation { - final String type; +class EvaluationRule { + final EvaluationType type; final double? percentage; final bool? result; final Map _tags; Map get tags => Map.unmodifiable(_tags); - Evaluation(this.type, this.percentage, this.result, this._tags); + EvaluationRule(this.type, this.percentage, this.result, this._tags); - factory Evaluation.fromJson(Map json) { - return Evaluation( - json['type'] as String, + factory EvaluationRule.fromJson(Map json) { + return EvaluationRule( + (json['type'] as String).toEvaluationType(), json['percentage'] as double?, json['result'] as bool?, Map.from(json['tags'] as Map), diff --git a/dart/lib/src/feature_flags/evaluation_type.dart b/dart/lib/src/feature_flags/evaluation_type.dart new file mode 100644 index 0000000000..b636fc1a48 --- /dev/null +++ b/dart/lib/src/feature_flags/evaluation_type.dart @@ -0,0 +1,18 @@ +enum EvaluationType { + match, + rollout, + none, +} + +extension EvaluationTypeEx on String { + EvaluationType toEvaluationType() { + switch (this) { + case 'match': + return EvaluationType.match; + case 'rollout': + return EvaluationType.rollout; + default: + return EvaluationType.none; + } + } +} diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index 0587b45859..a15865dce4 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -1,23 +1,23 @@ import 'package:meta/meta.dart'; -import 'evaluation.dart'; +import 'evaluation_rule.dart'; @immutable class FeatureFlag { final String name; final Map _tags; - final List _evaluations; + final List _evaluations; Map get tags => Map.unmodifiable(_tags); - List get evaluations => List.unmodifiable(_evaluations); + List get evaluations => List.unmodifiable(_evaluations); FeatureFlag(this.name, this._tags, this._evaluations); factory FeatureFlag.fromJson(Map json) { final evaluationsList = json['evaluation'] as List? ?? []; final evaluations = evaluationsList - .map((e) => Evaluation.fromJson(e)) + .map((e) => EvaluationRule.fromJson(e)) .toList(growable: false); return FeatureFlag( From de64e1d2175504e32ddf18b4e610d2171419b51b Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 14:44:50 +0200 Subject: [PATCH 05/33] add feature flags dump and transform it to a map --- dart/lib/src/feature_flags/feature_dump.dart | 24 ++++++++++++++++++++ dart/lib/src/feature_flags/feature_flag.dart | 4 +--- dart/lib/src/transport/http_transport.dart | 19 +++------------- dart/lib/src/transport/noop_transport.dart | 2 +- dart/lib/src/transport/transport.dart | 2 +- dart/test/mocks/mock_transport.dart | 4 ++-- dart/test/transport/http_transport_test.dart | 4 ++-- dio/test/mocks/mock_transport.dart | 4 ++-- flutter/lib/src/file_system_transport.dart | 2 +- 9 files changed, 37 insertions(+), 28 deletions(-) create mode 100644 dart/lib/src/feature_flags/feature_dump.dart diff --git a/dart/lib/src/feature_flags/feature_dump.dart b/dart/lib/src/feature_flags/feature_dump.dart new file mode 100644 index 0000000000..792b750811 --- /dev/null +++ b/dart/lib/src/feature_flags/feature_dump.dart @@ -0,0 +1,24 @@ +import 'package:meta/meta.dart'; + +import 'feature_flag.dart'; + +@immutable +class FeatureDump { + final Map featureFlags; + + FeatureDump(this.featureFlags); + + factory FeatureDump.fromJson(Map json) { + final featureFlagsJson = json['feature_flags'] as Map?; + Map featureFlags = {}; + + if (featureFlagsJson != null) { + for (final value in featureFlagsJson.entries) { + final featureFlag = FeatureFlag.fromJson(value.value); + featureFlags[value.key] = featureFlag; + } + } + + return FeatureDump(featureFlags); + } +} diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index a15865dce4..7d3fa11ffa 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -4,7 +4,6 @@ import 'evaluation_rule.dart'; @immutable class FeatureFlag { - final String name; final Map _tags; final List _evaluations; @@ -12,7 +11,7 @@ class FeatureFlag { List get evaluations => List.unmodifiable(_evaluations); - FeatureFlag(this.name, this._tags, this._evaluations); + FeatureFlag(this._tags, this._evaluations); factory FeatureFlag.fromJson(Map json) { final evaluationsList = json['evaluation'] as List? ?? []; @@ -21,7 +20,6 @@ class FeatureFlag { .toList(growable: false); return FeatureFlag( - json['name'] as String, Map.from(json['tags'] as Map), evaluations, ); diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 53208f7c12..93629930fd 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart'; import '../client_reports/client_report_recorder.dart'; import '../client_reports/discard_reason.dart'; +import '../feature_flags/feature_dump.dart'; import '../feature_flags/feature_flag.dart'; import 'data_category.dart'; import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; @@ -98,7 +99,7 @@ class HttpTransport implements Transport { // TODO: implement @override - Future?> fetchFeatureFlags() async { + Future?> fetchFeatureFlags() async { final response = await _options.httpClient.get(_dsn.featureFlagsUri, headers: _headers); @@ -116,21 +117,7 @@ class HttpTransport implements Transport { } final responseJson = json.decode(response.body); - final featureFlagsJson = responseJson['feature_flags'] as Map?; - - // for(final keys in featureFlagsJson) - if (featureFlagsJson == null || featureFlagsJson.entries.isEmpty) { - return null; - } - - List featureFlags = []; - for (final value in featureFlagsJson.entries) { - Map json = {'name': value.key, ...value.value}; - - final flag = FeatureFlag.fromJson(json); - featureFlags.add(flag); - } - return featureFlags; + return FeatureDump.fromJson(responseJson).featureFlags; } Future _createStreamedRequest( diff --git a/dart/lib/src/transport/noop_transport.dart b/dart/lib/src/transport/noop_transport.dart index d681e56182..0837f73bf0 100644 --- a/dart/lib/src/transport/noop_transport.dart +++ b/dart/lib/src/transport/noop_transport.dart @@ -11,5 +11,5 @@ class NoOpTransport implements Transport { Future send(SentryEnvelope envelope) async => null; @override - Future?> fetchFeatureFlags() async => null; + Future?> fetchFeatureFlags() async => null; } diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index 56ecbe90da..f0533acd31 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -9,5 +9,5 @@ import '../protocol.dart'; abstract class Transport { Future send(SentryEnvelope envelope); - Future?> fetchFeatureFlags(); + Future?> fetchFeatureFlags(); } diff --git a/dart/test/mocks/mock_transport.dart b/dart/test/mocks/mock_transport.dart index 4de989cbb2..7a451780ce 100644 --- a/dart/test/mocks/mock_transport.dart +++ b/dart/test/mocks/mock_transport.dart @@ -23,7 +23,7 @@ class MockTransport implements Transport { } @override - Future?> fetchFeatureFlags() async => null; + Future?> fetchFeatureFlags() async => null; Future _eventFromEnvelope(SentryEnvelope envelope) async { final envelopeItemData = []; @@ -70,5 +70,5 @@ class ThrowingTransport implements Transport { } @override - Future?> fetchFeatureFlags() async => null; + Future?> fetchFeatureFlags() async => null; } diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 8c5b227bbd..cfadc482df 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -223,8 +223,8 @@ void main() { final flags = await sut.fetchFeatureFlags(); - for (final flag in flags!) { - print(flag.name); + for (final flag in flags!.entries) { + print(flag.key); } }); }); diff --git a/dio/test/mocks/mock_transport.dart b/dio/test/mocks/mock_transport.dart index 8de005c28a..9efe28902c 100644 --- a/dio/test/mocks/mock_transport.dart +++ b/dio/test/mocks/mock_transport.dart @@ -24,7 +24,7 @@ class MockTransport with NoSuchMethodProvider implements Transport { } @override - Future?> fetchFeatureFlags() async => null; + Future?> fetchFeatureFlags() async => null; Future _eventFromEnvelope(SentryEnvelope envelope) async { final envelopeItemData = []; @@ -71,5 +71,5 @@ class ThrowingTransport implements Transport { } @override - Future?> fetchFeatureFlags() async => null; + Future?> fetchFeatureFlags() async => null; } diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index aae79e26f4..4132397789 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -32,5 +32,5 @@ class FileSystemTransport implements Transport { // TODO: implement or fallback to http transport @override - Future?> fetchFeatureFlags() async => null; + Future?> fetchFeatureFlags() async => null; } From fb53a0576ee6ff92dc9b86a4f5875afe5f4567cd Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 15:05:24 +0200 Subject: [PATCH 06/33] expose fetch feature flags api --- dart/lib/sentry.dart | 2 ++ dart/lib/src/hub.dart | 23 ++++++++++++++++++++++ dart/lib/src/hub_adapter.dart | 5 +++++ dart/lib/src/noop_hub.dart | 4 ++++ dart/lib/src/noop_sentry_client.dart | 4 ++++ dart/lib/src/protocol/dsn.dart | 2 +- dart/lib/src/sentry.dart | 4 ++++ dart/lib/src/sentry_client.dart | 4 ++++ dart/lib/src/transport/http_transport.dart | 1 - dart/test/test_utils.dart | 10 +++++----- 10 files changed, 52 insertions(+), 7 deletions(-) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index fe63b9a88c..9b12e2c090 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -32,3 +32,5 @@ export 'src/tracing.dart'; export 'src/sentry_measurement.dart'; // feature flags export 'src/feature_flags/feature_flag.dart'; +export 'src/feature_flags/evaluation_rule.dart'; +export 'src/feature_flags/evaluation_type.dart'; diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index a5b9ce6ce9..c4001829da 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -518,6 +518,29 @@ class Hub { } return event; } + + Future?> fetchFeatureFlags() async { + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'fetchFeatureFlags' call is a no-op.", + ); + return null; + } + try { + final item = _peek(); + + await item.client.fetchFeatureFlags(); + } catch (exception, stacktrace) { + _options.logger( + SentryLevel.error, + 'Error while fetching feature flags', + exception: exception, + stackTrace: stacktrace, + ); + } + return null; + } } class _StackItem { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index fb0331b6c5..c8bda740a7 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'feature_flags/feature_flag.dart'; import 'hub.dart'; import 'protocol.dart'; import 'sentry.dart'; @@ -158,4 +159,8 @@ class HubAdapter implements Hub { String transaction, ) => Sentry.currentHub.setSpanContext(throwable, span, transaction); + + @override + Future?> fetchFeatureFlags() async => + Sentry.fetchFeatureFlags(); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 16e8ab5e15..4360052c68 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'feature_flags/feature_flag.dart'; import 'hub.dart'; import 'protocol.dart'; import 'sentry_client.dart'; @@ -114,4 +115,7 @@ class NoOpHub implements Hub { @override void setSpanContext(throwable, ISentrySpan span, String transaction) {} + + @override + Future?> fetchFeatureFlags() async => null; } diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 2a45dfbb6d..537dad1105 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'feature_flags/feature_flag.dart'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_client.dart'; @@ -60,4 +61,7 @@ class NoOpSentryClient implements SentryClient { Scope? scope, }) async => SentryId.empty(); + + @override + Future?> fetchFeatureFlags() async => null; } diff --git a/dart/lib/src/protocol/dsn.dart b/dart/lib/src/protocol/dsn.dart index 3488003d46..5e964a0fbc 100644 --- a/dart/lib/src/protocol/dsn.dart +++ b/dart/lib/src/protocol/dsn.dart @@ -89,5 +89,5 @@ class _UriData { Uri.parse('$scheme://$host}$port/$apiPath/$projectId/envelope/'); Uri get featureFlagsUri => - Uri.parse('$scheme://$host}$port/$apiPath/$projectId/feature_flags/'); + Uri.parse('$scheme://$host}$port/$apiPath/$projectId/feature-flags/'); } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 9604f330f8..444dba81e9 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -6,6 +6,7 @@ import 'default_integrations.dart'; import 'enricher/enricher_event_processor.dart'; import 'environment/environment_variables.dart'; import 'event_processor/deduplication_event_processor.dart'; +import 'feature_flags/feature_flag.dart'; import 'hub.dart'; import 'hub_adapter.dart'; import 'integration.dart'; @@ -272,4 +273,7 @@ class Sentry { @internal static Hub get currentHub => _hub; + + static Future?> fetchFeatureFlags() => + _hub.fetchFeatureFlags(); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 57535d1f92..0fd8d9cbd4 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'event_processor.dart'; +import 'feature_flags/feature_flag.dart'; import 'sentry_user_feedback.dart'; import 'transport/rate_limiter.dart'; import 'protocol.dart'; @@ -396,4 +397,7 @@ class SentryClient { envelope.addClientReport(clientReport); return _options.transport.send(envelope); } + + Future?> fetchFeatureFlags() => + _options.transport.fetchFeatureFlags(); } diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 93629930fd..7e88ff64cc 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -97,7 +97,6 @@ class HttpTransport implements Transport { return SentryId.fromId(eventId); } - // TODO: implement @override Future?> fetchFeatureFlags() async { final response = diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index c364e323d8..4b02b96d1d 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -91,7 +91,7 @@ Future testCaptureException( } final dsn = Dsn.parse(options.dsn!); - expect(postUri, dsn.postUri); + expect(postUri, dsn.envelopeUri); testHeaders( headers, @@ -193,7 +193,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(testDsn)); expect( - dsn.postUri, + dsn.envelopeUri, Uri.parse('https://sentry.example.com/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); @@ -210,7 +210,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(_testDsnWithoutSecret)); expect( - dsn.postUri, + dsn.envelopeUri, Uri.parse('https://sentry.example.com/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); @@ -227,7 +227,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(_testDsnWithPath)); expect( - dsn.postUri, + dsn.envelopeUri, Uri.parse('https://sentry.example.com/path/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); @@ -243,7 +243,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(_testDsnWithPort)); expect( - dsn.postUri, + dsn.envelopeUri, Uri.parse('https://sentry.example.com:8888/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); From 5a9f9bea61a42a11ea2ba7a6c9c15214048e1900 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 15:34:03 +0200 Subject: [PATCH 07/33] add test for parsing feature flag response --- dart/lib/src/hub.dart | 3 +- dart/test/transport/http_transport_test.dart | 36 ++++++++++++++++++-- dart/test_resources/feature_flags.json | 8 ++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index c4001829da..51fdb5d1ae 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -527,10 +527,11 @@ class Hub { ); return null; } + try { final item = _peek(); - await item.client.fetchFeatureFlags(); + return item.client.fetchFeatureFlags(); } catch (exception, stacktrace) { _options.logger( SentryLevel.error, diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index cfadc482df..e5d92c5947 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -223,9 +223,39 @@ void main() { final flags = await sut.fetchFeatureFlags(); - for (final flag in flags!.entries) { - print(flag.key); - } + // accessToProfiling + final accessToProfiling = flags!['accessToProfiling']!; + + expect(accessToProfiling.tags['isEarlyAdopter'], 'true'); + + final rollout = accessToProfiling.evaluations.first; + expect(rollout.percentage, 0.5); + expect(rollout.result, true); + expect(rollout.tags['userSegment'], 'slow'); + expect(rollout.type, EvaluationType.rollout); + + final match = accessToProfiling.evaluations.last; + expect(match.percentage, isNull); + expect(match.result, true); + expect(match.tags['isSentryDev'], 'true'); + expect(match.type, EvaluationType.match); + + // profilingEnabled + final profilingEnabled = flags['profilingEnabled']!; + + expect(profilingEnabled.tags.isEmpty, true); + + final rolloutProfiling = profilingEnabled.evaluations.first; + expect(rolloutProfiling.percentage, 0.05); + expect(rolloutProfiling.result, true); + expect(rolloutProfiling.tags['isSentryDev'], 'true'); + expect(rolloutProfiling.type, EvaluationType.rollout); + + final matchProfiling = profilingEnabled.evaluations.last; + expect(matchProfiling.percentage, isNull); + expect(matchProfiling.result, true); + expect(matchProfiling.tags.isEmpty, true); + expect(matchProfiling.type, EvaluationType.match); }); }); } diff --git a/dart/test_resources/feature_flags.json b/dart/test_resources/feature_flags.json index 330d4e4a5c..1a57d2a126 100644 --- a/dart/test_resources/feature_flags.json +++ b/dart/test_resources/feature_flags.json @@ -23,9 +23,7 @@ ] }, "profilingEnabled": { - "tags": { - "isEarlyAdopter": "true" - }, + "tags": {}, "evaluation": [ { "type": "rollout", @@ -38,9 +36,7 @@ { "type": "match", "result": true, - "tags": { - "isSentryDev": "true" - } + "tags": {} } ] } From 2cef8f6d48f16f3d45020b3c8f51128a762f0211 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 16:30:46 +0200 Subject: [PATCH 08/33] fix dsn parsing and change request to post --- dart/example/bin/example.dart | 133 +++++++++++---------- dart/lib/src/protocol/dsn.dart | 69 +++++------ dart/lib/src/transport/http_transport.dart | 4 +- 3 files changed, 100 insertions(+), 106 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 159de7ad63..2ad27ef6d1 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'package:sentry/sentry.dart'; @@ -11,8 +12,10 @@ import 'event_example.dart'; /// Sends a test exception report to Sentry.io using this Dart client. Future main() async { // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io + // const dsn = + // 'https://9934c532bf8446ef961450973c898537@o447951.ingest.sentry.io/5428562'; const dsn = - 'https://9934c532bf8446ef961450973c898537@o447951.ingest.sentry.io/5428562'; + 'https://60d3409215134fd1a60765f2400b6b38@ac75-72-74-53-151.ngrok.io/1'; await Sentry.init( (options) => options @@ -22,71 +25,77 @@ Future main() async { ..addEventProcessor(TagEventProcessor()), appRunner: runApp, ); + + final featureFlags = await Sentry.fetchFeatureFlags(); + for (final flag in featureFlags!.entries) { + print(flag.key); + } } Future runApp() async { print('\nReporting a complete event example: '); - - Sentry.addBreadcrumb( - Breadcrumb( - message: 'Authenticated user', - category: 'auth', - type: 'debug', - data: { - 'admin': true, - 'permissions': [1, 2, 3] - }, - ), - ); - - await Sentry.configureScope((scope) async { - await scope.setUser(SentryUser( - id: '800', - username: 'first-user', - email: 'first@user.lan', - // ipAddress: '127.0.0.1', sendDefaultPii feature is enabled - extras: {'first-sign-in': '2020-01-01'}, - )); - scope - // ..fingerprint = ['example-dart'], fingerprint forces events to group together - ..transaction = '/example/app' - ..level = SentryLevel.warning; - await scope.setTag('build', '579'); - await scope.setExtra('company-name', 'Dart Inc'); - }); - - // Sends a full Sentry event payload to show the different parts of the UI. - final sentryId = await Sentry.captureEvent(event); - - print('Capture event result : SentryId : $sentryId'); - - print('\nCapture message: '); - - // Sends a full Sentry event payload to show the different parts of the UI. - final messageSentryId = await Sentry.captureMessage( - 'Message 1', - level: SentryLevel.warning, - template: 'Message %s', - params: ['1'], - ); - - print('Capture message result : SentryId : $messageSentryId'); - - try { - await loadConfig(); - } catch (error, stackTrace) { - print('\nReporting the following stack trace: '); - print(stackTrace); - final sentryId = await Sentry.captureException( - error, - stackTrace: stackTrace, - ); - - print('Capture exception result : SentryId : $sentryId'); - } - - // capture unhandled error - await loadConfig(); + // await Sentry.captureMessage('test'); + + // Sentry.addBreadcrumb( + // Breadcrumb( + // message: 'Authenticated user', + // category: 'auth', + // type: 'debug', + // data: { + // 'admin': true, + // 'permissions': [1, 2, 3] + // }, + // ), + // ); + + // await Sentry.configureScope((scope) async { + // await scope.setUser(SentryUser( + // id: '800', + // username: 'first-user', + // email: 'first@user.lan', + // // ipAddress: '127.0.0.1', sendDefaultPii feature is enabled + // extras: {'first-sign-in': '2020-01-01'}, + // )); + // scope + // // ..fingerprint = ['example-dart'], fingerprint forces events to group together + // ..transaction = '/example/app' + // ..level = SentryLevel.warning; + // await scope.setTag('build', '579'); + // await scope.setExtra('company-name', 'Dart Inc'); + // }); + + // // Sends a full Sentry event payload to show the different parts of the UI. + // final sentryId = await Sentry.captureEvent(event); + + // print('Capture event result : SentryId : $sentryId'); + + // print('\nCapture message: '); + + // // Sends a full Sentry event payload to show the different parts of the UI. + // final messageSentryId = await Sentry.captureMessage( + // 'Message 1', + // level: SentryLevel.warning, + // template: 'Message %s', + // params: ['1'], + // ); + + // print('Capture message result : SentryId : $messageSentryId'); + + // try { + // await loadConfig(); + // } catch (error, stackTrace) { + // print('\nReporting the following stack trace: '); + // print(stackTrace); + // final sentryId = await Sentry.captureException( + // error, + // stackTrace: stackTrace, + // ); + + // print('Capture exception result : SentryId : $sentryId'); + // } + + // // capture unhandled error + // await loadConfig(); } Future loadConfig() async { diff --git a/dart/lib/src/protocol/dsn.dart b/dart/lib/src/protocol/dsn.dart index 5e964a0fbc..7b88d57876 100644 --- a/dart/lib/src/protocol/dsn.dart +++ b/dart/lib/src/protocol/dsn.dart @@ -27,13 +27,35 @@ class Dsn { @Deprecated('Use [envelopeUri] instead') Uri get postUri => envelopeUri; - Uri get envelopeUri => _UriData.fromUri(uri!, projectId).envelopeUri; + Uri get envelopeUri { + final uriCopy = uri!; + final port = uriCopy.hasPort && + ((uriCopy.scheme == 'http' && uriCopy.port != 80) || + (uriCopy.scheme == 'https' && uriCopy.port != 443)) + ? ':${uriCopy.port}' + : ''; + + final pathLength = uriCopy.pathSegments.length; - Uri get featureFlagsUri => _UriData.fromUri(uri!, projectId).featureFlagsUri; + String apiPath; + if (pathLength > 1) { + // some paths would present before the projectID in the uri + apiPath = + (uriCopy.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); + } else { + apiPath = 'api'; + } + return Uri.parse( + '${uriCopy.scheme}://${uriCopy.host}$port/$apiPath/$projectId/envelope/', + ); + } - // Uri get featureFlagsUri { - // return postUri.replace() - // } + Uri get featureFlagsUri { + // TODO: implement proper Uri + final uriTemp = + envelopeUri.toString().replaceAll('envelope', 'feature-flags'); + return Uri.parse(uriTemp); + } /// Parses a DSN String to a Dsn object factory Dsn.parse(String dsn) { @@ -54,40 +76,3 @@ class Dsn { ); } } - -class _UriData { - final String scheme; - final String host; - final String port; - final String apiPath; - final String projectId; - - _UriData(this.scheme, this.host, this.port, this.apiPath, this.projectId); - - factory _UriData.fromUri(Uri uri, String projectId) { - final port = uri.hasPort && - ((uri.scheme == 'http' && uri.port != 80) || - (uri.scheme == 'https' && uri.port != 443)) - ? ':${uri.port}' - : ''; - - final pathLength = uri.pathSegments.length; - - String apiPath; - if (pathLength > 1) { - // some paths would present before the projectID in the uri - apiPath = - (uri.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); - } else { - apiPath = 'api'; - } - - return _UriData(uri.scheme, uri.host, port, apiPath, projectId); - } - - Uri get envelopeUri => - Uri.parse('$scheme://$host}$port/$apiPath/$projectId/envelope/'); - - Uri get featureFlagsUri => - Uri.parse('$scheme://$host}$port/$apiPath/$projectId/feature-flags/'); -} diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 7e88ff64cc..d96654bad4 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -99,8 +99,8 @@ class HttpTransport implements Transport { @override Future?> fetchFeatureFlags() async { - final response = - await _options.httpClient.get(_dsn.featureFlagsUri, headers: _headers); + final response = await _options.httpClient.post(_dsn.featureFlagsUri, + headers: _credentialBuilder.configure(_headers)); if (response.statusCode != 200) { // body guard to not log the error as it has performance impact to allocate From 2381b41028caa32e354e12242b2c6f2f3adb8be4 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 16:50:40 +0200 Subject: [PATCH 09/33] add payload field --- dart/lib/src/feature_flags/evaluation_rule.dart | 14 +++++++++++++- dart/test/transport/http_transport_test.dart | 4 ++++ dart/test_resources/feature_flags.json | 6 +++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/dart/lib/src/feature_flags/evaluation_rule.dart b/dart/lib/src/feature_flags/evaluation_rule.dart index 85bdce53e7..f609b52c20 100644 --- a/dart/lib/src/feature_flags/evaluation_rule.dart +++ b/dart/lib/src/feature_flags/evaluation_rule.dart @@ -7,17 +7,29 @@ class EvaluationRule { final double? percentage; final bool? result; final Map _tags; + final Map? _payload; Map get tags => Map.unmodifiable(_tags); - EvaluationRule(this.type, this.percentage, this.result, this._tags); + Map? get payload => + _payload != null ? Map.unmodifiable(_payload!) : null; + + EvaluationRule( + this.type, + this.percentage, + this.result, + this._tags, + this._payload, + ); factory EvaluationRule.fromJson(Map json) { + final payload = json['payload']; return EvaluationRule( (json['type'] as String).toEvaluationType(), json['percentage'] as double?, json['result'] as bool?, Map.from(json['tags'] as Map), + payload != null ? Map.from(payload) : null, ); } } diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index e5d92c5947..fff41870ac 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -233,12 +233,14 @@ void main() { expect(rollout.result, true); expect(rollout.tags['userSegment'], 'slow'); expect(rollout.type, EvaluationType.rollout); + expect(rollout.payload, isNull); final match = accessToProfiling.evaluations.last; expect(match.percentage, isNull); expect(match.result, true); expect(match.tags['isSentryDev'], 'true'); expect(match.type, EvaluationType.match); + expect(match.payload!['background_image'], 'https://example.com/modus1.png'); // profilingEnabled final profilingEnabled = flags['profilingEnabled']!; @@ -250,12 +252,14 @@ void main() { expect(rolloutProfiling.result, true); expect(rolloutProfiling.tags['isSentryDev'], 'true'); expect(rolloutProfiling.type, EvaluationType.rollout); + expect(rolloutProfiling.payload, isNull); final matchProfiling = profilingEnabled.evaluations.last; expect(matchProfiling.percentage, isNull); expect(matchProfiling.result, true); expect(matchProfiling.tags.isEmpty, true); expect(matchProfiling.type, EvaluationType.match); + expect(matchProfiling.payload, isNull); }); }); } diff --git a/dart/test_resources/feature_flags.json b/dart/test_resources/feature_flags.json index 1a57d2a126..18c01ccdd2 100644 --- a/dart/test_resources/feature_flags.json +++ b/dart/test_resources/feature_flags.json @@ -11,13 +11,17 @@ "result": true, "tags": { "userSegment": "slow" - } + }, + "payload": null }, { "type": "match", "result": true, "tags": { "isSentryDev": "true" + }, + "payload": { + "background_image": "https://example.com/modus1.png" } } ] From c7dab0d9b78783b7550d4ae4225aaf44b398136d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 22 Aug 2022 17:58:41 +0200 Subject: [PATCH 10/33] temp commit --- dart/lib/src/feature_flags/feature_flag.dart | 8 ++-- dart/lib/src/hub.dart | 26 ++++++++++++ dart/lib/src/hub_adapter.dart | 5 ++- dart/lib/src/noop_hub.dart | 3 ++ dart/lib/src/noop_sentry_client.dart | 3 ++ dart/lib/src/sentry.dart | 3 ++ dart/lib/src/sentry_client.dart | 43 ++++++++++++++++++++ dart/test/transport/http_transport_test.dart | 4 +- dart/test_resources/feature_flags.json | 4 -- 9 files changed, 88 insertions(+), 11 deletions(-) diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index 7d3fa11ffa..08698eee4d 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -4,14 +4,14 @@ import 'evaluation_rule.dart'; @immutable class FeatureFlag { - final Map _tags; + // final Map _tags; final List _evaluations; - Map get tags => Map.unmodifiable(_tags); + // Map get tags => Map.unmodifiable(_tags); List get evaluations => List.unmodifiable(_evaluations); - FeatureFlag(this._tags, this._evaluations); + FeatureFlag(this._evaluations); factory FeatureFlag.fromJson(Map json) { final evaluationsList = json['evaluation'] as List? ?? []; @@ -20,7 +20,7 @@ class FeatureFlag { .toList(growable: false); return FeatureFlag( - Map.from(json['tags'] as Map), + // Map.from(json['tags'] as Map), evaluations, ); } diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 51fdb5d1ae..971e0ee033 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -542,6 +542,32 @@ class Hub { } return null; } + + Future isFeatureEnabled(String key) async { + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'isFeatureEnabled' call is a no-op.", + ); + // TODO: default value + return false; + } + + try { + final item = _peek(); + + return item.client.isFeatureEnabled(key); + } catch (exception, stacktrace) { + _options.logger( + SentryLevel.error, + 'Error while fetching feature flags', + exception: exception, + stackTrace: stacktrace, + ); + } + // TODO: default value + return false; + } } class _StackItem { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index c8bda740a7..cd9faceb0e 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -161,6 +161,9 @@ class HubAdapter implements Hub { Sentry.currentHub.setSpanContext(throwable, span, transaction); @override - Future?> fetchFeatureFlags() async => + Future?> fetchFeatureFlags() => Sentry.fetchFeatureFlags(); + + @override + Future isFeatureEnabled(String key) => Sentry.isFeatureEnabled(key); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 4360052c68..9b9f9ca1c5 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -118,4 +118,7 @@ class NoOpHub implements Hub { @override Future?> fetchFeatureFlags() async => null; + + @override + Future isFeatureEnabled(String key) async => false; } diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 537dad1105..8621a66aca 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -64,4 +64,7 @@ class NoOpSentryClient implements SentryClient { @override Future?> fetchFeatureFlags() async => null; + + @override + Future isFeatureEnabled(String key) async => false; } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 444dba81e9..903d7b93e9 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -276,4 +276,7 @@ class Sentry { static Future?> fetchFeatureFlags() => _hub.fetchFeatureFlags(); + + static Future isFeatureEnabled(String key) => + _hub.isFeatureEnabled(key); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 0fd8d9cbd4..79e59424e9 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:meta/meta.dart'; import 'event_processor.dart'; +import 'feature_flags/evaluation_type.dart'; import 'feature_flags/feature_flag.dart'; import 'sentry_user_feedback.dart'; import 'transport/rate_limiter.dart'; @@ -400,4 +401,46 @@ class SentryClient { Future?> fetchFeatureFlags() => _options.transport.fetchFeatureFlags(); + + Future isFeatureEnabled(String key) async { + // TODO: ideally cache the result of fetchFeatureFlags or + // let the user decide if it uses the cached value or not + final flags = await fetchFeatureFlags(); + final flag = flags?[key]; + + if (flag == null) { + // TODO default value + return false; + } + + // TODO: build context and fall back to global context + Map context = {}; + + for (final evalConfig in flag.evaluations) { + if (!_matchesTags(evalConfig.tags, context)) { + continue; + } + + switch (evalConfig.type) { + case EvaluationType.rollout: + break; + case EvaluationType.match: + break; + default: + break; + } + } + + // TODO default value + return false; + } + + // double _rollRandomNumber(Map context) { + + // } + + bool _matchesTags(Map tags, Map context) { + // TODO: implement + return true; + } } diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index fff41870ac..1e1572efeb 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -226,7 +226,7 @@ void main() { // accessToProfiling final accessToProfiling = flags!['accessToProfiling']!; - expect(accessToProfiling.tags['isEarlyAdopter'], 'true'); + // expect(accessToProfiling.tags['isEarlyAdopter'], 'true'); final rollout = accessToProfiling.evaluations.first; expect(rollout.percentage, 0.5); @@ -245,7 +245,7 @@ void main() { // profilingEnabled final profilingEnabled = flags['profilingEnabled']!; - expect(profilingEnabled.tags.isEmpty, true); + // expect(profilingEnabled.tags.isEmpty, true); final rolloutProfiling = profilingEnabled.evaluations.first; expect(rolloutProfiling.percentage, 0.05); diff --git a/dart/test_resources/feature_flags.json b/dart/test_resources/feature_flags.json index 18c01ccdd2..d75393f376 100644 --- a/dart/test_resources/feature_flags.json +++ b/dart/test_resources/feature_flags.json @@ -1,9 +1,6 @@ { "feature_flags": { "accessToProfiling": { - "tags": { - "isEarlyAdopter": "true" - }, "evaluation": [ { "type": "rollout", @@ -27,7 +24,6 @@ ] }, "profilingEnabled": { - "tags": {}, "evaluation": [ { "type": "rollout", From db85e99ab5d44fc90e34fbb0809a5a2c7355e7fc Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 23 Aug 2022 13:20:43 +0200 Subject: [PATCH 11/33] added random number generator --- dart/example/bin/example.dart | 22 +++++-- dart/lib/sentry.dart | 1 + .../feature_flags/feature_flag_context.dart | 11 ++++ .../lib/src/feature_flags/xor_shift_rand.dart | 47 ++++++++++++++ dart/lib/src/hub.dart | 57 ++++++++-------- dart/lib/src/hub_adapter.dart | 14 ++-- dart/lib/src/noop_hub.dart | 12 ++-- dart/lib/src/noop_sentry_client.dart | 8 ++- dart/lib/src/sentry.dart | 15 +++-- dart/lib/src/sentry_client.dart | 65 ++++++++++++++++--- dart/pubspec.yaml | 1 + dart/test/sentry_test.dart | 5 ++ dart/test/transport/http_transport_test.dart | 3 +- dart/test/xor_shift_rand_test.dart | 19 ++++++ 14 files changed, 225 insertions(+), 55 deletions(-) create mode 100644 dart/lib/src/feature_flags/feature_flag_context.dart create mode 100644 dart/lib/src/feature_flags/xor_shift_rand.dart create mode 100644 dart/test/xor_shift_rand_test.dart diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 2ad27ef6d1..b03e860635 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -25,11 +25,23 @@ Future main() async { ..addEventProcessor(TagEventProcessor()), appRunner: runApp, ); - - final featureFlags = await Sentry.fetchFeatureFlags(); - for (final flag in featureFlags!.entries) { - print(flag.key); - } + await Sentry.configureScope((scope) async { + await scope.setUser( + SentryUser( + id: '800', + ), + ); + }); + + // final featureFlags = await Sentry.fetchFeatureFlags(); + // for (final flag in featureFlags!.entries) { + // print(flag.key); + // } + final enabled = await Sentry.isFeatureEnabled('test', + context: (myContext) => { + myContext.tags['stickyId'] = 'myCustomStickyId', + }); + print(enabled); } Future runApp() async { diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 9b12e2c090..35032d9924 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -34,3 +34,4 @@ export 'src/sentry_measurement.dart'; export 'src/feature_flags/feature_flag.dart'; export 'src/feature_flags/evaluation_rule.dart'; export 'src/feature_flags/evaluation_type.dart'; +export 'src/feature_flags/feature_flag_context.dart'; diff --git a/dart/lib/src/feature_flags/feature_flag_context.dart b/dart/lib/src/feature_flags/feature_flag_context.dart new file mode 100644 index 0000000000..ff6f03b767 --- /dev/null +++ b/dart/lib/src/feature_flags/feature_flag_context.dart @@ -0,0 +1,11 @@ +typedef FeatureFlagContextCallback = void Function(FeatureFlagContext context); + +class FeatureFlagContext { + // String stickyId; + // String userId; + // String deviceId; + Map tags = {}; + + // FeatureFlagContext(this.stickyId, this.userId, this.deviceId, this.tags); + FeatureFlagContext(this.tags); +} diff --git a/dart/lib/src/feature_flags/xor_shift_rand.dart b/dart/lib/src/feature_flags/xor_shift_rand.dart new file mode 100644 index 0000000000..d93617ea26 --- /dev/null +++ b/dart/lib/src/feature_flags/xor_shift_rand.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +/// final rand = XorShiftRandom('wohoo'); +/// rand.next(); +class XorShiftRandom { + List state = [0, 0, 0, 0]; + static const mask = 0xffffffff; + + XorShiftRandom(String seed) { + _seed(seed); + } + + void _seed(String seed) { + final encoded = utf8.encode(seed); + final bytes = sha1.convert(encoded).bytes; + final slice = bytes.sublist(0, 16); + + for (var i = 0; i < state.length; i++) { + final unpack = (slice[i * 4] << 24) | + (slice[i * 4 + 1] << 16) | + (slice[i * 4 + 2] << 8) | + (slice[i * 4 + 3]); + state[i] = unpack; + } + } + + double next() { + return nextu32() / mask; + } + + int nextu32() { + var t = state[3]; + final s = state[0]; + + state[3] = state[2]; + state[2] = state[1]; + state[1] = s; + + t = (t << 11) & mask; + t ^= t >> 8; + state[0] = (t ^ s ^ (s >> 19)) & mask; + + return state[0]; + } +} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 971e0ee033..c8be059928 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -519,31 +519,32 @@ class Hub { return event; } - Future?> fetchFeatureFlags() async { - if (!_isEnabled) { - _options.logger( - SentryLevel.warning, - "Instance is disabled and this 'fetchFeatureFlags' call is a no-op.", - ); - return null; - } - - try { - final item = _peek(); - - return item.client.fetchFeatureFlags(); - } catch (exception, stacktrace) { - _options.logger( - SentryLevel.error, - 'Error while fetching feature flags', - exception: exception, - stackTrace: stacktrace, - ); - } - return null; - } - - Future isFeatureEnabled(String key) async { + // Future?> fetchFeatureFlags() async { + // if (!_isEnabled) { + // _options.logger( + // SentryLevel.warning, + // "Instance is disabled and this 'fetchFeatureFlags' call is a no-op.", + // ); + // return null; + // } + + // try { + // final item = _peek(); + + // return item.client.fetchFeatureFlags(); + // } catch (exception, stacktrace) { + // _options.logger( + // SentryLevel.error, + // 'Error while fetching feature flags', + // exception: exception, + // stackTrace: stacktrace, + // ); + // } + // return null; + // } + + Future isFeatureEnabled(String key, + {FeatureFlagContextCallback? context}) async { if (!_isEnabled) { _options.logger( SentryLevel.warning, @@ -556,7 +557,11 @@ class Hub { try { final item = _peek(); - return item.client.isFeatureEnabled(key); + return item.client.isFeatureEnabled( + key, + scope: item.scope, + context: context, + ); } catch (exception, stacktrace) { _options.logger( SentryLevel.error, diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index cd9faceb0e..49a14ff8b6 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'feature_flags/feature_flag.dart'; +import 'feature_flags/feature_flag_context.dart'; import 'hub.dart'; import 'protocol.dart'; import 'sentry.dart'; @@ -160,10 +160,14 @@ class HubAdapter implements Hub { ) => Sentry.currentHub.setSpanContext(throwable, span, transaction); - @override - Future?> fetchFeatureFlags() => - Sentry.fetchFeatureFlags(); + // @override + // Future?> fetchFeatureFlags() => + // Sentry.fetchFeatureFlags(); @override - Future isFeatureEnabled(String key) => Sentry.isFeatureEnabled(key); + Future isFeatureEnabled( + String key, { + FeatureFlagContextCallback? context, + }) => + Sentry.isFeatureEnabled(key, context: context); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 9b9f9ca1c5..5a9994890c 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'feature_flags/feature_flag.dart'; +import 'feature_flags/feature_flag_context.dart'; import 'hub.dart'; import 'protocol.dart'; import 'sentry_client.dart'; @@ -116,9 +116,13 @@ class NoOpHub implements Hub { @override void setSpanContext(throwable, ISentrySpan span, String transaction) {} - @override - Future?> fetchFeatureFlags() async => null; + // @override + // Future?> fetchFeatureFlags() async => null; @override - Future isFeatureEnabled(String key) async => false; + Future isFeatureEnabled( + String key, { + FeatureFlagContextCallback? context, + }) async => + false; } diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 8621a66aca..f4331ff095 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'feature_flags/feature_flag.dart'; +import 'feature_flags/feature_flag_context.dart'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_client.dart'; @@ -66,5 +67,10 @@ class NoOpSentryClient implements SentryClient { Future?> fetchFeatureFlags() async => null; @override - Future isFeatureEnabled(String key) async => false; + Future isFeatureEnabled( + String key, { + Scope? scope, + FeatureFlagContextCallback? context, + }) async => + false; } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 903d7b93e9..cbb515f7c2 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -6,7 +6,7 @@ import 'default_integrations.dart'; import 'enricher/enricher_event_processor.dart'; import 'environment/environment_variables.dart'; import 'event_processor/deduplication_event_processor.dart'; -import 'feature_flags/feature_flag.dart'; +import 'feature_flags/feature_flag_context.dart'; import 'hub.dart'; import 'hub_adapter.dart'; import 'integration.dart'; @@ -274,9 +274,14 @@ class Sentry { @internal static Hub get currentHub => _hub; - static Future?> fetchFeatureFlags() => - _hub.fetchFeatureFlags(); + // static Future?> fetchFeatureFlags() => + // _hub.fetchFeatureFlags(); - static Future isFeatureEnabled(String key) => - _hub.isFeatureEnabled(key); + // typedef FeatureFlagContextCallback = void Function(); + + static Future isFeatureEnabled( + String key, { + FeatureFlagContextCallback? context, + }) => + _hub.isFeatureEnabled(key, context: context); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 79e59424e9..e5190f9d49 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -5,6 +5,8 @@ import 'package:meta/meta.dart'; import 'event_processor.dart'; import 'feature_flags/evaluation_type.dart'; import 'feature_flags/feature_flag.dart'; +import 'feature_flags/feature_flag_context.dart'; +import 'feature_flags/xor_shift_rand.dart'; import 'sentry_user_feedback.dart'; import 'transport/rate_limiter.dart'; import 'protocol.dart'; @@ -402,7 +404,11 @@ class SentryClient { Future?> fetchFeatureFlags() => _options.transport.fetchFeatureFlags(); - Future isFeatureEnabled(String key) async { + Future isFeatureEnabled( + String key, { + Scope? scope, + FeatureFlagContextCallback? context, + }) async { // TODO: ideally cache the result of fetchFeatureFlags or // let the user decide if it uses the cached value or not final flags = await fetchFeatureFlags(); @@ -414,18 +420,53 @@ class SentryClient { } // TODO: build context and fall back to global context - Map context = {}; + final featureFlagContext = FeatureFlagContext({ + 'deviceId': 'myDeviceId', // TODO: get from flutter thru message channels + }); + + // add userId from Scope + final userId = scope?.user?.id; + if (userId != null) { + featureFlagContext.tags['userId'] = userId; + } + // add the release + final release = _options.release; + if (release != null) { + featureFlagContext.tags['release'] = release; + } + // add the env + final environment = _options.environment; + if (environment != null) { + featureFlagContext.tags['environment'] = environment; + } + + // run feature flag context callback and allow user adding/removing tags + if (context != null) { + context(featureFlagContext); + } + + // fallback stickyId if not provided by the user + // fallbacks to userId if set or deviceId + if (!featureFlagContext.tags.containsKey('stickyId')) { + final stickyId = featureFlagContext.tags['userId'] ?? + featureFlagContext.tags['deviceId']; + featureFlagContext.tags['stickyId'] = stickyId; + } for (final evalConfig in flag.evaluations) { - if (!_matchesTags(evalConfig.tags, context)) { + if (!_matchesTags(evalConfig.tags, featureFlagContext.tags)) { continue; } switch (evalConfig.type) { case EvaluationType.rollout: + final percentage = _rollRandomNumber(evalConfig.tags); + if (percentage >= (evalConfig.percentage ?? 0)) { + return evalConfig.result ?? false; // TODO: return default value + } break; case EvaluationType.match: - break; + return evalConfig.result ?? false; // TODO: return default value default: break; } @@ -435,12 +476,20 @@ class SentryClient { return false; } - // double _rollRandomNumber(Map context) { - - // } + double _rollRandomNumber(Map tags) { + final stickyId = tags['stickyId'] as String; + + final rand = XorShiftRandom(stickyId); + return rand.next(); + } bool _matchesTags(Map tags, Map context) { - // TODO: implement + // TODO: double check this impl. + for (final item in tags.entries) { + if (item.value != context[item.key]) { + return false; + } + } return true; } } diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index 9019a9df33..d37e95008e 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -11,6 +11,7 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: + crypto: ^3.0.2 # TODO: consider vendoring sha1 http: ^0.13.0 meta: ^1.3.0 stack_trace: ^1.10.0 diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 989f63a58e..12a6f24724 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -182,6 +182,11 @@ void main() { group('Sentry init', () { tearDown(() async { await Sentry.close(); + + await Sentry.isFeatureEnabled('test', + context: (myContext) => { + myContext.tags['something'] = 'true', + }); }); test('should install integrations', () async { diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 1e1572efeb..5b95c7e2d4 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -240,7 +240,8 @@ void main() { expect(match.result, true); expect(match.tags['isSentryDev'], 'true'); expect(match.type, EvaluationType.match); - expect(match.payload!['background_image'], 'https://example.com/modus1.png'); + expect( + match.payload!['background_image'], 'https://example.com/modus1.png'); // profilingEnabled final profilingEnabled = flags['profilingEnabled']!; diff --git a/dart/test/xor_shift_rand_test.dart b/dart/test/xor_shift_rand_test.dart new file mode 100644 index 0000000000..11098843d1 --- /dev/null +++ b/dart/test/xor_shift_rand_test.dart @@ -0,0 +1,19 @@ +import 'package:sentry/src/feature_flags/xor_shift_rand.dart'; +import 'package:test/test.dart'; + +void main() { + test('test random determinisc generator', () { + final rand = XorShiftRandom('wohoo'); + + expect(rand.nextu32(), 3709882355); + expect(rand.nextu32(), 3406141351); + expect(rand.nextu32(), 2220835615); + expect(rand.nextu32(), 1978561524); + expect(rand.nextu32(), 2006162129); + expect(rand.nextu32(), 1526862107); + expect(rand.nextu32(), 2715875971); + expect(rand.nextu32(), 3524055327); + expect(rand.nextu32(), 1313248726); + expect(rand.nextu32(), 1591659718); + }); +} From 8ccccc0536d26df4b49a0c9066d934a8192dee9c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 23 Aug 2022 13:37:17 +0200 Subject: [PATCH 12/33] add default value --- dart/example/bin/example.dart | 4 ++-- dart/lib/src/hub.dart | 14 ++++++++------ dart/lib/src/hub_adapter.dart | 7 ++++++- dart/lib/src/noop_hub.dart | 1 + dart/lib/src/noop_sentry_client.dart | 1 + dart/lib/src/sentry.dart | 7 ++++++- dart/lib/src/sentry_client.dart | 14 ++++++++------ 7 files changed, 32 insertions(+), 16 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index b03e860635..af8c6bffcd 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -21,8 +21,8 @@ Future main() async { (options) => options ..dsn = dsn ..debug = true - ..sendDefaultPii = true - ..addEventProcessor(TagEventProcessor()), + ..release = 'myapp@1.0.0+1' + ..environment = 'prod', appRunner: runApp, ); await Sentry.configureScope((scope) async { diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index c8be059928..bc6b59979f 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -543,15 +543,17 @@ class Hub { // return null; // } - Future isFeatureEnabled(String key, - {FeatureFlagContextCallback? context}) async { + Future isFeatureEnabled( + String key, { + bool defaultValue = false, + FeatureFlagContextCallback? context, + }) async { if (!_isEnabled) { _options.logger( SentryLevel.warning, "Instance is disabled and this 'isFeatureEnabled' call is a no-op.", ); - // TODO: default value - return false; + return defaultValue; } try { @@ -560,6 +562,7 @@ class Hub { return item.client.isFeatureEnabled( key, scope: item.scope, + defaultValue: defaultValue, context: context, ); } catch (exception, stacktrace) { @@ -570,8 +573,7 @@ class Hub { stackTrace: stacktrace, ); } - // TODO: default value - return false; + return defaultValue; } } diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 49a14ff8b6..2f3a21849e 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -167,7 +167,12 @@ class HubAdapter implements Hub { @override Future isFeatureEnabled( String key, { + bool defaultValue = false, FeatureFlagContextCallback? context, }) => - Sentry.isFeatureEnabled(key, context: context); + Sentry.isFeatureEnabled( + key, + defaultValue: defaultValue, + context: context, + ); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 5a9994890c..a3504238a8 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -122,6 +122,7 @@ class NoOpHub implements Hub { @override Future isFeatureEnabled( String key, { + bool defaultValue = false, FeatureFlagContextCallback? context, }) async => false; diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index f4331ff095..3c4e4dce15 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -70,6 +70,7 @@ class NoOpSentryClient implements SentryClient { Future isFeatureEnabled( String key, { Scope? scope, + bool defaultValue = false, FeatureFlagContextCallback? context, }) async => false; diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index cbb515f7c2..32c2273d67 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -281,7 +281,12 @@ class Sentry { static Future isFeatureEnabled( String key, { + bool defaultValue = false, FeatureFlagContextCallback? context, }) => - _hub.isFeatureEnabled(key, context: context); + _hub.isFeatureEnabled( + key, + defaultValue: defaultValue, + context: context, + ); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index e5190f9d49..5140604419 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; import 'package:meta/meta.dart'; +import 'package:uuid/uuid.dart'; import 'event_processor.dart'; import 'feature_flags/evaluation_type.dart'; @@ -407,6 +408,7 @@ class SentryClient { Future isFeatureEnabled( String key, { Scope? scope, + bool defaultValue = false, FeatureFlagContextCallback? context, }) async { // TODO: ideally cache the result of fetchFeatureFlags or @@ -415,11 +417,9 @@ class SentryClient { final flag = flags?[key]; if (flag == null) { - // TODO default value - return false; + return defaultValue; } - // TODO: build context and fall back to global context final featureFlagContext = FeatureFlagContext({ 'deviceId': 'myDeviceId', // TODO: get from flutter thru message channels }); @@ -447,9 +447,11 @@ class SentryClient { // fallback stickyId if not provided by the user // fallbacks to userId if set or deviceId + // if none were provided, it generated an Uuid if (!featureFlagContext.tags.containsKey('stickyId')) { final stickyId = featureFlagContext.tags['userId'] ?? - featureFlagContext.tags['deviceId']; + featureFlagContext.tags['deviceId'] ?? + Uuid().v4().toString(); featureFlagContext.tags['stickyId'] = stickyId; } @@ -462,11 +464,11 @@ class SentryClient { case EvaluationType.rollout: final percentage = _rollRandomNumber(evalConfig.tags); if (percentage >= (evalConfig.percentage ?? 0)) { - return evalConfig.result ?? false; // TODO: return default value + return evalConfig.result ?? defaultValue; } break; case EvaluationType.match: - return evalConfig.result ?? false; // TODO: return default value + return evalConfig.result ?? defaultValue; default: break; } From 726b47dc12944ceb697d707e8ffc97e76757fb9e Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 23 Aug 2022 13:50:14 +0200 Subject: [PATCH 13/33] add caching of feature flags --- dart/example/bin/example.dart | 11 +++++++---- dart/lib/src/sentry_client.dart | 9 +++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index af8c6bffcd..40345b9c81 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -37,10 +37,13 @@ Future main() async { // for (final flag in featureFlags!.entries) { // print(flag.key); // } - final enabled = await Sentry.isFeatureEnabled('test', - context: (myContext) => { - myContext.tags['stickyId'] = 'myCustomStickyId', - }); + final enabled = await Sentry.isFeatureEnabled( + 'test', + defaultValue: false, + context: (myContext) => { + myContext.tags['stickyId'] = 'myCustomStickyId', + }, + ); print(enabled); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 5140604419..fc11862782 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -41,6 +41,8 @@ class SentryClient { SentryStackTraceFactory get _stackTraceFactory => _options.stackTraceFactory; + Map? _featureFlags; + /// Instantiates a client using [SentryOptions] factory SentryClient(SentryOptions options) { if (options.sendClientReports) { @@ -411,10 +413,9 @@ class SentryClient { bool defaultValue = false, FeatureFlagContextCallback? context, }) async { - // TODO: ideally cache the result of fetchFeatureFlags or - // let the user decide if it uses the cached value or not - final flags = await fetchFeatureFlags(); - final flag = flags?[key]; + // TODO: add mechanism to reset caching + _featureFlags = _featureFlags ?? await fetchFeatureFlags(); + final flag = _featureFlags?[key]; if (flag == null) { return defaultValue; From 4329bd1b828e1ae8b4ce2d500545a26ede4710d4 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 23 Aug 2022 15:28:42 +0200 Subject: [PATCH 14/33] fix --- dart/example/bin/example.dart | 9 ++-- .../src/feature_flags/evaluation_rule.dart | 8 ++-- dart/lib/src/hub.dart | 48 +++++++++---------- dart/lib/src/hub_adapter.dart | 9 ++-- dart/lib/src/noop_hub.dart | 7 +-- dart/lib/src/noop_sentry_client.dart | 3 ++ dart/lib/src/sentry.dart | 9 ++-- dart/lib/src/sentry_client.dart | 37 ++++++++------ 8 files changed, 71 insertions(+), 59 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 40345b9c81..4786b321e9 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -33,18 +33,17 @@ Future main() async { ); }); - // final featureFlags = await Sentry.fetchFeatureFlags(); - // for (final flag in featureFlags!.entries) { - // print(flag.key); - // } final enabled = await Sentry.isFeatureEnabled( - 'test', + '@@accessToProfiling', defaultValue: false, context: (myContext) => { myContext.tags['stickyId'] = 'myCustomStickyId', }, ); print(enabled); + + // TODO: does it return the active EvaluationRule? do we create a new model for that? + final flag = await Sentry.getFeatureFlagInfo('test'); } Future runApp() async { diff --git a/dart/lib/src/feature_flags/evaluation_rule.dart b/dart/lib/src/feature_flags/evaluation_rule.dart index f609b52c20..6fbbc8d698 100644 --- a/dart/lib/src/feature_flags/evaluation_rule.dart +++ b/dart/lib/src/feature_flags/evaluation_rule.dart @@ -5,7 +5,7 @@ import 'evaluation_type.dart'; class EvaluationRule { final EvaluationType type; final double? percentage; - final bool? result; + final bool result; final Map _tags; final Map? _payload; @@ -24,11 +24,13 @@ class EvaluationRule { factory EvaluationRule.fromJson(Map json) { final payload = json['payload']; + final tags = json['tags']; + return EvaluationRule( (json['type'] as String).toEvaluationType(), json['percentage'] as double?, - json['result'] as bool?, - Map.from(json['tags'] as Map), + json['result'] as bool, + tags != null ? Map.from(tags) : {}, payload != null ? Map.from(payload) : null, ); } diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index bc6b59979f..362e4665e7 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -519,30 +519,6 @@ class Hub { return event; } - // Future?> fetchFeatureFlags() async { - // if (!_isEnabled) { - // _options.logger( - // SentryLevel.warning, - // "Instance is disabled and this 'fetchFeatureFlags' call is a no-op.", - // ); - // return null; - // } - - // try { - // final item = _peek(); - - // return item.client.fetchFeatureFlags(); - // } catch (exception, stacktrace) { - // _options.logger( - // SentryLevel.error, - // 'Error while fetching feature flags', - // exception: exception, - // stackTrace: stacktrace, - // ); - // } - // return null; - // } - Future isFeatureEnabled( String key, { bool defaultValue = false, @@ -575,6 +551,30 @@ class Hub { } return defaultValue; } + + Future getFeatureFlagInfo(String key) async { + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'getFeatureFlagInfo' call is a no-op.", + ); + return null; + } + + try { + final item = _peek(); + + return item.client.getFeatureFlagInfo(key); + } catch (exception, stacktrace) { + _options.logger( + SentryLevel.error, + 'Error while fetching feature flags', + exception: exception, + stackTrace: stacktrace, + ); + } + return null; + } } class _StackItem { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 2f3a21849e..62753777bc 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; import 'hub.dart'; import 'protocol.dart'; @@ -160,10 +161,6 @@ class HubAdapter implements Hub { ) => Sentry.currentHub.setSpanContext(throwable, span, transaction); - // @override - // Future?> fetchFeatureFlags() => - // Sentry.fetchFeatureFlags(); - @override Future isFeatureEnabled( String key, { @@ -175,4 +172,8 @@ class HubAdapter implements Hub { defaultValue: defaultValue, context: context, ); + + @override + Future getFeatureFlagInfo(String key) => + Sentry.getFeatureFlagInfo(key); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index a3504238a8..7c81f8707d 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; import 'hub.dart'; import 'protocol.dart'; @@ -116,9 +117,6 @@ class NoOpHub implements Hub { @override void setSpanContext(throwable, ISentrySpan span, String transaction) {} - // @override - // Future?> fetchFeatureFlags() async => null; - @override Future isFeatureEnabled( String key, { @@ -126,4 +124,7 @@ class NoOpHub implements Hub { FeatureFlagContextCallback? context, }) async => false; + + @override + Future getFeatureFlagInfo(String key) async => null; } diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 3c4e4dce15..9fda5a37ef 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -74,4 +74,7 @@ class NoOpSentryClient implements SentryClient { FeatureFlagContextCallback? context, }) async => false; + + @override + Future getFeatureFlagInfo(String key) async => null; } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 32c2273d67..bff08897eb 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -6,6 +6,7 @@ import 'default_integrations.dart'; import 'enricher/enricher_event_processor.dart'; import 'environment/environment_variables.dart'; import 'event_processor/deduplication_event_processor.dart'; +import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; import 'hub.dart'; import 'hub_adapter.dart'; @@ -274,11 +275,6 @@ class Sentry { @internal static Hub get currentHub => _hub; - // static Future?> fetchFeatureFlags() => - // _hub.fetchFeatureFlags(); - - // typedef FeatureFlagContextCallback = void Function(); - static Future isFeatureEnabled( String key, { bool defaultValue = false, @@ -289,4 +285,7 @@ class Sentry { defaultValue: defaultValue, context: context, ); + + static Future getFeatureFlagInfo(String key) => + _hub.getFeatureFlagInfo(key); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index fc11862782..2cd63bbcdc 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -413,9 +413,8 @@ class SentryClient { bool defaultValue = false, FeatureFlagContextCallback? context, }) async { - // TODO: add mechanism to reset caching - _featureFlags = _featureFlags ?? await fetchFeatureFlags(); - final flag = _featureFlags?[key]; + final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); + final flag = featureFlags?[key]; if (flag == null) { return defaultValue; @@ -449,8 +448,9 @@ class SentryClient { // fallback stickyId if not provided by the user // fallbacks to userId if set or deviceId // if none were provided, it generated an Uuid - if (!featureFlagContext.tags.containsKey('stickyId')) { - final stickyId = featureFlagContext.tags['userId'] ?? + var stickyId = featureFlagContext.tags['stickyId']; + if (stickyId == null) { + stickyId = featureFlagContext.tags['userId'] ?? featureFlagContext.tags['deviceId'] ?? Uuid().v4().toString(); featureFlagContext.tags['stickyId'] = stickyId; @@ -463,31 +463,27 @@ class SentryClient { switch (evalConfig.type) { case EvaluationType.rollout: - final percentage = _rollRandomNumber(evalConfig.tags); - if (percentage >= (evalConfig.percentage ?? 0)) { - return evalConfig.result ?? defaultValue; + final percentage = _rollRandomNumber(stickyId); + if (percentage >= (evalConfig.percentage ?? 0.0)) { + return evalConfig.result; } break; case EvaluationType.match: - return evalConfig.result ?? defaultValue; + return evalConfig.result; default: break; } } - // TODO default value - return false; + return defaultValue; } - double _rollRandomNumber(Map tags) { - final stickyId = tags['stickyId'] as String; - + double _rollRandomNumber(String stickyId) { final rand = XorShiftRandom(stickyId); return rand.next(); } bool _matchesTags(Map tags, Map context) { - // TODO: double check this impl. for (final item in tags.entries) { if (item.value != context[item.key]) { return false; @@ -495,4 +491,15 @@ class SentryClient { } return true; } + + Future getFeatureFlagInfo(String key) async { + final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); + return featureFlags?[key]; + } + + Future?> _getFeatureFlagsFromCacheOrNetwork() async { + // TODO: add mechanism to reset caching + _featureFlags = _featureFlags ?? await fetchFeatureFlags(); + return _featureFlags; + } } From 264889e912861d733be2b58171dc8c9cbd32daca Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 23 Aug 2022 15:33:41 +0200 Subject: [PATCH 15/33] fix --- dart/example/bin/example.dart | 66 +------------------------- dart/lib/src/sentry_client.dart | 2 +- dart/test_resources/feature_flags.json | 5 +- 3 files changed, 4 insertions(+), 69 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 4786b321e9..61cbcbf147 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -46,71 +46,7 @@ Future main() async { final flag = await Sentry.getFeatureFlagInfo('test'); } -Future runApp() async { - print('\nReporting a complete event example: '); - // await Sentry.captureMessage('test'); - - // Sentry.addBreadcrumb( - // Breadcrumb( - // message: 'Authenticated user', - // category: 'auth', - // type: 'debug', - // data: { - // 'admin': true, - // 'permissions': [1, 2, 3] - // }, - // ), - // ); - - // await Sentry.configureScope((scope) async { - // await scope.setUser(SentryUser( - // id: '800', - // username: 'first-user', - // email: 'first@user.lan', - // // ipAddress: '127.0.0.1', sendDefaultPii feature is enabled - // extras: {'first-sign-in': '2020-01-01'}, - // )); - // scope - // // ..fingerprint = ['example-dart'], fingerprint forces events to group together - // ..transaction = '/example/app' - // ..level = SentryLevel.warning; - // await scope.setTag('build', '579'); - // await scope.setExtra('company-name', 'Dart Inc'); - // }); - - // // Sends a full Sentry event payload to show the different parts of the UI. - // final sentryId = await Sentry.captureEvent(event); - - // print('Capture event result : SentryId : $sentryId'); - - // print('\nCapture message: '); - - // // Sends a full Sentry event payload to show the different parts of the UI. - // final messageSentryId = await Sentry.captureMessage( - // 'Message 1', - // level: SentryLevel.warning, - // template: 'Message %s', - // params: ['1'], - // ); - - // print('Capture message result : SentryId : $messageSentryId'); - - // try { - // await loadConfig(); - // } catch (error, stackTrace) { - // print('\nReporting the following stack trace: '); - // print(stackTrace); - // final sentryId = await Sentry.captureException( - // error, - // stackTrace: stackTrace, - // ); - - // print('Capture exception result : SentryId : $sentryId'); - // } - - // // capture unhandled error - // await loadConfig(); -} +Future runApp() async {} Future loadConfig() async { await parseConfig(); diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 2cd63bbcdc..3285c895a9 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -447,7 +447,7 @@ class SentryClient { // fallback stickyId if not provided by the user // fallbacks to userId if set or deviceId - // if none were provided, it generated an Uuid + // if none were provided, it generates an Uuid var stickyId = featureFlagContext.tags['stickyId']; if (stickyId == null) { stickyId = featureFlagContext.tags['userId'] ?? diff --git a/dart/test_resources/feature_flags.json b/dart/test_resources/feature_flags.json index d75393f376..c006edd749 100644 --- a/dart/test_resources/feature_flags.json +++ b/dart/test_resources/feature_flags.json @@ -35,10 +35,9 @@ }, { "type": "match", - "result": true, - "tags": {} + "result": true } ] } } -} \ No newline at end of file +} From 901d883478fd2589d76ff8a40aefaaecf330f291 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 23 Aug 2022 17:42:39 +0200 Subject: [PATCH 16/33] implement fetch for flutter transport --- dart/lib/sentry.dart | 2 ++ .../src/feature_flags/evaluation_rule.dart | 4 ++-- dart/lib/src/feature_flags/feature_flag.dart | 6 +++-- dart/lib/src/sentry_client.dart | 15 +++++++----- dart/lib/src/sentry_options.dart | 3 +++ .../io/sentry/flutter/SentryFlutterPlugin.kt | 7 +++++- flutter/example/lib/main.dart | 23 ++++++++++++++++++- .../Classes/SentryFlutterPluginApple.swift | 7 +++++- flutter/lib/src/default_integrations.dart | 8 ++++++- flutter/lib/src/file_system_transport.dart | 9 ++++++-- 10 files changed, 68 insertions(+), 16 deletions(-) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 35032d9924..c35d0ca1d1 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -27,6 +27,8 @@ export 'src/http_client/sentry_http_client.dart'; export 'src/http_client/sentry_http_client_error.dart'; export 'src/sentry_attachment/sentry_attachment.dart'; export 'src/sentry_user_feedback.dart'; +export 'src/transport/rate_limiter.dart'; +export 'src/transport/http_transport.dart'; // tracing export 'src/tracing.dart'; export 'src/sentry_measurement.dart'; diff --git a/dart/lib/src/feature_flags/evaluation_rule.dart b/dart/lib/src/feature_flags/evaluation_rule.dart index 6fbbc8d698..88003e49b9 100644 --- a/dart/lib/src/feature_flags/evaluation_rule.dart +++ b/dart/lib/src/feature_flags/evaluation_rule.dart @@ -5,7 +5,7 @@ import 'evaluation_type.dart'; class EvaluationRule { final EvaluationType type; final double? percentage; - final bool result; + final dynamic result; final Map _tags; final Map? _payload; @@ -29,7 +29,7 @@ class EvaluationRule { return EvaluationRule( (json['type'] as String).toEvaluationType(), json['percentage'] as double?, - json['result'] as bool, + json['result'], tags != null ? Map.from(tags) : {}, payload != null ? Map.from(payload) : null, ); diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index 08698eee4d..27fe1e5f45 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -6,21 +6,23 @@ import 'evaluation_rule.dart'; class FeatureFlag { // final Map _tags; final List _evaluations; + final String kind; // Map get tags => Map.unmodifiable(_tags); List get evaluations => List.unmodifiable(_evaluations); - FeatureFlag(this._evaluations); + FeatureFlag(this.kind, this._evaluations); factory FeatureFlag.fromJson(Map json) { + final kind = json['kind']; final evaluationsList = json['evaluation'] as List? ?? []; final evaluations = evaluationsList .map((e) => EvaluationRule.fromJson(e)) .toList(growable: false); return FeatureFlag( - // Map.from(json['tags'] as Map), + kind, evaluations, ); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 3285c895a9..76af2cf465 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -419,22 +419,25 @@ class SentryClient { if (flag == null) { return defaultValue; } + final featureFlagContext = FeatureFlagContext({}); - final featureFlagContext = FeatureFlagContext({ - 'deviceId': 'myDeviceId', // TODO: get from flutter thru message channels - }); + // set the device id + final deviceId = _options.distinctId; + if (deviceId != null) { + featureFlagContext.tags['deviceId'] = deviceId; + } - // add userId from Scope + // set userId from Scope final userId = scope?.user?.id; if (userId != null) { featureFlagContext.tags['userId'] = userId; } - // add the release + // set the release final release = _options.release; if (release != null) { featureFlagContext.tags['release'] = release; } - // add the env + // set the env final environment = _options.environment; if (environment != null) { featureFlagContext.tags['environment'] = environment; diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index fbb48b7fa5..4cd7ac5295 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -271,6 +271,9 @@ class SentryOptions { /// If enabled, [scopeObservers] will be called when mutating scope. bool enableScopeSync = true; + /// The distinct Id/device Id used for feature flags + String? distinctId; + final List _scopeObservers = []; List get scopeObservers => _scopeObservers; diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 047285a059..4d622b2de7 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -171,7 +171,12 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { // missing proxy, enableScopeSync } - result.success("") + + // make Installation.id(context) public + val item = mapOf( + "deviceId" to Sentry.getCurrentHub().options.distinctId + ) + result.success(item) } private fun fetchNativeAppStart(result: Result) { diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index d14cabc470..de53dd3c1c 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sentry_logging/sentry_logging.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String _exampleDsn = - 'https://9934c532bf8446ef961450973c898537@o447951.ingest.sentry.io/5428562'; + 'https://60d3409215134fd1a60765f2400b6b38@ac75-72-74-53-151.ngrok.io/1'; final _channel = const MethodChannel('example.flutter.sentry.io'); @@ -401,6 +401,27 @@ class AndroidExample extends StatelessWidget { }, child: const Text('Logging'), ), + ElevatedButton( + onPressed: () async { + await Sentry.configureScope((scope) async { + await scope.setUser( + SentryUser( + id: '800', + ), + ); + }); + + final enabled = await Sentry.isFeatureEnabled( + '@@accessToProfiling', + defaultValue: false, + context: (myContext) => { + myContext.tags['myCustomTag'] = 'myCustomValue', + }, + ); + print(enabled); + }, + child: const Text('Check feature flags'), + ), ]); } } diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index 174bfdeb8e..0b7629ace9 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -231,7 +231,12 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { didReceiveDidBecomeActiveNotification = false } - result("") + let deviceId = PrivateSentrySDKOnly.installationID + let item: [String: Any] = [ + "deviceId": deviceId + ] + + result(item) } private func closeNativeSdk(_ call: FlutterMethodCall, result: @escaping FlutterResult) { diff --git a/flutter/lib/src/default_integrations.dart b/flutter/lib/src/default_integrations.dart index 3fc708dcf4..8a095e9268 100644 --- a/flutter/lib/src/default_integrations.dart +++ b/flutter/lib/src/default_integrations.dart @@ -348,7 +348,8 @@ class NativeSdkIntegration extends Integration { return; } try { - await _channel.invokeMethod('initNativeSdk', { + final result = + await _channel.invokeMethod('initNativeSdk', { 'dsn': options.dsn, 'debug': options.debug, 'environment': options.environment, @@ -375,6 +376,11 @@ class NativeSdkIntegration extends Integration { 'enableAutoPerformanceTracking': options.enableAutoPerformanceTracking, 'sendClientReports': options.sendClientReports, }); + final infos = Map.from(result); + + // set the device id + final deviceId = infos['deviceId'] as String?; + options.distinctId = deviceId; options.sdk.addIntegration('nativeSdkIntegration'); } catch (exception, stackTrace) { diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index 4132397789..8b57b725ff 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -9,6 +9,11 @@ class FileSystemTransport implements Transport { final MethodChannel _channel; final SentryOptions _options; + // late because the configuration callback needs to run first + // before creating the http transport with the dsn + late final HttpTransport _httpTransport = + HttpTransport(_options, RateLimiter(_options)); + @override Future send(SentryEnvelope envelope) async { final envelopeData = []; @@ -30,7 +35,7 @@ class FileSystemTransport implements Transport { return envelope.header.eventId; } - // TODO: implement or fallback to http transport @override - Future?> fetchFeatureFlags() async => null; + Future?> fetchFeatureFlags() async => + _httpTransport.fetchFeatureFlags(); } From b662c62b40830b3531fbebdc923eae886d2ae7c2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 10:52:24 +0200 Subject: [PATCH 17/33] fix get feature flag info --- dart/example/bin/example.dart | 11 +- dart/lib/sentry.dart | 1 + .../src/feature_flags/evaluation_rule.dart | 6 +- dart/lib/src/feature_flags/feature_flag.dart | 3 - .../feature_flags/feature_flag_context.dart | 6 +- .../src/feature_flags/feature_flag_info.dart | 16 +++ dart/lib/src/hub.dart | 11 +- dart/lib/src/hub_adapter.dart | 12 +- dart/lib/src/noop_hub.dart | 8 +- dart/lib/src/noop_sentry_client.dart | 8 +- dart/lib/src/sentry.dart | 12 +- dart/lib/src/sentry_client.dart | 131 ++++++++++++------ dart/lib/src/transport/http_transport.dart | 76 ++++++++-- dart/test/mocks.dart | 1 - dart/test/protocol/rate_limiter_test.dart | 1 - dart/test/transport/http_transport_test.dart | 2 - dart/test_resources/feature_flags.json | 2 + 17 files changed, 225 insertions(+), 82 deletions(-) create mode 100644 dart/lib/src/feature_flags/feature_flag_info.dart diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 61cbcbf147..6527ed785a 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -34,16 +34,21 @@ Future main() async { }); final enabled = await Sentry.isFeatureEnabled( - '@@accessToProfiling', + 'accessToProfiling', defaultValue: false, context: (myContext) => { - myContext.tags['stickyId'] = 'myCustomStickyId', + myContext.tags['isSentryDev'] = 'true', }, ); print(enabled); // TODO: does it return the active EvaluationRule? do we create a new model for that? - final flag = await Sentry.getFeatureFlagInfo('test'); + final flag = await Sentry.getFeatureFlagInfo('accessToProfiling', + context: (myContext) => { + myContext.tags['isSentryDev'] = 'true', + }); + + print(flag?.result ?? 'whaat'); } Future runApp() async {} diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index c35d0ca1d1..c698397736 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -37,3 +37,4 @@ export 'src/feature_flags/feature_flag.dart'; export 'src/feature_flags/evaluation_rule.dart'; export 'src/feature_flags/evaluation_type.dart'; export 'src/feature_flags/feature_flag_context.dart'; +export 'src/feature_flags/feature_flag_info.dart'; diff --git a/dart/lib/src/feature_flags/evaluation_rule.dart b/dart/lib/src/feature_flags/evaluation_rule.dart index 88003e49b9..6af17c51d6 100644 --- a/dart/lib/src/feature_flags/evaluation_rule.dart +++ b/dart/lib/src/feature_flags/evaluation_rule.dart @@ -6,10 +6,10 @@ class EvaluationRule { final EvaluationType type; final double? percentage; final dynamic result; - final Map _tags; + final Map _tags; final Map? _payload; - Map get tags => Map.unmodifiable(_tags); + Map get tags => Map.unmodifiable(_tags); Map? get payload => _payload != null ? Map.unmodifiable(_payload!) : null; @@ -30,7 +30,7 @@ class EvaluationRule { (json['type'] as String).toEvaluationType(), json['percentage'] as double?, json['result'], - tags != null ? Map.from(tags) : {}, + tags != null ? Map.from(tags) : {}, payload != null ? Map.from(payload) : null, ); } diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index 27fe1e5f45..80f5512ca7 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -4,12 +4,9 @@ import 'evaluation_rule.dart'; @immutable class FeatureFlag { - // final Map _tags; final List _evaluations; final String kind; - // Map get tags => Map.unmodifiable(_tags); - List get evaluations => List.unmodifiable(_evaluations); FeatureFlag(this.kind, this._evaluations); diff --git a/dart/lib/src/feature_flags/feature_flag_context.dart b/dart/lib/src/feature_flags/feature_flag_context.dart index ff6f03b767..ee8129b20e 100644 --- a/dart/lib/src/feature_flags/feature_flag_context.dart +++ b/dart/lib/src/feature_flags/feature_flag_context.dart @@ -1,11 +1,7 @@ typedef FeatureFlagContextCallback = void Function(FeatureFlagContext context); class FeatureFlagContext { - // String stickyId; - // String userId; - // String deviceId; - Map tags = {}; + Map tags = {}; - // FeatureFlagContext(this.stickyId, this.userId, this.deviceId, this.tags); FeatureFlagContext(this.tags); } diff --git a/dart/lib/src/feature_flags/feature_flag_info.dart b/dart/lib/src/feature_flags/feature_flag_info.dart new file mode 100644 index 0000000000..8538b0bf40 --- /dev/null +++ b/dart/lib/src/feature_flags/feature_flag_info.dart @@ -0,0 +1,16 @@ +class FeatureFlagInfo { + final dynamic result; + final Map _tags; + final Map? _payload; + + Map get tags => Map.unmodifiable(_tags); + + Map? get payload => + _payload != null ? Map.unmodifiable(_payload!) : null; + + FeatureFlagInfo( + this.result, + this._tags, + this._payload, + ); +} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 362e4665e7..94e403d5d6 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -552,7 +552,10 @@ class Hub { return defaultValue; } - Future getFeatureFlagInfo(String key) async { + Future getFeatureFlagInfo( + String key, { + FeatureFlagContextCallback? context, + }) async { if (!_isEnabled) { _options.logger( SentryLevel.warning, @@ -564,7 +567,11 @@ class Hub { try { final item = _peek(); - return item.client.getFeatureFlagInfo(key); + return item.client.getFeatureFlagInfo( + key, + scope: item.scope, + context: context, + ); } catch (exception, stacktrace) { _options.logger( SentryLevel.error, diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 62753777bc..84213e4752 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; +import 'feature_flags/feature_flag_info.dart'; import 'hub.dart'; import 'protocol.dart'; import 'sentry.dart'; @@ -174,6 +174,12 @@ class HubAdapter implements Hub { ); @override - Future getFeatureFlagInfo(String key) => - Sentry.getFeatureFlagInfo(key); + Future getFeatureFlagInfo( + String key, { + FeatureFlagContextCallback? context, + }) => + Sentry.getFeatureFlagInfo( + key, + context: context, + ); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 7c81f8707d..b743ec43d0 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; +import 'feature_flags/feature_flag_info.dart'; import 'hub.dart'; import 'protocol.dart'; import 'sentry_client.dart'; @@ -126,5 +126,9 @@ class NoOpHub implements Hub { false; @override - Future getFeatureFlagInfo(String key) async => null; + Future getFeatureFlagInfo( + String key, { + FeatureFlagContextCallback? context, + }) async => + null; } diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 9fda5a37ef..87e48a07e1 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; +import 'feature_flags/feature_flag_info.dart'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_client.dart'; @@ -76,5 +77,10 @@ class NoOpSentryClient implements SentryClient { false; @override - Future getFeatureFlagInfo(String key) async => null; + Future getFeatureFlagInfo( + String key, { + Scope? scope, + FeatureFlagContextCallback? context, + }) async => + null; } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index bff08897eb..efaff18535 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -6,8 +6,8 @@ import 'default_integrations.dart'; import 'enricher/enricher_event_processor.dart'; import 'environment/environment_variables.dart'; import 'event_processor/deduplication_event_processor.dart'; -import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; +import 'feature_flags/feature_flag_info.dart'; import 'hub.dart'; import 'hub_adapter.dart'; import 'integration.dart'; @@ -286,6 +286,12 @@ class Sentry { context: context, ); - static Future getFeatureFlagInfo(String key) => - _hub.getFeatureFlagInfo(key); + static Future getFeatureFlagInfo( + String key, { + FeatureFlagContextCallback? context, + }) => + _hub.getFeatureFlagInfo( + key, + context: context, + ); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 76af2cf465..262a0055ea 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -4,9 +4,11 @@ import 'package:meta/meta.dart'; import 'package:uuid/uuid.dart'; import 'event_processor.dart'; +import 'feature_flags/evaluation_rule.dart'; import 'feature_flags/evaluation_type.dart'; import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; +import 'feature_flags/feature_flag_info.dart'; import 'feature_flags/xor_shift_rand.dart'; import 'sentry_user_feedback.dart'; import 'transport/rate_limiter.dart'; @@ -419,6 +421,95 @@ class SentryClient { if (flag == null) { return defaultValue; } + final featureFlagContext = _getFeatureFlagContext(scope, context); + // there's always a stickyId + final stickyId = featureFlagContext.tags['stickyId']!; + + final evaluationRule = + _getEvaluationRuleMatch(flag, featureFlagContext, stickyId); + + return evaluationRule?.result ?? defaultValue; + } + + double _rollRandomNumber(String stickyId) { + final rand = XorShiftRandom(stickyId); + return rand.next(); + } + + bool _matchesTags(Map tags, Map context) { + for (final item in tags.entries) { + if (item.value != context[item.key]) { + return false; + } + } + return true; + } + + Future getFeatureFlagInfo( + String key, { + Scope? scope, + FeatureFlagContextCallback? context, + }) async { + final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); + final featureFlag = featureFlags?[key]; + + if (featureFlag == null) { + return null; + } + + final featureFlagContext = _getFeatureFlagContext(scope, context); + // there's always a stickyId + final stickyId = featureFlagContext.tags['stickyId']!; + final evaluationRule = + _getEvaluationRuleMatch(featureFlag, featureFlagContext, stickyId); + + if (evaluationRule == null) { + return null; + } + final payload = evaluationRule.payload != null + ? Map.from(evaluationRule.payload as Map) + : null; + + return FeatureFlagInfo( + evaluationRule.result, + Map.from(evaluationRule.tags), + payload, + ); + } + + EvaluationRule? _getEvaluationRuleMatch( + FeatureFlag featureFlag, + FeatureFlagContext context, + String stickyId, + ) { + EvaluationRule? ruleMatch; + for (final evalConfig in featureFlag.evaluations) { + if (!_matchesTags(evalConfig.tags, context.tags)) { + continue; + } + + switch (evalConfig.type) { + case EvaluationType.rollout: + final percentage = _rollRandomNumber(stickyId); + if (percentage >= (evalConfig.percentage ?? 0.0)) { + ruleMatch = evalConfig; + } + break; + case EvaluationType.match: + ruleMatch = evalConfig; + break; + default: + break; + } + } + + return ruleMatch; + } + + FeatureFlagContext _getFeatureFlagContext( + Scope? scope, + FeatureFlagContextCallback? context, + ) { final featureFlagContext = FeatureFlagContext({}); // set the device id @@ -459,45 +550,7 @@ class SentryClient { featureFlagContext.tags['stickyId'] = stickyId; } - for (final evalConfig in flag.evaluations) { - if (!_matchesTags(evalConfig.tags, featureFlagContext.tags)) { - continue; - } - - switch (evalConfig.type) { - case EvaluationType.rollout: - final percentage = _rollRandomNumber(stickyId); - if (percentage >= (evalConfig.percentage ?? 0.0)) { - return evalConfig.result; - } - break; - case EvaluationType.match: - return evalConfig.result; - default: - break; - } - } - - return defaultValue; - } - - double _rollRandomNumber(String stickyId) { - final rand = XorShiftRandom(stickyId); - return rand.next(); - } - - bool _matchesTags(Map tags, Map context) { - for (final item in tags.entries) { - if (item.value != context[item.key]) { - return false; - } - } - return true; - } - - Future getFeatureFlagInfo(String key) async { - final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); - return featureFlags?[key]; + return featureFlagContext; } Future?> _getFeatureFlagsFromCacheOrNetwork() async { diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index d96654bad4..57fa28d18f 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -99,23 +99,71 @@ class HttpTransport implements Transport { @override Future?> fetchFeatureFlags() async { - final response = await _options.httpClient.post(_dsn.featureFlagsUri, - headers: _credentialBuilder.configure(_headers)); - - if (response.statusCode != 200) { - // body guard to not log the error as it has performance impact to allocate - // the body String. - if (_options.debug) { - _options.logger( - SentryLevel.error, - 'API returned an error, statusCode = ${response.statusCode}, ' - 'body = ${response.body}', - ); + // TODO: handle rate limiting, client reports, etc... + + // final response = await _options.httpClient.post(_dsn.featureFlagsUri, + // headers: _credentialBuilder.configure(_headers)); + + // if (response.statusCode != 200) { + // if (_options.debug) { + // _options.logger( + // SentryLevel.error, + // 'API returned an error, statusCode = ${response.statusCode}, ' + // 'body = ${response.body}', + // ); + // } + // return null; + // } + + // final responseJson = json.decode(response.body); + + final responseJson = json.decode('''{ + "feature_flags": { + "accessToProfiling": { + "kind": "bool", + "evaluation": [ + { + "type": "rollout", + "percentage": 0.5, + "result": true, + "tags": { + "userSegment": "slow" + }, + "payload": null + }, + { + "type": "match", + "result": true, + "tags": { + "isSentryDev": "true" + }, + "payload": { + "background_image": "https://example.com/modus1.png" + } + } + ] + }, + "profilingEnabled": { + "kind": "bool", + "evaluation": [ + { + "type": "rollout", + "percentage": 0.05, + "result": true, + "tags": { + "isSentryDev": "true" + } + }, + { + "type": "match", + "result": true + } + ] + } } - return null; } + '''); - final responseJson = json.decode(response.body); return FeatureDump.fromJson(responseJson).featureFlags; } diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index cb36172d06..cab459541b 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; -import 'package:sentry/src/transport/rate_limiter.dart'; final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; diff --git a/dart/test/protocol/rate_limiter_test.dart b/dart/test/protocol/rate_limiter_test.dart index e95dd1afe4..a51545fd53 100644 --- a/dart/test/protocol/rate_limiter_test.dart +++ b/dart/test/protocol/rate_limiter_test.dart @@ -3,7 +3,6 @@ import 'package:sentry/src/client_reports/discard_reason.dart'; import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; -import 'package:sentry/src/transport/rate_limiter.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/sentry_envelope_header.dart'; diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 5b95c7e2d4..164c235efe 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -8,12 +8,10 @@ import 'package:sentry/src/sentry_envelope_header.dart'; import 'package:sentry/src/sentry_envelope_item_header.dart'; import 'package:sentry/src/sentry_item_type.dart'; import 'package:sentry/src/transport/data_category.dart'; -import 'package:sentry/src/transport/rate_limiter.dart'; import 'package:test/test.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/sentry.dart'; -import 'package:sentry/src/transport/http_transport.dart'; import '../mocks.dart'; import '../mocks/mock_client_report_recorder.dart'; diff --git a/dart/test_resources/feature_flags.json b/dart/test_resources/feature_flags.json index c006edd749..ec435fedee 100644 --- a/dart/test_resources/feature_flags.json +++ b/dart/test_resources/feature_flags.json @@ -1,6 +1,7 @@ { "feature_flags": { "accessToProfiling": { + "kind": "bool", "evaluation": [ { "type": "rollout", @@ -24,6 +25,7 @@ ] }, "profilingEnabled": { + "kind": "bool", "evaluation": [ { "type": "rollout", From 4de969b665f85e3412abcb9892cfee1df4b9dff3 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 12:24:30 +0200 Subject: [PATCH 18/33] fixes --- dart/example/bin/example.dart | 33 ++---- dart/lib/src/hub.dart | 8 +- dart/lib/src/hub_adapter.dart | 4 +- dart/lib/src/noop_hub.dart | 2 +- dart/lib/src/noop_sentry_client.dart | 2 +- dart/lib/src/sentry.dart | 6 +- dart/lib/src/sentry_client.dart | 38 +++++-- dart/lib/src/transport/http_transport.dart | 118 +++++++++++++-------- dart/lib/src/transport/transport.dart | 3 + dart/test/sentry_test.dart | 5 - flutter/example/lib/main.dart | 2 +- 11 files changed, 130 insertions(+), 91 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 6527ed785a..aa53ddb0cc 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -31,43 +31,26 @@ Future main() async { id: '800', ), ); + // await scope.setTag('isSentryDev', 'true'); }); - final enabled = await Sentry.isFeatureEnabled( - 'accessToProfiling', + final enabled = await Sentry.isFeatureFlagEnabled( + 'tracesSampleRate', defaultValue: false, context: (myContext) => { - myContext.tags['isSentryDev'] = 'true', + // myContext.tags['userSegment'] = 'slow', }, ); print(enabled); // TODO: does it return the active EvaluationRule? do we create a new model for that? - final flag = await Sentry.getFeatureFlagInfo('accessToProfiling', + final flag = await Sentry.getFeatureFlagInfo('tracesSampleRate', context: (myContext) => { - myContext.tags['isSentryDev'] = 'true', + myContext.tags['myCustomTag'] = 'true', }); - print(flag?.result ?? 'whaat'); + // print(flag?.payload?['internal_setting'] ?? 'whaat'); + print(flag?.payload ?? {}); } Future runApp() async {} - -Future loadConfig() async { - await parseConfig(); -} - -Future parseConfig() async { - await decode(); -} - -Future decode() async { - throw StateError('This is a test error'); -} - -class TagEventProcessor extends EventProcessor { - @override - FutureOr apply(SentryEvent event, {hint}) { - return event..tags?.addAll({'page-locale': 'en-us'}); - } -} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 94e403d5d6..6b493eadf2 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -519,7 +519,8 @@ class Hub { return event; } - Future isFeatureEnabled( + @experimental + Future isFeatureFlagEnabled( String key, { bool defaultValue = false, FeatureFlagContextCallback? context, @@ -527,7 +528,7 @@ class Hub { if (!_isEnabled) { _options.logger( SentryLevel.warning, - "Instance is disabled and this 'isFeatureEnabled' call is a no-op.", + "Instance is disabled and this 'isFeatureFlagEnabled' call is a no-op.", ); return defaultValue; } @@ -535,7 +536,7 @@ class Hub { try { final item = _peek(); - return item.client.isFeatureEnabled( + return item.client.isFeatureFlagEnabled( key, scope: item.scope, defaultValue: defaultValue, @@ -552,6 +553,7 @@ class Hub { return defaultValue; } + @experimental Future getFeatureFlagInfo( String key, { FeatureFlagContextCallback? context, diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 84213e4752..ae03cfe25d 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -162,12 +162,12 @@ class HubAdapter implements Hub { Sentry.currentHub.setSpanContext(throwable, span, transaction); @override - Future isFeatureEnabled( + Future isFeatureFlagEnabled( String key, { bool defaultValue = false, FeatureFlagContextCallback? context, }) => - Sentry.isFeatureEnabled( + Sentry.isFeatureFlagEnabled( key, defaultValue: defaultValue, context: context, diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index b743ec43d0..bfd97f8652 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -118,7 +118,7 @@ class NoOpHub implements Hub { void setSpanContext(throwable, ISentrySpan span, String transaction) {} @override - Future isFeatureEnabled( + Future isFeatureFlagEnabled( String key, { bool defaultValue = false, FeatureFlagContextCallback? context, diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 87e48a07e1..10f2c605c7 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -68,7 +68,7 @@ class NoOpSentryClient implements SentryClient { Future?> fetchFeatureFlags() async => null; @override - Future isFeatureEnabled( + Future isFeatureFlagEnabled( String key, { Scope? scope, bool defaultValue = false, diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index efaff18535..2edb077875 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -275,17 +275,19 @@ class Sentry { @internal static Hub get currentHub => _hub; - static Future isFeatureEnabled( + @experimental + static Future isFeatureFlagEnabled( String key, { bool defaultValue = false, FeatureFlagContextCallback? context, }) => - _hub.isFeatureEnabled( + _hub.isFeatureFlagEnabled( key, defaultValue: defaultValue, context: context, ); + @experimental static Future getFeatureFlagInfo( String key, { FeatureFlagContextCallback? context, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 262a0055ea..c20bf1de69 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -409,7 +409,8 @@ class SentryClient { Future?> fetchFeatureFlags() => _options.transport.fetchFeatureFlags(); - Future isFeatureEnabled( + @experimental + Future isFeatureFlagEnabled( String key, { Scope? scope, bool defaultValue = false, @@ -428,7 +429,26 @@ class SentryClient { final evaluationRule = _getEvaluationRuleMatch(flag, featureFlagContext, stickyId); - return evaluationRule?.result ?? defaultValue; + final resultType = + _checkResultType(evaluationRule, flag.kind, defaultValue); + return resultType ? true : defaultValue; + } + + bool _checkResultType(EvaluationRule? rule, String kind, bool defaultValue) { + if (rule == null) { + return defaultValue; + } + + switch (kind) { + case 'bool': + return rule.result is bool ? true : defaultValue; + case 'string': + return rule.result is String ? true : defaultValue; + case 'number': + return rule.result is num ? true : defaultValue; + default: + return defaultValue; + } } double _rollRandomNumber(String stickyId) { @@ -445,6 +465,7 @@ class SentryClient { return true; } + @experimental Future getFeatureFlagInfo( String key, { Scope? scope, @@ -482,7 +503,6 @@ class SentryClient { FeatureFlagContext context, String stickyId, ) { - EvaluationRule? ruleMatch; for (final evalConfig in featureFlag.evaluations) { if (!_matchesTags(evalConfig.tags, context.tags)) { continue; @@ -491,19 +511,18 @@ class SentryClient { switch (evalConfig.type) { case EvaluationType.rollout: final percentage = _rollRandomNumber(stickyId); - if (percentage >= (evalConfig.percentage ?? 0.0)) { - ruleMatch = evalConfig; + if (percentage < (evalConfig.percentage ?? 0.0)) { + return evalConfig; } break; case EvaluationType.match: - ruleMatch = evalConfig; - break; + return evalConfig; default: break; } } - return ruleMatch; + return null; } FeatureFlagContext _getFeatureFlagContext( @@ -534,6 +553,9 @@ class SentryClient { featureFlagContext.tags['environment'] = environment; } + // set all the tags from the scope as well + featureFlagContext.tags.addAll(scope?.tags ?? {}); + // run feature flag context callback and allow user adding/removing tags if (context != null) { context(featureFlagContext); diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 57fa28d18f..c7a46d080f 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -117,52 +117,84 @@ class HttpTransport implements Transport { // final responseJson = json.decode(response.body); - final responseJson = json.decode('''{ - "feature_flags": { - "accessToProfiling": { - "kind": "bool", - "evaluation": [ - { - "type": "rollout", - "percentage": 0.5, - "result": true, - "tags": { - "userSegment": "slow" - }, - "payload": null - }, - { - "type": "match", - "result": true, - "tags": { - "isSentryDev": "true" - }, - "payload": { - "background_image": "https://example.com/modus1.png" - } - } - ] + final responseJson = json.decode(''' +{ + "feature_flags": { + "accessToProfiling": { + "kind": "bool", + "evaluation": [ + { + "type": "rollout", + "percentage": 0.5, + "result": true, + "tags": { + "userSegment": "slow" + } }, - "profilingEnabled": { - "kind": "bool", - "evaluation": [ - { - "type": "rollout", - "percentage": 0.05, - "result": true, - "tags": { - "isSentryDev": "true" - } - }, - { - "type": "match", - "result": true - } - ] + { + "type": "match", + "result": true, + "tags": { + "isSentryDev": "true" + }, + "payload": { + "internal_setting": "wat" + } } - } + ] + }, + "profilingEnabled": { + "kind": "bool", + "evaluation": [ + { + "type": "match", + "result": true, + "tags": { + "isSentryDev": "true" + } + }, + { + "type": "rollout", + "percentage": 0.05, + "result": true + } + ] + }, + "loginBanner": { + "kind": "string", + "evaluation": [ + { + "type": "match", + "result": "banner1", + "tags": { + "isSentryDev": "true" + }, + "payload": { + "imageUrl": "https://mycdn.com/banner1.png" + } + }, + { + "type": "match", + "result": "banner2", + "payload": { + "imageUrl": "https://mycdn.com/banner2.png" + } + } + ] + }, + "tracesSampleRate": { + "kind": "number", + "evaluation": [ + { + "type": "match", + "result": 0.25 + } + ] } - '''); + + } +} + '''); return FeatureDump.fromJson(responseJson).featureFlags; } diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index f0533acd31..746605c78c 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:meta/meta.dart'; + import '../feature_flags/feature_flag.dart'; import '../sentry_envelope.dart'; import '../protocol.dart'; @@ -9,5 +11,6 @@ import '../protocol.dart'; abstract class Transport { Future send(SentryEnvelope envelope); + @experimental Future?> fetchFeatureFlags(); } diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 12a6f24724..989f63a58e 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -182,11 +182,6 @@ void main() { group('Sentry init', () { tearDown(() async { await Sentry.close(); - - await Sentry.isFeatureEnabled('test', - context: (myContext) => { - myContext.tags['something'] = 'true', - }); }); test('should install integrations', () async { diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index de53dd3c1c..a606ce6c75 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -411,7 +411,7 @@ class AndroidExample extends StatelessWidget { ); }); - final enabled = await Sentry.isFeatureEnabled( + final enabled = await Sentry.isFeatureFlagEnabled( '@@accessToProfiling', defaultValue: false, context: (myContext) => { From eeab0aaea875277b6f67b87a7b6a64c50232efc5 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 13:14:16 +0200 Subject: [PATCH 19/33] add method with generics --- dart/example/bin/example.dart | 73 ++++++++++++++++++++++++---- dart/lib/src/hub.dart | 22 +++++++-- dart/lib/src/hub_adapter.dart | 19 ++++++-- dart/lib/src/noop_hub.dart | 15 ++++-- dart/lib/src/noop_sentry_client.dart | 15 ++++-- dart/lib/src/sentry.dart | 15 +++++- dart/lib/src/sentry_client.dart | 42 +++++++++++----- 7 files changed, 162 insertions(+), 39 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index aa53ddb0cc..4fa6e52caa 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -34,23 +34,76 @@ Future main() async { // await scope.setTag('isSentryDev', 'true'); }); - final enabled = await Sentry.isFeatureFlagEnabled( - 'tracesSampleRate', + // accessToProfilingRollout + final accessToProfilingRollout = await Sentry.isFeatureFlagEnabled( + 'accessToProfiling', + defaultValue: false, + context: (myContext) => { + myContext.tags['userSegment'] = 'slow', + }, + ); + print( + 'accessToProfilingRollout $accessToProfilingRollout'); // false for user 800 + + // accessToProfilingMatch + final accessToProfilingMatch = await Sentry.isFeatureFlagEnabled( + 'accessToProfiling', defaultValue: false, context: (myContext) => { - // myContext.tags['userSegment'] = 'slow', + myContext.tags['isSentryDev'] = 'true', }, ); - print(enabled); + print('accessToProfilingMatch $accessToProfilingMatch'); // returns true + + // profilingEnabledMatch + final profilingEnabledMatch = await Sentry.isFeatureFlagEnabled( + 'profilingEnabled', + defaultValue: false, + context: (myContext) => { + myContext.tags['isSentryDev'] = 'true', + }, + ); + print('profilingEnabledMatch $profilingEnabledMatch'); // returns true + + // profilingEnabledRollout + final profilingEnabledRollout = await Sentry.isFeatureFlagEnabled( + 'profilingEnabled', + defaultValue: false, + ); + print( + 'profilingEnabledRollout $profilingEnabledRollout'); // false for user 800 + + // loginBannerMatch + final loginBannerMatch = await Sentry.getFeatureFlagValue( + 'loginBanner', + defaultValue: 'banner0', + context: (myContext) => { + myContext.tags['isSentryDev'] = 'true', + }, + ); + print('loginBannerMatch $loginBannerMatch'); // returns banner1 + + // loginBannerMatch2 + final loginBannerMatch2 = await Sentry.getFeatureFlagValue( + 'loginBanner', + defaultValue: 'banner0', + ); + print('loginBannerMatch2 $loginBannerMatch2'); // returns banner2 + + // tracesSampleRate + final tracesSampleRate = await Sentry.getFeatureFlagValue( + 'tracesSampleRate', + defaultValue: 0.0, + ); + print('tracesSampleRate $tracesSampleRate'); // returns 0.25 - // TODO: does it return the active EvaluationRule? do we create a new model for that? - final flag = await Sentry.getFeatureFlagInfo('tracesSampleRate', - context: (myContext) => { - myContext.tags['myCustomTag'] = 'true', - }); + // final flag = await Sentry.getFeatureFlagInfo('loginBanner', + // context: (myContext) => { + // myContext.tags['myCustomTag'] = 'true', + // }); // print(flag?.payload?['internal_setting'] ?? 'whaat'); - print(flag?.payload ?? {}); + // print(flag?.payload ?? {}); } Future runApp() async {} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 6b493eadf2..3710b4f739 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -519,16 +519,30 @@ class Hub { return event; } + // @experimental + // Future isFeatureFlagEnabled( + // String key, { + // bool defaultValue = false, + // FeatureFlagContextCallback? context, + // }) async { + // return await getFeatureFlagValue( + // key, + // defaultValue: defaultValue, + // context: context, + // ) ?? + // defaultValue; + // } + @experimental - Future isFeatureFlagEnabled( + Future getFeatureFlagValue( String key, { - bool defaultValue = false, + T? defaultValue, FeatureFlagContextCallback? context, }) async { if (!_isEnabled) { _options.logger( SentryLevel.warning, - "Instance is disabled and this 'isFeatureFlagEnabled' call is a no-op.", + "Instance is disabled and this 'getFeatureFlag' call is a no-op.", ); return defaultValue; } @@ -536,7 +550,7 @@ class Hub { try { final item = _peek(); - return item.client.isFeatureFlagEnabled( + return item.client.getFeatureFlagValue( key, scope: item.scope, defaultValue: defaultValue, diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index ae03cfe25d..8a25131b71 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -161,13 +161,24 @@ class HubAdapter implements Hub { ) => Sentry.currentHub.setSpanContext(throwable, span, transaction); - @override - Future isFeatureFlagEnabled( + // @override + // Future isFeatureFlagEnabled( + // String key, { + // bool defaultValue = false, + // FeatureFlagContextCallback? context, + // }) => + // Sentry.isFeatureFlagEnabled( + // key, + // defaultValue: defaultValue, + // context: context, + // ); + + Future getFeatureFlagValue( String key, { - bool defaultValue = false, + T? defaultValue, FeatureFlagContextCallback? context, }) => - Sentry.isFeatureFlagEnabled( + Sentry.getFeatureFlagValue( key, defaultValue: defaultValue, context: context, diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index bfd97f8652..d64f1dca32 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -117,13 +117,20 @@ class NoOpHub implements Hub { @override void setSpanContext(throwable, ISentrySpan span, String transaction) {} - @override - Future isFeatureFlagEnabled( + // @override + // Future isFeatureFlagEnabled( + // String key, { + // bool defaultValue = false, + // FeatureFlagContextCallback? context, + // }) async => + // false; + + Future getFeatureFlagValue( String key, { - bool defaultValue = false, + T? defaultValue, FeatureFlagContextCallback? context, }) async => - false; + null; @override Future getFeatureFlagInfo( diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 10f2c605c7..fd849b0df6 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -67,14 +67,23 @@ class NoOpSentryClient implements SentryClient { @override Future?> fetchFeatureFlags() async => null; + // @override + // Future isFeatureFlagEnabled( + // String key, { + // Scope? scope, + // bool defaultValue = false, + // FeatureFlagContextCallback? context, + // }) async => + // false; + @override - Future isFeatureFlagEnabled( + Future getFeatureFlagValue( String key, { Scope? scope, - bool defaultValue = false, + T? defaultValue, FeatureFlagContextCallback? context, }) async => - false; + null; @override Future getFeatureFlagInfo( diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 2edb077875..71b98f65ca 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -280,8 +280,21 @@ class Sentry { String key, { bool defaultValue = false, FeatureFlagContextCallback? context, + }) async { + return await getFeatureFlagValue( + key, + defaultValue: defaultValue, + context: context, + ) ?? + defaultValue; + } + + static Future getFeatureFlagValue( + String key, { + T? defaultValue, + FeatureFlagContextCallback? context, }) => - _hub.isFeatureFlagEnabled( + _hub.getFeatureFlagValue( key, defaultValue: defaultValue, context: context, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index c20bf1de69..71a1e9f60d 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -410,10 +410,10 @@ class SentryClient { _options.transport.fetchFeatureFlags(); @experimental - Future isFeatureFlagEnabled( + Future getFeatureFlagValue( String key, { Scope? scope, - bool defaultValue = false, + T? defaultValue, FeatureFlagContextCallback? context, }) async { final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); @@ -429,25 +429,41 @@ class SentryClient { final evaluationRule = _getEvaluationRuleMatch(flag, featureFlagContext, stickyId); - final resultType = - _checkResultType(evaluationRule, flag.kind, defaultValue); - return resultType ? true : defaultValue; - } - - bool _checkResultType(EvaluationRule? rule, String kind, bool defaultValue) { - if (rule == null) { + if (evaluationRule == null) { return defaultValue; } + final resultType = _checkResultType(evaluationRule, flag.kind); + + return resultType ? evaluationRule.result as T : defaultValue; + } + + // @experimental + // Future isFeatureFlagEnabled( + // String key, { + // Scope? scope, + // bool defaultValue = false, + // FeatureFlagContextCallback? context, + // }) async { + // return await getFeatureFlagValue( + // key, + // scope: scope, + // defaultValue: defaultValue, + // context: context, + // ) ?? + // defaultValue; + // } + + bool _checkResultType(EvaluationRule rule, String kind) { switch (kind) { case 'bool': - return rule.result is bool ? true : defaultValue; + return rule.result is bool && rule.result is T ? true : false; case 'string': - return rule.result is String ? true : defaultValue; + return rule.result is String && rule.result is T ? true : false; case 'number': - return rule.result is num ? true : defaultValue; + return rule.result is num && rule.result is T ? true : false; default: - return defaultValue; + return false; } } From c0b5c8044c6976efee4a145a6aff8dd0c0e70c4a Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 13:15:44 +0200 Subject: [PATCH 20/33] remove non used code --- dart/lib/src/hub.dart | 14 -------------- dart/lib/src/hub_adapter.dart | 13 +------------ dart/lib/src/noop_hub.dart | 9 +-------- dart/lib/src/noop_sentry_client.dart | 9 --------- dart/lib/src/sentry_client.dart | 16 ---------------- 5 files changed, 2 insertions(+), 59 deletions(-) diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 3710b4f739..b00397b963 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -519,20 +519,6 @@ class Hub { return event; } - // @experimental - // Future isFeatureFlagEnabled( - // String key, { - // bool defaultValue = false, - // FeatureFlagContextCallback? context, - // }) async { - // return await getFeatureFlagValue( - // key, - // defaultValue: defaultValue, - // context: context, - // ) ?? - // defaultValue; - // } - @experimental Future getFeatureFlagValue( String key, { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 8a25131b71..ed255dfa5d 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -161,18 +161,7 @@ class HubAdapter implements Hub { ) => Sentry.currentHub.setSpanContext(throwable, span, transaction); - // @override - // Future isFeatureFlagEnabled( - // String key, { - // bool defaultValue = false, - // FeatureFlagContextCallback? context, - // }) => - // Sentry.isFeatureFlagEnabled( - // key, - // defaultValue: defaultValue, - // context: context, - // ); - + @override Future getFeatureFlagValue( String key, { T? defaultValue, diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index d64f1dca32..a703b54f05 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -117,14 +117,7 @@ class NoOpHub implements Hub { @override void setSpanContext(throwable, ISentrySpan span, String transaction) {} - // @override - // Future isFeatureFlagEnabled( - // String key, { - // bool defaultValue = false, - // FeatureFlagContextCallback? context, - // }) async => - // false; - + @override Future getFeatureFlagValue( String key, { T? defaultValue, diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index fd849b0df6..47914f7462 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -67,15 +67,6 @@ class NoOpSentryClient implements SentryClient { @override Future?> fetchFeatureFlags() async => null; - // @override - // Future isFeatureFlagEnabled( - // String key, { - // Scope? scope, - // bool defaultValue = false, - // FeatureFlagContextCallback? context, - // }) async => - // false; - @override Future getFeatureFlagValue( String key, { diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 71a1e9f60d..f94c80992f 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -438,22 +438,6 @@ class SentryClient { return resultType ? evaluationRule.result as T : defaultValue; } - // @experimental - // Future isFeatureFlagEnabled( - // String key, { - // Scope? scope, - // bool defaultValue = false, - // FeatureFlagContextCallback? context, - // }) async { - // return await getFeatureFlagValue( - // key, - // scope: scope, - // defaultValue: defaultValue, - // context: context, - // ) ?? - // defaultValue; - // } - bool _checkResultType(EvaluationRule rule, String kind) { switch (kind) { case 'bool': From 9226546f749888f028e2e71b8bf7b888b2b1683d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 13:20:38 +0200 Subject: [PATCH 21/33] revert mocked transport --- dart/lib/src/transport/http_transport.dart | 184 ++++++++++----------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index c7a46d080f..65403a3814 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -101,100 +101,100 @@ class HttpTransport implements Transport { Future?> fetchFeatureFlags() async { // TODO: handle rate limiting, client reports, etc... - // final response = await _options.httpClient.post(_dsn.featureFlagsUri, - // headers: _credentialBuilder.configure(_headers)); - - // if (response.statusCode != 200) { - // if (_options.debug) { - // _options.logger( - // SentryLevel.error, - // 'API returned an error, statusCode = ${response.statusCode}, ' - // 'body = ${response.body}', - // ); - // } - // return null; - // } - - // final responseJson = json.decode(response.body); - - final responseJson = json.decode(''' -{ - "feature_flags": { - "accessToProfiling": { - "kind": "bool", - "evaluation": [ - { - "type": "rollout", - "percentage": 0.5, - "result": true, - "tags": { - "userSegment": "slow" - } - }, - { - "type": "match", - "result": true, - "tags": { - "isSentryDev": "true" - }, - "payload": { - "internal_setting": "wat" - } - } - ] - }, - "profilingEnabled": { - "kind": "bool", - "evaluation": [ - { - "type": "match", - "result": true, - "tags": { - "isSentryDev": "true" - } - }, - { - "type": "rollout", - "percentage": 0.05, - "result": true - } - ] - }, - "loginBanner": { - "kind": "string", - "evaluation": [ - { - "type": "match", - "result": "banner1", - "tags": { - "isSentryDev": "true" - }, - "payload": { - "imageUrl": "https://mycdn.com/banner1.png" - } - }, - { - "type": "match", - "result": "banner2", - "payload": { - "imageUrl": "https://mycdn.com/banner2.png" - } - } - ] - }, - "tracesSampleRate": { - "kind": "number", - "evaluation": [ - { - "type": "match", - "result": 0.25 - } - ] + final response = await _options.httpClient.post(_dsn.featureFlagsUri, + headers: _credentialBuilder.configure(_headers)); + + if (response.statusCode != 200) { + if (_options.debug) { + _options.logger( + SentryLevel.error, + 'API returned an error, statusCode = ${response.statusCode}, ' + 'body = ${response.body}', + ); + } + return null; } - } -} - '''); + final responseJson = json.decode(response.body); + +// final responseJson = json.decode(''' +// { +// "feature_flags": { +// "accessToProfiling": { +// "kind": "bool", +// "evaluation": [ +// { +// "type": "rollout", +// "percentage": 0.5, +// "result": true, +// "tags": { +// "userSegment": "slow" +// } +// }, +// { +// "type": "match", +// "result": true, +// "tags": { +// "isSentryDev": "true" +// }, +// "payload": { +// "internal_setting": "wat" +// } +// } +// ] +// }, +// "profilingEnabled": { +// "kind": "bool", +// "evaluation": [ +// { +// "type": "match", +// "result": true, +// "tags": { +// "isSentryDev": "true" +// } +// }, +// { +// "type": "rollout", +// "percentage": 0.05, +// "result": true +// } +// ] +// }, +// "loginBanner": { +// "kind": "string", +// "evaluation": [ +// { +// "type": "match", +// "result": "banner1", +// "tags": { +// "isSentryDev": "true" +// }, +// "payload": { +// "imageUrl": "https://mycdn.com/banner1.png" +// } +// }, +// { +// "type": "match", +// "result": "banner2", +// "payload": { +// "imageUrl": "https://mycdn.com/banner2.png" +// } +// } +// ] +// }, +// "tracesSampleRate": { +// "kind": "number", +// "evaluation": [ +// { +// "type": "match", +// "result": 0.25 +// } +// ] +// } + +// } +// } +// '''); return FeatureDump.fromJson(responseJson).featureFlags; } From 4f8507cb02235a2dddb37a5d1576526c17a6f356 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 13:27:40 +0200 Subject: [PATCH 22/33] ref --- dart/lib/src/sentry_client.dart | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index f94c80992f..83b1c8ccf5 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -423,29 +423,27 @@ class SentryClient { return defaultValue; } final featureFlagContext = _getFeatureFlagContext(scope, context); - // there's always a stickyId - final stickyId = featureFlagContext.tags['stickyId']!; final evaluationRule = - _getEvaluationRuleMatch(flag, featureFlagContext, stickyId); + _getEvaluationRuleMatch(flag.evaluations, featureFlagContext); if (evaluationRule == null) { return defaultValue; } - final resultType = _checkResultType(evaluationRule, flag.kind); + final resultType = _checkResultType(evaluationRule.result, flag.kind); return resultType ? evaluationRule.result as T : defaultValue; } - bool _checkResultType(EvaluationRule rule, String kind) { + bool _checkResultType(dynamic result, String kind) { switch (kind) { case 'bool': - return rule.result is bool && rule.result is T ? true : false; + return result is bool && result is T ? true : false; case 'string': - return rule.result is String && rule.result is T ? true : false; + return result is String && result is T ? true : false; case 'number': - return rule.result is num && rule.result is T ? true : false; + return result is num && result is T ? true : false; default: return false; } @@ -456,9 +454,10 @@ class SentryClient { return rand.next(); } - bool _matchesTags(Map tags, Map context) { + bool _matchesTags( + Map tags, Map contextTags) { for (final item in tags.entries) { - if (item.value != context[item.key]) { + if (item.value != contextTags[item.key]) { return false; } } @@ -479,10 +478,9 @@ class SentryClient { } final featureFlagContext = _getFeatureFlagContext(scope, context); - // there's always a stickyId - final stickyId = featureFlagContext.tags['stickyId']!; + final evaluationRule = - _getEvaluationRuleMatch(featureFlag, featureFlagContext, stickyId); + _getEvaluationRuleMatch(featureFlag.evaluations, featureFlagContext); if (evaluationRule == null) { return null; @@ -499,11 +497,13 @@ class SentryClient { } EvaluationRule? _getEvaluationRuleMatch( - FeatureFlag featureFlag, + List evaluations, FeatureFlagContext context, - String stickyId, ) { - for (final evalConfig in featureFlag.evaluations) { + // there's always a stickyId + final stickyId = context.tags['stickyId']!; + + for (final evalConfig in evaluations) { if (!_matchesTags(evalConfig.tags, context.tags)) { continue; } From f79435078b0cb53e0c0ac8d6ecdcb489b72cf5f4 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 13:57:56 +0200 Subject: [PATCH 23/33] fix type check --- dart/example/bin/example.dart | 2 +- .../src/feature_flags/evaluation_rule.dart | 4 ++-- dart/lib/src/feature_flags/feature_flag.dart | 8 +++---- .../src/feature_flags/feature_flag_info.dart | 4 ++-- dart/lib/src/sentry_client.dart | 23 ++++++++----------- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 4fa6e52caa..3de6f86cc2 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -15,7 +15,7 @@ Future main() async { // const dsn = // 'https://9934c532bf8446ef961450973c898537@o447951.ingest.sentry.io/5428562'; const dsn = - 'https://60d3409215134fd1a60765f2400b6b38@ac75-72-74-53-151.ngrok.io/1'; + 'https://fe85fc5123d44d5c99202d9e8f09d52e@395f015cf6c1.eu.ngrok.io/2'; await Sentry.init( (options) => options diff --git a/dart/lib/src/feature_flags/evaluation_rule.dart b/dart/lib/src/feature_flags/evaluation_rule.dart index 6af17c51d6..c320649142 100644 --- a/dart/lib/src/feature_flags/evaluation_rule.dart +++ b/dart/lib/src/feature_flags/evaluation_rule.dart @@ -6,10 +6,10 @@ class EvaluationRule { final EvaluationType type; final double? percentage; final dynamic result; - final Map _tags; + final Map _tags; final Map? _payload; - Map get tags => Map.unmodifiable(_tags); + Map get tags => Map.unmodifiable(_tags); Map? get payload => _payload != null ? Map.unmodifiable(_payload!) : null; diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index 80f5512ca7..2e14e0b711 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -5,21 +5,21 @@ import 'evaluation_rule.dart'; @immutable class FeatureFlag { final List _evaluations; - final String kind; + // final String kind; List get evaluations => List.unmodifiable(_evaluations); - FeatureFlag(this.kind, this._evaluations); + FeatureFlag(this._evaluations); factory FeatureFlag.fromJson(Map json) { - final kind = json['kind']; + // final kind = json['kind']; final evaluationsList = json['evaluation'] as List? ?? []; final evaluations = evaluationsList .map((e) => EvaluationRule.fromJson(e)) .toList(growable: false); return FeatureFlag( - kind, + // kind, evaluations, ); } diff --git a/dart/lib/src/feature_flags/feature_flag_info.dart b/dart/lib/src/feature_flags/feature_flag_info.dart index 8538b0bf40..23cac17c0e 100644 --- a/dart/lib/src/feature_flags/feature_flag_info.dart +++ b/dart/lib/src/feature_flags/feature_flag_info.dart @@ -1,9 +1,9 @@ class FeatureFlagInfo { final dynamic result; - final Map _tags; + final Map _tags; final Map? _payload; - Map get tags => Map.unmodifiable(_tags); + Map get tags => Map.unmodifiable(_tags); Map? get payload => _payload != null ? Map.unmodifiable(_payload!) : null; diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 83b1c8ccf5..5d105ffce5 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -431,22 +431,13 @@ class SentryClient { return defaultValue; } - final resultType = _checkResultType(evaluationRule.result, flag.kind); + final resultType = _checkResultType(evaluationRule.result); return resultType ? evaluationRule.result as T : defaultValue; } - bool _checkResultType(dynamic result, String kind) { - switch (kind) { - case 'bool': - return result is bool && result is T ? true : false; - case 'string': - return result is String && result is T ? true : false; - case 'number': - return result is num && result is T ? true : false; - default: - return false; - } + bool _checkResultType(dynamic result) { + return result is T ? true : false; } double _rollRandomNumber(String stickyId) { @@ -455,9 +446,13 @@ class SentryClient { } bool _matchesTags( - Map tags, Map contextTags) { + Map tags, Map contextTags) { for (final item in tags.entries) { - if (item.value != contextTags[item.key]) { + if (item.value is List) { + if (!(item.value as List).contains(contextTags[item.key])) { + return false; + } + } else if (item.value != contextTags[item.key]) { return false; } } From 5f7505a562e3de99d69d19a674b8b7d80da475cc Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 24 Aug 2022 17:59:22 +0200 Subject: [PATCH 24/33] read traces sample rate and error traces rate automatically --- dart/example/bin/example.dart | 19 ++- dart/lib/src/hub.dart | 49 +++++- dart/lib/src/hub_adapter.dart | 14 +- dart/lib/src/noop_hub.dart | 10 +- dart/lib/src/noop_sentry_client.dart | 13 +- dart/lib/src/sentry.dart | 17 ++- dart/lib/src/sentry_client.dart | 85 +++++++++-- dart/lib/src/sentry_options.dart | 5 + dart/lib/src/sentry_traces_sampler.dart | 3 +- dart/lib/src/transport/http_transport.dart | 165 +++++++++++---------- dart/test/sentry_traces_sampler_test.dart | 10 +- 11 files changed, 268 insertions(+), 122 deletions(-) diff --git a/dart/example/bin/example.dart b/dart/example/bin/example.dart index 3de6f86cc2..449ed1cde0 100644 --- a/dart/example/bin/example.dart +++ b/dart/example/bin/example.dart @@ -18,13 +18,16 @@ Future main() async { 'https://fe85fc5123d44d5c99202d9e8f09d52e@395f015cf6c1.eu.ngrok.io/2'; await Sentry.init( - (options) => options - ..dsn = dsn - ..debug = true - ..release = 'myapp@1.0.0+1' - ..environment = 'prod', + (options) { + options.dsn = dsn; + options.debug = true; + options.release = 'myapp@1.0.0+1'; + options.environment = 'prod'; + options.experimental['featureFlagsEnabled'] = true; + }, appRunner: runApp, ); + await Sentry.configureScope((scope) async { await scope.setUser( SentryUser( @@ -74,7 +77,7 @@ Future main() async { 'profilingEnabledRollout $profilingEnabledRollout'); // false for user 800 // loginBannerMatch - final loginBannerMatch = await Sentry.getFeatureFlagValue( + final loginBannerMatch = await Sentry.getFeatureFlagValueAsync( 'loginBanner', defaultValue: 'banner0', context: (myContext) => { @@ -84,14 +87,14 @@ Future main() async { print('loginBannerMatch $loginBannerMatch'); // returns banner1 // loginBannerMatch2 - final loginBannerMatch2 = await Sentry.getFeatureFlagValue( + final loginBannerMatch2 = await Sentry.getFeatureFlagValueAsync( 'loginBanner', defaultValue: 'banner0', ); print('loginBannerMatch2 $loginBannerMatch2'); // returns banner2 // tracesSampleRate - final tracesSampleRate = await Sentry.getFeatureFlagValue( + final tracesSampleRate = await Sentry.getFeatureFlagValueAsync( 'tracesSampleRate', defaultValue: 0.0, ); diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index b00397b963..db18f90a06 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -393,9 +393,18 @@ class Hub { final samplingContext = SentrySamplingContext( transactionContext, customSamplingContext ?? {}); + final tracesSampleRate = item.client.getFeatureFlagValue( + '@@tracesSampleRate', + scope: item.scope, + defaultValue: _options.tracesSampleRate, + ); + // if transactionContext has no sampled decision, run the traces sampler if (transactionContext.sampled == null) { - final sampled = _tracesSampler.sample(samplingContext); + final sampled = _tracesSampler.sample( + samplingContext, + tracesSampleRate, + ); transactionContext = transactionContext.copyWith(sampled: sampled); } @@ -520,7 +529,7 @@ class Hub { } @experimental - Future getFeatureFlagValue( + Future getFeatureFlagValueAsync( String key, { T? defaultValue, FeatureFlagContextCallback? context, @@ -528,7 +537,41 @@ class Hub { if (!_isEnabled) { _options.logger( SentryLevel.warning, - "Instance is disabled and this 'getFeatureFlag' call is a no-op.", + "Instance is disabled and this 'getFeatureFlagValueAsync' call is a no-op.", + ); + return defaultValue; + } + + try { + final item = _peek(); + + return item.client.getFeatureFlagValueAsync( + key, + scope: item.scope, + defaultValue: defaultValue, + context: context, + ); + } catch (exception, stacktrace) { + _options.logger( + SentryLevel.error, + 'Error while fetching feature flags', + exception: exception, + stackTrace: stacktrace, + ); + } + return defaultValue; + } + + @experimental + T? getFeatureFlagValue( + String key, { + T? defaultValue, + FeatureFlagContextCallback? context, + }) { + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'getFeatureFlagValue' call is a no-op.", ); return defaultValue; } diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index ed255dfa5d..541c4a68b2 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -162,7 +162,19 @@ class HubAdapter implements Hub { Sentry.currentHub.setSpanContext(throwable, span, transaction); @override - Future getFeatureFlagValue( + Future getFeatureFlagValueAsync( + String key, { + T? defaultValue, + FeatureFlagContextCallback? context, + }) => + Sentry.getFeatureFlagValueAsync( + key, + defaultValue: defaultValue, + context: context, + ); + + @override + T? getFeatureFlagValue( String key, { T? defaultValue, FeatureFlagContextCallback? context, diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index a703b54f05..f53ac0e0d4 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -118,13 +118,21 @@ class NoOpHub implements Hub { void setSpanContext(throwable, ISentrySpan span, String transaction) {} @override - Future getFeatureFlagValue( + Future getFeatureFlagValueAsync( String key, { T? defaultValue, FeatureFlagContextCallback? context, }) async => null; + @override + T? getFeatureFlagValue( + String key, { + T? defaultValue, + FeatureFlagContextCallback? context, + }) => + null; + @override Future getFeatureFlagInfo( String key, { diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index 47914f7462..dc7499b0d6 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'feature_flags/feature_flag.dart'; import 'feature_flags/feature_flag_context.dart'; import 'feature_flags/feature_flag_info.dart'; import 'protocol.dart'; @@ -65,15 +64,21 @@ class NoOpSentryClient implements SentryClient { SentryId.empty(); @override - Future?> fetchFeatureFlags() async => null; + Future getFeatureFlagValueAsync( + String key, { + Scope? scope, + T? defaultValue, + FeatureFlagContextCallback? context, + }) async => + null; @override - Future getFeatureFlagValue( + T? getFeatureFlagValue( String key, { Scope? scope, T? defaultValue, FeatureFlagContextCallback? context, - }) async => + }) => null; @override diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 71b98f65ca..7fc3fd872e 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -281,7 +281,7 @@ class Sentry { bool defaultValue = false, FeatureFlagContextCallback? context, }) async { - return await getFeatureFlagValue( + return await getFeatureFlagValueAsync( key, defaultValue: defaultValue, context: context, @@ -289,7 +289,20 @@ class Sentry { defaultValue; } - static Future getFeatureFlagValue( + @experimental + static Future getFeatureFlagValueAsync( + String key, { + T? defaultValue, + FeatureFlagContextCallback? context, + }) => + _hub.getFeatureFlagValueAsync( + key, + defaultValue: defaultValue, + context: context, + ); + + @experimental + static T? getFeatureFlagValue( String key, { T? defaultValue, FeatureFlagContextCallback? context, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 5d105ffce5..bf9e9abdeb 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -35,7 +35,7 @@ const _defaultIpAddress = '{{auto}}'; class SentryClient { final SentryOptions _options; - final Random? _random; + final _random = Random(); static final _sentryId = Future.value(SentryId.empty()); @@ -58,8 +58,7 @@ class SentryClient { } /// Instantiates a client using [SentryOptions] - SentryClient._(this._options) - : _random = _options.sampleRate == null ? null : Random(); + SentryClient._(this._options); /// Reports an [event] to Sentry.io. Future captureEvent( @@ -68,7 +67,7 @@ class SentryClient { dynamic stackTrace, dynamic hint, }) async { - if (_sampleRate()) { + if (_sampleRate(scope)) { _recordLostEvent(event, DiscardReason.sampleRate); _options.logger( SentryLevel.debug, @@ -369,9 +368,14 @@ class SentryClient { return processedEvent; } - bool _sampleRate() { - if (_options.sampleRate != null && _random != null) { - return (_options.sampleRate! < _random!.nextDouble()); + bool _sampleRate(Scope? scope) { + final sampleRate = getFeatureFlagValue( + '@@errorsSampleRate', + scope: scope, + defaultValue: _options.sampleRate, + ); + if (sampleRate != null) { + return (sampleRate < _random.nextDouble()); } return false; } @@ -406,17 +410,13 @@ class SentryClient { return _options.transport.send(envelope); } - Future?> fetchFeatureFlags() => - _options.transport.fetchFeatureFlags(); - - @experimental - Future getFeatureFlagValue( + T? _getFeatureFlagValue( + Map? featureFlags, String key, { Scope? scope, T? defaultValue, FeatureFlagContextCallback? context, - }) async { - final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); + }) { final flag = featureFlags?[key]; if (flag == null) { @@ -436,6 +436,47 @@ class SentryClient { return resultType ? evaluationRule.result as T : defaultValue; } + @experimental + Future getFeatureFlagValueAsync( + String key, { + Scope? scope, + T? defaultValue, + FeatureFlagContextCallback? context, + }) async { + if (!_isFeatureFlagsEnabled()) { + return defaultValue; + } + + final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); + return _getFeatureFlagValue( + featureFlags, + key, + scope: scope, + defaultValue: defaultValue, + context: context, + ); + } + + @experimental + T? getFeatureFlagValue( + String key, { + Scope? scope, + T? defaultValue, + FeatureFlagContextCallback? context, + }) { + if (!_isFeatureFlagsEnabled()) { + return defaultValue; + } + + return _getFeatureFlagValue( + _featureFlags, + key, + scope: scope, + defaultValue: defaultValue, + context: context, + ); + } + bool _checkResultType(dynamic result) { return result is T ? true : false; } @@ -465,6 +506,10 @@ class SentryClient { Scope? scope, FeatureFlagContextCallback? context, }) async { + if (!_isFeatureFlagsEnabled()) { + return null; + } + final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); final featureFlag = featureFlags?[key]; @@ -547,6 +592,11 @@ class SentryClient { if (environment != null) { featureFlagContext.tags['environment'] = environment; } + // set the transaction + final transaction = scope?.transaction; + if (transaction != null) { + featureFlagContext.tags['transaction'] = transaction; + } // set all the tags from the scope as well featureFlagContext.tags.addAll(scope?.tags ?? {}); @@ -572,7 +622,12 @@ class SentryClient { Future?> _getFeatureFlagsFromCacheOrNetwork() async { // TODO: add mechanism to reset caching - _featureFlags = _featureFlags ?? await fetchFeatureFlags(); + _featureFlags = + _featureFlags ?? await _options.transport.fetchFeatureFlags(); return _featureFlags; } + + bool _isFeatureFlagsEnabled() { + return _options.experimental['featureFlagsEnabled'] as bool? ?? false; + } } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 4cd7ac5295..2f49a5cce2 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -285,6 +285,11 @@ class SentryOptions { @internal late ClientReportRecorder recorder = NoOpClientReportRecorder(); + /// experimental features + final Map experimental = { + 'featureFlagsEnabled': false, + }; + SentryOptions({this.dsn, PlatformChecker? checker}) { if (checker != null) { platformChecker = checker; diff --git a/dart/lib/src/sentry_traces_sampler.dart b/dart/lib/src/sentry_traces_sampler.dart index 9d981cca04..52cc5499cf 100644 --- a/dart/lib/src/sentry_traces_sampler.dart +++ b/dart/lib/src/sentry_traces_sampler.dart @@ -14,7 +14,7 @@ class SentryTracesSampler { Random? random, }) : _random = random ?? Random(); - bool sample(SentrySamplingContext samplingContext) { + bool sample(SentrySamplingContext samplingContext, double? tracesSampleRate) { final sampled = samplingContext.transactionContext.sampled; if (sampled != null) { return sampled; @@ -33,7 +33,6 @@ class SentryTracesSampler { return parentSampled; } - final tracesSampleRate = _options.tracesSampleRate; if (tracesSampleRate != null) { return _sample(tracesSampleRate); } diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 65403a3814..55f72a4a62 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -112,89 +112,92 @@ class HttpTransport implements Transport { 'body = ${response.body}', ); } - return null; + // return null; } - final responseJson = json.decode(response.body); - -// final responseJson = json.decode(''' -// { -// "feature_flags": { -// "accessToProfiling": { -// "kind": "bool", -// "evaluation": [ -// { -// "type": "rollout", -// "percentage": 0.5, -// "result": true, -// "tags": { -// "userSegment": "slow" -// } -// }, -// { -// "type": "match", -// "result": true, -// "tags": { -// "isSentryDev": "true" -// }, -// "payload": { -// "internal_setting": "wat" -// } -// } -// ] -// }, -// "profilingEnabled": { -// "kind": "bool", -// "evaluation": [ -// { -// "type": "match", -// "result": true, -// "tags": { -// "isSentryDev": "true" -// } -// }, -// { -// "type": "rollout", -// "percentage": 0.05, -// "result": true -// } -// ] -// }, -// "loginBanner": { -// "kind": "string", -// "evaluation": [ -// { -// "type": "match", -// "result": "banner1", -// "tags": { -// "isSentryDev": "true" -// }, -// "payload": { -// "imageUrl": "https://mycdn.com/banner1.png" -// } -// }, -// { -// "type": "match", -// "result": "banner2", -// "payload": { -// "imageUrl": "https://mycdn.com/banner2.png" -// } -// } -// ] -// }, -// "tracesSampleRate": { -// "kind": "number", -// "evaluation": [ -// { -// "type": "match", -// "result": 0.25 -// } -// ] -// } - -// } -// } -// '''); + // final responseJson = json.decode(response.body); + + final responseJson = json.decode(''' +{ + "feature_flags": { + "@@accessToProfiling": { + "evaluation": [ + { + "type": "match", + "result": true, + "tags": { + "isSentryDev": "true" + } + }, + { + "type": "rollout", + "percentage": 0.5, + "result": true + } + ], + "kind": "boolean" + }, + "@@errorsSampleRate": { + "evaluation": [ + { + "type": "match", + "result": 0.75 + } + ], + "kind": "number" + }, + "@@profilingEnabled": { + "evaluation": [ + { + "type": "match", + "result": true, + "tags": { + "isSentryDev": "true" + } + }, + { + "type": "rollout", + "percentage": 0.05, + "result": true + } + ], + "kind": "boolean" + }, + "@@tracesSampleRate": { + "evaluation": [ + { + "type": "match", + "result": 0.25 + } + ], + "kind": "number" + }, + "accessToProfiling": { + "evaluation": [ + { + "type": "rollout", + "percentage": 1.0, + "result": true + } + ], + "kind": "boolean" + }, + "welcomeBanner": { + "evaluation": [ + { + "type": "rollout", + "percentage": 1.0, + "result": "dev.png", + "tags": { + "environment": "dev" + } + } + ], + "kind": "string" + } + } +} + '''); return FeatureDump.fromJson(responseJson).featureFlags; } diff --git a/dart/test/sentry_traces_sampler_test.dart b/dart/test/sentry_traces_sampler_test.dart index a465b4cc60..38e901ec4e 100644 --- a/dart/test/sentry_traces_sampler_test.dart +++ b/dart/test/sentry_traces_sampler_test.dart @@ -17,7 +17,7 @@ void main() { ); final context = SentrySamplingContext(trContext, {}); - expect(sut.sample(context), true); + expect(sut.sample(context, fixture.options.tracesSampleRate), true); }); test('options has sampler', () { @@ -36,7 +36,7 @@ void main() { ); final context = SentrySamplingContext(trContext, {}); - expect(sut.sample(context), true); + expect(sut.sample(context, fixture.options.tracesSampleRate), true); }); test('transactionContext has parentSampled', () { @@ -49,7 +49,7 @@ void main() { ); final context = SentrySamplingContext(trContext, {}); - expect(sut.sample(context), true); + expect(sut.sample(context, fixture.options.tracesSampleRate), true); }); test('options has rate 1.0', () { @@ -61,7 +61,7 @@ void main() { ); final context = SentrySamplingContext(trContext, {}); - expect(sut.sample(context), true); + expect(sut.sample(context, fixture.options.tracesSampleRate), true); }); test('options has rate 0.0', () { @@ -73,7 +73,7 @@ void main() { ); final context = SentrySamplingContext(trContext, {}); - expect(sut.sample(context), false); + expect(sut.sample(context, fixture.options.tracesSampleRate), false); }); } From f93e43040a634c172b54cde09907c1f556280dd8 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 25 Aug 2022 09:52:17 +0200 Subject: [PATCH 25/33] add group --- dart/lib/src/feature_flags/feature_flag.dart | 11 ++-- dart/lib/src/sentry_client.dart | 37 +++++++----- dart/lib/src/sentry_options.dart | 5 ++ flutter/example/lib/main.dart | 61 +++++++++++++------- 4 files changed, 69 insertions(+), 45 deletions(-) diff --git a/dart/lib/src/feature_flags/feature_flag.dart b/dart/lib/src/feature_flags/feature_flag.dart index 2e14e0b711..0b6016838e 100644 --- a/dart/lib/src/feature_flags/feature_flag.dart +++ b/dart/lib/src/feature_flags/feature_flag.dart @@ -5,22 +5,19 @@ import 'evaluation_rule.dart'; @immutable class FeatureFlag { final List _evaluations; - // final String kind; + final String? group; List get evaluations => List.unmodifiable(_evaluations); - FeatureFlag(this._evaluations); + FeatureFlag(this._evaluations, this.group); factory FeatureFlag.fromJson(Map json) { - // final kind = json['kind']; + final group = json['group']; final evaluationsList = json['evaluation'] as List? ?? []; final evaluations = evaluationsList .map((e) => EvaluationRule.fromJson(e)) .toList(growable: false); - return FeatureFlag( - // kind, - evaluations, - ); + return FeatureFlag(evaluations, group); } } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index bf9e9abdeb..07758df449 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -424,8 +424,11 @@ class SentryClient { } final featureFlagContext = _getFeatureFlagContext(scope, context); - final evaluationRule = - _getEvaluationRuleMatch(flag.evaluations, featureFlagContext); + final evaluationRule = _getEvaluationRuleMatch( + key, + flag, + featureFlagContext, + ); if (evaluationRule == null) { return defaultValue; @@ -443,7 +446,7 @@ class SentryClient { T? defaultValue, FeatureFlagContextCallback? context, }) async { - if (!_isFeatureFlagsEnabled()) { + if (!_options.isFeatureFlagsEnabled()) { return defaultValue; } @@ -464,7 +467,7 @@ class SentryClient { T? defaultValue, FeatureFlagContextCallback? context, }) { - if (!_isFeatureFlagsEnabled()) { + if (!_options.isFeatureFlagsEnabled()) { return defaultValue; } @@ -481,8 +484,9 @@ class SentryClient { return result is T ? true : false; } - double _rollRandomNumber(String stickyId) { - final rand = XorShiftRandom(stickyId); + double _rollRandomNumber(String stickyId, String group) { + final seed = '$group|$stickyId'; + final rand = XorShiftRandom(seed); return rand.next(); } @@ -506,7 +510,7 @@ class SentryClient { Scope? scope, FeatureFlagContextCallback? context, }) async { - if (!_isFeatureFlagsEnabled()) { + if (!_options.isFeatureFlagsEnabled()) { return null; } @@ -519,8 +523,11 @@ class SentryClient { final featureFlagContext = _getFeatureFlagContext(scope, context); - final evaluationRule = - _getEvaluationRuleMatch(featureFlag.evaluations, featureFlagContext); + final evaluationRule = _getEvaluationRuleMatch( + key, + featureFlag, + featureFlagContext, + ); if (evaluationRule == null) { return null; @@ -537,20 +544,22 @@ class SentryClient { } EvaluationRule? _getEvaluationRuleMatch( - List evaluations, + String key, + FeatureFlag featureFlag, FeatureFlagContext context, ) { // there's always a stickyId final stickyId = context.tags['stickyId']!; + final group = featureFlag.group ?? key; - for (final evalConfig in evaluations) { + for (final evalConfig in featureFlag.evaluations) { if (!_matchesTags(evalConfig.tags, context.tags)) { continue; } switch (evalConfig.type) { case EvaluationType.rollout: - final percentage = _rollRandomNumber(stickyId); + final percentage = _rollRandomNumber(stickyId, group); if (percentage < (evalConfig.percentage ?? 0.0)) { return evalConfig; } @@ -626,8 +635,4 @@ class SentryClient { _featureFlags ?? await _options.transport.fetchFeatureFlags(); return _featureFlags; } - - bool _isFeatureFlagsEnabled() { - return _options.experimental['featureFlagsEnabled'] as bool? ?? false; - } } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 2f49a5cce2..e371a632fb 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -290,6 +290,11 @@ class SentryOptions { 'featureFlagsEnabled': false, }; + // experimental + bool isFeatureFlagsEnabled() { + return experimental['featureFlagsEnabled'] as bool? ?? false; + } + SentryOptions({this.dsn, PlatformChecker? checker}) { if (checker != null) { platformChecker = checker; diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index a606ce6c75..85fc47a606 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -16,7 +16,7 @@ import 'package:sentry_logging/sentry_logging.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String _exampleDsn = - 'https://60d3409215134fd1a60765f2400b6b38@ac75-72-74-53-151.ngrok.io/1'; + 'https://fe85fc5123d44d5c99202d9e8f09d52e@395f015cf6c1.eu.ngrok.io/2'; final _channel = const MethodChannel('example.flutter.sentry.io'); @@ -31,6 +31,7 @@ Future main() async { options.attachThreads = true; options.enableWindowMetricBreadcrumbs = true; options.addIntegration(LoggingIntegration()); + options.experimental['featureFlagsEnabled'] = true; }, // Init your App. appRunner: () => runApp( @@ -341,6 +342,43 @@ class MainScaffold extends StatelessWidget { }, child: const Text('Show UserFeedback Dialog without event'), ), + ElevatedButton( + onPressed: () async { + await Sentry.configureScope((scope) async { + await scope.setUser( + SentryUser( + id: '800', + ), + ); + }); + + final accessToProfiling = await Sentry.isFeatureFlagEnabled( + '@@accessToProfiling', + defaultValue: false, + context: (myContext) => { + myContext.tags['isSentryDev'] = 'true', + }, + ); + print('accessToProfiling: $accessToProfiling'); + + final errorsSampleRate = + Sentry.getFeatureFlagValue('@@errorsSampleRate'); + print('errorsSampleRate: $errorsSampleRate'); + + final tracesSampleRate = + Sentry.getFeatureFlagValue('@@tracesSampleRate'); + print('tracesSampleRate: $tracesSampleRate'); + + final welcomeBannerResult = Sentry.getFeatureFlagValue( + 'welcomeBanner', + context: (myContext) => { + myContext.tags['environment'] = 'dev', + }, + ); + print('welcomeBanner: $welcomeBannerResult'); + }, + child: const Text('Check feature flags'), + ), if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), @@ -401,27 +439,6 @@ class AndroidExample extends StatelessWidget { }, child: const Text('Logging'), ), - ElevatedButton( - onPressed: () async { - await Sentry.configureScope((scope) async { - await scope.setUser( - SentryUser( - id: '800', - ), - ); - }); - - final enabled = await Sentry.isFeatureFlagEnabled( - '@@accessToProfiling', - defaultValue: false, - context: (myContext) => { - myContext.tags['myCustomTag'] = 'myCustomValue', - }, - ); - print(enabled); - }, - child: const Text('Check feature flags'), - ), ]); } } From 1f323e8e9eacc1ee1659aaebeb7fea989507306c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 25 Aug 2022 10:28:57 +0200 Subject: [PATCH 26/33] fix tests --- dart/lib/src/transport/http_transport.dart | 166 ++++++++++----------- dart/test/hub_test.dart | 1 + dart/test/mocks/mock_sentry_client.dart | 28 ++++ dart/test/sentry_test.dart | 4 +- 4 files changed, 115 insertions(+), 84 deletions(-) diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 55f72a4a62..a486b37bbe 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -115,89 +115,89 @@ class HttpTransport implements Transport { // return null; } - // final responseJson = json.decode(response.body); - - final responseJson = json.decode(''' -{ - "feature_flags": { - "@@accessToProfiling": { - "evaluation": [ - { - "type": "match", - "result": true, - "tags": { - "isSentryDev": "true" - } - }, - { - "type": "rollout", - "percentage": 0.5, - "result": true - } - ], - "kind": "boolean" - }, - "@@errorsSampleRate": { - "evaluation": [ - { - "type": "match", - "result": 0.75 - } - ], - "kind": "number" - }, - "@@profilingEnabled": { - "evaluation": [ - { - "type": "match", - "result": true, - "tags": { - "isSentryDev": "true" - } - }, - { - "type": "rollout", - "percentage": 0.05, - "result": true - } - ], - "kind": "boolean" - }, - "@@tracesSampleRate": { - "evaluation": [ - { - "type": "match", - "result": 0.25 - } - ], - "kind": "number" - }, - "accessToProfiling": { - "evaluation": [ - { - "type": "rollout", - "percentage": 1.0, - "result": true - } - ], - "kind": "boolean" - }, - "welcomeBanner": { - "evaluation": [ - { - "type": "rollout", - "percentage": 1.0, - "result": "dev.png", - "tags": { - "environment": "dev" - } - } - ], - "kind": "string" - } - } -} - '''); + final responseJson = json.decode(response.body); + +// final responseJson = json.decode(''' +// { +// "feature_flags": { +// "@@accessToProfiling": { +// "evaluation": [ +// { +// "type": "match", +// "result": true, +// "tags": { +// "isSentryDev": "true" +// } +// }, +// { +// "type": "rollout", +// "percentage": 0.5, +// "result": true +// } +// ], +// "kind": "boolean" +// }, +// "@@errorsSampleRate": { +// "evaluation": [ +// { +// "type": "match", +// "result": 0.75 +// } +// ], +// "kind": "number" +// }, +// "@@profilingEnabled": { +// "evaluation": [ +// { +// "type": "match", +// "result": true, +// "tags": { +// "isSentryDev": "true" +// } +// }, +// { +// "type": "rollout", +// "percentage": 0.05, +// "result": true +// } +// ], +// "kind": "boolean" +// }, +// "@@tracesSampleRate": { +// "evaluation": [ +// { +// "type": "match", +// "result": 0.25 +// } +// ], +// "kind": "number" +// }, +// "accessToProfiling": { +// "evaluation": [ +// { +// "type": "rollout", +// "percentage": 1.0, +// "result": true +// } +// ], +// "kind": "boolean" +// }, +// "welcomeBanner": { +// "evaluation": [ +// { +// "type": "rollout", +// "percentage": 1.0, +// "result": "dev.png", +// "tags": { +// "environment": "dev" +// } +// } +// ], +// "kind": "string" +// } +// } +// } +// '''); return FeatureDump.fromJson(responseJson).featureFlags; } diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 0c697e0fd7..849ee1f05c 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -537,6 +537,7 @@ class Fixture { tracer = SentryTracer(_context, hub); + client.featureFlagValue = tracesSampleRate; hub.bindClient(client); options.recorder = recorder; diff --git a/dart/test/mocks/mock_sentry_client.dart b/dart/test/mocks/mock_sentry_client.dart index d42f86f48e..6c7aecd748 100644 --- a/dart/test/mocks/mock_sentry_client.dart +++ b/dart/test/mocks/mock_sentry_client.dart @@ -10,6 +10,8 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { List captureTransactionCalls = []; List userFeedbackCalls = []; int closeCalls = 0; + dynamic featureFlagValue; + FeatureFlagInfo? featureFlagInfo; @override Future captureEvent( @@ -87,6 +89,32 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { captureTransactionCalls.add(CaptureTransactionCall(transaction)); return transaction.eventId; } + + @override + Future getFeatureFlagValueAsync( + String key, { + Scope? scope, + T? defaultValue, + FeatureFlagContextCallback? context, + }) async => + featureFlagValue; + + @override + T? getFeatureFlagValue( + String key, { + Scope? scope, + T? defaultValue, + FeatureFlagContextCallback? context, + }) => + featureFlagValue; + + @override + Future getFeatureFlagInfo( + String key, { + Scope? scope, + FeatureFlagContextCallback? context, + }) async => + featureFlagInfo; } class CaptureEventCall { diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 989f63a58e..9b39a42c2c 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -18,15 +18,17 @@ void main() { var anException = Exception(); setUp(() async { + final tracesSampleRate = 1.0; await Sentry.init( (options) => { options.dsn = fakeDsn, - options.tracesSampleRate = 1.0, + options.tracesSampleRate = tracesSampleRate, }, ); anException = Exception('anException'); client = MockSentryClient(); + client.featureFlagValue = tracesSampleRate; Sentry.bindClient(client); }); From 14a660284f53177a486406b98d6570818365c59d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 25 Aug 2022 14:27:53 +0200 Subject: [PATCH 27/33] fetch feature flags on start --- dart/lib/src/default_integrations.dart | 11 +++++++++++ dart/lib/src/hub.dart | 23 +++++++++++++++++++++++ dart/lib/src/hub_adapter.dart | 3 +++ dart/lib/src/noop_hub.dart | 3 +++ dart/lib/src/noop_sentry_client.dart | 3 +++ dart/lib/src/sentry.dart | 1 + dart/lib/src/sentry_client.dart | 11 +++++------ 7 files changed, 49 insertions(+), 6 deletions(-) diff --git a/dart/lib/src/default_integrations.dart b/dart/lib/src/default_integrations.dart index 758bf62182..c26ee4fc39 100644 --- a/dart/lib/src/default_integrations.dart +++ b/dart/lib/src/default_integrations.dart @@ -99,3 +99,14 @@ class RunZonedGuardedIntegration extends Integration { return completer.future; } } + +class FetchFeatureFlagsAsync extends Integration { + @override + FutureOr call(Hub hub, SentryOptions options) async { + // request feature flags and cache it in memory + if (!options.isFeatureFlagsEnabled()) { + return; + } + await hub.requestFeatureFlags(); + } +} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index db18f90a06..372f7a1552 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -627,6 +627,29 @@ class Hub { } return null; } + + Future requestFeatureFlags() async { + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'requestFeatureFlags' call is a no-op.", + ); + return; + } + + try { + final item = _peek(); + + return item.client.requestFeatureFlags(); + } catch (exception, stacktrace) { + _options.logger( + SentryLevel.error, + 'Error while fetching feature flags', + exception: exception, + stackTrace: stacktrace, + ); + } + } } class _StackItem { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 541c4a68b2..848e50d4d6 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -194,4 +194,7 @@ class HubAdapter implements Hub { key, context: context, ); + + @override + Future requestFeatureFlags() => Sentry.currentHub.requestFeatureFlags(); } diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index f53ac0e0d4..4f03310b05 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -139,4 +139,7 @@ class NoOpHub implements Hub { FeatureFlagContextCallback? context, }) async => null; + + @override + Future requestFeatureFlags() async {} } diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index dc7499b0d6..297dbb1ca0 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -88,4 +88,7 @@ class NoOpSentryClient implements SentryClient { FeatureFlagContextCallback? context, }) async => null; + + @override + Future requestFeatureFlags() async {} } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 7fc3fd872e..6f850395d3 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -116,6 +116,7 @@ class Sentry { final runZonedGuardedIntegration = RunZonedGuardedIntegration(runIntegrationsAndAppRunner); options.addIntegrationByIndex(0, runZonedGuardedIntegration); + options.addIntegration(FetchFeatureFlagsAsync()); // RunZonedGuardedIntegration will run other integrations and appRunner // runZonedGuarded so all exception caught in the error handler are diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 07758df449..1fbea1cf62 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -450,9 +450,9 @@ class SentryClient { return defaultValue; } - final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); + await requestFeatureFlags(); return _getFeatureFlagValue( - featureFlags, + _featureFlags, key, scope: scope, defaultValue: defaultValue, @@ -514,8 +514,8 @@ class SentryClient { return null; } - final featureFlags = await _getFeatureFlagsFromCacheOrNetwork(); - final featureFlag = featureFlags?[key]; + await requestFeatureFlags(); + final featureFlag = _featureFlags?[key]; if (featureFlag == null) { return null; @@ -629,10 +629,9 @@ class SentryClient { return featureFlagContext; } - Future?> _getFeatureFlagsFromCacheOrNetwork() async { + Future requestFeatureFlags() async { // TODO: add mechanism to reset caching _featureFlags = _featureFlags ?? await _options.transport.fetchFeatureFlags(); - return _featureFlags; } } From 7c6b89c034836a0a13ee3533cd5cbc70e887afce Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 26 Aug 2022 09:21:02 +0200 Subject: [PATCH 28/33] fix tests --- flutter/test/default_integrations_test.dart | 4 +++- flutter/test/native_sdk_integration_test.dart | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/test/default_integrations_test.dart b/flutter/test/default_integrations_test.dart index 5ff56817fc..9789514639 100644 --- a/flutter/test/default_integrations_test.dart +++ b/flutter/test/default_integrations_test.dart @@ -219,7 +219,9 @@ void main() { }); test('nativeSdkIntegration adds integration', () async { - _channel.setMockMethodCallHandler((MethodCall methodCall) async {}); + _channel.setMockMethodCallHandler((MethodCall methodCall) async { + return {'deviceId': 'test'}; + }); final integration = NativeSdkIntegration(_channel); diff --git a/flutter/test/native_sdk_integration_test.dart b/flutter/test/native_sdk_integration_test.dart index 9f757d5fba..6ec1b6c716 100644 --- a/flutter/test/native_sdk_integration_test.dart +++ b/flutter/test/native_sdk_integration_test.dart @@ -128,7 +128,9 @@ void main() { }); test('adds integration', () async { - final channel = createChannelWithCallback((call) async {}); + final channel = createChannelWithCallback((call) async { + return {'deviceId': 'test'}; + }); var sut = fixture.getSut(channel); final options = createOptions(); From 45bb842d60e128aea117df97c1cdf16cf937ed1e Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 26 Aug 2022 09:31:13 +0200 Subject: [PATCH 29/33] fix broken tests --- dart/test/transport/http_transport_test.dart | 4 +++- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 164c235efe..a8d6048450 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -259,7 +259,9 @@ void main() { expect(matchProfiling.tags.isEmpty, true); expect(matchProfiling.type, EvaluationType.match); expect(matchProfiling.payload, isNull); - }); + }, onPlatform: { + 'browser': Skip() + }); // TODO: web does not have File/readAsString }); } diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 4d622b2de7..69b86fd05d 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -174,7 +174,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { // make Installation.id(context) public val item = mapOf( - "deviceId" to Sentry.getCurrentHub().options.distinctId + "deviceId" to Sentry.getCurrentHub().options.distinctId ) result.success(item) } From c4a77ae45ef1e43674c5bfce66a5b5c220f83eae Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 26 Aug 2022 10:04:30 +0200 Subject: [PATCH 30/33] vendor sha1 --- dart/lib/src/cryptography/digest.dart | 53 +++ dart/lib/src/cryptography/digest_sink.dart | 29 ++ dart/lib/src/cryptography/hash.dart | 35 ++ dart/lib/src/cryptography/hash_sink.dart | 178 ++++++++ dart/lib/src/cryptography/sha1.dart | 105 +++++ dart/lib/src/cryptography/utils.dart | 22 + .../lib/src/feature_flags/xor_shift_rand.dart | 2 +- dart/lib/src/typed_data/typed_buffer.dart | 426 ++++++++++++++++++ dart/pubspec.yaml | 1 - 9 files changed, 849 insertions(+), 2 deletions(-) create mode 100644 dart/lib/src/cryptography/digest.dart create mode 100644 dart/lib/src/cryptography/digest_sink.dart create mode 100644 dart/lib/src/cryptography/hash.dart create mode 100644 dart/lib/src/cryptography/hash_sink.dart create mode 100644 dart/lib/src/cryptography/sha1.dart create mode 100644 dart/lib/src/cryptography/utils.dart create mode 100644 dart/lib/src/typed_data/typed_buffer.dart diff --git a/dart/lib/src/cryptography/digest.dart b/dart/lib/src/cryptography/digest.dart new file mode 100644 index 0000000000..6503fedd7c --- /dev/null +++ b/dart/lib/src/cryptography/digest.dart @@ -0,0 +1,53 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:typed_data'; + +/// A message digest as computed by a `Hash` or `HMAC` function. +class Digest { + /// The message digest as an array of bytes. + final List bytes; + + Digest(this.bytes); + + /// Returns whether this is equal to another digest. + /// + /// This should be used instead of manual comparisons to avoid leaking + /// information via timing. + @override + bool operator ==(Object other) { + if (other is Digest) { + final a = bytes; + final b = other.bytes; + final n = a.length; + if (n != b.length) { + return false; + } + var mismatch = 0; + for (var i = 0; i < n; i++) { + mismatch |= a[i] ^ b[i]; + } + return mismatch == 0; + } + return false; + } + + @override + int get hashCode => Object.hashAll(bytes); + + /// The message digest as a string of hexadecimal digits. + @override + String toString() => _hexEncode(bytes); +} + +String _hexEncode(List bytes) { + const hexDigits = '0123456789abcdef'; + var charCodes = Uint8List(bytes.length * 2); + for (var i = 0, j = 0; i < bytes.length; i++) { + var byte = bytes[i]; + charCodes[j++] = hexDigits.codeUnitAt((byte >> 4) & 0xF); + charCodes[j++] = hexDigits.codeUnitAt(byte & 0xF); + } + return String.fromCharCodes(charCodes); +} diff --git a/dart/lib/src/cryptography/digest_sink.dart b/dart/lib/src/cryptography/digest_sink.dart new file mode 100644 index 0000000000..9ba4df91e1 --- /dev/null +++ b/dart/lib/src/cryptography/digest_sink.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'digest.dart'; + +/// A sink used to get a digest value out of `Hash.startChunkedConversion`. +class DigestSink extends Sink { + /// The value added to the sink. + /// + /// A value must have been added using [add] before reading the `value`. + Digest get value => _value!; + + Digest? _value; + + /// Adds [value] to the sink. + /// + /// Unlike most sinks, this may only be called once. + @override + void add(Digest value) { + if (_value != null) throw StateError('add may only be called once.'); + _value = value; + } + + @override + void close() { + if (_value == null) throw StateError('add must be called once.'); + } +} diff --git a/dart/lib/src/cryptography/hash.dart b/dart/lib/src/cryptography/hash.dart new file mode 100644 index 0000000000..8e05160581 --- /dev/null +++ b/dart/lib/src/cryptography/hash.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'digest.dart'; +import 'digest_sink.dart'; + +/// An interface for cryptographic hash functions. +/// +/// Every hash is a converter that takes a list of ints and returns a single +/// digest. When used in chunked mode, it will only ever add one digest to the +/// inner [Sink]. +abstract class Hash extends Converter, Digest> { + /// The internal block size of the hash in bytes. + /// + /// This is exposed for use by the `Hmac` class, + /// which needs to know the block size for the [Hash] it uses. + int get blockSize; + + const Hash(); + + @override + Digest convert(List input) { + var innerSink = DigestSink(); + var outerSink = startChunkedConversion(innerSink); + outerSink.add(input); + outerSink.close(); + return innerSink.value; + } + + @override + ByteConversionSink startChunkedConversion(Sink sink); +} diff --git a/dart/lib/src/cryptography/hash_sink.dart b/dart/lib/src/cryptography/hash_sink.dart new file mode 100644 index 0000000000..7fd57c7ae6 --- /dev/null +++ b/dart/lib/src/cryptography/hash_sink.dart @@ -0,0 +1,178 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:typed_data'; + +// import '../typed_data/uint8_buffer.dart'; +import '../typed_data/typed_buffer.dart'; +import 'digest.dart'; +import 'utils.dart'; + +/// A base class for [Sink] implementations for hash algorithms. +/// +/// Subclasses should override [updateHash] and [digest]. +abstract class HashSink implements Sink> { + /// The inner sink that this should forward to. + final Sink _sink; + + /// Whether the hash function operates on big-endian words. + final Endian _endian; + + /// The words in the current chunk. + /// + /// This is an instance variable to avoid re-allocating, but its data isn't + /// used across invocations of [_iterate]. + final Uint32List _currentChunk; + + /// Messages with more than 2^53-1 bits are not supported. + /// + /// This is the largest value that is precisely representable + /// on both JS and the Dart VM. + /// So the maximum length in bytes is (2^53-1)/8. + static const _maxMessageLengthInBytes = 0x0003ffffffffffff; + + /// The length of the input data so far, in bytes. + int _lengthInBytes = 0; + + /// Data that has yet to be processed by the hash function. + final _pendingData = Uint8Buffer(); + + /// Whether [close] has been called. + bool _isClosed = false; + + /// The words in the current digest. + /// + /// This should be updated each time [updateHash] is called. + Uint32List get digest; + + /// The number of signature bytes emitted at the end of the message. + /// + /// An encrypted message is followed by a signature which depends + /// on the encryption algorithm used. This value specifies the + /// number of bytes used by this signature. It must always be + /// a power of 2 and no less than 8. + final int _signatureBytes; + + /// Creates a new hash. + /// + /// [chunkSizeInWords] represents the size of the input chunks processed by + /// the algorithm, in terms of 32-bit words. + HashSink(this._sink, int chunkSizeInWords, + {Endian endian = Endian.big, int signatureBytes = 8}) + : _endian = endian, + assert(signatureBytes >= 8), + _signatureBytes = signatureBytes, + _currentChunk = Uint32List(chunkSizeInWords); + + /// Runs a single iteration of the hash computation, updating [digest] with + /// the result. + /// + /// [chunk] is the current chunk, whose size is given by the + /// `chunkSizeInWords` parameter passed to the constructor. + void updateHash(Uint32List chunk); + + @override + void add(List data) { + if (_isClosed) throw StateError('Hash.add() called after close().'); + _lengthInBytes += data.length; + _pendingData.addAll(data); + _iterate(); + } + + @override + void close() { + if (_isClosed) return; + _isClosed = true; + + _finalizeData(); + _iterate(); + assert(_pendingData.isEmpty); + _sink.add(Digest(_byteDigest())); + _sink.close(); + } + + Uint8List _byteDigest() { + if (_endian == Endian.host) return digest.buffer.asUint8List(); + + // Cache the digest locally as `get` could be expensive. + final cachedDigest = digest; + final byteDigest = Uint8List(cachedDigest.lengthInBytes); + final byteData = byteDigest.buffer.asByteData(); + for (var i = 0; i < cachedDigest.length; i++) { + byteData.setUint32(i * bytesPerWord, cachedDigest[i]); + } + return byteDigest; + } + + /// Iterates through [_pendingData], updating the hash computation for each + /// chunk. + void _iterate() { + var pendingDataBytes = _pendingData.buffer.asByteData(); + var pendingDataChunks = _pendingData.length ~/ _currentChunk.lengthInBytes; + for (var i = 0; i < pendingDataChunks; i++) { + // Copy words from the pending data buffer into the current chunk buffer. + for (var j = 0; j < _currentChunk.length; j++) { + _currentChunk[j] = pendingDataBytes.getUint32( + i * _currentChunk.lengthInBytes + j * bytesPerWord, _endian); + } + + // Run the hash function on the current chunk. + updateHash(_currentChunk); + } + + // Remove all pending data up to the last clean chunk break. + _pendingData.removeRange( + 0, pendingDataChunks * _currentChunk.lengthInBytes); + } + + /// Finalizes [_pendingData]. + /// + /// This adds a 1 bit to the end of the message, and expands it with 0 bits to + /// pad it out. + void _finalizeData() { + // Pad out the data with 0x80, eight or sixteen 0s, and as many more 0s + // as we need to land cleanly on a chunk boundary. + _pendingData.add(0x80); + + final contentsLength = _lengthInBytes + 1 /* 0x80 */ + _signatureBytes; + final finalizedLength = + _roundUp(contentsLength, _currentChunk.lengthInBytes); + + for (var i = 0; i < finalizedLength - contentsLength; i++) { + _pendingData.add(0); + } + + if (_lengthInBytes > _maxMessageLengthInBytes) { + throw UnsupportedError( + 'Hashing is unsupported for messages with more than 2^53 bits.'); + } + + var lengthInBits = _lengthInBytes * bitsPerByte; + + // Add the full length of the input data as a 64-bit value at the end of the + // hash. Note: we're only writing out 64 bits, so skip ahead 8 if the + // signature is 128-bit. + final offset = _pendingData.length + (_signatureBytes - 8); + + _pendingData.addAll(Uint8List(_signatureBytes)); + var byteData = _pendingData.buffer.asByteData(); + + // We're essentially doing byteData.setUint64(offset, lengthInBits, _endian) + // here, but that method isn't supported on dart2js so we implement it + // manually instead. + var highBits = lengthInBits ~/ 0x100000000; // >> 32 + var lowBits = lengthInBits & mask32; + if (_endian == Endian.big) { + byteData.setUint32(offset, highBits, _endian); + byteData.setUint32(offset + bytesPerWord, lowBits, _endian); + } else { + byteData.setUint32(offset, lowBits, _endian); + byteData.setUint32(offset + bytesPerWord, highBits, _endian); + } + } + + /// Rounds [val] up to the next multiple of [n], as long as [n] is a power of + /// two. + int _roundUp(int val, int n) => (val + n - 1) & -n; +} diff --git a/dart/lib/src/cryptography/sha1.dart b/dart/lib/src/cryptography/sha1.dart new file mode 100644 index 0000000000..e352de2b64 --- /dev/null +++ b/dart/lib/src/cryptography/sha1.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// vendored from https://pub.dev/packages/crypto 3.0.2 +// otherwise we'd need to ubmp the min. version of the Dart SDK + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'digest.dart'; +import 'hash.dart'; +import 'hash_sink.dart'; +import 'utils.dart'; + +/// An implementation of the [SHA-1][rfc] hash function. +/// +/// [rfc]: http://tools.ietf.org/html/rfc3174 +const Hash sha1 = _Sha1._(); + +/// An implementation of the [SHA-1][rfc] hash function. +/// +/// [rfc]: http://tools.ietf.org/html/rfc3174 +class _Sha1 extends Hash { + @override + final int blockSize = 16 * bytesPerWord; + + const _Sha1._(); + + @override + ByteConversionSink startChunkedConversion(Sink sink) => + ByteConversionSink.from(_Sha1Sink(sink)); +} + +/// The concrete implementation of [Sha1]. +/// +/// This is separate so that it can extend [HashSink] without leaking additional +/// public members. +class _Sha1Sink extends HashSink { + @override + final digest = Uint32List(5); + + /// The sixteen words from the original chunk, extended to 80 words. + /// + /// This is an instance variable to avoid re-allocating, but its data isn't + /// used across invocations of [updateHash]. + final Uint32List _extended; + + _Sha1Sink(Sink sink) + : _extended = Uint32List(80), + super(sink, 16) { + digest[0] = 0x67452301; + digest[1] = 0xEFCDAB89; + digest[2] = 0x98BADCFE; + digest[3] = 0x10325476; + digest[4] = 0xC3D2E1F0; + } + + @override + void updateHash(Uint32List chunk) { + assert(chunk.length == 16); + + var a = digest[0]; + var b = digest[1]; + var c = digest[2]; + var d = digest[3]; + var e = digest[4]; + + for (var i = 0; i < 80; i++) { + if (i < 16) { + _extended[i] = chunk[i]; + } else { + _extended[i] = rotl32( + _extended[i - 3] ^ + _extended[i - 8] ^ + _extended[i - 14] ^ + _extended[i - 16], + 1); + } + + var newA = add32(add32(rotl32(a, 5), e), _extended[i]); + if (i < 20) { + newA = add32(add32(newA, (b & c) | (~b & d)), 0x5A827999); + } else if (i < 40) { + newA = add32(add32(newA, (b ^ c ^ d)), 0x6ED9EBA1); + } else if (i < 60) { + newA = add32(add32(newA, (b & c) | (b & d) | (c & d)), 0x8F1BBCDC); + } else { + newA = add32(add32(newA, b ^ c ^ d), 0xCA62C1D6); + } + + e = d; + d = c; + c = rotl32(b, 30); + b = a; + a = newA & mask32; + } + + digest[0] = add32(a, digest[0]); + digest[1] = add32(b, digest[1]); + digest[2] = add32(c, digest[2]); + digest[3] = add32(d, digest[3]); + digest[4] = add32(e, digest[4]); + } +} diff --git a/dart/lib/src/cryptography/utils.dart b/dart/lib/src/cryptography/utils.dart new file mode 100644 index 0000000000..9ac8efc140 --- /dev/null +++ b/dart/lib/src/cryptography/utils.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// A bitmask that limits an integer to 32 bits. +const mask32 = 0xFFFFFFFF; + +/// The number of bits in a byte. +const bitsPerByte = 8; + +/// The number of bytes in a 32-bit word. +const bytesPerWord = 4; + +/// Adds [x] and [y] with 32-bit overflow semantics. +int add32(int x, int y) => (x + y) & mask32; + +/// Bitwise rotates [val] to the left by [shift], obeying 32-bit overflow +/// semantics. +int rotl32(int val, int shift) { + var modShift = shift & 31; + return ((val << modShift) & mask32) | ((val & mask32) >> (32 - modShift)); +} diff --git a/dart/lib/src/feature_flags/xor_shift_rand.dart b/dart/lib/src/feature_flags/xor_shift_rand.dart index d93617ea26..968ca5d05a 100644 --- a/dart/lib/src/feature_flags/xor_shift_rand.dart +++ b/dart/lib/src/feature_flags/xor_shift_rand.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:crypto/crypto.dart'; +import '../cryptography/sha1.dart'; /// final rand = XorShiftRandom('wohoo'); /// rand.next(); diff --git a/dart/lib/src/typed_data/typed_buffer.dart b/dart/lib/src/typed_data/typed_buffer.dart new file mode 100644 index 0000000000..05566c202f --- /dev/null +++ b/dart/lib/src/typed_data/typed_buffer.dart @@ -0,0 +1,426 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// vendored from https://github.com/dart-lang/typed_data/blob/master/lib/src/typed_buffer.dart +// + +import 'dart:collection' show ListBase; +import 'dart:typed_data'; + +abstract class TypedDataBuffer extends ListBase { + static const int _initialLength = 8; + + /// The underlying data buffer. + /// + /// This is always both a List and a TypedData, which we don't have a type + /// for here. For example, for a `Uint8Buffer`, this is a `Uint8List`. + List _buffer; + + /// Returns a view of [_buffer] as a [TypedData]. + TypedData get _typedBuffer => _buffer as TypedData; + + /// The length of the list being built. + int _length; + + TypedDataBuffer(List buffer) + : _buffer = buffer, + _length = buffer.length; + + @override + int get length => _length; + + @override + E operator [](int index) { + if (index >= length) throw RangeError.index(index, this); + return _buffer[index]; + } + + @override + void operator []=(int index, E value) { + if (index >= length) throw RangeError.index(index, this); + _buffer[index] = value; + } + + @override + set length(int newLength) { + if (newLength < _length) { + var defaultValue = _defaultValue; + for (var i = newLength; i < _length; i++) { + _buffer[i] = defaultValue; + } + } else if (newLength > _buffer.length) { + List newBuffer; + if (_buffer.isEmpty) { + newBuffer = _createBuffer(newLength); + } else { + newBuffer = _createBiggerBuffer(newLength); + } + newBuffer.setRange(0, _length, _buffer); + _buffer = newBuffer; + } + _length = newLength; + } + + void _add(E value) { + if (_length == _buffer.length) _grow(_length); + _buffer[_length++] = value; + } + + // We override the default implementation of `add` because it grows the list + // by setting the length in increments of one. We want to grow by doubling + // capacity in most cases. + @override + void add(E element) { + _add(element); + } + + /// Appends all objects of [values] to the end of this buffer. + /// + /// This adds values from [start] (inclusive) to [end] (exclusive) in + /// [values]. If [end] is omitted, it defaults to adding all elements of + /// [values] after [start]. + /// + /// The [start] value must be non-negative. The [values] iterable must have at + /// least [start] elements, and if [end] is specified, it must be greater than + /// or equal to [start] and [values] must have at least [end] elements. + @override + void addAll(Iterable values, [int start = 0, int? end]) { + RangeError.checkNotNegative(start, 'start'); + if (end != null && start > end) { + throw RangeError.range(end, start, null, 'end'); + } + + _addAll(values, start, end); + } + + /// Inserts all objects of [values] at position [index] in this list. + /// + /// This adds values from [start] (inclusive) to [end] (exclusive) in + /// [values]. If [end] is omitted, it defaults to adding all elements of + /// [values] after [start]. + /// + /// The [start] value must be non-negative. The [values] iterable must have at + /// least [start] elements, and if [end] is specified, it must be greater than + /// or equal to [start] and [values] must have at least [end] elements. + @override + void insertAll(int index, Iterable values, [int start = 0, int? end]) { + RangeError.checkValidIndex(index, this, 'index', _length + 1); + RangeError.checkNotNegative(start, 'start'); + if (end != null) { + if (start > end) { + throw RangeError.range(end, start, null, 'end'); + } + if (start == end) return; + } + + // If we're adding to the end of the list anyway, use [_addAll]. This lets + // us avoid converting [values] into a list even if [end] is null, since we + // can add values iteratively to the end of the list. We can't do so in the + // center because copying the trailing elements every time is non-linear. + if (index == _length) { + _addAll(values, start, end); + return; + } + + if (end == null && values is List) { + end = values.length; + } + if (end != null) { + _insertKnownLength(index, values, start, end); + return; + } + + // Add elements at end, growing as appropriate, then put them back at + // position [index] using flip-by-double-reverse. + var writeIndex = _length; + var skipCount = start; + for (var value in values) { + if (skipCount > 0) { + skipCount--; + continue; + } + if (writeIndex == _buffer.length) { + _grow(writeIndex); + } + _buffer[writeIndex++] = value; + } + + if (skipCount > 0) { + throw StateError('Too few elements'); + } + if (end != null && writeIndex < end) { + throw RangeError.range(end, start, writeIndex, 'end'); + } + + // Swap [index.._length) and [_length..writeIndex) by double-reversing. + _reverse(_buffer, index, _length); + _reverse(_buffer, _length, writeIndex); + _reverse(_buffer, index, writeIndex); + _length = writeIndex; + return; + } + + // Reverses the range [start..end) of buffer. + static void _reverse(List buffer, int start, int end) { + end--; // Point to last element, not after last element. + while (start < end) { + var first = buffer[start]; + var last = buffer[end]; + buffer[end] = first; + buffer[start] = last; + start++; + end--; + } + } + + /// Does the same thing as [addAll]. + /// + /// This allows [addAll] and [insertAll] to share implementation without a + /// subclass unexpectedly overriding both when it intended to only override + /// [addAll]. + void _addAll(Iterable values, [int start = 0, int? end]) { + if (values is List) end ??= values.length; + + // If we know the length of the segment to add, do so with [addRange]. This + // way we know how much to grow the buffer in advance, and it may be even + // more efficient for typed data input. + if (end != null) { + _insertKnownLength(_length, values, start, end); + return; + } + + // Otherwise, just add values one at a time. + var i = 0; + for (var value in values) { + if (i >= start) add(value); + i++; + } + if (i < start) throw StateError('Too few elements'); + } + + /// Like [insertAll], but with a guaranteed non-`null` [start] and [end]. + void _insertKnownLength(int index, Iterable values, int start, int end) { + if (values is List) { + if (start > values.length || end > values.length) { + throw StateError('Too few elements'); + } + } + + var valuesLength = end - start; + var newLength = _length + valuesLength; + _ensureCapacity(newLength); + + _buffer.setRange( + index + valuesLength, _length + valuesLength, _buffer, index); + _buffer.setRange(index, index + valuesLength, values, start); + _length = newLength; + } + + @override + void insert(int index, E element) { + if (index < 0 || index > _length) { + throw RangeError.range(index, 0, _length); + } + if (_length < _buffer.length) { + _buffer.setRange(index + 1, _length + 1, _buffer, index); + _buffer[index] = element; + _length++; + return; + } + var newBuffer = _createBiggerBuffer(null); + newBuffer.setRange(0, index, _buffer); + newBuffer.setRange(index + 1, _length + 1, _buffer, index); + newBuffer[index] = element; + _length++; + _buffer = newBuffer; + } + + /// Ensures that [_buffer] is at least [requiredCapacity] long, + /// + /// Grows the buffer if necessary, preserving existing data. + void _ensureCapacity(int requiredCapacity) { + if (requiredCapacity <= _buffer.length) return; + var newBuffer = _createBiggerBuffer(requiredCapacity); + newBuffer.setRange(0, _length, _buffer); + _buffer = newBuffer; + } + + /// Create a bigger buffer. + /// + /// This method determines how much bigger a bigger buffer should + /// be. If [requiredCapacity] is not null, it will be at least that + /// size. It will always have at least have double the capacity of + /// the current buffer. + List _createBiggerBuffer(int? requiredCapacity) { + var newLength = _buffer.length * 2; + if (requiredCapacity != null && newLength < requiredCapacity) { + newLength = requiredCapacity; + } else if (newLength < _initialLength) { + newLength = _initialLength; + } + return _createBuffer(newLength); + } + + /// Grows the buffer. + /// + /// This copies the first [length] elements into the new buffer. + void _grow(int length) { + _buffer = _createBiggerBuffer(null)..setRange(0, length, _buffer); + } + + @override + void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { + if (end > _length) throw RangeError.range(end, 0, _length); + _setRange(start, end, iterable, skipCount); + } + + /// Like [setRange], but with no bounds checking. + void _setRange(int start, int end, Iterable source, int skipCount) { + if (source is TypedDataBuffer) { + _buffer.setRange(start, end, source._buffer, skipCount); + } else { + _buffer.setRange(start, end, source, skipCount); + } + } + + // TypedData. + + int get elementSizeInBytes => _typedBuffer.elementSizeInBytes; + + int get lengthInBytes => _length * _typedBuffer.elementSizeInBytes; + + int get offsetInBytes => _typedBuffer.offsetInBytes; + + /// Returns the underlying [ByteBuffer]. + /// + /// The returned buffer may be replaced by operations that change the [length] + /// of this list. + /// + /// The buffer may be larger than [lengthInBytes] bytes, but never smaller. + ByteBuffer get buffer => _typedBuffer.buffer; + + // Specialization for the specific type. + + // Return zero for integers, 0.0 for floats, etc. + // Used to fill buffer when changing length. + E get _defaultValue; + + // Create a new typed list to use as buffer. + List _createBuffer(int size); +} + +abstract class _IntBuffer extends TypedDataBuffer { + _IntBuffer(List buffer) : super(buffer); + + @override + int get _defaultValue => 0; +} + +abstract class _FloatBuffer extends TypedDataBuffer { + _FloatBuffer(List buffer) : super(buffer); + + @override + double get _defaultValue => 0.0; +} + +class Uint8Buffer extends _IntBuffer { + Uint8Buffer([int initialLength = 0]) : super(Uint8List(initialLength)); + + @override + Uint8List _createBuffer(int size) => Uint8List(size); +} + +class Int8Buffer extends _IntBuffer { + Int8Buffer([int initialLength = 0]) : super(Int8List(initialLength)); + + @override + Int8List _createBuffer(int size) => Int8List(size); +} + +class Uint8ClampedBuffer extends _IntBuffer { + Uint8ClampedBuffer([int initialLength = 0]) + : super(Uint8ClampedList(initialLength)); + + @override + Uint8ClampedList _createBuffer(int size) => Uint8ClampedList(size); +} + +class Uint16Buffer extends _IntBuffer { + Uint16Buffer([int initialLength = 0]) : super(Uint16List(initialLength)); + + @override + Uint16List _createBuffer(int size) => Uint16List(size); +} + +class Int16Buffer extends _IntBuffer { + Int16Buffer([int initialLength = 0]) : super(Int16List(initialLength)); + + @override + Int16List _createBuffer(int size) => Int16List(size); +} + +class Uint32Buffer extends _IntBuffer { + Uint32Buffer([int initialLength = 0]) : super(Uint32List(initialLength)); + + @override + Uint32List _createBuffer(int size) => Uint32List(size); +} + +class Int32Buffer extends _IntBuffer { + Int32Buffer([int initialLength = 0]) : super(Int32List(initialLength)); + + @override + Int32List _createBuffer(int size) => Int32List(size); +} + +class Uint64Buffer extends _IntBuffer { + Uint64Buffer([int initialLength = 0]) : super(Uint64List(initialLength)); + + @override + Uint64List _createBuffer(int size) => Uint64List(size); +} + +class Int64Buffer extends _IntBuffer { + Int64Buffer([int initialLength = 0]) : super(Int64List(initialLength)); + + @override + Int64List _createBuffer(int size) => Int64List(size); +} + +class Float32Buffer extends _FloatBuffer { + Float32Buffer([int initialLength = 0]) : super(Float32List(initialLength)); + + @override + Float32List _createBuffer(int size) => Float32List(size); +} + +class Float64Buffer extends _FloatBuffer { + Float64Buffer([int initialLength = 0]) : super(Float64List(initialLength)); + + @override + Float64List _createBuffer(int size) => Float64List(size); +} + +class Int32x4Buffer extends TypedDataBuffer { + static final Int32x4 _zero = Int32x4(0, 0, 0, 0); + + Int32x4Buffer([int initialLength = 0]) : super(Int32x4List(initialLength)); + + @override + Int32x4 get _defaultValue => _zero; + + @override + Int32x4List _createBuffer(int size) => Int32x4List(size); +} + +class Float32x4Buffer extends TypedDataBuffer { + Float32x4Buffer([int initialLength = 0]) + : super(Float32x4List(initialLength)); + + @override + Float32x4 get _defaultValue => Float32x4.zero(); + + @override + Float32x4List _createBuffer(int size) => Float32x4List(size); +} diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index d37e95008e..9019a9df33 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -11,7 +11,6 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: - crypto: ^3.0.2 # TODO: consider vendoring sha1 http: ^0.13.0 meta: ^1.3.0 stack_trace: ^1.10.0 From a9a9361b6fd275130f4d2c23303e9777e34afc64 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 26 Aug 2022 10:09:22 +0200 Subject: [PATCH 31/33] supress long method --- .../src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 69b86fd05d..8480b68544 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -103,6 +103,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { return true } + @Suppress("LongMethod") private fun initNativeSdk(call: MethodCall, result: Result) { if (!this::context.isInitialized) { result.error("1", "Context is null", null) From 163ddcf9f1357986108e76e95528a454ea5ad305 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 26 Aug 2022 10:29:37 +0200 Subject: [PATCH 32/33] vendor Object.hashAll --- dart/lib/src/cryptography/digest.dart | 3 ++- dart/lib/src/utils/hash_code.dart | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/dart/lib/src/cryptography/digest.dart b/dart/lib/src/cryptography/digest.dart index 6503fedd7c..405d6763b3 100644 --- a/dart/lib/src/cryptography/digest.dart +++ b/dart/lib/src/cryptography/digest.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:typed_data'; +import '../utils/hash_code.dart'; /// A message digest as computed by a `Hash` or `HMAC` function. class Digest { @@ -34,7 +35,7 @@ class Digest { } @override - int get hashCode => Object.hashAll(bytes); + int get hashCode => hashAll(bytes); /// The message digest as a string of hexadecimal digits. @override diff --git a/dart/lib/src/utils/hash_code.dart b/dart/lib/src/utils/hash_code.dart index 3fbcb5dd2b..cd7a7cde63 100644 --- a/dart/lib/src/utils/hash_code.dart +++ b/dart/lib/src/utils/hash_code.dart @@ -1,5 +1,5 @@ // Borrowed from https://api.dart.dev/stable/2.17.6/dart-core/Object/hash.html -// Since Object.hash(a, b) is only available from Dart 2.14 +// Since Object.hash(a, b) and Object.hashAll are only available from Dart 2.14 // A per-isolate seed for hash code computations. final int _hashSeed = identityHashCode(Object); @@ -22,3 +22,11 @@ int _finish(int hash) { hash = hash ^ (hash >> 11); return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); } + +int hashAll(Iterable objects) { + int hash = _hashSeed; + for (var object in objects) { + hash = _combine(hash, object.hashCode); + } + return _finish(hash); +} From 0fac27ec8f543d23d034bb8dcff9ecbec4dfe78e Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 26 Aug 2022 11:50:52 +0200 Subject: [PATCH 33/33] remove non used comment --- dart/lib/src/cryptography/hash_sink.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/dart/lib/src/cryptography/hash_sink.dart b/dart/lib/src/cryptography/hash_sink.dart index 7fd57c7ae6..6524ad6af2 100644 --- a/dart/lib/src/cryptography/hash_sink.dart +++ b/dart/lib/src/cryptography/hash_sink.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; -// import '../typed_data/uint8_buffer.dart'; import '../typed_data/typed_buffer.dart'; import 'digest.dart'; import 'utils.dart';