Skip to content

Commit 4adb170

Browse files
committed
Merge branch 'main' into enha/rel-platfomr-cause
2 parents 6fdbf5b + 0aa80ac commit 4adb170

15 files changed

+474
-4
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add `FeatureFlagIntegration` ([#2825](https://github.com/getsentry/sentry-dart/pull/2825))
8+
```dart
9+
// Manually track a feature flag
10+
Sentry.addFeatureFlag('my-feature', true);
11+
```
12+
513
### Behavioral changes
614

715
- Set log level to `warning` by default when `debug = true` ([#2836](https://github.com/getsentry/sentry-dart/pull/2836))

dart/lib/sentry.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export 'src/noop_isolate_error_integration.dart'
2424
export 'src/observers.dart';
2525
export 'src/performance_collector.dart';
2626
export 'src/protocol.dart';
27+
export 'src/protocol/sentry_feature_flags.dart';
28+
export 'src/protocol/sentry_feature_flag.dart';
2729
export 'src/protocol/sentry_feedback.dart';
2830
export 'src/protocol/sentry_proxy.dart';
2931
export 'src/run_zoned_guarded_integration.dart';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import 'dart:async';
2+
3+
import 'hub.dart';
4+
import 'integration.dart';
5+
import 'sentry_options.dart';
6+
import 'protocol/sentry_feature_flags.dart';
7+
import 'protocol/sentry_feature_flag.dart';
8+
9+
/// Integration which handles adding feature flags to the scope.
10+
class FeatureFlagsIntegration extends Integration<SentryOptions> {
11+
Hub? _hub;
12+
13+
@override
14+
void call(Hub hub, SentryOptions options) {
15+
_hub = hub;
16+
options.sdk.addIntegration('FeatureFlagsIntegration');
17+
}
18+
19+
FutureOr<void> addFeatureFlag(String name, bool value) async {
20+
final flags =
21+
_hub?.scope.contexts[SentryFeatureFlags.type] as SentryFeatureFlags? ??
22+
SentryFeatureFlags(values: []);
23+
final values = flags.values;
24+
25+
if (values.length >= 100) {
26+
values.removeAt(0);
27+
}
28+
29+
final index = values.indexWhere((element) => element.name == name);
30+
if (index != -1) {
31+
values[index] = SentryFeatureFlag(name: name, value: value);
32+
} else {
33+
values.add(SentryFeatureFlag(name: name, value: value));
34+
}
35+
36+
flags.values = values;
37+
38+
await _hub?.scope.setContexts(SentryFeatureFlags.type, flags);
39+
}
40+
41+
@override
42+
FutureOr<void> close() {
43+
_hub = null;
44+
}
45+
}

dart/lib/src/protocol.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ export 'protocol/sentry_view_hierarchy_element.dart';
3939
export 'protocol/span_id.dart';
4040
export 'protocol/span_status.dart';
4141
export 'sentry_event_like.dart';
42+
export 'protocol/sentry_feature_flag.dart';
43+
export 'protocol/sentry_feature_flags.dart';

dart/lib/src/protocol/contexts.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Contexts extends MapView<String, dynamic> {
2121
SentryTraceContext? trace,
2222
SentryResponse? response,
2323
SentryFeedback? feedback,
24+
SentryFeatureFlags? flags,
2425
}) : super({
2526
SentryDevice.type: device,
2627
SentryOperatingSystem.type: operatingSystem,
@@ -32,6 +33,7 @@ class Contexts extends MapView<String, dynamic> {
3233
SentryTraceContext.type: trace,
3334
SentryResponse.type: response,
3435
SentryFeedback.type: feedback,
36+
SentryFeatureFlags.type: flags,
3537
});
3638

3739
/// Deserializes [Contexts] from JSON [Map].
@@ -68,6 +70,9 @@ class Contexts extends MapView<String, dynamic> {
6870
feedback: data[SentryFeedback.type] != null
6971
? SentryFeedback.fromJson(Map.from(data[SentryFeedback.type]))
7072
: null,
73+
flags: data[SentryFeatureFlags.type] != null
74+
? SentryFeatureFlags.fromJson(Map.from(data[SentryFeatureFlags.type]))
75+
: null,
7176
);
7277

7378
data.keys
@@ -151,6 +156,11 @@ class Contexts extends MapView<String, dynamic> {
151156

152157
set feedback(SentryFeedback? value) => this[SentryFeedback.type] = value;
153158

159+
/// Feature flags context for a feature flag event.
160+
SentryFeatureFlags? get flags => this[SentryFeatureFlags.type];
161+
162+
set flags(SentryFeatureFlags? value) => this[SentryFeatureFlags.type] = value;
163+
154164
/// Produces a [Map] that can be serialized to JSON.
155165
Map<String, dynamic> toJson() {
156166
final json = <String, dynamic>{};
@@ -250,6 +260,13 @@ class Contexts extends MapView<String, dynamic> {
250260

251261
break;
252262

263+
case SentryFeatureFlags.type:
264+
final flagsMap = flags?.toJson();
265+
if (flagsMap?.isNotEmpty ?? false) {
266+
json[SentryFeatureFlags.type] = flagsMap;
267+
}
268+
break;
269+
253270
default:
254271
if (value != null) {
255272
json[key] = value;
@@ -273,6 +290,7 @@ class Contexts extends MapView<String, dynamic> {
273290
response: response?.clone(),
274291
runtimes: runtimes.map((runtime) => runtime.clone()).toList(),
275292
feedback: feedback?.clone(),
293+
flags: flags?.clone(),
276294
)..addEntries(
277295
entries.where((element) => !_defaultFields.contains(element.key)),
278296
);
@@ -293,6 +311,7 @@ class Contexts extends MapView<String, dynamic> {
293311
SentryTraceContext? trace,
294312
SentryResponse? response,
295313
SentryFeedback? feedback,
314+
SentryFeatureFlags? flags,
296315
}) =>
297316
Contexts(
298317
device: device ?? this.device,
@@ -306,6 +325,7 @@ class Contexts extends MapView<String, dynamic> {
306325
trace: trace ?? this.trace,
307326
response: response ?? this.response,
308327
feedback: feedback ?? this.feedback,
328+
flags: flags ?? this.flags,
309329
)..addEntries(
310330
entries.where((element) => !_defaultFields.contains(element.key)),
311331
);
@@ -322,5 +342,6 @@ class Contexts extends MapView<String, dynamic> {
322342
SentryTraceContext.type,
323343
SentryResponse.type,
324344
SentryFeedback.type,
345+
SentryFeatureFlags.type,
325346
];
326347
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:meta/meta.dart';
2+
3+
import 'access_aware_map.dart';
4+
5+
class SentryFeatureFlag {
6+
final String name;
7+
final bool value;
8+
9+
@internal
10+
final Map<String, dynamic>? unknown;
11+
12+
SentryFeatureFlag({
13+
required this.name,
14+
required this.value,
15+
this.unknown,
16+
});
17+
18+
factory SentryFeatureFlag.fromJson(Map<String, dynamic> data) {
19+
final json = AccessAwareMap(data);
20+
21+
return SentryFeatureFlag(
22+
name: json['name'],
23+
value: json['value'],
24+
unknown: json.notAccessed(),
25+
);
26+
}
27+
28+
Map<String, dynamic> toJson() {
29+
return {
30+
...?unknown,
31+
'name': name,
32+
'value': value,
33+
};
34+
}
35+
36+
@Deprecated('Assign values directly to the instance.')
37+
SentryFeatureFlag copyWith({
38+
String? name,
39+
bool? value,
40+
Map<String, dynamic>? unknown,
41+
}) {
42+
return SentryFeatureFlag(
43+
name: name ?? this.name,
44+
value: value ?? this.value,
45+
unknown: unknown ?? this.unknown,
46+
);
47+
}
48+
49+
@Deprecated('Will be removed in a future version.')
50+
SentryFeatureFlag clone() => copyWith();
51+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:meta/meta.dart';
2+
import 'sentry_feature_flag.dart';
3+
import 'access_aware_map.dart';
4+
5+
class SentryFeatureFlags {
6+
static const type = 'flags';
7+
8+
List<SentryFeatureFlag> values;
9+
10+
@internal
11+
Map<String, dynamic>? unknown;
12+
13+
SentryFeatureFlags({
14+
required this.values,
15+
this.unknown,
16+
});
17+
18+
factory SentryFeatureFlags.fromJson(Map<String, dynamic> data) {
19+
final json = AccessAwareMap(data);
20+
21+
final valuesValues = json['values'] as List<dynamic>?;
22+
final values = valuesValues
23+
?.map((e) => SentryFeatureFlag.fromJson(e))
24+
.toList(growable: false);
25+
26+
return SentryFeatureFlags(
27+
values: values ?? [],
28+
unknown: json.notAccessed(),
29+
);
30+
}
31+
32+
Map<String, dynamic> toJson() {
33+
return {
34+
...?unknown,
35+
'values': values.map((e) => e.toJson()).toList(growable: false),
36+
};
37+
}
38+
39+
@Deprecated('Assign values directly to the instance.')
40+
SentryFeatureFlags copyWith({
41+
List<SentryFeatureFlag>? values,
42+
Map<String, dynamic>? unknown,
43+
}) {
44+
return SentryFeatureFlags(
45+
values: values ??
46+
this.values.map((e) => e.copyWith()).toList(growable: false),
47+
unknown: unknown ?? this.unknown,
48+
);
49+
}
50+
51+
@Deprecated('Will be removed in a future version.')
52+
SentryFeatureFlags clone() => copyWith();
53+
}

dart/lib/src/sentry.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'sentry_run_zoned_guarded.dart';
2626
import 'tracing.dart';
2727
import 'transport/data_category.dart';
2828
import 'transport/task_queue.dart';
29+
import 'feature_flags_integration.dart';
2930

3031
/// Configuration options callback
3132
typedef OptionsConfiguration = FutureOr<void> Function(SentryOptions);
@@ -106,6 +107,8 @@ class Sentry {
106107
options.addIntegration(LoadDartDebugImagesIntegration());
107108
}
108109

110+
options.addIntegration(FeatureFlagsIntegration());
111+
109112
options.addEventProcessor(EnricherEventProcessor(options));
110113
options.addEventProcessor(ExceptionEventProcessor(options));
111114
options.addEventProcessor(DeduplicationEventProcessor(options));
@@ -361,6 +364,22 @@ class Sentry {
361364
/// Gets the current active transaction or span bound to the scope.
362365
static ISentrySpan? getSpan() => _hub.getSpan();
363366

367+
static Future<void> addFeatureFlag(String name, bool value) async {
368+
final featureFlagsIntegration = currentHub.options.integrations
369+
.whereType<FeatureFlagsIntegration>()
370+
.firstOrNull;
371+
372+
if (featureFlagsIntegration == null) {
373+
currentHub.options.logger(
374+
SentryLevel.warning,
375+
'$FeatureFlagsIntegration not found. Make sure Sentry is initialized before accessing the addFeatureFlag API.',
376+
);
377+
return;
378+
}
379+
380+
await featureFlagsIntegration.addFeatureFlag(name, value);
381+
}
382+
364383
@internal
365384
static Hub get currentHub => _hub;
366385

dart/test/contexts_test.dart

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,21 @@ void main() {
4747

4848
final gpu = SentryGpu(name: 'Radeon', version: '1');
4949

50+
final flags = SentryFeatureFlags(
51+
values: [
52+
SentryFeatureFlag(name: 'feature_flag_1', value: true),
53+
SentryFeatureFlag(name: 'feature_flag_2', value: false),
54+
],
55+
);
56+
5057
final contexts = Contexts(
5158
device: testDevice,
5259
operatingSystem: testOS,
5360
runtimes: testRuntimes,
5461
app: testApp,
5562
browser: testBrowser,
5663
gpu: gpu,
64+
flags: flags,
5765
)
5866
..['theme'] = {'value': 'material'}
5967
..['version'] = {'value': 9};
@@ -94,6 +102,12 @@ void main() {
94102
'testrt2': {'name': 'testRT2', 'type': 'runtime', 'version': '2.3.1'},
95103
'theme': {'value': 'material'},
96104
'version': {'value': 9},
105+
'flags': {
106+
'values': [
107+
{'name': 'feature_flag_1', 'value': true},
108+
{'name': 'feature_flag_2', 'value': false},
109+
]
110+
},
97111
};
98112

99113
test('serializes to JSON', () {
@@ -122,7 +136,7 @@ void main() {
122136
expect(
123137
clone.operatingSystem!.toJson(), contexts.operatingSystem!.toJson());
124138
expect(clone.gpu!.toJson(), contexts.gpu!.toJson());
125-
139+
expect(clone.flags!.toJson(), contexts.flags!.toJson());
126140
for (final element in contexts.runtimes) {
127141
expect(
128142
clone.runtimes.where(
@@ -185,6 +199,17 @@ void main() {
185199
expect(contexts.runtimes.length, 2);
186200
expect(contexts.runtimes.last.name, 'testRT2');
187201
});
202+
203+
test('set flags', () {
204+
final contexts = Contexts();
205+
contexts.flags = SentryFeatureFlags(
206+
values: [
207+
SentryFeatureFlag(name: 'feature_flag_1', value: true),
208+
SentryFeatureFlag(name: 'feature_flag_2', value: false),
209+
],
210+
);
211+
expect(contexts.flags!.toJson(), flags.toJson());
212+
});
188213
});
189214

190215
group('parse contexts', () {
@@ -295,7 +320,12 @@ const jsonContexts = '''
295320
"raw_description":"runtime description RT1 1.0"
296321
},
297322
"browser": {"version": "12.3.4"},
298-
"gpu": {"name": "Radeon", "version": "1"}
299-
323+
"gpu": {"name": "Radeon", "version": "1"},
324+
"flags": {
325+
"values": [
326+
{"name": "feature_flag_1", "value": true},
327+
{"name": "feature_flag_2", "value": false}
328+
]
329+
}
300330
}
301331
''';

0 commit comments

Comments
 (0)