Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/sdk-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:
jobs:
sdk-spec:
# Keep the ref in `uses:` and `spec-ref` in sync.
uses: Flagsmith/sdk-spec/.github/workflows/run-spec.yml@v0.5.0
uses: Flagsmith/sdk-spec/.github/workflows/run-spec.yml@v0.7.0
with:
spec-ref: v0.5.0
spec-ref: v0.7.0
language: dart
13 changes: 9 additions & 4 deletions lib/src/flagsmith_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ class FlagsmithClient {

Future<void> _setupAnalyticsTimer(int analyticsInterval) async {
_analyticsTimer?.cancel();
_analyticsTimer = Timer.periodic(
Duration(milliseconds: analyticsInterval), (_) => syncAnalyticsData());
// Failures are logged, and the analytics are resent on the next tick.
_analyticsTimer = Timer.periodic(Duration(milliseconds: analyticsInterval),
(_) => syncAnalyticsData().ignore());
}

Future<Response<dynamic>?> syncAnalyticsData() async {
Expand Down Expand Up @@ -379,8 +380,9 @@ class FlagsmithClient {
/// EXPERIMENTS
///
/// Resolve a flag for [user] (or [cachedUser]) and fire one `$flag_exposure`
/// event with the variant as value. Skipped unless events are enabled, the
/// flag is enabled and `flag.experiment.inExperiment` is true.
/// event with the variant as value and [traits] attached. Skipped unless
/// events are enabled, the flag is enabled and
/// `flag.experiment.inExperiment` is true.
///
/// Fetches only when storage holds another identity's flags;
/// [reload] overrides this.
Expand Down Expand Up @@ -426,6 +428,9 @@ class FlagsmithClient {
trackExposureEvent(featureName,
user: identity,
value: flag.variant,
traits: traits == null || traits.isEmpty
? null
: {for (final trait in traits) trait.key: trait.value},
metadata: <String, dynamic>{'experiment_id': experiment.id});
return flag;
}
Expand Down
46 changes: 46 additions & 0 deletions sdk-spec/steps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ Future<FlagsmithClient> createClient(String environmentKey, String apiUrl) =>
config: FlagsmithConfig(baseURI: apiUrl),
);

// Create the SDK client with events enabled
Future<FlagsmithClient> createClientWithEvents(
String environmentKey, String apiUrl, String eventsApiUrl) =>
FlagsmithClient.init(
apiKey: environmentKey,
config: FlagsmithConfig(
baseURI: apiUrl,
enableEvents: true,
eventsURI: eventsApiUrl,
),
);

// When the application gets the environment flags
Future<List<Map<String, Object?>>> getEnvironmentFlags(
FlagsmithClient flagsmith) async =>
Expand All @@ -24,6 +36,40 @@ Future<List<Map<String, Object?>>> getIdentityFlags(FlagsmithClient flagsmith,
],
));

// When the application gets the experiment flag "{feature}" for identity "{identifier}" with traits:
Future<List<Map<String, Object?>>> getExperimentFlag(FlagsmithClient flagsmith,
String feature, String identifier, Map<String, Object?> traits) async {
final flag = await flagsmith.getExperimentFlag(
feature,
user: Identity(identifier: identifier),
traits: [
for (final MapEntry(:key, :value) in traits.entries)
Trait(key: key, value: value),
],
);
return asSpecFlags([if (flag != null) flag]);
}

// When the application tracks event "{event}" for identity "{identifier}" with value {value} and metadata:
void trackEvent(FlagsmithClient flagsmith, String event, String identifier,
Object? value, Map<String, Object?> metadata) =>
flagsmith.trackEvent(
event,
user: Identity(identifier: identifier),
value: value,
metadata: metadata,
);

// When the application tracks an exposure of feature "{feature}" for identity "{identifier}" with value {value} and metadata:
void trackExposureEvent(FlagsmithClient flagsmith, String feature,
String identifier, Object? value, Map<String, Object?> metadata) =>
flagsmith.trackExposureEvent(
feature,
user: Identity(identifier: identifier),
value: value,
metadata: metadata,
);

List<Map<String, Object?>> asSpecFlags(List<Flag> flags) => [
for (final flag in flags)
{
Expand Down
45 changes: 45 additions & 0 deletions test/fg/flagsmith_analytics_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flagsmith/flagsmith.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:test/test.dart';

import '../shared.dart';
Expand Down Expand Up @@ -91,5 +93,48 @@ void main() {

expect(fs.flagAnalytics.isEmpty, isFalse);
});
test(
'When scheduled analytics fail to send, then the app keeps running '
'and keeps the analytics', () async {
// Given
final scheduled = await FlagsmithClient.init(
apiKey: apiKey,
seeds: seeds,
config: const FlagsmithConfig(
baseURI: 'https://offline.net/',
caches: true,
analyticsInterval: 10,
),
);
addTearDown(scheduled.close);
var failures = 0;
final secondFailure = Completer<void>();
scheduled.client.interceptors.add(InterceptorsWrapper(onError: (e, h) {
if (e.requestOptions.path == scheduled.config.analyticsURI) {
failures++;
if (failures == 2) {
secondFailure.complete();
}
}
h.next(e);
}));
setupEmptyAdapter(scheduled, cb: (config, adapter) {
adapter.onPost(config.analyticsURI, (server) {
return server.throws(
404,
DioException(
requestOptions: RequestOptions(path: config.analyticsURI),
),
);
}, data: Matchers.any);
});

// When
await scheduled.getFeatureFlagValue('my_feature');
await secondFailure.future.timeout(const Duration(seconds: 1));

// Then
expect(scheduled.flagAnalytics['my_feature'], 1);
});
});
}
21 changes: 21 additions & 0 deletions test/fg/flagsmith_experiments_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ void main() {
expect(fs.eventProcessor!.buffer, isEmpty);
});

test('When identity is enrolled with traits, then exposure carries them',
() async {
// Given
final traits = [
Trait(key: 'plan', value: 'premium'),
Trait(key: 'age', value: 30),
];

// When
await fs.getExperimentFlag(experimentFeatureName,
user: user, traits: traits);
await fs.flushEvents();

// Then
final event = capture.events.single;
expect(event['event'], r'$flag_exposure');
expect(event['identifier'], user.identifier);
expect(event['value'], experimentVariant);
expect(event['traits'], {'plan': 'premium', 'age': 30});
});

test('When identity is not enrolled, then no exposure', () async {
final flag = await fs.getExperimentFlag(experimentNotEnrolledFeatureName);
expect(flag!.variant, 'control');
Expand Down
Loading