| Index: packages/usage/lib/usage.dart
|
| diff --git a/packages/usage/lib/usage.dart b/packages/usage/lib/usage.dart
|
| index 9a4279bbe37cbfcd418128a5cb15b154d3f6acc5..e79701cbe6b51fccbe996deb292e52fed7047261 100644
|
| --- a/packages/usage/lib/usage.dart
|
| +++ b/packages/usage/lib/usage.dart
|
| @@ -29,10 +29,13 @@ import 'dart:async';
|
| // Matches file:/, non-ws, /, non-ws, .dart
|
| final RegExp _pathRegex = new RegExp(r'file:/\S+/(\S+\.dart)');
|
|
|
| +// Match multiple tabs or spaces.
|
| +final RegExp _tabOrSpaceRegex = new RegExp(r'[\t ]+');
|
| +
|
| /**
|
| * An interface to a Google Analytics session. [AnalyticsHtml] and [AnalyticsIO]
|
| * are concrete implementations of this interface. [AnalyticsMock] can be used
|
| - * for testing or for some varients of an opt-in workflow.
|
| + * for testing or for some variants of an opt-in workflow.
|
| *
|
| * The analytics information is sent on a best-effort basis. So, failures to
|
| * send the GA information will not result in errors from the asynchronous
|
| @@ -44,27 +47,57 @@ abstract class Analytics {
|
| */
|
| String get trackingId;
|
|
|
| + /// The application name.
|
| + String get applicationName;
|
| +
|
| + /// The application version.
|
| + String get applicationVersion;
|
| +
|
| + /**
|
| + * Is this the first time the tool has run?
|
| + */
|
| + bool get firstRun;
|
| +
|
| + /**
|
| + * Whether the [Analytics] instance is configured in an opt-in or opt-out manner.
|
| + */
|
| + AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut;
|
| +
|
| + /**
|
| + * Will analytics data be sent.
|
| + */
|
| + bool get enabled;
|
| +
|
| /**
|
| - * Whether the user has opt-ed in to additional analytics.
|
| + * Enable or disable sending of analytics data.
|
| */
|
| - bool optIn;
|
| + set enabled(bool value);
|
|
|
| /**
|
| - * Whether the [optIn] value has been explicitly set (either `true` or
|
| - * `false`).
|
| + * Anonymous client ID in UUID v4 format.
|
| + *
|
| + * The value is randomly-generated and should be reasonably stable for the
|
| + * computer sending analytics data.
|
| */
|
| - bool get hasSetOptIn;
|
| + String get clientId;
|
|
|
| /**
|
| * Sends a screen view hit to Google Analytics.
|
| + *
|
| + * [parameters] can be any analytics key/value pair. Useful
|
| + * for custom dimensions, etc.
|
| */
|
| - Future sendScreenView(String viewName);
|
| + Future sendScreenView(String viewName, {Map<String, String> parameters});
|
|
|
| /**
|
| * Sends an Event hit to Google Analytics. [label] specifies the event label.
|
| * [value] specifies the event value. Values must be non-negative.
|
| + *
|
| + * [parameters] can be any analytics key/value pair. Useful
|
| + * for custom dimensions, etc.
|
| */
|
| - Future sendEvent(String category, String action, {String label, int value});
|
| + Future sendEvent(String category, String action,
|
| + {String label, int value, Map<String, String> parameters});
|
|
|
| /**
|
| * Sends a Social hit to Google Analytics. [network] specifies the social
|
| @@ -81,8 +114,8 @@ abstract class Analytics {
|
| * milliseconds). [category] specifies the category of the timing. [label]
|
| * specifies the label of the timing.
|
| */
|
| - Future sendTiming(String variableName, int time, {String category,
|
| - String label});
|
| + Future sendTiming(String variableName, int time,
|
| + {String category, String label});
|
|
|
| /**
|
| * Start a timer. The time won't be calculated, and the analytics information
|
| @@ -98,6 +131,11 @@ abstract class Analytics {
|
| */
|
| Future sendException(String description, {bool fatal});
|
|
|
| + /**
|
| + * Gets a session variable value.
|
| + */
|
| + dynamic getSessionValue(String param);
|
| +
|
| /**
|
| * Sets a session variable value. The value is persistent for the life of the
|
| * [Analytics] instance. This variable will be sent in with every analytics
|
| @@ -106,6 +144,16 @@ abstract class Analytics {
|
| */
|
| void setSessionValue(String param, dynamic value);
|
|
|
| + /**
|
| + * Fires events when the usage library sends any data over the network. This
|
| + * will not fire if analytics has been disabled or if the throttling algorithm
|
| + * has been engaged.
|
| + *
|
| + * This method is public to allow library clients to more easily test their
|
| + * analytics implementations.
|
| + */
|
| + Stream<Map<String, dynamic>> get onSend;
|
| +
|
| /**
|
| * Wait for all of the outstanding analytics pings to complete. The returned
|
| * `Future` will always complete without errors. You can pass in an optional
|
| @@ -116,9 +164,27 @@ abstract class Analytics {
|
| * users won't want their CLI app to pause at the end of the process waiting
|
| * for Google analytics requests to complete. This method allows CLI apps to
|
| * delay for a short time waiting for GA requests to complete, and then do
|
| - * something like call `exit()` explicitly themselves.
|
| + * something like call `dart:io`'s `exit()` explicitly themselves (or the
|
| + * [close] method below).
|
| */
|
| Future waitForLastPing({Duration timeout});
|
| +
|
| + /// Free any used resources.
|
| + ///
|
| + /// The [Analytics] instance should not be used after this call.
|
| + void close();
|
| +}
|
| +
|
| +enum AnalyticsOpt {
|
| + /**
|
| + * Users must opt-in before any analytics data is sent.
|
| + */
|
| + optIn,
|
| +
|
| + /**
|
| + * Users must opt-out for analytics data to not be sent.
|
| + */
|
| + optOut
|
| }
|
|
|
| /**
|
| @@ -155,8 +221,8 @@ class AnalyticsTimer {
|
| if (_endMillis != null) return new Future.value();
|
|
|
| _endMillis = new DateTime.now().millisecondsSinceEpoch;
|
| - return analytics.sendTiming(
|
| - variableName, currentElapsedMillis, category: category, label: label);
|
| + return analytics.sendTiming(variableName, currentElapsedMillis,
|
| + category: category, label: label);
|
| }
|
| }
|
|
|
| @@ -165,11 +231,20 @@ class AnalyticsTimer {
|
| * stand-in for that will never ping the GA server, or as a mock in test code.
|
| */
|
| class AnalyticsMock implements Analytics {
|
| + @override
|
| String get trackingId => 'UA-0';
|
| + @override
|
| + String get applicationName => 'mock-app';
|
| + @override
|
| + String get applicationVersion => '1.0.0';
|
| +
|
| final bool logCalls;
|
|
|
| - bool optIn = false;
|
| - bool hasSetOptIn = true;
|
| + /**
|
| + * Events are never added to this controller for the mock implementation.
|
| + */
|
| + StreamController<Map<String, dynamic>> _sendController =
|
| + new StreamController.broadcast();
|
|
|
| /**
|
| * Create a new [AnalyticsMock]. If [logCalls] is true, all calls will be
|
| @@ -177,36 +252,76 @@ class AnalyticsMock implements Analytics {
|
| */
|
| AnalyticsMock([this.logCalls = false]);
|
|
|
| - Future sendScreenView(String viewName) =>
|
| - _log('screenView', {'viewName': viewName});
|
| + @override
|
| + bool get firstRun => false;
|
| +
|
| + @override
|
| + AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut;
|
| +
|
| + @override
|
| + bool enabled = true;
|
| +
|
| + @override
|
| + String get clientId => '00000000-0000-4000-0000-000000000000';
|
| +
|
| + @override
|
| + Future sendScreenView(String viewName, {Map<String, String> parameters}) {
|
| + parameters ??= <String, String>{};
|
| + parameters['viewName'] = viewName;
|
| + return _log('screenView', parameters);
|
| + }
|
|
|
| - Future sendEvent(String category, String action, {String label, int value}) {
|
| - return _log('event', {'category': category, 'action': action,
|
| - 'label': label, 'value': value});
|
| + @override
|
| + Future sendEvent(String category, String action,
|
| + {String label, int value, Map<String, String> parameters}) {
|
| + parameters ??= <String, String>{};
|
| + return _log(
|
| + 'event',
|
| + {'category': category, 'action': action, 'label': label, 'value': value}
|
| + ..addAll(parameters));
|
| }
|
|
|
| + @override
|
| Future sendSocial(String network, String action, String target) =>
|
| _log('social', {'network': network, 'action': action, 'target': target});
|
|
|
| - Future sendTiming(String variableName, int time, {String category,
|
| - String label}) {
|
| - return _log('timing', {'variableName': variableName, 'time': time,
|
| - 'category': category, 'label': label});
|
| + @override
|
| + Future sendTiming(String variableName, int time,
|
| + {String category, String label}) {
|
| + return _log('timing', {
|
| + 'variableName': variableName,
|
| + 'time': time,
|
| + 'category': category,
|
| + 'label': label
|
| + });
|
| }
|
|
|
| + @override
|
| AnalyticsTimer startTimer(String variableName,
|
| {String category, String label}) {
|
| - return new AnalyticsTimer(this,
|
| - variableName, category: category, label: label);
|
| + return new AnalyticsTimer(this, variableName,
|
| + category: category, label: label);
|
| }
|
|
|
| + @override
|
| Future sendException(String description, {bool fatal}) =>
|
| _log('exception', {'description': description, 'fatal': fatal});
|
|
|
| - void setSessionValue(String param, dynamic value) { }
|
| + @override
|
| + dynamic getSessionValue(String param) => null;
|
| +
|
| + @override
|
| + void setSessionValue(String param, dynamic value) {}
|
|
|
| + @override
|
| + Stream<Map<String, dynamic>> get onSend => _sendController.stream;
|
| +
|
| + @override
|
| Future waitForLastPing({Duration timeout}) => new Future.value();
|
|
|
| + @override
|
| + void close() {}
|
| +
|
| Future _log(String hitType, Map m) {
|
| if (logCalls) {
|
| print('analytics: ${hitType} ${m}');
|
| @@ -234,16 +349,13 @@ String sanitizeStacktrace(dynamic st, {bool shorten: true}) {
|
|
|
| for (Match match in iter) {
|
| String replacement = match.group(1);
|
| - str = str.substring(0, match.start)
|
| - + replacement + str.substring(match.end);
|
| + str =
|
| + str.substring(0, match.start) + replacement + str.substring(match.end);
|
| }
|
|
|
| if (shorten) {
|
| // Shorten the stacktrace up a bit.
|
| - str = str
|
| - .replaceAll('(package:', '(')
|
| - .replaceAll('(dart:', '(')
|
| - .replaceAll(new RegExp(r'\s+'), ' ');
|
| + str = str.replaceAll(_tabOrSpaceRegex, ' ');
|
| }
|
|
|
| return str;
|
|
|