| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * `usage` is a wrapper around Google Analytics for both command-line apps | |
| 7 * and web apps. | |
| 8 * | |
| 9 * In order to use this library as a web app, import the `analytics_html.dart` | |
| 10 * library and instantiate the [AnalyticsHtml] class. | |
| 11 * | |
| 12 * In order to use this library as a command-line app, import the | |
| 13 * `analytics_io.dart` library and instantiate the [AnalyticsIO] class. | |
| 14 * | |
| 15 * For both classes, you need to provide a Google Analytics tracking ID, the | |
| 16 * application name, and the application version. | |
| 17 * | |
| 18 * Your application should provide an opt-in option for the user. If they | |
| 19 * opt-in, set the [optIn] field to `true`. This setting will persist across | |
| 20 * sessions automatically. | |
| 21 * | |
| 22 * For more information, please see the Google Analytics Measurement Protocol | |
| 23 * [Policy](https://developers.google.com/analytics/devguides/collection/protoco
l/policy). | |
| 24 */ | |
| 25 library usage; | |
| 26 | |
| 27 import 'dart:async'; | |
| 28 | |
| 29 // Matches file:/, non-ws, /, non-ws, .dart | |
| 30 final RegExp _pathRegex = new RegExp(r'file:/\S+/(\S+\.dart)'); | |
| 31 | |
| 32 /** | |
| 33 * An interface to a Google Analytics session. [AnalyticsHtml] and [AnalyticsIO] | |
| 34 * are concrete implementations of this interface. [AnalyticsMock] can be used | |
| 35 * for testing or for some varients of an opt-in workflow. | |
| 36 * | |
| 37 * The analytics information is sent on a best-effort basis. So, failures to | |
| 38 * send the GA information will not result in errors from the asynchronous | |
| 39 * `send` methods. | |
| 40 */ | |
| 41 abstract class Analytics { | |
| 42 /** | |
| 43 * Tracking ID / Property ID. | |
| 44 */ | |
| 45 String get trackingId; | |
| 46 | |
| 47 /** | |
| 48 * Whether the user has opt-ed in to additional analytics. | |
| 49 */ | |
| 50 bool optIn; | |
| 51 | |
| 52 /** | |
| 53 * Whether the [optIn] value has been explicitly set (either `true` or | |
| 54 * `false`). | |
| 55 */ | |
| 56 bool get hasSetOptIn; | |
| 57 | |
| 58 /** | |
| 59 * Sends a screen view hit to Google Analytics. | |
| 60 */ | |
| 61 Future sendScreenView(String viewName); | |
| 62 | |
| 63 /** | |
| 64 * Sends an Event hit to Google Analytics. [label] specifies the event label. | |
| 65 * [value] specifies the event value. Values must be non-negative. | |
| 66 */ | |
| 67 Future sendEvent(String category, String action, {String label, int value}); | |
| 68 | |
| 69 /** | |
| 70 * Sends a Social hit to Google Analytics. [network] specifies the social | |
| 71 * network, for example Facebook or Google Plus. [action] specifies the social | |
| 72 * interaction action. For example on Google Plus when a user clicks the +1 | |
| 73 * button, the social action is 'plus'. [target] specifies the target of a | |
| 74 * social interaction. This value is typically a URL but can be any text. | |
| 75 */ | |
| 76 Future sendSocial(String network, String action, String target); | |
| 77 | |
| 78 /** | |
| 79 * Sends a Timing hit to Google Analytics. [variableName] specifies the | |
| 80 * variable name of the timing. [time] specifies the user timing value (in | |
| 81 * milliseconds). [category] specifies the category of the timing. [label] | |
| 82 * specifies the label of the timing. | |
| 83 */ | |
| 84 Future sendTiming(String variableName, int time, {String category, | |
| 85 String label}); | |
| 86 | |
| 87 /** | |
| 88 * Start a timer. The time won't be calculated, and the analytics information | |
| 89 * sent, until the [AnalyticsTimer.finish] method is called. | |
| 90 */ | |
| 91 AnalyticsTimer startTimer(String variableName, | |
| 92 {String category, String label}); | |
| 93 | |
| 94 /** | |
| 95 * In order to avoid sending any personally identifying information, the | |
| 96 * [description] field must not contain the exception message. In addition, | |
| 97 * only the first 100 chars of the description will be sent. | |
| 98 */ | |
| 99 Future sendException(String description, {bool fatal}); | |
| 100 | |
| 101 /** | |
| 102 * Sets a session variable value. The value is persistent for the life of the | |
| 103 * [Analytics] instance. This variable will be sent in with every analytics | |
| 104 * hit. A list of valid variable names can be found here: | |
| 105 * https://developers.google.com/analytics/devguides/collection/protocol/v1/pa
rameters. | |
| 106 */ | |
| 107 void setSessionValue(String param, dynamic value); | |
| 108 | |
| 109 /** | |
| 110 * Wait for all of the outstanding analytics pings to complete. The returned | |
| 111 * `Future` will always complete without errors. You can pass in an optional | |
| 112 * `Duration` to specify to only wait for a certain amount of time. | |
| 113 * | |
| 114 * This method is particularly useful for command-line clients. Outstanding | |
| 115 * I/O requests will cause the VM to delay terminating the process. Generally, | |
| 116 * users won't want their CLI app to pause at the end of the process waiting | |
| 117 * for Google analytics requests to complete. This method allows CLI apps to | |
| 118 * delay for a short time waiting for GA requests to complete, and then do | |
| 119 * something like call `exit()` explicitly themselves. | |
| 120 */ | |
| 121 Future waitForLastPing({Duration timeout}); | |
| 122 } | |
| 123 | |
| 124 /** | |
| 125 * An object, returned by [Analytics.startTimer], that is used to measure an | |
| 126 * asynchronous process. | |
| 127 */ | |
| 128 class AnalyticsTimer { | |
| 129 final Analytics analytics; | |
| 130 final String variableName; | |
| 131 final String category; | |
| 132 final String label; | |
| 133 | |
| 134 int _startMillis; | |
| 135 int _endMillis; | |
| 136 | |
| 137 AnalyticsTimer(this.analytics, this.variableName, | |
| 138 {this.category, this.label}) { | |
| 139 _startMillis = new DateTime.now().millisecondsSinceEpoch; | |
| 140 } | |
| 141 | |
| 142 int get currentElapsedMillis { | |
| 143 if (_endMillis == null) { | |
| 144 return new DateTime.now().millisecondsSinceEpoch - _startMillis; | |
| 145 } else { | |
| 146 return _endMillis - _startMillis; | |
| 147 } | |
| 148 } | |
| 149 | |
| 150 /** | |
| 151 * Finish the timer, calculate the elapsed time, and send the information to | |
| 152 * analytics. Once this is called, any future invocations are no-ops. | |
| 153 */ | |
| 154 Future finish() { | |
| 155 if (_endMillis != null) return new Future.value(); | |
| 156 | |
| 157 _endMillis = new DateTime.now().millisecondsSinceEpoch; | |
| 158 return analytics.sendTiming( | |
| 159 variableName, currentElapsedMillis, category: category, label: label); | |
| 160 } | |
| 161 } | |
| 162 | |
| 163 /** | |
| 164 * A no-op implementation of the [Analytics] class. This can be used as a | |
| 165 * stand-in for that will never ping the GA server, or as a mock in test code. | |
| 166 */ | |
| 167 class AnalyticsMock implements Analytics { | |
| 168 String get trackingId => 'UA-0'; | |
| 169 final bool logCalls; | |
| 170 | |
| 171 bool optIn = false; | |
| 172 bool hasSetOptIn = true; | |
| 173 | |
| 174 /** | |
| 175 * Create a new [AnalyticsMock]. If [logCalls] is true, all calls will be | |
| 176 * logged to stdout. | |
| 177 */ | |
| 178 AnalyticsMock([this.logCalls = false]); | |
| 179 | |
| 180 Future sendScreenView(String viewName) => | |
| 181 _log('screenView', {'viewName': viewName}); | |
| 182 | |
| 183 Future sendEvent(String category, String action, {String label, int value}) { | |
| 184 return _log('event', {'category': category, 'action': action, | |
| 185 'label': label, 'value': value}); | |
| 186 } | |
| 187 | |
| 188 Future sendSocial(String network, String action, String target) => | |
| 189 _log('social', {'network': network, 'action': action, 'target': target}); | |
| 190 | |
| 191 Future sendTiming(String variableName, int time, {String category, | |
| 192 String label}) { | |
| 193 return _log('timing', {'variableName': variableName, 'time': time, | |
| 194 'category': category, 'label': label}); | |
| 195 } | |
| 196 | |
| 197 AnalyticsTimer startTimer(String variableName, | |
| 198 {String category, String label}) { | |
| 199 return new AnalyticsTimer(this, | |
| 200 variableName, category: category, label: label); | |
| 201 } | |
| 202 | |
| 203 Future sendException(String description, {bool fatal}) => | |
| 204 _log('exception', {'description': description, 'fatal': fatal}); | |
| 205 | |
| 206 void setSessionValue(String param, dynamic value) { } | |
| 207 | |
| 208 Future waitForLastPing({Duration timeout}) => new Future.value(); | |
| 209 | |
| 210 Future _log(String hitType, Map m) { | |
| 211 if (logCalls) { | |
| 212 print('analytics: ${hitType} ${m}'); | |
| 213 } | |
| 214 | |
| 215 return new Future.value(); | |
| 216 } | |
| 217 } | |
| 218 | |
| 219 /** | |
| 220 * Sanitize a stacktrace. This will shorten file paths in order to remove any | |
| 221 * PII that may be contained in the full file path. For example, this will | |
| 222 * shorten `file:///Users/foobar/tmp/error.dart` to `error.dart`. | |
| 223 * | |
| 224 * If [shorten] is `true`, this method will also attempt to compress the text | |
| 225 * of the stacktrace. GA has a 100 char limit on the text that can be sent for | |
| 226 * an exception. This will try and make those first 100 chars contain | |
| 227 * information useful to debugging the issue. | |
| 228 */ | |
| 229 String sanitizeStacktrace(dynamic st, {bool shorten: true}) { | |
| 230 String str = '${st}'; | |
| 231 | |
| 232 Iterable<Match> iter = _pathRegex.allMatches(str); | |
| 233 iter = iter.toList().reversed; | |
| 234 | |
| 235 for (Match match in iter) { | |
| 236 String replacement = match.group(1); | |
| 237 str = str.substring(0, match.start) | |
| 238 + replacement + str.substring(match.end); | |
| 239 } | |
| 240 | |
| 241 if (shorten) { | |
| 242 // Shorten the stacktrace up a bit. | |
| 243 str = str | |
| 244 .replaceAll('(package:', '(') | |
| 245 .replaceAll('(dart:', '(') | |
| 246 .replaceAll(new RegExp(r'\s+'), ' '); | |
| 247 } | |
| 248 | |
| 249 return str; | |
| 250 } | |
| OLD | NEW |