Skip to content

Commit

Permalink
Survey handler functionality to fetch available surveys (#91)
Browse files Browse the repository at this point in the history
* Add constant for endpoint that stores metadata json file

* Development began on survey handler class with fetch

* Update survey_handler.dart

* Parsing functionality added in `survey_handler`

* Condition class `operator` relabeled to `operatorString`

* `Analytics` test and default constructors to use `SurveyHandler`

* Refactor + cleanup + error handling

* `dart format` fix

* Evaluating functionality added to `Analytics`

* Format fix

* `!=` operator added to `Condition` class

* Refactor for fake survey handler to use list of surveys or string

* Initial test cases added

* Tests added to use json in `FakeSurveyHandler`

* Fix nit

* Early exit if on null `logFileStats`

* Test to check each field in `Survey` and `Condition`

* Documentation update
  • Loading branch information
eliasyishak authored May 18, 2023
1 parent 0eb4141 commit 681f712
Show file tree
Hide file tree
Showing 8 changed files with 761 additions and 15 deletions.
4 changes: 4 additions & 0 deletions pkgs/unified_analytics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.2.0

- Added `SurveyHandler` feature to `Analytics` instance to fetch available surveys from remote endpoint to display to users

## 1.1.1

- Refactoring `dateStamp` utility function to be defined in `utils.dart` instead of having static methods in `Initializer` and `ConfigHandler`
Expand Down
93 changes: 80 additions & 13 deletions pkgs/unified_analytics/lib/src/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'ga_client.dart';
import 'initializer.dart';
import 'log_handler.dart';
import 'session.dart';
import 'survey_handler.dart';
import 'user_property.dart';
import 'utils.dart';

Expand Down Expand Up @@ -62,6 +63,7 @@ abstract class Analytics {
toolsMessageVersion: kToolsMessageVersion,
fs: fs,
gaClient: gaClient,
surveyHandler: const SurveyHandler(),
);
}

Expand Down Expand Up @@ -109,6 +111,7 @@ abstract class Analytics {
toolsMessageVersion: kToolsMessageVersion,
fs: fs,
gaClient: gaClient,
surveyHandler: const SurveyHandler(),
);
}

Expand All @@ -125,8 +128,9 @@ abstract class Analytics {
required String dartVersion,
int toolsMessageVersion = kToolsMessageVersion,
String toolsMessage = kToolsMessage,
FileSystem? fs,
required FileSystem fs,
required DevicePlatform platform,
SurveyHandler? surveyHandler,
}) =>
TestAnalytics(
tool: tool,
Expand All @@ -136,13 +140,10 @@ abstract class Analytics {
flutterVersion: flutterVersion,
dartVersion: dartVersion,
platform: platform,
fs: fs ??
MemoryFileSystem.test(
style: io.Platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
),
fs: fs,
gaClient: FakeGAClient(),
surveyHandler:
surveyHandler ?? FakeSurveyHandler.fromList(initializedSurveys: []),
);

/// Retrieves the consent message to prompt users with on first
Expand Down Expand Up @@ -180,6 +181,15 @@ abstract class Analytics {
/// that need to be sent off
void close();

/// Method to fetch surveys from the specified endpoint [kContextualSurveyUrl]
///
/// Any survey that is returned by this method has already passed
/// the survey conditions specified in the remote survey metadata file
///
/// If the method returns an empty list, then there are no surveys to be
/// shared with the user
Future<List<Survey>> fetchAvailableSurveys();

/// Query the persisted event data stored on the user's machine
///
/// Returns null if there are no persisted logs
Expand All @@ -204,6 +214,7 @@ class AnalyticsImpl implements Analytics {
final FileSystem fs;
late final ConfigHandler _configHandler;
final GAClient _gaClient;
final SurveyHandler _surveyHandler;
late final String _clientId;
late final File _clientIdFile;
late final UserProperty userProperty;
Expand Down Expand Up @@ -239,8 +250,10 @@ class AnalyticsImpl implements Analytics {
required DevicePlatform platform,
required this.toolsMessageVersion,
required this.fs,
required gaClient,
}) : _gaClient = gaClient {
required GAClient gaClient,
required SurveyHandler surveyHandler,
}) : _gaClient = gaClient,
_surveyHandler = surveyHandler {
// Initialize date formatting for `package:intl` within constructor
// so clients using this package won't need to
initializeDateFormatting();
Expand Down Expand Up @@ -366,6 +379,56 @@ class AnalyticsImpl implements Analytics {
@override
void close() => _gaClient.close();

@override
Future<List<Survey>> fetchAvailableSurveys() async {
final List<Survey> surveysToShow = [];
final LogFileStats? logFileStats = _logHandler.logFileStats();

if (logFileStats == null) return [];

for (final Survey survey in await _surveyHandler.fetchSurveyList()) {
// Counter to check each survey condition, if all are met, then
// this integer will be equal to the number of conditions in
// [Survey.conditionList]
int conditionsMet = 0;
for (final Condition condition in survey.conditionList) {
// Retrieve the value from the [LogFileStats] with
// the label provided in the condtion
final int? logFileStatsValue =
logFileStats.getValueByString(condition.field);

if (logFileStatsValue == null) continue;

switch (condition.operatorString) {
case '>=':
if (logFileStatsValue >= condition.value) conditionsMet++;
break;
case '<=':
if (logFileStatsValue <= condition.value) conditionsMet++;
break;
case '>':
if (logFileStatsValue > condition.value) conditionsMet++;
break;
case '<':
if (logFileStatsValue < condition.value) conditionsMet++;
break;
case '==':
if (logFileStatsValue == condition.value) conditionsMet++;
break;
case '!=':
if (logFileStatsValue != condition.value) conditionsMet++;
break;
}
}

if (conditionsMet == survey.conditionList.length) {
surveysToShow.add(survey);
}
}

return surveysToShow;
}

@override
LogFileStats? logFileStats() => _logHandler.logFileStats();

Expand Down Expand Up @@ -432,10 +495,6 @@ class AnalyticsImpl implements Analytics {
/// This is for clients that opt to either not send analytics, or will migrate
/// to use [AnalyticsImpl] at a later time.
class NoOpAnalytics implements Analytics {
const NoOpAnalytics._();

factory NoOpAnalytics() => const NoOpAnalytics._();

@override
final String getConsentMessage = '';

Expand All @@ -455,12 +514,19 @@ class NoOpAnalytics implements Analytics {
final Map<String, Map<String, Object?>> userPropertyMap =
const <String, Map<String, Object?>>{};

factory NoOpAnalytics() => const NoOpAnalytics._();

const NoOpAnalytics._();

@override
void clientShowedMessage() {}

@override
void close() {}

@override
Future<List<Survey>> fetchAvailableSurveys() async => const <Survey>[];

@override
LogFileStats? logFileStats() => null;

Expand Down Expand Up @@ -492,6 +558,7 @@ class TestAnalytics extends AnalyticsImpl {
required super.toolsMessageVersion,
required super.fs,
required super.gaClient,
required super.surveyHandler,
});

@override
Expand Down
6 changes: 5 additions & 1 deletion pkgs/unified_analytics/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ reporting=1
# a number representing the version of the message that was
# displayed.''';

/// Link to contextual survey metadata file
const String kContextualSurveyUrl =
'https://docs.flutter.dev/f/contextual-survey-metadata.json';

/// Name of the directory where all of the files necessary for this package
/// will be located
const String kDartToolDirectoryName = '.dart-tool';
Expand All @@ -70,7 +74,7 @@ const int kLogFileLength = 2500;
const String kLogFileName = 'dart-flutter-telemetry.log';

/// The current version of the package, should be in line with pubspec version.
const String kPackageVersion = '1.1.1';
const String kPackageVersion = '1.2.0';

/// The minimum length for a session
const int kSessionDurationMinutes = 30;
Expand Down
31 changes: 31 additions & 0 deletions pkgs/unified_analytics/lib/src/log_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,37 @@ class LogFileStats {
'recordCount': recordCount,
'eventCount': eventCount,
});

/// Pass in a string label for one of the instance variables
/// and return the integer value of that label
///
/// If label passed for [DateTime] instance variable, integer
/// in the form of [DateTime.millisecondsSinceEpoch] will be
/// returned
///
/// Returns null if the label passed does not match anything
int? getValueByString(String label) {
switch (label) {
case 'logFileStats.startDateTime':
return startDateTime.millisecondsSinceEpoch;
case 'logFileStats.minsFromStartDateTime':
return minsFromStartDateTime;
case 'logFileStats.endDateTime':
return endDateTime.millisecondsSinceEpoch;
case 'logFileStats.minsFromEndDateTime':
return minsFromEndDateTime;
case 'logFileStats.sessionCount':
return sessionCount;
case 'logFileStats.flutterChannelCount':
return flutterChannelCount;
case 'logFileStats.toolCount':
return toolCount;
case 'logFileStats.recordCount':
return recordCount;
}

return null;
}
}

/// This class is responsible for writing to a log
Expand Down
Loading

0 comments on commit 681f712

Please sign in to comment.