Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HackWeek - Feature Flags Support #984

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
212f896
HackWeek - feature flags
marandaneto Aug 22, 2022
4f336aa
add import
marandaneto Aug 22, 2022
d75831f
parsing of the spec
marandaneto Aug 22, 2022
14d7c9c
add type enum and rename evaluation to evaluation rule
marandaneto Aug 22, 2022
de64e1d
add feature flags dump and transform it to a map
marandaneto Aug 22, 2022
fb53a05
expose fetch feature flags api
marandaneto Aug 22, 2022
5a9f9be
add test for parsing feature flag response
marandaneto Aug 22, 2022
2cef8f6
fix dsn parsing and change request to post
marandaneto Aug 22, 2022
2381b41
add payload field
marandaneto Aug 22, 2022
c7dab0d
temp commit
marandaneto Aug 22, 2022
db85e99
added random number generator
marandaneto Aug 23, 2022
8ccccc0
add default value
marandaneto Aug 23, 2022
726b47d
add caching of feature flags
marandaneto Aug 23, 2022
4329bd1
fix
marandaneto Aug 23, 2022
264889e
fix
marandaneto Aug 23, 2022
901d883
implement fetch for flutter transport
marandaneto Aug 23, 2022
b662c62
fix get feature flag info
marandaneto Aug 24, 2022
4de969b
fixes
marandaneto Aug 24, 2022
eeab0aa
add method with generics
marandaneto Aug 24, 2022
c0b5c80
remove non used code
marandaneto Aug 24, 2022
9226546
revert mocked transport
marandaneto Aug 24, 2022
4f8507c
ref
marandaneto Aug 24, 2022
f794350
fix type check
marandaneto Aug 24, 2022
5f7505a
read traces sample rate and error traces rate automatically
marandaneto Aug 24, 2022
f93e430
add group
marandaneto Aug 25, 2022
1f323e8
fix tests
marandaneto Aug 25, 2022
14a6602
fetch feature flags on start
marandaneto Aug 25, 2022
7c6b89c
fix tests
marandaneto Aug 26, 2022
45bb842
fix broken tests
marandaneto Aug 26, 2022
c4a77ae
vendor sha1
marandaneto Aug 26, 2022
a9a9361
supress long method
marandaneto Aug 26, 2022
163ddcf
vendor Object.hashAll
marandaneto Aug 26, 2022
0fac27e
remove non used comment
marandaneto Aug 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 22 additions & 63 deletions dart/example/bin/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:collection';

import 'package:sentry/sentry.dart';

Expand All @@ -11,84 +12,42 @@ import 'event_example.dart';
/// Sends a test exception report to Sentry.io using this Dart client.
Future<void> 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
..dsn = dsn
..debug = true
..sendDefaultPii = true
..addEventProcessor(TagEventProcessor()),
..release = 'myapp@1.0.0+1'
..environment = 'prod',
appRunner: runApp,
);
}

Future<void> 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: <String, String>{'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');
await scope.setUser(
SentryUser(
id: '800',
),
);
});

// 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'],
final enabled = await Sentry.isFeatureEnabled(
'@@accessToProfiling',
defaultValue: false,
context: (myContext) => {
myContext.tags['stickyId'] = 'myCustomStickyId',
},
);
print(enabled);

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();
// TODO: does it return the active EvaluationRule? do we create a new model for that?
final flag = await Sentry.getFeatureFlagInfo('test');
}

Future<void> runApp() async {}

Future<void> loadConfig() async {
await parseConfig();
}
Expand Down
7 changes: 7 additions & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ 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';
// feature flags
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';
37 changes: 37 additions & 0 deletions dart/lib/src/feature_flags/evaluation_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:meta/meta.dart';
import 'evaluation_type.dart';

@immutable
class EvaluationRule {
final EvaluationType type;
final double? percentage;
final dynamic result;
final Map<String, dynamic> _tags;
final Map<String, dynamic>? _payload;

Map<String, dynamic> get tags => Map.unmodifiable(_tags);

Map<String, dynamic>? get payload =>
_payload != null ? Map.unmodifiable(_payload!) : null;

EvaluationRule(
this.type,
this.percentage,
this.result,
this._tags,
this._payload,
);

factory EvaluationRule.fromJson(Map<String, dynamic> json) {
final payload = json['payload'];
final tags = json['tags'];

return EvaluationRule(
(json['type'] as String).toEvaluationType(),
json['percentage'] as double?,
json['result'],
tags != null ? Map<String, dynamic>.from(tags) : {},
payload != null ? Map<String, dynamic>.from(payload) : null,
);
}
}
18 changes: 18 additions & 0 deletions dart/lib/src/feature_flags/evaluation_type.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
24 changes: 24 additions & 0 deletions dart/lib/src/feature_flags/feature_dump.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:meta/meta.dart';

import 'feature_flag.dart';

@immutable
class FeatureDump {
final Map<String, FeatureFlag> featureFlags;

FeatureDump(this.featureFlags);

factory FeatureDump.fromJson(Map<String, dynamic> json) {
final featureFlagsJson = json['feature_flags'] as Map?;
Map<String, FeatureFlag> featureFlags = {};

if (featureFlagsJson != null) {
for (final value in featureFlagsJson.entries) {
final featureFlag = FeatureFlag.fromJson(value.value);
featureFlags[value.key] = featureFlag;
}
}

return FeatureDump(featureFlags);
}
}
29 changes: 29 additions & 0 deletions dart/lib/src/feature_flags/feature_flag.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:meta/meta.dart';

import 'evaluation_rule.dart';

@immutable
class FeatureFlag {
// final Map<String, dynamic> _tags;
final List<EvaluationRule> _evaluations;
final String kind;

// Map<String, dynamic> get tags => Map.unmodifiable(_tags);

List<EvaluationRule> get evaluations => List.unmodifiable(_evaluations);

FeatureFlag(this.kind, this._evaluations);

factory FeatureFlag.fromJson(Map<String, dynamic> json) {
final kind = json['kind'];
final evaluationsList = json['evaluation'] as List<dynamic>? ?? [];
final evaluations = evaluationsList
.map((e) => EvaluationRule.fromJson(e))
.toList(growable: false);

return FeatureFlag(
kind,
evaluations,
);
}
}
11 changes: 11 additions & 0 deletions dart/lib/src/feature_flags/feature_flag_context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
typedef FeatureFlagContextCallback = void Function(FeatureFlagContext context);

class FeatureFlagContext {
// String stickyId;
// String userId;
// String deviceId;
Map<String, dynamic> tags = {};

// FeatureFlagContext(this.stickyId, this.userId, this.deviceId, this.tags);
FeatureFlagContext(this.tags);
}
47 changes: 47 additions & 0 deletions dart/lib/src/feature_flags/xor_shift_rand.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'dart:convert';

import 'package:crypto/crypto.dart';

/// final rand = XorShiftRandom('wohoo');
/// rand.next();
class XorShiftRandom {
List<int> 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];
}
}
57 changes: 57 additions & 0 deletions dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,63 @@ class Hub {
}
return event;
}

Future<bool> 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.",
);
return defaultValue;
}

try {
final item = _peek();

return item.client.isFeatureEnabled(
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;
}

Future<FeatureFlag?> 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 {
Expand Down
18 changes: 18 additions & 0 deletions dart/lib/src/hub_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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';
Expand Down Expand Up @@ -158,4 +160,20 @@ class HubAdapter implements Hub {
String transaction,
) =>
Sentry.currentHub.setSpanContext(throwable, span, transaction);

@override
Future<bool> isFeatureEnabled(
String key, {
bool defaultValue = false,
FeatureFlagContextCallback? context,
}) =>
Sentry.isFeatureEnabled(
key,
defaultValue: defaultValue,
context: context,
);

@override
Future<FeatureFlag?> getFeatureFlagInfo(String key) =>
Sentry.getFeatureFlagInfo(key);
}
Loading