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.
@@ -0,0 +1,54 @@
1
+ require 'date'
2
+
3
+ module Bluecap
4
+ class Event
5
+
6
+ attr_reader :id, :name, :timestamp
7
+
8
+ # Initialize an Event handler.
9
+ #
10
+ # data - A Hash containing event data:
11
+ # :id - The Integer identifer of the user that generated
12
+ # the event.
13
+ # :name - The String type of event.
14
+ # :timestamp - The Integer UNIX timestamp when the event was created,
15
+ # defaults to current time (optional).
16
+ #
17
+ # Examples
18
+ #
19
+ # Bluecap::Event.new(
20
+ # id: 3,
21
+ # name: 'Created Account',
22
+ # timestamp: 1341845456
23
+ # )
24
+ def initialize(data)
25
+ @id = data.fetch(:id)
26
+ @name = data.fetch(:name)
27
+ @timestamp = data.fetch(:timestamp, Time.now.to_i)
28
+ end
29
+
30
+ # Converts the object's timestamp to a %Y%m%d String.
31
+ #
32
+ # Returns the String date.
33
+ def date
34
+ Time.at(@timestamp).strftime('%Y%m%d')
35
+ end
36
+
37
+ # Proxy for an event key.
38
+ #
39
+ # Returns the String key.
40
+ def key
41
+ Bluecap::Keys.event(@name, date)
42
+ end
43
+
44
+ # Store the user's event in a bitset of all the events matching that name
45
+ # for the date.
46
+ #
47
+ # Returns nil.
48
+ def handle
49
+ Bluecap.redis.setbit(key, @id, 1)
50
+
51
+ nil
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,40 @@
1
+ module Bluecap
2
+ class Identify
3
+
4
+ attr_reader :name
5
+
6
+ # Initialize an Identify handler.
7
+ #
8
+ # name - A String to uniquely identify the user from the source system.
9
+ #
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+
14
+ # Returns an id to track a user in Bluecap, creating an id if one does not
15
+ # already exist.
16
+ #
17
+ # Bluecap::Identify.new('Andy').handle
18
+ # # => 1
19
+ #
20
+ # Bluecap::Identify.new('Evelyn').handle
21
+ # # => 2
22
+ #
23
+ # Returns the Integer id.
24
+ def handle
25
+ id = Bluecap.redis.hget('user.map', @name)
26
+ return id.to_i if id
27
+
28
+ id = Bluecap.redis.incr('user.ids')
29
+ if Bluecap.redis.hsetnx('user.map', @name, id)
30
+ return id
31
+ else
32
+ # Race condition, another process has set the id for this user.
33
+ # The unused id shouldn't cause inaccuracies in reporting since
34
+ # the position will be 0 in all bitsets across the system.
35
+ return redis.hget('user.map', @name).to_i
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ module Bluecap
2
+ class NullHandler
3
+
4
+ attr_reader :data
5
+
6
+ # Initialize a NullHandler.
7
+ def initialize(data)
8
+ @data = data
9
+ end
10
+
11
+ # Catch the handle message.
12
+ #
13
+ # Returns nothing.
14
+ def handle
15
+ Bluecap.log "NullHandler received message, contents: #{@data}"
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,75 @@
1
+ require 'date'
2
+ require 'securerandom'
3
+
4
+ module Bluecap
5
+ class Report
6
+
7
+ attr_reader :initial_event, :engagement_event, :attributes, :year_month
8
+
9
+ # Initialize a Report handler.
10
+ #
11
+ # data - A Hash containing options to scope the report by:
12
+ # :initial_event - The String event that cohorts shared.
13
+ # :engagement_event - The String event to track engagement by.
14
+ # :attributes - The Hash attributes of users (optional).
15
+ # :start_date - The String start date of the report.
16
+ # :end_date - The String end date of the report.
17
+ #
18
+ # Examples
19
+ #
20
+ # Bluecap::Report.new(
21
+ # initial_event: 'Created Account',
22
+ # engagement_event: 'Logged In',
23
+ # attributes: {
24
+ # country: 'Australia',
25
+ # gender: 'Female'
26
+ # },
27
+ # start_date: '20120401',
28
+ # end_date: '20120430'
29
+ # )
30
+ def initialize(data)
31
+ @initial_event = data.fetch(:initial_event)
32
+ @engagement_event = data.fetch(:engagement_event)
33
+ @attributes = data.fetch(:attributes, Hash.new)
34
+ @start_date = Date.parse(data.fetch(:start_date))
35
+ @end_date = Date.parse(data.fetch(:end_date))
36
+ end
37
+
38
+ def report_id
39
+ Bluecap.redis.incr('report.ids')
40
+ end
41
+
42
+ # Generates a report to track engagement of cohorts over time.
43
+ #
44
+ # Returns the String with report data in JSON format.
45
+ def handle
46
+ report = Hash.new
47
+ (@start_date...@end_date).each do |date|
48
+ cohort = Cohort.new(
49
+ :initial_event => @initial_event,
50
+ :attributes => @attributes,
51
+ :date => date,
52
+ :report_id => report_id
53
+ )
54
+
55
+ # The start date of the engagement is measured from the day after the
56
+ # initial event.
57
+ engagement = Engagement.new(
58
+ :cohort => cohort,
59
+ :engagement_event => engagement_event,
60
+ :start_date => date + 1,
61
+ :end_date => @end_date,
62
+ :report_id => report_id
63
+ )
64
+
65
+ cohort_results = Hash.new
66
+ cohort_results[:total] = cohort.total
67
+ cohort_results[:engagement] = engagement.measure
68
+ report[date.strftime('%Y%m%d')] = cohort_results
69
+ end
70
+
71
+ MultiJson.dump(report)
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,44 @@
1
+ module Bluecap
2
+ module Keys
3
+
4
+ # Returns a cleaned version of a string for use in a Redis key. Strips,
5
+ # downcases, then treats any remaining characters like word separators
6
+ # as periods.
7
+ #
8
+ # str - The String to be cleaned.
9
+ #
10
+ # Examples
11
+ #
12
+ # clean('Country')
13
+ # # => "country"
14
+ #
15
+ # clean('Logged In')
16
+ # # => "logged.in"
17
+ #
18
+ # Returns the new String.
19
+ def self.clean(str)
20
+ str.strip.downcase.gsub(/[^a-z0-9]/, '.')
21
+ end
22
+
23
+ # Returns a key used to store the events for a day.
24
+ #
25
+ # Examples
26
+ #
27
+ # Bluecap::Keys.event 'Sign Up', '20120710'
28
+ # # => "events:sign.up:20120710"
29
+ #
30
+ # Returns the String key.
31
+ def self.event(name, date)
32
+ "events:#{clean(name)}:#{date}"
33
+ end
34
+
35
+ def self.cohort(report_id, cohort_id)
36
+ "reports:cohort:#{report_id}:#{cohort_id}"
37
+ end
38
+
39
+ def self.engagement(report_id, cohort_id, date)
40
+ "reports:e:#{report_id}:#{cohort_id}:#{date}"
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ module Bluecap
2
+ class Message
3
+
4
+ # Initialize a Message with data.
5
+ #
6
+ # data - The String JSON message to parse.
7
+ def initialize(data)
8
+ @data = MultiJson.load(data, symbolize_keys: true)
9
+ end
10
+
11
+ # The signature of the handler that should respond to the message.
12
+ #
13
+ # Examples
14
+ #
15
+ # message = Message.new('{"identify": "Evelyn"}')
16
+ # message.recipient
17
+ # # => :identify
18
+ #
19
+ # Returns the Symbol recipient of the messsage.
20
+ def recipient
21
+ @data.first[0].to_sym if @data.first
22
+ end
23
+
24
+ # The contents of the message.
25
+ #
26
+ # Examples
27
+ #
28
+ # message = Message.new('{"identify": "Evelyn"}')
29
+ # message.contents
30
+ # # => "Evelyn"
31
+ #
32
+ # Returns the contents of the message; the data type is different based on
33
+ # the contents of the message.
34
+ def contents
35
+ @data.first[1]
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,72 @@
1
+ require 'socket'
2
+
3
+ module Bluecap
4
+ module Server
5
+
6
+ # Returns a Hash of handlers, creating a new empty hash if none have
7
+ # previously been set.
8
+ def self.handlers
9
+ @handlers ||= {
10
+ attributes: Bluecap::Attributes,
11
+ event: Bluecap::Event,
12
+ identify: Bluecap::Identify,
13
+ report: Bluecap::Report
14
+ }
15
+ end
16
+
17
+ # Assign handlers to respond to new data.
18
+ #
19
+ # handlers - A Hash of lookup keys to handler classes.
20
+ #
21
+ # Examples
22
+ #
23
+ # handlers = {
24
+ # event: Bluecap::Event,
25
+ # identify: Bluecap::Identify
26
+ # }
27
+ #
28
+ # Returns nothing.
29
+ def self.handlers=(handlers)
30
+ @handlers = handlers
31
+ end
32
+
33
+ def process(message)
34
+ klass = Bluecap::Server.handlers.fetch(message.recipient, Bluecap::NullHandler)
35
+ port, ip_address = Socket.unpack_sockaddr_in(get_peername)
36
+ Bluecap.log "Message received for #{message.recipient} handler from #{ip_address}:#{port}"
37
+ handler = klass.new(message.contents)
38
+ handler.handle
39
+ end
40
+
41
+ # Process a message received from a client, sending a response if
42
+ # necessary.
43
+ #
44
+ # data - The String containing JSON received from the client.
45
+ #
46
+ # Examples
47
+ #
48
+ # receive_data('{"identify": "Andy"}')
49
+ #
50
+ # Returns nothing.
51
+ def receive_data(data)
52
+ begin
53
+ message = Bluecap::Message.new(data)
54
+ response = process(message)
55
+ send_data(response) if response
56
+ rescue Exception => e
57
+ Bluecap.log e
58
+ end
59
+ end
60
+
61
+ # Starts a TCP server running on the EventMachine loop.
62
+ #
63
+ # Returns nothing.
64
+ def self.run
65
+ EventMachine::run do
66
+ EventMachine::start_server(Bluecap.host, Bluecap.port, Bluecap::Server)
67
+ Bluecap.log "Server started on #{Bluecap.host}:#{Bluecap.port}"
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ require 'helper'
2
+
3
+ describe Bluecap::Attributes do
4
+
5
+ before do
6
+ Bluecap.redis.flushall
7
+ end
8
+
9
+ subject do
10
+ Bluecap::Attributes.new :id => 3,
11
+ :attributes => {:country => 'Australia', :gender => 'Female'}
12
+ end
13
+
14
+ it 'should create attributes key' do
15
+ subject.key('gender', 'female').should == 'attributes:gender:female'
16
+ end
17
+
18
+ it 'should create attributes key using cleaned name and date' do
19
+ subject.key(' COUNTRY', 'Australia').should == 'attributes:country:australia'
20
+ end
21
+
22
+ it 'should create keys for multiple attributes' do
23
+ subject.keys.should =~ ['attributes:country:australia',
24
+ 'attributes:gender:female']
25
+ end
26
+
27
+ it 'should track attributes for user by setting corresponding bits to 1' do
28
+ data = {
29
+ attributes: {
30
+ gender: 'female',
31
+ country: 'australia',
32
+ },
33
+ id: 5
34
+ }
35
+
36
+ subject.handle
37
+
38
+ # Check that bits have changed.
39
+ Bluecap.redis.getbit('attributes:gender:female', subject.id).should == 1
40
+ Bluecap.redis.getbit('attributes:country:australia', subject.id).should == 1
41
+ end
42
+
43
+ end
@@ -0,0 +1,77 @@
1
+ require 'helper'
2
+ require 'date'
3
+
4
+ describe Bluecap::Cohort do
5
+
6
+ before do
7
+ Bluecap.redis.flushall
8
+
9
+ @date = Date.parse('20120701')
10
+ @initial_event = 'Sign Up'
11
+ @attributes = {:country => 'Australia', :gender => 'Female'}
12
+ @users = {
13
+ :evelyn => Bluecap::Identify.new('Evelyn').handle,
14
+ :charlotte => Bluecap::Identify.new('Charlotte').handle
15
+ }
16
+ @users.values.each do |id|
17
+ event = Bluecap::Event.new :id => id,
18
+ :name => @initial_event,
19
+ :timestamp => @date.to_time.to_i
20
+ event.handle
21
+
22
+ attribute = Bluecap::Attributes.new :id => id,
23
+ :attributes => @attributes
24
+ attribute.handle
25
+ end
26
+ end
27
+
28
+ it 'should find cohort total for date' do
29
+ cohort = Bluecap::Cohort.new :initial_event => @initial_event,
30
+ :date => @date,
31
+ :report_id => 1
32
+
33
+ cohort.total.should == 2
34
+ end
35
+
36
+ it 'should not include in cohort if initial event did not occur on date' do
37
+ sarah = Bluecap::Identify.new('Sarah').handle
38
+ event = Bluecap::Event.new :id => sarah,
39
+ :name => @initial_event,
40
+ :timestamp => (@date + 1).to_time.to_i
41
+ event.handle
42
+
43
+ cohort = Bluecap::Cohort.new :initial_event => @initial_event,
44
+ :date => @date,
45
+ :report_id => 1
46
+ cohort.total.should == 2
47
+ end
48
+
49
+ it 'should allow cohorts to be constructed with attributes of users' do
50
+ cohort = Bluecap::Cohort.new :initial_event => @initial_event,
51
+ :date => @date,
52
+ :attributes => @attributes,
53
+ :report_id => 1
54
+
55
+ cohort.total.should == 2
56
+ end
57
+
58
+ it 'shold not include in cohort if attributes are not matched' do
59
+ sarah = Bluecap::Identify.new('Sarah').handle
60
+ event = Bluecap::Event.new :id => sarah,
61
+ :name => @initial_event,
62
+ :timestamp => @date.to_time.to_i
63
+ event.handle
64
+ attributes = @attributes.clone
65
+ attributes[:country] = 'New Zealand'
66
+ attribute = Bluecap::Attributes.new :id => sarah,
67
+ :attributes => attributes
68
+ attribute.handle
69
+
70
+ cohort = Bluecap::Cohort.new :initial_event => @initial_event,
71
+ :date => @date,
72
+ :attributes => @attributes,
73
+ :report_id => 1
74
+ cohort.total.should == 2
75
+ end
76
+
77
+ end