bluecap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) Christopher Wright
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Bluecap
2
+
3
+ Bluecap is a Redis-backed system for measuring user engagement over time. It
4
+ tracks events passed in from external systems, such as when a user creates an
5
+ account, logs in or performs some other key action. Bluecap can also track
6
+ properties of users like their gender or location.
7
+
8
+ Systems can then query Bluecap for data on how a group of users engages with
9
+ a product over time, e.g.: for users that created an account a month ago, what
10
+ percentage of these users have logged into their account since then? How about
11
+ the same report, but only for users that are living in Australia?
12
+
13
+ ## Installation
14
+
15
+ Bluecap requires Ruby 1.9.2 or greater, and Redis 2.5.11 or greater for access
16
+ to bitcount and bitop commands. Install the gem by running:
17
+
18
+ gem install bluecap
19
+
20
+ ## Usage
21
+
22
+ Run the server:
23
+
24
+ bluecap
25
+
26
+ By default this will run Bluecap on `0.0.0.0:6088` with Redis on
27
+ `localhost:6379`. Run `bluecap -h` for options.
28
+
29
+ The server responds to JSON messages received over TCP, the root level key in
30
+ the JSON indicates the type of message being sent. Valid message types are:
31
+ `identify`, `event`, `attributes` and `report`.
32
+
33
+ ### Identify a user
34
+
35
+ Send Bluecap a string that uniquely identifies the user in your own system:
36
+
37
+ {"identify": "Charlotte"}
38
+ # => 1
39
+
40
+ {"identify": {"brian@example.com"}
41
+ # => 2
42
+
43
+ The server responds with an integer id. When tracking events for the user, the
44
+ id is passed along with event data.
45
+
46
+ ### Tracking events
47
+
48
+ Send the id, name of the event to track, and optionally a UNIX timestamp:
49
+
50
+ {"event": {"id": 1, "name": "Created Account", "timestamp": "1341845456"}}
51
+
52
+ ### Setting user attributes
53
+
54
+ Send the id and a hash of attributes to set:
55
+
56
+ {
57
+ "attributes": {
58
+ "id": 1,
59
+ "attributes": {
60
+ "gender": "Female",
61
+ "country": "Australia"
62
+ }
63
+ }
64
+
65
+ ### Generating a report
66
+
67
+ The reports generate retention for users over a date range. The report requires
68
+ an initial event name to identify cohorts, an event name that measures
69
+ engagement, a date range and optional attributes to limit the users contained
70
+ in the report:
71
+
72
+ {
73
+ "report": {
74
+ "initial_event": "Created Account",
75
+ "engagement_event": "Logged In",
76
+ "attributes": {
77
+ "country": "Australia",
78
+ "gender": "Female"
79
+ },
80
+ "start_date": "20120701",
81
+ "end_date": "20120731",
82
+ }
83
+ }
84
+
85
+ A report is returned in JSON format showing the engagement over time for each
86
+ cohort, e.g.:
87
+
88
+ {
89
+ "20120701": {
90
+ "total": 3751,
91
+ "engagement": {
92
+ "20120702": 53.97,
93
+ "20120703": 43.22,
94
+ ...
95
+ },
96
+ },
97
+ "20120702": {
98
+ "total": 4099,
99
+ "engagement": {
100
+ "20120703": 55.81,
101
+ "20120704": 46.73,
102
+ ...
103
+ },
104
+ }
105
+ }
106
+
107
+ ## Development
108
+
109
+ Clone the repository and run the tests:
110
+
111
+ git clone git://github.com/christopherwright/bluecap.git
112
+ cd bluecap
113
+ rspec
114
+
115
+ The tests will attempt to create a Redis instance by running `redis-server`.
data/bin/bluecap ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+ require 'optparse'
7
+ require 'bluecap'
8
+
9
+ parser = OptionParser.new do |opts|
10
+ opts.banner = 'Usage: bluecap [options]'
11
+
12
+ opts.separator ''
13
+ opts.separator 'Options:'
14
+
15
+ opts.on('-r', '--redis [HOST:PORT]', 'Redis connection string') do |redis|
16
+ Bluecap.redis = redis
17
+ end
18
+
19
+ opts.on('-b', '--bind [HOST]', 'Hostname to bind to') do |host|
20
+ Bluecap.host = host
21
+ end
22
+
23
+ opts.on('-p', '--port [PORT]', 'Port to listen on') do |port|
24
+ Bluecap.port = port
25
+ end
26
+
27
+ opts.on('-h', '--help', 'Show this message') do
28
+ puts opts
29
+ exit
30
+ end
31
+ end
32
+
33
+ parser.parse!
34
+ Bluecap.log 'Starting server'
35
+ server = Bluecap::Server.run
data/lib/bluecap.rb ADDED
@@ -0,0 +1,78 @@
1
+ require 'eventmachine'
2
+ require 'redis'
3
+ require 'multi_json'
4
+
5
+ require 'bluecap/keys'
6
+ require 'bluecap/message'
7
+ require 'bluecap/cohort'
8
+ require 'bluecap/engagement'
9
+ require 'bluecap/server'
10
+ require 'bluecap/handlers/attributes'
11
+ require 'bluecap/handlers/event'
12
+ require 'bluecap/handlers/identify'
13
+ require 'bluecap/handlers/null_handler'
14
+ require 'bluecap/handlers/report'
15
+
16
+ module Bluecap
17
+
18
+ extend self
19
+
20
+ # Connect to Redis and store the resulting client.
21
+ #
22
+ # server - A String of conncetion details in host:port format.
23
+ #
24
+ # Examples
25
+ #
26
+ # redis = 'localhost:6379'
27
+ #
28
+ # Returns nothing.
29
+ def redis=(server)
30
+ host, port, database = server.split(':')
31
+ @redis = Redis.new(host: host, port: port, database: database)
32
+ end
33
+
34
+ # Returns the Redis client, creating a new client if one does not already
35
+ # exist.
36
+ def redis
37
+ return @redis if @redis
38
+ self.redis = 'localhost:6379'
39
+ self.redis
40
+ end
41
+
42
+ # Set the host to bind to.
43
+ #
44
+ # Returns nothing.
45
+ def host=(host)
46
+ @host = host
47
+ end
48
+
49
+ # Returns the String host to bind to.
50
+ def host
51
+ return @host if @host
52
+ self.host = '0.0.0.0'
53
+ self.host
54
+ end
55
+
56
+ # Set the port to bind to.
57
+ #
58
+ # Returns nothing.
59
+ def port=(port)
60
+ @port = port
61
+ end
62
+
63
+ # Returns the Integer port to bind to.
64
+ def port
65
+ return @port if @port
66
+ self.port = 6088
67
+ self.port
68
+ end
69
+
70
+ # Log a message to STDOUT.
71
+ #
72
+ # Returns nothing.
73
+ def log(message)
74
+ time = Time.now.strftime('%Y-%m-%d %H:%M:%S')
75
+ puts "#{time} - #{message}"
76
+ end
77
+
78
+ end
@@ -0,0 +1,92 @@
1
+ module Bluecap
2
+ class Cohort
3
+
4
+ attr_reader :initial_event, :attributes, :date, :report_id
5
+
6
+ # Initialize a Cohort.
7
+ #
8
+ # options - A Hash containing options to define the cohort:
9
+ # :initial_event - The String event that members of the cohort
10
+ # shared.
11
+ # :attributes - The Hash attributes of cohort members.
12
+ # :date - The Date the initial event occurred on.
13
+ # :report_id - The Integer identifier of the report this
14
+ # cohort is being used with.
15
+ #
16
+ # Examples
17
+ #
18
+ # Bluecap::Cohort.new :initial_event => 'Created Account',
19
+ # :attributes => {:country => 'Australia', :gender => 'Female'},
20
+ # :date => Date.parse('20120701'),
21
+ # :report_id => 1
22
+ def initialize(options)
23
+ @initial_event = options.fetch(:initial_event)
24
+ @attributes = options.fetch(:attributes, Hash.new)
25
+ @date = options.fetch(:date)
26
+ @report_id = options.fetch(:report_id)
27
+ end
28
+
29
+ # Convert the date of the cohort to a string.
30
+ #
31
+ # Returns the String date.
32
+ def date_str
33
+ @date.strftime("%Y%m%d")
34
+ end
35
+
36
+ # An identifier for the cohort. The date of the initial event is currently
37
+ # unique for each cohort in a report, but that might change in the future if
38
+ # cohorts are not limited to the initial event occurring on a single day.
39
+ #
40
+ # Returns the String id.
41
+ def id
42
+ date_str
43
+ end
44
+
45
+ # A Redis key containing a bitmask of all the properties for this cohort.
46
+ # The method is memoized since the bitmask is cached in Redis for an hour.
47
+ #
48
+ # Returns the String key name in Redis.
49
+ def key
50
+ return @key if @key
51
+
52
+ key_name = Bluecap::Keys.cohort(@report_id, id)
53
+ Bluecap.redis.multi do
54
+ Bluecap.redis.bitop('and', key_name, keys)
55
+ Bluecap.redis.expire(key_name, 3600)
56
+ end
57
+ @key = key_name
58
+ end
59
+
60
+ # Generate an array of Redis keys where the attributes of users in the
61
+ # cohort can be found.
62
+ #
63
+ # Returns the Array of String keys.
64
+ def keys_for_attributes
65
+ attributes = Bluecap::Attributes.new :id => 0, :attributes => @attributes
66
+ attributes.keys
67
+ end
68
+
69
+ # Generate an array of Redis keys for use in creating a bitmask of the
70
+ # cohort. This consists of the bitset of the users with the initial event
71
+ # along with all the attributes of the cohort.
72
+ #
73
+ # Returns the Array of String keys.
74
+ def keys
75
+ keys_for_initial_event = Array.new
76
+ keys_for_initial_event.push(Bluecap::Keys.event(@initial_event, date_str))
77
+ keys_for_initial_event += keys_for_attributes
78
+ end
79
+
80
+ # Calculate the total number of users in this cohort by doing a bitcount
81
+ # on the cohort bitmask. The method is memoized as the calculation is
82
+ # expensive.
83
+ #
84
+ # Returns the Integer total.
85
+ def total
86
+ return @total if @total
87
+
88
+ @total = Bluecap.redis.bitcount(key) || 0
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,79 @@
1
+ module Bluecap
2
+ class Engagement
3
+
4
+ # Initialize an Engagement class to measure Cohort retention.
5
+ #
6
+ # options - A hash containing options that define the engagement.
7
+ # :cohort - The Cohort to measure engagement for.
8
+ # :engagement_event - The String event that defines engagement.
9
+ # :start_date - The Date starting period to measure
10
+ # engagement for.
11
+ # :end_date - The Date ending period to measure
12
+ # engagement for.
13
+ # :report_id - The Integer identifier of the report this
14
+ # cohort is being used with.
15
+ #
16
+ # Examples
17
+ #
18
+ # engagement = Bluecap::Engagement.new :cohort => cohort,
19
+ # :engagement_event => 'Logged In',
20
+ # :start_date => Date.parse('20120702'),
21
+ # :end_date => Date.parse('20120707'),
22
+ # :report_id => 1
23
+ def initialize(options)
24
+ @cohort = options.fetch(:cohort)
25
+ @engagement_event = options.fetch(:engagement_event)
26
+ @start_date = options.fetch(:start_date)
27
+ @end_date = options.fetch(:end_date)
28
+ @report_id = options.fetch(:report_id)
29
+ end
30
+
31
+ # A key name in Redis where the bitcount operation for engagement on a
32
+ # date should be stored.
33
+ #
34
+ # Returns the String key name.
35
+ def key(date)
36
+ Bluecap::Keys.engagement(@report_id, @cohort.id, date.strftime('%Y%m%d'))
37
+ end
38
+
39
+ # The keys that comprise the bitmask for the engagement on a date.
40
+ #
41
+ # Returns the Array of String keys.
42
+ def keys(date)
43
+ [@cohort.key, Bluecap::Keys.event(@engagement_event, date.strftime('%Y%m%d'))]
44
+ end
45
+
46
+ # Measure the engagement of a cohort over a given period.
47
+ #
48
+ # Examples
49
+ #
50
+ # measure
51
+ # # => {"20120702" => 100.0, "20120703" => 50.0, ...}
52
+ #
53
+ # Returns a Hash with engagement percentages for each day in the period.
54
+ def measure
55
+ results = Hash.new
56
+ (@start_date..@end_date).each do |date|
57
+ key_name = key(date)
58
+ Bluecap.redis.multi do
59
+ Bluecap.redis.bitop('and', key_name, keys(date))
60
+ Bluecap.redis.expire(key_name, 3600)
61
+ end
62
+ sum_for_day = Bluecap.redis.bitcount(key_name) || 0
63
+
64
+ if not @cohort.total.zero?
65
+ engagement_for_day = (sum_for_day.to_f / @cohort.total)
66
+ else
67
+ engagement_for_day = 0.to_f
68
+ end
69
+ engagement_for_day *= 100.0
70
+ engagement_for_day.round(2)
71
+
72
+ results[date.strftime('%Y%m%d')] = engagement_for_day
73
+ end
74
+
75
+ results
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,68 @@
1
+ module Bluecap
2
+ class Attributes
3
+
4
+ attr_reader :id, :attributes
5
+
6
+ # Initialize an Attributes handler.
7
+ #
8
+ # data - A Hash containing attributes to set for a user:
9
+ # :attributes - The hash key/value pairs to be set.
10
+ # :id - The id of the user to set the attributes for.
11
+ #
12
+ # Examples
13
+ #
14
+ # Bluecap::Attributes.new(
15
+ # id: 3,
16
+ # attributes: {
17
+ # gender: 'Female',
18
+ # country: 'Australia'
19
+ # }
20
+ # )
21
+ def initialize(data)
22
+ @id = data.fetch(:id)
23
+ @attributes = data.fetch(:attributes)
24
+ end
25
+
26
+ # Returns keys for each of the attributes.
27
+ #
28
+ # Examples
29
+ #
30
+ # attributes = Bluecap::Attributes.new(
31
+ # id:3,
32
+ # attributes: {
33
+ # gender: 'Female',
34
+ # country: 'Australia'
35
+ # }
36
+ # attributes.keys
37
+ # # => ["attributes:gender:female", "attributes:country:australia"]
38
+ #
39
+ # Returns the Array of keys.
40
+ def keys
41
+ @attributes.map { |k, v| key(k.to_s, v) }
42
+ end
43
+
44
+ # Returns a cleaned key for an attribute and value.
45
+ #
46
+ # Examples
47
+ #
48
+ # key('gender', 'female')
49
+ # # => "attributes:gender:female"
50
+ def key(attribute, value)
51
+ "attributes:#{Bluecap::Keys.clean(attribute)}:#{Bluecap::Keys.clean(value)}"
52
+ end
53
+
54
+ # Store attributes for a user. Each attribute/value has its own bitset
55
+ # so this is best used with data that has a limited number of values
56
+ # (e.g.: gender, country).
57
+ #
58
+ # Returns nil.
59
+ def handle
60
+ Bluecap.redis.multi do
61
+ keys.each { |k| Bluecap.redis.setbit(k, @id, 1) }
62
+ end
63
+
64
+ nil
65
+ end
66
+
67
+ end
68
+ end