| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:math' as math; |
| |
| import '../usage.dart'; |
| import 'uuid.dart'; |
| |
| final int _MAX_EXCEPTION_LENGTH = 100; |
| |
| String postEncode(Map<String, dynamic> map) { |
| // &foo=bar |
| return map.keys.map((key) { |
| String value = '${map[key]}'; |
| return "${key}=${Uri.encodeComponent(value)}"; |
| }).join('&'); |
| } |
| |
| /** |
| * A throttling algorithim. This models the throttling after a bucket with |
| * water dripping into it at the rate of 1 drop per second. If the bucket has |
| * water when an operation is requested, 1 drop of water is removed and the |
| * operation is performed. If not the operation is skipped. This algorithim |
| * lets operations be peformed in bursts without throttling, but holds the |
| * overall average rate of operations to 1 per second. |
| */ |
| class ThrottlingBucket { |
| final int startingCount; |
| int drops; |
| int _lastReplenish; |
| |
| ThrottlingBucket(this.startingCount) { |
| drops = startingCount; |
| _lastReplenish = new DateTime.now().millisecondsSinceEpoch; |
| } |
| |
| bool removeDrop() { |
| _checkReplenish(); |
| |
| if (drops <= 0) { |
| return false; |
| } else { |
| drops--; |
| return true; |
| } |
| } |
| |
| void _checkReplenish() { |
| int now = new DateTime.now().millisecondsSinceEpoch; |
| |
| if (_lastReplenish + 1000 >= now) { |
| int inc = (now - _lastReplenish) ~/ 1000; |
| drops = math.min(drops + inc, startingCount); |
| _lastReplenish += (1000 * inc); |
| } |
| } |
| } |
| |
| class AnalyticsImpl implements Analytics { |
| static const String _defaultAnalyticsUrl = 'https://www.google-analytics.com/collect'; |
| |
| /** |
| * Tracking ID / Property ID. |
| */ |
| final String trackingId; |
| |
| final PersistentProperties properties; |
| final PostHandler postHandler; |
| |
| final ThrottlingBucket _bucket = new ThrottlingBucket(20); |
| final Map<String, dynamic> _variableMap = {}; |
| |
| final List<Future> _futures = []; |
| |
| AnalyticsOpt analyticsOpt = AnalyticsOpt.optOut; |
| |
| String _url; |
| |
| StreamController<Map<String, dynamic>> _sendController = new StreamController.broadcast(sync: true); |
| |
| AnalyticsImpl( |
| this.trackingId, |
| this.properties, |
| this.postHandler, { |
| String applicationName, |
| String applicationVersion, |
| String analyticsUrl |
| }) { |
| assert(trackingId != null); |
| |
| if (applicationName != null) setSessionValue('an', applicationName); |
| if (applicationVersion != null) setSessionValue('av', applicationVersion); |
| |
| _url = analyticsUrl ?? _defaultAnalyticsUrl; |
| } |
| |
| bool _firstRun; |
| |
| bool get firstRun { |
| if (_firstRun == null) { |
| _firstRun = properties['firstRun'] == null; |
| |
| if (properties['firstRun'] != false) { |
| properties['firstRun'] = false; |
| } |
| } |
| |
| return _firstRun; |
| } |
| |
| /** |
| * Will analytics data be sent? |
| */ |
| bool get enabled { |
| bool optIn = analyticsOpt == AnalyticsOpt.optIn; |
| return optIn ? properties['enabled'] == true : properties['enabled'] != false; |
| } |
| |
| /** |
| * Enable or disable sending of analytics data. |
| */ |
| set enabled(bool value) { |
| properties['enabled'] = value; |
| } |
| |
| Future sendScreenView(String viewName) { |
| Map<String, dynamic> args = {'cd': viewName}; |
| return _sendPayload('screenview', args); |
| } |
| |
| Future sendEvent(String category, String action, {String label, int value}) { |
| Map<String, dynamic> args = {'ec': category, 'ea': action}; |
| if (label != null) args['el'] = label; |
| if (value != null) args['ev'] = value; |
| return _sendPayload('event', args); |
| } |
| |
| Future sendSocial(String network, String action, String target) { |
| Map<String, dynamic> args = {'sn': network, 'sa': action, 'st': target}; |
| return _sendPayload('social', args); |
| } |
| |
| Future sendTiming(String variableName, int time, {String category, String label}) { |
| Map<String, dynamic> args = {'utv': variableName, 'utt': time}; |
| if (label != null) args['utl'] = label; |
| if (category != null) args['utc'] = category; |
| return _sendPayload('timing', args); |
| } |
| |
| AnalyticsTimer startTimer(String variableName, {String category, String label}) { |
| return new AnalyticsTimer(this, |
| variableName, category: category, label: label); |
| } |
| |
| Future sendException(String description, {bool fatal}) { |
| // In order to ensure that the client of this API is not sending any PII |
| // data, we strip out any stack trace that may reference a path on the |
| // user's drive (file:/...). |
| if (description.contains('file:/')) { |
| description = description.substring(0, description.indexOf('file:/')); |
| } |
| |
| if (description != null && description.length > _MAX_EXCEPTION_LENGTH) { |
| description = description.substring(0, _MAX_EXCEPTION_LENGTH); |
| } |
| |
| Map<String, dynamic> args = {'exd': description}; |
| if (fatal != null && fatal) args['exf'] = '1'; |
| return _sendPayload('exception', args); |
| } |
| |
| dynamic getSessionValue(String param) => _variableMap[param]; |
| |
| void setSessionValue(String param, dynamic value) { |
| if (value == null) { |
| _variableMap.remove(param); |
| } else { |
| _variableMap[param] = value; |
| } |
| } |
| |
| Stream<Map<String, dynamic>> get onSend => _sendController.stream; |
| |
| Future waitForLastPing({Duration timeout}) { |
| Future f = Future.wait(_futures).catchError((e) => null); |
| |
| if (timeout != null) { |
| f = f.timeout(timeout, onTimeout: () => null); |
| } |
| |
| return f; |
| } |
| |
| /** |
| * Anonymous Client ID. The value of this field should be a random UUID v4. |
| */ |
| String get _clientId => properties['clientId']; |
| |
| void _initClientId() { |
| if (_clientId == null) { |
| properties['clientId'] = new Uuid().generateV4(); |
| } |
| } |
| |
| /** |
| * Send raw data to analytics. Callers should generally use one of the typed |
| * methods (`sendScreenView`, `sendEvent`, ...). |
| * |
| * Valid values for [hitType] are: 'pageview', 'screenview', 'event', |
| * 'transaction', 'item', 'social', 'exception', and 'timing'. |
| */ |
| Future sendRaw(String hitType, Map<String, dynamic> args) { |
| return _sendPayload(hitType, args); |
| } |
| |
| /** |
| * Valid values for [hitType] are: 'pageview', 'screenview', 'event', |
| * 'transaction', 'item', 'social', 'exception', and 'timing'. |
| */ |
| Future _sendPayload(String hitType, Map<String, dynamic> args) { |
| if (!enabled) return new Future.value(); |
| |
| if (_bucket.removeDrop()) { |
| _initClientId(); |
| |
| _variableMap.forEach((key, value) { |
| args[key] = value; |
| }); |
| |
| args['v'] = '1'; // protocol version |
| args['tid'] = trackingId; |
| args['cid'] = _clientId; |
| args['t'] = hitType; |
| |
| _sendController.add(args); |
| |
| return _recordFuture(postHandler.sendPost(_url, args)); |
| } else { |
| return new Future.value(); |
| } |
| } |
| |
| Future _recordFuture(Future f) { |
| _futures.add(f); |
| return f.whenComplete(() => _futures.remove(f)); |
| } |
| } |
| |
| /** |
| * A persistent key/value store. An [AnalyticsImpl] instance expects to have one |
| * of these injected into it. There are default implementations for `dart:io` |
| * and `dart:html` clients. |
| * |
| * The [name] paramater is used to uniquely store these properties on disk / |
| * persistent storage. |
| */ |
| abstract class PersistentProperties { |
| final String name; |
| |
| PersistentProperties(this.name); |
| |
| dynamic operator[](String key); |
| void operator[]=(String key, dynamic value); |
| } |
| |
| /** |
| * A utility class to perform HTTP POSTs. An [AnalyticsImpl] instance expects to |
| * have one of these injected into it. There are default implementations for |
| * `dart:io` and `dart:html` clients. |
| * |
| * The POST information should be sent on a best-effort basis. The `Future` from |
| * [sendPost] should complete when the operation is finished, but failures to |
| * send the information should be silent. |
| */ |
| abstract class PostHandler { |
| Future sendPost(String url, Map<String, dynamic> parameters); |
| } |