Skip to content

Commit

Permalink
Send event for surveys shown and surveys dismissed (#133)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
eliasyishak authored Jul 28, 2023
1 parent 1b75f97 commit ae5cc41
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 119 deletions.
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';
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
22 changes: 11 additions & 11 deletions pkgs/unified_analytics/lib/src/survey_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,17 @@ class Survey {

/// A data class that contains the relevant information for a given
/// survey parsed from the survey's metadata file
Survey(
this.uniqueId,
this.url,
this.startDate,
this.endDate,
this.description,
this.dismissForMinutes,
this.moreInfoUrl,
this.samplingRate,
this.conditionList,
);
Survey({
required this.uniqueId,
required this.url,
required this.startDate,
required this.endDate,
required this.description,
required this.dismissForMinutes,
required this.moreInfoUrl,
required this.samplingRate,
required this.conditionList,
});

/// Parse the contents of the json metadata file hosted externally
Survey.fromJson(Map<String, dynamic> json)
Expand Down
142 changes: 142 additions & 0 deletions pkgs/unified_analytics/test/events_with_fake_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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:test/test.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/fake_analytics.dart';

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

late FakeAnalytics fakeAnalytics;
late FileSystem fs;
late Directory homeDirectory;

/// Survey to load into the fake 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(
uniqueId: 'uniqueId',
url: 'url',
startDate: DateTime(2022, 1, 1),
endDate: DateTime(2022, 12, 31),
description: 'description',
dismissForMinutes: 10,
moreInfoUrl: 'moreInfoUrl',
samplingRate: 1.0, // 100% sample rate
conditionList: <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');

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
withClock(Clock.fixed(DateTime(2022, 3, 3)), () {
fakeAnalytics = FakeAnalytics(
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 fakeAnalytics.send(testEvent);

final surveyList = await fakeAnalytics.fetchAvailableSurveys();
expect(surveyList.length, 1);
expect(fakeAnalytics.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
fakeAnalytics.surveyShown(survey);

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

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

final surveyList = await fakeAnalytics.fetchAvailableSurveys();
expect(surveyList.length, 1);
expect(fakeAnalytics.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
fakeAnalytics.dismissSurvey(survey: survey, surveyAccepted: true);

expect(fakeAnalytics.sentEvents.length, 2);
expect(fakeAnalytics.sentEvents.last.eventName, DashEvent.surveyAction);
expect(fakeAnalytics.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 fakeAnalytics.send(testEvent);

final surveyList = await fakeAnalytics.fetchAvailableSurveys();
expect(surveyList.length, 1);
expect(fakeAnalytics.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
fakeAnalytics.dismissSurvey(survey: survey, surveyAccepted: false);

expect(fakeAnalytics.sentEvents.length, 2);
expect(fakeAnalytics.sentEvents.last.eventName, DashEvent.surveyAction);
expect(fakeAnalytics.sentEvents.last.eventData,
{'surveyId': 'uniqueId', 'status': 'dismissed'});
});
}
57 changes: 57 additions & 0 deletions pkgs/unified_analytics/test/src/fake_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 FakeAnalytics extends AnalyticsImpl {
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
FakeAnalytics({
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);
}
}
Loading

0 comments on commit ae5cc41

Please sign in to comment.