Skip to content

Commit 921611a

Browse files
authored
Survey handler feature (#109)
* Survey handler functionality to fetch available surveys (#91) * 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 * No surveys returned for opted out users * Revert "No surveys returned for opted out users" This reverts commit f6d9f8e. * No surveys for opted out users (#99) * Check `okToSend` before fetching surveys * Added test * dart format fix * Update CHANGELOG.md * Mark as dev * Change version suffix * `dart fix --apply --code=combinators_ordering` * Fix `survey_handler.dart` with new lints * Add'l fixes to survey_handler * Remove left hand types from `analytics.dart` * Fix `survey_handler_test.dart` with new lints * Fix tests with survey_handler class from lint fixes * `dart format` fix * Sampling rate functionality added (#122) * Sampling rate functionality added * Update tests to have 100% sampling rate * Tests added to test sampling rate * Update survey_handler_test.dart * Fix type for `jsonDecode` * New utility function to convert string into integer * Fix tests with new outputs for sample rate * Use uniqueId for survey instead of description * Add hyphen to lookup * Fix documentation * Fix survey handler tests to use new .send method * Fix tests to use new maps for `LogFileStats` * Dismissing and persisting surveys (#127) * Add constant for new file name + clean up session handler Removing NoOp session instance since that was only being used before `2.0.0` * Updating survey handler to create file to persist ids * Revert changes to session handler * Update constant to be a json file * Initialize dismiss surveys file with empty json * Initializer for dismissed file made static * Functionality added to check if survey snoozed or dismissed * Dismiss functionality added * `dismissForDays` -> `dismissForMinutes` * Update survey_handler_test.dart * Clean up external functions to be class methods * Tests added for snoozing and dismissing permanently * Test added for malformed json * Check sample rate before using LogFileStats * Add `surveyShown` API to snooze surveys * Use new URL for survey metadata * Error handling for missing json file * Sample rate example added (#130) * Added example file * Including example's output in code * Update sample_rate.dart * Fix nits * Send event for surveys shown and surveys dismissed (#133) * Added enum and event constructor survey actions * Fix format errors * Using two events for survey shown and survey action * Created mock class to confirm events are sent * Clean up constructors * Fix nits * Refactor for buttons array with `SurveyButton` class (#134) * Added newe `SurveyButton` class * Fix tests * Add documentation for enums * Update sample_rate.dart * Update tests to check for `SurveyButton` classes * Remove enum for status of action * Use `snoozeForMinutes` instead of dismiss * Expose `SurveyButton` * Fixing documentation for event class * Order members in survey handler * Refactor to pass button to `surveyInteracted(..)` * `surveyButtonList` --> `buttonList` renaming * Adding example file for how to use survey handler feature * Adding conditional check for url to display * Format fix * Allow surveys with no conditions to be passed Only checking if `logFileStats` is null if there is a condition in the condition array in the json * Update version * Simplify utility functions for sample rate + check date * `const` constructor for `Survey` unnamed constructor * Fix test to unit test sampling function * Fix dartdocs + check for null outside loop + breaks removed * Add documentation to example files * `dart format` * Catch `TypeError` when parsing json survey * Adding tests for the sampling rate with margin of error
1 parent af3fc99 commit 921611a

19 files changed

+2268
-20
lines changed

pkgs/unified_analytics/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
## 3.1.0-wip
1+
## 3.1.0
22

33
- Enhanced `LogFileStats` data to include information about flutter channel counts and tool counts
44
- Added new method to suppress telemetry collection temporarily for current invocation via `analytics.suppressTelemetry()`
5+
- Added `SurveyHandler` feature to `Analytics` instance to fetch available surveys from remote endpoint to display to users along with functionality to dismiss them
6+
- Surveys will be disabled for any users that have been opted out
57

68
## 3.0.0
79

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:unified_analytics/src/utils.dart';
6+
7+
/// The purpose of this example file is to demonstrate the sampling
8+
/// rate functionality from the survey handler.
9+
///
10+
/// It defines a [remoteUniqueId] that subs in for a real survey's unique
11+
/// ID that is hosted in the remote json file.
12+
///
13+
/// Begin the simulation by setting the [testSampleRate] and [iterations]
14+
/// variables, where [iterations] simulates real users and [testSampleRate]
15+
/// represents a fraction of how many people should be sampled.
16+
///
17+
/// In this example, we have set the [testSampleRate] to `0.3`, meaning we want
18+
/// sample 30% of users, and [iterations] to `10,000`, which simulates `10,000`
19+
/// users.
20+
///
21+
/// Running the script with predefined seed of `123` will
22+
/// generate the below `stdout`
23+
/// ```
24+
/// Test sample rate = 0.3
25+
/// Number of iterations = 10000
26+
/// ---
27+
///
28+
/// Count of iterations sampled (successes) = 3046
29+
/// Actual sample rate = 0.3046
30+
/// ---
31+
///
32+
/// Runtime = 8ms
33+
/// ```
34+
///
35+
/// The actual results yielded 3,046 people selected for a rate
36+
/// of `30.46%` which is about the `30%` defined in [testSampleRate].
37+
void main() {
38+
// Seed has been set to replicate results
39+
//
40+
// Test with your own seed and alter other parameters
41+
// as needed
42+
final uuidGenerator = Uuid(123);
43+
44+
// Randomly generate an ID that will simulate being used for
45+
// a given survey
46+
final remoteUniqueId = uuidGenerator.generateV4();
47+
48+
// Define a sampling rate that we would like to test
49+
//
50+
// Setting 0.3 means any generated doubles less than or
51+
// equal to 0.3 results in a survey getting delievered
52+
const testSampleRate = 0.3;
53+
54+
// Define how many iterations to run, each iteration can
55+
// be thought of as a developer using a dash tool
56+
const iterations = 10000;
57+
58+
// Initializing a counter that will count the number of
59+
// iterations that were below the sampling rate
60+
var count = 0;
61+
62+
final start = DateTime.now();
63+
for (var i = 0; i < iterations; i++) {
64+
// Each newly generated ID is simulating a unique
65+
// developer's CLIENT ID that is persisted on their disk
66+
final clientId = uuidGenerator.generateV4();
67+
68+
// Generate a double that will be compared against the sampleRate
69+
final generatedDouble = sampleRate(remoteUniqueId, clientId);
70+
71+
// Count successes if the generated double is less than our
72+
// testing sample rate
73+
if (generatedDouble <= testSampleRate) {
74+
count++;
75+
}
76+
}
77+
final end = DateTime.now();
78+
79+
print('''
80+
Test sample rate = $testSampleRate
81+
Number of iterations = $iterations
82+
---
83+
84+
Count of iterations sampled (successes) = $count
85+
Actual sample rate = ${(count / iterations).toStringAsFixed(4)}
86+
---
87+
88+
Runtime = ${end.difference(start).inMilliseconds}ms
89+
''');
90+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:clock/clock.dart';
6+
import 'package:file/file.dart';
7+
import 'package:file/memory.dart';
8+
9+
import 'package:unified_analytics/src/constants.dart';
10+
import 'package:unified_analytics/src/enums.dart';
11+
import 'package:unified_analytics/src/survey_handler.dart';
12+
import 'package:unified_analytics/unified_analytics.dart';
13+
14+
/// This example code is intended to only be used as guidance for
15+
/// clients using this package. Clients using this package should avoid
16+
/// the use of the [Analytics.test] constructor.
17+
///
18+
/// It was used in this example file so that the real [FileSystem] was swapped
19+
/// out for a [MemoryFileSystem] so that repeated runs of this script yield
20+
/// the same results.
21+
void main() async {
22+
late final MemoryFileSystem fs;
23+
late final Analytics analytics;
24+
late final Directory home;
25+
// We need to initialize with a fake clock since the surveys have
26+
// a period of time they are valid for
27+
await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 0)), () async {
28+
// Use a memory file system to repeatedly run this example
29+
// file with the test instance
30+
fs = MemoryFileSystem(style: FileSystemStyle.posix);
31+
home = fs.directory('home');
32+
home.createSync();
33+
34+
// The purpose of `initialAnalytics` is so that the tool is able to
35+
// send events after its first run; this instance won't be used below
36+
//
37+
// ignore: invalid_use_of_visible_for_testing_member
38+
final initialAnalytics = Analytics.test(
39+
tool: DashTool.flutterTool,
40+
homeDirectory: home,
41+
measurementId: 'measurementId',
42+
apiSecret: 'apiSecret',
43+
dartVersion: 'dartVersion',
44+
fs: fs,
45+
platform: DevicePlatform.macos,
46+
);
47+
// The below command allows `DashTool.flutterTool` to send telemetry
48+
initialAnalytics.clientShowedMessage();
49+
50+
// ignore: invalid_use_of_visible_for_testing_member
51+
analytics = Analytics.test(
52+
tool: DashTool.flutterTool,
53+
homeDirectory: home,
54+
measurementId: 'measurementId',
55+
apiSecret: 'apiSecret',
56+
dartVersion: 'dartVersion',
57+
fs: fs,
58+
platform: DevicePlatform.macos,
59+
surveyHandler: FakeSurveyHandler.fromList(
60+
homeDirectory: home,
61+
fs: fs,
62+
initializedSurveys: [
63+
Survey(
64+
uniqueId: 'uniqueId',
65+
startDate: DateTime(2023, 1, 1),
66+
endDate: DateTime(2023, 5, 31),
67+
description: 'description',
68+
snoozeForMinutes: 10,
69+
samplingRate: 1.0,
70+
conditionList: [],
71+
buttonList: [
72+
SurveyButton(
73+
buttonText: 'View Survey',
74+
action: 'accept',
75+
promptRemainsVisible: false,
76+
url: 'http://example.com',
77+
),
78+
SurveyButton(
79+
buttonText: 'More Info',
80+
action: 'snooze',
81+
promptRemainsVisible: true,
82+
url: 'http://example2.com',
83+
),
84+
SurveyButton(
85+
buttonText: 'Dismiss Survey',
86+
action: 'dismiss',
87+
promptRemainsVisible: false,
88+
)
89+
],
90+
),
91+
],
92+
));
93+
});
94+
95+
// Each client of this package will be able to fetch all of
96+
// the available surveys with the below method
97+
//
98+
// Sample rate will be applied automatically; it also won't
99+
// fetch any surveys in the snooze period or if they have
100+
// been dismissed
101+
final surveyList = await analytics.fetchAvailableSurveys();
102+
assert(surveyList.length == 1);
103+
104+
// Grab the first and only survey to simulate displaying it to a user
105+
final survey = surveyList.first;
106+
print('Simulating displaying the survey with a print below:');
107+
print('Survey id: ${survey.uniqueId}\n');
108+
109+
// Immediately after displaying the survey, the method below
110+
// should be run so that no other clients using this tool will show
111+
// it at the same time
112+
//
113+
// It will "snoozed" when the below is run as well as reported to
114+
// Google Analytics 4 that this survey was shown
115+
analytics.surveyShown(survey);
116+
117+
// Get the file where this is persisted to show it getting updated
118+
final persistedSurveyFile = home
119+
.childDirectory(kDartToolDirectoryName)
120+
.childFile(kDismissedSurveyFileName);
121+
print('The contents of the json file '
122+
'after invoking `analytics.surveyShown(survey);`');
123+
print('${persistedSurveyFile.readAsStringSync()}\n');
124+
125+
// Change the index below to decide which button to simulate pressing
126+
//
127+
// 0 - accept
128+
// 1 - snooze
129+
// 2 - dismiss
130+
final selectedButtonIndex = 1;
131+
assert([0, 1, 2].contains(selectedButtonIndex));
132+
133+
// Get the survey button by index that will need to be passed along with
134+
// the survey to simulate an interaction with the survey
135+
final selectedSurveyButton = survey.buttonList[selectedButtonIndex];
136+
print('The simulated button pressed was: '
137+
'"${selectedSurveyButton.buttonText}" '
138+
'(action = ${selectedSurveyButton.action})\n');
139+
140+
// The below method will handle whatever action the button
141+
analytics.surveyInteracted(
142+
survey: survey,
143+
surveyButton: selectedSurveyButton,
144+
);
145+
146+
// Conditional to check if there is a URl to route to
147+
if (selectedSurveyButton.url != null) {
148+
print('***This button also has a survey URL link '
149+
'to route to at "${selectedSurveyButton.url}"***\n');
150+
}
151+
152+
// Conditional to check what simulating a popup to stay up
153+
if (selectedSurveyButton.promptRemainsVisible) {
154+
print('***This button has its promptRemainsVisible field set to `true` '
155+
'so this simulates what seeing a pop up again would look like***\n');
156+
}
157+
158+
print('The contents of the json file '
159+
'after invoking '
160+
'`analytics.surveyInteracted(survey: survey, '
161+
'surveyButton: selectedSurveyButton);`');
162+
print('${persistedSurveyFile.readAsStringSync()}\n');
163+
164+
// Demonstrating that the survey doesn't get returned again
165+
print('Attempting to fetch surveys again will result in an empty list');
166+
print(await analytics.fetchAvailableSurveys());
167+
}

0 commit comments

Comments
 (0)