Skip to content

Sampling rate functionality added #122

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

Merged
6 changes: 5 additions & 1 deletion pkgs/unified_analytics/lib/src/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,11 @@ class AnalyticsImpl implements Analytics {
}
}

if (conditionsMet == survey.conditionList.length) {
// If all conditions are met above, a double value will be generated from
// the clientID and survey description strings and compared against the
// sampling rate found in the survey
if (conditionsMet == survey.conditionList.length &&
survey.samplingRate >= sampleRate(_clientId, survey.uniqueId)) {
surveysToShow.add(survey);
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkgs/unified_analytics/lib/src/survey_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ class SurveyHandler {

/// Retrieves the survey metadata file from [kContextualSurveyUrl]
Future<List<Survey>> fetchSurveyList() async {
final List<Map<String, dynamic>> body;
final List<dynamic> body;
try {
final payload = await _fetchContents();
body = jsonDecode(payload) as List<Map<String, dynamic>>;
body = jsonDecode(payload) as List<dynamic>;
// ignore: avoid_catches_without_on_clauses
} catch (err) {
return [];
Expand Down
23 changes: 23 additions & 0 deletions pkgs/unified_analytics/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ Directory? getHomeDirectory(FileSystem fs) {
return fs.directory(home);
}

/// Function used to convert a string into an integer value
int intFromString(String input) {
const lookup = 'abcdefghijklmnopqrstuvwxyz0123456789-';
var result = 1;

for (var codeUnit in input.codeUnits) {
var char = String.fromCharCode(codeUnit);
var val = lookup.indexOf(char);
if (val > 0) result = result + val;
}

return result;
}

/// Returns `true` if user has opted out of legacy analytics in Dart or Flutter
///
/// Checks legacy opt-out status for the Flutter
Expand Down Expand Up @@ -181,6 +195,15 @@ bool legacyOptOut({
return false;
}

/// Will use two strings to produce a double for applying a sampling
/// rate for [Survey] to be returned to the user
double sampleRate(String string1, String string2) {
final int1 = intFromString(string1);
final int2 = intFromString(string2);

return ((int1 + int2) % 101) / 100;
}

/// A UUID generator.
///
/// This will generate unique IDs in the format:
Expand Down
130 changes: 124 additions & 6 deletions pkgs/unified_analytics/test/survey_handler_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:unified_analytics/src/constants.dart';

import 'package:unified_analytics/src/survey_handler.dart';
import 'package:unified_analytics/src/utils.dart';
import 'package:unified_analytics/unified_analytics.dart';

void main() {
Expand Down Expand Up @@ -82,6 +85,7 @@ void main() {
}
]
''';
// The value for the condition is not a valid integer
final invalidContents = '''
[
{
Expand Down Expand Up @@ -141,11 +145,23 @@ void main() {
late Analytics analytics;
late Directory homeDirectory;
late FileSystem fs;
late File clientIdFile;

setUp(() {
fs = MemoryFileSystem.test(style: FileSystemStyle.posix);
homeDirectory = fs.directory('home');

// Write the client ID file out so that we don't get
// a randomly assigned id for this test generated within
// the analytics constructor
clientIdFile = fs.file(p.join(
homeDirectory.path,
kDartToolDirectoryName,
kClientIdFileName,
));
clientIdFile.createSync(recursive: true);
clientIdFile.writeAsStringSync('string1');

final initialAnalytics = Analytics.test(
tool: DashTool.flutterTool,
homeDirectory: homeDirectory,
Expand Down Expand Up @@ -178,7 +194,7 @@ void main() {
'description',
10,
'moreInfoUrl',
0.1,
1.0,
<Condition>[
Condition('logFileStats.recordCount', '>=', 50),
Condition('logFileStats.toolCount', '>', 0),
Expand Down Expand Up @@ -262,7 +278,7 @@ void main() {
"description": "description123",
"dismissForDays": "10",
"moreInfoURL": "moreInfoUrl123",
"samplingRate": "0.1",
"samplingRate": "1.0",
"conditions": [
{
"field": "logFileStats.recordCount",
Expand Down Expand Up @@ -297,7 +313,7 @@ void main() {
expect(survey.description, 'description123');
expect(survey.dismissForDays, 10);
expect(survey.moreInfoUrl, 'moreInfoUrl123');
expect(survey.samplingRate, 0.1);
expect(survey.samplingRate, 1.0);
expect(survey.conditionList.length, 1);

final condition = survey.conditionList.first;
Expand Down Expand Up @@ -372,7 +388,7 @@ void main() {
"description": "xxxxxxx",
"dismissForDays": "10",
"moreInfoURL": "xxxxxx",
"samplingRate": "0.1",
"samplingRate": "1.0",
"conditions": [
{
"field": "logFileStats.recordCount",
Expand All @@ -389,7 +405,7 @@ void main() {
"description": "xxxxxxx",
"dismissForDays": "10",
"moreInfoURL": "xxxxxx",
"samplingRate": "0.1",
"samplingRate": "1.0",
"conditions": [
{
"field": "logFileStats.recordCount",
Expand Down Expand Up @@ -440,7 +456,7 @@ void main() {
'description',
10,
'moreInfoUrl',
0.1,
1.0,
<Condition>[
Condition('logFileStats.recordCount', '>=', 50),
Condition('logFileStats.toolCount', '>', 0),
Expand Down Expand Up @@ -475,5 +491,107 @@ void main() {
expect(fetchedSurveys.length, 1);
});
});

test('Unit testing the sampleRate method', () {
// These strings had a predetermined output from the utility function
final string1 = 'string1';
final string2 = 'string2';

expect(sampleRate(string1, string2), 0.17);
});

test('Sampling rate correctly returns a valid survey', () async {
// This test will use a predefined client ID string of `string1`
// which has been set in the setup along with a predefined
// string for the survey ID of `string2` to get a sample rate value
//
// The combination of `string1` and `string2` will return 0.17
// from the sampleRate utility function so we have set the threshold
// to be 0.6 which should return surveys
await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async {
final survey = Survey(
'string2',
'url',
DateTime(2023, 1, 1),
DateTime(2023, 12, 31),
'description',
10,
'moreInfoUrl',
0.6,
<Condition>[
Condition('logFileStats.recordCount', '>=', 50),
Condition('logFileStats.toolCount', '>', 0),
],
);
analytics = Analytics.test(
tool: DashTool.flutterTool,
homeDirectory: homeDirectory,
measurementId: 'measurementId',
apiSecret: 'apiSecret',
dartVersion: 'dartVersion',
fs: fs,
platform: DevicePlatform.macos,
surveyHandler: FakeSurveyHandler.fromList(
initializedSurveys: <Survey>[survey],
),
);

// Simulate 60 events to send so that the first condition is satisified
for (var i = 0; i < 60; i++) {
await analytics.sendEvent(
eventName: DashEvent.analyticsCollectionEnabled);
}

final fetchedSurveys = await analytics.fetchAvailableSurveys();

expect(survey.samplingRate, 0.6);
expect(fetchedSurveys.length, 1);
});
});

test('Sampling rate filters out a survey', () async {
// We will reduce the survey's sampling rate to be 0.3 which is
// less than value returned from the predefined client ID and
// survey sample
await withClock(Clock.fixed(DateTime(2023, 3, 3)), () async {
final survey = Survey(
'string2',
'url',
DateTime(2023, 1, 1),
DateTime(2023, 12, 31),
'description',
10,
'moreInfoUrl',
0.15,
<Condition>[
Condition('logFileStats.recordCount', '>=', 50),
Condition('logFileStats.toolCount', '>', 0),
],
);
analytics = Analytics.test(
tool: DashTool.flutterTool,
homeDirectory: homeDirectory,
measurementId: 'measurementId',
apiSecret: 'apiSecret',
dartVersion: 'dartVersion',
fs: fs,
platform: DevicePlatform.macos,
surveyHandler: FakeSurveyHandler.fromList(
initializedSurveys: <Survey>[survey],
),
);

// Simulate 60 events to send so that the first condition is satisified
for (var i = 0; i < 60; i++) {
await analytics.sendEvent(
eventName: DashEvent.analyticsCollectionEnabled);
}

final fetchedSurveys = await analytics.fetchAvailableSurveys();

expect(survey.samplingRate, 0.15);
expect(fetchedSurveys.length, 0);
});
});
});
}