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