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
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