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 |