From 204e1ab5b339daa009792f68f67dc3c2c0c46445 Mon Sep 17 00:00:00 2001
From: Devon Carew <devoncarew@gmail.com>
Date: Mon, 1 Sep 2014 22:24:12 -0700
Subject: [PATCH] work on a ga library

---
 lib/analytics/analytics.dart        | 139 ++++++++++++++++++++++++++++
 lib/properties/properties.dart      |  43 +++++++++
 lib/properties/properties_html.dart |  37 ++++++++
 lib/properties/properties_io.dart   |  52 +++++++++++
 pubspec.yaml                        |   3 +-
 test/all.dart                       |   5 +-
 test/properties_io_test.dart        |  33 +++++++
 7 files changed, 308 insertions(+), 4 deletions(-)
 create mode 100644 lib/analytics/analytics.dart
 create mode 100644 lib/properties/properties.dart
 create mode 100644 lib/properties/properties_html.dart
 create mode 100644 lib/properties/properties_io.dart
 create mode 100644 test/properties_io_test.dart

diff --git a/lib/analytics/analytics.dart b/lib/analytics/analytics.dart
new file mode 100644
index 0000000..8cb1cbd
--- /dev/null
+++ b/lib/analytics/analytics.dart
@@ -0,0 +1,139 @@
+// Copyright (c) 2014, Google Inc. 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.
+
+/**
+ * TODO:
+ */
+library stagehand.analytics;
+
+import 'dart:async';
+import 'dart:math' as math;
+
+// TODO: Under construction!
+
+// TODO: store a generated anonymous client id
+
+// TODO: store whether the user has opted in / out
+
+// https://developers.google.com/analytics/devguides/collection/protocol/policy
+
+class Analytics {
+  static const String _GA_URL = 'https://www.google-analytics.com/collect';
+
+  /// Tracking ID / Property ID.
+  final String trackingId;
+  final PropertiesHandler propertiesHandler;
+  final PostHandler postHandler;
+  final String applicationVersion;
+  final String applicationId;
+
+  /// Anonymous Client ID. The value of this field should be a random UUID
+  /// (version 4).
+  String _clientId;
+  final _ThrottlingBucket _bucket = new _ThrottlingBucket(20);
+
+  Analytics(this.trackingId, this.propertiesHandler, this.postHandler,
+      {this.applicationVersion, this.applicationId}) {
+    assert(trackingId != null);
+  }
+
+//  Future sendPageView() {
+//
+//  }
+
+  Future sendScreenView() {
+    // TODO:
+    Map args = {};
+    return _sendPayload('screenview', args);
+  }
+
+  Future sendEvent() {
+    // TODO:
+    return new Future.value();
+  }
+
+  Future sendException(String description, [bool fatal]) {
+    Map args = {'exd': description};
+    if (fatal != null && fatal) args['exf'] = '1';
+    return _sendPayload('exception', args);
+  }
+
+  // Valid values for [hitType] are: 'pageview', 'screenview', 'event',
+  // 'transaction', 'item', 'social', 'exception', and 'timing'.
+  Future _sendPayload(String hitType, Map args) {
+    assert(hitType != null);
+
+    if (_bucket.removeDrop()) {
+      args['v'] = '1'; // version
+      args['tid'] = trackingId;
+      args['cid'] = _clientId;
+      args['t'] = hitType;
+
+      if (applicationId != null) args['aid'] = applicationId;
+      if (applicationVersion != null) args['av'] = applicationVersion;
+
+      return postHandler.sendPost(_GA_URL, args);
+    } else {
+      return new Future.value();
+    }
+  }
+}
+
+/**
+ * TODO:
+ */
+abstract class PropertiesHandler {
+  dynamic getProperty(String key);
+  void setProperty(String key, dynamic value);
+}
+
+/**
+ * TODO:
+ */
+abstract class PostHandler {
+  /**
+   * TODO:
+   */
+  Future sendPost(String url, Map<String, String> parameters);
+}
+
+/**
+ * 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;
+    } {
+      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);
+    }
+  }
+}
diff --git a/lib/properties/properties.dart b/lib/properties/properties.dart
new file mode 100644
index 0000000..8fa1713
--- /dev/null
+++ b/lib/properties/properties.dart
@@ -0,0 +1,43 @@
+// Copyright (c) 2014, Google Inc. 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.
+
+library stagehand.properties;
+
+import 'dart:async';
+
+/**
+ * TODO: doc
+ */
+abstract class Properties {
+  final String name;
+  final Map<String, dynamic> _map = {};
+  bool dirty = false;
+
+  Properties(this.name);
+
+  /**
+   * Get the value for the given key. This is an alias for the index operator to
+   * aid in discoverability.
+   */
+  dynamic getValue(String key) => this[key];
+
+  /**
+   * Set the value for the given key. This is an alias for the index operator to
+   * aid in discoverability.
+   */
+  void setValue(String key, dynamic value) => this[key] = value;
+
+  dynamic operator[](String key) => _map[key];
+
+  void operator[]=(String key, dynamic value) {
+    dirty = true;
+    _map[key] = value;
+  }
+
+  Future flush();
+
+  Map<String, dynamic> toMap() => new Map.from(_map);
+
+  String toString() => '${name} properties';
+}
diff --git a/lib/properties/properties_html.dart b/lib/properties/properties_html.dart
new file mode 100644
index 0000000..3aa2716
--- /dev/null
+++ b/lib/properties/properties_html.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2014, Google Inc. 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.
+
+library stagehand.properties_html;
+
+import 'dart:async';
+import 'dart:convert' show JSON;
+import 'dart:html';
+
+import 'properties.dart';
+export 'properties.dart';
+
+// TODO: test the html version
+
+/**
+ * TODO: doc
+ */
+class PropertiesHtml extends Properties {
+  static Future<PropertiesHtml> create(String name) {
+    String str = window.localStorage[name];
+    if (str == null || str.isEmpty) str = '{}';
+    Map map = JSON.decode(str);
+    return new Future.value(new PropertiesHtml._(name, map));
+  }
+
+  PropertiesHtml._(String name, Map other) : super(name) {
+    // Copy the map.
+    other.forEach((key, value) => this[key] = value);
+    dirty = false;
+  }
+
+  Future flush() {
+    window.localStorage[name] = JSON.encode(toMap());
+    return new Future.value();
+  }
+}
diff --git a/lib/properties/properties_io.dart b/lib/properties/properties_io.dart
new file mode 100644
index 0000000..ba88265
--- /dev/null
+++ b/lib/properties/properties_io.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2014, Google Inc. 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.
+
+library stagehand.properties_io;
+
+import 'dart:async';
+import 'dart:convert' show JSON;
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+
+import 'properties.dart';
+export 'properties.dart';
+
+/**
+ * TODO: doc
+ */
+class PropertiesIo extends Properties {
+  static Future<PropertiesIo> create(String name) {
+    String filename = '.${name.replaceAll(' ', '_')}';
+    File file = new File(path.join(_userHomeDir(), filename));
+
+    return file.create().then((_) {
+      return file.readAsString();
+    }).then((String contents) {
+      if (contents.isEmpty) contents = '{}';
+      Map map = JSON.decode(contents);
+      return new PropertiesIo._(name, file, map);
+    });
+  }
+
+  final File file;
+
+  PropertiesIo._(String name, this.file, Map other) : super(name) {
+    // Copy the map.
+    other.forEach((key, value) => this[key] = value);
+    dirty = false;
+  }
+
+  Future flush() {
+    return file.writeAsString(JSON.encode(toMap())).then((_) {
+      dirty = false;
+    });
+  }
+}
+
+String _userHomeDir() {
+  String envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
+  String value = Platform.environment[envKey];
+  return value == null ? '.' : value;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 2c8e5c6..515be65 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -5,7 +5,8 @@ homepage: https://github.com/sethladd/stagehand
 authors:
 - Seth Ladd <sethladd@gmail.com>
 - Devon Carew <devoncarew@google.com>
-dev_dependencies:
+dependencies:
   args: any
   path: any
+dev_dependencies:
   unittest: any
diff --git a/test/all.dart b/test/all.dart
index ce3d4aa..453a0d0 100644
--- a/test/all.dart
+++ b/test/all.dart
@@ -5,12 +5,11 @@
 import 'cli_test.dart' as cli_test;
 import 'common_test.dart' as common_test;
 import 'generators_test.dart' as generators_test;
-
-// TODO: integration tests. generate all the samples, run the analyzer over the
-// dart code
+import 'properties_io_test.dart' as properties_io_test;
 
 void main() {
   cli_test.defineTests();
   common_test.defineTests();
   generators_test.defineTests();
+  properties_io_test.defineTests();
 }
diff --git a/test/properties_io_test.dart b/test/properties_io_test.dart
new file mode 100644
index 0000000..893a83d
--- /dev/null
+++ b/test/properties_io_test.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2014, Google Inc. 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.
+
+library stagehand.properties_io_test;
+
+import 'package:stagehand/properties/properties_io.dart';
+import 'package:unittest/unittest.dart';
+
+void main() => defineTests();
+
+void defineTests() {
+  group('properties_io', () {
+    test('create', () {
+      return PropertiesIo.create('test_foo');
+    });
+
+    test('set get', () {
+      return PropertiesIo.create('test_foo').then((Properties props) {
+        props['foo'] = 'bar';
+        expect(props['foo'], 'bar');
+      });
+    });
+
+    test('dirty', () {
+      return PropertiesIo.create('test_foo').then((Properties props) {
+        expect(props.dirty, false);
+        props['foo'] = 'bar';
+        expect(props.dirty, true);
+      });
+    });
+  });
+}
-- 
GitLab