├── .codeclimate.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .document ├── .gitignore ├── .rubocop.yml ├── Gemfile ├── Rakefile ├── changelog.md ├── config └── mixpanel.template.yml ├── development.md ├── lib ├── mixpanel │ ├── client.rb │ ├── exceptions.rb │ ├── uri.rb │ ├── utils.rb │ └── version.rb └── mixpanel_client.rb ├── license ├── manual_test └── basic.rb ├── mixpanel_client.gemspec ├── readme.md └── spec ├── mixpanel_client ├── events_externalspec.rb ├── mixpanel_client_spec.rb ├── properties_externalspec.rb └── uri_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | fixme: 9 | enabled: true 10 | rubocop: 11 | enabled: true 12 | markdownlint: 13 | enabled: true 14 | reek: 15 | enabled: true 16 | ratings: 17 | paths: 18 | - "**.rb" 19 | exclude_paths: 20 | - config/ 21 | - spec/ 22 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3 2 | 3 | ARG USERNAME=devcontainer 4 | ARG USER_UID=1000 5 | ARG USER_GID=$USER_UID 6 | 7 | # Install basic development tools 8 | RUN apt update && apt install -y less man-db sudo 9 | 10 | # Set up unprivileged local user 11 | RUN groupadd --gid $USER_GID $USERNAME \ 12 | && groupadd bundler \ 13 | && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME --shell /bin/bash --groups bundler \ 14 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ 15 | && chmod 0440 /etc/sudoers.d/$USERNAME 16 | 17 | # Set unprivileged user as default user 18 | USER $USERNAME 19 | 20 | # Set `DEVCONTAINER` environment variable to help with orientation 21 | ENV DEVCONTAINER=true 22 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://containers.dev/implementors/json_reference/ for configuration reference 2 | { 3 | "name": "mixpanel_client", 4 | "build": { 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "remoteUser": "devcontainer" 8 | } 9 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swo 15 | *.swp 16 | 17 | ## PROJECT::GENERAL 18 | config/mixpanel.yml 19 | coverage 20 | rdoc 21 | pkg 22 | doc 23 | .yardoc 24 | 25 | ## PROJECT::SPECIFIC 26 | test/ 27 | tmp/ 28 | 29 | ## Bundler/Gems 30 | *.gem 31 | .bundle 32 | Gemfile.lock 33 | pkg/* 34 | bin/* 35 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # For now we're ignoring rules in the following file. We should gradually 2 | # remove rules over time so that we don't need this file anymore. 3 | #inherit_from: rubocop-todo.yml 4 | 5 | AllCops: 6 | Include: 7 | - Rakefile 8 | Exclude: 9 | - bin/** 10 | NewCops: enable 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in mixpanel_client.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'rake' 5 | 6 | require 'bundler' 7 | Bundler::GemHelper.install_tasks 8 | 9 | require 'rspec/core/rake_task' 10 | RSpec::Core::RakeTask.new(:spec) do |spec| 11 | spec.pattern = 'spec/**/*_spec.rb' 12 | spec.rspec_opts = ['--color'] 13 | end 14 | 15 | namespace :spec do 16 | desc 'Run all tests that depend on external dependencies' 17 | RSpec::Core::RakeTask.new(:externals) do |t| 18 | t.pattern = 'spec/**/*_externalspec.rb' 19 | t.rspec_opts = ['--color'] 20 | end 21 | end 22 | 23 | RSpec::Core::RakeTask.new(:rcov) do |spec| 24 | spec.pattern = 'spec/**/*_spec.rb' 25 | spec.rcov = true 26 | end 27 | 28 | desc 'Run Rspec test suite' 29 | task :spec 30 | task default: :spec 31 | 32 | require 'rdoc/task' 33 | Rake::RDocTask.new do |rdoc| 34 | version = File.exist?('VERSION') ? File.read('VERSION') : '' 35 | rdoc.rdoc_dir = 'rdoc' 36 | rdoc.title = "mixpanel_client #{version}" 37 | rdoc.rdoc_files.include('README*') 38 | rdoc.rdoc_files.include('lib/**/*.rb') 39 | end 40 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.1.0 4 | 5 | - Merged #63: Allow URI overrides 6 | - Fixed #40: Added minimum supported Ruby version 7 | 8 | ## 5.0.0 9 | 10 | - Removed parallel option in favor of having no runtime dependencies (fixes #58) 11 | - Updated `expect` to use block instead of an argument 12 | 13 | ## 4.1.6 14 | 15 | - Remove deprecated authentication options. Fixes #55 16 | - Use `expect` rspec syntax 17 | - Added codeclimate config 18 | 19 | ## 4.1.5 20 | 21 | - Use new authentication method for mixpanel. Fixes #52 22 | - Use `expect` rspec syntax 23 | 24 | ## 4.1.4 25 | 26 | - Add timeout option: Merged PR #48 27 | 28 | ## 4.1.3 29 | 30 | - Make request options optional. Closes #46 31 | 32 | ## 4.1.2 33 | 34 | - Removes typhoeus version lock, fixes a "broken" test 35 | 36 | ## 4.1.1 37 | 38 | - Add raw response 39 | 40 | ## 4.1.0 41 | 42 | - Drop support for config keys to be strings. Use symbols instead 43 | - Fixed some rubocop offences 44 | - Require Typhoeus when used 45 | 46 | ## 4.0.1 47 | 48 | - Raise ConfigurationError if api_key or api_secret are not present 49 | 50 | ## 4.0.0 51 | 52 | - Dropped support for Ruby 1.8.x 53 | - Code cleanup via rubocop 54 | 55 | ## 3.1.4 56 | 57 | - Updated docs 58 | - Updated to latest typhoeus gem 59 | 60 | ## 3.1.3 61 | 62 | - Added support for the import API 63 | - Allow setting of custom expiry 64 | 65 | ## v3.1.2 66 | 67 | - Gem updates 68 | 69 | ## 3.1.1 70 | 71 | - Avoid overriding the arg of client.request 72 | - Allow retrieving the request_uri of a Mixpanel request 73 | 74 | ## 3.1.0 75 | 76 | - Parallel requests option 77 | 78 | ## 3.0.0 79 | 80 | - NOTE: This version breaks backwards compatibility 81 | - Use a regular ruby hash instead of metaprogramming for mixpanel options 82 | 83 | ## 2.2.3 84 | 85 | - Added some more options 86 | 87 | ## 2.2.2 88 | 89 | - Added some more options 90 | 91 | ## 2.2.1 92 | 93 | - Added support for the raw data export API 94 | 95 | ## 2.2.0 96 | 97 | - BASE_URI is now https 98 | - Changed funnel to funnel_id 99 | 100 | ## 2.1.0 101 | 102 | - Updated json dependency to 1.6 103 | 104 | ## 2.0.2 105 | 106 | - Added timezone to available options 107 | - All exceptions can be caught under `Mixpanel::Error` 108 | 109 | ## 2.0.1 110 | 111 | - Added options used in segmentation resources 112 | 113 | ## 2.0.0 114 | 115 | - Manually tested compatibility with Mixpanel gem 116 | 117 | ## 2.0.0.beta2 118 | 119 | - Added JSON to gemspec for ruby versions less than 1.9 120 | 121 | ## 2.0.0.beta1 122 | 123 | - Reverted to namespacing via module name because it's a better practice. 124 | I.e. Use `Mixpanel::Client` instead of `MixpanelClient`. 125 | - Added 'values' as an optional parameter 126 | - `gem install mixpanel_client --pre` 127 | 128 | ## 1.0.1 129 | 130 | - Minor housekeeping and organizing 131 | - Refactored specs 132 | 133 | ## 1.0.0 134 | 135 | - Changed "Mixpanel" class name to "MixpanelClient" to prevent naming collision in other 136 | libraries. [a710a84e8ba4b6f018b7](https://github.com/keolo/mixpanel_client/commit/a710a84e8ba4b6f018b7404ab9fabc8f08b4a4f3) 137 | -------------------------------------------------------------------------------- /config/mixpanel.template.yml: -------------------------------------------------------------------------------- 1 | mixpanel: 2 | :api_secret: 'changeme' 3 | -------------------------------------------------------------------------------- /development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Workspace options: 4 | 5 | 1. Work within [Codespaces](https://github.com/features/codespaces) 6 | 2. Work locally 7 | - `cd` to a working directory for this project 8 | - `git clone [REPOSITORY URL]` 9 | 10 | Setup and make changes: 11 | 12 | - Install dependencies with `bundle install` 13 | - Try `rake -T` to see available commands 14 | - Try `rake spec` to run the unit tests 15 | - Make code and test changes 16 | 17 | Verify quality: 18 | 19 | - Verify code quality using `rubocop` 20 | - Verify specs pass 21 | - Run manual tests `lib/mixpanel/manual_test/basic.rb` 22 | 23 | Build and release gem: 24 | 25 | - Run `rake build` to build the gem into the pkg directory 26 | - Run `rake release` to build and release to rubygems 27 | -------------------------------------------------------------------------------- /lib/mixpanel/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -Ku 2 | 3 | # Mixpanel API Ruby Client Library 4 | # 5 | # Allows access to the mixpanel.com API using the ruby programming language 6 | # 7 | # Copyright (c) 2009+ Keolo Keagy 8 | # See LICENSE for details 9 | module Mixpanel 10 | # Return metrics from Mixpanel Data API 11 | class Client 12 | BASE_URI = 'https://mixpanel.com/api/2.0'.freeze 13 | DATA_URI = 'https://data.mixpanel.com/api/2.0'.freeze 14 | IMPORT_URI = 'https://api.mixpanel.com'.freeze 15 | 16 | attr_reader :uri 17 | attr_accessor :api_secret, :timeout 18 | 19 | def self.base_uri_for_resource(resource) 20 | if resource == 'export' 21 | @@data_uri ? @@data_uri : DATA_URI 22 | elsif resource == 'import' 23 | @@import_uri ? @@import_uri : IMPORT_URI 24 | else 25 | @@base_uri ? @@base_uri : BASE_URI 26 | end 27 | end 28 | 29 | # Configure the client 30 | # 31 | # @example 32 | # config = {api_secret: '456'} 33 | # client = Mixpanel::Client.new(config) 34 | # 35 | # @param [Hash] config consisting of an 'api_secret' and additonal options 36 | def initialize(config) 37 | @api_secret = config[:api_secret] 38 | @timeout = config[:timeout] || nil 39 | @@base_uri = config[:base_uri] || nil 40 | @@data_uri = config[:data_uri] || nil 41 | @@import_uri = config[:import_uri] || nil 42 | 43 | raise ConfigurationError, 'api_secret is required' if @api_secret.nil? 44 | end 45 | 46 | # Return mixpanel data as a JSON object or CSV string 47 | # 48 | # @example 49 | # data = client.request( 50 | # 'events/properties', 51 | # event: '["test-event"]', 52 | # name: 'hello', 53 | # values: '["uno", "dos"]', 54 | # type: 'general', 55 | # unit: 'hour', 56 | # interval: 24, 57 | # limit: 5, 58 | # bucket: 'contents' 59 | # ) 60 | # 61 | # @resource [String] mixpanel api resource endpoint 62 | # @options [Hash] options variables used to make a specific request for 63 | # mixpanel data 64 | # @return [JSON, String] mixpanel response as a JSON object or CSV string 65 | def request(resource, options) 66 | @uri = request_uri(resource, options) 67 | 68 | response = URI.get(@uri, @timeout, @api_secret) 69 | 70 | if %w(export import).include?(resource) && @format != 'raw' 71 | response = %([#{response.split("\n").join(',')}]) 72 | end 73 | 74 | Utils.to_hash(response, @format) 75 | end 76 | 77 | # Return mixpanel URI to the data 78 | # 79 | # @example 80 | # uri = client.request_uri( 81 | # 'events/properties', 82 | # event: '["test-event"]', 83 | # name: 'hello', 84 | # values: '["uno", "dos"]', 85 | # type: 'general', 86 | # unit: 'hour', 87 | # interval: 24, 88 | # limit: 5, 89 | # bucket: 'contents' 90 | # ) 91 | # 92 | # @resource [String] mixpanel api resource endpoint 93 | # @options [Hash] options variables used to make a specific request for 94 | # mixpanel data 95 | # @return [JSON, String] mixpanel response as a JSON object or CSV string 96 | def request_uri(resource, options = {}) 97 | @format = options[:format] || :json 98 | URI.mixpanel(resource, normalize_options(options)) 99 | end 100 | 101 | private 102 | 103 | # Return a hash of options along with defaults and a generated signature 104 | # 105 | # @return [Hash] collection of options including defaults and generated 106 | # signature 107 | def normalize_options(options) 108 | normalized_options = options.dup 109 | normalized_options.merge!(format: @format) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/mixpanel/exceptions.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -Ku 2 | 3 | # Mixpanel API Ruby Client Library 4 | # 5 | # Define exceptions for this library 6 | # 7 | # Copyright (c) 2009+ Keolo Keagy 8 | # See LICENSE for details 9 | module Mixpanel 10 | # URI related exceptions 11 | class Error < StandardError; end 12 | class HTTPError < Error; end 13 | class TimeoutError < Error; end 14 | class ParseError < Error; end 15 | class ConfigurationError < Error; end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mixpanel/uri.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -Ku 2 | 3 | # Mixpanel API Ruby Client Library 4 | # 5 | # URI related helpers 6 | # 7 | # Copyright (c) 2009+ Keolo Keagy 8 | # See LICENSE for details 9 | module Mixpanel 10 | # Utilities to assist generating and requesting URIs 11 | class URI 12 | def self.mixpanel(resource, params) 13 | base = Mixpanel::Client.base_uri_for_resource(resource) 14 | "#{File.join([base, resource.to_s])}?#{encode(params)}" 15 | end 16 | 17 | def self.encode(params) 18 | params.map { |key, val| "#{key}=#{CGI.escape(val.to_s)}" }.sort.join('&') 19 | end 20 | 21 | def self.get(uri, timeout, secret) 22 | ::URI.parse(uri).read( 23 | read_timeout: timeout, 24 | http_basic_authentication: [secret, nil] 25 | ) 26 | rescue OpenURI::HTTPError => error 27 | raise HTTPError, JSON.parse(error.io.read)['error'] 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/mixpanel/utils.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -Ku 2 | 3 | # Mixpanel API Ruby Client Library 4 | # 5 | # Allows access to the mixpanel.com api using the ruby programming language 6 | # 7 | # Copyright (c) 2009+ Keolo Keagy 8 | # See LICENSE for details 9 | module Mixpanel 10 | # Utility methods for Mixpanel::Client 11 | class Client 12 | # Mixpanel API Ruby Client Library 13 | # 14 | # Utility helpers 15 | # 16 | # Copyright (c) 2009+ Keolo Keagy 17 | # See LICENSE for details 18 | module Utils 19 | # Return a JSON object or a string depending on a given format 20 | # 21 | # @param [String] data either CSV or JSON formatted 22 | # @return [JSON, String] data 23 | def self.to_hash(data, format) 24 | if format == 'csv' || format == 'raw' 25 | data 26 | else 27 | begin 28 | JSON.parse(data) 29 | rescue JSON::ParserError => error 30 | raise ParseError, error 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/mixpanel/version.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -Ku 2 | 3 | # Mixpanel API Ruby Client Library 4 | # 5 | # Set version number for mixpanel_client 6 | # 7 | # Copyright (c) 2009+ Keolo Keagy 8 | # See LICENSE for details 9 | module Mixpanel 10 | # Return metrics from Mixpanel Data API 11 | class Client 12 | # Mixpanel::Client library version 13 | VERSION = '5.1.0'.freeze 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mixpanel_client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -Ku 2 | 3 | # Mixpanel API Ruby Client Library 4 | # 5 | # Library includes. 6 | # 7 | # Copyright (c) 2009+ Keolo Keagy. 8 | # See LICENSE for details. 9 | 10 | # Libraries. 11 | require 'cgi' 12 | require 'digest/md5' 13 | require 'open-uri' 14 | require 'json' unless defined?(JSON) 15 | 16 | # Mixpanel::Client libraries. 17 | require "#{File.dirname(__FILE__)}/mixpanel/client" 18 | require "#{File.dirname(__FILE__)}/mixpanel/utils" 19 | require "#{File.dirname(__FILE__)}/mixpanel/uri" 20 | require "#{File.dirname(__FILE__)}/mixpanel/exceptions" 21 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009+ Keolo Keagy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /manual_test/basic.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'rubygems' 4 | require 'mixpanel_client' 5 | require 'yaml' 6 | 7 | config = YAML.load_file(File.join( 8 | File.dirname(__FILE__), 9 | '..', 10 | 'config', 11 | 'mixpanel.yml' 12 | ))['mixpanel'] 13 | 14 | client = Mixpanel::Client.new( 15 | api_secret: config[:api_secret] 16 | ) 17 | 18 | data = client.request('events/properties', 19 | event: '["test-event"]', 20 | type: 'general', 21 | unit: 'hour', 22 | name: 'test') 23 | 24 | puts data.inspect 25 | -------------------------------------------------------------------------------- /mixpanel_client.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'mixpanel/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.license = 'MIT' 7 | s.name = 'mixpanel_client' 8 | s.version = Mixpanel::Client::VERSION 9 | s.platform = Gem::Platform::RUBY 10 | s.authors = ['Keolo Keagy'] 11 | s.email = ['keolo@kea.gy'] 12 | s.homepage = 'http://github.com/keolo/mixpanel_client' 13 | s.summary = %q{Ruby Mixpanel API Client Library} 14 | s.description = %q{Simple ruby client interface to the Mixpanel Data API.} 15 | 16 | s.rubyforge_project = 'mixpanel_client' 17 | 18 | s.files = `git ls-files`.split("\n") 19 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 20 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 21 | s.require_paths = ['lib'] 22 | 23 | s.required_ruby_version = ">= 2.4.0" 24 | 25 | s.add_development_dependency('bundler', '~>2.4') 26 | s.add_development_dependency('rake', '~>13.0') 27 | s.add_development_dependency('rdoc', '~>6.5') 28 | s.add_development_dependency('rspec', '~>3.12') 29 | s.add_development_dependency('webmock', '~>3.18') 30 | s.add_development_dependency('pry', '~>0.14') 31 | s.add_development_dependency('pry-byebug', '~>3.10') 32 | s.add_development_dependency('pry-stack_explorer', '~>0.6') 33 | s.add_development_dependency('rubocop', '~>1.41') 34 | end 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Mixpanel Data API Client 2 | 3 | [![Gem Version](https://badge.fury.io/rb/mixpanel_client.svg)](https://badge.fury.io/rb/mixpanel_client) 4 | [![Code Climate](https://codeclimate.com/github/keolo/mixpanel_client/badges/gpa.svg)](https://codeclimate.com/github/keolo/mixpanel_client) 5 | 6 | Ruby access to the [Mixpanel](http://mixpanel.com/) web analytics tool. 7 | 8 | [Mixpanel Data API Reference](https://mixpanel.com/docs/api-documentation/data-export-api) 9 | 10 | ## Installation 11 | 12 | gem install mixpanel_client 13 | 14 | or if you use a Gemfile 15 | 16 | gem 'mixpanel_client' 17 | 18 | ## Usage 19 | 20 | require 'rubygems' 21 | require 'mixpanel_client' 22 | 23 | client = Mixpanel::Client.new( 24 | api_secret: 'changeme' 25 | timeout: 240, # Default is 60 seconds, increase to reduce timeout errors. 26 | 27 | # Optional URI overrides (e.g. https://developer.mixpanel.com/reference/overview) 28 | base_uri: 'api-eu.mixpanel.com', 29 | data_uri: 'example-data.com', 30 | import_uri: 'example-import.com' 31 | ) 32 | 33 | data = client.request( 34 | 'events/properties', 35 | event: 'Product Clicked', 36 | name: 'product-clicked', 37 | values: '["value1", "value2"]', 38 | type: 'unique', 39 | unit: 'day', 40 | limit: 5, 41 | from_date: '2013-12-1', #<- Date range 42 | to_date: '2014-3-1' #<- 43 | ) 44 | 45 | The API also supports passing a time interval rather than an explicit date range. 46 | 47 | data = client.request( 48 | 'events/properties', 49 | event: 'Product Clicked', 50 | name: 'product-clicked', 51 | values: '["value1", "value2"]', 52 | type: 'unique', 53 | unit: 'day', 54 | limit: 5, 55 | interval: 7 #<- Interval 56 | ) 57 | 58 | Use the Import API to specify a time in the past. You'll need to include your 59 | API token in the data ([docs](https://mixpanel.com/docs/api-documentation/importing-events-older-than-31-days.)). 60 | 61 | To import, encode the data as JSON and use Base64. Encode the data like this: 62 | 63 | data_to_import = { 64 | 'event' => 'firstLogin', 65 | 'properties' => { 66 | 'distinct_id' => guid, 67 | 'time' => time_as_integer_seconds_since_epoch, 68 | 'token' => api_token 69 | } 70 | } 71 | encoded_data = Base64.encode64(data_to_import.to_json) 72 | 73 | Then make a request to the API with the given API key, passing in the encoded data: 74 | 75 | data = client.request('import', {:data => encoded_data, :api_key => api_key}) 76 | 77 | You can only import one event at a time. 78 | 79 | ## Parallel 80 | 81 | The option to make parallel requests has been removed (in v5) so that there are no runtime dependencies. 82 | 83 | ## Development 84 | 85 | List of rake tasks. 86 | 87 | rake -T 88 | 89 | Run specs. 90 | 91 | rake spec 92 | 93 | Run external specs. 94 | 95 | cp config/mixpanel.template.yml config/mixpanel.yml 96 | vi config/mixpanel.yml 97 | rake spec:externals 98 | 99 | Run rubocop and fix offences. 100 | 101 | rubocop 102 | 103 | ## Changelog 104 | 105 | [Changelog](changelog.md) 106 | 107 | ## Collaborators and Maintainers 108 | 109 | [Contributors](https://github.com/keolo/mixpanel_client/graphs/contributors) 110 | 111 | ## Copyright 112 | 113 | Copyright (c) 2009+ Keolo Keagy. See [license](license) for details. 114 | -------------------------------------------------------------------------------- /spec/mixpanel_client/events_externalspec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | WebMock.allow_net_connect! 4 | 5 | describe 'External calls to mixpanel' do 6 | before :all do 7 | config = YAML.load_file(File.join( 8 | File.dirname(__FILE__), 9 | '..', 10 | '..', 11 | 'config', 12 | 'mixpanel.yml' 13 | ))['mixpanel'] 14 | 15 | expect(config).to_not be_nil 16 | @client = Mixpanel::Client.new(config) 17 | end 18 | 19 | context 'when requesting events' do 20 | it 'should raise an error for bad requests' do 21 | data = lambda do 22 | @client.request('events', {}) 23 | end 24 | expect(data).to raise_error(Mixpanel::HTTPError) 25 | end 26 | 27 | it 'should return events' do 28 | data = @client.request('events', 29 | event: '["test-event"]', 30 | type: 'general', 31 | unit: 'hour', 32 | interval: 24) 33 | expect(data).to_not be_a Exception 34 | end 35 | 36 | it 'should return events in csv format' do 37 | data = @client.request('events', 38 | event: '["test-event"]', 39 | type: 'general', 40 | unit: 'hour', 41 | interval: 24, 42 | format: 'csv') 43 | expect(data).to_not be_a Exception 44 | end 45 | 46 | it 'should return events with optional bucket' do 47 | data = @client.request('events', 48 | event: '["test-event"]', 49 | type: 'general', 50 | unit: 'hour', 51 | interval: 24, 52 | bucket: 'test') 53 | expect(data).to_not be_a Exception 54 | end 55 | 56 | it 'should return top events' do 57 | data = @client.request('events/top', 58 | type: 'general', 59 | limit: 10) 60 | expect(data).to_not be_a Exception 61 | end 62 | 63 | it 'should return names' do 64 | data = @client.request('events/names', 65 | type: 'general', 66 | unit: 'hour', 67 | interval: 24, 68 | limit: 10) 69 | expect(data).to_not be_a Exception 70 | end 71 | 72 | it 'should return retention' do 73 | pending 'Retention now has its own endpoint.' 74 | data = @client.request('events/retention', 75 | event: '["test-event"]', 76 | type: 'general', 77 | unit: 'hour', 78 | interval: 24) 79 | expect(data).to_not be_a Exception 80 | end 81 | 82 | it 'should return retention in csv format' do 83 | pending 'Retention now has its own endpoint.' 84 | data = @client.request('events/retention', 85 | event: '["test-event"]', 86 | type: 'general', 87 | unit: 'hour', 88 | interval: 24, 89 | format: 'csv') 90 | expect(data).to_not be_a Exception 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/mixpanel_client/mixpanel_client_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | describe Mixpanel::Client do 4 | before :all do 5 | @client = Mixpanel::Client.new( 6 | api_secret: 'test_secret' 7 | ) 8 | 9 | @uri = Regexp.escape(Mixpanel::Client::BASE_URI) 10 | end 11 | 12 | context 'when initializing a new Mixpanel::Client' do 13 | it 'should set a timeout option as nil by default' do 14 | expect(Mixpanel::Client.new( 15 | api_secret: 'test_secret' 16 | ).timeout).to be_nil 17 | end 18 | 19 | it 'should be able to set a timeout option when passed' do 20 | expect(Mixpanel::Client.new( 21 | api_secret: 'test_secret', 22 | timeout: 3 23 | ).timeout).to eql(3) 24 | end 25 | end 26 | 27 | context 'when making an invalid request' do 28 | it 'should raise an error when API secret is null' do 29 | expect do 30 | Mixpanel::Client.new( 31 | api_secret: nil 32 | ) 33 | end.to raise_error(Mixpanel::ConfigurationError) 34 | end 35 | 36 | it 'should return an argument error "Wrong number of arguments" if using 37 | the deprecated usage' do 38 | # Stub Mixpanel request 39 | stub_request(:get, /^#{@uri}.*/) 40 | .to_return( 41 | body: '{"legend_size": 0, "data": {"series": [], "values": {}}}' 42 | ) 43 | 44 | expect { 45 | @client.request( 46 | nil, 47 | :events, 48 | event: '["test-event"]', 49 | unit: 'hour', 50 | interval: 24 51 | ) 52 | }.to raise_error(ArgumentError) 53 | end 54 | end 55 | 56 | context 'when making a valid request' do 57 | it 'should work without an endpoint' do 58 | # Stub Mixpanel request 59 | stub_request(:get, /^#{@uri}.*/) 60 | .to_return( 61 | body: '{"legend_size": 0, "data": {"series": [], "values": {}}}' 62 | ) 63 | 64 | # No endpoint 65 | data = @client.request( 66 | 'events', 67 | event: '["test-event"]', 68 | unit: 'hour', 69 | interval: 24 70 | ) 71 | 72 | expect(data).to eq( 73 | 'data' => { 74 | 'series' => [], 75 | 'values' => {} 76 | }, 77 | 'legend_size' => 0 78 | ) 79 | end 80 | 81 | it 'should work when it receives an integer response on import' do 82 | @import_uri = Regexp.escape(Mixpanel::Client::IMPORT_URI) 83 | # Stub Mixpanel import request to return a realistic response 84 | stub_request(:get, /^#{@import_uri}.*/).to_return(body: '1') 85 | 86 | data = @client.request( 87 | 'import', 88 | data: 'base64_encoded_data', 89 | api_key: 'test_key' 90 | ) 91 | 92 | expect(data).to eq([1]) 93 | end 94 | 95 | it 'should work with an endpoint, method, and type' do 96 | # Stub Mixpanel request 97 | stub_request(:get, /^#{@uri}.*/) 98 | .to_return( 99 | body: '{"events": [], "type": "general"}' 100 | ) 101 | 102 | # With endpoint 103 | data = @client.request( 104 | 'events/top', 105 | type: 'general' 106 | ) 107 | 108 | expect(data).to eq( 109 | 'events' => [], 110 | 'type' => 'general' 111 | ) 112 | end 113 | 114 | it 'does not modify the provided options' do 115 | options = { foo: 'bar' } 116 | # Stub Mixpanel request 117 | stub_request(:get, /^#{@uri}.*/) 118 | .to_return( 119 | body: '{"events": [], "type": "general"}' 120 | ) 121 | 122 | expect do 123 | @client.request('events/top', options) 124 | end.to_not change { options } 125 | end 126 | end 127 | 128 | describe '#to_hash' do 129 | it 'should return a ruby hash given json as a string' do 130 | expect(Mixpanel::Client::Utils.to_hash( 131 | '{"a" : "ey", "b" : "bee"}', 132 | :json 133 | )).to eq( 134 | 'a' => 'ey', 135 | 'b' => 'bee' 136 | ) 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/mixpanel_client/properties_externalspec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | 3 | WebMock.allow_net_connect! 4 | 5 | describe 'External calls to mixpanel' do 6 | before :all do 7 | config = YAML.load_file(File.join( 8 | File.dirname(__FILE__), 9 | '..', 10 | '..', 11 | 'config', 12 | 'mixpanel.yml' 13 | ))['mixpanel'] 14 | 15 | expect(config).not_to be_nil 16 | @client = Mixpanel::Client.new(config) 17 | end 18 | 19 | context 'when requesting event properties' do 20 | it 'should raise an error for bad requests' do 21 | data = lambda do 22 | @client.request('properties', {}) 23 | end 24 | expect(data).to raise_error(Mixpanel::HTTPError) 25 | end 26 | 27 | it 'should return events' do 28 | data = @client.request('events/properties', 29 | event: '["test-event"]', 30 | name: 'hello', 31 | values: '["uno", "dos"]', 32 | type: 'general', 33 | unit: 'hour', 34 | interval: 24, 35 | limit: 5, 36 | bucket: 'kicked') 37 | expect(data).not_to be_a Exception 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/mixpanel_client/uri_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 3 | 4 | describe Mixpanel::URI do 5 | describe '.mixpanel' do 6 | it 'should return a properly formatted mixpanel uri as a string (without an 7 | endpoint)' do 8 | resource = 'events' 9 | params = { c: 'see', a: 'ey' } 10 | 11 | expect(Mixpanel::URI.mixpanel(resource, params)).to eq( 12 | "#{Mixpanel::Client::BASE_URI}/events?a=ey&c=see" 13 | ) 14 | end 15 | 16 | it 'should return a properly formatted mixpanel uri as a string (with an 17 | endpoint)' do 18 | resource = 'events/top' 19 | params = { c: 'see', a: 'ey' } 20 | 21 | expect(Mixpanel::URI.mixpanel(resource, params)).to eq( 22 | "#{Mixpanel::Client::BASE_URI}/events/top?a=ey&c=see" 23 | ) 24 | end 25 | 26 | it 'should return a uri with a different endpoint when doing a raw data 27 | export' do 28 | resource = 'export' 29 | params = { c: 'see', a: 'ey' } 30 | 31 | expect(Mixpanel::URI.mixpanel(resource, params)).to eq( 32 | "#{Mixpanel::Client::DATA_URI}/export?a=ey&c=see" 33 | ) 34 | end 35 | 36 | it 'should return a uri with a the correct endpoint when doing an 37 | import' do 38 | resource = 'import' 39 | params = { c: 'see', a: 'ey' } 40 | expect(Mixpanel::URI.mixpanel(resource, params)).to eq( 41 | "#{Mixpanel::Client::IMPORT_URI}/import?a=ey&c=see" 42 | ) 43 | end 44 | end 45 | 46 | describe '.encode' do 47 | it 'should return a string with url encoded values.' do 48 | params = { hey: '!@#$%^&*()\/"Ü', soo: 'hëllö?' } 49 | 50 | expect(Mixpanel::URI.encode(params)).to eq( 51 | 'hey=%21%40%23%24%25%5E%26%2A%28%29%5C%2F%22%C3%9C&' + 52 | 'soo=h%C3%ABll%C3%B6%3F' 53 | ) 54 | end 55 | end 56 | 57 | describe '.get' do 58 | it 'should return a string response' do 59 | stub_request(:get, 'http://example.com').to_return(body: 'something') 60 | 61 | expect(Mixpanel::URI.get('http://example.com', nil, 'secret')).to \ 62 | eq('something') 63 | end 64 | 65 | context 'when timeout is not nil' do 66 | context 'when the request times out' do 67 | it 'should return a timeout error' do 68 | stub_request(:get, 'http://example.com').to_timeout 69 | 70 | expect do 71 | Mixpanel::URI.get('http://example.com', 3, 'secret') 72 | end.to raise_error Timeout::Error 73 | end 74 | end 75 | 76 | context 'when the request does not timeout' do 77 | it 'should return a string response' do 78 | stub_request(:get, 'http://example.com').to_return(body: 'something') 79 | 80 | expect(Mixpanel::URI.get('http://example.com', 3, 'secret')).to \ 81 | eq('something') 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'mixpanel_client' 4 | require 'webmock/rspec' 5 | 6 | RSpec.configure do |config| 7 | config.include WebMock::API 8 | end 9 | --------------------------------------------------------------------------------