├── .rspec ├── Gemfile ├── .gitignore ├── lib ├── mixpanel-ruby │ ├── version.rb │ ├── events.rb │ ├── tracker.rb │ ├── people.rb │ └── consumer.rb └── mixpanel-ruby.rb ├── .travis.yml ├── demo ├── simple_messages.rb ├── faraday_consumer.rb └── out_of_process_consumer.rb ├── Rakefile ├── spec ├── spec_helper.rb └── mixpanel-ruby │ ├── events_spec.rb │ ├── tracker_spec.rb │ ├── people_spec.rb │ └── consumer_spec.rb ├── mixpanel-ruby.gemspec ├── Readme.rdoc └── LICENSE /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | Gemfile.lock 3 | html 4 | mixpanel-ruby*.gem 5 | .bundle 6 | -------------------------------------------------------------------------------- /lib/mixpanel-ruby/version.rb: -------------------------------------------------------------------------------- 1 | module Mixpanel 2 | VERSION = '2.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /lib/mixpanel-ruby.rb: -------------------------------------------------------------------------------- 1 | require 'mixpanel-ruby/consumer.rb' 2 | require 'mixpanel-ruby/tracker.rb' 3 | require 'mixpanel-ruby/version.rb' 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.0 4 | - 2.1.0 5 | - 2.0.0 6 | - 1.9.3 7 | - jruby-19mode 8 | - rbx-2 9 | 10 | matrix: 11 | allow_failures: 12 | - rvm: "rbx-2" 13 | -------------------------------------------------------------------------------- /demo/simple_messages.rb: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 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.run_all_when_everything_filtered = true 7 | config.filter_run :focus 8 | config.raise_errors_for_deprecations! 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 16 | -------------------------------------------------------------------------------- /mixpanel-ruby.gemspec: -------------------------------------------------------------------------------- 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 | spec.license = 'Apache License 2.0' 14 | 15 | spec.add_development_dependency 'rake' 16 | spec.add_development_dependency 'rspec', '~> 3.0.0' 17 | spec.add_development_dependency 'webmock', '~> 1.18.0' 18 | end 19 | -------------------------------------------------------------------------------- /demo/faraday_consumer.rb: -------------------------------------------------------------------------------- 1 | require 'mixpanel-ruby' 2 | require 'faraday' 3 | 4 | # The Mixpanel library's default consumer will use the standard 5 | # Net::HTTP library to communicate with servers, but you can extend 6 | # your consumers to use other libraries. This example sends data using 7 | # the Faraday library (so you'll need that library available to run it) 8 | 9 | class FaradayConsumer < Mixpanel::Consumer 10 | def request(endpoint, form_data) 11 | conn = ::Faraday.new(endpoint) 12 | response = conn.post(nil, form_data) 13 | [response.status, response.body] 14 | end 15 | end 16 | 17 | if __FILE__ == $0 18 | # Replace this with the token from your project settings 19 | DEMO_TOKEN = '072f77c15bd04a5d0044d3d76ced7fea' 20 | faraday_consumer = FaradayConsumer.new 21 | 22 | faraday_tracker = Mixpanel::Tracker.new(DEMO_TOKEN) do |type, message| 23 | faraday_consumer.send!(type, message) 24 | end 25 | faraday_tracker.track('ID', 'Event tracked through Faraday') 26 | 27 | # It's also easy to delegate from a BufferedConsumer to your custom 28 | # consumer. 29 | 30 | buffered_faraday_consumer = Mixpanel::BufferedConsumer.new do |type, message| 31 | faraday_consumer.send!(type, message) 32 | end 33 | 34 | buffered_faraday_tracker = Mixpanel::Tracker.new(DEMO_TOKEN) do |type, message| 35 | buffered_faraday_consumer.send!(type, message) 36 | end 37 | 38 | buffered_faraday_tracker.track('ID', 'Event tracked (buffered) through faraday') 39 | buffered_faraday_consumer.flush 40 | end 41 | -------------------------------------------------------------------------------- /spec/mixpanel-ruby/events_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mixpanel-ruby/events.rb' 3 | require 'mixpanel-ruby/version.rb' 4 | require 'time' 5 | 6 | describe Mixpanel::Events do 7 | before(:each) do 8 | @time_now = Time.parse('Jun 6 1972, 16:23:04') 9 | allow(Time).to receive(:now).and_return(@time_now) 10 | 11 | @log = [] 12 | @events = Mixpanel::Events.new('TEST TOKEN') do |type, message| 13 | @log << [type, JSON.load(message)] 14 | end 15 | end 16 | 17 | it 'should send a well formed track/ message' do 18 | @events.track('TEST ID', 'Test Event', { 19 | 'Circumstances' => 'During a test' 20 | }) 21 | expect(@log).to eq([[:event, 'data' => { 22 | 'event' => 'Test Event', 23 | 'properties' => { 24 | 'Circumstances' => 'During a test', 25 | 'distinct_id' => 'TEST ID', 26 | 'mp_lib' => 'ruby', 27 | '$lib_version' => Mixpanel::VERSION, 28 | 'token' => 'TEST TOKEN', 29 | 'time' => @time_now.to_i 30 | } 31 | }]]) 32 | end 33 | 34 | it 'should send a well formed import/ message' do 35 | @events.import('API_KEY', 'TEST ID', 'Test Event', { 36 | 'Circumstances' => 'During a test' 37 | }) 38 | expect(@log).to eq([[:import, { 39 | 'api_key' => 'API_KEY', 40 | 'data' => { 41 | 'event' => 'Test Event', 42 | 'properties' => { 43 | 'Circumstances' => 'During a test', 44 | 'distinct_id' => 'TEST ID', 45 | 'mp_lib' => 'ruby', 46 | '$lib_version' => Mixpanel::VERSION, 47 | 'token' => 'TEST TOKEN', 48 | 'time' => @time_now.to_i 49 | } 50 | } 51 | } ]]) 52 | end 53 | end 54 | 55 | -------------------------------------------------------------------------------- /demo/out_of_process_consumer.rb: -------------------------------------------------------------------------------- 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 72 | -------------------------------------------------------------------------------- /Readme.rdoc: -------------------------------------------------------------------------------- 1 | {Build Status}[https://travis-ci.org/mixpanel/mixpanel-ruby] 2 | 3 | = mixpanel-ruby: The official Mixpanel Ruby library 4 | 5 | mixpanel-ruby is a library for tracking events and sending \Mixpanel profile 6 | updates to \Mixpanel from your ruby applications. 7 | 8 | == Installation 9 | 10 | gem install mixpanel-ruby 11 | 12 | == Getting Started 13 | 14 | require 'mixpanel-ruby' 15 | 16 | tracker = Mixpanel::Tracker.new(YOUR_TOKEN) 17 | 18 | # Track an event on behalf of user "User1" 19 | tracker.track('User1', 'A Mixpanel Event') 20 | 21 | # Send an update to User1's profile 22 | tracker.people.set('User1', { 23 | '$first_name' => 'David', 24 | '$last_name' => 'Bowie', 25 | 'Best Album' => 'The Rise and Fall of Ziggy Stardust and the Spiders from Mars' 26 | }) 27 | 28 | The primary class you will use to track events is Mixpanel::Tracker. An instance of 29 | Mixpanel::Tracker is enough to send events directly to \Mixpanel, and get you integrated 30 | right away. 31 | 32 | == Additional Information 33 | 34 | For more information please visit: 35 | 36 | * Our Ruby API Integration page[https://mixpanel.com/help/reference/ruby#introduction] 37 | * The usage demo[https://github.com/mixpanel/mixpanel-ruby/tree/master/demo] 38 | * The documentation[http://mixpanel.github.io/mixpanel-ruby/] 39 | 40 | The official Mixpanel gem is built with simplicity and broad applicability in 41 | mind, but there are also third party Ruby libraries that can work with the library 42 | to provide useful features in common situations, and support different development 43 | points of view. 44 | 45 | In particular, for Rails apps, the following projects are currently actively maintained: 46 | 47 | * MetaEvents[https://github.com/swiftype/meta_events] 48 | * Mengpaneel[https://github.com/DouweM/mengpaneel] 49 | 50 | == Changes 51 | 52 | == 2.0.1 53 | * Add Deprecated version of Mixpanel::BufferedConsumer#send 54 | 55 | == 2.0.0 56 | * Raise mixpanel server and connection errors in Mixpanel::Consumer. 57 | * All public methods in Mixpanel::Event, Mixpanel::People, and subsequently Mixpanel::Tracker 58 | rescue Mixpanel errors and return false in the case of an error, return true otherwise 59 | * Deprecate Mixpanel::Consumer#send, replace with Mixpanel::Consumer#send! 60 | * Require ruby version minimum of 2.0.0 61 | 62 | == 1.4.0 63 | * Allow unset to unset multiple properties 64 | 65 | == 1.3.0 66 | * Added Consumer#request method, demo with Faraday integration 67 | 68 | == 1.2.0 69 | * All objects with a "strftime" method will be formatted as dates in 70 | people updates. 71 | 72 | == 1.1.0 73 | * The default consumer now sends requests (and expects responses) in 74 | verbose, JSON mode, which may improve error reporting. 75 | 76 | === 1.0.2 77 | * Allow ip and optional_params arguments to be accepted by all 78 | Mixpanel::People methods (except #destroy_user) 79 | 80 | === 1.0.1 81 | * Compatibility with earlier versions of ruby. Library development will continue 82 | to target 1.9, so later versions may not be compatible with Ruby 1.8, but we 83 | love patches! 84 | 85 | === 1.0.0 86 | * tracker#import added 87 | * Change to internal tracking message format. Messages written 88 | by earlier versions of the library will not work with 1.0.0 consumer classes. 89 | * alias bugfixed 90 | * Fixes to tests to allow for different timezones 91 | * Support for optional/experimental people api properties in people calls 92 | -------------------------------------------------------------------------------- /spec/mixpanel-ruby/tracker_spec.rb: -------------------------------------------------------------------------------- 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 | allow(Time).to receive(:now).and_return(@time_now) 10 | end 11 | 12 | it 'should send an alias message to mixpanel no matter what the consumer is' do 13 | WebMock.reset! 14 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 15 | mixpanel = Mixpanel::Tracker.new('TEST TOKEN') {|*args| } 16 | mixpanel.alias('TEST ALIAS', 'TEST ID') 17 | 18 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track'). 19 | with(:body => {:data => 'eyJldmVudCI6IiRjcmVhdGVfYWxpYXMiLCJwcm9wZXJ0aWVzIjp7ImRpc3RpbmN0X2lkIjoiVEVTVCBJRCIsImFsaWFzIjoiVEVTVCBBTElBUyIsInRva2VuIjoiVEVTVCBUT0tFTiJ9fQ==', 'verbose' => '1'}) 20 | end 21 | 22 | it 'should send a request to the track api with the default consumer' do 23 | WebMock.reset! 24 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 25 | stub_request(:any, 'https://api.mixpanel.com/engage').to_return({:body => '{"status": 1, "error": null}'}) 26 | mixpanel = Mixpanel::Tracker.new('TEST TOKEN') 27 | 28 | mixpanel.track('TEST ID', 'TEST EVENT', {'Circumstances' => 'During test'}) 29 | 30 | body = nil 31 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track'). 32 | with { |req| body = req.body } 33 | 34 | message_urlencoded = body[/^data=(.*?)(?:&|$)/, 1] 35 | message_json = Base64.strict_decode64(URI.unescape(message_urlencoded)) 36 | message = JSON.load(message_json) 37 | expect(message).to eq({ 38 | 'event' => 'TEST EVENT', 39 | 'properties' => { 40 | 'Circumstances' => 'During test', 41 | 'distinct_id' => 'TEST ID', 42 | 'mp_lib' => 'ruby', 43 | '$lib_version' => Mixpanel::VERSION, 44 | 'token' => 'TEST TOKEN', 45 | 'time' => @time_now.to_i 46 | } 47 | }) 48 | end 49 | 50 | it 'should call a consumer block if one is given' do 51 | messages = [] 52 | mixpanel = Mixpanel::Tracker.new('TEST TOKEN') do |type, message| 53 | messages << [type, JSON.load(message)] 54 | end 55 | mixpanel.track('ID', 'Event') 56 | mixpanel.import('API_KEY', 'ID', 'Import') 57 | mixpanel.people.set('ID', {'k' => 'v'}) 58 | mixpanel.people.append('ID', {'k' => 'v'}) 59 | 60 | expect = [ 61 | [ :event, 'data' => 62 | { 'event' => 'Event', 63 | 'properties' => { 64 | 'distinct_id' => 'ID', 65 | 'mp_lib' => 'ruby', 66 | '$lib_version' => Mixpanel::VERSION, 67 | 'token' => 'TEST TOKEN', 68 | 'time' => @time_now.to_i 69 | } 70 | } 71 | ], 72 | [ :import, { 73 | 'data' => { 74 | 'event' => 'Import', 75 | 'properties' => { 76 | 'distinct_id' => 'ID', 77 | 'mp_lib' => 'ruby', 78 | '$lib_version' => Mixpanel::VERSION, 79 | 'token' => 'TEST TOKEN', 80 | 'time' => @time_now.to_i 81 | } 82 | }, 83 | 'api_key' => 'API_KEY', 84 | } 85 | ], 86 | [ :profile_update, 'data' => 87 | { '$token' => 'TEST TOKEN', 88 | '$distinct_id' => 'ID', 89 | '$time' => @time_now.to_i * 1000, 90 | '$set' => {'k' => 'v'} 91 | } 92 | ], 93 | [ :profile_update, 'data' => 94 | { '$token' => 'TEST TOKEN', 95 | '$distinct_id' => 'ID', 96 | '$time' => @time_now.to_i * 1000, 97 | '$append' => {'k' => 'v'} 98 | } 99 | ] 100 | ] 101 | expect.zip(messages).each do |expect, found| 102 | expect(expect).to eq(found) 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/mixpanel-ruby/events.rb: -------------------------------------------------------------------------------- 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 | 26 | if block 27 | @sink = block 28 | else 29 | consumer = Consumer.new 30 | @sink = consumer.method(:send!) 31 | end 32 | end 33 | 34 | # Notes that an event has occurred, along with a distinct_id 35 | # representing the source of that event (for example, a user id), 36 | # an event name describing the event and a set of properties 37 | # describing that event. Properties are provided as a Hash with 38 | # string keys and strings, numbers or booleans as values. 39 | # 40 | # tracker = Mixpanel::Tracker.new 41 | # 42 | # # Track that user "12345"'s credit card was declined 43 | # tracker.track("12345", "Credit Card Declined") 44 | # 45 | # # Properties describe the circumstances of the event, 46 | # # or aspects of the source or user associated with the event 47 | # tracker.track("12345", "Welcome Email Sent", { 48 | # 'Email Template' => 'Pretty Pink Welcome', 49 | # 'User Sign-up Cohort' => 'July 2013' 50 | # }) 51 | def track(distinct_id, event, properties={}, ip=nil) 52 | properties = { 53 | 'distinct_id' => distinct_id, 54 | 'token' => @token, 55 | 'time' => Time.now.to_i, 56 | 'mp_lib' => 'ruby', 57 | '$lib_version' => Mixpanel::VERSION, 58 | }.merge(properties) 59 | properties['ip'] = ip if ip 60 | 61 | data = { 62 | 'event' => event, 63 | 'properties' => properties, 64 | } 65 | 66 | message = {'data' => data} 67 | 68 | ret = true 69 | begin 70 | @sink.call(:event, message.to_json) 71 | rescue MixpanelError 72 | ret = false 73 | end 74 | 75 | ret 76 | end 77 | 78 | # Imports an event that has occurred in the past, along with a distinct_id 79 | # representing the source of that event (for example, a user id), 80 | # an event name describing the event and a set of properties 81 | # describing that event. Properties are provided as a Hash with 82 | # string keys and strings, numbers or booleans as values. 83 | # 84 | # tracker = Mixpanel::Tracker.new 85 | # 86 | # # Track that user "12345"'s credit card was declined 87 | # tracker.import("API_KEY", "12345", "Credit Card Declined") 88 | # 89 | # # Properties describe the circumstances of the event, 90 | # # or aspects of the source or user associated with the event 91 | # tracker.import("API_KEY", "12345", "Welcome Email Sent", { 92 | # 'Email Template' => 'Pretty Pink Welcome', 93 | # 'User Sign-up Cohort' => 'July 2013' 94 | # }) 95 | def import(api_key, distinct_id, event, properties={}, ip=nil) 96 | properties = { 97 | 'distinct_id' => distinct_id, 98 | 'token' => @token, 99 | 'time' => Time.now.to_i, 100 | 'mp_lib' => 'ruby', 101 | '$lib_version' => Mixpanel::VERSION, 102 | }.merge(properties) 103 | properties['ip'] = ip if ip 104 | 105 | data = { 106 | 'event' => event, 107 | 'properties' => properties, 108 | } 109 | 110 | message = { 111 | 'data' => data, 112 | 'api_key' => api_key, 113 | } 114 | 115 | ret = true 116 | begin 117 | @sink.call(:import, message.to_json) 118 | rescue MixpanelError 119 | ret = false 120 | end 121 | 122 | ret 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/mixpanel-ruby/tracker.rb: -------------------------------------------------------------------------------- 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 | # You can find your project token in the settings dialog for your 17 | # project, inside of the Mixpanel web application. 18 | # 19 | # Mixpanel::Tracker is a subclass of Mixpanel::Events, and exposes 20 | # an instance of Mixpanel::People as Tracker#people 21 | class Tracker < Events 22 | # An instance of Mixpanel::People. Use this to 23 | # send profile updates 24 | attr_reader :people 25 | 26 | # Takes your Mixpanel project token, as a string. 27 | # 28 | # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN) 29 | # 30 | # By default, the tracker will send an message to Mixpanel 31 | # synchronously with each call, using an instance of Mixpanel::Consumer. 32 | # 33 | # You can also provide a block to the constructor 34 | # to specify particular consumer behaviors (for 35 | # example, if you wanted to write your messages to 36 | # a queue instead of sending them directly to Mixpanel) 37 | # 38 | # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN) do |type, message| 39 | # @kestrel.set(MY_MIXPANEL_QUEUE, [type,message].to_json) 40 | # end 41 | # 42 | # If a block is provided, it is passed a type (one of :event or :profile_update) 43 | # and a string message. This same format is accepted by Mixpanel::Consumer#send! 44 | # and Mixpanel::BufferedConsumer#send! 45 | def initialize(token, &block) 46 | super(token, &block) 47 | @token = token 48 | @people = People.new(token, &block) 49 | end 50 | 51 | # A call to #track is a report that an event has occurred. #track 52 | # takes a distinct_id representing the source of that event (for 53 | # example, a user id), an event name describing the event, and a 54 | # set of properties describing that event. Properties are provided 55 | # as a Hash with string keys and strings, numbers or booleans as 56 | # values. 57 | # 58 | # tracker = Mixpanel::Tracker.new 59 | # 60 | # # Track that user "12345"'s credit card was declined 61 | # tracker.track("12345", "Credit Card Declined") 62 | # 63 | # # Properties describe the circumstances of the event, 64 | # # or aspects of the source or user associated with the event 65 | # tracker.track("12345", "Welcome Email Sent", { 66 | # 'Email Template' => 'Pretty Pink Welcome', 67 | # 'User Sign-up Cohort' => 'July 2013' 68 | # }) 69 | def track(distinct_id, event, properties={}, ip=nil) 70 | # This is here strictly to allow rdoc to include the relevant 71 | # documentation 72 | super 73 | end 74 | 75 | # A call to #import is to import an event occurred in the past. #import 76 | # takes a distinct_id representing the source of that event (for 77 | # example, a user id), an event name describing the event, and a 78 | # set of properties describing that event. Properties are provided 79 | # as a Hash with string keys and strings, numbers or booleans as 80 | # values. 81 | # 82 | # tracker = Mixpanel::Tracker.new 83 | # 84 | # # Import event that user "12345"'s credit card was declined 85 | # tracker.import("API_KEY", "12345", "Credit Card Declined", { 86 | # 'time' => 1310111365 87 | # }) 88 | # 89 | # # Properties describe the circumstances of the event, 90 | # # or aspects of the source or user associated with the event 91 | # tracker.import("API_KEY", "12345", "Welcome Email Sent", { 92 | # 'Email Template' => 'Pretty Pink Welcome', 93 | # 'User Sign-up Cohort' => 'July 2013', 94 | # 'time' => 1310111365 95 | # }) 96 | def import(api_key, distinct_id, event, properties={}, ip=nil) 97 | # This is here strictly to allow rdoc to include the relevant 98 | # documentation 99 | super 100 | end 101 | 102 | # Creates a distinct_id alias. \Events and updates with an alias 103 | # will be considered by mixpanel to have the same source, and 104 | # refer to the same profile. 105 | # 106 | # Multiple aliases can map to the same real_id, once a real_id is 107 | # used to track events or send updates, it should never be used as 108 | # an alias itself. 109 | # 110 | # Alias requests are always sent synchronously, directly to 111 | # the \Mixpanel service, regardless of how the tracker is configured. 112 | def alias(alias_id, real_id, events_endpoint=nil) 113 | consumer = Mixpanel::Consumer.new(events_endpoint) 114 | data = { 115 | 'event' => '$create_alias', 116 | 'properties' => { 117 | 'distinct_id' => real_id, 118 | 'alias' => alias_id, 119 | 'token' => @token, 120 | } 121 | } 122 | 123 | message = {'data' => data} 124 | 125 | ret = true 126 | begin 127 | consumer.send!(:event, message.to_json) 128 | rescue MixpanelError 129 | ret = false 130 | end 131 | 132 | ret 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/mixpanel-ruby/people_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'mixpanel-ruby/people' 3 | 4 | describe Mixpanel::People do 5 | before(:each) do 6 | @time_now = Time.parse('Jun 6 1972, 16:23:04') 7 | allow(Time).to receive(:now).and_return(@time_now) 8 | 9 | @log = [] 10 | @people = Mixpanel::People.new('TEST TOKEN') do |type, message| 11 | @log << [type, JSON.load(message)] 12 | end 13 | end 14 | 15 | it 'should send a well formed engage/set message' do 16 | @people.set("TEST ID", { 17 | '$firstname' => 'David', 18 | '$lastname' => 'Bowie', 19 | }) 20 | expect(@log).to eq([[:profile_update, 'data' => { 21 | '$token' => 'TEST TOKEN', 22 | '$distinct_id' => 'TEST ID', 23 | '$time' => @time_now.to_i * 1000, 24 | '$set' => { 25 | '$firstname' => 'David', 26 | '$lastname' => 'Bowie' 27 | } 28 | }]]) 29 | end 30 | 31 | it 'should properly cast dates' do 32 | @people.set("TEST ID", { 33 | 'created_at' => DateTime.new(2013, 1, 2, 3, 4, 5) 34 | }) 35 | expect(@log).to eq([[:profile_update, 'data' => { 36 | '$token' => 'TEST TOKEN', 37 | '$distinct_id' => 'TEST ID', 38 | '$time' => @time_now.to_i * 1000, 39 | '$set' => { 40 | 'created_at' => '2013-01-02T03:04:05' 41 | } 42 | }]]) 43 | end 44 | 45 | it 'should convert offset datetimes to UTC' do 46 | @people.set("TEST ID", { 47 | 'created_at' => DateTime.new(2013, 1, 1, 18, 4, 5, '-9') 48 | }) 49 | expect(@log).to eq([[:profile_update, 'data' => { 50 | '$token' => 'TEST TOKEN', 51 | '$distinct_id' => 'TEST ID', 52 | '$time' => @time_now.to_i * 1000, 53 | '$set' => { 54 | 'created_at' => '2013-01-02T03:04:05' 55 | } 56 | }]]) 57 | end 58 | 59 | it 'should send a well formed engage/set_once message' do 60 | @people.set_once("TEST ID", { 61 | '$firstname' => 'David', 62 | '$lastname' => 'Bowie', 63 | }) 64 | expect(@log).to eq([[:profile_update, 'data' => { 65 | '$token' => 'TEST TOKEN', 66 | '$distinct_id' => 'TEST ID', 67 | '$time' => @time_now.to_i * 1000, 68 | '$set_once' => { 69 | '$firstname' => 'David', 70 | '$lastname' => 'Bowie' 71 | } 72 | }]]) 73 | end 74 | 75 | it 'should send a well formed engage/add message' do 76 | @people.increment("TEST ID", {'Albums Released' => 10}) 77 | expect(@log).to eq([[:profile_update, 'data' => { 78 | '$token' => 'TEST TOKEN', 79 | '$distinct_id' => 'TEST ID', 80 | '$time' => @time_now.to_i * 1000, 81 | '$add' => { 82 | 'Albums Released' => 10 83 | } 84 | }]]) 85 | end 86 | 87 | it 'should send an engage/add message with a value of 1' do 88 | @people.plus_one("TEST ID", 'Albums Released') 89 | expect(@log).to eq([[:profile_update, 'data' => { 90 | '$token' => 'TEST TOKEN', 91 | '$distinct_id' => 'TEST ID', 92 | '$time' => @time_now.to_i * 1000, 93 | '$add' => { 94 | 'Albums Released' => 1 95 | } 96 | }]]) 97 | end 98 | 99 | it 'should send a well formed engage/append message' do 100 | @people.append("TEST ID", {'Albums' => 'Diamond Dogs'}) 101 | expect(@log).to eq([[:profile_update, 'data' => { 102 | '$token' => 'TEST TOKEN', 103 | '$distinct_id' => 'TEST ID', 104 | '$time' => @time_now.to_i * 1000, 105 | '$append' => { 106 | 'Albums' => 'Diamond Dogs' 107 | } 108 | }]]) 109 | end 110 | 111 | it 'should send a well formed engage/union message' do 112 | @people.union("TEST ID", {'Albums' => ['Diamond Dogs']}) 113 | expect(@log).to eq([[:profile_update, 'data' => { 114 | '$token' => 'TEST TOKEN', 115 | '$distinct_id' => 'TEST ID', 116 | '$time' => @time_now.to_i * 1000, 117 | '$union' => { 118 | 'Albums' => ['Diamond Dogs'] 119 | } 120 | }]]) 121 | end 122 | 123 | it 'should send a well formed unset message' do 124 | @people.unset('TEST ID', 'Albums') 125 | expect(@log).to eq([[:profile_update, 'data' => { 126 | '$token' => 'TEST TOKEN', 127 | '$distinct_id' => 'TEST ID', 128 | '$time' => @time_now.to_i * 1000, 129 | '$unset' => ['Albums'] 130 | }]]) 131 | end 132 | 133 | it 'should send a well formed unset message with multiple properties' do 134 | @people.unset('TEST ID', ['Albums', 'Vinyls']) 135 | expect(@log).to eq([[:profile_update, 'data' => { 136 | '$token' => 'TEST TOKEN', 137 | '$distinct_id' => 'TEST ID', 138 | '$time' => @time_now.to_i * 1000, 139 | '$unset' => ['Albums', 'Vinyls'] 140 | }]]) 141 | end 142 | 143 | it 'should send an engage/append with the right $transaction stuff' do 144 | @people.track_charge("TEST ID", 25.42, { 145 | '$time' => DateTime.new(1999,12,24,14, 02, 53), 146 | 'SKU' => '1234567' 147 | }) 148 | expect(@log).to eq([[:profile_update, 'data' => { 149 | '$token' => 'TEST TOKEN', 150 | '$distinct_id' => 'TEST ID', 151 | '$time' => @time_now.to_i * 1000, 152 | '$append' => { 153 | '$transactions' => { 154 | '$time' => '1999-12-24T14:02:53', 155 | 'SKU' => '1234567', 156 | '$amount' => 25.42 157 | } 158 | } 159 | }]]) 160 | end 161 | 162 | it 'should send a well formed engage/unset message for $transaction' do 163 | @people.clear_charges("TEST ID") 164 | expect(@log).to eq([[:profile_update, 'data' => { 165 | '$token' => 'TEST TOKEN', 166 | '$distinct_id' => 'TEST ID', 167 | '$time' => @time_now.to_i * 1000, 168 | '$unset' => ['$transactions'] 169 | }]]) 170 | end 171 | 172 | it 'should send a well formed engage/delete message' do 173 | @people.delete_user("TEST ID") 174 | expect(@log).to eq([[:profile_update, 'data' => { 175 | '$token' => 'TEST TOKEN', 176 | '$distinct_id' => 'TEST ID', 177 | '$time' => @time_now.to_i * 1000, 178 | '$delete' => '' 179 | }]]) 180 | end 181 | 182 | it 'should send a well formed engage/delete message with blank optional_params' do 183 | @people.delete_user("TEST ID", {}) 184 | expect(@log).to eq([[:profile_update, 'data' => { 185 | '$token' => 'TEST TOKEN', 186 | '$distinct_id' => 'TEST ID', 187 | '$time' => @time_now.to_i * 1000, 188 | '$delete' => '' 189 | }]]) 190 | end 191 | 192 | it 'should send a well formed engage/delete message with ignore_alias true' do 193 | @people.delete_user("TEST ID", {"$ignore_alias"=>true}) 194 | expect(@log).to eq([[:profile_update, 'data' => { 195 | '$token' => 'TEST TOKEN', 196 | '$distinct_id' => 'TEST ID', 197 | '$time' => @time_now.to_i * 1000, 198 | '$delete' => '', 199 | "$ignore_alias"=>true 200 | }]]) 201 | end 202 | 203 | end 204 | -------------------------------------------------------------------------------- /spec/mixpanel-ruby/consumer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'webmock' 3 | require 'base64' 4 | require 'mixpanel-ruby/consumer' 5 | 6 | describe Mixpanel::Consumer do 7 | before { WebMock.reset! } 8 | 9 | shared_examples_for 'consumer' do 10 | it 'should send a request to api.mixpanel.com/track on events' do 11 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 12 | subject.send!(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json) 13 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track'). 14 | with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' }) 15 | end 16 | 17 | it 'should send a request to api.mixpanel.com/people on profile updates' do 18 | stub_request(:any, 'https://api.mixpanel.com/engage').to_return({:body => '{"status": 1, "error": null}'}) 19 | subject.send!(:profile_update, {'data' => 'TEST EVENT MESSAGE'}.to_json) 20 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/engage'). 21 | with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' }) 22 | end 23 | 24 | it 'should send a request to api.mixpanel.com/import on event imports' do 25 | stub_request(:any, 'https://api.mixpanel.com/import').to_return({:body => '{"status": 1, "error": null}'}) 26 | subject.send!(:import, {'data' => 'TEST EVENT MESSAGE', 'api_key' => 'API_KEY','verbose' => '1' }.to_json) 27 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/import'). 28 | with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'api_key' => 'API_KEY', 'verbose' => '1' }) 29 | end 30 | 31 | it 'should encode long messages without newlines' do 32 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 33 | 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) 34 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track'). 35 | with(:body => {'data' => 'IkJBU0U2NC1FTkNPREVEIFZFUlNJT04gT0YgQklOLiBUSElTIE1FVEhPRCBDT01QTElFUyBXSVRIIFJGQyAyMDQ1LiBMSU5FIEZFRURTIEFSRSBBRERFRCBUTyBFVkVSWSA2MCBFTkNPREVEIENIQVJBQ1RPUlMuIElOIFJVQlkgMS44IFdFIE5FRUQgVE8gSlVTVCBDQUxMIEVOQ09ERTY0IEFORCBSRU1PVkUgVEhFIExJTkUgRkVFRFMsIElOIFJVQlkgMS45IFdFIENBTEwgU1RSSUNfRU5DT0RFRDY0IE1FVEhPRCBJTlNURUFEIg==', 'verbose' => '1'}) 36 | end 37 | 38 | it 'should provide thorough information in case mixpanel fails' do 39 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:status => 401, :body => "nutcakes"}) 40 | expect { subject.send!(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json) }.to raise_exception('Could not write to Mixpanel, server responded with 401 returning: \'nutcakes\'') 41 | end 42 | 43 | it 'should still respond to send' do 44 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 45 | subject.send(:event, {'data' => 'TEST EVENT MESSAGE'}.to_json) 46 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track'). 47 | with(:body => {'data' => 'IlRFU1QgRVZFTlQgTUVTU0FHRSI=', 'verbose' => '1' }) 48 | end 49 | end 50 | 51 | context 'raw consumer' do 52 | it_behaves_like 'consumer' 53 | end 54 | 55 | context 'custom request consumer' do 56 | subject do 57 | ret = Mixpanel::Consumer.new 58 | class << ret 59 | attr_reader :called 60 | def request(*args) 61 | @called = true 62 | super(*args) 63 | end 64 | end 65 | 66 | ret 67 | end 68 | 69 | after(:each) do 70 | expect(subject.called).to be_truthy 71 | end 72 | 73 | it_behaves_like 'consumer' 74 | end 75 | 76 | end 77 | 78 | describe Mixpanel::BufferedConsumer do 79 | let(:max_length) { 10 } 80 | before { WebMock.reset! } 81 | 82 | context 'Default BufferedConsumer' do 83 | subject { Mixpanel::BufferedConsumer.new(nil, nil, nil, max_length) } 84 | 85 | it 'should not send a request for a single message until flush is called' do 86 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 87 | subject.send!(:event, {'data' => 'TEST EVENT 1'}.to_json) 88 | expect(WebMock).to have_not_requested(:post, 'https://api.mixpanel.com/track') 89 | 90 | subject.flush() 91 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track'). 92 | with(:body => {'data' => 'WyJURVNUIEVWRU5UIDEiXQ==', 'verbose' => '1' }) 93 | end 94 | 95 | it 'should still respond to send' do 96 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 97 | subject.send(:event, {'data' => 'TEST EVENT 1'}.to_json) 98 | expect(WebMock).to have_not_requested(:post, 'https://api.mixpanel.com/track') 99 | end 100 | 101 | it 'should send one message when max_length events are tracked' do 102 | stub_request(:any, 'https://api.mixpanel.com/track').to_return({:body => '{"status": 1, "error": null}'}) 103 | 104 | max_length.times do |i| 105 | subject.send!(:event, {'data' => "x #{i}"}.to_json) 106 | end 107 | 108 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/track'). 109 | with(:body => {'data' => 'WyJ4IDAiLCJ4IDEiLCJ4IDIiLCJ4IDMiLCJ4IDQiLCJ4IDUiLCJ4IDYiLCJ4IDciLCJ4IDgiLCJ4IDkiXQ==', 'verbose' => '1' }) 110 | end 111 | 112 | it 'should send one message per api key on import' do 113 | stub_request(:any, 'https://api.mixpanel.com/import').to_return({:body => '{"status": 1, "error": null}'}) 114 | subject.send!(:import, {'data' => 'TEST EVENT 1', 'api_key' => 'KEY 1'}.to_json) 115 | subject.send!(:import, {'data' => 'TEST EVENT 1', 'api_key' => 'KEY 2'}.to_json) 116 | subject.send!(:import, {'data' => 'TEST EVENT 2', 'api_key' => 'KEY 1'}.to_json) 117 | subject.send!(:import, {'data' => 'TEST EVENT 2', 'api_key' => 'KEY 2'}.to_json) 118 | subject.flush 119 | 120 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/import'). 121 | with(:body => {'data' => 'IlRFU1QgRVZFTlQgMSI=', 'api_key' => 'KEY 1', 'verbose' => '1' }) 122 | 123 | expect(WebMock).to have_requested(:post, 'https://api.mixpanel.com/import'). 124 | with(:body => {'data' => 'IlRFU1QgRVZFTlQgMSI=', 'api_key' => 'KEY 2', 'verbose' => '1' }) 125 | end 126 | end 127 | 128 | context 'BufferedConsumer with block' do 129 | let(:messages_seen) { [] } 130 | subject do 131 | Mixpanel::BufferedConsumer.new(nil, nil, nil, 3) do |type, message| 132 | messages_seen << [type, message] 133 | end 134 | end 135 | 136 | it 'should call block instead of making default requests on flush' do 137 | 3.times do |i| 138 | subject.send!(:event, {'data' => "x #{i}"}.to_json) 139 | end 140 | 141 | expect(messages_seen).to match_array( 142 | [[:event, "{\"data\":[\"x 0\",\"x 1\",\"x 2\"]}"]] 143 | ) 144 | end 145 | 146 | end 147 | 148 | end 149 | -------------------------------------------------------------------------------- /lib/mixpanel-ruby/people.rb: -------------------------------------------------------------------------------- 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(YOUR_MIXPANEL_TOKEN) 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 | 26 | if block 27 | @sink = block 28 | else 29 | consumer = Consumer.new 30 | @sink = consumer.method(:send!) 31 | end 32 | end 33 | 34 | # Sets properties on a user record. Takes a Hash with string 35 | # keys, and values that are strings, numbers, booleans, or 36 | # DateTimes 37 | # 38 | # tracker = Mixpanel::Tracker.new 39 | # # Sets properties on profile with id "1234" 40 | # tracker.people.set("1234", { 41 | # 'company' => 'Acme', 42 | # 'plan' => 'Premium', 43 | # 'Sign-Up Date' => DateTime.now 44 | # }); 45 | # 46 | # If you provide an ip argument, \Mixpanel will use that 47 | # ip address for geolocation (rather than the ip of your server) 48 | def set(distinct_id, properties, ip=nil, optional_params={}) 49 | properties = fix_property_dates(properties) 50 | message = { 51 | '$distinct_id' => distinct_id, 52 | '$set' => properties, 53 | }.merge(optional_params) 54 | message['$ip'] = ip if ip 55 | 56 | update(message) 57 | end 58 | 59 | # set_once works just like #set, but will only change the 60 | # value of properties if they are not already present 61 | # in the profile. That means you can call set_once many times 62 | # without changing an original value. 63 | # 64 | # tracker = Mixpanel::Tracker.new 65 | # tracker.people.set_once("12345", { 66 | # 'First Login Date': DateTime.now 67 | # }); 68 | # 69 | def set_once(distinct_id, properties, ip=nil, optional_params={}) 70 | properties = fix_property_dates(properties) 71 | message = { 72 | '$distinct_id' => distinct_id, 73 | '$set_once' => properties, 74 | }.merge(optional_params) 75 | message['$ip'] = ip if ip 76 | 77 | update(message) 78 | end 79 | 80 | # Changes the value of properties by a numeric amount. Takes a 81 | # hash with string keys and numeric properties. \Mixpanel will add 82 | # the given amount to whatever value is currently assigned to the 83 | # property. If no property exists with a given name, the value 84 | # will be added to zero. 85 | # 86 | # tracker = Mixpanel::Tracker.new 87 | # tracker.people.increment("12345", { 88 | # 'Coins Spent' => 7, 89 | # 'Coins Earned' => -7, # Use a negative number to subtract 90 | # }); 91 | # 92 | def increment(distinct_id, properties, ip=nil, optional_params={}) 93 | properties = fix_property_dates(properties) 94 | message = { 95 | '$distinct_id' => distinct_id, 96 | '$add' => properties, 97 | }.merge(optional_params) 98 | message['$ip'] = ip if ip 99 | 100 | update(message) 101 | end 102 | 103 | # Convenience method- increases the value of a numeric property 104 | # by one. Calling #plus_one(distinct_id, property_name) is the same as calling 105 | # #increment(distinct_id, {property_name => 1}) 106 | # 107 | # tracker = Mixpanel::Tracker.new 108 | # tracker.people.plus_one("12345", "Albums Released") 109 | # 110 | def plus_one(distinct_id, property_name, ip=nil, optional_params={}) 111 | increment(distinct_id, {property_name => 1}, ip, optional_params) 112 | end 113 | 114 | # Appends a values to the end of list-valued properties. 115 | # If the given properties don't exist, a new list-valued 116 | # property will be created. 117 | # 118 | # tracker = Mixpanel::Tracker.new 119 | # tracker.people.append("12345", { 120 | # 'Login Dates' => DateTime.now, 121 | # 'Alter Ego Names' => 'Ziggy Stardust' 122 | # }); 123 | # 124 | def append(distinct_id, properties, ip=nil, optional_params={}) 125 | properties = fix_property_dates(properties) 126 | message = { 127 | '$distinct_id' => distinct_id, 128 | '$append' => properties, 129 | }.merge(optional_params) 130 | message['$ip'] = ip if ip 131 | 132 | update(message) 133 | end 134 | 135 | # Set union on list valued properties. 136 | # Associates a list containing all elements of a given list, 137 | # and all elements currently in a list associated with the given 138 | # property. After a union, every element in the list associated 139 | # with a property will be unique. 140 | # 141 | # tracker = Mixpanel::Tracker.new 142 | # tracker.people.union("12345", { 143 | # 'Levels Completed' => ['Suffragette City'] 144 | # }); 145 | # 146 | def union(distinct_id, properties, ip=nil, optional_params={}) 147 | properties = fix_property_dates(properties) 148 | message = { 149 | '$distinct_id' => distinct_id, 150 | '$union' => properties, 151 | }.merge(optional_params) 152 | message['$ip'] = ip if ip 153 | 154 | update(message) 155 | end 156 | 157 | # Removes properties and their values from a profile. 158 | # 159 | # tracker = Mixpanel::Tracker.new 160 | # 161 | # # removes a single property and its value from a profile 162 | # tracker.people.unset("12345", "Overdue Since") 163 | # 164 | # # removes multiple properties and their values from a profile 165 | # tracker.people.unset("12345", ["Overdue Since", "Paid Date"]) 166 | # 167 | def unset(distinct_id, properties, ip=nil, optional_params={}) 168 | properties = [properties] unless properties.is_a?(Array) 169 | message = { 170 | '$distinct_id' => distinct_id, 171 | '$unset' => properties, 172 | }.merge(optional_params) 173 | message['$ip'] = ip if ip 174 | 175 | update(message) 176 | end 177 | 178 | # Records a payment to you to a profile. Charges recorded with 179 | # #track_charge will appear in the \Mixpanel revenue report. 180 | # 181 | # tracker = Mixpanel::Tracker.new 182 | # 183 | # # records a charge of $25.32 from user 12345 184 | # tracker.people.track_charge("12345", 25.32) 185 | # 186 | # # records a charge of $30.50 on the 2nd of January, 187 | # mixpanel.people.track_charge("12345", 30.50, { 188 | # '$time' => DateTime.parse("Jan 2 2013") 189 | # }) 190 | # 191 | def track_charge(distinct_id, amount, properties={}, ip=nil, optional_params={}) 192 | properties = fix_property_dates(properties) 193 | charge_properties = properties.merge({'$amount' => amount}) 194 | append(distinct_id, {'$transactions' => charge_properties}, ip, optional_params) 195 | end 196 | 197 | # Clear all charges from a \Mixpanel people profile 198 | def clear_charges(distinct_id, ip=nil, optional_params={}) 199 | unset(distinct_id, '$transactions', ip, optional_params) 200 | end 201 | 202 | # Permanently delete a profile from \Mixpanel people analytics 203 | # To delete a user and ignore alias pass into optional params 204 | # {"$ignore_alias"=>true} 205 | def delete_user(distinct_id, optional_params={}) 206 | update({ 207 | '$distinct_id' => distinct_id, 208 | '$delete' => '', 209 | }.merge(optional_params)) 210 | end 211 | 212 | # Send a generic update to \Mixpanel people analytics. 213 | # Caller is responsible for formatting the update message, as 214 | # documented in the \Mixpanel HTTP specification, and passing 215 | # the message as a dict to #update. This 216 | # method might be useful if you want to use very new 217 | # or experimental features of people analytics from Ruby 218 | # The \Mixpanel HTTP tracking API is documented at 219 | # https://mixpanel.com/help/reference/http 220 | def update(message) 221 | data = { 222 | '$token' => @token, 223 | '$time' => ((Time.now.to_f) * 1000.0).to_i, 224 | }.merge(message) 225 | 226 | message = {'data' => data} 227 | 228 | ret = true 229 | begin 230 | @sink.call(:profile_update, message.to_json) 231 | rescue MixpanelError 232 | ret = false 233 | end 234 | 235 | ret 236 | end 237 | 238 | private 239 | 240 | def fix_property_dates(h) 241 | h.inject({}) do |ret,(k,v)| 242 | v = v.respond_to?(:new_offset) ? v.new_offset('0') : v 243 | ret[k] = v.respond_to?(:strftime) ? v.strftime('%Y-%m-%dT%H:%M:%S') : v 244 | ret 245 | end 246 | end 247 | end 248 | end 249 | -------------------------------------------------------------------------------- /lib/mixpanel-ruby/consumer.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'net/https' 3 | require 'json' 4 | 5 | module Mixpanel 6 | class MixpanelError < StandardError 7 | end 8 | 9 | class ConnectionError < MixpanelError 10 | end 11 | 12 | class ServerError < MixpanelError 13 | end 14 | 15 | @@init_http = nil 16 | 17 | # This method exists for backwards compatibility. The preferred 18 | # way to customize or configure the HTTP library of a consumer 19 | # is to override Consumer#request. 20 | # 21 | # Ruby's default SSL does not verify the server certificate. 22 | # To verify a certificate, or install a proxy, pass a block 23 | # to Mixpanel.config_http that configures the Net::HTTP object. 24 | # For example, if running in Ubuntu Linux, you can run 25 | # 26 | # Mixpanel.config_http do |http| 27 | # http.ca_path = '/etc/ssl/certs' 28 | # http.ca_file = '/etc/ssl/certs/ca-certificates.crt' 29 | # http.verify_mode = OpenSSL::SSL::VERIFY_PEER 30 | # end 31 | # 32 | # \Mixpanel Consumer and BufferedConsumer will call your block 33 | # to configure their connections 34 | def self.config_http(&block) 35 | @@init_http = block 36 | end 37 | 38 | # A Consumer recieves messages from a Mixpanel::Tracker, and 39 | # sends them elsewhere- probably to Mixpanel's analytics services, 40 | # but can also enqueue them for later processing, log them to a 41 | # file, or do whatever else you might find useful. 42 | # 43 | # You can provide your own consumer to your Mixpanel::Trackers, 44 | # either by passing in an argument with a #send! method when you construct 45 | # the tracker, or just passing a block to Mixpanel::Tracker.new 46 | # 47 | # tracker = Mixpanel::Tracker.new(MY_TOKEN) do |type, message| 48 | # # type will be one of :event, :profile_update or :import 49 | # @kestrel.set(ANALYTICS_QUEUE, [type, message].to_json) 50 | # end 51 | # 52 | # You can also instantiate the library consumers yourself, and use 53 | # them wherever you would like. For example, the working that 54 | # consumes the above queue might work like this: 55 | # 56 | # mixpanel = Mixpanel::Consumer 57 | # while true 58 | # message_json = @kestrel.get(ANALYTICS_QUEUE) 59 | # mixpanel.send!(*JSON.load(message_json)) 60 | # end 61 | # 62 | # Mixpanel::Consumer is the default consumer. It sends each message, 63 | # as the message is recieved, directly to Mixpanel. 64 | class Consumer 65 | 66 | # Create a Mixpanel::Consumer. If you provide endpoint arguments, 67 | # they will be used instead of the default Mixpanel endpoints. 68 | # This can be useful for proxying, debugging, or if you prefer 69 | # not to use SSL for your events. 70 | def initialize(events_endpoint=nil, update_endpoint=nil, import_endpoint=nil) 71 | @events_endpoint = events_endpoint || 'https://api.mixpanel.com/track' 72 | @update_endpoint = update_endpoint || 'https://api.mixpanel.com/engage' 73 | @import_endpoint = import_endpoint || 'https://api.mixpanel.com/import' 74 | end 75 | 76 | # Send the given string message to Mixpanel. Type should be 77 | # one of :event, :profile_update or :import, which will determine the endpoint. 78 | # 79 | # Mixpanel::Consumer#send! sends messages to Mixpanel immediately on 80 | # each call. To reduce the overall bandwidth you use when communicating 81 | # with Mixpanel, you can also use Mixpanel::BufferedConsumer 82 | def send!(type, message) 83 | type = type.to_sym 84 | endpoint = { 85 | :event => @events_endpoint, 86 | :profile_update => @update_endpoint, 87 | :import => @import_endpoint, 88 | }[type] 89 | 90 | decoded_message = JSON.load(message) 91 | api_key = decoded_message["api_key"] 92 | data = Base64.encode64(decoded_message["data"].to_json).gsub("\n", '') 93 | 94 | form_data = {"data" => data, "verbose" => 1} 95 | form_data.merge!("api_key" => api_key) if api_key 96 | 97 | begin 98 | response_code, response_body = request(endpoint, form_data) 99 | rescue => e 100 | raise ConnectionError.new("Could not connect to Mixpanel, with error \"#{e.message}\".") 101 | end 102 | 103 | succeeded = nil 104 | if response_code.to_i == 200 105 | result = JSON.load(response_body) rescue {} 106 | succeeded = result['status'] == 1 107 | end 108 | 109 | if !succeeded 110 | raise ServerError.new("Could not write to Mixpanel, server responded with #{response_code} returning: '#{response_body}'") 111 | end 112 | end 113 | 114 | # This method was deprecated in release 2.0.0, please use send! instead 115 | def send(type, message) 116 | warn '[DEPRECATION] send has been deprecated as of release 2.0.0, please use send! instead' 117 | send!(type, message) 118 | end 119 | 120 | # Request takes an endpoint HTTP or HTTPS url, and a Hash of data 121 | # to post to that url. It should return a pair of 122 | # 123 | # [response code, response body] 124 | # 125 | # as the result of the response. Response code should be nil if 126 | # the request never recieves a response for some reason. 127 | def request(endpoint, form_data) 128 | uri = URI(endpoint) 129 | request = Net::HTTP::Post.new(uri.request_uri) 130 | request.set_form_data(form_data) 131 | 132 | client = Net::HTTP.new(uri.host, uri.port) 133 | client.use_ssl = true 134 | client.open_timeout = 2 135 | client.continue_timeout = 10 136 | client.read_timeout = 10 137 | client.ssl_timeout = 2 138 | 139 | Mixpanel.with_http(client) 140 | 141 | response = client.request(request) 142 | [response.code, response.body] 143 | end 144 | 145 | end 146 | 147 | # BufferedConsumer buffers messages in memory, and sends messages as 148 | # a batch. This can improve performance, but calls to #send! may 149 | # still block if the buffer is full. If you use this consumer, you 150 | # should call #flush when your application exits or the messages 151 | # remaining in the buffer will not be sent. 152 | # 153 | # To use a BufferedConsumer directly with a Mixpanel::Tracker, 154 | # instantiate your Tracker like this 155 | # 156 | # buffered_consumer = Mixpanel::BufferedConsumer.new 157 | # begin 158 | # buffered_tracker = Mixpanel::Tracker.new(YOUR_TOKEN) do |type, message| 159 | # buffered_consumer.send!(type, message) 160 | # end 161 | # # Do some tracking here 162 | # ... 163 | # ensure 164 | # buffered_consumer.flush 165 | # end 166 | # 167 | class BufferedConsumer 168 | MAX_LENGTH = 50 169 | 170 | # Create a Mixpanel::BufferedConsumer. If you provide endpoint arguments, 171 | # they will be used instead of the default Mixpanel endpoints. 172 | # This can be useful for proxying, debugging, or if you prefer 173 | # not to use SSL for your events. 174 | # 175 | # You can also change the preferred buffer size before the 176 | # consumer automatically sends its buffered events. The Mixpanel 177 | # endpoints have a limit of 50 events per HTTP request, but 178 | # you can lower the limit if your individual events are very large. 179 | # 180 | # By default, BufferedConsumer will use a standard Mixpanel 181 | # consumer to send the events once the buffer is full (or on calls 182 | # to #flush), but you can override this behavior by passing a 183 | # block to the constructor, in the same way you might pass a block 184 | # to the Mixpanel::Tracker constructor. If a block is passed to 185 | # the constructor, the *_endpoint constructor arguments are 186 | # ignored. 187 | def initialize(events_endpoint=nil, update_endpoint=nil, import_endpoint=nil, max_buffer_length=MAX_LENGTH, &block) 188 | @max_length = [max_buffer_length, MAX_LENGTH].min 189 | @buffers = { 190 | :event => [], 191 | :profile_update => [], 192 | } 193 | 194 | if block 195 | @sink = block 196 | else 197 | consumer = Consumer.new(events_endpoint, update_endpoint, import_endpoint) 198 | @sink = consumer.method(:send!) 199 | end 200 | end 201 | 202 | # Stores a message for Mixpanel in memory. When the buffer 203 | # hits a maximum length, the consumer will flush automatically. 204 | # Flushes are synchronous when they occur. 205 | # 206 | # Currently, only :event and :profile_update messages are buffered, 207 | # :import messages will be send immediately on call. 208 | def send!(type, message) 209 | type = type.to_sym 210 | 211 | if @buffers.has_key? type 212 | @buffers[type] << message 213 | flush_type(type) if @buffers[type].length >= @max_length 214 | else 215 | @sink.call(type, message) 216 | end 217 | end 218 | 219 | # This method was deprecated in release 2.0.0, please use send! instead 220 | def send(type, message) 221 | warn '[DEPRECATION] send has been deprecated as of release 2.0.0, please use send! instead' 222 | send!(type, message) 223 | end 224 | 225 | # Pushes all remaining messages in the buffer to Mixpanel. 226 | # You should call #flush before your application exits or 227 | # messages may not be sent. 228 | def flush 229 | @buffers.keys.each { |k| flush_type(k) } 230 | end 231 | 232 | private 233 | 234 | def flush_type(type) 235 | @buffers[type].each_slice(@max_length) do |chunk| 236 | data = chunk.map {|message| JSON.load(message)['data'] } 237 | @sink.call(type, {'data' => data}.to_json) 238 | end 239 | @buffers[type] = [] 240 | end 241 | end 242 | 243 | private 244 | 245 | def self.with_http(http) 246 | if @@init_http 247 | @@init_http.call(http) 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 --------------------------------------------------------------------------------