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