| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * `usage` is a wrapper around Google Analytics for both command-line apps | 6 * `usage` is a wrapper around Google Analytics for both command-line apps |
| 7 * and web apps. | 7 * and web apps. |
| 8 * | 8 * |
| 9 * In order to use this library as a web app, import the `analytics_html.dart` | 9 * In order to use this library as a web app, import the `analytics_html.dart` |
| 10 * library and instantiate the [AnalyticsHtml] class. | 10 * library and instantiate the [AnalyticsHtml] class. |
| (...skipping 11 matching lines...) Expand all Loading... |
| 22 * For more information, please see the Google Analytics Measurement Protocol | 22 * For more information, please see the Google Analytics Measurement Protocol |
| 23 * [Policy](https://developers.google.com/analytics/devguides/collection/protoco
l/policy). | 23 * [Policy](https://developers.google.com/analytics/devguides/collection/protoco
l/policy). |
| 24 */ | 24 */ |
| 25 library usage; | 25 library usage; |
| 26 | 26 |
| 27 import 'dart:async'; | 27 import 'dart:async'; |
| 28 | 28 |
| 29 // Matches file:/, non-ws, /, non-ws, .dart | 29 // Matches file:/, non-ws, /, non-ws, .dart |
| 30 final RegExp _pathRegex = new RegExp(r'file:/\S+/(\S+\.dart)'); | 30 final RegExp _pathRegex = new RegExp(r'file:/\S+/(\S+\.dart)'); |
| 31 | 31 |
| 32 // Match multiple tabs or spaces. | |
| 33 final RegExp _tabOrSpaceRegex = new RegExp(r'[\t ]+'); | |
| 34 | |
| 35 /** | 32 /** |
| 36 * An interface to a Google Analytics session. [AnalyticsHtml] and [AnalyticsIO] | 33 * An interface to a Google Analytics session. [AnalyticsHtml] and [AnalyticsIO] |
| 37 * are concrete implementations of this interface. [AnalyticsMock] can be used | 34 * are concrete implementations of this interface. [AnalyticsMock] can be used |
| 38 * for testing or for some variants of an opt-in workflow. | 35 * for testing or for some varients of an opt-in workflow. |
| 39 * | 36 * |
| 40 * The analytics information is sent on a best-effort basis. So, failures to | 37 * The analytics information is sent on a best-effort basis. So, failures to |
| 41 * send the GA information will not result in errors from the asynchronous | 38 * send the GA information will not result in errors from the asynchronous |
| 42 * `send` methods. | 39 * `send` methods. |
| 43 */ | 40 */ |
| 44 abstract class Analytics { | 41 abstract class Analytics { |
| 45 /** | 42 /** |
| 46 * Tracking ID / Property ID. | 43 * Tracking ID / Property ID. |
| 47 */ | 44 */ |
| 48 String get trackingId; | 45 String get trackingId; |
| 49 | 46 |
| 50 /// The application name. | 47 /** |
| 51 String get applicationName; | 48 * Whether the user has opt-ed in to additional analytics. |
| 49 */ |
| 50 bool get optIn; |
| 52 | 51 |
| 53 /// The application version. | 52 set optIn(bool value); |
| 54 String get applicationVersion; | |
| 55 | 53 |
| 56 /** | 54 /** |
| 57 * Is this the first time the tool has run? | 55 * Whether the [optIn] value has been explicitly set (either `true` or |
| 56 * `false`). |
| 58 */ | 57 */ |
| 59 bool get firstRun; | 58 bool get hasSetOptIn; |
| 60 | |
| 61 /** | |
| 62 * Whether the [Analytics] instance is configured in an opt-in or opt-out mann
er. | |
| 63 */ | |
| 64 AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut; | |
| 65 | |
| 66 /** | |
| 67 * Will analytics data be sent. | |
| 68 */ | |
| 69 bool get enabled; | |
| 70 | |
| 71 /** | |
| 72 * Enable or disable sending of analytics data. | |
| 73 */ | |
| 74 set enabled(bool value); | |
| 75 | |
| 76 /** | |
| 77 * Anonymous client ID in UUID v4 format. | |
| 78 * | |
| 79 * The value is randomly-generated and should be reasonably stable for the | |
| 80 * computer sending analytics data. | |
| 81 */ | |
| 82 String get clientId; | |
| 83 | 59 |
| 84 /** | 60 /** |
| 85 * Sends a screen view hit to Google Analytics. | 61 * Sends a screen view hit to Google Analytics. |
| 86 * | |
| 87 * [parameters] can be any analytics key/value pair. Useful | |
| 88 * for custom dimensions, etc. | |
| 89 */ | 62 */ |
| 90 Future sendScreenView(String viewName, {Map<String, String> parameters}); | 63 Future sendScreenView(String viewName); |
| 91 | 64 |
| 92 /** | 65 /** |
| 93 * Sends an Event hit to Google Analytics. [label] specifies the event label. | 66 * Sends an Event hit to Google Analytics. [label] specifies the event label. |
| 94 * [value] specifies the event value. Values must be non-negative. | 67 * [value] specifies the event value. Values must be non-negative. |
| 95 * | |
| 96 * [parameters] can be any analytics key/value pair. Useful | |
| 97 * for custom dimensions, etc. | |
| 98 */ | 68 */ |
| 99 Future sendEvent(String category, String action, | 69 Future sendEvent(String category, String action, {String label, int value}); |
| 100 {String label, int value, Map<String, String> parameters}); | |
| 101 | 70 |
| 102 /** | 71 /** |
| 103 * Sends a Social hit to Google Analytics. [network] specifies the social | 72 * Sends a Social hit to Google Analytics. [network] specifies the social |
| 104 * network, for example Facebook or Google Plus. [action] specifies the social | 73 * network, for example Facebook or Google Plus. [action] specifies the social |
| 105 * interaction action. For example on Google Plus when a user clicks the +1 | 74 * interaction action. For example on Google Plus when a user clicks the +1 |
| 106 * button, the social action is 'plus'. [target] specifies the target of a | 75 * button, the social action is 'plus'. [target] specifies the target of a |
| 107 * social interaction. This value is typically a URL but can be any text. | 76 * social interaction. This value is typically a URL but can be any text. |
| 108 */ | 77 */ |
| 109 Future sendSocial(String network, String action, String target); | 78 Future sendSocial(String network, String action, String target); |
| 110 | 79 |
| 111 /** | 80 /** |
| 112 * Sends a Timing hit to Google Analytics. [variableName] specifies the | 81 * Sends a Timing hit to Google Analytics. [variableName] specifies the |
| 113 * variable name of the timing. [time] specifies the user timing value (in | 82 * variable name of the timing. [time] specifies the user timing value (in |
| 114 * milliseconds). [category] specifies the category of the timing. [label] | 83 * milliseconds). [category] specifies the category of the timing. [label] |
| 115 * specifies the label of the timing. | 84 * specifies the label of the timing. |
| 116 */ | 85 */ |
| 117 Future sendTiming(String variableName, int time, | 86 Future sendTiming(String variableName, int time, {String category, |
| 118 {String category, String label}); | 87 String label}); |
| 119 | 88 |
| 120 /** | 89 /** |
| 121 * Start a timer. The time won't be calculated, and the analytics information | 90 * Start a timer. The time won't be calculated, and the analytics information |
| 122 * sent, until the [AnalyticsTimer.finish] method is called. | 91 * sent, until the [AnalyticsTimer.finish] method is called. |
| 123 */ | 92 */ |
| 124 AnalyticsTimer startTimer(String variableName, | 93 AnalyticsTimer startTimer(String variableName, |
| 125 {String category, String label}); | 94 {String category, String label}); |
| 126 | 95 |
| 127 /** | 96 /** |
| 128 * In order to avoid sending any personally identifying information, the | 97 * In order to avoid sending any personally identifying information, the |
| 129 * [description] field must not contain the exception message. In addition, | 98 * [description] field must not contain the exception message. In addition, |
| 130 * only the first 100 chars of the description will be sent. | 99 * only the first 100 chars of the description will be sent. |
| 131 */ | 100 */ |
| 132 Future sendException(String description, {bool fatal}); | 101 Future sendException(String description, {bool fatal}); |
| 133 | 102 |
| 134 /** | 103 /** |
| 135 * Gets a session variable value. | |
| 136 */ | |
| 137 dynamic getSessionValue(String param); | |
| 138 | |
| 139 /** | |
| 140 * Sets a session variable value. The value is persistent for the life of the | 104 * Sets a session variable value. The value is persistent for the life of the |
| 141 * [Analytics] instance. This variable will be sent in with every analytics | 105 * [Analytics] instance. This variable will be sent in with every analytics |
| 142 * hit. A list of valid variable names can be found here: | 106 * hit. A list of valid variable names can be found here: |
| 143 * https://developers.google.com/analytics/devguides/collection/protocol/v1/pa
rameters. | 107 * https://developers.google.com/analytics/devguides/collection/protocol/v1/pa
rameters. |
| 144 */ | 108 */ |
| 145 void setSessionValue(String param, dynamic value); | 109 void setSessionValue(String param, dynamic value); |
| 146 | 110 |
| 147 /** | 111 /** |
| 148 * Fires events when the usage library sends any data over the network. This | |
| 149 * will not fire if analytics has been disabled or if the throttling algorithm | |
| 150 * has been engaged. | |
| 151 * | |
| 152 * This method is public to allow library clients to more easily test their | |
| 153 * analytics implementations. | |
| 154 */ | |
| 155 Stream<Map<String, dynamic>> get onSend; | |
| 156 | |
| 157 /** | |
| 158 * Wait for all of the outstanding analytics pings to complete. The returned | 112 * Wait for all of the outstanding analytics pings to complete. The returned |
| 159 * `Future` will always complete without errors. You can pass in an optional | 113 * `Future` will always complete without errors. You can pass in an optional |
| 160 * `Duration` to specify to only wait for a certain amount of time. | 114 * `Duration` to specify to only wait for a certain amount of time. |
| 161 * | 115 * |
| 162 * This method is particularly useful for command-line clients. Outstanding | 116 * This method is particularly useful for command-line clients. Outstanding |
| 163 * I/O requests will cause the VM to delay terminating the process. Generally, | 117 * I/O requests will cause the VM to delay terminating the process. Generally, |
| 164 * users won't want their CLI app to pause at the end of the process waiting | 118 * users won't want their CLI app to pause at the end of the process waiting |
| 165 * for Google analytics requests to complete. This method allows CLI apps to | 119 * for Google analytics requests to complete. This method allows CLI apps to |
| 166 * delay for a short time waiting for GA requests to complete, and then do | 120 * delay for a short time waiting for GA requests to complete, and then do |
| 167 * something like call `dart:io`'s `exit()` explicitly themselves (or the | 121 * something like call `exit()` explicitly themselves. |
| 168 * [close] method below). | |
| 169 */ | 122 */ |
| 170 Future waitForLastPing({Duration timeout}); | 123 Future waitForLastPing({Duration timeout}); |
| 171 | |
| 172 /// Free any used resources. | |
| 173 /// | |
| 174 /// The [Analytics] instance should not be used after this call. | |
| 175 void close(); | |
| 176 } | |
| 177 | |
| 178 enum AnalyticsOpt { | |
| 179 /** | |
| 180 * Users must opt-in before any analytics data is sent. | |
| 181 */ | |
| 182 optIn, | |
| 183 | |
| 184 /** | |
| 185 * Users must opt-out for analytics data to not be sent. | |
| 186 */ | |
| 187 optOut | |
| 188 } | 124 } |
| 189 | 125 |
| 190 /** | 126 /** |
| 191 * An object, returned by [Analytics.startTimer], that is used to measure an | 127 * An object, returned by [Analytics.startTimer], that is used to measure an |
| 192 * asynchronous process. | 128 * asynchronous process. |
| 193 */ | 129 */ |
| 194 class AnalyticsTimer { | 130 class AnalyticsTimer { |
| 195 final Analytics analytics; | 131 final Analytics analytics; |
| 196 final String variableName; | 132 final String variableName; |
| 197 final String category; | 133 final String category; |
| (...skipping 16 matching lines...) Expand all Loading... |
| 214 } | 150 } |
| 215 | 151 |
| 216 /** | 152 /** |
| 217 * Finish the timer, calculate the elapsed time, and send the information to | 153 * Finish the timer, calculate the elapsed time, and send the information to |
| 218 * analytics. Once this is called, any future invocations are no-ops. | 154 * analytics. Once this is called, any future invocations are no-ops. |
| 219 */ | 155 */ |
| 220 Future finish() { | 156 Future finish() { |
| 221 if (_endMillis != null) return new Future.value(); | 157 if (_endMillis != null) return new Future.value(); |
| 222 | 158 |
| 223 _endMillis = new DateTime.now().millisecondsSinceEpoch; | 159 _endMillis = new DateTime.now().millisecondsSinceEpoch; |
| 224 return analytics.sendTiming(variableName, currentElapsedMillis, | 160 return analytics.sendTiming( |
| 225 category: category, label: label); | 161 variableName, currentElapsedMillis, category: category, label: label); |
| 226 } | 162 } |
| 227 } | 163 } |
| 228 | 164 |
| 229 /** | 165 /** |
| 230 * A no-op implementation of the [Analytics] class. This can be used as a | 166 * A no-op implementation of the [Analytics] class. This can be used as a |
| 231 * stand-in for that will never ping the GA server, or as a mock in test code. | 167 * stand-in for that will never ping the GA server, or as a mock in test code. |
| 232 */ | 168 */ |
| 233 class AnalyticsMock implements Analytics { | 169 class AnalyticsMock implements Analytics { |
| 234 @override | |
| 235 String get trackingId => 'UA-0'; | 170 String get trackingId => 'UA-0'; |
| 236 @override | |
| 237 String get applicationName => 'mock-app'; | |
| 238 @override | |
| 239 String get applicationVersion => '1.0.0'; | |
| 240 | |
| 241 final bool logCalls; | 171 final bool logCalls; |
| 242 | 172 |
| 243 /** | 173 bool optIn = false; |
| 244 * Events are never added to this controller for the mock implementation. | 174 bool hasSetOptIn = true; |
| 245 */ | |
| 246 StreamController<Map<String, dynamic>> _sendController = | |
| 247 new StreamController.broadcast(); | |
| 248 | 175 |
| 249 /** | 176 /** |
| 250 * Create a new [AnalyticsMock]. If [logCalls] is true, all calls will be | 177 * Create a new [AnalyticsMock]. If [logCalls] is true, all calls will be |
| 251 * logged to stdout. | 178 * logged to stdout. |
| 252 */ | 179 */ |
| 253 AnalyticsMock([this.logCalls = false]); | 180 AnalyticsMock([this.logCalls = false]); |
| 254 | 181 |
| 255 @override | 182 Future sendScreenView(String viewName) => |
| 256 bool get firstRun => false; | 183 _log('screenView', {'viewName': viewName}); |
| 257 | 184 |
| 258 @override | 185 Future sendEvent(String category, String action, {String label, int value}) { |
| 259 AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut; | 186 return _log('event', {'category': category, 'action': action, |
| 260 | 187 'label': label, 'value': value}); |
| 261 @override | |
| 262 bool enabled = true; | |
| 263 | |
| 264 @override | |
| 265 String get clientId => '00000000-0000-4000-0000-000000000000'; | |
| 266 | |
| 267 @override | |
| 268 Future sendScreenView(String viewName, {Map<String, String> parameters}) { | |
| 269 parameters ??= <String, String>{}; | |
| 270 parameters['viewName'] = viewName; | |
| 271 return _log('screenView', parameters); | |
| 272 } | 188 } |
| 273 | 189 |
| 274 @override | |
| 275 Future sendEvent(String category, String action, | |
| 276 {String label, int value, Map<String, String> parameters}) { | |
| 277 parameters ??= <String, String>{}; | |
| 278 return _log( | |
| 279 'event', | |
| 280 {'category': category, 'action': action, 'label': label, 'value': value} | |
| 281 ..addAll(parameters)); | |
| 282 } | |
| 283 | |
| 284 @override | |
| 285 Future sendSocial(String network, String action, String target) => | 190 Future sendSocial(String network, String action, String target) => |
| 286 _log('social', {'network': network, 'action': action, 'target': target}); | 191 _log('social', {'network': network, 'action': action, 'target': target}); |
| 287 | 192 |
| 288 @override | 193 Future sendTiming(String variableName, int time, {String category, |
| 289 Future sendTiming(String variableName, int time, | 194 String label}) { |
| 290 {String category, String label}) { | 195 return _log('timing', {'variableName': variableName, 'time': time, |
| 291 return _log('timing', { | 196 'category': category, 'label': label}); |
| 292 'variableName': variableName, | |
| 293 'time': time, | |
| 294 'category': category, | |
| 295 'label': label | |
| 296 }); | |
| 297 } | 197 } |
| 298 | 198 |
| 299 @override | |
| 300 AnalyticsTimer startTimer(String variableName, | 199 AnalyticsTimer startTimer(String variableName, |
| 301 {String category, String label}) { | 200 {String category, String label}) { |
| 302 return new AnalyticsTimer(this, variableName, | 201 return new AnalyticsTimer(this, |
| 303 category: category, label: label); | 202 variableName, category: category, label: label); |
| 304 } | 203 } |
| 305 | 204 |
| 306 @override | |
| 307 Future sendException(String description, {bool fatal}) => | 205 Future sendException(String description, {bool fatal}) => |
| 308 _log('exception', {'description': description, 'fatal': fatal}); | 206 _log('exception', {'description': description, 'fatal': fatal}); |
| 309 | 207 |
| 310 @override | 208 void setSessionValue(String param, dynamic value) { } |
| 311 dynamic getSessionValue(String param) => null; | |
| 312 | 209 |
| 313 @override | |
| 314 void setSessionValue(String param, dynamic value) {} | |
| 315 | |
| 316 @override | |
| 317 Stream<Map<String, dynamic>> get onSend => _sendController.stream; | |
| 318 | |
| 319 @override | |
| 320 Future waitForLastPing({Duration timeout}) => new Future.value(); | 210 Future waitForLastPing({Duration timeout}) => new Future.value(); |
| 321 | 211 |
| 322 @override | |
| 323 void close() {} | |
| 324 | |
| 325 Future _log(String hitType, Map m) { | 212 Future _log(String hitType, Map m) { |
| 326 if (logCalls) { | 213 if (logCalls) { |
| 327 print('analytics: ${hitType} ${m}'); | 214 print('analytics: ${hitType} ${m}'); |
| 328 } | 215 } |
| 329 | 216 |
| 330 return new Future.value(); | 217 return new Future.value(); |
| 331 } | 218 } |
| 332 } | 219 } |
| 333 | 220 |
| 334 /** | 221 /** |
| 335 * Sanitize a stacktrace. This will shorten file paths in order to remove any | 222 * Sanitize a stacktrace. This will shorten file paths in order to remove any |
| 336 * PII that may be contained in the full file path. For example, this will | 223 * PII that may be contained in the full file path. For example, this will |
| 337 * shorten `file:///Users/foobar/tmp/error.dart` to `error.dart`. | 224 * shorten `file:///Users/foobar/tmp/error.dart` to `error.dart`. |
| 338 * | 225 * |
| 339 * If [shorten] is `true`, this method will also attempt to compress the text | 226 * If [shorten] is `true`, this method will also attempt to compress the text |
| 340 * of the stacktrace. GA has a 100 char limit on the text that can be sent for | 227 * of the stacktrace. GA has a 100 char limit on the text that can be sent for |
| 341 * an exception. This will try and make those first 100 chars contain | 228 * an exception. This will try and make those first 100 chars contain |
| 342 * information useful to debugging the issue. | 229 * information useful to debugging the issue. |
| 343 */ | 230 */ |
| 344 String sanitizeStacktrace(dynamic st, {bool shorten: true}) { | 231 String sanitizeStacktrace(dynamic st, {bool shorten: true}) { |
| 345 String str = '${st}'; | 232 String str = '${st}'; |
| 346 | 233 |
| 347 Iterable<Match> iter = _pathRegex.allMatches(str); | 234 Iterable<Match> iter = _pathRegex.allMatches(str); |
| 348 iter = iter.toList().reversed; | 235 iter = iter.toList().reversed; |
| 349 | 236 |
| 350 for (Match match in iter) { | 237 for (Match match in iter) { |
| 351 String replacement = match.group(1); | 238 String replacement = match.group(1); |
| 352 str = | 239 str = str.substring(0, match.start) |
| 353 str.substring(0, match.start) + replacement + str.substring(match.end); | 240 + replacement + str.substring(match.end); |
| 354 } | 241 } |
| 355 | 242 |
| 356 if (shorten) { | 243 if (shorten) { |
| 357 // Shorten the stacktrace up a bit. | 244 // Shorten the stacktrace up a bit. |
| 358 str = str.replaceAll(_tabOrSpaceRegex, ' '); | 245 str = str |
| 246 .replaceAll('(package:', '(') |
| 247 .replaceAll('(dart:', '(') |
| 248 .replaceAll(new RegExp(r'\s+'), ' '); |
| 359 } | 249 } |
| 360 | 250 |
| 361 return str; | 251 return str; |
| 362 } | 252 } |
| OLD | NEW |