greenfinch-ruby 0.1.0

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,184 @@
1
+ require 'greenfinch-ruby/events.rb'
2
+ require 'greenfinch-ruby/people.rb'
3
+ require 'greenfinch-ruby/groups.rb'
4
+
5
+ module Greenfinch
6
+ # Use Greenfinch::Tracker to track events and profile updates in your application.
7
+ # To track an event, call
8
+ #
9
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN)
10
+ # Greenfinch::Tracker.track(a_distinct_id, an_event_name, {properties})
11
+ #
12
+ # To send people updates, call
13
+ #
14
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN)
15
+ # tracker.people.set(a_distinct_id, {properties})
16
+ #
17
+ # To send groups updates, call
18
+ #
19
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN)
20
+ # tracker.groups.set(group_key, group_id, {properties})
21
+ #
22
+ # You can find your project token in the settings dialog for your
23
+ # project, inside of the Greenfinch web application.
24
+ #
25
+ # Greenfinch::Tracker is a subclass of Greenfinch::Events, and exposes
26
+ # an instance of Greenfinch::People as Tracker#people
27
+ # and an instance of Greenfinch::Groups as Tracker#groups
28
+ class Tracker < Events
29
+ # An instance of Greenfinch::People. Use this to
30
+ # send profile updates
31
+ attr_reader :people
32
+
33
+ # An instance of Greenfinch::Groups. Use this to send groups updates
34
+ attr_reader :groups
35
+
36
+ # Takes your Greenfinch project token, as a string.
37
+ #
38
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN)
39
+ #
40
+ # By default, the tracker will send an message to Greenfinch
41
+ # synchronously with each call, using an instance of Greenfinch::Consumer.
42
+ #
43
+ # You can also provide a block to the constructor
44
+ # to specify particular consumer behaviors (for
45
+ # example, if you wanted to write your messages to
46
+ # a queue instead of sending them directly to Greenfinch)
47
+ #
48
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN) do |type, message|
49
+ # @kestrel.set(MY_GREENFINCH_QUEUE, [type,message].to_json)
50
+ # end
51
+ #
52
+ # If a block is provided, it is passed a type (one of :event or :profile_update)
53
+ # and a string message. This same format is accepted by Greenfinch::Consumer#send!
54
+ # and Greenfinch::BufferedConsumer#send!
55
+ def initialize(token, service_name, debug, error_handler=nil, &block)
56
+ super(token, service_name, debug, error_handler, &block)
57
+ @token = token
58
+ @service_name = service_name
59
+ @debug = debug
60
+ end
61
+
62
+ # A call to #track is a report that an event has occurred. #track
63
+ # takes a distinct_id representing the source of that event (for
64
+ # example, a user id), an event name describing the event, and a
65
+ # set of properties describing that event. Properties are provided
66
+ # as a Hash with string keys and strings, numbers or booleans as
67
+ # values.
68
+ #
69
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN)
70
+ #
71
+ # # Track that user "12345"'s credit card was declined
72
+ # tracker.track("12345", "Credit Card Declined")
73
+ #
74
+ # # Properties describe the circumstances of the event,
75
+ # # or aspects of the source or user associated with the event
76
+ # tracker.track("12345", "Welcome Email Sent", {
77
+ # 'Email Template' => 'Pretty Pink Welcome',
78
+ # 'User Sign-up Cohort' => 'July 2013'
79
+ # })
80
+ def track(distinct_id, event, properties={}, ip=nil)
81
+ # This is here strictly to allow rdoc to include the relevant
82
+ # documentation
83
+ super
84
+ end
85
+
86
+ # A call to #import is to import an event occurred in the past. #import
87
+ # takes a distinct_id representing the source of that event (for
88
+ # example, a user id), an event name describing the event, and a
89
+ # set of properties describing that event. Properties are provided
90
+ # as a Hash with string keys and strings, numbers or booleans as
91
+ # values.
92
+ #
93
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN)
94
+ #
95
+ # # Import event that user "12345"'s credit card was declined
96
+ # tracker.import("API_KEY", "12345", "Credit Card Declined", {
97
+ # 'time' => 1310111365
98
+ # })
99
+ #
100
+ # # Properties describe the circumstances of the event,
101
+ # # or aspects of the source or user associated with the event
102
+ # tracker.import("API_KEY", "12345", "Welcome Email Sent", {
103
+ # 'Email Template' => 'Pretty Pink Welcome',
104
+ # 'User Sign-up Cohort' => 'July 2013',
105
+ # 'time' => 1310111365
106
+ # })
107
+ # def import(api_key, distinct_id, event, properties={}, ip=nil)
108
+ # # This is here strictly to allow rdoc to include the relevant
109
+ # # documentation
110
+ # super
111
+ # end
112
+
113
+ # Creates a distinct_id alias. \Events and updates with an alias
114
+ # will be considered by greenfinch to have the same source, and
115
+ # refer to the same profile.
116
+ #
117
+ # Multiple aliases can map to the same real_id, once a real_id is
118
+ # used to track events or send updates, it should never be used as
119
+ # an alias itself.
120
+ #
121
+ # Alias requests are always sent synchronously, directly to
122
+ # the \Greenfinch service, regardless of how the tracker is configured.
123
+ # def alias(alias_id, real_id, events_endpoint=nil)
124
+ # consumer = Greenfinch::Consumer.new(events_endpoint)
125
+ # data = {
126
+ # 'event' => '$create_alias',
127
+ # 'properties' => {
128
+ # 'distinct_id' => real_id,
129
+ # 'alias' => alias_id,
130
+ # 'token' => @token,
131
+ # }
132
+ # }
133
+ #
134
+ # message = {'data' => data}
135
+ #
136
+ # ret = true
137
+ # begin
138
+ # consumer.send!(:event, message.to_json)
139
+ # rescue GreenfinchError => e
140
+ # @error_handler.handle(e)
141
+ # ret = false
142
+ # end
143
+ #
144
+ # ret
145
+ # end
146
+
147
+ # A call to #generate_tracking_url will return a formatted url for
148
+ # pixel based tracking. #generate_tracking_url takes a distinct_id
149
+ # representing the source of that event (for example, a user id),
150
+ # an event name describing the event, and a set of properties describing
151
+ # that event. Properties are provided as a Hash with string keys and
152
+ # strings, numbers or booleans as values. For more information, please see:
153
+ # https://greenfinch.com/docs/api-documentation/pixel-based-event-tracking
154
+ #
155
+ # tracker = Greenfinch::Tracker.new(YOUR_GREENFINCH_TOKEN)
156
+ #
157
+ # # generate pixel tracking url in order to track that user
158
+ # # "12345"'s credit card was declined
159
+ # url = tracker.generate_tracking_url("12345", "Credit Card Declined", {
160
+ # 'time' => 1310111365
161
+ # })
162
+ #
163
+ # url == 'https://api.greenfinch.com/track/?data=[BASE_64_JSON_EVENT]&ip=1&img=1'
164
+ # def generate_tracking_url(distinct_id, event, properties={}, endpoint=nil)
165
+ # properties = {
166
+ # 'distinct_id' => distinct_id,
167
+ # 'token' => @token,
168
+ # 'time' => Time.now.to_i,
169
+ # 'mp_lib' => 'ruby',
170
+ # '$lib_version' => Greenfinch::VERSION,
171
+ # }.merge(properties)
172
+ #
173
+ # raw_data = {
174
+ # 'event' => event,
175
+ # 'properties' => properties,
176
+ # }
177
+ #
178
+ # endpoint = endpoint || 'https://api.greenfinch.com/track/'
179
+ # data = Base64.urlsafe_encode64(raw_data.to_json)
180
+ #
181
+ # "#{endpoint}?data=#{data}&ip=1&img=1"
182
+ # end
183
+ end
184
+ end
@@ -0,0 +1,3 @@
1
+ module Greenfinch
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,208 @@
1
+ require 'base64'
2
+ require 'spec_helper'
3
+ require 'webmock'
4
+
5
+ require 'mixpanel-ruby/consumer'
6
+
7
+ describe Mixpanel::Consumer do
8
+ before { WebMock.reset! }
9
+
10
+ shared_examples_for 'consumer' do
11
+ it 'should send a request to api.mixpanel.com/track on events' do
12
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'})
13
+ subject.send!(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json)
14
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track').
15
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' })
16
+ end
17
+
18
+ it 'should send a request to api.mixpanel.com/people on profile updates' do
19
+ stub_request(:any, 'https://api.mixpanel.com/engage').to_return({:body => '{"status": 1, "error": null}'})
20
+ subject.send!(:profile_update, {'data' => 'TEST EVENT MESSAGE'}.to_json)
21
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/engage').
22
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' })
23
+ end
24
+
25
+ it 'should send a request to api.mixpanel.com/groups on groups updates' do
26
+ stub_request(:any, 'https://api.mixpanel.com/groups').to_return({:body => '{"status": 1, "error": null}'})
27
+ subject.send!(:group_update, {'data' => 'TEST EVENT MESSAGE'}.to_json)
28
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/groups').
29
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' })
30
+ end
31
+
32
+ it 'should send a request to api.mixpanel.com/import on event imports' do
33
+ stub_request(:any, 'https://api.mixpanel.com/import').to_return({:body => '{"status": 1, "error": null}'})
34
+ subject.send!(:import, {'data' => 'TEST EVENT MESSAGE', 'api_key' => 'API_KEY','verbose' => '1' }.to_json)
35
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/import').
36
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'api_key' => 'API_KEY', 'verbose' => '1' })
37
+ end
38
+
39
+ it 'should encode long messages without newlines' do
40
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'})
41
+ subject.send!(:event, {'data' => 'BASE64-ENCODED VERSION OF BIN. THIS METHOD COMPLIES WITH RFC 2045. LINE FEEDS ARE ADDED TO EVERY 60 ENCODED CHARACTORS. IN RUBY 1.8 WE NEED TO JUST CALL ENCODE64 AND REMOVE THE LINE FEEDS, IN RUBY 1.9 WE CALL STRIC_ENCODED64 METHOD INSTEAD'}.to_json)
42
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track').
43
+ with(:body => {'data' => 'IkJBU0U2NC1FTkNPREVEIFZFUlNJT04gT0YgQklOLiBUSElTIE1FVEhPRCBDT01QTElFUyBXSVRIIFJGQyAyMDQ1LiBMSU5FIEZFRURTIEFSRSBBRERFRCBUTyBFVkVSWSA2MCBFTkNPREVEIENIQVJBQ1RPUlMuIElOIFJVQlkgMS44IFdFIE5FRUQgVE8gSlVTVCBDQUxMIEVOQ09ERTY0IEFORCBSRU1PVkUgVEhFIExJTkUgRkVFRFMsIElOIFJVQlkgMS45IFdFIENBTEwgU1RSSUNfRU5DT0RFRDY0IE1FVEhPRCBJTlNURUFEIg==', 'verbose' => '1'})
44
+ end
45
+
46
+ it 'should provide thorough information in case mixpanel fails' do
47
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:status => 401, :body => "nutcakes"})
48
+ expect { subject.send!(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json) }.to raise_exception('Could not write to Mixpanel, server responded with 401 returning: \'nutcakes\'')
49
+ end
50
+
51
+ it 'should still respond to send' do
52
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'})
53
+ subject.send(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json)
54
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track').
55
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' })
56
+ end
57
+
58
+ it 'should raise server error if response body is empty' do
59
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => ''})
60
+ expect { subject.send!(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json) }.to raise_exception(Mixpanel::ServerError, /Could not interpret Mixpanel server response: ''/)
61
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track').
62
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' })
63
+ end
64
+
65
+ it 'should raise server error when verbose is disabled', :skip => true do
66
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '0'})
67
+ expect { subject.send!(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json) }.to raise_exception(Mixpanel::ServerError, /Could not interpret Mixpanel server response: '0'/)
68
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track').
69
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' })
70
+ end
71
+ end
72
+
73
+ context 'raw consumer' do
74
+ it_behaves_like 'consumer'
75
+ end
76
+
77
+ context 'custom request consumer' do
78
+ subject do
79
+ ret = Mixpanel::Consumer.new
80
+ class << ret
81
+ attr_reader :called
82
+ def request(*args)
83
+ @called = true
84
+ super(*args)
85
+ end
86
+ end
87
+
88
+ ret
89
+ end
90
+
91
+ after(:each) do
92
+ expect(subject.called).to be_truthy
93
+ end
94
+
95
+ it_behaves_like 'consumer'
96
+ end
97
+
98
+ end
99
+
100
+ describe Mixpanel::BufferedConsumer do
101
+ let(:max_length) { 10 }
102
+ before { WebMock.reset! }
103
+
104
+ context 'Default BufferedConsumer' do
105
+ subject { Mixpanel::BufferedConsumer.new(nil, nil, nil, max_length) }
106
+
107
+ it 'should not send a request for a single message until flush is called' do
108
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'})
109
+ subject.send!(:event, {'data' => 'TEST EVENT 1'}.to_json)
110
+ expect(WebMock).to have_not_requested(:post, 'https://api.mixpanel.com/track')
111
+
112
+ subject.flush()
113
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track').
114
+ with(:body => {'data' => 'WyJURVNUIEVWRU5UIDEiXQ==', 'verbose' => '1' })
115
+ end
116
+
117
+ it 'should still respond to send' do
118
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'})
119
+ subject.send(:event, {'data' => 'TEST EVENT 1'}.to_json)
120
+ expect(WebMock).to have_not_requested(:post, 'https://api.mixpanel.com/track')
121
+ end
122
+
123
+ it 'should send one message when max_length events are tracked' do
124
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'})
125
+
126
+ max_length.times do |i|
127
+ subject.send!(:event, {'data' => "x #{i}"}.to_json)
128
+ end
129
+
130
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track').
131
+ with(:body => {'data' => 'WyJ4IDAiLCJ4IDEiLCJ4IDIiLCJ4IDMiLCJ4IDQiLCJ4IDUiLCJ4IDYiLCJ4IDciLCJ4IDgiLCJ4IDkiXQ==', 'verbose' => '1' })
132
+ end
133
+
134
+ it 'should send one message per api key on import' do
135
+ stub_request(:any, 'https://api.mixpanel.com/import').to_return({:body => '{"status": 1, "error": null}'})
136
+ subject.send!(:import, {'data' => 'TEST EVENT 1', 'api_key' => 'KEY 1'}.to_json)
137
+ subject.send!(:import, {'data' => 'TEST EVENT 1', 'api_key' => 'KEY 2'}.to_json)
138
+ subject.send!(:import, {'data' => 'TEST EVENT 2', 'api_key' => 'KEY 1'}.to_json)
139
+ subject.send!(:import, {'data' => 'TEST EVENT 2', 'api_key' => 'KEY 2'}.to_json)
140
+ subject.flush
141
+
142
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/import').
143
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgMSI=', 'api_key' => 'KEY 1', 'verbose' => '1' })
144
+
145
+ expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/import').
146
+ with(:body => {'data' => 'IlRFU1QgRVZFTlQgMSI=', 'api_key' => 'KEY 2', 'verbose' => '1' })
147
+ end
148
+ end
149
+
150
+ context 'BufferedConsumer with block' do
151
+ let(:messages_seen) { [] }
152
+ subject do
153
+ Mixpanel::BufferedConsumer.new(nil, nil, nil, 3) do |type, message|
154
+ messages_seen << [type, message]
155
+ end
156
+ end
157
+
158
+ it 'should call block instead of making default requests on flush' do
159
+ 3.times do |i|
160
+ subject.send!(:event, {'data' => "x #{i}"}.to_json)
161
+ end
162
+
163
+ expect(messages_seen).to match_array(
164
+ [[:event, "{\"data\":[\"x 0\",\"x 1\",\"x 2\"]}"]]
165
+ )
166
+ end
167
+
168
+ end
169
+
170
+ context 'with failing requests' do
171
+ let(:sent_messages) { [] }
172
+ let(:submission_queue) { [] }
173
+ subject do
174
+ Mixpanel::BufferedConsumer.new(nil, nil, nil, 2) do |type, message|
175
+ raise Mixpanel::ServerError if submission_queue.shift == :fail
176
+ sent_messages << [type, message]
177
+ end
178
+ end
179
+
180
+ it 'clears any slices that complete on flush' do
181
+ # construct a consumer that is backed up and has a multi-slice buffer
182
+ 3.times { submission_queue << :fail }
183
+ 4.times do |i|
184
+ begin
185
+ subject.send!(:event, {'data' => i}.to_json)
186
+ rescue Mixpanel::ServerError
187
+ end
188
+ end
189
+ expect(sent_messages).to match_array([])
190
+
191
+ submission_queue << :pass
192
+ submission_queue << :fail
193
+
194
+ expect { subject.flush }.to raise_error Mixpanel::ServerError
195
+ expect(sent_messages).to match_array([
196
+ [:event, '{"data":[0,1]}']
197
+ ])
198
+
199
+ submission_queue << :pass
200
+ subject.flush
201
+ expect(sent_messages).to match_array([
202
+ [:event, '{"data":[0,1]}'],
203
+ [:event, '{"data":[2,3]}'],
204
+ ])
205
+ end
206
+ end
207
+
208
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mixpanel-ruby/error.rb'
4
+ require 'mixpanel-ruby/events.rb'
5
+
6
+ class TestErrorHandler < Mixpanel::ErrorHandler
7
+ def initialize(log)
8
+ @log = log
9
+ end
10
+
11
+ def handle(error)
12
+ @log << error.to_s
13
+ end
14
+ end
15
+
16
+ describe Mixpanel::ErrorHandler do
17
+ it "should respond to #handle`" do
18
+ error_handler = Mixpanel::ErrorHandler.new
19
+ expect(error_handler.respond_to?(:handle)).to be true
20
+ end
21
+
22
+ context 'without a customer error_handler' do
23
+
24
+ before(:each) do
25
+ @tracker = Mixpanel::Tracker.new('TEST TOKEN') do |type, message|
26
+ raise Mixpanel::MixpanelError
27
+ end
28
+ end
29
+
30
+ it "should silence errors in track calls" do
31
+ expect {
32
+ expect(@tracker.track('TEST ID', 'Test Event')).to be false
33
+ }.to_not raise_error
34
+ end
35
+
36
+ it "should handle errors in import calls" do
37
+ expect {
38
+ expect(@tracker.import('TEST API KEY', 'TEST DISTINCT_ID', 'Test Event')).to be false
39
+ }.to_not raise_error
40
+ end
41
+
42
+ it "should handle errors in people calls" do
43
+ expect {
44
+ expect(@tracker.people.set('TEST ID', {})).to be false
45
+ }.to_not raise_error
46
+ end
47
+
48
+ end
49
+
50
+ context 'with a custom error_handler' do
51
+
52
+ before(:each) do
53
+ @log = []
54
+ @error_handler = TestErrorHandler.new(@log)
55
+ @tracker = Mixpanel::Tracker.new('TEST TOKEN', @error_handler) do |type, message|
56
+ raise Mixpanel::MixpanelError
57
+ end
58
+ end
59
+
60
+ it "should handle errors in track calls" do
61
+ @tracker.track('TEST ID', 'Test Event', {})
62
+ expect(@log).to eq(['Mixpanel::MixpanelError'])
63
+ end
64
+
65
+ it "should handle errors in import calls" do
66
+ @tracker.import('TEST API KEY', 'TEST DISTINCT_ID', 'Test Event')
67
+ expect(@log).to eq(['Mixpanel::MixpanelError'])
68
+ end
69
+
70
+ it "should handle errors in people calls" do
71
+ @tracker.people.set('TEST ID', {})
72
+ expect(@log).to eq(['Mixpanel::MixpanelError'])
73
+ end
74
+
75
+ end
76
+ end