| 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 library usage_impl; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:math' as math; | |
| 9 | |
| 10 import 'uuid.dart'; | |
| 11 import '../usage.dart'; | |
| 12 | |
| 13 final int _MAX_EXCEPTION_LENGTH = 100; | |
| 14 | |
| 15 String postEncode(Map<String, dynamic> map) { | |
| 16 // &foo=bar | |
| 17 return map.keys.map((key) { | |
| 18 String value = '${map[key]}'; | |
| 19 return "${key}=${Uri.encodeComponent(value)}"; | |
| 20 }).join('&'); | |
| 21 } | |
| 22 | |
| 23 /** | |
| 24 * A throttling algorithim. 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 | |
| 26 * 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 | |
| 28 * lets operations be peformed in bursts without throttling, but holds the | |
| 29 * overall average rate of operations to 1 per second. | |
| 30 */ | |
| 31 class ThrottlingBucket { | |
| 32 final int startingCount; | |
| 33 int drops; | |
| 34 int _lastReplenish; | |
| 35 | |
| 36 ThrottlingBucket(this.startingCount) { | |
| 37 drops = startingCount; | |
| 38 _lastReplenish = new DateTime.now().millisecondsSinceEpoch; | |
| 39 } | |
| 40 | |
| 41 bool removeDrop() { | |
| 42 _checkReplenish(); | |
| 43 | |
| 44 if (drops <= 0) { | |
| 45 return false; | |
| 46 } else { | |
| 47 drops--; | |
| 48 return true; | |
| 49 } | |
| 50 } | |
| 51 | |
| 52 void _checkReplenish() { | |
| 53 int now = new DateTime.now().millisecondsSinceEpoch; | |
| 54 | |
| 55 if (_lastReplenish + 1000 >= now) { | |
| 56 int inc = (now - _lastReplenish) ~/ 1000; | |
| 57 drops = math.min(drops + inc, startingCount); | |
| 58 _lastReplenish += (1000 * inc); | |
| 59 } | |
| 60 } | |
| 61 } | |
| 62 | |
| 63 abstract class AnalyticsImpl extends Analytics { | |
| 64 static const String _GA_URL = 'https://www.google-analytics.com/collect'; | |
| 65 | |
| 66 /// Tracking ID / Property ID. | |
| 67 final String trackingId; | |
| 68 | |
| 69 final PersistentProperties properties; | |
| 70 final PostHandler postHandler; | |
| 71 | |
| 72 final ThrottlingBucket _bucket = new ThrottlingBucket(20); | |
| 73 final Map<String, dynamic> _variableMap = {}; | |
| 74 | |
| 75 final List<Future> _futures = []; | |
| 76 | |
| 77 AnalyticsImpl(this.trackingId, this.properties, this.postHandler, | |
| 78 {String applicationName, String applicationVersion}) { | |
| 79 assert(trackingId != null); | |
| 80 | |
| 81 if (applicationName != null) setSessionValue('an', applicationName); | |
| 82 if (applicationVersion != null) setSessionValue('av', applicationVersion); | |
| 83 } | |
| 84 | |
| 85 bool get optIn => properties['optIn'] == true; | |
| 86 | |
| 87 set optIn(bool value) { | |
| 88 properties['optIn'] = value; | |
| 89 } | |
| 90 | |
| 91 bool get hasSetOptIn => properties['optIn'] != null; | |
| 92 | |
| 93 Future sendScreenView(String viewName) { | |
| 94 Map args = {'cd': viewName}; | |
| 95 return _sendPayload('screenview', args); | |
| 96 } | |
| 97 | |
| 98 Future sendEvent(String category, String action, {String label, int value}) { | |
| 99 if (!optIn) return new Future.value(); | |
| 100 | |
| 101 Map args = {'ec': category, 'ea': action}; | |
| 102 if (label != null) args['el'] = label; | |
| 103 if (value != null) args['ev'] = value; | |
| 104 return _sendPayload('event', args); | |
| 105 } | |
| 106 | |
| 107 Future sendSocial(String network, String action, String target) { | |
| 108 if (!optIn) return new Future.value(); | |
| 109 | |
| 110 Map args = {'sn': network, 'sa': action, 'st': target}; | |
| 111 return _sendPayload('social', args); | |
| 112 } | |
| 113 | |
| 114 Future sendTiming(String variableName, int time, {String category, | |
| 115 String label}) { | |
| 116 if (!optIn) return new Future.value(); | |
| 117 | |
| 118 Map args = {'utv': variableName, 'utt': time}; | |
| 119 if (label != null) args['utl'] = label; | |
| 120 if (category != null) args['utc'] = category; | |
| 121 return _sendPayload('timing', args); | |
| 122 } | |
| 123 | |
| 124 AnalyticsTimer startTimer(String variableName, {String category, String label}
) { | |
| 125 return new AnalyticsTimer(this, | |
| 126 variableName, category: category, label: label); | |
| 127 } | |
| 128 | |
| 129 Future sendException(String description, {bool fatal}) { | |
| 130 if (!optIn) return new Future.value(); | |
| 131 | |
| 132 // 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 | |
| 134 // user's drive (file:/...). | |
| 135 if (description.contains('file:/')) { | |
| 136 description = description.substring(0, description.indexOf('file:/')); | |
| 137 } | |
| 138 | |
| 139 if (description != null && description.length > _MAX_EXCEPTION_LENGTH) { | |
| 140 description = description.substring(0, _MAX_EXCEPTION_LENGTH); | |
| 141 } | |
| 142 | |
| 143 Map args = {'exd': description}; | |
| 144 if (fatal != null && fatal) args['exf'] = '1'; | |
| 145 return _sendPayload('exception', args); | |
| 146 } | |
| 147 | |
| 148 void setSessionValue(String param, dynamic value) { | |
| 149 if (value == null) { | |
| 150 _variableMap.remove(param); | |
| 151 } else { | |
| 152 _variableMap[param] = value; | |
| 153 } | |
| 154 } | |
| 155 | |
| 156 Future waitForLastPing({Duration timeout}) { | |
| 157 Future f = Future.wait(_futures).catchError((e) => null); | |
| 158 | |
| 159 if (timeout != null) { | |
| 160 f = f.timeout(timeout, onTimeout: () => null); | |
| 161 } | |
| 162 | |
| 163 return f; | |
| 164 } | |
| 165 | |
| 166 /** | |
| 167 * Anonymous Client ID. The value of this field should be a random UUID v4. | |
| 168 */ | |
| 169 String get _clientId => properties['clientId']; | |
| 170 | |
| 171 void _initClientId() { | |
| 172 if (_clientId == null) { | |
| 173 properties['clientId'] = new Uuid().generateV4(); | |
| 174 } | |
| 175 } | |
| 176 | |
| 177 // Valid values for [hitType] are: 'pageview', 'screenview', 'event', | |
| 178 // 'transaction', 'item', 'social', 'exception', and 'timing'. | |
| 179 Future _sendPayload(String hitType, Map args) { | |
| 180 if (_bucket.removeDrop()) { | |
| 181 _initClientId(); | |
| 182 | |
| 183 _variableMap.forEach((key, value) { | |
| 184 args[key] = value; | |
| 185 }); | |
| 186 | |
| 187 args['v'] = '1'; // protocol version | |
| 188 args['tid'] = trackingId; | |
| 189 args['cid'] = _clientId; | |
| 190 args['t'] = hitType; | |
| 191 | |
| 192 return _recordFuture(postHandler.sendPost(_GA_URL, args)); | |
| 193 } else { | |
| 194 return new Future.value(); | |
| 195 } | |
| 196 } | |
| 197 | |
| 198 Future _recordFuture(Future f) { | |
| 199 _futures.add(f); | |
| 200 return f.whenComplete(() => _futures.remove(f)); | |
| 201 } | |
| 202 } | |
| 203 | |
| 204 /** | |
| 205 * 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` | |
| 207 * and `dart:html` clients. | |
| 208 * | |
| 209 * The [name] paramater is used to uniquely store these properties on disk / | |
| 210 * persistent storage. | |
| 211 */ | |
| 212 abstract class PersistentProperties { | |
| 213 final String name; | |
| 214 | |
| 215 PersistentProperties(this.name); | |
| 216 | |
| 217 dynamic operator[](String key); | |
| 218 void operator[]=(String key, dynamic value); | |
| 219 } | |
| 220 | |
| 221 /** | |
| 222 * 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 | |
| 224 * `dart:io` and `dart:html` clients. | |
| 225 * | |
| 226 * 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 | |
| 228 * send the information should be silent. | |
| 229 */ | |
| 230 abstract class PostHandler { | |
| 231 Future sendPost(String url, Map<String, String> parameters); | |
| 232 } | |
| OLD | NEW |