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 |