bluecap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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