From 710bf6f4011b42e8584c108150b22e09c761c688 Mon Sep 17 00:00:00 2001 From: "lukechurch@google.com" Date: Fri, 5 Sep 2014 14:00:08 +0000 Subject: [PATCH] First cut of basic functionality This CL addresses comments from the previous Pull Request https://github.com/lukechurch/dart-microlytics/pull/1 It can be used to reporting usage and performance data to Google Analytics. Known issues: -> unittest library is still used -> some comments are still not in full sentences R=ahe@google.com, danrubel@google.com Review URL: https://codereview.chromium.org//515993003 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@39914 260f80e4-7a28-3924-810f-c04153c831b5 --- pkg/microlytics/example/simple.dart | 26 ++++ pkg/microlytics/lib/channels.dart | 55 ++++++++ pkg/microlytics/lib/html_channels.dart | 14 ++ pkg/microlytics/lib/io_channels.dart | 21 +++ pkg/microlytics/lib/microlytics.dart | 61 +++++++++ pkg/microlytics/pubspec.yaml | 8 ++ .../test/dart_microlytics_test.dart | 120 ++++++++++++++++++ pkg/microlytics/test/test_channel.dart | 19 +++ 8 files changed, 324 insertions(+) create mode 100644 pkg/microlytics/example/simple.dart create mode 100644 pkg/microlytics/lib/channels.dart create mode 100644 pkg/microlytics/lib/html_channels.dart create mode 100644 pkg/microlytics/lib/io_channels.dart create mode 100644 pkg/microlytics/lib/microlytics.dart create mode 100644 pkg/microlytics/pubspec.yaml create mode 100644 pkg/microlytics/test/dart_microlytics_test.dart create mode 100644 pkg/microlytics/test/test_channel.dart diff --git a/pkg/microlytics/example/simple.dart b/pkg/microlytics/example/simple.dart new file mode 100644 index 00000000000..b0b6792fc3b --- /dev/null +++ b/pkg/microlytics/example/simple.dart @@ -0,0 +1,26 @@ +// 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 'package:microlytics/channels.dart'; +import 'package:microlytics/io_channels.dart'; +import 'package:microlytics/microlytics.dart'; + +void main(List arguments) { + // Create the channel that will be used to communicate to analytics. + var channel = new RateLimitingBufferedChannel( + new HttpClientChannel(), packetsPerSecond: 1.0); + + if (arguments.length != 1) { + print("usage: dart simple.dart GA-Client-ID"); + return; + } + final String clientID = arguments.single; + + // Create the logger. + var lg = new AnalyticsLogger(channel, "555", clientID, "test", "1.2"); + + // Send some messages. + lg.logAnonymousEvent("hello", "world"); + lg.logAnonymousTiming("loader", "var", 42); +} \ No newline at end of file diff --git a/pkg/microlytics/lib/channels.dart b/pkg/microlytics/lib/channels.dart new file mode 100644 index 00000000000..88dee4988d8 --- /dev/null +++ b/pkg/microlytics/lib/channels.dart @@ -0,0 +1,55 @@ +// 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. + +library microlytics.channels; + +import 'dart:async'; + +const String ANALYTICS_URL = "https://ssl.google-analytics.com/collect"; + +abstract class Channel { + void sendData(String data); + void shutdown() {} +} + +/// [Channel] that implements a leaky bucket +/// algorithm to provide rate limiting. +/// See [http://en.wikipedia.org/wiki/Leaky_bucket]. +class RateLimitingBufferedChannel extends Channel { + final List _buffer = []; + final Channel _innerChannel; + final int _bufferSizeLimit; + Timer _timer; + + RateLimitingBufferedChannel( + this._innerChannel, + {int bufferSizeLimit: 10, + double packetsPerSecond: 1.0}) + : this._bufferSizeLimit = bufferSizeLimit { + if (!(packetsPerSecond > 0)) { + throw new ArgumentError("packetsPerSecond must be larger than zero."); + } + + int transmitDelay = (1000 / packetsPerSecond).floor(); + _timer = new Timer.periodic( + new Duration(milliseconds: transmitDelay), _onTimerTick); + } + + void _onTimerTick(_) { + if (_buffer.length > 0) { + String item = _buffer.removeLast(); + _innerChannel.sendData(item); + } + } + + void sendData(String data) { + if (_buffer.length >= _bufferSizeLimit) return; + _buffer.add(data); + } + + void shutdown() { + _timer.cancel(); + _innerChannel.shutdown(); + } +} \ No newline at end of file diff --git a/pkg/microlytics/lib/html_channels.dart b/pkg/microlytics/lib/html_channels.dart new file mode 100644 index 00000000000..b3eef43bbac --- /dev/null +++ b/pkg/microlytics/lib/html_channels.dart @@ -0,0 +1,14 @@ +// 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. + +library microlytics.html_channels; + +import 'dart:html'; +import 'channels.dart'; + +class HttpRequestChannel extends Channel { + void sendData(String data) { + HttpRequest.request(ANALYTICS_URL, method: "POST", sendData: data); + } +} diff --git a/pkg/microlytics/lib/io_channels.dart b/pkg/microlytics/lib/io_channels.dart new file mode 100644 index 00000000000..035e9a1a76c --- /dev/null +++ b/pkg/microlytics/lib/io_channels.dart @@ -0,0 +1,21 @@ +// 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. + +library microlytics.io_channels; + +import 'dart:io'; +import 'channels.dart'; + +class HttpClientChannel extends Channel { + void sendData(String data) { + HttpClient client = new HttpClient(); + client.postUrl(Uri.parse(ANALYTICS_URL)).then((HttpClientRequest req) { + req.write(data); + return req.close(); + }).then((HttpClientResponse response) { + response.drain(); + }); + } +} + diff --git a/pkg/microlytics/lib/microlytics.dart b/pkg/microlytics/lib/microlytics.dart new file mode 100644 index 00000000000..84d89e3257d --- /dev/null +++ b/pkg/microlytics/lib/microlytics.dart @@ -0,0 +1,61 @@ +// 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. + +library microlytics; + +import 'channels.dart'; + +/// Very limited implementation of an API to report usage to Google Analytics. +/// No Personally Identifiable Information must ever be passed to this class. +class AnalyticsLogger { + final Channel _channel; + final String _clientID; + final String _analyticsID; + final String _appName; + final String _appVersion; + final String _messagePrefix; //Computed prefix for analytics messages + + /// Create a new logger + /// [channel] represents how this is going to be sent, this would typically + /// be a [RateLimitingBufferedChannel] wrapping either a [HttpRequestChannel] + /// or a [HttpClientChannel]. + /// [clientID] is a version 4 UUID associated with the site or app. + /// [appName] is an application name. + /// [appVersion] is a verion string. + AnalyticsLogger(Channel channel, String clientID, String analyticsID, + String appName, String appVersion) + : this._channel = channel, + this._clientID = clientID, + this._analyticsID = analyticsID, + this._appName = appName, + this._appVersion = appVersion, + this._messagePrefix = + "v=1" + "&tid=$analyticsID" + "&cid=$clientID" + "&an=$appName" + "&av=$appVersion"; + + void logAnonymousTiming(String category, String variable, int ms) { + category = Uri.encodeComponent(category); + variable = Uri.encodeComponent(variable); + _channel.sendData( + "${this._messagePrefix}" + "&t=timing" + "&utc=$category" + "&utv=$variable" + "&utt=$ms"); + } + + void logAnonymousEvent(String category, String event) { + category = Uri.encodeComponent(category); + event = Uri.encodeComponent(event); + _channel.sendData( + "${this._messagePrefix}" + "&t=event" + "&ec=$category" + "&ea=$event"); + } +} + diff --git a/pkg/microlytics/pubspec.yaml b/pkg/microlytics/pubspec.yaml new file mode 100644 index 00000000000..620b7cb6f83 --- /dev/null +++ b/pkg/microlytics/pubspec.yaml @@ -0,0 +1,8 @@ +# 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. + +name: microlytics +description: A minimal implementation of the Analytics API in pure Dart +dev_dependencies: + unittest: any diff --git a/pkg/microlytics/test/dart_microlytics_test.dart b/pkg/microlytics/test/dart_microlytics_test.dart new file mode 100644 index 00000000000..eab10025ee3 --- /dev/null +++ b/pkg/microlytics/test/dart_microlytics_test.dart @@ -0,0 +1,120 @@ +// 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. + +library microlytics.test; + +import 'package:expect/expect.dart'; +import 'package:microlytics/microlytics.dart'; + +import 'test_channel.dart'; + +void main() { + testBasicEventRead(); + testBasicNegativeEventRead(); + testBasicTimingRead(); + testBasicTimingMultiread(); +} + +void testBasicEventRead() { + TestChannel c = new TestChannel(); + AnalyticsLogger logger = new AnalyticsLogger( + c, + "2cfac780-31e2-11e4-8c21-0800200c9a66", + "UA-53895644-1", + "TestApp", + "0.42"); + logger.logAnonymousEvent("video", "play"); + Expect.isTrue(c.contains( + "v=1" + "&tid=UA-53895644-1" + "&cid=2cfac780-31e2-11e4-8c21-0800200c9a66" + "&an=TestApp" + "&av=0.42" + "&t=event" + "&ec=video" + "&ea=play")); +} + +void testBasicNegativeEventRead() { + TestChannel c = new TestChannel(); + AnalyticsLogger logger = new AnalyticsLogger( + c, + "2cfac780-31e2-11e4-8c21-0800200c9a66", + "UA-53895644-1", + "TestApp", + "0.42"); + logger.logAnonymousEvent("video", "play"); + Expect.isFalse(c.contains( + "v=1" + "&tid=UA-53895644-1" + "&cid=2cfac780-31e2-11e4-8c21-0800200c9a66" + "&an=TestApp" + "&av=XXX" + "&t=event" + "&ec=video" + "&ea=play")); +} + +void testBasicTimingRead() { + TestChannel c = new TestChannel(); + AnalyticsLogger logger = new AnalyticsLogger( + c, + "2cfac780-31e2-11e4-8c21-0800200c9a66", + "UA-53895644-1", + "TestApp", + "0.42"); + logger.logAnonymousTiming("video", "delay", 157); + Expect.isTrue(c.contains( + "v=1" + "&tid=UA-53895644-1" + "&cid=2cfac780-31e2-11e4-8c21-0800200c9a66" + "&an=TestApp" + "&av=0.42" + "&t=timing" + "&utc=video" + "&utv=delay" + "&utt=157")); +} + +void testBasicTimingMultiread() { + TestChannel c = new TestChannel(); + AnalyticsLogger logger = new AnalyticsLogger( + c, + "2cfac780-31e2-11e4-8c21-0800200c9a66", + "UA-53895644-1", + "TestApp", + "0.42"); + logger.logAnonymousTiming("video", "delay", 159); + logger.logAnonymousTiming("video", "delay", 152); + Expect.isTrue(c.contains( + "v=1" + "&tid=UA-53895644-1" + "&cid=2cfac780-31e2-11e4-8c21-0800200c9a66" + "&an=TestApp" + "&av=0.42" + "&t=timing" + "&utc=video" + "&utv=delay" + "&utt=152")); + Expect.isTrue(c.contains( + "v=1" + "&tid=UA-53895644-1" + "&cid=2cfac780-31e2-11e4-8c21-0800200c9a66" + "&an=TestApp" + "&av=0.42" + "&t=timing" + "&utc=video" + "&utv=delay" + "&utt=159")); + Expect.isFalse(c.contains( + "v=1" + "&tid=UA-53895644-1" + "&cid=2cfac780-31e2-11e4-8c21-0800200c9a66" + "&an=TestApp" + "&av=0.42" + "&t=timing" + "&utc=video" + "&utv=delay" + "&utt=19")); +} \ No newline at end of file diff --git a/pkg/microlytics/test/test_channel.dart b/pkg/microlytics/test/test_channel.dart new file mode 100644 index 00000000000..a7f9c8bd8b1 --- /dev/null +++ b/pkg/microlytics/test/test_channel.dart @@ -0,0 +1,19 @@ +// 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. + +library microlytics.test_channel; + +import 'package:microlytics/channels.dart'; + +class TestChannel extends Channel { + List _channelLog = []; + + void sendData(String data) { + _channelLog.add(data); + } + + bool contains(String data) { + return _channelLog.contains(data); + } +}