| 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 |