mixpanel-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ html
3
+ mixpanel-ruby*.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,190 @@
1
+ Copyright 2012 Mixpanel, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this work except in compliance with the License.
5
+ You may obtain a copy of the License below, or at:
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
15
+ Apache License
16
+ Version 2.0, January 2004
17
+ http://www.apache.org/licenses/
18
+
19
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
20
+
21
+ 1. Definitions.
22
+
23
+ "License" shall mean the terms and conditions for use, reproduction,
24
+ and distribution as defined by Sections 1 through 9 of this document.
25
+
26
+ "Licensor" shall mean the copyright owner or entity authorized by
27
+ the copyright owner that is granting the License.
28
+
29
+ "Legal Entity" shall mean the union of the acting entity and all
30
+ other entities that control, are controlled by, or are under common
31
+ control with that entity. For the purposes of this definition,
32
+ "control" means (i) the power, direct or indirect, to cause the
33
+ direction or management of such entity, whether by contract or
34
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
35
+ outstanding shares, or (iii) beneficial ownership of such entity.
36
+
37
+ "You" (or "Your") shall mean an individual or Legal Entity
38
+ exercising permissions granted by this License.
39
+
40
+ "Source" form shall mean the preferred form for making modifications,
41
+ including but not limited to software source code, documentation
42
+ source, and configuration files.
43
+
44
+ "Object" form shall mean any form resulting from mechanical
45
+ transformation or translation of a Source form, including but
46
+ not limited to compiled object code, generated documentation,
47
+ and conversions to other media types.
48
+
49
+ "Work" shall mean the work of authorship, whether in Source or
50
+ Object form, made available under the License, as indicated by a
51
+ copyright notice that is included in or attached to the work
52
+ (an example is provided in the Appendix below).
53
+
54
+ "Derivative Works" shall mean any work, whether in Source or Object
55
+ form, that is based on (or derived from) the Work and for which the
56
+ editorial revisions, annotations, elaborations, or other modifications
57
+ represent, as a whole, an original work of authorship. For the purposes
58
+ of this License, Derivative Works shall not include works that remain
59
+ separable from, or merely link (or bind by name) to the interfaces of,
60
+ the Work and Derivative Works thereof.
61
+
62
+ "Contribution" shall mean any work of authorship, including
63
+ the original version of the Work and any modifications or additions
64
+ to that Work or Derivative Works thereof, that is intentionally
65
+ submitted to Licensor for inclusion in the Work by the copyright owner
66
+ or by an individual or Legal Entity authorized to submit on behalf of
67
+ the copyright owner. For the purposes of this definition, "submitted"
68
+ means any form of electronic, verbal, or written communication sent
69
+ to the Licensor or its representatives, including but not limited to
70
+ communication on electronic mailing lists, source code control systems,
71
+ and issue tracking systems that are managed by, or on behalf of, the
72
+ Licensor for the purpose of discussing and improving the Work, but
73
+ excluding communication that is conspicuously marked or otherwise
74
+ designated in writing by the copyright owner as "Not a Contribution."
75
+
76
+ "Contributor" shall mean Licensor and any individual or Legal Entity
77
+ on behalf of whom a Contribution has been received by Licensor and
78
+ subsequently incorporated within the Work.
79
+
80
+ 2. Grant of Copyright License. Subject to the terms and conditions of
81
+ this License, each Contributor hereby grants to You a perpetual,
82
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
83
+ copyright license to reproduce, prepare Derivative Works of,
84
+ publicly display, publicly perform, sublicense, and distribute the
85
+ Work and such Derivative Works in Source or Object form.
86
+
87
+ 3. Grant of Patent License. Subject to the terms and conditions of
88
+ this License, each Contributor hereby grants to You a perpetual,
89
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
90
+ (except as stated in this section) patent license to make, have made,
91
+ use, offer to sell, sell, import, and otherwise transfer the Work,
92
+ where such license applies only to those patent claims licensable
93
+ by such Contributor that are necessarily infringed by their
94
+ Contribution(s) alone or by combination of their Contribution(s)
95
+ with the Work to which such Contribution(s) was submitted. If You
96
+ institute patent litigation against any entity (including a
97
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
98
+ or a Contribution incorporated within the Work constitutes direct
99
+ or contributory patent infringement, then any patent licenses
100
+ granted to You under this License for that Work shall terminate
101
+ as of the date such litigation is filed.
102
+
103
+ 4. Redistribution. You may reproduce and distribute copies of the
104
+ Work or Derivative Works thereof in any medium, with or without
105
+ modifications, and in Source or Object form, provided that You
106
+ meet the following conditions:
107
+
108
+ (a) You must give any other recipients of the Work or
109
+ Derivative Works a copy of this License; and
110
+
111
+ (b) You must cause any modified files to carry prominent notices
112
+ stating that You changed the files; and
113
+
114
+ (c) You must retain, in the Source form of any Derivative Works
115
+ that You distribute, all copyright, patent, trademark, and
116
+ attribution notices from the Source form of the Work,
117
+ excluding those notices that do not pertain to any part of
118
+ the Derivative Works; and
119
+
120
+ (d) If the Work includes a "NOTICE" text file as part of its
121
+ distribution, then any Derivative Works that You distribute must
122
+ include a readable copy of the attribution notices contained
123
+ within such NOTICE file, excluding those notices that do not
124
+ pertain to any part of the Derivative Works, in at least one
125
+ of the following places: within a NOTICE text file distributed
126
+ as part of the Derivative Works; within the Source form or
127
+ documentation, if provided along with the Derivative Works; or,
128
+ within a display generated by the Derivative Works, if and
129
+ wherever such third-party notices normally appear. The contents
130
+ of the NOTICE file are for informational purposes only and
131
+ do not modify the License. You may add Your own attribution
132
+ notices within Derivative Works that You distribute, alongside
133
+ or as an addendum to the NOTICE text from the Work, provided
134
+ that such additional attribution notices cannot be construed
135
+ as modifying the License.
136
+
137
+ You may add Your own copyright statement to Your modifications and
138
+ may provide additional or different license terms and conditions
139
+ for use, reproduction, or distribution of Your modifications, or
140
+ for any such Derivative Works as a whole, provided Your use,
141
+ reproduction, and distribution of the Work otherwise complies with
142
+ the conditions stated in this License.
143
+
144
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
145
+ any Contribution intentionally submitted for inclusion in the Work
146
+ by You to the Licensor shall be under the terms and conditions of
147
+ this License, without any additional terms or conditions.
148
+ Notwithstanding the above, nothing herein shall supersede or modify
149
+ the terms of any separate license agreement you may have executed
150
+ with Licensor regarding such Contributions.
151
+
152
+ 6. Trademarks. This License does not grant permission to use the trade
153
+ names, trademarks, service marks, or product names of the Licensor,
154
+ except as required for reasonable and customary use in describing the
155
+ origin of the Work and reproducing the content of the NOTICE file.
156
+
157
+ 7. Disclaimer of Warranty. Unless required by applicable law or
158
+ agreed to in writing, Licensor provides the Work (and each
159
+ Contributor provides its Contributions) on an "AS IS" BASIS,
160
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
161
+ implied, including, without limitation, any warranties or conditions
162
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
163
+ PARTICULAR PURPOSE. You are solely responsible for determining the
164
+ appropriateness of using or redistributing the Work and assume any
165
+ risks associated with Your exercise of permissions under this License.
166
+
167
+ 8. Limitation of Liability. In no event and under no legal theory,
168
+ whether in tort (including negligence), contract, or otherwise,
169
+ unless required by applicable law (such as deliberate and grossly
170
+ negligent acts) or agreed to in writing, shall any Contributor be
171
+ liable to You for damages, including any direct, indirect, special,
172
+ incidental, or consequential damages of any character arising as a
173
+ result of this License or out of the use or inability to use the
174
+ Work (including but not limited to damages for loss of goodwill,
175
+ work stoppage, computer failure or malfunction, or any and all
176
+ other commercial damages or losses), even if such Contributor
177
+ has been advised of the possibility of such damages.
178
+
179
+ 9. Accepting Warranty or Additional Liability. While redistributing
180
+ the Work or Derivative Works thereof, You may choose to offer,
181
+ and charge a fee for, acceptance of support, warranty, indemnity,
182
+ or other liability obligations and/or rights consistent with this
183
+ License. However, in accepting such obligations, You may act only
184
+ on Your own behalf and on Your sole responsibility, not on behalf
185
+ of any other Contributor, and only if You agree to indemnify,
186
+ defend, and hold each Contributor harmless for any liability
187
+ incurred by, or claims asserted against, such Contributor by reason
188
+ of your accepting any such warranty or additional liability.
189
+
190
+ END OF TERMS AND CONDITIONS
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rdoc/task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |spec|
5
+ spec.pattern = 'spec/**/*_spec.rb'
6
+ end
7
+
8
+ Rake::RDocTask.new do |rd|
9
+ rd.main = "Readme.rdoc"
10
+ rd.rdoc_files.include("Readme.rdoc", "lib/**/*.rb")
11
+ end
12
+
13
+ task :default => :spec
data/Readme.rdoc ADDED
@@ -0,0 +1,33 @@
1
+ = mixpanel-ruby: The official Mixpanel Ruby library
2
+
3
+ mixpanel-ruby is a library for tracking events and sending \Mixpanel profile
4
+ updates to \Mixpanel from your ruby applications. It's straightforward to
5
+ get started sending your first events and updates:
6
+
7
+ == Installation
8
+
9
+ gem install mixpanel-ruby
10
+
11
+ == Getting Started
12
+
13
+ require 'mixpanel-ruby'
14
+
15
+ tracker = Mixpanel::Tracker.new(YOUR_TOKEN)
16
+
17
+ # Track an event on behalf of user "User1"
18
+ tracker.track('User1', 'A Mixpanel Event')
19
+
20
+ # Send an update to User1's profile
21
+ tracker.people.set('User1', {
22
+ '$first_name' => 'David',
23
+ '$last_name' => 'Bowie',
24
+ 'Best Album' => 'The Rise and Fall of Ziggy Stardust and the Spiders from Mars'
25
+ })
26
+
27
+ The primary class you will use to track events is Mixpanel::Tracker. An instance of
28
+ Mixpanel::Tracker is enough to send events directly to \Mixpanel, and get you integrated
29
+ right away.
30
+
31
+
32
+
33
+
@@ -0,0 +1,71 @@
1
+ require 'mixpanel-ruby'
2
+ require 'thread'
3
+ require 'json'
4
+ require 'securerandom'
5
+
6
+ # As your application scales, it's likely you'll want to
7
+ # to detect events in one place and send them somewhere
8
+ # else. For example, you might write the events to a queue
9
+ # to be consumed by another process.
10
+ #
11
+ # This demo shows how you might do things, using
12
+ # the block constructor in Mixpanel to enqueue events,
13
+ # and a MixpanelBufferedConsumer to send them to
14
+ # Mixpanel
15
+
16
+ # Mixpanel uses the Net::HTTP library, which by default
17
+ # will not verify remote SSL certificates. In your app,
18
+ # you'll need to call Mixpanel.config_http with the path
19
+ # to your Certificate authority resources, or the library
20
+ # won't verify the remote certificate identity.
21
+ Mixpanel.config_http do |http|
22
+ http.ca_path = '/etc/ssl/certs'
23
+ http.ca_file = "/etc/ssl/certs/ca-certificates.crt"
24
+ http.use_ssl = true
25
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
26
+ end
27
+
28
+ class OutOfProcessExample
29
+ class << self
30
+ def run(token, distinct_id)
31
+ open('|-', 'w+') do |subprocess|
32
+ if subprocess
33
+ # This is the tracking process. Once we configure
34
+ # The tracker to write to our subprocess, we can quickly
35
+ # call #track without delaying our other work.
36
+ mixpanel_tracker = Mixpanel::Tracker.new(token) do |*message|
37
+ subprocess.write(message.to_json + "\n")
38
+ end
39
+
40
+ 100.times do |i|
41
+ event = 'Tick'
42
+ mixpanel_tracker.track(distinct_id, event, { 'Tick Number' => i })
43
+ puts "tick #{i}"
44
+ end
45
+
46
+ else
47
+ # This is the consumer process. In your applications, code
48
+ # like this may end up in queue consumers or in a separate
49
+ # thread.
50
+ mixpanel_consumer = Mixpanel::BufferedConsumer.new
51
+ begin
52
+ $stdin.each_line do |line|
53
+ message = JSON.load(line)
54
+ type, content = message
55
+ mixpanel_consumer.send(type, content)
56
+ end
57
+ ensure
58
+ mixpanel_consumer.flush
59
+ end
60
+ end
61
+ end
62
+ end # run
63
+ end
64
+ end
65
+
66
+ if __FILE__ == $0
67
+ # Replace this with the token from your project settings
68
+ DEMO_TOKEN = '072f77c15bd04a5d0044d3d76ced7fea'
69
+ run_id = SecureRandom.base64
70
+ OutOfProcessExample.run(DEMO_TOKEN, run_id)
71
+ end
@@ -0,0 +1,8 @@
1
+ require 'mixpanel-ruby'
2
+
3
+ if __FILE__ == $0
4
+ # Replace this with the token from your project settings
5
+ DEMO_TOKEN = '072f77c15bd04a5d0044d3d76ced7fea'
6
+ mixpanel_tracker = Mixpanel::Tracker.new(DEMO_TOKEN)
7
+ mixpanel_tracker.track('ID', 'Script run')
8
+ end
@@ -0,0 +1,3 @@
1
+ require 'mixpanel-ruby/consumer.rb'
2
+ require 'mixpanel-ruby/tracker.rb'
3
+ require 'mixpanel-ruby/version.rb'
@@ -0,0 +1,153 @@
1
+ require 'base64'
2
+ require 'net/https'
3
+
4
+ module Mixpanel
5
+ class ConnectionError < IOError
6
+ end
7
+
8
+ @@init_http = nil
9
+
10
+ # Ruby's default SSL does not verify the server certificate.
11
+ # To verify a certificate, or install a proxy, pass a block
12
+ # to Mixpanel::use_ssl that configures the Net::HTTP object.
13
+ # For example, if running in Ubuntu Linux, you can run
14
+ #
15
+ # Mixpanel::use_ssl do |http|
16
+ # http.ca_path = '/etc/ssl/certs'
17
+ # http.ca_file = '/etc/ssl/certs/ca-certificates.crt'
18
+ # http.verify_mode = OpenSSL::SSL::VERIFY_PEER
19
+ # end
20
+ #
21
+ # \Mixpanel Consumer and BufferedConsumer will call your block
22
+ # to configure their connections
23
+ def self.config_http(&block)
24
+ @@init_http = block
25
+ end
26
+
27
+ # A Consumer recieves messages from a Mixpanel::Tracker, and
28
+ # sends them elsewhere- probably to Mixpanel's analytics services,
29
+ # but can also enqueue them for later processing, log them to a
30
+ # file, or do whatever else you might find useful.
31
+ #
32
+ # You can provide your own consumer to your Mixpanel::Trackers,
33
+ # either by passing in an argument with a #send method when you construct
34
+ # the tracker, or just passing a block to Mixpanel::Tracker.new
35
+ #
36
+ # tracker = Mixpanel::Tracker.new(MY_TOKEN) do |type, message|
37
+ # # type will be one of :event or :profile_update
38
+ # @kestrel.set(ANALYTICS_QUEUE, [ type, message ].to_json)
39
+ # end
40
+ #
41
+ # You can also instantiate the library consumers yourself, and use
42
+ # them wherever you would like. For example, the working that
43
+ # consumes the above queue might work like this:
44
+ #
45
+ # mixpanel = Mixpanel::Consumer
46
+ # while true
47
+ # message_json = @kestrel.get(ANALYTICS_QUEUE)
48
+ # mixpanel.send(*JSON.load(message_json))
49
+ # end
50
+ #
51
+ # Mixpanel::Consumer is the default consumer. It sends each message,
52
+ # as the message is recieved, directly to Mixpanel.
53
+ class Consumer
54
+ def initialize(events_endpoint=nil, update_endpoint=nil)
55
+ @events_endpoint = events_endpoint || 'https://api.mixpanel.com/track'
56
+ @update_endpoint = update_endpoint || 'https://api.mixpanel.com/engage'
57
+ end
58
+
59
+ # Send the given string message to Mixpanel. Type should be
60
+ # one of :event or :profile_update, which will determine the endpoint.
61
+ def send(type, message)
62
+ type = type.to_sym
63
+ endpoint = {
64
+ :event => @events_endpoint,
65
+ :profile_update => @update_endpoint,
66
+ }[ type ]
67
+ data = Base64.strict_encode64(message)
68
+ uri = URI(endpoint)
69
+
70
+ client = Net::HTTP.new(uri.host, uri.port)
71
+ client.use_ssl = true
72
+ Mixpanel.with_http(client)
73
+
74
+ request = Net::HTTP::Post.new(uri.request_uri)
75
+ request.set_form_data({"data" => data })
76
+ response = client.request(request)
77
+
78
+ if response.code == '200' and response.body == '1'
79
+ return true
80
+ else
81
+ raise ConnectionError.new('Could not write to Mixpanel')
82
+ end
83
+ end
84
+ end
85
+
86
+ # BufferedConsumer buffers messages in memory, and sends messages as
87
+ # a batch. This can improve performance, but calls to #send may
88
+ # still block if the buffer is full. If you use this consumer, you
89
+ # should call #flush when your application exits or the messages
90
+ # remaining in the buffer will not be sent.
91
+ #
92
+ # To use a BufferedConsumer directly with a Mixpanel::Tracker,
93
+ # instantiate your Tracker like this
94
+ #
95
+ # buffered_consumer = Mixpanel::BufferedConsumer.new
96
+ # begin
97
+ # buffered_tracker = Mixpanel::Tracker.new(YOUR_TOKEN) do |type, message|
98
+ # buffered_consumer.send(type, message)
99
+ # end
100
+ # # Do some tracking here
101
+ # ...
102
+ # ensure
103
+ # buffered_consumer.flush
104
+ # end
105
+ #
106
+ class BufferedConsumer
107
+ MAX_LENGTH = 50
108
+
109
+ def initialize(events_endpoint=nil, update_endpoint=nil, max_buffer_length=MAX_LENGTH)
110
+ @max_length = [ max_buffer_length, MAX_LENGTH ].min
111
+ @consumer = Consumer.new(events_endpoint, update_endpoint)
112
+ @buffers = {
113
+ :event => [],
114
+ :profile_update => [],
115
+ }
116
+ end
117
+
118
+ # Stores a message for Mixpanel in memory. When the buffer
119
+ # hits a maximum length, the consumer will flush automatically.
120
+ # Flushes are synchronous when they occur.
121
+ def send(type, message)
122
+ type = type.to_sym
123
+ @buffers[type] << message
124
+ if @buffers[type].length >= @max_length
125
+ flush_type(type)
126
+ end
127
+ end
128
+
129
+ # Pushes all remaining messages in the buffer to Mixpanel.
130
+ # You should call #flush before your application exits or
131
+ # messages may not be sent.
132
+ def flush
133
+ @buffers.keys.each { |k| flush_type(k) }
134
+ end
135
+
136
+ private
137
+
138
+ def flush_type(type)
139
+ @buffers[type].each_slice(@max_length) do |chunk|
140
+ message = "[ #{chunk.join(',')} ]"
141
+ @consumer.send(type, message)
142
+ end
143
+ @buffers[type] = []
144
+ end
145
+ end
146
+
147
+ private
148
+ def self.with_http(http)
149
+ if @@init_http
150
+ @@init_http.call(http)
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,68 @@
1
+ require 'mixpanel-ruby/consumer'
2
+ require 'time'
3
+
4
+ module Mixpanel
5
+
6
+ # Handles formatting Mixpanel event tracking messages
7
+ # and sending them to the consumer. Mixpanel::Tracker
8
+ # is a subclass of this class, and the best way to
9
+ # track events is to instantiate a Mixpanel::Tracker
10
+ #
11
+ # tracker = Mixpanel::Tracker.new # Has all of the methods of Mixpanel::Event
12
+ # tracker.track(...)
13
+ #
14
+ class Events
15
+
16
+ # You likely won't need to instantiate an instance of
17
+ # Mixpanel::Events directly. The best way to get an instance
18
+ # is to use Mixpanel::Tracker
19
+ #
20
+ # # tracker has all of the methods of Mixpanel::Events
21
+ # tracker = Mixpanel::Tracker.new(...)
22
+ #
23
+ def initialize(token, &block)
24
+ @token = token
25
+ if block
26
+ @sink = block
27
+ else
28
+ consumer = Consumer.new
29
+ @sink = consumer.method(:send)
30
+ end
31
+ end
32
+
33
+ # Notes that an event has occurred, along with a distinct_id
34
+ # representing the source of that event (for example, a user id),
35
+ # an event name describing the event and a set of properties
36
+ # describing that event. Properties are provided as a Hash with
37
+ # string keys and strings, numbers or booleans as values.
38
+ #
39
+ # tracker = Mixpanel::Tracker.new
40
+ #
41
+ # # Track that user "12345"'s credit card was declined
42
+ # tracker.track("12345", "Credit Card Declined")
43
+ #
44
+ # # Properties describe the circumstances of the event,
45
+ # # or aspects of the source or user associated with the event
46
+ # tracker.track("12345", "Welcome Email Sent", {
47
+ # 'Email Template' => 'Pretty Pink Welcome',
48
+ # 'User Sign-up Cohort' => 'July 2013'
49
+ # })
50
+ def track(distinct_id, event, properties={}, ip=nil)
51
+ properties = {
52
+ 'distinct_id' => distinct_id,
53
+ 'token' => @token,
54
+ 'time' => Time.now.to_i
55
+ }.merge(properties)
56
+ if ip
57
+ properties['ip'] = ip
58
+ end
59
+
60
+ message = {
61
+ 'event' => event,
62
+ 'properties' => properties
63
+ }
64
+
65
+ @sink.call(:event, message.to_json)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,243 @@
1
+ require 'mixpanel-ruby/consumer'
2
+ require 'json'
3
+ require 'date'
4
+ require 'time'
5
+
6
+ module Mixpanel
7
+
8
+ # Handles formatting Mixpanel profile updates and
9
+ # sending them to the consumer. You will rarely need
10
+ # to instantiate this class directly- to send
11
+ # profile updates, use Mixpanel::Tracker#people
12
+ #
13
+ # tracker = Mixpanel::Tracker.new
14
+ # tracker.people.set(...) # Or .append(..), or track_charge(...) etc.
15
+ class People
16
+
17
+ # You likely won't need to instantiate instances of Mixpanel::People
18
+ # directly. The best way to get an instance of Mixpanel::People is
19
+ #
20
+ # tracker = Mixpanel::Tracker.new(...)
21
+ # tracker.people # An instance of Mixpanel::People
22
+ #
23
+ def initialize(token, &block)
24
+ @token = token
25
+ if block
26
+ @sink = block
27
+ else
28
+ consumer = Consumer.new
29
+ @sink = consumer.method(:send)
30
+ end
31
+ end
32
+
33
+ # Sets properties on a user record. Takes a Hash with string
34
+ # keys, and values that are strings, numbers, booleans, or
35
+ # DateTimes
36
+ #
37
+ # tracker = Mixpanel::Tracker.new
38
+ # # Sets properties on profile with id "1234"
39
+ # tracker.people.set("1234", {
40
+ # 'company' => 'Acme',
41
+ # 'plan' => 'Premium',
42
+ # 'Sign-Up Date' => DateTime.now
43
+ # });
44
+ #
45
+ # If you provide an ip argument, Mixpanel will use that
46
+ # ip address for geolocation (rather than the ip of your server)
47
+ def set(distinct_id, properties, ip=nil)
48
+ properties = fix_property_dates(properties)
49
+ message = {
50
+ '$distinct_id' => distinct_id,
51
+ '$set' => properties,
52
+ }
53
+
54
+ if ip
55
+ message['$ip'] = ip
56
+ end
57
+
58
+ update(message)
59
+ end
60
+
61
+ # set_once works just like #set, but will only change the
62
+ # value of properties if they are not already present
63
+ # in the profile. That means you can call set_once many times
64
+ # without changing an original value.
65
+ #
66
+ # tracker = Mixpanel::Tracker.new
67
+ # tracker.people.set("12345", {
68
+ # 'First Login Date': DateTime.now
69
+ # });
70
+ #
71
+ def set_once(distinct_id, properties, ip=nil)
72
+ properties = fix_property_dates(properties)
73
+ message = {
74
+ '$distinct_id' => distinct_id,
75
+ '$set_once' => properties,
76
+ }
77
+
78
+ if ip
79
+ message['$ip'] = ip
80
+ end
81
+
82
+ update(message)
83
+ end
84
+
85
+ # Changes the value of properties by a numeric amount. Takes a
86
+ # hash with string keys and numeric properties. Mixpanel will add
87
+ # the given amount to whatever value is currently assigned to the
88
+ # property. If no property exists with a given name, the value
89
+ # will be added to zero.
90
+ #
91
+ # tracker = Mixpanel::Tracker.new
92
+ # tracker.people.set("12345", {
93
+ # 'Coins Spent' => 7,
94
+ # 'Coins Earned' => -7, # Use a negative number to subtract
95
+ # });
96
+ #
97
+ def increment(distinct_id, properties, ip=nil)
98
+ properties = fix_property_dates(properties)
99
+ message = {
100
+ '$distinct_id' => distinct_id,
101
+ '$add' => properties,
102
+ }
103
+
104
+ if ip
105
+ message['$ip'] = ip
106
+ end
107
+
108
+ update(message)
109
+ end
110
+
111
+ # Appends a values to the end of list-valued properties.
112
+ # If the given properties don't exist, a new list-valued
113
+ # property will be created.
114
+ #
115
+ # tracker = Mixpanel::Tracker.new
116
+ # tracker.people.append("12345", {
117
+ # 'Login Dates' => DateTime.now,
118
+ # 'Alter Ego Names' => 'Ziggy Stardust'
119
+ # });
120
+ #
121
+ def append(distinct_id, properties, ip=nil)
122
+ properties = fix_property_dates(properties)
123
+ message = {
124
+ '$distinct_id' => distinct_id,
125
+ '$append' => properties,
126
+ }
127
+
128
+ if ip
129
+ message['$ip'] = ip
130
+ end
131
+
132
+ update(message)
133
+ end
134
+
135
+ # Appends a value to the end of list-valued properties,
136
+ # only if the given value is not already present in the list.
137
+ #
138
+ # tracker = Mixpanel::Tracker.new
139
+ # tracker.people.union("12345", {
140
+ # 'Levels Completed' => 'Suffragette City'
141
+ # });
142
+ #
143
+ def union(distinct_id, properties, ip=nil)
144
+ properties = fix_property_dates(properties)
145
+ message = {
146
+ '$distinct_id' => distinct_id,
147
+ '$union' => properties,
148
+ }
149
+
150
+ if ip
151
+ message['$ip'] = ip
152
+ end
153
+
154
+ update(message)
155
+ end
156
+
157
+ # Removes a property and it's value from a profile.
158
+ #
159
+ # tracker = Mixpanel::Tracker.new
160
+ # tracker.people.unset("12345", "Overdue Since")
161
+ #
162
+ def unset(distinct_id, property)
163
+ update({
164
+ '$distinct_id' => distinct_id,
165
+ '$unset' => [ property ]
166
+ })
167
+ end
168
+
169
+ # Records a payment to you to a profile. Charges recorded with
170
+ # #track_charge will appear in the Mixpanel revenue report.
171
+ #
172
+ # tracker = Mixpanel::Tracker.new
173
+ #
174
+ # # records a charge of $25.32 from user 12345
175
+ # tracker.people.track_charge("12345", 25.32)
176
+ #
177
+ # # records a charge of $30.50 on the 2nd of January,
178
+ # mixpanel.people.track_charge("12345", 30.50, {
179
+ # '$time' => DateTime.parse("Jan 2 2013")
180
+ # })
181
+ #
182
+ def track_charge(distinct_id, amount, properties, ip=nil)
183
+ properties = fix_property_dates(properties)
184
+ charge_properties = properties.merge({ '$amount' => amount })
185
+ append(distinct_id, { '$transactions' => charge_properties }, ip)
186
+ end
187
+
188
+ # Clear all charges from a Mixpanel people profile
189
+ def clear_charges(distinct_id)
190
+ unset(distinct_id, '$transactions')
191
+ end
192
+
193
+ # Permanently delete a profile from Mixpanel people analytics
194
+ def delete_user(distinct_id)
195
+ update({
196
+ '$distinct_id' => distinct_id,
197
+ '$delete' => ''
198
+ })
199
+ end
200
+
201
+ # Send a generic update to Mixpanel people analytics.
202
+ # Caller is responsible for formatting the update, as
203
+ # documented in the Mixpanel HTTP specification. This
204
+ # method might be useful if you want to use very new
205
+ # or experimental features of people analytics from Ruby
206
+ # The Mixpanel HTTP tracking API is documented at
207
+ # http://joe.dev.mixpanel.org/help/reference/http
208
+ def update(message)
209
+ message = {
210
+ '$token' => @token,
211
+ '$time' => ((Time.now.to_f) * 1000.0).to_i
212
+ }.merge(message)
213
+ @sink.call(:profile_update, message.to_json)
214
+ end
215
+
216
+ private
217
+
218
+ def fix_property_dates(h)
219
+ h.inject({}) do |ret,(k,v)|
220
+ ret[k] = PeopleDate.asPeopleDate(v)
221
+ ret
222
+ end
223
+ end
224
+
225
+ class PeopleDate
226
+ def initialize(date)
227
+ @date = date
228
+ end
229
+
230
+ def to_json(*a)
231
+ @date.strftime('%Y-%m-%dT%H:%M:%S').to_json(*a)
232
+ end
233
+
234
+ def self.asPeopleDate(thing)
235
+ if thing.is_a?(Date)
236
+ PeopleDate.new(thing)
237
+ else
238
+ thing
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,61 @@
1
+ require 'mixpanel-ruby/events.rb'
2
+ require 'mixpanel-ruby/people.rb'
3
+
4
+ module Mixpanel
5
+ # Use Mixpanel::Tracker to track events and profile updates in your application.
6
+ # To track an event, call
7
+ #
8
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
9
+ # Mixpanel::Tracker.track(a_distinct_id, an_event_name, { properties })
10
+ #
11
+ # To send people updates, call
12
+ #
13
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
14
+ # tracker.people.set(a_distinct_id, { properties })
15
+ #
16
+ # Mixpanel::Tracker is a subclass of Mixpanel::Events, and exposes
17
+ # an instance of Mixpanel::People as Tracker#people
18
+ class Tracker < Events
19
+ # An instance of Mixpanel::People. Use this to
20
+ # send profile updates
21
+ attr_reader :people
22
+
23
+ # Takes your Mixpanel project token, as a string.
24
+ #
25
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
26
+ #
27
+ # By default, the tracker will send an message to Mixpanel
28
+ # synchronously with each call, using an instance of Mixpanel::Consumer.
29
+ #
30
+ # You can also provide a block to the constructor
31
+ # to specify particular consumer behaviors (for
32
+ # example, if you wanted to write your messages to
33
+ # a queue instead of sending them directly to Mixpanel)
34
+ #
35
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN) do |type, message|
36
+ # @kestrel.set(MY_MIXPANEL_QUEUE, [type,message].to_json)
37
+ # end
38
+ #
39
+ # If a block is provided, it is passed a type (one of :event or :profile_update)
40
+ # and a string message. This same format is accepted by Mixpanel::Consumer#send
41
+ # and Mixpanel::BufferedConsumer#send
42
+ def initialize(token, &block)
43
+ super(token, &block)
44
+ @people = People.new(token, &block)
45
+ end
46
+
47
+
48
+ # Creates a distinct_id alias. \Events and updates with an alias
49
+ # will be considered by mixpanel to have the same source, and
50
+ # refer to the same profile.
51
+ #
52
+ # Multiple aliases can map to the same real_id, once a real_id is
53
+ # used to track events or send updates, it should never be used as
54
+ # an alias itself.
55
+ def alias(alias_id, real_id)
56
+ track(real_id, '$create_alias', {
57
+ 'alias' => alias_id
58
+ })
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ module Mixpanel
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,17 @@
1
+ require File.join(File.dirname(__FILE__), 'lib/mixpanel-ruby/version.rb')
2
+
3
+ spec = Gem::Specification.new do |spec|
4
+ spec.name = 'mixpanel-ruby'
5
+ spec.version = Mixpanel::VERSION
6
+ spec.files = Dir.glob(`git ls-files`.split("\n"))
7
+ spec.require_paths = ['lib']
8
+ spec.summary = 'Official Mixpanel tracking library for ruby'
9
+ spec.description = 'The official Mixpanel tracking library for ruby'
10
+ spec.authors = [ 'Mixpanel' ]
11
+ spec.email = 'support@mixpanel.com'
12
+ spec.homepage = 'https://mixpanel.com/help/reference/ruby'
13
+
14
+ spec.add_development_dependency('rake')
15
+ spec.add_development_dependency('rspec')
16
+ spec.add_development_dependency('webmock')
17
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+ require 'webmock'
3
+ require 'base64'
4
+ require 'mixpanel-ruby/consumer'
5
+
6
+ describe Mixpanel::Consumer do
7
+ before(:each) do
8
+ WebMock.reset!
9
+ @consumer = Mixpanel::Consumer.new
10
+ end
11
+
12
+ it 'should send a request to api.mixpanel.com/track on events' do
13
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({ :body => "1" })
14
+ @consumer.send(:event, 'TEST EVENT MESSAGE')
15
+ WebMock.should have_requested(:post, 'https://api.mixpanel.com/track').
16
+ with(:body => {'data' => 'VEVTVCBFVkVOVCBNRVNTQUdF' })
17
+ end
18
+
19
+ it 'should send a request to api.mixpanel.com/people on profile updates' do
20
+ stub_request(:any, 'https://api.mixpanel.com/engage').to_return({ :body => "1" })
21
+ @consumer.send(:profile_update, 'TEST EVENT MESSAGE')
22
+ WebMock.should have_requested(:post, 'https://api.mixpanel.com/engage').
23
+ with(:body => {'data' => 'VEVTVCBFVkVOVCBNRVNTQUdF' })
24
+ end
25
+ end
26
+
27
+ describe Mixpanel::BufferedConsumer do
28
+ before(:each) do
29
+ WebMock.reset!
30
+ @max_length = 10
31
+ @consumer = Mixpanel::BufferedConsumer.new(nil, nil, @max_length)
32
+ end
33
+
34
+ it 'should not send a request for a single message until flush is called' do
35
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({ :body => "1" })
36
+ @consumer.send(:event, 'TEST EVENT 1')
37
+ WebMock.should have_not_requested(:post, 'https://api.mixpanel.com/track')
38
+
39
+ @consumer.flush()
40
+ WebMock.should have_requested(:post, 'https://api.mixpanel.com/track').
41
+ with(:body => {'data' => 'WyBURVNUIEVWRU5UIDEgXQ==' })
42
+ end
43
+
44
+ it 'should send one message when max_length events are tracked' do
45
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({ :body => "1" })
46
+
47
+ @max_length.times do |i|
48
+ @consumer.send(:event, "x #{i}")
49
+ end
50
+
51
+ WebMock.should have_requested(:post, 'https://api.mixpanel.com/track').
52
+ with(:body => {'data' => 'WyB4IDAseCAxLHggMix4IDMseCA0LHggNSx4IDYseCA3LHggOCx4IDkgXQ==' })
53
+ end
54
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'mixpanel-ruby/events.rb'
3
+ require 'time'
4
+
5
+ describe Mixpanel::Events do
6
+ before(:each) do
7
+ @time_now = Time.parse('Jun 6 1972, 16:23:04')
8
+ Time.stub!(:now).and_return(@time_now)
9
+
10
+ @log = []
11
+ @events = Mixpanel::Events.new('TEST TOKEN') do |type, message|
12
+ @log << [ type, JSON.load(message) ]
13
+ end
14
+ end
15
+
16
+ it 'should send a well formed track/ message' do
17
+ @events.track('TEST ID', 'Test Event', {
18
+ 'Circumstances' => 'During a test'
19
+ })
20
+ @log.should eq([[ :event, {
21
+ 'event' => 'Test Event',
22
+ 'properties' => {
23
+ 'Circumstances' => 'During a test',
24
+ 'distinct_id' => 'TEST ID',
25
+ 'token' => 'TEST TOKEN',
26
+ 'time' => 76695784
27
+ }
28
+ }]])
29
+ end
30
+ end
31
+
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+ require 'date'
3
+ require 'json'
4
+ require 'mixpanel-ruby/people.rb'
5
+
6
+ describe Mixpanel::People do
7
+ before(:each) do
8
+ @time_now = Time.parse('Jun 6 1972, 16:23:04')
9
+ Time.stub!(:now).and_return(@time_now)
10
+
11
+ @log = []
12
+ @people = Mixpanel::People.new('TEST TOKEN') do |type, message|
13
+ @log << [ type, JSON.load(message) ]
14
+ end
15
+ end
16
+
17
+ it 'should send a well formed engage/set message' do
18
+ @people.set("TEST ID", {
19
+ '$firstname' => 'David',
20
+ '$lastname' => 'Bowie',
21
+ })
22
+ @log.should eq([[ :profile_update, {
23
+ '$token' => 'TEST TOKEN',
24
+ '$distinct_id' => 'TEST ID',
25
+ '$time' => 76695784000,
26
+ '$set' => {
27
+ '$firstname' => 'David',
28
+ '$lastname' => 'Bowie'
29
+ }
30
+ }]])
31
+ end
32
+
33
+ it 'should send a well formed engage/set_once message' do
34
+ @people.set_once("TEST ID", {
35
+ '$firstname' => 'David',
36
+ '$lastname' => 'Bowie',
37
+ })
38
+ @log.should eq([[ :profile_update, {
39
+ '$token' => 'TEST TOKEN',
40
+ '$distinct_id' => 'TEST ID',
41
+ '$time' => 76695784000,
42
+ '$set_once' => {
43
+ '$firstname' => 'David',
44
+ '$lastname' => 'Bowie'
45
+ }
46
+ }]])
47
+ end
48
+
49
+ it 'should send a well formed engage/add message' do
50
+ @people.increment("TEST ID", { 'Albums Released' => 10 })
51
+ @log.should eq([[ :profile_update, {
52
+ '$token' => 'TEST TOKEN',
53
+ '$distinct_id' => 'TEST ID',
54
+ '$time' => 76695784000,
55
+ '$add' => {
56
+ 'Albums Released' => 10
57
+ }
58
+ }]])
59
+ end
60
+
61
+ it 'should send a well formed engage/append message' do
62
+ @people.append("TEST ID", { 'Albums' => 'Diamond Dogs' })
63
+ @log.should eq([[ :profile_update, {
64
+ '$token' => 'TEST TOKEN',
65
+ '$distinct_id' => 'TEST ID',
66
+ '$time' => 76695784000,
67
+ '$append' => {
68
+ 'Albums' => 'Diamond Dogs'
69
+ }
70
+ }]])
71
+ end
72
+
73
+ it 'should send a well formed engage/union message' do
74
+ @people.union("TEST ID", { 'Albums' => 'Diamond Dogs' })
75
+ @log.should eq([[ :profile_update, {
76
+ '$token' => 'TEST TOKEN',
77
+ '$distinct_id' => 'TEST ID',
78
+ '$time' => 76695784000,
79
+ '$union' => {
80
+ 'Albums' => 'Diamond Dogs'
81
+ }
82
+ }]])
83
+ end
84
+
85
+ it 'should send a well formed unset message' do
86
+ @people.unset('TEST ID', 'Albums')
87
+ @log.should eq([[ :profile_update, {
88
+ '$token' => 'TEST TOKEN',
89
+ '$distinct_id' => 'TEST ID',
90
+ '$time' => 76695784000,
91
+ '$unset' => [ 'Albums' ]
92
+ }]])
93
+ end
94
+
95
+ it 'should send an engage/append with the right $transaction stuff' do
96
+ @people.track_charge("TEST ID", 25.42, {
97
+ '$time' => DateTime.new(1999,12,24,14, 02, 53),
98
+ 'SKU' => '1234567'
99
+ })
100
+ @log.should eq([[ :profile_update, {
101
+ '$token' => 'TEST TOKEN',
102
+ '$distinct_id' => 'TEST ID',
103
+ '$time' => 76695784000,
104
+ '$append' => {
105
+ '$transactions' => {
106
+ '$time' => '1999-12-24T14:02:53',
107
+ 'SKU' => '1234567',
108
+ '$amount' => 25.42
109
+ }
110
+ }
111
+ }]])
112
+ end
113
+
114
+ it 'should send a well formed engage/unset message for $transaction' do
115
+ @people.clear_charges("TEST ID")
116
+ @log.should eq([[ :profile_update, {
117
+ '$token' => 'TEST TOKEN',
118
+ '$distinct_id' => 'TEST ID',
119
+ '$time' => 76695784000,
120
+ '$unset' => [ '$transactions' ]
121
+ }]])
122
+ end
123
+
124
+ it 'should send a well formed engage/delete message' do
125
+ @people.delete_user("TEST ID")
126
+ @log.should eq([[ :profile_update, {
127
+ '$token' => 'TEST TOKEN',
128
+ '$distinct_id' => 'TEST ID',
129
+ '$time' => 76695784000,
130
+ '$delete' => ''
131
+ }]])
132
+ end
133
+ end
@@ -0,0 +1,90 @@
1
+ require 'mixpanel-ruby'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ describe Mixpanel::Tracker do
7
+ before(:each) do
8
+ @time_now = Time.parse('Jun 6 1972, 16:23:04')
9
+ Time.stub!(:now).and_return(@time_now)
10
+ end
11
+
12
+ it 'should send a well formed alias message' do
13
+ log = []
14
+ mixpanel = Mixpanel::Tracker.new('TEST TOKEN') do |type, message|
15
+ log << [ type, JSON.load(message) ]
16
+ end
17
+ mixpanel.alias('TEST ALIAS', 'TEST ID')
18
+ log.should eq([[ :event, {
19
+ 'event' => '$create_alias',
20
+ 'properties' => {
21
+ 'alias' => 'TEST ALIAS',
22
+ 'distinct_id' => 'TEST ID',
23
+ 'token' => 'TEST TOKEN',
24
+ 'time' => 76695784
25
+ }
26
+ }]])
27
+ end
28
+
29
+ it 'should send a request to the track api with the default consumer' do
30
+ WebMock.reset!
31
+ stub_request(:any, 'https://api.mixpanel.com/track').to_return({ :body => "1" })
32
+ stub_request(:any, 'https://api.mixpanel.com/engage').to_return({ :body => "1" })
33
+ mixpanel = Mixpanel::Tracker.new('TEST TOKEN')
34
+
35
+ mixpanel.track('TEST ID', 'TEST EVENT', { 'Circumstances' => 'During test' })
36
+
37
+ body = nil
38
+ WebMock.should have_requested(:post, 'https://api.mixpanel.com/track').
39
+ with { |req| body = req.body }
40
+
41
+ message_urlencoded = body[/^data=(.*)$/, 1]
42
+ message_json = Base64.strict_decode64(URI.unescape(message_urlencoded))
43
+ message = JSON.load(message_json)
44
+ message.should eq({
45
+ 'event' => 'TEST EVENT',
46
+ 'properties' => {
47
+ 'Circumstances' => 'During test',
48
+ 'distinct_id' => 'TEST ID',
49
+ 'token' => 'TEST TOKEN',
50
+ 'time' => 76695784
51
+ }
52
+ })
53
+ end
54
+
55
+ it 'should call a consumer block if one is given' do
56
+ messages = []
57
+ mixpanel = Mixpanel::Tracker.new('TEST TOKEN') do |type, message|
58
+ messages << [ type, JSON.load(message) ]
59
+ end
60
+ mixpanel.track('ID', 'Event')
61
+ mixpanel.people.set('ID', { 'k' => 'v' })
62
+ mixpanel.people.append('ID', { 'k' => 'v' })
63
+
64
+ messages.should eq([
65
+ [ :event,
66
+ { 'event' => 'Event',
67
+ 'properties' => {
68
+ 'distinct_id' => 'ID',
69
+ 'token' => 'TEST TOKEN',
70
+ 'time' => 76695784
71
+ }
72
+ }
73
+ ],
74
+ [ :profile_update,
75
+ { '$token' => 'TEST TOKEN',
76
+ '$distinct_id' => 'ID',
77
+ '$time' => 76695784000,
78
+ '$set' => { 'k' => 'v' }
79
+ }
80
+ ],
81
+ [ :profile_update,
82
+ { '$token' => 'TEST TOKEN',
83
+ '$distinct_id' => 'ID',
84
+ '$time' => 76695784000,
85
+ '$append' => { 'k' => 'v' }
86
+ }
87
+ ]
88
+ ])
89
+ end
90
+ end
@@ -0,0 +1,15 @@
1
+ require 'json'
2
+ require 'webmock/rspec'
3
+
4
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
5
+ RSpec.configure do |config|
6
+ config.treat_symbols_as_metadata_keys_with_true_values = true
7
+ config.run_all_when_everything_filtered = true
8
+ config.filter_run :focus
9
+
10
+ # Run specs in random order to surface order dependencies. If you find an
11
+ # order dependency and want to debug it, you can fix the order by providing
12
+ # the seed, which is printed after each run.
13
+ # --seed 1234
14
+ config.order = 'random'
15
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mixpanel-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mixpanel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: webmock
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: The official Mixpanel tracking library for ruby
63
+ email: support@mixpanel.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - .gitignore
69
+ - .rspec
70
+ - Gemfile
71
+ - LICENSE
72
+ - Rakefile
73
+ - Readme.rdoc
74
+ - demo/out_of_process_consumer.rb
75
+ - demo/simple_messages.rb
76
+ - lib/mixpanel-ruby.rb
77
+ - lib/mixpanel-ruby/consumer.rb
78
+ - lib/mixpanel-ruby/events.rb
79
+ - lib/mixpanel-ruby/people.rb
80
+ - lib/mixpanel-ruby/tracker.rb
81
+ - lib/mixpanel-ruby/version.rb
82
+ - mixpanel-ruby.gemspec
83
+ - spec/mixpanel-ruby/consumer_spec.rb
84
+ - spec/mixpanel-ruby/events_spec.rb
85
+ - spec/mixpanel-ruby/people_spec.rb
86
+ - spec/mixpanel-ruby/tracker_spec.rb
87
+ - spec/spec_helper.rb
88
+ homepage: https://mixpanel.com/help/reference/ruby
89
+ licenses: []
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 1.8.25
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: Official Mixpanel tracking library for ruby
112
+ test_files: []