Skip to content

Commit

Permalink
Add Fake Analytics instance that uses list to save events sent (#149)
Browse files Browse the repository at this point in the history
* Add Fake Analytics instance that uses list to save events sent

* Check enableAsserts bool

* Adding overrides for comparing events

* Fix bugs + add tests

* Equality operator within list

* Fix nit + add dartdoc for `FakeAnalytics`

* Fix format error + change log update
  • Loading branch information
eliasyishak authored Sep 13, 2023
1 parent fa01f9b commit 1512f3d
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 107 deletions.
1 change: 1 addition & 0 deletions pkgs/unified_analytics/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Added new method to suppress telemetry collection temporarily for current invocation via `analytics.suppressTelemetry()`
- Added `SurveyHandler` feature to `Analytics` instance to fetch available surveys from remote endpoint to display to users along with functionality to dismiss them
- Surveys will be disabled for any users that have been opted out
- Shipping `FakeAnalytics` for clients of this tool that need to ensure workflows are sending events in tests

## 3.0.0

Expand Down
60 changes: 57 additions & 3 deletions pkgs/unified_analytics/lib/src/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ abstract class Analytics {

// Ensure that the home directory has permissions enabled to write
final homeDirectory = getHomeDirectory(fs);
if (homeDirectory == null ||
!checkDirectoryForWritePermissions(homeDirectory)) {
return NoOpAnalytics();
if (homeDirectory == null) {
throw Exception('Unable to determine the home directory, '
'ensure it is available in the environment');
}
if (!checkDirectoryForWritePermissions(homeDirectory)) {
throw Exception('Permissions error on the home directory!');
}

// Resolve the OS using dart:io
Expand Down Expand Up @@ -627,6 +630,57 @@ class AnalyticsImpl implements Analytics {
}
}

/// This fake instance of [Analytics] is intended to be used by clients of
/// this package for testing purposes. It exposes a list [sentEvents] that
/// keeps track of all events that have been sent.
///
/// This is useful for confirming that events are being sent for a given
/// workflow. Invoking the [send] method on this instance will not make any
/// network requests to Google Analytics.
class FakeAnalytics extends AnalyticsImpl {
/// Use this list to check for events that have been emitted when
/// invoking the send method
final List<Event> sentEvents = [];

/// 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.fs,
required super.surveyHandler,
super.flutterChannel,
super.flutterVersion,
}) : super(
gaClient: const FakeGAClient(),
enableAsserts: true,
toolsMessageVersion: kToolsMessageVersion,
);

@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,
);

if (_enableAsserts) 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);
}
}

/// An implementation that will never send events.
///
/// This is for clients that opt to either not send analytics, or will migrate
Expand Down
95 changes: 53 additions & 42 deletions pkgs/unified_analytics/lib/src/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:convert';

import 'enums.dart';
import 'utils.dart';

final class Event {
final DashEvent eventName;
Expand All @@ -18,48 +19,6 @@ final class Event {
: eventName = DashEvent.analyticsCollectionEnabled,
eventData = {'status': status};

/// Event that is emitted when a Dart CLI command has been executed.
///
/// [name] - the name of the command that was executed
///
/// [enabledExperiments] - a set of Dart language experiments enabled when
/// running the command.
///
/// [exitCode] - the process exit code set as a result of running the command.
Event.dartCliCommandExecuted({
required String name,
required String enabledExperiments,
int? exitCode,
}) : eventName = DashEvent.dartCliCommandExecuted,
eventData = {
'name': name,
'enabledExperiments': enabledExperiments,
if (exitCode != null) 'exitCode': exitCode,
};

/// Event that is emitted when `pub get` is run.
///
/// [packageName] - the name of the package that was resolved
///
/// [version] - the resolved, canonicalized package version
///
/// [dependencyKind] - the kind of dependency that resulted in this package
/// being resolved (e.g., direct, transitive, or dev dependencies).
Event.pubGet({
required String packageName,
required String version,
required String dependencyType,
}) : eventName = DashEvent.pubGet,
eventData = {
'packageName': packageName,
'version': version,
'dependencyType': dependencyType,
};

Event.hotReloadTime({required int timeMs})
: eventName = DashEvent.hotReloadTime,
eventData = {'timeMs': timeMs};

/// Event that is emitted periodically to report the performance of the
/// analysis server's handling of a specific kind of notification from the
/// client.
Expand Down Expand Up @@ -228,6 +187,29 @@ final class Event {
'transitiveFileUniqueLineCount': transitiveFileUniqueLineCount,
};

/// Event that is emitted when a Dart CLI command has been executed.
///
/// [name] - the name of the command that was executed
///
/// [enabledExperiments] - a set of Dart language experiments enabled when
/// running the command.
///
/// [exitCode] - the process exit code set as a result of running the command.
Event.dartCliCommandExecuted({
required String name,
required String enabledExperiments,
int? exitCode,
}) : eventName = DashEvent.dartCliCommandExecuted,
eventData = {
'name': name,
'enabledExperiments': enabledExperiments,
if (exitCode != null) 'exitCode': exitCode,
};

Event.hotReloadTime({required int timeMs})
: eventName = DashEvent.hotReloadTime,
eventData = {'timeMs': timeMs};

/// Event that is emitted periodically to report the number of times each lint
/// has been enabled.
///
Expand Down Expand Up @@ -309,6 +291,25 @@ final class Event {
'pluginId': pluginId,
};

/// Event that is emitted when `pub get` is run.
///
/// [packageName] - the name of the package that was resolved
///
/// [version] - the resolved, canonicalized package version
///
/// [dependencyKind] - the kind of dependency that resulted in this package
/// being resolved (e.g., direct, transitive, or dev dependencies).
Event.pubGet({
required String packageName,
required String version,
required String dependencyType,
}) : eventName = DashEvent.pubGet,
eventData = {
'packageName': packageName,
'version': version,
'dependencyType': dependencyType,
};

/// Event that is emitted on shutdown to report information about the whole
/// session for which the analysis server was running.
///
Expand Down Expand Up @@ -383,6 +384,16 @@ final class Event {
'surveyId': surveyId,
};

@override
int get hashCode => eventData.hashCode;

@override
bool operator ==(Object other) =>
other is Event &&
other.runtimeType == runtimeType &&
other.eventName == eventName &&
compareEventData(other.eventData, eventData);

@override
String toString() => jsonEncode({
'eventName': eventName.label,
Expand Down
20 changes: 20 additions & 0 deletions pkgs/unified_analytics/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ bool checkDirectoryForWritePermissions(Directory directory) {
return fileStat.modeString()[1] == 'w';
}

/// Utility function to take in two maps [a] and [b] and compares them
/// to ensure that they have the same keys and values
bool compareEventData(Map<String, Object?> a, Map<String, Object?> b) {
final keySetA = a.keys.toSet();
final keySetB = b.keys.toSet();

// Ensure that the keys are the same for each object
if (keySetA.intersection(keySetB).length != keySetA.length ||
keySetA.intersection(keySetB).length != keySetB.length) {
return false;
}

// Ensure that each of the key's values are the same
for (final key in a.keys) {
if (a[key] != b[key]) return false;
}

return true;
}

/// Format time as 'yyyy-MM-dd HH:mm:ss Z' where Z is the difference between the
/// timezone of t and UTC formatted according to RFC 822.
String formatDateTime(DateTime t) {
Expand Down
2 changes: 1 addition & 1 deletion pkgs/unified_analytics/lib/unified_analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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.

export 'src/analytics.dart' show Analytics, NoOpAnalytics;
export 'src/analytics.dart' show Analytics, FakeAnalytics, NoOpAnalytics;
export 'src/config_handler.dart' show ToolInfo;
export 'src/enums.dart' show DashTool;
export 'src/event.dart' show Event;
Expand Down
4 changes: 0 additions & 4 deletions pkgs/unified_analytics/test/events_with_fake_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ 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
Expand Down Expand Up @@ -71,14 +69,12 @@ void main() {
homeDirectory: homeDirectory,
dartVersion: 'dartVersion',
platform: DevicePlatform.macos,
toolsMessageVersion: 1,
fs: fs,
surveyHandler: FakeSurveyHandler.fromList(
homeDirectory: homeDirectory,
fs: fs,
initializedSurveys: [testSurvey],
),
enableAsserts: true,
);
});
});
Expand Down
57 changes: 0 additions & 57 deletions pkgs/unified_analytics/test/src/fake_analytics.dart

This file was deleted.

45 changes: 45 additions & 0 deletions pkgs/unified_analytics/test/unified_analytics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1169,4 +1169,49 @@ further information will be sent. This data is collected in accordance with
the Google Privacy Policy (https://policies.google.com/privacy).
'''));
});

test('Equality operator works for identical events', () {
final eventOne = Event.clientRequest(
duration: 'duration',
latency: 'latency',
method: 'method',
);
final eventTwo = Event.clientRequest(
duration: 'duration',
latency: 'latency',
method: 'method',
);

expect(eventOne == eventTwo, true);
});

test('Equality operator works for non-identical events', () {
final eventOne = Event.clientRequest(
duration: 'duration',
latency: 'latency',
method: 'method',
added: 'DIFFERENT FROM EVENT TWO',
);
final eventTwo = Event.clientRequest(
duration: 'duration',
latency: 'latency',
method: 'method',
);

expect(eventOne == eventTwo, false);
});

test('Find a match for an event in a list of events', () {
final eventList = [
Event.analyticsCollectionEnabled(status: true),
Event.memoryInfo(rss: 500),
Event.clientRequest(
duration: 'duration', latency: 'latency', method: 'method'),
];

final eventToMatch = Event.memoryInfo(rss: 500);

expect(eventList.contains(eventToMatch), true);
expect(eventList.where((element) => element == eventToMatch).length, 1);
});
}

0 comments on commit 1512f3d

Please sign in to comment.