Skip to content
Merged
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
2 changes: 1 addition & 1 deletion pkgs/unified_analytics/example/sample_rate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Number of iterations = $iterations
---

Count of iterations sampled (successes) = $count
Actual sample rate = ${count / iterations}
Actual sample rate = ${(count / iterations).toStringAsFixed(4)}
---

Runtime = ${end.difference(start).inMilliseconds}ms
Expand Down
159 changes: 159 additions & 0 deletions pkgs/unified_analytics/example/serving_surveys.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import 'package:clock/clock.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';

import 'package:unified_analytics/src/constants.dart';
import 'package:unified_analytics/src/enums.dart';
import 'package:unified_analytics/src/survey_handler.dart';
import 'package:unified_analytics/unified_analytics.dart';

void main() async {
late final MemoryFileSystem fs;
late final Analytics analytics;
late final Directory home;
Comment on lines +10 to +13
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DanTup this example file should make it easier for us to discuss any changes to the workflow (if any are needed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bwilkerson also an fyi if this example file workflow... from the analysis server side, does this workflow make sense to you?

// We need to initialize with a fake clock since the surveys have
// a period of time they are valid for
await withClock(Clock.fixed(DateTime(2023, 3, 3, 12, 0)), () async {
// Use a memory file system to repeatedly run this example
// file with the test instance
fs = MemoryFileSystem.test(style: FileSystemStyle.posix);
home = fs.directory('home');
home.createSync();

// The purpose of `initialAnalytics` is so that the tool is able to
// send events after its first run; this instance won't be used below
//
// ignore: invalid_use_of_visible_for_testing_member
final initialAnalytics = Analytics.test(
tool: DashTool.flutterTool,
homeDirectory: home,
measurementId: 'measurementId',
apiSecret: 'apiSecret',
dartVersion: 'dartVersion',
fs: fs,
platform: DevicePlatform.macos,
);
// The below command allows `DashTool.flutterTool` to send telemetry
initialAnalytics.clientShowedMessage();

// ignore: invalid_use_of_visible_for_testing_member
analytics = Analytics.test(
tool: DashTool.flutterTool,
homeDirectory: home,
measurementId: 'measurementId',
apiSecret: 'apiSecret',
dartVersion: 'dartVersion',
fs: fs,
platform: DevicePlatform.macos,
surveyHandler: FakeSurveyHandler.fromList(
homeDirectory: home,
fs: fs,
initializedSurveys: [
Survey(
uniqueId: 'uniqueId',
startDate: DateTime(2023, 1, 1),
endDate: DateTime(2023, 5, 31),
description: 'description',
snoozeForMinutes: 10,
samplingRate: 1.0,
conditionList: [],
buttonList: [
SurveyButton(
buttonText: 'View Survey',
action: 'accept',
promptRemainsVisible: false,
url: 'http://example.com',
),
SurveyButton(
buttonText: 'More Info',
action: 'snooze',
promptRemainsVisible: true,
url: 'http://example2.com',
),
SurveyButton(
buttonText: 'Dismiss Survey',
action: 'dismiss',
promptRemainsVisible: false,
)
],
),
],
));

// Send one event to allow `LogFileStats` to not be null
await analytics.send(Event.hotReloadTime(timeMs: 50));
});

// Each client of this package will be able to fetch all of
// the available surveys with the below method
//
// Sample rate will be applied automatically; it also won't
// fetch any surveys in the snooze period or if they have
// been dismissed
final surveyList = await analytics.fetchAvailableSurveys();
assert(surveyList.length == 1);

// Grab the first and only survey to simulate displaying it to a user
final survey = surveyList.first;
print('Simulating displaying the survey with a print below:');
print('Survey id: ${survey.uniqueId}\n');

// Immediately after displaying the survey, the method below
// should be run so that no other clients using this tool will show
// it at the same time
//
// It will "snoozed" when the below is run as well as reported to
// Google Analytics 4 that this survey was shown
analytics.surveyShown(survey);

// Get the file where this is persisted to show it getting updated
final persistedSurveyFile = home
.childDirectory(kDartToolDirectoryName)
.childFile(kDismissedSurveyFileName);
print('The contents of the json file '
'after invoking `analytics.surveyShown(survey);`');
print('${persistedSurveyFile.readAsStringSync()}\n');

// Change the index below to decide which button to simulate pressing
//
// 0 - accept
// 1 - snooze
// 2 - dismiss
final selectedButtonIndex = 1;
assert([0, 1, 2].contains(selectedButtonIndex));

// Get the survey button by index that will need to be passed along with
// the survey to simulate an interaction with the survey
final selectedSurveyButton = survey.buttonList[selectedButtonIndex];
print('The simulated button pressed was: '
'"${selectedSurveyButton.buttonText}" '
'(action = ${selectedSurveyButton.action})\n');

// The below method will handle whatever action the button
analytics.surveyInteracted(
survey: survey,
surveyButton: selectedSurveyButton,
);

// Conditional to check if there is a URl to route to
if (selectedSurveyButton.url != null) {
print('***This button also has a survey URL link '
'to route to at "${selectedSurveyButton.url}"***\n');
}

// Conditional to check what simulating a popup to stay up
if (selectedSurveyButton.promptRemainsVisible) {
print('***This button has its promptRemainsVisible field set to `true` '
'so this simulates what seeing a pop up again would look like***\n');
}
Comment on lines +144 to +148
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is useful info for debugging, printing when there's a URL that would be opened might be useful too.


print('The contents of the json file '
'after invoking '
'`analytics.surveyInteracted(survey: survey, '
'surveyButton: selectedSurveyButton);`');
print('${persistedSurveyFile.readAsStringSync()}\n');

// Demonstrating that the survey doesn't get returned again
print('Attempting to fetch surveys again will result in an empty list');
print(await analytics.fetchAvailableSurveys());
}
52 changes: 32 additions & 20 deletions pkgs/unified_analytics/lib/src/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,6 @@ abstract class Analytics {
/// that need to be sent off
void close();

/// Method to dismiss a survey permanently
///
/// Pass a [Survey] instance which can be retrieved from
/// `fetchAvailableSurveys()`
///
/// [surveyAccepted] indicates if the user opened the survey if `true`
/// or `false` if the user rejects to open it
void dismissSurvey({required Survey survey, required bool surveyAccepted});

/// Method to fetch surveys from the specified endpoint [kContextualSurveyUrl]
///
/// Any survey that is returned by this method has already passed
Expand Down Expand Up @@ -260,11 +251,22 @@ abstract class Analytics {
/// collection use `setTelemetry(false)`
void suppressTelemetry();

/// Method to run after interacting with a [Survey]
///
/// Pass a [Survey] instance which can be retrieved from
/// `fetchAvailableSurveys()`
///
/// [sureyButton] is the button that was interacted with by the user
void surveyInteracted({
required Survey survey,
required SurveyButton surveyButton,
});

/// Method to be called after a survey has been shown to the user
///
/// Calling this will snooze the survey so it won't be shown immediately
///
/// The snooze period is defined within the `dismissForMinutes`
/// The snooze period is defined within the `snoozeForMinutes`
/// field in [Survey]
void surveyShown(Survey survey);
}
Expand Down Expand Up @@ -453,13 +455,6 @@ class AnalyticsImpl implements Analytics {
@override
void close() => _gaClient.close();

@override
void dismissSurvey({required Survey survey, required bool surveyAccepted}) {
_surveyHandler.dismiss(survey, true);
final status = surveyAccepted ? 'accepted' : 'dismissed';
send(Event.surveyAction(surveyId: survey.uniqueId, status: status));
}

@override
Future<List<Survey>> fetchAvailableSurveys() async {
final surveysToShow = <Survey>[];
Expand Down Expand Up @@ -593,6 +588,20 @@ class AnalyticsImpl implements Analytics {
@override
void suppressTelemetry() => _telemetrySuppressed = true;

@override
void surveyInteracted({
required Survey survey,
required SurveyButton surveyButton,
}) {
// Any action, except for 'snooze' will permanently dismiss a given survey
final permanentlyDismissed = surveyButton.action == 'snooze' ? false : true;
_surveyHandler.dismiss(survey, permanentlyDismissed);
send(Event.surveyAction(
surveyId: survey.uniqueId,
status: surveyButton.action,
));
}

@override
void surveyShown(Survey survey) {
_surveyHandler.dismiss(survey, false);
Expand Down Expand Up @@ -634,9 +643,6 @@ class NoOpAnalytics implements Analytics {
@override
void close() {}

@override
void dismissSurvey({required Survey survey, required bool surveyAccepted}) {}

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

Expand All @@ -652,6 +658,12 @@ class NoOpAnalytics implements Analytics {
@override
void suppressTelemetry() {}

@override
void surveyInteracted({
required Survey survey,
required SurveyButton surveyButton,
}) {}

@override
void surveyShown(Survey survey) {}
}
4 changes: 2 additions & 2 deletions pkgs/unified_analytics/lib/src/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ final class Event {
///
/// [surveyId] - the unique id for a given survey
///
/// [status] - `'accepted'` if the user accepted the survey, or
/// `'dismissed'` if the user rejected it
/// [status] - the string identifier for a given [SurveyButton] under
/// the `action` field
Event.surveyAction({
required String surveyId,
required String status,
Expand Down
Loading