Skip to content
Browse files

Initial commit.

  • Loading branch information...
0 parents commit 254e89a22177b1032be4905cf9648f50fb4f56ea @dzello dzello committed Jan 5, 2013
Showing with 4,037 additions and 0 deletions.
  1. +5 −0 .gitignore
  2. +2 −0 .rspec
  3. +19 −0 .travis.yml
  4. +12 −0 Gemfile
  5. +16 −0 Guardfile
  6. +20 −0 LICENSE
  7. +10 −0 README.md
  8. +15 −0 Rakefile
  9. +3,376 −0 config/cacert.pem
  10. +32 −0 keen.gemspec
  11. +40 −0 lib/keen.rb
  12. +108 −0 lib/keen/client.rb
  13. +51 −0 lib/keen/http.rb
  14. +3 −0 lib/keen/version.rb
  15. +7 −0 script/travis
  16. +60 −0 spec/integration/api_spec.rb
  17. +8 −0 spec/integration/spec_helper.rb
  18. +163 −0 spec/keen/client_spec.rb
  19. +61 −0 spec/keen/keen_spec.rb
  20. +10 −0 spec/keen/spec_helper.rb
  21. +19 −0 spec/spec_helper.rb
5 .gitignore
@@ -0,0 +1,5 @@
+*.bundle
+Gemfile.lock
+tmp
+.env
+log
2 .rspec
@@ -0,0 +1,2 @@
+--format progress
+--color
19 .travis.yml
@@ -0,0 +1,19 @@
+language: ruby
+bundler_args: --without development
+
+rvm:
+ - 1.8.7
+ - 1.9.3
+ - jruby-19mode
+ - rbx-19mode
+
+env:
+ global:
+ - CI=true
+ - KEEN_API_KEY=f806128f31c349fda124b62d1f4cf4b2
+ - KEEN_PROJECT_ID=50e5ffa6897a2c319b000000
+ matrix:
+ - INTEGRATED=true
+ - INTEGRATED=false
+
+script: "./script/travis"
12 Gemfile
@@ -0,0 +1,12 @@
+source :rubygems
+
+group :development do
+ gem 'rb-readline' # or compile ruby w/ readline
+ gem 'rb-inotify', :require => false
+ gem 'rb-fsevent', :require => false
+ gem 'rb-fchange', :require => false
+ gem 'ruby-debug', :platforms => :mri_18
+ gem 'debugger', :platforms => :mri_19
+end
+
+gemspec
16 Guardfile
@@ -0,0 +1,16 @@
+group :unit do
+ guard 'rspec', :spec_paths => "spec/keen" do
+ watch('spec/spec_helper.rb') { "spec" }
+ watch('spec/keen/spec_helper.rb') { "spec" }
+ watch(%r{^spec/keen/.+_spec\.rb$})
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/keen/#{m[1]}_spec.rb" }
+ end
+end
+
+group :integration do
+ guard 'rspec', :spec_paths => "spec/integration" do
+ watch('spec/spec_helper.rb') { "spec" }
+ watch('spec/integration/spec_helper.rb') { "spec" }
+ watch(%r{^spec/integration/.+_spec\.rb$})
+ end
+end
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2012 Keen Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 README.md
@@ -0,0 +1,10 @@
+# Keen gem
+
+[![Build Status](https://secure.travis-ci.org/keen/keen-gem.png?branch=master)](http://travis-ci.org/keen/keen-gem)
+
+keen-gem is the official Ruby Client for the [Keen IO](https://keen.io/) API. The
+Keen IO API lets developers collect arbitrary application events and perform analytics on them.
+
+### Warning
+
+This gem is yet unreleased. Until this message is removed, use with caution.
15 Rakefile
@@ -0,0 +1,15 @@
+require 'bundler'
+require 'rspec/core/rake_task'
+
+desc "Run Rspec unit tests"
+RSpec::Core::RakeTask.new(:spec) do |t|
+ t.pattern = "spec/keen/**/*_spec.rb"
+end
+
+desc "Run Rspec integration tests"
+RSpec::Core::RakeTask.new(:integration) do |t|
+ t.pattern = "spec/integration/**/*_spec.rb"
+end
+
+task :default => :spec
+task :test => [:spec]
3,376 config/cacert.pem
3,376 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
32 keen.gemspec
@@ -0,0 +1,32 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "keen/version"
+
+Gem::Specification.new do |s|
+ s.name = "keen"
+ s.version = Keen::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Kyle Wild", "Josh Dzielak"]
+ s.email = "josh@keen.io"
+ s.homepage = "https://github.com/keenlabs/keen-gem"
+ s.summary = "Keen IO API Client"
+ s.description = "Batch and send events to the Keen IO API. Supports asychronous requests."
+
+ s.add_dependency "multi_json", "~> 1.0"
+ s.add_dependency "jruby-openssl" if defined?(JRUBY_VERSION)
+
+ s.add_development_dependency 'oj'
+ s.add_development_dependency 'rspec'
+ s.add_development_dependency 'rack'
+ s.add_development_dependency 'rake'
+ s.add_development_dependency "em-http-request"
+ s.add_development_dependency 'webmock'
+ s.add_development_dependency 'ruby_gntp'
+ s.add_development_dependency 'guard'
+ s.add_development_dependency 'guard-rspec'
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
40 lib/keen.rb
@@ -0,0 +1,40 @@
+require 'logger'
+require 'forwardable'
+require 'multi_json'
+
+require 'keen/client'
+
+module Keen
+ class Error < RuntimeError; end
+ class ConfigurationError < Error; end
+ class HttpError < Error; end
+ class BadRequestError < HttpError; end
+ class AuthenticationError < HttpError; end
+ class NotFoundError < HttpError; end
+
+ class << self
+ extend Forwardable
+
+ def_delegators :default_client, :project_id, :api_key,
+ :project_id=, :api_key=, :publish, :publish_async
+
+ attr_writer :logger
+
+ def logger
+ @logger ||= lambda {
+ logger = Logger.new($stdout)
+ logger.level = Logger::INFO
+ logger
+ }.call
+ end
+
+ private
+
+ def default_client
+ @default_client || Keen::Client.new(
+ :project_id => ENV['KEEN_PROJECT_ID'],
+ :api_key => ENV['KEEN_API_KEY']
+ )
+ end
+ end
+end
108 lib/keen/client.rb
@@ -0,0 +1,108 @@
+require 'keen/http'
+
+module Keen
+ class Client
+ attr_accessor :project_id, :api_key
+
+ CONFIG = {
+ :api_host => "api.keen.io",
+ :api_port => 443,
+ :api_sync_http_options => {
+ :use_ssl => true,
+ :verify_mode => OpenSSL::SSL::VERIFY_PEER,
+ :verify_depth => 5,
+ :ca_file => File.expand_path("../../../../config/cacert.pem", __FILE__) },
+ :api_async_http_options => {},
+ :api_headers => {
+ "Content-Type" => "application/json",
+ "User-Agent" => "keen-gem v#{Keen::VERSION}"
+ }
+ }
+
+ def initialize(*args)
+ options = args[0]
+ unless options.is_a?(Hash)
+ # deprecated, pass a hash of options instead
+ options = {
+ :project_id => args[0],
+ :api_key => args[1],
+ }.merge(args[2] || {})
+ end
+
+ @project_id, @api_key = options.values_at(
+ :project_id, :api_key)
+ end
+
+ def publish(event_name, properties)
+ check_configuration!
+ begin
+ response = Keen::HTTP::Sync.new(
+ api_host, api_port, api_sync_http_options).post(
+ :path => api_path(event_name),
+ :headers => api_headers_with_auth,
+ :body => MultiJson.encode(properties))
+ rescue => e
+ raise Error, "Couldn't connect to Keen IO: #{e.inspect}"
+ end
+ process_response(response.code, response.body.chomp)
+ end
+
+ def publish_async(event_name, properties)
+ check_configuration!
+
+ http_client = Keen::HTTP::Async.new(api_host, api_port, api_async_http_options)
+ http = http_client.post({
+ :path => api_path(event_name),
+ :headers => api_headers_with_auth,
+ :body => MultiJson.encode(properties)
+ })
+ http.callback { |status_code, response_body|
+ process_response(status_code, response_body)
+ }
+ http.errback { |http, error|
+ Keen.logger.error("Couldn't connect to Keen IO")
+ }
+ http
+ end
+
+ # deprecated
+ def add_event(event_name, properties, options={})
+ self.publish(event_name, properties, options)
+ end
+
+ private
+
+ def process_response(status_code, body)
+ body = MultiJson.decode(body)
+ case status_code.to_i
+ when 200..201
+ return body
+ when 400
+ raise BadRequestError.new(body)
+ when 401
+ raise AuthenticationError.new(body)
+ when 404
+ raise NotFoundError.new(body)
+ else
+ raise HttpError.new(body)
+ end
+ end
+
+ def api_path(collection)
+ "/3.0/projects/#{project_id}/events/#{collection}"
+ end
+
+ def api_headers_with_auth
+ api_headers.merge("Authorization" => api_key)
+ end
+
+ def check_configuration!
+ raise ConfigurationError, "Project ID must be set" unless project_id
+ raise ConfigurationError, "API Key must be set" unless api_key
+ end
+
+ def method_missing(_method, *args, &block)
+ CONFIG[_method.to_sym] || super
+ end
+ end
+end
51 lib/keen/http.rb
@@ -0,0 +1,51 @@
+module Keen
+ module HTTP
+ class Sync
+ def initialize(host, port, options={})
+ require 'net/https'
+ @http = Net::HTTP.new(host, port)
+ options.each_pair { |key, value| @http.send "#{key}=", value }
+ end
+
+ def post(options)
+ path, headers, body = options.values_at(
+ :path, :headers, :body)
+ @http.post(path, body, headers)
+ end
+ end
+
+ class Async
+ def initialize(host, port, options={})
+ if defined?(EventMachine) && EventMachine.reactor_running?
+ require 'em-http-request'
+ else
+ raise Error, "An EventMachine loop must be running to use publish_async calls"
+ end
+
+ @host, @port, @http_options = host, port, options
+ end
+
+ def post(options)
+ path, headers, body = options.values_at(
+ :path, :headers, :body)
+
+ uri = "https://#{@host}:#{@port}#{path}"
+
+ http_client = EventMachine::HttpRequest.new(uri, @http_options)
+ deferrable = EventMachine::DefaultDeferrable.new
+
+ http = http_client.post(
+ :body => body,
+ :head => headers
+ )
+ http.callback {
+ deferrable.succeed(http.response_header.status, http.response.chomp)
+ }
+ http.errback {
+ deferrable.fail(Error.new("Couldn't connect to Keen IO"))
+ }
+ deferrable
+ end
+ end
+ end
+end
3 lib/keen/version.rb
@@ -0,0 +1,3 @@
+module Keen
+ VERSION = "0.4.0"
+end
7 script/travis
@@ -0,0 +1,7 @@
+#! /usr/bin/env ruby
+
+if ENV['INTEGRATED'] == true
+ exec "rake spec"
+else
+ exec "rake integration"
+end
60 spec/integration/api_spec.rb
@@ -0,0 +1,60 @@
+require File.expand_path("../spec_helper", __FILE__)
+
+describe "Keen IO API" do
+ let(:project_id) { ENV['KEEN_PROJECT_ID'] }
+ let(:api_key) { ENV['KEEN_API_KEY'] }
+
+ let(:collection) { "users" }
+ let(:event_properties) { { "name" => "Bob" } }
+
+ describe "success" do
+ let(:expected_api_response) { { "created" => true } }
+
+ it "should return a created status for a valid post" do
+ Keen.publish(collection, event_properties).should ==
+ expected_api_response
+ end
+ end
+
+ describe "failure" do
+ it "should raise a not found error if an invalid project id" do
+ client = Keen::Client.new(
+ :api_key => api_key, :project_id => "riker")
+ expect {
+ client.publish(collection, event_properties)
+ }.to raise_error(Keen::NotFoundError)
+ end
+
+ it "should raise authentication error if invalid API Key" do
+ client = Keen::Client.new(
+ :api_key => "wrong", :project_id => project_id)
+ expect {
+ client.publish(collection, event_properties)
+ }.to raise_error(Keen::AuthenticationError)
+ end
+
+ it "should raise bad request if no JSON is supplied" do
+ expect {
+ Keen.publish(collection, nil)
+ }.to raise_error(Keen::BadRequestError)
+ end
+
+ it "should return not found for an invalid collection name" do
+ expect {
+ Keen.publish(nil, event_properties)
+ }.to raise_error(Keen::NotFoundError)
+ end
+ end
+
+ describe "async" do
+ it "should work" do
+ deferrable = Keen.publish_async(collection, event_properties)
+ callback = double("callback")
+ callback.should_receive("hit")
+ deferrable.callback {
+ callback.hit
+ }
+ sleep 2
+ end
+ end
+end
8 spec/integration/spec_helper.rb
@@ -0,0 +1,8 @@
+require File.expand_path("../../spec_helper", __FILE__)
+
+RSpec.configure do |config|
+ unless ENV['KEEN_PROJECT_ID'] && ENV['KEEN_API_KEY']
+ raise "Please set a KEEN_PROJECT_ID and KEEN_API_KEY on the environment
+ before running the integration specs."
+ end
+end
163 spec/keen/client_spec.rb
@@ -0,0 +1,163 @@
+require File.expand_path("../spec_helper", __FILE__)
+
+describe Keen::Client do
+ let(:project_id) { "12345" }
+ let(:api_key) { "abcde" }
+ let(:collection) { "users" }
+ let(:event_properties) { { "name" => "Bob" } }
+
+ def stub_api(url, status, json_body)
+ stub_request(:post, url).to_return(
+ :status => status,
+ :body => MultiJson.encode(json_body))
+ end
+
+ def expect_post(url, event_properties, api_key)
+ WebMock.should have_requested(:post, url).with(
+ :body => MultiJson.encode(event_properties),
+ :headers => { "Content-Type" => "application/json",
+ "User-Agent" => "keen-gem v#{Keen::VERSION}",
+ "Authorization" => api_key })
+ end
+
+ def api_url(collection)
+ "https://api.keen.io/3.0/projects/#{project_id}/events/#{collection}"
+ end
+
+ describe "#initialize" do
+ context "deprecated" do
+ it "should allow created via project_id and api_key args" do
+ client = Keen::Client.new(project_id, api_key)
+ client.api_key.should == api_key
+ client.project_id.should == project_id
+ end
+ end
+
+ it "should initialize with options" do
+ client = Keen::Client.new(
+ :project_id => project_id,
+ :api_key => api_key)
+ client.api_key.should == api_key
+ client.project_id.should == project_id
+ end
+ end
+
+ describe "with a unconfigured client" do
+ [:publish, :publish_async].each do |_method|
+ describe "##{_method}" do
+ it "should raise an exception if no project_id" do
+ expect {
+ Keen::Client.new(:api_key => api_key).
+ send(_method, collection, event_properties)
+ }.to raise_error(Keen::ConfigurationError)
+ end
+
+ it "should raise an exception if no api_key" do
+ expect {
+ Keen::Client.new(:project_id => project_id).
+ send(_method, collection, event_properties)
+ }.to raise_error(Keen::ConfigurationError)
+ end
+ end
+ end
+ end
+
+ describe "with a configured client" do
+ before do
+ @client = Keen::Client.new(
+ :project_id => project_id,
+ :api_key => api_key)
+ end
+
+ describe "#publish" do
+ it "should post using the collection and properties" do
+ stub_api(api_url(collection), 201, "")
+ @client.publish(collection, event_properties)
+ expect_post(api_url(collection), event_properties, api_key)
+ end
+
+ it "should return the proper response" do
+ api_response = { "created" => true }
+ stub_api(api_url(collection), 201, api_response)
+ @client.publish(collection, event_properties).should == api_response
+ end
+ end
+
+ describe "#publish_async" do
+ it "should post the event data" do
+ stub_api(api_url(collection), 201, "")
+ @client.publish_async(collection, event_properties)
+ expect_post(api_url(collection), event_properties, api_key)
+ end
+
+ describe "deferrable callbacks" do
+ it "should trigger callbacks" do
+ stub_api(api_url(collection), 201, "")
+ deferrable = @client.publish_async(collection, event_properties)
+ callback = double("callback")
+ callback.should_receive("hit")
+ deferrable.callback {
+ callback.hit
+ }
+ sleep 0.1
+ end
+
+ xit "should trigger errbacks" do
+ # pending: can't get errback to trigger
+ stub_request(:post, api_url(collection)).to_raise(StandardError)
+ deferrable = @client.publish_async(collection, event_properties)
+ errback = double("errback")
+ errback.should_receive("hit")
+ deferrable.errback {
+ errback.hit
+ }
+ sleep 0.1
+ end
+ end
+ end
+
+ describe "response handling" do
+ def stub_status_and_publish(code, api_response=nil)
+ stub_api(api_url(collection), code, api_response)
+ @client.publish(collection, event_properties)
+ end
+
+ it "should return the json body for a 200-201" do
+ api_response = { "created" => "true" }
+ stub_status_and_publish(200, api_response).should == api_response
+ stub_status_and_publish(201, api_response).should == api_response
+ end
+
+ it "should raise a bad request error for a 400" do
+ expect {
+ stub_status_and_publish(400)
+ }.to raise_error(Keen::BadRequestError)
+ end
+
+ it "should raise a authentication error for a 401" do
+ expect {
+ stub_status_and_publish(401)
+ }.to raise_error(Keen::AuthenticationError)
+ end
+
+ it "should raise a not found error for a 404" do
+ expect {
+ stub_status_and_publish(404)
+ }.to raise_error(Keen::NotFoundError)
+ end
+
+ it "should raise an http error otherwise" do
+ expect {
+ stub_status_and_publish(420)
+ }.to raise_error(Keen::HttpError)
+ end
+ end
+
+ describe "#add_event" do
+ it "should alias to publish" do
+ @client.should_receive(:publish).with("users", {:a => 1}, {:b => 2})
+ @client.add_event("users", {:a => 1}, {:b => 2})
+ end
+ end
+ end
+end
61 spec/keen/keen_spec.rb
@@ -0,0 +1,61 @@
+require File.expand_path("../spec_helper", __FILE__)
+
+describe Keen do
+ describe "default client" do
+ describe "configuring from the environment" do
+ before do
+ ENV["KEEN_PROJECT_ID"] = "12345"
+ ENV["KEEN_API_KEY"] = "abcde"
+ end
+
+ let(:client) { Keen.send(:default_client) }
+
+ it "should set a project id from the environment" do
+ client.project_id.should == "12345"
+ end
+
+ it "should set a project id from the environment" do
+ client.api_key.should == "abcde"
+ end
+
+ after do
+ ENV["KEEN_PROJECT_ID"] = nil
+ ENV["KEEN_API_KEY"] = nil
+ end
+ end
+ end
+
+ describe "forwardable" do
+ before do
+ @default_client = double("client")
+ Keen.stub(:default_client).and_return(@default_client)
+ end
+
+ [:project_id, :api_key].each do |_method|
+ it "should forward the #{_method} method" do
+ @default_client.should_receive(_method)
+ Keen.send(_method)
+ end
+ end
+
+ [:project_id=, :api_key=].each do |_method|
+ it "should forward the #{_method} method" do
+ @default_client.should_receive(_method).with("12345")
+ Keen.send(_method, "12345")
+ end
+ end
+
+ [:publish, :publish_async].each do |_method|
+ it "should forward the #{_method} method" do
+ @default_client.should_receive(_method).with("users", {})
+ Keen.send(_method, "users", {})
+ end
+ end
+ end
+
+ describe "logger" do
+ it "should be set to info" do
+ Keen.logger.level.should == Logger::INFO
+ end
+ end
+end
10 spec/keen/spec_helper.rb
@@ -0,0 +1,10 @@
+require File.expand_path("../../spec_helper", __FILE__)
+
+require 'webmock/rspec'
+
+RSpec.configure do |config|
+ config.before(:each) do
+ WebMock.disable_net_connect!
+ WebMock.reset!
+ end
+end
19 spec/spec_helper.rb
@@ -0,0 +1,19 @@
+begin
+ require 'bundler/setup'
+rescue LoadError
+ puts 'Use of Bundler is recommended'
+end
+
+require 'rspec'
+require 'net/https'
+require 'em-http'
+
+require File.expand_path("../../lib/keen", __FILE__)
+
+RSpec.configure do |config|
+ config.before(:all) do
+ unless EventMachine.reactor_running?
+ Thread.new { EventMachine.run }
+ end
+ end
+end

0 comments on commit 254e89a

Please sign in to comment.
Something went wrong with that request. Please try again.