Skip to content
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

Send event for surveys shown and surveys dismissed #133

Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions pkgs/unified_analytics/lib/src/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ class AnalyticsImpl implements Analytics {
@override
void dismissSurvey({required Survey survey, required bool surveyAccepted}) {
_surveyHandler.dismiss(survey, true);
final status = surveyAccepted ? 'accepted' : 'dismissed';
eliasyishak marked this conversation as resolved.
Show resolved Hide resolved
send(Event.surveyAction(surveyId: survey.uniqueId, status: status));
}

@override
Expand Down Expand Up @@ -594,6 +596,7 @@ class AnalyticsImpl implements Analytics {
@override
void surveyShown(Survey survey) {
_surveyHandler.dismiss(survey, false);
send(Event.surveyShown(surveyId: survey.uniqueId));
}
}

Expand Down
8 changes: 8 additions & 0 deletions pkgs/unified_analytics/lib/src/enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ enum DashEvent {
label: 'analytics_collection_enabled',
description: 'The opt-in status for analytics collection',
),
surveyAction(
label: 'survey_action',
description: 'Actions taken by users when shown survey',
),
surveyShown(
label: 'survey_shown',
description: 'Survey shown to the user',
),

// Events for flutter_tools
hotReloadTime(
Expand Down
35 changes: 35 additions & 0 deletions pkgs/unified_analytics/lib/src/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'enums.dart';

final class Event {
Expand Down Expand Up @@ -312,4 +314,37 @@ final class Event {
'diagnostic': diagnostic,
'adjustments': adjustments,
};

/// Event that is emitted by `package:unified_analytics` when
/// the user takes action when prompted with a survey
///
/// [surveyId] - the unique id for a given survey
///
/// [status] - `'accepted'` if the user accepted the survey, or
/// `'dismissed'` if the user rejected it
Event.surveyAction({
required String surveyId,
required String status,
}) : eventName = DashEvent.surveyAction,
eventData = {
'surveyId': surveyId,
'status': status,
};

/// Event that is emitted by `package:unified_analytics` when the
/// user has been shown a survey
///
/// [surveyId] - the unique id for a given survey
Event.surveyShown({
required String surveyId,
}) : eventName = DashEvent.surveyShown,
eventData = {
'surveyId': surveyId,
};

@override
String toString() => jsonEncode({
'eventName': eventName.label,
'eventData': eventData,
});
}
2 changes: 2 additions & 0 deletions pkgs/unified_analytics/lib/src/ga_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:http/http.dart' as http;
import 'constants.dart';

class FakeGAClient implements GAClient {
const FakeGAClient();

@override
String get apiSecret => throw UnimplementedError();

Expand Down
156 changes: 156 additions & 0 deletions pkgs/unified_analytics/test/events_with_mock_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

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/enums.dart';
import 'package:unified_analytics/src/survey_handler.dart';
import 'package:unified_analytics/unified_analytics.dart';

import 'src/mock_analytics.dart';

void main() {
// The mocked analytics instance can be used to ensure events
// are being sent when invoking methods on the `Analytics` instance

late MockAnalytics mockAnalytics;
late FileSystem fs;
late Directory homeDirectory;
late File clientIdFile;

/// Survey to load into the mock instance to fetch
///
/// The 1.0 sample rate means that this will always show
/// up from the method to fetch available surveys
final testSurvey = Survey(
eliasyishak marked this conversation as resolved.
Show resolved Hide resolved
'uniqueId',
'url',
DateTime(2022, 1, 1),
DateTime(2022, 12, 31),
'description',
10,
'moreInfoUrl',
1.0, // 100% sample rate
<Condition>[],
);

/// Test event that will need to be sent since surveys won't
/// be fetched until at least one event is logged in the persisted
/// log file on disk
final testEvent = Event.hotReloadTime(timeMs: 10);

setUp(() async {
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(
eliasyishak marked this conversation as resolved.
Show resolved Hide resolved
homeDirectory.path,
kDartToolDirectoryName,
kClientIdFileName,
));
clientIdFile.createSync(recursive: true);
clientIdFile.writeAsStringSync('string1');

final initialAnalytics = Analytics.test(
tool: DashTool.flutterTool,
homeDirectory: homeDirectory,
measurementId: 'measurementId',
apiSecret: 'apiSecret',
dartVersion: 'dartVersion',
toolsMessageVersion: 1,
fs: fs,
platform: DevicePlatform.macos,
);
initialAnalytics.clientShowedMessage();

// Recreate a second instance since events cannot be sent on
// the first run
await withClock(Clock.fixed(DateTime(2022, 3, 3)), () async {
eliasyishak marked this conversation as resolved.
Show resolved Hide resolved
mockAnalytics = MockAnalytics(
tool: DashTool.flutterTool,
homeDirectory: homeDirectory,
dartVersion: 'dartVersion',
platform: DevicePlatform.macos,
toolsMessageVersion: 1,
fs: fs,
surveyHandler: FakeSurveyHandler.fromList(
homeDirectory: homeDirectory,
fs: fs,
initializedSurveys: [testSurvey],
),
enableAsserts: true,
);
});
});

test('event sent when survey shown', () async {
// Fire off the test event to allow surveys to be fetched
await mockAnalytics.send(testEvent);

final surveyList = await mockAnalytics.fetchAvailableSurveys();
expect(surveyList.length, 1);
expect(mockAnalytics.sentEvents.length, 1,
reason: 'Only one event sent from the test event above');

final survey = surveyList.first;
expect(survey.uniqueId, 'uniqueId');

// Simulate the survey being shown
mockAnalytics.surveyShown(survey);

expect(mockAnalytics.sentEvents.length, 2);
expect(mockAnalytics.sentEvents.last.eventName, DashEvent.surveyShown);
expect(mockAnalytics.sentEvents.last.eventData, {'surveyId': 'uniqueId'});
});

test('event sent when survey accepted', () async {
// Fire off the test event to allow surveys to be fetched
await mockAnalytics.send(testEvent);

final surveyList = await mockAnalytics.fetchAvailableSurveys();
expect(surveyList.length, 1);
expect(mockAnalytics.sentEvents.length, 1,
reason: 'Only one event sent from the test event above');

final survey = surveyList.first;
expect(survey.uniqueId, 'uniqueId');

// Simulate the survey being shown
mockAnalytics.dismissSurvey(survey: survey, surveyAccepted: true);

expect(mockAnalytics.sentEvents.length, 2);
expect(mockAnalytics.sentEvents.last.eventName, DashEvent.surveyAction);
expect(mockAnalytics.sentEvents.last.eventData,
{'surveyId': 'uniqueId', 'status': 'accepted'});
});

test('event sent when survey rejected', () async {
// Fire off the test event to allow surveys to be fetched
await mockAnalytics.send(testEvent);

final surveyList = await mockAnalytics.fetchAvailableSurveys();
expect(surveyList.length, 1);
expect(mockAnalytics.sentEvents.length, 1,
reason: 'Only one event sent from the test event above');

final survey = surveyList.first;
expect(survey.uniqueId, 'uniqueId');

// Simulate the survey being shown
mockAnalytics.dismissSurvey(survey: survey, surveyAccepted: false);

expect(mockAnalytics.sentEvents.length, 2);
expect(mockAnalytics.sentEvents.last.eventName, DashEvent.surveyAction);
expect(mockAnalytics.sentEvents.last.eventData,
{'surveyId': 'uniqueId', 'status': 'dismissed'});
});
}
57 changes: 57 additions & 0 deletions pkgs/unified_analytics/test/src/mock_analytics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:http/http.dart';

import 'package:unified_analytics/src/analytics.dart';
import 'package:unified_analytics/src/asserts.dart';
import 'package:unified_analytics/src/event.dart';
import 'package:unified_analytics/src/ga_client.dart';
import 'package:unified_analytics/src/log_handler.dart';
import 'package:unified_analytics/src/utils.dart';

class MockAnalytics extends AnalyticsImpl {
eliasyishak marked this conversation as resolved.
Show resolved Hide resolved
final List<Event> sentEvents = [];
final LogHandler _logHandler;
final FakeGAClient _gaClient;
final String _clientId = 'hard-coded-client-id';

/// Class to use when you want to see which events were sent
MockAnalytics({
required super.tool,
required super.homeDirectory,
required super.dartVersion,
required super.platform,
required super.toolsMessageVersion,
required super.fs,
required super.surveyHandler,
required super.enableAsserts,
super.flutterChannel,
super.flutterVersion,
FakeGAClient super.gaClient = const FakeGAClient(),
}) : _logHandler = LogHandler(fs: fs, homeDirectory: homeDirectory),
_gaClient = gaClient;

@override
Future<Response>? send(Event event) {
if (!okToSend) return null;

// Construct the body of the request
final body = generateRequestBody(
clientId: _clientId,
eventName: event.eventName,
eventData: event.eventData,
userProperty: userProperty,
);

checkBody(body);

_logHandler.save(data: body);

// Using this list to validate that events are being sent
// for internal methods in the `Analytics` instance
sentEvents.add(event);
return _gaClient.sendData(body);
}
}