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