| // 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 = RegExp(r'file:/\S+/(\S+\.dart)'); |
| |
| // Match multiple tabs or spaces. |
| final RegExp _tabOrSpaceRegex = 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 = DateTime.now().millisecondsSinceEpoch; |
| } |
| |
| int get currentElapsedMillis { |
| if (_endMillis == null) { |
| return 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 Future.value(); |
| |
| _endMillis = 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. |
| final StreamController<Map<String, dynamic>> _sendController = |
| 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 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}) => Future.value(); |
| |
| @override |
| void close() {} |
| |
| Future _log(String hitType, Map m) { |
| if (logCalls) { |
| print('analytics: ${hitType} ${m}'); |
| } |
| |
| return 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}) { |
| var str = '${st}'; |
| |
| Iterable<Match> iter = _pathRegex.allMatches(str); |
| iter = iter.toList().reversed; |
| |
| for (var match in iter) { |
| var 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; |
| } |