blob: e79701cbe6b51fccbe996deb292e52fed7047261 [file] [log] [blame]
// Copyright (c) 2014, 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.
/**
* `usage` is a wrapper around Google Analytics for both command-line apps
* and web apps.
*
* In order to use this library as a web app, import the `analytics_html.dart`
* library and instantiate the [AnalyticsHtml] class.
*
* In order to use this library as a command-line app, import the
* `analytics_io.dart` library and instantiate the [AnalyticsIO] class.
*
* For both classes, you need to provide a Google Analytics tracking ID, the
* application name, and the application version.
*
* Your application should provide an opt-in option for the user. If they
* opt-in, set the [optIn] field to `true`. This setting will persist across
* sessions automatically.
*
* For more information, please see the Google Analytics Measurement Protocol
* [Policy](https://developers.google.com/analytics/devguides/collection/protocol/policy).
*/
library usage;
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 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
* `send` methods.
*/
abstract class Analytics {
/**
* Tracking ID / Property ID.
*/
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;
/**
* Enable or disable sending of analytics data.
*/
set enabled(bool value);
/**
* Anonymous client ID in UUID v4 format.
*
* The value is randomly-generated and should be reasonably stable for the
* computer sending analytics data.
*/
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, {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, Map<String, String> parameters});
/**
* Sends a Social hit to Google Analytics. [network] specifies the social
* network, for example Facebook or Google Plus. [action] specifies the social
* interaction action. For example on Google Plus when a user clicks the +1
* button, the social action is 'plus'. [target] specifies the target of a
* social interaction. This value is typically a URL but can be any text.
*/
Future sendSocial(String network, String action, String target);
/**
* Sends a Timing hit to Google Analytics. [variableName] specifies the
* variable name of the timing. [time] specifies the user timing value (in
* 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});
/**
* Start a timer. The time won't be calculated, and the analytics information
* sent, until the [AnalyticsTimer.finish] method is called.
*/
AnalyticsTimer startTimer(String variableName,
{String category, String label});
/**
* In order to avoid sending any personally identifying information, the
* [description] field must not contain the exception message. In addition,
* only the first 100 chars of the description will be sent.
*/
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
* hit. A list of valid variable names can be found here:
* https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters.
*/
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
* `Duration` to specify to only wait for a certain amount of time.
*
* This method is particularly useful for command-line clients. Outstanding
* I/O requests will cause the VM to delay terminating the process. Generally,
* 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 `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
}
/**
* An object, returned by [Analytics.startTimer], that is used to measure an
* asynchronous process.
*/
class AnalyticsTimer {
final Analytics analytics;
final String variableName;
final String category;
final String label;
int _startMillis;
int _endMillis;
AnalyticsTimer(this.analytics, this.variableName,
{this.category, this.label}) {
_startMillis = new DateTime.now().millisecondsSinceEpoch;
}
int get currentElapsedMillis {
if (_endMillis == null) {
return new DateTime.now().millisecondsSinceEpoch - _startMillis;
} else {
return _endMillis - _startMillis;
}
}
/**
* Finish the timer, calculate the elapsed time, and send the information to
* analytics. Once this is called, any future invocations are no-ops.
*/
Future finish() {
if (_endMillis != null) return new Future.value();
_endMillis = new DateTime.now().millisecondsSinceEpoch;
return analytics.sendTiming(variableName, currentElapsedMillis,
category: category, label: label);
}
}
/**
* A no-op implementation of the [Analytics] class. This can be used as a
* 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;
/**
* 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
* logged to stdout.
*/
AnalyticsMock([this.logCalls = false]);
@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);
}
@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});
@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);
}
@override
Future sendException(String description, {bool fatal}) =>
_log('exception', {'description': description, 'fatal': fatal});
@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}');
}
return new Future.value();
}
}
/**
* Sanitize a stacktrace. This will shorten file paths in order to remove any
* PII that may be contained in the full file path. For example, this will
* shorten `file:///Users/foobar/tmp/error.dart` to `error.dart`.
*
* If [shorten] is `true`, this method will also attempt to compress the text
* of the stacktrace. GA has a 100 char limit on the text that can be sent for
* an exception. This will try and make those first 100 chars contain
* information useful to debugging the issue.
*/
String sanitizeStacktrace(dynamic st, {bool shorten: true}) {
String str = '${st}';
Iterable<Match> iter = _pathRegex.allMatches(str);
iter = iter.toList().reversed;
for (Match match in iter) {
String replacement = match.group(1);
str =
str.substring(0, match.start) + replacement + str.substring(match.end);
}
if (shorten) {
// Shorten the stacktrace up a bit.
str = str.replaceAll(_tabOrSpaceRegex, ' ');
}
return str;
}