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 library usage_impl; | |
6 | |
7 import 'dart:async'; | 5 import 'dart:async'; |
8 import 'dart:math' as math; | 6 import 'dart:math' as math; |
9 | 7 |
10 import 'uuid.dart'; | |
11 import '../usage.dart'; | 8 import '../usage.dart'; |
12 | 9 import '../uuid/uuid.dart'; |
13 final int _MAX_EXCEPTION_LENGTH = 100; | |
14 | 10 |
15 String postEncode(Map<String, dynamic> map) { | 11 String postEncode(Map<String, dynamic> map) { |
16 // &foo=bar | 12 // &foo=bar |
17 return map.keys.map((key) { | 13 return map.keys.map((key) { |
18 String value = '${map[key]}'; | 14 String value = '${map[key]}'; |
19 return "${key}=${Uri.encodeComponent(value)}"; | 15 return "${key}=${Uri.encodeComponent(value)}"; |
20 }).join('&'); | 16 }).join('&'); |
21 } | 17 } |
22 | 18 |
23 /** | 19 /** |
24 * A throttling algorithim. This models the throttling after a bucket with | 20 * A throttling algorithm. This models the throttling after a bucket with |
25 * water dripping into it at the rate of 1 drop per second. If the bucket has | 21 * water dripping into it at the rate of 1 drop per second. If the bucket has |
26 * water when an operation is requested, 1 drop of water is removed and the | 22 * water when an operation is requested, 1 drop of water is removed and the |
27 * operation is performed. If not the operation is skipped. This algorithim | 23 * operation is performed. If not the operation is skipped. This algorithm |
28 * lets operations be peformed in bursts without throttling, but holds the | 24 * lets operations be performed in bursts without throttling, but holds the |
29 * overall average rate of operations to 1 per second. | 25 * overall average rate of operations to 1 per second. |
30 */ | 26 */ |
31 class ThrottlingBucket { | 27 class ThrottlingBucket { |
32 final int startingCount; | 28 final int startingCount; |
33 int drops; | 29 int drops; |
34 int _lastReplenish; | 30 int _lastReplenish; |
35 | 31 |
36 ThrottlingBucket(this.startingCount) { | 32 ThrottlingBucket(this.startingCount) { |
37 drops = startingCount; | 33 drops = startingCount; |
38 _lastReplenish = new DateTime.now().millisecondsSinceEpoch; | 34 _lastReplenish = new DateTime.now().millisecondsSinceEpoch; |
(...skipping 14 matching lines...) Expand all Loading... |
53 int now = new DateTime.now().millisecondsSinceEpoch; | 49 int now = new DateTime.now().millisecondsSinceEpoch; |
54 | 50 |
55 if (_lastReplenish + 1000 >= now) { | 51 if (_lastReplenish + 1000 >= now) { |
56 int inc = (now - _lastReplenish) ~/ 1000; | 52 int inc = (now - _lastReplenish) ~/ 1000; |
57 drops = math.min(drops + inc, startingCount); | 53 drops = math.min(drops + inc, startingCount); |
58 _lastReplenish += (1000 * inc); | 54 _lastReplenish += (1000 * inc); |
59 } | 55 } |
60 } | 56 } |
61 } | 57 } |
62 | 58 |
63 abstract class AnalyticsImpl extends Analytics { | 59 class AnalyticsImpl implements Analytics { |
64 static const String _GA_URL = 'https://www.google-analytics.com/collect'; | 60 static const String _defaultAnalyticsUrl = |
| 61 'https://www.google-analytics.com/collect'; |
65 | 62 |
66 /// Tracking ID / Property ID. | 63 @override |
67 final String trackingId; | 64 final String trackingId; |
| 65 @override |
| 66 final String applicationName; |
| 67 @override |
| 68 final String applicationVersion; |
68 | 69 |
69 final PersistentProperties properties; | 70 final PersistentProperties properties; |
70 final PostHandler postHandler; | 71 final PostHandler postHandler; |
71 | 72 |
72 final ThrottlingBucket _bucket = new ThrottlingBucket(20); | 73 final ThrottlingBucket _bucket = new ThrottlingBucket(20); |
73 final Map<String, dynamic> _variableMap = {}; | 74 final Map<String, dynamic> _variableMap = {}; |
74 | 75 |
75 final List<Future> _futures = []; | 76 final List<Future> _futures = []; |
76 | 77 |
| 78 @override |
| 79 AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut; |
| 80 |
| 81 String _url; |
| 82 |
| 83 StreamController<Map<String, dynamic>> _sendController = |
| 84 new StreamController.broadcast(sync: true); |
| 85 |
77 AnalyticsImpl(this.trackingId, this.properties, this.postHandler, | 86 AnalyticsImpl(this.trackingId, this.properties, this.postHandler, |
78 {String applicationName, String applicationVersion}) { | 87 {this.applicationName, this.applicationVersion, String analyticsUrl}) { |
79 assert(trackingId != null); | 88 assert(trackingId != null); |
80 | 89 |
81 if (applicationName != null) setSessionValue('an', applicationName); | 90 if (applicationName != null) setSessionValue('an', applicationName); |
82 if (applicationVersion != null) setSessionValue('av', applicationVersion); | 91 if (applicationVersion != null) setSessionValue('av', applicationVersion); |
| 92 |
| 93 _url = analyticsUrl ?? _defaultAnalyticsUrl; |
83 } | 94 } |
84 | 95 |
85 bool get optIn => properties['optIn'] == true; | 96 bool _firstRun; |
86 | 97 |
87 set optIn(bool value) { | 98 @override |
88 properties['optIn'] = value; | 99 bool get firstRun { |
| 100 if (_firstRun == null) { |
| 101 _firstRun = properties['firstRun'] == null; |
| 102 |
| 103 if (properties['firstRun'] != false) { |
| 104 properties['firstRun'] = false; |
| 105 } |
| 106 } |
| 107 |
| 108 return _firstRun; |
89 } | 109 } |
90 | 110 |
91 bool get hasSetOptIn => properties['optIn'] != null; | 111 @override |
| 112 bool get enabled { |
| 113 bool optIn = analyticsOpt == AnalyticsOpt.optIn; |
| 114 return optIn |
| 115 ? properties['enabled'] == true |
| 116 : properties['enabled'] != false; |
| 117 } |
92 | 118 |
93 Future sendScreenView(String viewName) { | 119 @override |
| 120 set enabled(bool value) { |
| 121 properties['enabled'] = value; |
| 122 } |
| 123 |
| 124 @override |
| 125 Future sendScreenView(String viewName, {Map<String, String> parameters}) { |
94 Map<String, dynamic> args = {'cd': viewName}; | 126 Map<String, dynamic> args = {'cd': viewName}; |
| 127 if (parameters != null) { |
| 128 args.addAll(parameters); |
| 129 } |
95 return _sendPayload('screenview', args); | 130 return _sendPayload('screenview', args); |
96 } | 131 } |
97 | 132 |
98 Future sendEvent(String category, String action, {String label, int value}) { | 133 @override |
99 if (!optIn) return new Future.value(); | 134 Future sendEvent(String category, String action, |
100 | 135 {String label, int value, Map<String, String> parameters}) { |
101 Map<String, dynamic> args = {'ec': category, 'ea': action}; | 136 Map<String, dynamic> args = {'ec': category, 'ea': action}; |
102 if (label != null) args['el'] = label; | 137 if (label != null) args['el'] = label; |
103 if (value != null) args['ev'] = value; | 138 if (value != null) args['ev'] = value; |
| 139 if (parameters != null) { |
| 140 args.addAll(parameters); |
| 141 } |
104 return _sendPayload('event', args); | 142 return _sendPayload('event', args); |
105 } | 143 } |
106 | 144 |
| 145 @override |
107 Future sendSocial(String network, String action, String target) { | 146 Future sendSocial(String network, String action, String target) { |
108 if (!optIn) return new Future.value(); | |
109 | |
110 Map<String, dynamic> args = {'sn': network, 'sa': action, 'st': target}; | 147 Map<String, dynamic> args = {'sn': network, 'sa': action, 'st': target}; |
111 return _sendPayload('social', args); | 148 return _sendPayload('social', args); |
112 } | 149 } |
113 | 150 |
114 Future sendTiming(String variableName, int time, {String category, | 151 @override |
115 String label}) { | 152 Future sendTiming(String variableName, int time, |
116 if (!optIn) return new Future.value(); | 153 {String category, String label}) { |
117 | |
118 Map<String, dynamic> args = {'utv': variableName, 'utt': time}; | 154 Map<String, dynamic> args = {'utv': variableName, 'utt': time}; |
119 if (label != null) args['utl'] = label; | 155 if (label != null) args['utl'] = label; |
120 if (category != null) args['utc'] = category; | 156 if (category != null) args['utc'] = category; |
121 return _sendPayload('timing', args); | 157 return _sendPayload('timing', args); |
122 } | 158 } |
123 | 159 |
124 AnalyticsTimer startTimer(String variableName, {String category, String label}
) { | 160 @override |
125 return new AnalyticsTimer(this, | 161 AnalyticsTimer startTimer(String variableName, |
126 variableName, category: category, label: label); | 162 {String category, String label}) { |
| 163 return new AnalyticsTimer(this, variableName, |
| 164 category: category, label: label); |
127 } | 165 } |
128 | 166 |
| 167 @override |
129 Future sendException(String description, {bool fatal}) { | 168 Future sendException(String description, {bool fatal}) { |
130 if (!optIn) return new Future.value(); | 169 // We trim exceptions to a max length; google analytics will apply it's own |
| 170 // truncation, likely around 150 chars or so. |
| 171 const int maxExceptionLength = 1000; |
131 | 172 |
132 // In order to ensure that the client of this API is not sending any PII | 173 // In order to ensure that the client of this API is not sending any PII |
133 // data, we strip out any stack trace that may reference a path on the | 174 // data, we strip out any stack trace that may reference a path on the |
134 // user's drive (file:/...). | 175 // user's drive (file:/...). |
135 if (description.contains('file:/')) { | 176 if (description.contains('file:/')) { |
136 description = description.substring(0, description.indexOf('file:/')); | 177 description = description.substring(0, description.indexOf('file:/')); |
137 } | 178 } |
138 | 179 |
139 if (description != null && description.length > _MAX_EXCEPTION_LENGTH) { | 180 description = description.replaceAll('\n', '; '); |
140 description = description.substring(0, _MAX_EXCEPTION_LENGTH); | 181 |
| 182 if (description.length > maxExceptionLength) { |
| 183 description = description.substring(0, maxExceptionLength); |
141 } | 184 } |
142 | 185 |
143 Map<String, dynamic> args = {'exd': description}; | 186 Map<String, dynamic> args = {'exd': description}; |
144 if (fatal != null && fatal) args['exf'] = '1'; | 187 if (fatal != null && fatal) args['exf'] = '1'; |
145 return _sendPayload('exception', args); | 188 return _sendPayload('exception', args); |
146 } | 189 } |
147 | 190 |
| 191 @override |
| 192 dynamic getSessionValue(String param) => _variableMap[param]; |
| 193 |
| 194 @override |
148 void setSessionValue(String param, dynamic value) { | 195 void setSessionValue(String param, dynamic value) { |
149 if (value == null) { | 196 if (value == null) { |
150 _variableMap.remove(param); | 197 _variableMap.remove(param); |
151 } else { | 198 } else { |
152 _variableMap[param] = value; | 199 _variableMap[param] = value; |
153 } | 200 } |
154 } | 201 } |
155 | 202 |
| 203 @override |
| 204 Stream<Map<String, dynamic>> get onSend => _sendController.stream; |
| 205 |
| 206 @override |
156 Future waitForLastPing({Duration timeout}) { | 207 Future waitForLastPing({Duration timeout}) { |
157 Future f = Future.wait(_futures).catchError((e) => null); | 208 Future f = Future.wait(_futures).catchError((e) => null); |
158 | 209 |
159 if (timeout != null) { | 210 if (timeout != null) { |
160 f = f.timeout(timeout, onTimeout: () => null); | 211 f = f.timeout(timeout, onTimeout: () => null); |
161 } | 212 } |
162 | 213 |
163 return f; | 214 return f; |
164 } | 215 } |
165 | 216 |
| 217 @override |
| 218 void close() => postHandler.close(); |
| 219 |
| 220 @override |
| 221 String get clientId => properties['clientId'] ??= new Uuid().generateV4(); |
| 222 |
166 /** | 223 /** |
167 * Anonymous Client ID. The value of this field should be a random UUID v4. | 224 * Send raw data to analytics. Callers should generally use one of the typed |
| 225 * methods (`sendScreenView`, `sendEvent`, ...). |
| 226 * |
| 227 * Valid values for [hitType] are: 'pageview', 'screenview', 'event', |
| 228 * 'transaction', 'item', 'social', 'exception', and 'timing'. |
168 */ | 229 */ |
169 String get _clientId => properties['clientId']; | 230 Future sendRaw(String hitType, Map<String, dynamic> args) { |
170 | 231 return _sendPayload(hitType, args); |
171 void _initClientId() { | |
172 if (_clientId == null) { | |
173 properties['clientId'] = new Uuid().generateV4(); | |
174 } | |
175 } | 232 } |
176 | 233 |
177 // Valid values for [hitType] are: 'pageview', 'screenview', 'event', | 234 /** |
178 // 'transaction', 'item', 'social', 'exception', and 'timing'. | 235 * Valid values for [hitType] are: 'pageview', 'screenview', 'event', |
| 236 * 'transaction', 'item', 'social', 'exception', and 'timing'. |
| 237 */ |
179 Future _sendPayload(String hitType, Map<String, dynamic> args) { | 238 Future _sendPayload(String hitType, Map<String, dynamic> args) { |
| 239 if (!enabled) return new Future.value(); |
| 240 |
180 if (_bucket.removeDrop()) { | 241 if (_bucket.removeDrop()) { |
181 _initClientId(); | |
182 | |
183 _variableMap.forEach((key, value) { | 242 _variableMap.forEach((key, value) { |
184 args[key] = value; | 243 args[key] = value; |
185 }); | 244 }); |
186 | 245 |
187 args['v'] = '1'; // protocol version | 246 args['v'] = '1'; // protocol version |
188 args['tid'] = trackingId; | 247 args['tid'] = trackingId; |
189 args['cid'] = _clientId; | 248 args['cid'] = clientId; |
190 args['t'] = hitType; | 249 args['t'] = hitType; |
191 | 250 |
192 return _recordFuture(postHandler.sendPost(_GA_URL, args)); | 251 _sendController.add(args); |
| 252 |
| 253 return _recordFuture(postHandler.sendPost(_url, args)); |
193 } else { | 254 } else { |
194 return new Future.value(); | 255 return new Future.value(); |
195 } | 256 } |
196 } | 257 } |
197 | 258 |
198 Future _recordFuture(Future f) { | 259 Future _recordFuture(Future f) { |
199 _futures.add(f); | 260 _futures.add(f); |
200 return f.whenComplete(() => _futures.remove(f)); | 261 return f.whenComplete(() => _futures.remove(f)); |
201 } | 262 } |
202 } | 263 } |
203 | 264 |
204 /** | 265 /** |
205 * A persistent key/value store. An [AnalyticsImpl] instance expects to have one | 266 * A persistent key/value store. An [AnalyticsImpl] instance expects to have one |
206 * of these injected into it. There are default implementations for `dart:io` | 267 * of these injected into it. There are default implementations for `dart:io` |
207 * and `dart:html` clients. | 268 * and `dart:html` clients. |
208 * | 269 * |
209 * The [name] paramater is used to uniquely store these properties on disk / | 270 * The [name] parameter is used to uniquely store these properties on disk / |
210 * persistent storage. | 271 * persistent storage. |
211 */ | 272 */ |
212 abstract class PersistentProperties { | 273 abstract class PersistentProperties { |
213 final String name; | 274 final String name; |
214 | 275 |
215 PersistentProperties(this.name); | 276 PersistentProperties(this.name); |
216 | 277 |
217 dynamic operator[](String key); | 278 dynamic operator [](String key); |
218 void operator[]=(String key, dynamic value); | 279 void operator []=(String key, dynamic value); |
| 280 |
| 281 /// Re-read settings from the backing store. This may be a no-op on some |
| 282 /// platforms. |
| 283 void syncSettings(); |
219 } | 284 } |
220 | 285 |
221 /** | 286 /** |
222 * A utility class to perform HTTP POSTs. An [AnalyticsImpl] instance expects to | 287 * A utility class to perform HTTP POSTs. An [AnalyticsImpl] instance expects to |
223 * have one of these injected into it. There are default implementations for | 288 * have one of these injected into it. There are default implementations for |
224 * `dart:io` and `dart:html` clients. | 289 * `dart:io` and `dart:html` clients. |
225 * | 290 * |
226 * The POST information should be sent on a best-effort basis. The `Future` from | 291 * The POST information should be sent on a best-effort basis. The `Future` from |
227 * [sendPost] should complete when the operation is finished, but failures to | 292 * [sendPost] should complete when the operation is finished, but failures to |
228 * send the information should be silent. | 293 * send the information should be silent. |
229 */ | 294 */ |
230 abstract class PostHandler { | 295 abstract class PostHandler { |
231 Future sendPost(String url, Map<String, dynamic> parameters); | 296 Future sendPost(String url, Map<String, dynamic> parameters); |
| 297 |
| 298 /// Free any used resources. |
| 299 void close(); |
232 } | 300 } |
OLD | NEW |