Skip to content

Commit

Permalink
New event added for sending analytics within package on errors (#229)
Browse files Browse the repository at this point in the history
* Added new event + refactoring sentEvent on impl

* Fix tests + limiting one error event for logFileStats

* Make `Analytics` required for `LogHandler`

* Make error sent a field in class

* Events added for error handling in session handler

* Remove unnecessary `io` import

* Refactoring `legacyOptOut` to use loop

* Only expose `sentEvents` on the  `FakeAnalytics` instance

* Bump version

* Misc

* Convert to wip

* Pass send method instead of `Analytics` + nits

* `ErrorHandler` class created + used in session

* Use `ErrorHandler` with `LogHandler`

* Check telemetry status in `Session`

* Tests added for the survey handler

* Fix error

* Tests added for log handler exceptions

* Use set for sent error messages

* Test added to check for 2 unique error events
  • Loading branch information
eliasyishak authored Feb 13, 2024
1 parent 2ef7673 commit 8323b21
Show file tree
Hide file tree
Showing 15 changed files with 734 additions and 231 deletions.
5 changes: 5 additions & 0 deletions pkgs/unified_analytics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 5.8.2-wip

- Added new event `Event.analyticsException` to track internal errors for this package
- Redirecting the `Analytics.test` factory to return an instance of `FakeAnalytics`

## 5.8.1

- Refactor logic for `okToSend` and `shouldShowMessage`
Expand Down
37 changes: 27 additions & 10 deletions pkgs/unified_analytics/lib/src/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'asserts.dart';
import 'config_handler.dart';
import 'constants.dart';
import 'enums.dart';
import 'error_handler.dart';
import 'event.dart';
import 'ga_client.dart';
import 'initializer.dart';
Expand All @@ -25,6 +26,9 @@ import 'survey_handler.dart';
import 'user_property.dart';
import 'utils.dart';

/// For passing the [Analytics.send] method to classes created by [Analytics].
typedef SendFunction = void Function(Event event);

abstract class Analytics {
/// The default factory constructor that will return an implementation
/// of the [Analytics] abstract class using the [LocalFileSystem].
Expand Down Expand Up @@ -60,7 +64,7 @@ abstract class Analytics {
final homeDirectory = getHomeDirectory(fs);
if (homeDirectory == null ||
!checkDirectoryForWritePermissions(homeDirectory)) {
return NoOpAnalytics();
return const NoOpAnalytics();
}

// Resolve the OS using dart:io
Expand Down Expand Up @@ -187,7 +191,7 @@ abstract class Analytics {
int toolsMessageVersion = kToolsMessageVersion,
String toolsMessage = kToolsMessage,
}) =>
AnalyticsImpl(
FakeAnalytics(
tool: tool,
homeDirectory: homeDirectory,
flutterChannel: flutterChannel,
Expand All @@ -203,7 +207,6 @@ abstract class Analytics {
initializedSurveys: [],
),
gaClient: gaClient ?? const FakeGAClient(),
enableAsserts: true,
clientIde: clientIde,
enabledFeatures: enabledFeatures,
);
Expand Down Expand Up @@ -325,6 +328,7 @@ class AnalyticsImpl implements Analytics {
late final UserProperty userProperty;
late final LogHandler _logHandler;
late final Session _sessionHandler;
late final ErrorHandler _errorHandler;
final int toolsMessageVersion;

/// Tells the client if they need to show a message to the
Expand Down Expand Up @@ -414,11 +418,20 @@ class AnalyticsImpl implements Analytics {
p.join(homeDirectory.path, kDartToolDirectoryName, kClientIdFileName));
_clientId = _clientIdFile.readAsStringSync();

// Initialization for the error handling class that will prevent duplicate
// [Event.analyticsException] events from being sent to GA4
_errorHandler = ErrorHandler(sendFunction: send);

// Initialize the user property class that will be attached to
// each event that is sent to Google Analytics -- it will be responsible
// for getting the session id or rolling the session if the duration
// exceeds [kSessionDurationMinutes]
_sessionHandler = Session(homeDirectory: homeDirectory, fs: fs);
_sessionHandler = Session(
homeDirectory: homeDirectory,
fs: fs,
errorHandler: _errorHandler,
telemetryEnabled: telemetryEnabled,
);
userProperty = UserProperty(
session: _sessionHandler,
flutterChannel: flutterChannel,
Expand All @@ -436,7 +449,11 @@ class AnalyticsImpl implements Analytics {
);

// Initialize the log handler to persist events that are being sent
_logHandler = LogHandler(fs: fs, homeDirectory: homeDirectory);
_logHandler = LogHandler(
fs: fs,
homeDirectory: homeDirectory,
errorHandler: _errorHandler,
);
}

@override
Expand Down Expand Up @@ -712,10 +729,12 @@ class FakeAnalytics extends AnalyticsImpl {
super.flutterVersion,
super.clientIde,
super.enabledFeatures,
int? toolsMessageVersion,
GAClient? gaClient,
}) : super(
gaClient: const FakeGAClient(),
gaClient: gaClient ?? const FakeGAClient(),
enableAsserts: true,
toolsMessageVersion: kToolsMessageVersion,
toolsMessageVersion: toolsMessageVersion ?? kToolsMessageVersion,
);

@override
Expand Down Expand Up @@ -767,9 +786,7 @@ class NoOpAnalytics implements Analytics {
final Map<String, Map<String, Object?>> userPropertyMap =
const <String, Map<String, Object?>>{};

factory NoOpAnalytics() => const NoOpAnalytics._();

const NoOpAnalytics._();
const NoOpAnalytics();

@override
String get clientId => staticClientId;
Expand Down
2 changes: 1 addition & 1 deletion pkgs/unified_analytics/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const int kLogFileLength = 2500;
const String kLogFileName = 'dart-flutter-telemetry.log';

/// The current version of the package, should be in line with pubspec version.
const String kPackageVersion = '5.8.1';
const String kPackageVersion = '5.8.2-wip';

/// The minimum length for a session.
const int kSessionDurationMinutes = 30;
Expand Down
4 changes: 4 additions & 0 deletions pkgs/unified_analytics/lib/src/enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ enum DashEvent {
label: 'analytics_collection_enabled',
description: 'The opt-in status for analytics collection',
),
analyticsException(
label: 'analytics_exception',
description: 'Errors that are encountered within package:unified_analytics',
),
exception(
label: 'exception',
description: 'General errors to log',
Expand Down
32 changes: 32 additions & 0 deletions pkgs/unified_analytics/lib/src/error_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 'analytics.dart';
import 'enums.dart';
import 'event.dart';

class ErrorHandler {
/// Stores each of the events that have been sent to GA4 so that the
/// same error doesn't get sent twice.
final Set<Event> _sentErrorEvents = {};
final SendFunction _sendFunction;

/// Handles any errors encountered within package:unified_analytics.
ErrorHandler({required SendFunction sendFunction})
: _sendFunction = sendFunction;

/// Sends the encountered error [Event.analyticsException] to GA4 backend.
///
/// This method will not send the event to GA4 if it has already been
/// sent before during the current process.
void log(Event event) {
if (event.eventName != DashEvent.analyticsException ||
_sentErrorEvents.contains(event)) {
return;
}

_sendFunction(event);
_sentErrorEvents.add(event);
}
}
26 changes: 25 additions & 1 deletion pkgs/unified_analytics/lib/src/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@ final class Event {
: eventName = DashEvent.analyticsCollectionEnabled,
eventData = {'status': status};

/// Event that is emitted when an error occurs within
/// `package:unified_analytics`, tools that are using this package
/// should not use this event constructor.
///
/// Tools using this package should instead use the more generic
/// [Event.exception] constructor.
///
/// [workflow] - refers to what process caused the error, such as
/// "LogHandler.logFileStats".
///
/// [error] - the name of the error, such as "FormatException".
///
/// [description] - the description of the error being caught.
Event.analyticsException({
required String workflow,
required String error,
String? description,
}) : eventName = DashEvent.analyticsException,
eventData = {
'workflow': workflow,
'error': error,
if (description != null) 'description': description,
};

/// This is for various workflows within the flutter tool related
/// to iOS and macOS workflows.
///
Expand Down Expand Up @@ -685,7 +709,7 @@ final class Event {
};

@override
int get hashCode => eventData.hashCode;
int get hashCode => Object.hash(eventName, jsonEncode(eventData));

@override
bool operator ==(Object other) =>
Expand Down
Loading

0 comments on commit 8323b21

Please sign in to comment.