diff --git a/README.md b/README.md index 10619c4..238175d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Ruby access to the [Mixpanel](http://mixpanel.com/) web analytics tool. require 'rubygems' require 'mixpanel_client' - client = Mixpanel::Client.new('api_key' => 'changeme', 'api_secret' => 'changeme') + client = MixpanelClient.new('api_key' => 'changeme', 'api_secret' => 'changeme') data = client.request do resource 'events/retention' @@ -26,6 +26,12 @@ Ruby access to the [Mixpanel](http://mixpanel.com/) web analytics tool. puts data.inspect +## Collaborators and Maintainers +[Keolo Keagy](http://github.com/keolo) (Author) +[Nathan Chong](http://github.com/paramaw) +[Paul McMahon](http://github.com/pwim) +[Chad Etzel](http://github.com/jazzychad) + ## Copyright Copyright (c) 2009+ Keolo Keagy. See LICENSE for details. diff --git a/lib/mixpanel_client.rb b/lib/mixpanel_client.rb index c0c4448..b42c3a3 100644 --- a/lib/mixpanel_client.rb +++ b/lib/mixpanel_client.rb @@ -1,101 +1,2 @@ -#!/usr/bin/env ruby -Ku - -# Mixpanel API Ruby Client Library -# -# Copyright (c) 2009+ Keolo Keagy -# See LICENSE for details. -# -# Inspired by the official mixpanel php and python libraries. -# http://mixpanel.com/api/docs/guides/api/ - -require 'cgi' -require 'digest/md5' -require 'open-uri' -require 'json' unless defined?(JSON) - -# Ruby library for the mixpanel.com web service -module Mixpanel - BASE_URI = 'http://mixpanel.com/api' - VERSION = '2.0' - - # The mixpanel client can be used to easily consume data through the mixpanel API - class Client - OPTIONS = [:resource, :event, :funnel, :name, :type, :unit, :interval, :limit, :format, :bucket] - attr_reader :uri - attr_accessor :api_key, :api_secret - - OPTIONS.each do |option| - class_eval " - def #{option}(arg=nil) - arg ? @#{option} = arg : @#{option} - end - " - end - - def initialize(config) - @api_key = config['api_key'] - @api_secret = config['api_secret'] - end - - def params - OPTIONS.inject({}) do |params, param| - option = send(param) - params.merge!(param => option) if param != :resource && !option.nil? - params - end - end - - def request(&options) - reset_options - instance_eval(&options) - @uri = URI.mixpanel(resource, normalize_params(params)) - response = URI.get(@uri) - to_hash(response) - end - - def normalize_params(params) - params.merge!( - :api_key => @api_key, - :expire => Time.now.to_i + 600 # Grant this request 10 minutes - ).merge!(:sig => generate_signature(params)) - end - - def generate_signature(args) - Digest::MD5.hexdigest(args.map{|key,val| "#{key}=#{val}"}.sort.join + api_secret) - end - - def to_hash(data) - if @format == 'csv' - data - else - JSON.parse(data) - end - end - - def reset_options - (OPTIONS - [:resource]).each do |option| - eval "remove_instance_variable(:@#{option}) if defined?(@#{option})" - end - end - end - - # URI related helpers - class URI - # Create an http error class for us to use - class HTTPError < StandardError; end - - def self.mixpanel(resource, params) - File.join([BASE_URI, VERSION, resource.to_s]) + "?#{self.encode(params)}" - end - - def self.encode(params) - params.map{|key,val| "#{key}=#{CGI.escape(val.to_s)}"}.sort.join('&') - end - - def self.get(uri) - ::URI.parse(uri).read - rescue OpenURI::HTTPError => error - raise HTTPError, JSON.parse(error.io.read)['error'] - end - end -end +require "#{File.dirname(__FILE__)}/mixpanel_client/mixpanel_client" +require "#{File.dirname(__FILE__)}/mixpanel_client/mixpanel_client_uri" diff --git a/lib/mixpanel_client/mixpanel_client.rb b/lib/mixpanel_client/mixpanel_client.rb new file mode 100644 index 0000000..fe69218 --- /dev/null +++ b/lib/mixpanel_client/mixpanel_client.rb @@ -0,0 +1,79 @@ +#!/usr/bin/env ruby -Ku + +# Mixpanel API Ruby Client Library +# +# Copyright (c) 2009+ Keolo Keagy +# See LICENSE for details. +# +# Inspired by the official mixpanel php and python libraries. +# http://mixpanel.com/api/docs/guides/api/ + +require 'cgi' +require 'digest/md5' +require 'open-uri' +require 'json' unless defined?(JSON) + +# Ruby library for the mixpanel.com web service +class MixpanelClient + BASE_URI = 'http://mixpanel.com/api' + VERSION = '2.0' + + # The mixpanel client can be used to easily consume data through the mixpanel API + OPTIONS = [:resource, :event, :funnel, :name, :type, :unit, :interval, :limit, :format, :bucket] + attr_reader :uri + attr_accessor :api_key, :api_secret + + OPTIONS.each do |option| + class_eval " + def #{option}(arg=nil) + arg ? @#{option} = arg : @#{option} + end + " + end + + def initialize(config) + @api_key = config['api_key'] + @api_secret = config['api_secret'] + end + + def params + OPTIONS.inject({}) do |params, param| + option = send(param) + params.merge!(param => option) if param != :resource && !option.nil? + params + end + end + + def request(&options) + reset_options + instance_eval(&options) + @uri = URI.mixpanel(resource, normalize_params(params)) + response = URI.get(@uri) + to_hash(response) + end + + def normalize_params(params) + params.merge!( + :api_key => @api_key, + :expire => Time.now.to_i + 600 # Grant this request 10 minutes + ).merge!(:sig => generate_signature(params)) + end + + def generate_signature(args) + Digest::MD5.hexdigest(args.map{|key,val| "#{key}=#{val}"}.sort.join + api_secret) + end + + def to_hash(data) + if @format == 'csv' + data + else + JSON.parse(data) + end + end + + def reset_options + (OPTIONS - [:resource]).each do |option| + eval "remove_instance_variable(:@#{option}) if defined?(@#{option})" + end + end +end diff --git a/lib/mixpanel_client/mixpanel_client_uri.rb b/lib/mixpanel_client/mixpanel_client_uri.rb new file mode 100644 index 0000000..72e7bd2 --- /dev/null +++ b/lib/mixpanel_client/mixpanel_client_uri.rb @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby -Ku + +# Mixpanel API Ruby Client Library +# +# Copyright (c) 2009+ Keolo Keagy +# See LICENSE for details. +# +# Inspired by the official mixpanel php and python libraries. +# http://mixpanel.com/api/docs/guides/api/ + +# URI related helpers +class MixpanelClient::URI + # Create an http error class for us to use + class HTTPError < StandardError; end + + def self.mixpanel(resource, params) + File.join([MixpanelClient::BASE_URI, MixpanelClient::VERSION, resource.to_s]) + "?#{self.encode(params)}" + end + + def self.encode(params) + params.map{|key,val| "#{key}=#{CGI.escape(val.to_s)}"}.sort.join('&') + end + + def self.get(uri) + ::URI.parse(uri).read + rescue OpenURI::HTTPError => error + raise HTTPError, JSON.parse(error.io.read)['error'] + end +end diff --git a/spec/events_externalspec.rb b/spec/events_externalspec.rb index 7e9d287..f2de8db 100644 --- a/spec/events_externalspec.rb +++ b/spec/events_externalspec.rb @@ -7,7 +7,7 @@ before :all do config = YAML.load_file(File.dirname(__FILE__) + '/../config/mixpanel.yml') config.should_not be_nil - @client = Mixpanel::Client.new(config) + @client = MixpanelClient.new(config) end describe 'Events' do @@ -17,7 +17,7 @@ resource 'events' end } - data.should raise_error(Mixpanel::URI::HTTPError) + data.should raise_error(MixpanelClient::URI::HTTPError) end it 'should return events' do diff --git a/spec/mixpanel_client_spec.rb b/spec/mixpanel_client_spec.rb index 7bc25f8..ec4d3ce 100644 --- a/spec/mixpanel_client_spec.rb +++ b/spec/mixpanel_client_spec.rb @@ -1,11 +1,11 @@ require 'rubygems' require File.expand_path(File.dirname(__FILE__) + '/spec_helper') -describe Mixpanel::Client do +describe MixpanelClient do before :all do config = {'api_key' => 'test', 'api_secret' => 'test'} - @client = Mixpanel::Client.new(config) - @uri = Regexp.escape(Mixpanel::BASE_URI) + @client = MixpanelClient.new(config) + @uri = Regexp.escape(MixpanelClient::BASE_URI) end describe '#request' do @@ -96,7 +96,7 @@ bucket 'list' end - Mixpanel::Client::OPTIONS.each do |option| + MixpanelClient::OPTIONS.each do |option| @client.send(option).should_not be_nil end @@ -104,36 +104,36 @@ resource 'events/properties/top' end - (Mixpanel::Client::OPTIONS - [:resource]).each do |option| + (MixpanelClient::OPTIONS - [:resource]).each do |option| @client.send(option).should be_nil end end end end -describe Mixpanel::URI do +describe MixpanelClient::URI do describe '.mixpanel' do it 'should return a properly formatted mixpanel uri as a string (without an endpoint)' do resource, params = ['events', {:c => 'see', :a => 'aye'}] - Mixpanel::URI.mixpanel(resource, params).should == 'http://mixpanel.com/api/2.0/events?a=aye&c=see' + MixpanelClient::URI.mixpanel(resource, params).should == 'http://mixpanel.com/api/2.0/events?a=aye&c=see' end it 'should return a properly formatted mixpanel uri as a string (with an endpoint)' do resource, params = ['events/top', {:c => 'see', :a => 'aye'}] - Mixpanel::URI.mixpanel(resource, params).should == 'http://mixpanel.com/api/2.0/events/top?a=aye&c=see' + MixpanelClient::URI.mixpanel(resource, params).should == 'http://mixpanel.com/api/2.0/events/top?a=aye&c=see' end end describe '.encode' do it 'should return a string with url encoded values.' do params = {:hey => '!@#$%^&*()\/"Ü', :soo => "hëllö?"} - Mixpanel::URI.encode(params).should == 'hey=%21%40%23%24%25%5E%26%2A%28%29%5C%2F%22%C3%9C&soo=h%C3%ABll%C3%B6%3F' + MixpanelClient::URI.encode(params).should == 'hey=%21%40%23%24%25%5E%26%2A%28%29%5C%2F%22%C3%9C&soo=h%C3%ABll%C3%B6%3F' end end describe '.get' do it 'should return a string response' do stub_request(:get, 'http://example.com').to_return(:body => 'something') - Mixpanel::URI.get('http://example.com').should == 'something' + MixpanelClient::URI.get('http://example.com').should == 'something' end end end