├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── config └── cacert.pem ├── keen.gemspec ├── lib ├── keen.rb └── keen │ ├── access_keys.rb │ ├── aes_helper.rb │ ├── aes_helper_old.rb │ ├── cached_datasets.rb │ ├── client.rb │ ├── client │ ├── maintenance_methods.rb │ ├── publishing_methods.rb │ └── querying_methods.rb │ ├── http.rb │ ├── saved_queries.rb │ ├── scoped_key.rb │ └── version.rb └── spec ├── integration ├── access_keys_spec.rb ├── api_spec.rb ├── saved_query_spec.rb └── spec_helper.rb ├── keen ├── access_keys_spec.rb ├── cached_dataset_spec.rb ├── client │ ├── maintenance_methods_spec.rb │ ├── publishing_methods_spec.rb │ └── querying_methods_spec.rb ├── client_spec.rb ├── keen_spec.rb ├── saved_query_spec.rb ├── scoped_key_old_spec.rb ├── scoped_key_spec.rb └── spec_helper.rb ├── spec_helper.rb └── synchrony ├── spec_helper.rb └── synchrony_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle 2 | tmp 3 | .env 4 | log 5 | *.gem 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --tty 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without development 3 | 4 | rvm: 5 | - 2.1.10 6 | - 2.2.7 7 | - 2.4.1 8 | 9 | env: 10 | matrix: 11 | - PATTERN=keen 12 | - PATTERN=synchrony 13 | 14 | before_install: 15 | - gem update bundler 16 | - bundle --version 17 | - gem update --system 18 | - gem --version 19 | 20 | script: 21 | - bundle exec rake pattern 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Keen IO Community Code of Conduct 2 | 3 | The Keen IO Community is dedicated to providing a safe, inclusive, welcoming, and harassment-free space and experience for all community participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers. Our Code of Conduct exists because of that dedication, and we do not tolerate harassment in any form. See our reporting guidelines [here](https://github.com/keen/community-code-of-conduct/blob/master/incident-reporting.md). Our full Code of Conduct can be found at this [link](https://github.com/keen/community-code-of-conduct/blob/master/long-form-code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development, :test do 4 | gem 'rake' 5 | gem 'em-http-request' 6 | gem 'em-synchrony', :require => false 7 | gem 'rspec', '~>3.5' 8 | gem 'webmock', '<= 2.2' 9 | end 10 | 11 | gemspec 12 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | group :unit do 2 | guard 'rspec', :spec_paths => ["spec/keen"] do 3 | watch('spec/spec_helper.rb') { "spec" } 4 | watch('spec/keen/spec_helper.rb') { "spec" } 5 | watch(%r{^spec/keen/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/keen/#{m[1]}_spec.rb" } 7 | end 8 | end 9 | 10 | group :integration do 11 | guard 'rspec', :spec_paths => ["spec/integration"] do 12 | watch('spec/spec_helper.rb') { "spec" } 13 | watch('spec/integration/spec_helper.rb') { "spec" } 14 | watch(%r{^spec/integration/.+_spec\.rb$}) 15 | end 16 | end 17 | 18 | group :synchrony do 19 | guard 'rspec', :spec_paths => ["spec/synchrony"] do 20 | watch('spec/spec_helper.rb') { "spec" } 21 | watch('spec/synchrony/spec_helper.rb') { "spec" } 22 | watch(%r{^spec/synchrony/.+_spec\.rb$}) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Keen Labs 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keen IO Official Ruby Client Library 2 | 3 | [![Build Status](https://secure.travis-ci.org/keenlabs/keen-gem.png?branch=master)](http://travis-ci.org/keenlabs/keen-gem) [![Code Climate](https://codeclimate.com/github/keenlabs/keen-gem.png)](https://codeclimate.com/github/keenlabs/keen-gem) 4 | [![Gem Version](https://badge.fury.io/rb/keen.svg)](http://badge.fury.io/rb/keen) 5 | 6 | keen-gem is the official Ruby Client for the [Keen IO](https://keen.io/?s=gh-gem) API. The 7 | Keen IO API lets developers build analytics features directly into their apps. 8 | 9 | ### Installation 10 | 11 | Add to your Gemfile: 12 | 13 | gem 'keen' 14 | 15 | or install from Rubygems: 16 | 17 | gem install keen 18 | 19 | keen is tested with Ruby 1.9.3 + and on: 20 | 21 | * MRI 22 | * Rubinius 23 | * jRuby (except for asynchronous methods - no TLS support for EM on jRuby) 24 | 25 | ### Usage 26 | 27 | Before making any API calls, you must supply keen-gem with a Project ID and one or more authentication keys. 28 | (If you need a Keen IO account, [sign up here](https://keen.io/signup?s=gh-gem) - it's free.) 29 | 30 | Setting a write key is required for publishing events. Setting a read key is required for running queries. 31 | Setting a master key is required for performing deletes. You can find keys for all of your projects 32 | on [keen.io](https://keen.io?s=gh-gem). 33 | 34 | The recommended way to set keys is via the environment. The keys you can set are 35 | `KEEN_PROJECT_ID`, `KEEN_WRITE_KEY`, `KEEN_READ_KEY` and `KEEN_MASTER_KEY`. 36 | You only need to specify the keys that correspond to the API calls you'll be performing. 37 | If you're using [foreman](http://ddollar.github.com/foreman/), add this to your `.env` file: 38 | 39 | KEEN_PROJECT_ID=aaaaaaaaaaaaaaa 40 | KEEN_MASTER_KEY=xxxxxxxxxxxxxxx 41 | KEEN_WRITE_KEY=yyyyyyyyyyyyyyy 42 | KEEN_READ_KEY=zzzzzzzzzzzzzzz 43 | 44 | If not, make a script to export the variables into your shell or put it before the command you use to start your server. 45 | 46 | When you deploy, make sure your production environment variables are set. For example, 47 | set [config vars](https://devcenter.heroku.com/articles/config-vars) on Heroku. (We recommend this 48 | environment-based approach because it keeps sensitive information out of the codebase. If you can't do this, see the alternatives below.) 49 | 50 | Once your environment is properly configured, the `Keen` object is ready to go immediately. 51 | 52 | ### Data Enrichment 53 | 54 | A data enrichment is a powerful add-on to enrich the data you're already streaming to Keen IO by pre-processing the data and adding helpful data properties. To activate add-ons, you simply add some new properties within the "keen" namespace in your events. Detailed documentation for the configuration of our add-ons is available [here](https://keen.io/docs/api/ruby#data-enrichment). 55 | 56 | Here is an example of using the [URL parser](https://keen.io/docs/streams/data-enrichment-overview/#addon-url-parser): 57 | 58 | ```ruby 59 | Keen.publish(:requests, { 60 | :page_url => "http://my-website.com/cool/link?source=twitter&foo=bar/#title", 61 | :keen => { 62 | :addons => [ 63 | { 64 | :name => "keen:url_parser", 65 | :input => { 66 | :url => "page_url" 67 | }, 68 | :output => "parsed_page_url" 69 | } 70 | ] 71 | } 72 | }) 73 | ``` 74 | 75 | Keen IO will parse the URL for you and that would equivalent to: 76 | 77 | ```ruby 78 | Keen.publish(:request, { 79 | :page_url => "http://my-website.com/cool/link?source=twitter&foo=bar/#title", 80 | :parsed_page_url => { 81 | :protocol => "http", 82 | :domain => "my-website.com", 83 | :path => "/cool/link", 84 | :anchor => "title", 85 | :query_string => { 86 | :source => "twitter", 87 | :foo => "bar" 88 | } 89 | } 90 | }) 91 | ``` 92 | 93 | Here is another example of using the [Datetime parser](https://keen.io/docs/api/?shell#datetime-parser). 94 | Let's assume you want to do a deeper analysis on the "purchases" event by day of the week (Monday, Tuesday, Wednesday, etc.) and other interesting Datetime components. You can use "keen.timestamp" property that is included in your event automatically. 95 | 96 | ```ruby 97 | Keen.publish(:purchases, { 98 | :keen => { 99 | :addons => [ 100 | { 101 | :name => "keen:date_time_parser", 102 | :input => { 103 | :date_time => "keen.timestamp" 104 | }, 105 | :output => "timestamp_info" 106 | } 107 | ] 108 | }, 109 | :price => 500 110 | }) 111 | ``` 112 | 113 | Other Data Enrichment add-ons are located in the [API reference docs](https://keen.io/docs/api/ruby#data-enrichment). 114 | 115 | ### Synchronous Publishing 116 | 117 | Publishing events requires that `KEEN_WRITE_KEY` is set. Publish an event like this: 118 | 119 | ```ruby 120 | Keen.publish(:sign_ups, { :username => "lloyd", :referred_by => "harry" }) 121 | ``` 122 | 123 | This will publish an event to the `sign_ups` collection with the `username` and `referred_by` properties set. 124 | The event properties can be any valid Ruby hash. Nested properties are allowed. Lists of objects are also allowed, but not recommended because they can be difficult to query over. See alternatives to lists of objects [here](http://stackoverflow.com/questions/24620330/nested-json-objects-in-keen-io). You can learn more about data modeling with Keen IO with the [Data Modeling Guide](https://keen.io/docs/event-data-modeling/event-data-intro/?s=gh-gem). 125 | 126 | Protip: Marshalling gems like [Blockhead](https://github.com/vinniefranco/blockhead) make converting structs or objects to hashes easier. 127 | 128 | The event collection need not exist in advance. If it doesn't exist, Keen IO will create it on the first request. 129 | 130 | ### Asynchronous publishing 131 | 132 | Publishing events shouldn't slow your application down or make users wait longer for page loads & server requests. 133 | 134 | The Keen IO API is fast, but any synchronous network call you make will negatively impact response times. For this reason, we recommend you use the `publish_async` method to send events when latency is a concern. Alternatively, you can drop events into a background queue e.g. Delayed Jobs and publish synchronously from there. 135 | 136 | To publish asynchronously, first add 137 | [em-http-request](https://github.com/igrigorik/em-http-request) to your Gemfile. Make sure it's version 1.0 or above. 138 | 139 | ```ruby 140 | gem "em-http-request", "~> 1.0" 141 | ``` 142 | 143 | Next, run an instance of EventMachine. If you're using an EventMachine-based web server like 144 | thin or goliath you're already doing this. Otherwise, you'll need to start an EventMachine loop manually as follows: 145 | 146 | ```ruby 147 | require 'em-http-request' 148 | 149 | Thread.new { EventMachine.run } 150 | ``` 151 | 152 | The best place for this is in an initializer, or anywhere that runs when your app boots up. 153 | Here's a useful blog article that explains more about this approach - [EventMachine and Passenger](http://railstips.org/blog/archives/2011/05/04/eventmachine-and-passenger/). 154 | 155 | And here's a gist that shows an example of [Eventmachine with Unicorn](https://gist.github.com/jonkgrimes/5103321), specifically the Unicorn config for starting and stopping EventMachine after forking. 156 | 157 | Now, in your code, replace `publish` with `publish_async`. Bind callbacks if you require them. 158 | 159 | ```ruby 160 | http = Keen.publish_async("sign_ups", { :username => "lloyd", :referred_by => "harry" }) 161 | http.callback { |response| puts "Success: #{response}"} 162 | http.errback { puts "was a failurrr :,(" } 163 | ``` 164 | 165 | This will schedule the network call into the event loop and allow your request thread 166 | to resume processing immediately. 167 | 168 | ### Running queries 169 | 170 | The Keen IO API provides rich querying capabilities against your event data set. For more information, see the [Data Analysis API Guide](https://keen.io/docs/data-analysis/?s=gh-gem). 171 | 172 | Running queries requires that `KEEN_READ_KEY` is set. 173 | 174 | Here are some examples of querying with keen-gem. Let's assume you've added some events to the "purchases" collection. 175 | 176 | ```ruby 177 | # Various analysis types 178 | Keen.count("purchases") # => 100 179 | Keen.sum("purchases", :target_property => "price", :timeframe => "today") # => 10000 180 | Keen.minimum("purchases", :target_property => "price", :timeframe => "today") # => 20 181 | Keen.maximum("purchases", :target_property => "price", :timeframe => "today") # => 100 182 | Keen.average("purchases", :target_property => "price", :timeframe => "today") # => 60 183 | Keen.median("purchases", :target_property => "price", :timeframe => "today") # => 60 184 | Keen.percentile("purchases", :target_property => "price", :percentile => 90, :timeframe => "today") # => 100 185 | Keen.count_unique("purchases", :target_property => "username", :timeframe => "today") # => 3 186 | Keen.select_unique("purchases", :target_property => "username", :timeframe => "today") # => ["Bob", "Linda", "Travis"] 187 | 188 | # Group by's and filters 189 | Keen.sum("purchases", :target_property => "price", :group_by => "item.id", :timeframe => "this_14_days") # => [{ "item.id": 123, "result": 240 }] 190 | Keen.count("purchases", :timeframe => "today", :filters => [{ 191 | "property_name" => "referred_by", 192 | "operator" => "eq", 193 | "property_value" => "harry" 194 | }]) # => 2 195 | 196 | # Relative timeframes 197 | Keen.count("purchases", :timeframe => "today") # => 10 198 | 199 | # Absolute timeframes 200 | Keen.count("purchases", :timeframe => { 201 | :start => "2015-01-01T00:00:00Z", 202 | :end => "2015-31-01T00:00:00Z" 203 | }) # => 5 204 | 205 | # Extractions 206 | Keen.extraction("purchases", :timeframe => "today") # => [{ "keen" => { "timestamp" => "2014-01-01T00:00:00Z" }, "price" => 20 }] 207 | 208 | # Funnels 209 | Keen.funnel(:steps => [{ 210 | :actor_property => "username", :event_collection => "purchases", :timeframe => "yesterday" }, { 211 | :actor_property => "username", :event_collection => "referrals", :timeframe => "yesterday" }]) # => [20, 15] 212 | 213 | # Multi-analysis 214 | Keen.multi_analysis("purchases", analyses: { 215 | :gross => { :analysis_type => "sum", :target_property => "price" }, 216 | :customers => { :analysis_type => "count_unique", :target_property => "username" } }, 217 | :timeframe => 'today', :group_by => "item.id") # => [{ "item.id" => 2, "gross" => 314.49, "customers" => 8 } }] 218 | ``` 219 | 220 | Many of these queries can be performed with group by, filters, series and intervals. The response is returned as a Ruby Hash or Array. 221 | 222 | Detailed information on available parameters for each API resource can be found on the [API Technical Reference](https://keen.io/docs/api/reference/?s=gh-gem). 223 | 224 | ##### The Query Method 225 | 226 | You can also specify the analysis type as a parameter to a method called `query`: 227 | 228 | ``` ruby 229 | Keen.query("median", "purchases", :target_property => "price") # => 60 230 | ``` 231 | 232 | This simplifes querying code where the analysis type is dynamic. 233 | 234 | ##### Query Options 235 | 236 | Each query method or alias takes an optional hash of options as an additional parameter. Possible keys are: 237 | 238 | `:response` – Set to `:all_keys` to return the full API response (usually only the value of the `"result"` key is returned). 239 | `:method` - Set to `:post` to enable post body based query (https://keen.io/docs/data-analysis/post-queries/). 240 | 241 | ##### Query Logging 242 | 243 | You can log all GET and POST queries automatically by setting the `log_queries` option. 244 | 245 | ``` ruby 246 | Keen.log_queries = true 247 | Keen.count('purchases') 248 | # I, [2016-10-30T11:45:24.678745 #9978] INFO -- : [KEEN] Send GET query to https://api.keen.io/3.0/projects//queries/count?event_collection=purchases with options {} 249 | ``` 250 | 251 | ### Saved and Cached Queries 252 | 253 | You can manage your saved queries from the Keen ruby client. 254 | 255 | ##### Create a saved query 256 | ```ruby 257 | saved_query_attributes = { 258 | # NOTE : For now, refresh_rate must explicitly be set to 0 unless you 259 | # intend to create a Cached Query. 260 | refresh_rate: 0, 261 | query: { 262 | analysis_type: 'count', 263 | event_collection: 'purchases', 264 | timeframe: 'this_2_weeks', 265 | filters: [{ 266 | property_name: 'price', 267 | operator: 'gte', 268 | property_value: 1.00 269 | }] 270 | } 271 | } 272 | 273 | Keen.saved_queries.create 'saved-query-name', saved_query_attributes 274 | ``` 275 | 276 | ##### Get all saved queries 277 | ```ruby 278 | Keen.saved_queries.all 279 | ``` 280 | 281 | ##### Get one saved query 282 | ```ruby 283 | Keen.saved_queries.get 'saved-query-name' 284 | ``` 285 | 286 | ##### Get saved query with results 287 | ```ruby 288 | query_body = Keen.saved_queries.get('saved-query-name', true) 289 | query_body['result'] 290 | ``` 291 | 292 | ##### Updating a saved query 293 | 294 | NOTE : Updating Saved Queries through the API requires sending the entire query 295 | definition. Any attribute not sent is interpreted as being cleared/removed. This 296 | means that properties set via another client, including the Projects Explorer 297 | Web UI, would be lost this way. 298 | 299 | The `update` function makes this easier by allowing client code to just specify 300 | the properties that need updating. To do this, it will retrieve the existing 301 | query definition first, which means there will be two HTTP requests. Use 302 | `update_full` in code that already has a full query definition that can 303 | reasonably be expected to be current. 304 | 305 | Update a saved query to now be a cached query with the minimum refresh rate of 4 hrs 306 | ```ruby 307 | # using partial update: 308 | Keen.saved_queries.update 'saved-query-name', refresh_rate: 14400 309 | 310 | # using full update, if we've already fetched the query definition: 311 | saved_query_attributes['refresh_rate'] = 14400 312 | Keen.saved_queries.update_full('saved-query-name', update_attributes) 313 | ``` 314 | 315 | Update a saved query to a new resource name 316 | ```ruby 317 | # using partial update: 318 | Keen.saved_queries.update 'saved-query-name', query_name: 'cached-query-name' 319 | 320 | # using full update, if we've already fetched the query definition or have it lying around 321 | # for whatever reason. We send 'refresh_rate' again, along with the entire definition, or else 322 | # it would be reset: 323 | saved_query_attributes['query_name'] = 'cached-query-name' 324 | Keen.saved_queries.update_full('saved-query-name', saved_query_attributes) 325 | ``` 326 | 327 | Cache a query 328 | ```ruby 329 | Keen.saved_queries.cache 'saved-query-name', 14400 330 | ``` 331 | 332 | Uncache a query 333 | ```ruby 334 | Keen.saved_queries.uncache 'saved-query-name' 335 | ``` 336 | 337 | Delete a saved query (use the new resource name since we just changed it) 338 | ```ruby 339 | Keen.saved_queries.delete 'cached-query-name' 340 | ``` 341 | 342 | ##### Getting Query URLs 343 | 344 | Sometimes you just want the URL for a query, but don't actually need to run it. Maybe to paste into a dashboard, or open in your browser. In that case, use the `query_url` method: 345 | 346 | ``` ruby 347 | Keen.query_url("median", "purchases", :target_property => "price", { :timeframe => "today" }) 348 | # => "https://api.keen.io/3.0/projects//queries/median?target_property=price&event_collection=purchases&api_key=" 349 | ``` 350 | 351 | If you don't want the API key included, pass the `:exclude_api_key` option: 352 | 353 | ``` ruby 354 | Keen.query_url("median", "purchases", { :target_property => "price", :timeframe => "today" }, :exclude_api_key => true) 355 | # => "https://api.keen.io/3.0/projects//queries/median?target_property=price&event_collection=purchases" 356 | ``` 357 | 358 | ### Cached Datasets 359 | 360 | You can manage your cached datasets from the Keen ruby client. 361 | 362 | ##### Create a cached dataset 363 | ```ruby 364 | index_by = 'userId' 365 | query = { 366 | "project_id" => "PROJECT ID", 367 | "analysis_type" => "count", 368 | "event_collection" => "purchases", 369 | "filters" => [ 370 | { 371 | "property_name" => "price", 372 | "operator" => "gte", 373 | "property_value" => 100 374 | } 375 | ], 376 | "timeframe" => "this_500_days", 377 | "interval" => "daily", 378 | "group_by" => ["ip_geo_info.country"] 379 | } 380 | 381 | Keen.cached_datasets.create 'cached-dataset-name', index_by, query, 'My Dataset Display Name' 382 | ``` 383 | 384 | ##### Query cached dataset's results 385 | ```ruby 386 | response_json = Keen.cached_datasets.get_results('a-dataset-name', { 387 | start: "2012-08-13T19:00:00.000Z", 388 | end: "2013-09-20T19:00:00.000Z" 389 | }, index_by_value) 390 | response_json['result'] 391 | ``` 392 | 393 | ##### Retrieve definitions of cached datasets 394 | ```ruby 395 | Keen.cached_datasets.list 396 | Keen.cached_datasets.list(limit: 5, after_name: 'some-dataset') 397 | ``` 398 | 399 | ##### Get a cached dataset's definition 400 | ```ruby 401 | Keen.cached_datasets.get_definition 'a-dataset-name' 402 | ``` 403 | 404 | ##### Delete a cached dataset 405 | ```ruby 406 | Keen.cached_datasets.delete 'a-dataset-name' 407 | ``` 408 | 409 | ### Listing collections 410 | 411 | The Keen IO API let you get the event collections for the project set, it includes properties and their type. It also returns links to the collection resource. 412 | 413 | ```ruby 414 | Keen.event_collections # => [{ "name": "purchases", "properties": { "item.id": "num", ... }, ... }] 415 | ``` 416 | 417 | Getting the list of event collections requires that the `KEEN_MASTER_KEY` is set. 418 | 419 | ### Deleting events 420 | 421 | The Keen IO API allows you to [delete events](https://keen.io/docs/maintenance/#deleting-event-collections?s=gh-gem) 422 | from event collections, optionally supplying a filter to narrow the scope of what you would like to delete. 423 | 424 | Deleting events requires that the `KEEN_MASTER_KEY` is set. 425 | 426 | ```ruby 427 | # Assume some events in the 'signups' collection 428 | 429 | # We can delete them all 430 | Keen.delete(:signups) # => true 431 | 432 | # Or just delete an event corresponding to a particular user 433 | Keen.delete(:signups, filters: [{ 434 | :property_name => 'username', :operator => 'eq', :property_value => "Bob" 435 | }]) # => true 436 | ``` 437 | 438 | ### Other code examples 439 | 440 | #### Overwriting event timestamps 441 | 442 | Two time-related properties are included in your event automatically. The properties “keen.timestamp” and “keen.created_at” are set at the time your event is recorded. You have the ability to overwrite the keen.timestamp property. This could be useful, for example, if you are backfilling historical data. Be sure to use [ISO-8601 Format](https://keen.io/docs/event-data-modeling/event-data-intro/#iso-8601-format?s=gh-gem). 443 | 444 | Keen stores all date and time information in UTC! 445 | 446 | ```ruby 447 | Keen.publish(:sign_ups, { 448 | :keen => { :timestamp => "2012-12-14T20:24:01.123000+00:00" }, 449 | :username => "lloyd", 450 | :referred_by => "harry" 451 | }) 452 | ``` 453 | 454 | #### Batch publishing 455 | 456 | The keen-gem supports publishing events in batches via the `publish_batch` method. Here's an example usage: 457 | 458 | ```ruby 459 | Keen.publish_batch( 460 | :signups => [ 461 | { :name => "Bob" }, 462 | { :name => "Mary" } 463 | ], 464 | :purchases => [ 465 | { :price => 10 }, 466 | { :price => 20 } 467 | ] 468 | ) 469 | ``` 470 | 471 | This call would publish 2 `signups` events and 2 `purchases` events - all in just one API call. 472 | Batch publishing is ideal for loading historical events into Keen IO. 473 | 474 | #### Asynchronous batch publishing 475 | 476 | Ensuring the above guidance is followed for asynchronous publishing, batch publishing logic can used asynchronously with `publish_batch_async`: 477 | 478 | ```ruby 479 | Keen.publish_batch_async( 480 | :signups => [ 481 | { :name => "Bob" }, 482 | { :name => "Mary" } 483 | ], 484 | :purchases => [ 485 | { :price => 10 }, 486 | { :price => 20 } 487 | ] 488 | ) 489 | ``` 490 | 491 | #### Configurable and per-client authentication 492 | 493 | To configure keen-gem in code, do as follows: 494 | 495 | ```ruby 496 | Keen.project_id = 'xxxxxxxxxxxxxxx' 497 | Keen.write_key = 'yyyyyyyyyyyyyyy' 498 | Keen.read_key = 'zzzzzzzzzzzzzzz' 499 | Keen.master_key = 'aaaaaaaaaaaaaaa' 500 | ``` 501 | 502 | You can also configure unique client instances as follows: 503 | 504 | ```ruby 505 | keen = Keen::Client.new(:project_id => 'xxxxxxxxxxxxxxx', 506 | :write_key => 'yyyyyyyyyyyyyyy', 507 | :read_key => 'zzzzzzzzzzzzzzz', 508 | :master_key => 'aaaaaaaaaaaaaaa') 509 | ``` 510 | 511 | #### em-synchrony 512 | 513 | keen-gem can be used with [em-synchrony](https://github.com/igrigorik/em-synchrony). 514 | If you call `publish_async` and `EM::Synchrony` is defined the method will return the response 515 | directly. (It does not return the deferrable on which to register callbacks.) Likewise, it will raise 516 | exceptions 'synchronously' should they happen. 517 | 518 | #### Beacon URLs 519 | 520 | It's possible to publish events to your Keen IO project using the HTTP GET method. 521 | This is useful for situations like tracking email opens using [image beacons](http://en.wikipedia.org/wiki/Web_bug). 522 | 523 | In this situation, the JSON event data is passed by encoding it base-64 and adding it as a request parameter called `data`. 524 | The `beacon_url` method found on the `Keen::Client` does this for you. Here's an example: 525 | 526 | ```ruby 527 | Keen.project_id = 'xxxxxx'; 528 | Keen.write_key = 'yyyyyy'; 529 | Keen.beacon_url("sign_ups", :recipient => "foo@foo.com") 530 | # => "https://api.keen.io/3.0/projects/xxxxxx/events/email_opens?api_key=yyyyyy&data=eyJyZWNpcGllbnQiOiJmb29AZm9vLmNvbSJ9" 531 | ``` 532 | 533 | To track email opens, simply add an image to your email template that points to this URL. For further information on how to do this, see the [image beacon documentation](https://keen.io/docs/data-collection/image-beacon/?s=gh-gem). 534 | 535 | #### Redirect URLs 536 | Redirect URLs are just like image beacon URLs with the addition of a `redirect` query parameter. This parameter is used 537 | to issue a redirect to a certain URL after an event is recorded. 538 | 539 | ``` ruby 540 | Keen.redirect_url("sign_ups", { :recipient => "foo@foo.com" }, "http://foo.com") 541 | # => "https://api.keen.io/3.0/projects/xxxxxx/events/email_opens?api_key=yyyyyy&data=eyJyZWNpcGllbnQiOiJmb29AZm9vLmNvbSJ9&redirect=http://foo.com" 542 | ``` 543 | 544 | This is helpful for tracking email clickthroughs. See the [redirect documentation](https://keen.io/docs/data-collection/redirect/?s=gh-gem) for further information. 545 | 546 | #### Generating scoped keys 547 | 548 | Note, Scoped Keys are now *deprecated* in favor of [Access Keys](https://keen.io/docs/api/#access-keys?s=gh-gem). 549 | 550 | A [scoped key](https://keen.io/docs/security/#scoped-key?s=gh-gem) is a string, generated with your API Key, that represents some encrypted authentication and query options. 551 | Use them to control what data queries have access to. 552 | 553 | ``` ruby 554 | # "my-api-key" should be your MASTER API key 555 | scoped_key = Keen::ScopedKey.new("my-api-key", { "filters" => [{ 556 | "property_name" => "accountId", 557 | "operator" => "eq", 558 | "property_value" => "123456" 559 | }]}).encrypt! # "4d1982fe601b359a5cab7ac7845d3bf27026936cdbf8ce0ab4ebcb6930d6cf7f139e..." 560 | ``` 561 | 562 | You can use the scoped key created in Ruby for API requests from any client. Scoped keys are commonly used in JavaScript, where credentials are visible and need to be protected. 563 | 564 | #### Access Keys 565 | 566 | You can use [Access Keys](https://keen.io/docs/api/?ruby#access-keys) to restrict the functionality of a key you use with the Keen API. Access Keys can also enrich events that you send. 567 | 568 | [Create](https://keen.io/docs/api/?ruby#creating-an-access-key) a key that automatically adds information to each event published with that key: 569 | 570 | ``` ruby 571 | key_body = { 572 | "name" => "autofill foo", 573 | "is_active" => true, 574 | "permitted" => ["writes"], 575 | "options" => { 576 | "writes" => { 577 | "autofill": { 578 | "foo": "bar" 579 | } 580 | } 581 | } 582 | } 583 | 584 | new_key = Keen.access_keys.create(key_body) 585 | autofill_write_key = new_key["key"] 586 | ``` 587 | 588 | [List all](https://keen.io/docs/api/#list-all-access-keys) keys associated with a project. 589 | 590 | ``` 591 | Keen.access_keys.all 592 | ``` 593 | 594 | [Get info](https://keen.io/docs/api/#get-an-access-key) associated with a given key 595 | ``` 596 | access_key = '0000000000000000000000000000000000000000000000000000000000000000' 597 | Keen.access_keys.get(access_key) 598 | ``` 599 | 600 | [Update](https://keen.io/docs/api/#updating-an-access-key) a key. Information passed to this method will overwrite existing properties. 601 | 602 | ``` 603 | access_key = '0000000000000000000000000000000000000000000000000000000000000000' 604 | update_body = { 605 | name: 'updated key', 606 | is_active: false, 607 | permitted: ['reads'] 608 | } 609 | Keen.access_keys.update(access_key, update_body) 610 | ``` 611 | 612 | [Revoke](https://keen.io/docs/api/#revoking-an-access-key) a key. This will set the key's active flag to false, but keep it available to be unrevoked. If you want to permanently remove a key, use `delete`. 613 | 614 | ``` 615 | access_key = '0000000000000000000000000000000000000000000000000000000000000000' 616 | Keen.access_keys.revoke(access_key) 617 | ``` 618 | 619 | [Unrevoke](https://keen.io/docs/api/#un-revoking-an-access-key) a key. This will set a previously revoked key's active flag to true. 620 | 621 | ``` 622 | access_key = '0000000000000000000000000000000000000000000000000000000000000000' 623 | Keen.access_keys.unrevoke(access_key) 624 | ``` 625 | 626 | [Delete](https://keen.io/docs/api/#delete) a key. Once deleted, a key cannot be recovered. Consider `revoke` if you want to keep the key around but deactivate it. 627 | 628 | ``` 629 | access_key = '0000000000000000000000000000000000000000000000000000000000000000' 630 | Keen.access_keys.delete(access_key) 631 | ``` 632 | 633 | ### Additional options 634 | 635 | ##### HTTP Read Timeout 636 | 637 | The default `Net::HTTP` timeout is 60 seconds. That's usually enough, but if you're querying over a large collection you may need to increase it. The timeout on the API side is 300 seconds, so that's as far as you'd want to go. You can configure a read timeout (in seconds) by setting a `KEEN_READ_TIMEOUT` environment variable, or by passing in a `read_timeout` option to the client constructor as follows: 638 | 639 | ``` ruby 640 | keen = Keen::Client.new(:read_timeout => 300) 641 | ``` 642 | 643 | You can also configure the `NET::HTTP` open timeout, default is 60 seconds. To configure the timeout (in seconds) either set `KEEN_OPEN_TIMEOUT` environment variable, or by passing in a `open_timeout` option to the client constructor as follows: 644 | 645 | ``` ruby 646 | keen = Keen::Client.new(:open_timeout => 30) 647 | ``` 648 | 649 | 650 | ##### HTTP Proxy 651 | 652 | You can set the `KEEN_PROXY_TYPE` and `KEEN_PROXY_URL` environment variables to enable HTTP proxying. `KEEN_PROXY_TYPE` should be set to `socks5`. You can also configure this on client instances by passing in `proxy_type` and `proxy_url` keys. 653 | 654 | ``` ruby 655 | keen = Keen::Client.new(:proxy_type => 'socks5', :proxy_url => 'http://localhost:8888') 656 | ``` 657 | 658 | ### Troubleshooting 659 | 660 | ##### EventMachine 661 | 662 | If you run into `Keen::Error: Keen IO Exception: An EventMachine loop must be running to use publish_async calls` or 663 | `Uncaught RuntimeError: eventmachine not initialized: evma_set_pending_connect_timeout`, this means that the EventMachine 664 | loop has died. This can happen for a variety of reasons, and every app is different. [Issue #22](https://github.com/keenlabs/keen-gem/issues/22) shows how to add some extra protection to avoid this situation. 665 | 666 | ##### publish_async in a script or worker 667 | 668 | If you write a script that uses `publish_async`, you need to keep the script alive long enough for the call(s) to complete. 669 | EventMachine itself won't do this because it runs in a different thread. Here's an [example gist](https://gist.github.com/dzello/7472823) that shows how to exit the process after the event has been recorded. 670 | 671 | ### Additional Considerations 672 | 673 | ##### Bots 674 | 675 | It's not just us humans that browse the web. Spiders, crawlers, and bots share the pipes too. When it comes to analytics, this can cause a mild headache. Events generated by bots can inflate your metrics and eat up your event quota. 676 | 677 | If you want some bot protection, check out the [Voight-Kampff](https://github.com/biola/Voight-Kampff) gem. Use the gem's `request.bot?` method to detect bots and avoid logging events. 678 | 679 | ### Changelog 680 | 681 | ##### 1.1.1 682 | + Added an option to log queries 683 | + Added a cli option that includes the Keen code 684 | 685 | ##### 1.1.0 686 | + Add support for Access Keys 687 | + Move saved queries into the Keen namespace 688 | + Deprecate scoped keys in favor of Access Keys 689 | 690 | ##### 1.0.0 691 | + Remove support for ruby 1.9.3 692 | + Update a few dependencies 693 | 694 | ##### 0.9.10 695 | + Add ability to set the `open_time` setting for the http client. 696 | 697 | ##### 0.9.9 698 | + Added the ability to send additional optional headers. 699 | 700 | ##### 0.9.7 701 | + Added a new header `Keen-Sdk` that sends the SDK version information on all requests. 702 | 703 | ##### 0.9.6 704 | + Updated behavior of saved queries to allow fetching results using the READ KEY as opposed to requiring the MASTER KEY, making the gem consistent with https://keen.io/docs/api/#getting-saved-query-results 705 | 706 | ##### 0.9.5 707 | + Fix bug with scoped key generation not working with newer Keen projects. 708 | 709 | ##### 0.9.4 710 | + Add SDK support for Saved Queries 711 | + Removed support for Ruby MRI 1.8.7 712 | 713 | ##### 0.9.2 714 | + Added support for max_age as an integer. 715 | 716 | ##### 0.9.1 717 | + Added support for setting an IV for scoped keys. Thanks [@anatolydwnld](https://github.com/anatolydwnld) 718 | 719 | ##### 0.8.10 720 | + Added support for posting queries. Thanks [@soloman1124](https://github.com/soloman1124). 721 | 722 | ##### 0.8.9 723 | + Fix proxy support for sync client. Thanks [@nvieirafelipe](https://github.com/nvieirafelipe)! 724 | 725 | ##### 0.8.8 726 | + Add support for a configurable read timeout 727 | 728 | ##### 0.8.7 729 | + Add support for returning all keys back from query API responses 730 | 731 | ##### 0.8.6 732 | + Add support for getting [query URLs](https://github.com/keenlabs/keen-gem/pull/47) 733 | + Make the `query` method public so code supporting dynamic analysis types is easier to write 734 | 735 | ##### 0.8.4 736 | + Add support for getting [project details](https://keen.io/docs/api/reference/#project-row-resource?s=gh-gem) 737 | 738 | ##### 0.8.3 739 | + Add support for getting a list of a [project's collections](https://keen.io/docs/api/reference/#event-resource?s=gh-gem) 740 | 741 | ##### 0.8.2 742 | + Add support for `median` and `percentile` analysis 743 | + Support arrays for extraction `property_names` option 744 | 745 | ##### 0.8.1 746 | + Add support for asynchronous batch publishing 747 | 748 | ##### 0.8.0 749 | + **UPGRADE WARNING** Do you use spaces in collection names? Or other special characters? Read [this post](https://groups.google.com/forum/?fromgroups#!topic/keen-io-devs/VtCgPuNKrgY) from the mailing list to make sure your collection names don't change. 750 | + Add support for generating [scoped keys](https://keen.io/docs/security/#scoped-key?s=gh-gem). 751 | + Make collection name encoding more robust. Make sure collection names are encoded identically for publishing events, running queries, and performing deletes. 752 | + Add support for [grouping by multiple properties](https://keen.io/docs/data-analysis/group-by/#grouping-by-multiple-properties?s=gh-gem). 753 | 754 | ##### 0.7.8 755 | + Add support for redirect URL creation. 756 | 757 | ##### 0.7.7 758 | + Add support for HTTP and SOCKS proxies. Set `KEEN_PROXY_URL` to the proxy URL and `KEEN_PROXY_TYPE` to 'socks5' if you need to. These 759 | properties can also be set on the client instances as `proxy_url` and `proxy_type`. 760 | 761 | + Delegate the `master_key` fields from the Keen object. 762 | 763 | ##### 0.7.6 764 | + Explicitly require `CGI`. 765 | 766 | ##### 0.7.5 767 | + Use `CGI.escape` instead of `URI.escape` to get accurate URL encoding for certain characters 768 | 769 | ##### 0.7.4 770 | + Add support for deletes (thanks again [cbartlett](https://github.com/cbartlett)!) 771 | + Allow event collection names for publishing/deleting methods to be symbols 772 | 773 | ##### 0.7.3 774 | + Add batch publishing support 775 | + Allow event collection names for querying methods to be symbols. Thanks to [cbartlett](https://github.com/cbartlett). 776 | 777 | ##### 0.7.2 778 | + Fix support for non-https API URL testing 779 | 780 | ##### 0.7.1 781 | + Allow configuration of the base API URL via the KEEN_API_URL environment variable. Useful for local testing and proxies. 782 | 783 | ##### 0.7.0 784 | + BREAKING CHANGE! Added support for read and write scoped keys to reflect the new Keen IO security architecture. 785 | The advantage of scoped keys is finer grained permission control. Public clients that 786 | publish events (like a web browser) require a key that can write but not read. On the other hand, private dashboards and 787 | server-side querying processes require a Read key that should not be made public. 788 | 789 | ##### 0.6.1 790 | + Improved logging and exception handling. 791 | 792 | ##### 0.6.0 793 | + Added querying capabilities. A big thanks to [ifeelgoods](http://www.ifeelgoods.com/) for contributing! 794 | 795 | ##### 0.5.0 796 | + Removed API Key as a required field on Keen::Client. Only the Project ID is required to publish events. 797 | + You can continue to provide the API Key. Future features planned for this gem will require it. But for now, 798 | there is no keen-gem functionality that uses it. 799 | 800 | ##### 0.4.4 801 | + Event collections are URI escaped to account for spaces. 802 | + User agent of API calls made more granular to aid in support cases. 803 | + Throw arguments error for nil event_collection and properties arguments. 804 | 805 | ##### 0.4.3 806 | + Added beacon_url support 807 | + Add support for using em-synchrony with asynchronous calls 808 | 809 | ### Questions & Support 810 | 811 | For questions, bugs, or suggestions about this gem: 812 | [File a Github Issue](https://github.com/keenlabs/keen-gem/issues). 813 | 814 | For other Keen-IO related technical questions: 815 | ['keen-io' on Stack Overflow](http://stackoverflow.com/questions/tagged/keen-io) 816 | 817 | For general Keen IO discussion & feedback: 818 | ['keen-io-devs' Google Group](https://groups.google.com/forum/#!forum/keen-io-devs) 819 | 820 | ### Contributing 821 | keen-gem is an open source project and we welcome your contributions. 822 | Fire away with issues and pull requests! 823 | 824 | #### Running Tests 825 | 826 | `bundle exec rake spec` - Run unit specs. HTTP is mocked. 827 | 828 | `bundle exec rake integration` - Run integration specs with the real API. Requires env variables. See [.travis.yml](https://github.com/keenlabs/keen-gem/blob/master/.travis.yml). 829 | 830 | `bundle exec rake synchrony` - Run async publishing specs with `EM::Synchrony`. 831 | 832 | Similarly, you can use guard to listen for changes to files and run specs. 833 | 834 | `bundle exec guard -g unit` 835 | 836 | `bundle exec guard -g integration` 837 | 838 | `bundle exec guard -g synchrony` 839 | 840 | #### Running a Local Console 841 | 842 | You can spawn an `irb` session with the local files already loaded for debugging 843 | or experimentation. 844 | 845 | ``` 846 | $ bundle exec rake console 847 | 2.2.6 :001 > Keen 848 | => Keen 849 | ``` 850 | ### Community Contributors 851 | + [alexkwolfe](https://github.com/alexkwolfe) 852 | + [peteygao](https://github.com/peteygao) 853 | + [obieq](https://github.com/obieq) 854 | + [cbartlett](https://github.com/cbartlett) 855 | + [myrridin](https://github.com/myrridin) 856 | 857 | Thanks everyone, you rock! 858 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rspec/core/rake_task' 3 | 4 | desc "Run Rspec unit tests" 5 | RSpec::Core::RakeTask.new(:spec) do |t| 6 | t.pattern = "spec/keen/**/*_spec.rb" 7 | end 8 | 9 | desc "Run Rspec integration tests" 10 | RSpec::Core::RakeTask.new(:integration) do |t| 11 | t.pattern = "spec/integration/**/*_spec.rb" 12 | end 13 | 14 | desc "Run Rspec synchrony tests" 15 | RSpec::Core::RakeTask.new(:synchrony) do |t| 16 | t.pattern = "spec/synchrony/**/*_spec.rb" 17 | end 18 | 19 | desc "Run Rspec pattern" 20 | RSpec::Core::RakeTask.new(:pattern) do |t| 21 | t.pattern = "spec/#{ENV['PATTERN']}/**/*_spec.rb" 22 | end 23 | 24 | desc "Start a development console with local code loaded" 25 | task :console do 26 | exec "irb -r keen -I ./lib" 27 | end 28 | 29 | task :default => :spec 30 | task :test => [:spec] 31 | -------------------------------------------------------------------------------- /keen.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "keen/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "keen" 7 | s.version = Keen::VERSION 8 | s.authors = ["Alex Kleissner", "Joe Wegner"] 9 | s.email = "opensource@keen.io" 10 | s.homepage = "https://github.com/keenlabs/keen-gem" 11 | s.summary = "Keen IO API Client" 12 | s.description = "Send events and build analytics features into your Ruby applications." 13 | s.license = "MIT" 14 | 15 | s.add_dependency "multi_json", "~> 1.12" 16 | s.add_dependency "addressable", "~> 2.5" 17 | 18 | s.add_dependency 'rubysl', '~> 2.0' if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 19 | 20 | # guard 21 | s.add_development_dependency 'guard', '~> 2.14' 22 | s.add_development_dependency 'guard-rspec', '~> 4.7' 23 | 24 | # guard cross-platform listener trick 25 | s.add_development_dependency 'rb-inotify', '~> 0.9' 26 | s.add_development_dependency 'rb-fsevent', '~> 0.9' 27 | s.add_development_dependency 'rb-fchange', '~> 0.0.6' 28 | 29 | # guard notifications 30 | s.add_development_dependency 'ruby_gntp', '~> 0.3' 31 | 32 | # fix guard prompt 33 | s.add_development_dependency 'rb-readline', '~> 0.5' # or compile ruby w/ readline 34 | 35 | s.files = `git ls-files`.split("\n") 36 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 37 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 38 | s.require_paths = ["lib"] 39 | end 40 | -------------------------------------------------------------------------------- /lib/keen.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'forwardable' 3 | 4 | require 'keen/access_keys' 5 | require 'keen/client' 6 | require 'keen/saved_queries' 7 | require 'keen/cached_datasets' 8 | require 'keen/scoped_key' 9 | 10 | module Keen 11 | class Error < RuntimeError 12 | attr_accessor :original_error 13 | def initialize(message, _original_error=nil) 14 | self.original_error = _original_error 15 | super(message) 16 | end 17 | 18 | def to_s 19 | "Keen IO Exception: #{super}" 20 | end 21 | end 22 | 23 | class ConfigurationError < Error; end 24 | class HttpError < Error; end 25 | class BadRequestError < HttpError; end 26 | class AuthenticationError < HttpError; end 27 | class NotFoundError < HttpError; end 28 | 29 | class << self 30 | extend Forwardable 31 | 32 | def_delegators :default_client, 33 | :project_id, :project_id=, 34 | :write_key, :write_key=, 35 | :read_key, :read_key=, 36 | :master_key, :master_key=, 37 | :api_url, :api_url=, 38 | :log_queries, :log_queries= 39 | 40 | def_delegators :default_client, 41 | :proxy_url, :proxy_url=, 42 | :proxy_type, :proxy_type= 43 | 44 | def_delegators :default_client, 45 | :publish, :publish_async, :publish_batch, 46 | :publish_batch_async, :beacon_url, :redirect_url 47 | 48 | def_delegators :default_client, 49 | :count, :count_unique, :minimum, :maximum, 50 | :sum, :average, :select_unique, :funnel, :extraction, 51 | :multi_analysis, :median, :percentile 52 | 53 | def_delegators :default_client, 54 | :delete, 55 | :event_collections, 56 | :project_info, 57 | :query_url, 58 | :query, 59 | :saved_queries, 60 | :access_keys 61 | 62 | attr_writer :logger 63 | 64 | def logger 65 | @logger ||= lambda { 66 | logger = Logger.new($stdout) 67 | logger.level = Logger::INFO 68 | logger 69 | }.call 70 | end 71 | 72 | private 73 | 74 | def default_client 75 | @default_client ||= Keen::Client.new( 76 | :project_id => ENV['KEEN_PROJECT_ID'], 77 | :write_key => ENV['KEEN_WRITE_KEY'], 78 | :read_key => ENV['KEEN_READ_KEY'], 79 | :master_key => ENV['KEEN_MASTER_KEY'], 80 | :api_url => ENV['KEEN_API_URL'], 81 | :proxy_url => ENV['KEEN_PROXY_URL'], 82 | :proxy_type => ENV['KEEN_PROXY_TYPE'], 83 | :read_timeout => ENV['KEEN_READ_TIMEOUT'], 84 | :open_timeout => ENV['KEEN_OPEN_TIMEOUT'] 85 | ) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/keen/access_keys.rb: -------------------------------------------------------------------------------- 1 | require 'multi_json' 2 | 3 | module Keen 4 | class AccessKeys 5 | def initialize(client) 6 | @client = client 7 | end 8 | 9 | def get(key) 10 | client.ensure_master_key! 11 | path = "/#{key}" 12 | 13 | response = access_keys_get(client.master_key, path) 14 | client.process_response(response.code.to_i, response.body) 15 | end 16 | 17 | def all() 18 | client.ensure_master_key! 19 | 20 | response = access_keys_get(client.master_key) 21 | client.process_response(response.code.to_i, response.body) 22 | end 23 | 24 | # For information on the format of the key_body, see 25 | # https://keen.io/docs/api/#access-keys 26 | def create(key_body) 27 | client.ensure_master_key! 28 | 29 | path = "" 30 | response = access_keys_post(client.master_key, path, key_body) 31 | client.process_response(response.code.to_i, response.body) 32 | end 33 | 34 | def update(key, key_body) 35 | client.ensure_master_key! 36 | 37 | path = "/#{key}" 38 | response = access_keys_post(client.master_key, path, key_body) 39 | client.process_response(response.code.to_i, response.body) 40 | end 41 | 42 | def revoke(key) 43 | client.ensure_master_key! 44 | 45 | path = "/#{key}/revoke" 46 | response = access_keys_post(client.master_key, path) 47 | client.process_response(response.code.to_i, response.body) 48 | end 49 | 50 | def unrevoke(key) 51 | client.ensure_master_key! 52 | 53 | path = "/#{key}/unrevoke" 54 | response = access_keys_post(client.master_key, path) 55 | client.process_response(response.code.to_i, response.body) 56 | end 57 | 58 | def delete(key) 59 | client.ensure_master_key! 60 | 61 | response = Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).delete( 62 | path: access_keys_base_url + "/#{key}", 63 | headers: client.api_headers(client.master_key, "sync") 64 | ) 65 | 66 | client.process_response(response.code.to_i, response.body) 67 | end 68 | 69 | def access_keys_base_url 70 | client.ensure_project_id! 71 | "/#{client.api_version}/projects/#{client.project_id}/keys" 72 | end 73 | 74 | private 75 | 76 | attr_reader :client 77 | 78 | def access_keys_get(api_key, path = "") 79 | Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).get( 80 | path: access_keys_base_url + path, 81 | headers: client.api_headers(api_key, "sync") 82 | ) 83 | end 84 | 85 | def access_keys_post(api_key, path = "", body = "") 86 | Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).post( 87 | path: access_keys_base_url + path, 88 | headers: client.api_headers(api_key, "sync"), 89 | body: MultiJson.dump(body) 90 | ) 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/keen/aes_helper.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'digest' 3 | require 'base64' 4 | 5 | module Keen 6 | class AESHelper 7 | 8 | class << self 9 | def aes256_decrypt(key, iv_plus_encrypted) 10 | aes = OpenSSL::Cipher::AES.new(256, :CBC) 11 | iv = iv_plus_encrypted[0, aes.key_len] 12 | encrypted = iv_plus_encrypted[aes.key_len, iv_plus_encrypted.length] 13 | aes.decrypt 14 | aes.key = unhexlify(key) 15 | aes.iv = unhexlify(iv) 16 | aes.update(unhexlify(encrypted)) + aes.final 17 | end 18 | 19 | def aes256_encrypt(key, plaintext, iv = nil) 20 | raise OpenSSL::Cipher::CipherError.new("iv must be 16 bytes") if !iv.nil? && iv.length != 16 21 | aes = OpenSSL::Cipher::AES.new(256, :CBC) 22 | aes.encrypt 23 | aes.key = unhexlify(key) 24 | aes.iv = iv unless iv.nil? 25 | iv ||= aes.random_iv 26 | hexlify(iv) + hexlify(aes.update(plaintext) + aes.final) 27 | end 28 | 29 | def hexlify(msg) 30 | msg.unpack('H*')[0] 31 | end 32 | 33 | def unhexlify(msg) 34 | [msg].pack('H*') 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/keen/aes_helper_old.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'digest' 3 | require 'base64' 4 | 5 | module Keen 6 | class AESHelperOld 7 | 8 | BLOCK_SIZE = 32 9 | 10 | class << self 11 | def aes256_decrypt(key, iv_plus_encrypted) 12 | padded_key = pad(key) 13 | unhexed_iv_plus_encrypted = unhexlify(iv_plus_encrypted) 14 | iv = unhexed_iv_plus_encrypted[0, 16] 15 | encrypted = unhexed_iv_plus_encrypted[16, unhexed_iv_plus_encrypted.length] 16 | aes = OpenSSL::Cipher::AES.new(256, :CBC) 17 | aes.decrypt 18 | aes.key = padded_key 19 | aes.iv = iv 20 | aes.update(encrypted) + aes.final 21 | end 22 | 23 | def aes256_encrypt(key, plaintext, iv = nil) 24 | raise OpenSSL::Cipher::CipherError.new("iv must be 16 bytes") if !iv.nil? && iv.length != 16 25 | padded_key = pad(key) 26 | aes = OpenSSL::Cipher::AES.new(256, :CBC) 27 | aes.encrypt 28 | aes.key = padded_key 29 | aes.iv = iv unless iv.nil? 30 | iv ||= aes.random_iv 31 | encrypted = aes.update(plaintext) + aes.final 32 | hexlify(iv) + hexlify(encrypted) 33 | end 34 | 35 | def hexlify(msg) 36 | msg.unpack('H*')[0] 37 | end 38 | 39 | def unhexlify(msg) 40 | [msg].pack('H*') 41 | end 42 | 43 | def pad(msg) 44 | missing_chars = msg.length % BLOCK_SIZE 45 | return msg if missing_chars == 0 46 | 47 | pad_len = BLOCK_SIZE - missing_chars 48 | 49 | padding = pad_len.chr * pad_len 50 | padded = msg + padding 51 | padded 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/keen/cached_datasets.rb: -------------------------------------------------------------------------------- 1 | require 'keen/version' 2 | require "json" 3 | require 'uri' 4 | 5 | module Keen 6 | class CachedDatasets 7 | def initialize(client) 8 | @client = client 9 | end 10 | 11 | def list(limit: nil, after_name: nil) 12 | client.ensure_master_key! 13 | 14 | query_params = clear_nil_attributes(limit: limit, after_name: after_name) 15 | response = _http_get("", query_params) 16 | client.process_response(response.code.to_i, response.body) 17 | end 18 | 19 | def get_definition(dataset_name) 20 | client.ensure_master_key! 21 | response = _http_get("/#{dataset_name}") 22 | client.process_response(response.code.to_i, response.body) 23 | end 24 | 25 | def get_results(dataset_name, timeframe, index_by, api_key = nil) 26 | api_key || client.ensure_read_key! 27 | api_key = api_key || client.read_key 28 | path = "/#{dataset_name}/results" 29 | 30 | params = { 31 | timeframe: timeframe.is_a?(Hash) ? MultiJson.encode(timeframe) : timeframe, 32 | index_by: index_by 33 | } 34 | response = _http_get(path, params, api_key) 35 | client.process_response(response.code.to_i, response.body) 36 | end 37 | 38 | def create(name, index_by, query, display_name) 39 | client.ensure_master_key! 40 | 41 | request_body = { 42 | 'query' => clear_nil_attributes(query), 43 | 'index_by' => index_by, 44 | 'display_name' => display_name 45 | } 46 | 47 | response = Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).put( 48 | path: "#{datasets_base_url}/#{name}", 49 | headers: api_headers(client.master_key, "sync"), 50 | body: MultiJson.encode(request_body) 51 | ) 52 | client.process_response(response.code.to_i, response.body) 53 | end 54 | 55 | def delete(dataset_name) 56 | client.ensure_master_key! 57 | 58 | response = Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).delete( 59 | path: "#{datasets_base_url}/#{dataset_name}", 60 | headers: api_headers(client.master_key, "sync") 61 | ) 62 | client.process_response(response.code.to_i, response.body) 63 | end 64 | 65 | private 66 | 67 | attr_reader :client 68 | 69 | def _http_get(path = "", query_params = {}, api_key = nil) 70 | Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).get( 71 | path: [datasets_base_url + path, URI.encode_www_form(query_params)].compact.join('?'), 72 | headers: api_headers(api_key || client.master_key, "sync") 73 | ) 74 | end 75 | 76 | def datasets_base_url 77 | client.ensure_project_id! 78 | "/#{client.api_version}/projects/#{client.project_id}/datasets" 79 | end 80 | 81 | def api_headers(authorization, sync_type) 82 | user_agent = "keen-gem, v#{Keen::VERSION}, #{sync_type}" 83 | user_agent += ", #{RUBY_VERSION}, #{RUBY_PLATFORM}, #{RUBY_PATCHLEVEL}" 84 | if defined?(RUBY_ENGINE) 85 | user_agent += ", #{RUBY_ENGINE}" 86 | end 87 | { "Content-Type" => "application/json", 88 | "User-Agent" => user_agent, 89 | "Authorization" => authorization, 90 | "Keen-Sdk" => "ruby-#{Keen::VERSION}" } 91 | end 92 | 93 | # Remove any attributes with nil values in a saved query hash. The API will 94 | # already assume missing attributes are nil 95 | def clear_nil_attributes(hash) 96 | hash.reject! do |key, value| 97 | if value.nil? 98 | true 99 | elsif value.is_a? Hash 100 | value.reject! { |inner_key, inner_value| inner_value.nil? } 101 | else 102 | false 103 | end 104 | end 105 | 106 | hash 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/keen/client.rb: -------------------------------------------------------------------------------- 1 | require 'keen/http' 2 | require 'keen/version' 3 | require 'keen/client/publishing_methods' 4 | require 'keen/client/querying_methods' 5 | require 'keen/client/maintenance_methods' 6 | require 'keen/version' 7 | require 'openssl' 8 | require 'multi_json' 9 | require 'base64' 10 | require 'cgi' 11 | require 'addressable/uri' 12 | 13 | module Keen 14 | class Client 15 | include Keen::Client::PublishingMethods 16 | include Keen::Client::QueryingMethods 17 | include Keen::Client::MaintenanceMethods 18 | 19 | attr_accessor :project_id, :write_key, :read_key, :master_key, :api_url, :proxy_url, :proxy_type, :read_timeout, :log_queries, :open_timeout 20 | 21 | CONFIG = { 22 | :api_url => "https://api.keen.io", 23 | :api_version => "3.0", 24 | :api_headers => lambda { |authorization, sync_or_async| 25 | user_agent = "keen-gem, v#{Keen::VERSION}, #{sync_or_async}" 26 | user_agent += ", #{RUBY_VERSION}, #{RUBY_PLATFORM}, #{RUBY_PATCHLEVEL}" 27 | if defined?(RUBY_ENGINE) 28 | user_agent += ", #{RUBY_ENGINE}" 29 | end 30 | { "Content-Type" => "application/json", 31 | "User-Agent" => user_agent, 32 | "Authorization" => authorization, 33 | "Keen-Sdk" => "ruby-#{Keen::VERSION}" } 34 | } 35 | } 36 | 37 | def initialize(*args) 38 | options = args[0] 39 | unless options.is_a?(Hash) 40 | # deprecated, pass a hash of options instead 41 | options = { 42 | :project_id => args[0], 43 | :write_key => args[1], 44 | :read_key => args[2], 45 | }.merge(args[3] || {}) 46 | end 47 | 48 | self.project_id, self.write_key, self.read_key, self.master_key = options.values_at( 49 | :project_id, :write_key, :read_key, :master_key) 50 | 51 | self.api_url = options[:api_url] || CONFIG[:api_url] 52 | 53 | self.proxy_url, self.proxy_type = options.values_at(:proxy_url, :proxy_type) 54 | 55 | self.read_timeout = options[:read_timeout].to_f unless options[:read_timeout].nil? 56 | 57 | self.open_timeout = options[:open_timeout].to_f unless options[:open_timeout].nil? 58 | end 59 | 60 | def saved_queries 61 | @saved_queries ||= SavedQueries.new(self) 62 | end 63 | 64 | def cached_datasets 65 | @cached_datasets ||= CachedDatasets.new(self) 66 | end 67 | 68 | def access_keys 69 | @access_keys ||= AccessKeys.new(self) 70 | end 71 | 72 | def process_response(status_code, response_body) 73 | case status_code.to_i 74 | when 200..201 75 | begin 76 | return MultiJson.decode(response_body) 77 | rescue 78 | Keen.logger.warn("Invalid JSON for response code #{status_code}: #{response_body}") 79 | return {} 80 | end 81 | when 204 82 | return true 83 | when 400 84 | raise BadRequestError.new(response_body) 85 | when 401 86 | raise AuthenticationError.new(response_body) 87 | when 404 88 | raise NotFoundError.new(response_body) 89 | else 90 | raise HttpError.new(response_body) 91 | end 92 | end 93 | 94 | def ensure_project_id! 95 | raise ConfigurationError, "Project ID must be set" unless self.project_id 96 | end 97 | 98 | def ensure_write_key! 99 | raise ConfigurationError, "Write Key must be set for this operation" unless self.write_key 100 | end 101 | 102 | def ensure_master_key! 103 | raise ConfigurationError, "Master Key must be set for this operation" unless self.master_key 104 | end 105 | 106 | def ensure_read_key! 107 | raise ConfigurationError, "Read Key must be set for this operation" unless self.read_key 108 | end 109 | 110 | private 111 | 112 | def api_event_collection_resource_path(event_collection) 113 | encoded_collection_name = Addressable::URI.encode_component(event_collection.to_s) 114 | encoded_collection_name.gsub! '/', '%2F' 115 | "/#{api_version}/projects/#{project_id}/events/#{encoded_collection_name}" 116 | end 117 | 118 | def preprocess_params(params) 119 | params = params.delete_if {|key, val| val.nil?} 120 | 121 | preprocess_encodables(params) 122 | preprocess_timeframe(params) 123 | preprocess_max_age(params) 124 | preprocess_group_by(params) 125 | preprocess_percentile(params) 126 | preprocess_property_names(params) 127 | 128 | params.map { |key, value| "#{key}=#{CGI.escape(value)}" }.join('&') 129 | end 130 | 131 | def preprocess_encodables(params) 132 | [:filters, :steps, :analyses].each do |key| 133 | if params.key?(key) 134 | params[key] = MultiJson.encode(params[key]) 135 | end 136 | end 137 | end 138 | 139 | def preprocess_timeframe(params) 140 | timeframe = params[:timeframe] 141 | if timeframe.is_a?(Hash) 142 | params[:timeframe] = MultiJson.encode(timeframe) 143 | end 144 | end 145 | 146 | def preprocess_group_by(params) 147 | group_by = params[:group_by] 148 | if group_by.is_a?(Array) 149 | params[:group_by] = MultiJson.encode(group_by) 150 | end 151 | end 152 | 153 | def preprocess_max_age(params) 154 | max_age = params[:max_age] 155 | if max_age.is_a? Numeric 156 | params[:max_age] = params[:max_age].to_s 157 | else 158 | params.delete(:max_age) 159 | end 160 | end 161 | 162 | def preprocess_percentile(params) 163 | if params.key?(:percentile) 164 | params[:percentile] = params[:percentile].to_s 165 | end 166 | end 167 | 168 | def preprocess_property_names(params) 169 | property_names = params[:property_names] 170 | if property_names.is_a?(Array) 171 | params[:property_names] = MultiJson.encode(property_names) 172 | end 173 | end 174 | 175 | def method_missing(_method, *args, &block) 176 | if config = CONFIG[_method.to_sym] 177 | if config.is_a?(Proc) 178 | config.call(*args) 179 | else 180 | config 181 | end 182 | else 183 | super 184 | end 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /lib/keen/client/maintenance_methods.rb: -------------------------------------------------------------------------------- 1 | module Keen 2 | class Client 3 | module MaintenanceMethods 4 | 5 | # Runs a delete query. 6 | # See detailed documentation here: 7 | # https://keen.io/docs/maintenance/#deleting-event-collections 8 | # 9 | # @param event_collection 10 | # @param params [Hash] (optional) 11 | # filters (optional) [Array] 12 | def delete(event_collection, params={}) 13 | ensure_project_id! 14 | ensure_master_key! 15 | 16 | query_params = preprocess_params(params) if params != {} 17 | 18 | begin 19 | response = http_sync.delete( 20 | :path => [api_event_collection_resource_path(event_collection), query_params].compact.join('?'), 21 | :headers => api_headers(self.master_key, "sync")) 22 | rescue Exception => http_error 23 | raise HttpError.new("Couldn't perform delete of #{event_collection} on Keen IO: #{http_error.message}", http_error) 24 | end 25 | 26 | response_body = response.body ? response.body.chomp : '' 27 | process_response(response.code, response_body) 28 | end 29 | 30 | # Return list of collections for the configured project 31 | # See detailed documentation here: 32 | # https://keen.io/docs/api/reference/#event-resource 33 | def event_collections 34 | ensure_project_id! 35 | ensure_master_key! 36 | 37 | begin 38 | response = http_sync.get( 39 | :path => "/#{api_version}/projects/#{project_id}/events", 40 | :headers => api_headers(self.master_key, "sync")) 41 | rescue Exception => http_error 42 | raise HttpError.new("Couldn't perform events on Keen IO: #{http_error.message}", http_error) 43 | end 44 | 45 | response_body = response.body ? response.body.chomp : '' 46 | process_response(response.code, response_body) 47 | end 48 | 49 | # Return details for the current project 50 | # See detailed documentation here: 51 | # https://keen.io/docs/api/reference/#project-resource 52 | def project_info 53 | ensure_project_id! 54 | ensure_master_key! 55 | 56 | begin 57 | response = http_sync.get( 58 | :path => "/#{api_version}/projects/#{project_id}", 59 | :headers => api_headers(self.master_key, "sync")) 60 | rescue Exception => http_error 61 | raise HttpError.new("Couldn't perform project info on Keen IO: #{http_error.message}", http_error) 62 | end 63 | 64 | response_body = response.body ? response.body.chomp : '' 65 | process_response(response.code, response_body) 66 | end 67 | 68 | # Return the named collection for the configured project 69 | # See detailed documentation here: 70 | # https://keen.io/docs/api/reference/#event-collection-resource 71 | def event_collection(event_collection) 72 | ensure_project_id! 73 | ensure_master_key! 74 | 75 | begin 76 | response = http_sync.get( 77 | :path => "/#{api_version}/projects/#{project_id}/events/#{event_collection}", 78 | :headers => api_headers(self.master_key, "sync")) 79 | rescue Exception => http_error 80 | raise HttpError.new("Couldn't perform events on Keen IO: #{http_error.message}", http_error) 81 | end 82 | 83 | response_body = response.body ? response.body.chomp : '' 84 | process_response(response.code, response_body) 85 | end 86 | 87 | private 88 | 89 | def http_sync 90 | @http_sync ||= Keen::HTTP::Sync.new(self.api_url, self.proxy_url, self.read_timeout, self.open_timeout) 91 | end 92 | 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/keen/client/publishing_methods.rb: -------------------------------------------------------------------------------- 1 | module Keen 2 | class Client 3 | module PublishingMethods 4 | 5 | # @deprecated 6 | # 7 | # Publishes a synchronous event 8 | # @param event_collection 9 | # @param [Hash] event properties 10 | # 11 | # @return the JSON response from the API 12 | def add_event(event_collection, properties, options={}) 13 | self.publish(event_collection, properties) 14 | end 15 | 16 | # Publishes a synchronous event 17 | # See detailed documentation here 18 | # https://keen.io/docs/api/reference/#event-collection-resource 19 | # 20 | # @param event_collection 21 | # @param [Hash] event properties 22 | # 23 | # @return the JSON response from the API 24 | def publish(event_collection, properties) 25 | ensure_project_id! 26 | ensure_write_key! 27 | check_event_data!(event_collection, properties) 28 | publish_body( 29 | api_event_collection_resource_path(event_collection), 30 | MultiJson.encode(properties), 31 | "publish") 32 | end 33 | 34 | # Publishes a batch of events 35 | # See detailed documentation here 36 | # https://keen.io/docs/api/reference/#post-request-body-example-of-batch-event-posting 37 | # 38 | # @param events - a hash where the keys are event collection names 39 | # and the values are arrays of hashes (event properties) 40 | # 41 | # @return the JSON response from the API 42 | def publish_batch(events) 43 | ensure_project_id! 44 | ensure_write_key! 45 | publish_body( 46 | api_events_resource_path, 47 | MultiJson.encode(events), 48 | "publish") 49 | end 50 | 51 | # Publishes an asynchronous event 52 | # See detailed documentation here 53 | # https://keen.io/docs/api/reference/#event-collection-resource 54 | # 55 | # @param event_collection 56 | # @param [Hash] event properties 57 | # 58 | # @return a deferrable to apply callbacks to 59 | def publish_async(event_collection, properties) 60 | ensure_project_id! 61 | ensure_write_key! 62 | check_event_data!(event_collection, properties) 63 | 64 | http_client = Keen::HTTP::Async.new( 65 | self.api_url, 66 | {:proxy_url => self.proxy_url, :proxy_type => self.proxy_type}) 67 | http = http_client.post( 68 | :path => api_event_collection_resource_path(event_collection), 69 | :headers => api_headers(self.write_key, "async"), 70 | :body => MultiJson.encode(properties) 71 | ) 72 | 73 | if defined?(EM::Synchrony) 74 | process_with_synchrony(http) 75 | else 76 | process_with_callbacks(http) 77 | end 78 | end 79 | 80 | def publish_batch_async(events) 81 | ensure_project_id! 82 | ensure_write_key! 83 | 84 | http_client = Keen::HTTP::Async.new( 85 | self.api_url, 86 | {:proxy_url => self.proxy_url, :proxy_type => self.proxy_type}) 87 | 88 | http = http_client.post( 89 | :path => api_events_resource_path, 90 | :headers => api_headers(self.write_key, "async"), 91 | :body => MultiJson.encode(events) 92 | ) 93 | if defined?(EM::Synchrony) 94 | process_with_synchrony(http) 95 | else 96 | process_with_callbacks(http) 97 | end 98 | end 99 | 100 | # Returns an encoded URL that will record an event. Useful in email situations. 101 | # See detailed documentation here 102 | # https://keen.io/docs/api/reference/#event-collection-resource 103 | # 104 | # @param event_collection 105 | # @param [Hash] event properties 106 | # 107 | # @return a URL that will track an event when hit 108 | def beacon_url(event_collection, properties) 109 | json = MultiJson.encode(properties) 110 | data = [json].pack("m0").tr("+/", "-_").gsub("\n", "") 111 | "#{self.api_url}#{api_event_collection_resource_path(event_collection)}?api_key=#{self.write_key}&data=#{data}" 112 | end 113 | 114 | # Returns an encoded URL that will record an event and then redirect. Useful in email situations. 115 | # See detailed documentation here 116 | # https://keen.io/docs/api/reference/#event-collection-resource 117 | # 118 | # @param event_collection 119 | # @param [Hash] event properties 120 | # 121 | # @return a URL that will track an event when hit 122 | def redirect_url(event_collection, properties, redirect_url) 123 | require 'open-uri' 124 | encoded_url = CGI::escape(redirect_url) 125 | json = MultiJson.encode(properties) 126 | data = [json].pack("m0").tr("+/", "-_").gsub("\n", "") 127 | "#{self.api_url}#{api_event_collection_resource_path(event_collection)}?api_key=#{self.write_key}&data=#{data}&redirect=#{encoded_url}" 128 | end 129 | 130 | private 131 | 132 | def process_with_synchrony(http) 133 | if http.error 134 | error = HttpError.new("HTTP em-synchrony publish_async error: #{http.error}") 135 | Keen.logger.error(error) 136 | raise error 137 | else 138 | process_response(http.response_header.status, http.response.chomp) 139 | end 140 | end 141 | 142 | def process_with_callbacks(http) 143 | deferrable = EventMachine::DefaultDeferrable.new 144 | http.callback { 145 | begin 146 | response = process_response(http.response_header.status, http.response.chomp) 147 | rescue Exception => e 148 | Keen.logger.error(e) 149 | deferrable.fail(e) 150 | end 151 | deferrable.succeed(response) if response 152 | } 153 | http.errback { 154 | error = Error.new("HTTP publish_async failure: #{http.error}") 155 | Keen.logger.error(error) 156 | deferrable.fail(error) 157 | } 158 | deferrable 159 | end 160 | 161 | def publish_body(path, body, error_method) 162 | begin 163 | response = Keen::HTTP::Sync.new( 164 | self.api_url, self.proxy_url, self.read_timeout, self.open_timeout).post( 165 | :path => path, 166 | :headers => api_headers(self.write_key, "sync"), 167 | :body => body) 168 | rescue Exception => http_error 169 | raise HttpError.new("HTTP #{error_method} failure: #{http_error.message}", http_error) 170 | end 171 | process_response(response.code, response.body.chomp) 172 | end 173 | 174 | def api_events_resource_path 175 | "/#{api_version}/projects/#{project_id}/events" 176 | end 177 | 178 | def check_event_data!(event_collection, properties) 179 | raise ArgumentError, "Event collection can not be nil" unless event_collection 180 | raise ArgumentError, "Event properties can not be nil" unless properties 181 | end 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/keen/client/querying_methods.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Keen 4 | class Client 5 | module QueryingMethods 6 | 7 | # Runs a count query. 8 | # See detailed documentation here: 9 | # https://keen.io/docs/api/reference/#count-resource 10 | # 11 | # @param event_collection 12 | # @param params [Hash] (optional) 13 | # group_by (optional) 14 | # timeframe (optional) 15 | # interval (optional) 16 | # filters (optional) [Array] 17 | # timezone (optional) 18 | def count(event_collection, params={}, options={}) 19 | query(__method__, event_collection, params, options) 20 | end 21 | 22 | # Runs a count unique query. 23 | # See detailed documentation here: 24 | # https://keen.io/docs/api/reference/#count-unique-resource 25 | # 26 | # @param event_collection 27 | # @param params [Hash] (optional) 28 | # target_property (required) 29 | # group_by (optional) 30 | # timeframe (optional) 31 | # interval (optional) 32 | # filters (optional) [Array] 33 | # timezone (optional) 34 | def count_unique(event_collection, params, options={}) 35 | query(__method__, event_collection, params, options) 36 | end 37 | 38 | # Runs a minimum query. 39 | # See detailed documentation here: 40 | # https://keen.io/docs/api/reference/#minimum-resource 41 | # 42 | # @param event_collection 43 | # @param params [Hash] (optional) 44 | # target_property (required) 45 | # group_by (optional) 46 | # timeframe (optional) 47 | # interval (optional) 48 | # filters (optional) [Array] 49 | # timezone (optional) 50 | def minimum(event_collection, params, options={}) 51 | query(__method__, event_collection, params, options) 52 | end 53 | 54 | # Runs a maximum query. 55 | # See detailed documentation here: 56 | # https://keen.io/docs/api/reference/#maximum-resource 57 | # 58 | # @param event_collection 59 | # @param params [Hash] (optional) 60 | # target_property (required) 61 | # group_by (optional) 62 | # timeframe (optional) 63 | # interval (optional) 64 | # filters (optional) [Array] 65 | # timezone (optional) 66 | def maximum(event_collection, params, options={}) 67 | query(__method__, event_collection, params, options) 68 | end 69 | 70 | # Runs a sum query. 71 | # See detailed documentation here: 72 | # https://keen.io/docs/api/reference/#sum-resource 73 | # 74 | # @param event_collection 75 | # @param params [Hash] (optional) 76 | # target_property (required) 77 | # group_by (optional) 78 | # timeframe (optional) 79 | # interval (optional) 80 | # filters (optional) [Array] 81 | # timezone (optional) 82 | def sum(event_collection, params, options={}) 83 | query(__method__, event_collection, params, options) 84 | end 85 | 86 | # Runs a average query. 87 | # See detailed documentation here: 88 | # https://keen.io/docs/api/reference/#average-resource 89 | # 90 | # @param event_collection 91 | # @param params [Hash] (optional) 92 | # target_property (required) 93 | # group_by (optional) 94 | # timeframe (optional) 95 | # interval (optional) 96 | # filters (optional) [Array] 97 | # timezone (optional) 98 | def average(event_collection, params, options={}) 99 | query(__method__, event_collection, params, options) 100 | end 101 | 102 | # Runs a median query. 103 | # See detailed documentation here: 104 | # https://keen.io/docs/api/reference/#median-resource 105 | # 106 | # @param event_collection 107 | # @param params [Hash] (optional) 108 | # target_property (required) 109 | # group_by (optional) 110 | # timeframe (optional) 111 | # interval (optional) 112 | # filters (optional) [Array] 113 | # timezone (optional) 114 | def median(event_collection, params, options={}) 115 | query(__method__, event_collection, params, options) 116 | end 117 | 118 | # Runs a percentile query. 119 | # See detailed documentation here: 120 | # https://keen.io/docs/api/reference/#percentile-resource 121 | # 122 | # @param event_collection 123 | # @param params [Hash] (optional) 124 | # target_property (required) 125 | # percentile (required) 126 | # group_by (optional) 127 | # timeframe (optional) 128 | # interval (optional) 129 | # filters (optional) [Array] 130 | # timezone (optional) 131 | def percentile(event_collection, params, options={}) 132 | query(__method__, event_collection, params, options) 133 | end 134 | 135 | # Runs a select_unique query. 136 | # See detailed documentation here: 137 | # https://keen.io/docs/api/reference/#select-unique-resource 138 | # 139 | # @param event_collection 140 | # @param params [Hash] (optional) 141 | # target_property (required) 142 | # group_by (optional) 143 | # timeframe (optional) 144 | # interval (optional) 145 | # filters (optional) [Array] 146 | # timezone (optional) 147 | def select_unique(event_collection, params, options={}) 148 | query(__method__, event_collection, params, options) 149 | end 150 | 151 | # Runs a extraction query. 152 | # See detailed documentation here: 153 | # https://keen.io/docs/api/reference/#extraction-resource 154 | # 155 | # @param event_collection 156 | # @param params [Hash] (optional) 157 | # target_property (required) 158 | # group_by (optional) 159 | # timeframe (optional) 160 | # interval (optional) 161 | # filters (optional) [Array] 162 | # timezone (optional) 163 | # latest (optional) 164 | def extraction(event_collection, params={}, options={}) 165 | query(__method__, event_collection, params, options) 166 | end 167 | 168 | # Runs a funnel query. 169 | # See detailed documentation here: 170 | # https://keen.io/docs/api/reference/#funnel-resource 171 | # 172 | # @param event_collection 173 | # @param params [Hash] (optional) 174 | # steps (required) 175 | def funnel(params, options={}) 176 | query(__method__, nil, params, options) 177 | end 178 | 179 | # Runs a multi-analysis query 180 | # See detailed documentation here: 181 | # https://keen.io/docs/data-analysis/multi-analysis/ 182 | # 183 | # NOTE: why isn't multi-analysis listed in the 184 | # API Technical Reference? 185 | # 186 | # @param event_collection 187 | # @param params [Hash] 188 | # analyses [Hash] (required) 189 | # label (required) 190 | # analysis_type (required) 191 | # target_property (optional) 192 | def multi_analysis(event_collection, params, options={}) 193 | query(__method__, event_collection, params, options) 194 | end 195 | 196 | # Returns the URL for a Query without running it 197 | # @param event_colection 198 | # @param params [Hash] (required) 199 | # analysis_type (required) 200 | # group_by (optional) 201 | # timeframe (optional) 202 | # interval (optional) 203 | # filters (optional) [Array] 204 | # timezone (optional) 205 | # @param options 206 | # exclude_api_key 207 | def query_url(analysis_type, event_collection, params={}, options={}) 208 | str = _query_url(analysis_type, event_collection, params, options) 209 | str << "&api_key=#{self.read_key}" unless options[:exclude_api_key] 210 | str 211 | end 212 | 213 | # Run a query 214 | # @param event_colection 215 | # @param params [Hash] (required) 216 | # analysis_type (required) 217 | # group_by (optional) 218 | # timeframe (optional) 219 | # interval (optional) 220 | # filters (optional) [Array] 221 | # timezone (optional) 222 | def query(analysis_type, event_collection, params={}, options={}) 223 | response = 224 | if options[:method] == :post 225 | post_query(analysis_type, event_collection, params, options) 226 | else 227 | url = _query_url(analysis_type, event_collection, params, options) 228 | get_response(url, options) 229 | end 230 | 231 | response_body = response.body.chomp 232 | api_result = process_response(response.code, response_body) 233 | api_result = api_result["result"] unless options[:response] == :all_keys 234 | api_result 235 | end 236 | 237 | private 238 | 239 | def post_query(analysis_type, event_collection, params={}, options={}) 240 | ensure_project_id! 241 | ensure_read_key! 242 | 243 | log_query("#{self.api_url}#{api_query_resource_path(analysis_type)}", 'POST', params) if log_queries 244 | 245 | query_params = params.dup 246 | query_params[:event_collection] = event_collection.to_s if event_collection 247 | Keen::HTTP::Sync.new(self.api_url, self.proxy_url, self.read_timeout, self.open_timeout).post( 248 | :path => api_query_resource_path(analysis_type), 249 | :headers => request_headers(options), 250 | :body => MultiJson.encode(query_params) 251 | ) 252 | rescue Exception => http_error 253 | raise HttpError.new("Couldn't perform #{@analysis_type} on Keen IO: #{http_error.message}", http_error) 254 | end 255 | 256 | def _query_url(analysis_type, event_collection, params={}, options={}) 257 | ensure_project_id! 258 | ensure_read_key! 259 | 260 | query_params = params.dup 261 | query_params[:event_collection] = event_collection.to_s if event_collection 262 | "#{self.api_url}#{api_query_resource_path(analysis_type)}?#{preprocess_params(query_params)}" 263 | end 264 | 265 | def get_response(url, options={}) 266 | log_query(url) if log_queries 267 | uri = URI.parse(url) 268 | Keen::HTTP::Sync.new(self.api_url, self.proxy_url, self.read_timeout, self.open_timeout).get( 269 | :path => "#{uri.path}?#{uri.query}", 270 | :headers => request_headers(options) 271 | ) 272 | rescue Exception => http_error 273 | raise HttpError.new("Couldn't perform #{@analysis_type} on Keen IO: #{http_error.message}", http_error) 274 | end 275 | 276 | def api_query_resource_path(analysis_type) 277 | "/#{self.api_version}/projects/#{self.project_id}/queries/#{analysis_type}" 278 | end 279 | 280 | def log_query(url, method='GET', options={}) 281 | Keen.logger.info { "[KEEN] Send #{method} query to #{url} with options #{options}" } 282 | end 283 | 284 | def request_headers(options={}) 285 | base_headers = api_headers(self.read_key, "sync") 286 | options.has_key?(:headers) ? base_headers.merge(options[:headers]) : base_headers 287 | end 288 | end 289 | end 290 | end 291 | -------------------------------------------------------------------------------- /lib/keen/http.rb: -------------------------------------------------------------------------------- 1 | module Keen 2 | module HTTP 3 | class Sync 4 | def initialize(base_url, proxy_url=nil, read_timeout=nil, open_timeout=nil) 5 | require 'uri' 6 | require 'net/http' 7 | 8 | uri = URI.parse(base_url) 9 | arguments = [uri.host, uri.port] 10 | arguments+= proxy_arguments_for(proxy_url) if proxy_url 11 | 12 | @http = Net::HTTP.new(*arguments) 13 | @http.open_timeout = open_timeout if open_timeout 14 | @http.read_timeout = read_timeout if read_timeout 15 | 16 | if uri.scheme == "https" 17 | require 'net/https' 18 | @http.use_ssl = true; 19 | @http.verify_mode = OpenSSL::SSL::VERIFY_PEER 20 | @http.verify_depth = 5 21 | @http.ca_file = File.expand_path("../../../config/cacert.pem", __FILE__) 22 | end 23 | end 24 | 25 | def proxy_arguments_for(uri) 26 | proxy_uri = URI.parse(uri) 27 | [proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password] 28 | end 29 | 30 | def post(options) 31 | path, headers, body = options.values_at( 32 | :path, :headers, :body) 33 | @http.post(path, body, headers) 34 | end 35 | 36 | def put(options) 37 | path, headers, body = options.values_at( 38 | :path, :headers, :body) 39 | @http.put(path, body, headers) 40 | end 41 | 42 | def get(options) 43 | path, headers = options.values_at( 44 | :path, :headers) 45 | @http.get(path, headers) 46 | end 47 | 48 | def delete(options) 49 | path, headers = options.values_at( 50 | :path, :headers) 51 | @http.delete(path, headers) 52 | end 53 | end 54 | 55 | class Async 56 | def initialize(base_url, options={}) 57 | if defined?(EventMachine) && EventMachine.reactor_running? 58 | require 'em-http-request' 59 | else 60 | raise Error, "An EventMachine loop must be running to use publish_async calls" 61 | end 62 | 63 | @base_url = base_url 64 | @proxy_url, @proxy_type = options.values_at(:proxy_url, :proxy_type) 65 | end 66 | 67 | def post(options) 68 | path, headers, body = options.values_at( 69 | :path, :headers, :body) 70 | uri = "#{@base_url}#{path}" 71 | if @proxy_url 72 | proxy_uri = URI.parse(@proxy_url) 73 | connection_options = {:proxy => 74 | {:host => proxy_uri.host, 75 | :port => proxy_uri.port, 76 | :authorization => [proxy_uri.user, proxy_uri.password], 77 | :type => @proxy_type || "http"}} 78 | http_client = EventMachine::HttpRequest.new(uri, connection_options) 79 | else 80 | http_client = EventMachine::HttpRequest.new(uri) 81 | end 82 | http_client.post( 83 | :body => body, 84 | :head => headers 85 | ) 86 | 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/keen/saved_queries.rb: -------------------------------------------------------------------------------- 1 | require 'keen/version' 2 | require "json" 3 | 4 | module Keen 5 | class SavedQueries 6 | def initialize(client) 7 | @client = client 8 | end 9 | 10 | def all 11 | client.ensure_master_key! 12 | 13 | response = saved_query_response(client.master_key) 14 | client.process_response(response.code.to_i, response.body) 15 | end 16 | 17 | def get(saved_query_name, results = false) 18 | saved_query_path = "/#{saved_query_name}" 19 | if results 20 | client.ensure_read_key! 21 | saved_query_path += "/result" 22 | # The results path should use the READ KEY 23 | api_key = client.read_key 24 | else 25 | client.ensure_master_key! 26 | api_key = client.master_key 27 | end 28 | 29 | response = saved_query_response(api_key, saved_query_path) 30 | client.process_response(response.code.to_i, response.body) 31 | end 32 | 33 | def create(saved_query_name, saved_query_body) 34 | client.ensure_master_key! 35 | 36 | saved_query_body = clear_nil_attributes(saved_query_body) 37 | 38 | response = Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).put( 39 | path: "#{saved_query_base_url}/#{saved_query_name}", 40 | headers: api_headers(client.master_key, "sync"), 41 | body: MultiJson.encode(saved_query_body) 42 | ) 43 | client.process_response(response.code.to_i, response.body) 44 | end 45 | alias_method :update_full, :create 46 | 47 | def update(saved_query_name, update_body) 48 | current_query = get saved_query_name 49 | new_query = current_query.select { |key, val| %w(query_name refresh_rate query).include? key } 50 | update_full saved_query_name, new_query.merge(update_body) 51 | end 52 | 53 | def cache(saved_query_name, cache_rate) 54 | update saved_query_name, refresh_rate: cache_rate 55 | end 56 | 57 | def uncache(saved_query_name) 58 | update saved_query_name, refresh_rate: 0 59 | end 60 | 61 | def delete(saved_query_name) 62 | client.ensure_master_key! 63 | 64 | response = Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).delete( 65 | path: "#{saved_query_base_url}/#{saved_query_name}", 66 | headers: api_headers(client.master_key, "sync") 67 | ) 68 | client.process_response(response.code.to_i, response.body) 69 | end 70 | 71 | private 72 | 73 | attr_reader :client 74 | 75 | def saved_query_response(api_key, path = "") 76 | Keen::HTTP::Sync.new(client.api_url, client.proxy_url, client.read_timeout, client.open_timeout).get( 77 | path: saved_query_base_url + path, 78 | headers: api_headers(api_key, "sync") 79 | ) 80 | end 81 | 82 | def saved_query_base_url 83 | client.ensure_project_id! 84 | "/#{client.api_version}/projects/#{client.project_id}/queries/saved" 85 | end 86 | 87 | def api_headers(authorization, sync_type) 88 | user_agent = "keen-gem, v#{Keen::VERSION}, #{sync_type}" 89 | user_agent += ", #{RUBY_VERSION}, #{RUBY_PLATFORM}, #{RUBY_PATCHLEVEL}" 90 | if defined?(RUBY_ENGINE) 91 | user_agent += ", #{RUBY_ENGINE}" 92 | end 93 | { "Content-Type" => "application/json", 94 | "User-Agent" => user_agent, 95 | "Authorization" => authorization, 96 | "Keen-Sdk" => "ruby-#{Keen::VERSION}" } 97 | end 98 | 99 | # Remove any attributes with nil values in a saved query hash. The API will 100 | # already assume missing attributes are nil 101 | def clear_nil_attributes(hash) 102 | hash.reject! do |key, value| 103 | if value.nil? 104 | return true 105 | elsif value.is_a? Hash 106 | value.reject! { |inner_key, inner_value| inner_value.nil? } 107 | end 108 | 109 | false 110 | end 111 | 112 | hash 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/keen/scoped_key.rb: -------------------------------------------------------------------------------- 1 | require 'multi_json' 2 | require 'keen/aes_helper' 3 | require 'keen/aes_helper_old' 4 | 5 | module Keen 6 | # DEPRECATED: Please use access keys instead. 7 | class ScopedKey 8 | 9 | attr_accessor :api_key 10 | attr_accessor :data 11 | 12 | class << self 13 | def decrypt!(api_key, scoped_key) 14 | if api_key.length == 64 15 | decrypted = Keen::AESHelper.aes256_decrypt(api_key, scoped_key) 16 | else 17 | decrypted = Keen::AESHelperOld.aes256_decrypt(api_key, scoped_key) 18 | end 19 | data = MultiJson.load(decrypted) 20 | self.new(api_key, data) 21 | end 22 | end 23 | 24 | def initialize(api_key, data) 25 | self.api_key = api_key 26 | self.data = data 27 | end 28 | 29 | def encrypt!(iv = nil) 30 | warn "[DEPRECATION] Scoped keys are deprecated. Please use `access_keys` instead." 31 | 32 | json_str = MultiJson.dump(self.data) 33 | if self.api_key.length == 64 34 | Keen::AESHelper.aes256_encrypt(self.api_key, json_str, iv) 35 | else 36 | Keen::AESHelperOld.aes256_encrypt(self.api_key, json_str, iv) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/keen/version.rb: -------------------------------------------------------------------------------- 1 | module Keen 2 | VERSION = "1.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/integration/access_keys_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe "Access Keys" do 4 | let(:project_id) { ENV["KEEN_PROJECT_ID"] } 5 | let(:master_key) { ENV["KEEN_MASTER_KEY"] } 6 | let(:client) { Keen::Client.new(project_id: project_id, master_key: master_key) } 7 | 8 | describe "#all" do 9 | it "gets all access keys" do 10 | expect(client.access_keys.all).to be_instance_of(Array) 11 | end 12 | end 13 | 14 | describe "#create" do 15 | it "creates a key" do 16 | key_body = { 17 | "name" => "integration test key", 18 | "is_active" => true, 19 | "permitted" => ["queries"], 20 | "options" => {} 21 | } 22 | 23 | create_result = client.access_keys.create(key_body) 24 | expect(create_result["name"]).to eq(key_body["name"]) 25 | end 26 | end 27 | 28 | describe "#get" do 29 | it "gets a single access key" do 30 | all_keys = client.access_keys.all 31 | 32 | access_key = client.access_keys.get(all_keys.first["key"]) 33 | 34 | expect(access_key["name"]).to eq(all_keys.first["name"]) 35 | end 36 | end 37 | 38 | describe "#revoke" do 39 | it "sets the is_active to false" do 40 | all_keys = client.access_keys.all 41 | key = all_keys.first["key"] 42 | 43 | client.access_keys.revoke(key) 44 | new_key = client.access_keys.get(key) 45 | expect(new_key["is_active"]).to be_falsey 46 | end 47 | end 48 | 49 | describe "#unrevoke" do 50 | it "sets the is_active to true" do 51 | all_keys = client.access_keys.all 52 | key = all_keys.first["key"] 53 | 54 | client.access_keys.unrevoke(key) 55 | new_key = client.access_keys.get(key) 56 | expect(new_key["is_active"]).to be_truthy 57 | end 58 | end 59 | 60 | describe "#delete" do 61 | it "deletes a key" do 62 | all_keys = client.access_keys.all 63 | key = all_keys.first["key"] 64 | 65 | client.access_keys.delete(key) 66 | all_keys = client.access_keys.all 67 | expect(all_keys).to eq([]) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/integration/api_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe "Keen IO API" do 4 | let(:project_id) { ENV['KEEN_PROJECT_ID'] } 5 | let(:write_key) { ENV['KEEN_WRITE_KEY'] } 6 | 7 | def wait_for_count(event_collection, count) 8 | attempts = 0 9 | while attempts < 30 10 | break if Keen.count(event_collection, {:timeframe => "this_2_hours"}) == count 11 | attempts += 1 12 | sleep(1) 13 | end 14 | end 15 | 16 | describe "publishing" do 17 | let(:collection) { "User posts.new" } 18 | let(:event_properties) { { "name" => "Bob" } } 19 | let(:api_success) { { "created" => true } } 20 | 21 | describe "success" do 22 | it "should return a created status for a valid post" do 23 | expect(Keen.publish(collection, event_properties)).to eq(api_success) 24 | end 25 | end 26 | 27 | describe "failure" do 28 | it "should raise a not found error if an invalid project id" do 29 | client = Keen::Client.new(:project_id => "riker", :write_key => "whatever") 30 | expect { 31 | client.publish(collection, event_properties) 32 | }.to raise_error(Keen::NotFoundError) 33 | end 34 | 35 | it "should succeed if a non-url-safe event collection is specified" do 36 | expect(Keen.publish("infinite possibilities", event_properties)).to eq(api_success) 37 | end 38 | end 39 | 40 | describe "async" do 41 | # no TLS support in EventMachine on jRuby 42 | unless defined?(JRUBY_VERSION) 43 | 44 | it "should publish the event and trigger callbacks" do 45 | EM.run { 46 | Keen.publish_async(collection, event_properties).callback { |response| 47 | begin 48 | expect(response).to eq(api_success) 49 | ensure 50 | EM.stop 51 | end 52 | }.errback { |error| 53 | EM.stop 54 | fail error 55 | } 56 | } 57 | end 58 | 59 | it "should publish to non-url-safe collections" do 60 | EM.run { 61 | Keen.publish_async("foo bar", event_properties).callback { |response| 62 | begin 63 | expect(response).to eq(api_success) 64 | ensure 65 | EM.stop 66 | end 67 | } 68 | } 69 | end 70 | end 71 | end 72 | 73 | describe "batch" do 74 | it "should publish a batch of events" do 75 | expect(Keen.publish_batch( 76 | :batch_signups => [ 77 | { :name => "bob" }, 78 | { :name => "ted" } 79 | ], 80 | :batch_purchases => [ 81 | { :price => 30 }, 82 | { :price => 40 } 83 | ] 84 | )).to eq({ 85 | "batch_purchases" => [ 86 | { "success" => true }, 87 | { "success" => true } 88 | ], 89 | "batch_signups" => [ 90 | { "success" => true }, 91 | { "success"=>true } 92 | ]}) 93 | end 94 | end 95 | end 96 | 97 | describe "batch_async" do 98 | # no TLS support in EventMachine on jRuby 99 | unless defined?(JRUBY_VERSION) 100 | let(:api_success) { {"batch_purchases"=>[{"success"=>true}, {"success"=>true}], "batch_signups"=>[{"success"=>true}, {"success"=>true}]} } 101 | it "should publish the event and trigger callbacks" do 102 | EM.run { 103 | Keen.publish_batch_async( 104 | :batch_signups => [ 105 | { :name => "bob" }, 106 | { :name => "ted" } 107 | ], 108 | :batch_purchases => [ 109 | { :price => 30 }, 110 | { :price => 40 } 111 | ]).callback { |response| 112 | begin 113 | expect(response).to eq(api_success) 114 | ensure 115 | EM.stop 116 | end 117 | }.errback { |error| 118 | EM.stop 119 | fail error 120 | } 121 | } 122 | end 123 | end 124 | end 125 | 126 | describe "queries" do 127 | let(:read_key) { ENV['KEEN_READ_KEY'] } 128 | let(:event_collection) { @event_collection } 129 | let(:returns_event_collection) { @returns_event_collection } 130 | 131 | before(:all) do 132 | @event_collection = "purchases_" + rand(100000).to_s 133 | @returns_event_collection = "returns_" + rand(100000).to_s 134 | 135 | Keen.publish(@event_collection, { 136 | :username => "bob", 137 | :price => 10 138 | }) 139 | Keen.publish(@event_collection, { 140 | :username => "ted", 141 | :price => 20 142 | }) 143 | Keen.publish(@returns_event_collection, { 144 | :username => "bob", 145 | :price => 30 146 | }) 147 | 148 | # poll the count to know when to continue 149 | wait_for_count(@event_collection, 2) 150 | wait_for_count(@returns_event_collection, 1) 151 | end 152 | 153 | it "should return a valid count_unique" do 154 | expect(Keen.count_unique(event_collection, :timeframe => "this_2_hours", :target_property => "price")).to eq(2) 155 | end 156 | 157 | it "should return a valid count with group_by" do 158 | response = Keen.average(event_collection, :timeframe => "this_2_hours", :group_by => "username", :target_property => "price") 159 | bobs_response = response.select { |result| result["username"] == "bob" }.first 160 | expect(bobs_response["result"]).to eq(10) 161 | teds_response = response.select { |result| result["username"] == "ted" }.first 162 | expect(teds_response["result"]).to eq(20) 163 | end 164 | 165 | it "should return a valid count with multi-group_by" do 166 | response = Keen.average(event_collection, :timeframe => "this_2_hours", :group_by => ["username", "price"], :target_property => "price") 167 | bobs_response = response.select { |result| result["username"] == "bob" }.first 168 | expect(bobs_response["result"]).to eq(10) 169 | expect(bobs_response["price"]).to eq(10) 170 | teds_response = response.select { |result| result["username"] == "ted" }.first 171 | expect(teds_response["result"]).to eq(20) 172 | expect(teds_response["price"]).to eq(20) 173 | end 174 | 175 | it "should return a valid sum" do 176 | expect(Keen.sum(event_collection, :timeframe => "this_2_hours", :target_property => "price")).to eq(30) 177 | end 178 | 179 | it "should return a valid minimum" do 180 | expect(Keen.minimum(event_collection, :timeframe => "this_2_hours", :target_property => "price")).to eq(10) 181 | end 182 | 183 | it "should return a valid maximum" do 184 | expect(Keen.maximum(event_collection, :timeframe => "this_2_hours", :target_property => "price")).to eq(20) 185 | end 186 | 187 | it "should return a valid average" do 188 | expect(Keen.average(event_collection, :timeframe => "this_2_hours", :target_property => "price")).to eq(15) 189 | end 190 | 191 | it "should return a valid median" do 192 | expect(Keen.median(event_collection, :timeframe => "this_2_hours", :target_property => "price")).to eq(10) 193 | end 194 | 195 | it "should return a valid percentile" do 196 | expect(Keen.percentile(event_collection, :timeframe => "this_2_hours", :target_property => "price", :percentile => 50)).to eq(10) 197 | expect(Keen.percentile(event_collection, :timeframe => "this_2_hours", :target_property => "price", :percentile => 100)).to eq(20) 198 | end 199 | 200 | it "should return a valid select_unique" do 201 | results = Keen.select_unique(event_collection, :timeframe => "this_2_hours", :target_property => "price") 202 | expect(results.sort).to eq([10, 20].sort) 203 | end 204 | 205 | it "should return a valid extraction" do 206 | results = Keen.extraction(event_collection, :timeframe => "this_2_hours") 207 | expect(results.length).to eq(2) 208 | expect(results.all? { |result| result["keen"] }).to be_truthy 209 | expect(results.map { |result| result["price"] }.sort).to eq([10, 20]) 210 | expect(results.map { |result| result["username"] }.sort).to eq(["bob", "ted"]) 211 | end 212 | 213 | it "should return a valid extraction of one property name" do 214 | results = Keen.extraction(event_collection, :timeframe => "this_2_hours", :property_names => "price") 215 | expect(results.length).to eq(2) 216 | expect(results.any? { |result| result["keen"] }).to be_falsey 217 | expect(results.map { |result| result["price"] }.sort).to eq([10, 20]) 218 | expect(results.map { |result| result["username"] }.sort).to eq([nil, nil]) 219 | end 220 | 221 | it "should return a valid extraction of more than one property name" do 222 | results = Keen.extraction(event_collection, :timeframe => "this_2_hours", :property_names => ["price", "username"]) 223 | expect(results.length).to eq(2) 224 | expect(results.any? { |result| result["keen"] }).to be_falsey 225 | expect(results.map { |result| result["price"] }.sort).to eq([10, 20]) 226 | expect(results.map { |result| result["username"] }.sort).to eq(["bob", "ted"]) 227 | end 228 | 229 | it "should return a valid funnel" do 230 | steps = [{ 231 | :event_collection => event_collection, 232 | :actor_property => "username", 233 | :timeframe => "this_2_hours" 234 | }, { 235 | :event_collection => @returns_event_collection, 236 | :actor_property => "username", 237 | :timeframe => "this_2_hours" 238 | }] 239 | results = Keen.funnel(:steps => steps) 240 | expect(results).to eq([2, 1]) 241 | end 242 | 243 | it "should return all keys of valid funnel if full result option is passed" do 244 | steps = [{ 245 | :timeframe => "this_2_hours", 246 | :event_collection => event_collection, 247 | :actor_property => "username" 248 | }, { 249 | :timeframe => "this_2_hours", 250 | :event_collection => @returns_event_collection, 251 | :actor_property => "username" 252 | }] 253 | results = Keen.funnel({ :steps => steps }, { :response => :all_keys }) 254 | expect(results["result"]).to eq([2, 1]) 255 | end 256 | 257 | it "should apply filters" do 258 | expect(Keen.count(event_collection, :timeframe => "this_2_hours", :filters => [{ 259 | :property_name => "username", 260 | :operator => "eq", 261 | :property_value => "ted" 262 | }])).to eq(1) 263 | end 264 | end 265 | 266 | describe "deletes" do 267 | let(:event_collection) { "delete_test_#{rand(10000)}" } 268 | 269 | before do 270 | Keen.publish(event_collection, :delete => "me") 271 | Keen.publish(event_collection, :delete => "you") 272 | wait_for_count(event_collection, 2) 273 | end 274 | 275 | it "should delete the event" do 276 | Keen.delete(event_collection, :filters => [ 277 | { :property_name => "delete", :operator => "eq", :property_value => "me" } 278 | ]) 279 | wait_for_count(event_collection, 1) 280 | results = Keen.extraction(event_collection, :timeframe => "this_2_hours") 281 | expect(results.length).to eq(1) 282 | expect(results.first["delete"]).to eq("you") 283 | end 284 | end 285 | 286 | describe "project methods" do 287 | let(:event_collection) { "test_collection" } 288 | 289 | describe "event_collections" do 290 | # requires a project with at least 1 collection 291 | it "should return the project's collections as JSON" do 292 | first_collection = Keen.event_collections.first 293 | expect(first_collection["properties"]["keen.timestamp"]).to eq("datetime") 294 | end 295 | end 296 | 297 | describe "project_info" do 298 | it "should return the project info as JSON" do 299 | expect(Keen.project_info["url"]).to include(project_id) 300 | 301 | end 302 | end 303 | end 304 | end 305 | -------------------------------------------------------------------------------- /spec/integration/saved_query_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe "Saved Queries" do 4 | let(:project_id) { ENV["KEEN_PROJECT_ID"] } 5 | let(:master_key) { ENV["KEEN_MASTER_KEY"] } 6 | let(:read_key) { ENV["KEEN_READ_KEY"] } 7 | let(:client) { Keen::Client.new(project_id: project_id, master_key: master_key, read_key: read_key) } 8 | 9 | describe "#all" do 10 | it "gets all saved_queries" do 11 | expect(client.saved_queries.all).to be_instance_of(Array) 12 | end 13 | end 14 | 15 | describe "#get" do 16 | it "gets a single saved query" do 17 | all_queries = client.saved_queries.all 18 | 19 | single_saved_query = client.saved_queries.get(all_queries.first[:query_name]) 20 | 21 | expect(single_saved_query[:query_name]).to eq(all_queries.first[:query_name]) 22 | expect(single_saved_query[:results]).to be_nil 23 | end 24 | end 25 | 26 | describe "#results" do 27 | it "gets a single saved query" do 28 | all_queries = client.saved_queries.all 29 | 30 | single_saved_query = client.saved_queries.get(all_queries.last[:query_name], results: true) 31 | 32 | expect(single_saved_query[:result]).not_to be_nil 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/integration/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../spec_helper", __FILE__) 2 | 3 | RSpec.configure do |config| 4 | unless ENV['KEEN_PROJECT_ID'] 5 | raise "Please set a KEEN_PROJECT_ID on the environment 6 | before running the integration specs." 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/keen/access_keys_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe Keen do 4 | let(:client) do 5 | Keen::Client.new( 6 | project_id: "12341234", 7 | master_key: "abcdef", 8 | api_url: "https://notreal.keen.io" 9 | ) 10 | end 11 | 12 | describe "#access_keys" do 13 | describe "#all" do 14 | 15 | it "returns all access keys" do 16 | all_access_keys = [ 17 | key_object() 18 | ] 19 | 20 | stub_keen_get(access_keys_endpoint, 200, all_access_keys) 21 | 22 | all_access_keys_response = client.access_keys.all 23 | 24 | expect(all_access_keys_response).to eq(all_access_keys) 25 | end 26 | end 27 | 28 | describe "#get" do 29 | it "returns a specific access key given a key" do 30 | key = key_object() 31 | 32 | stub_keen_get( 33 | access_keys_endpoint + "/#{key["key"]}", 34 | 200, 35 | key 36 | ) 37 | 38 | access_key_response = client.access_keys.get(key["key"]) 39 | 40 | expect(access_key_response).to eq(key) 41 | end 42 | end 43 | 44 | describe "#create" do 45 | it "returns the created saved query when creation is successful" do 46 | key = key_object() 47 | 48 | stub_keen_post( 49 | access_keys_endpoint, 201, key 50 | ) 51 | 52 | access_keys_response = client.access_keys.create(key) 53 | 54 | expect(access_keys_response).to eq(key) 55 | end 56 | end 57 | 58 | describe "#update" do 59 | it "returns the updated access key when update is successful" do 60 | key = key_object() 61 | 62 | stub_keen_post( 63 | access_keys_endpoint + "/#{key["key"]}", 200, key 64 | ) 65 | 66 | access_keys_response = client.access_keys.update(key["key"], key) 67 | 68 | expect(access_keys_response).to eq(key) 69 | end 70 | end 71 | 72 | describe "#delete" do 73 | it "returns true with deletion is successful" do 74 | key = "asdf1234" 75 | 76 | stub_keen_delete( 77 | access_keys_endpoint + "/#{key}", 204 78 | ) 79 | 80 | access_keys_response = client.access_keys.delete(key) 81 | 82 | expect(access_keys_response).to eq(true) 83 | end 84 | end 85 | end 86 | 87 | def access_keys_endpoint 88 | client.api_url + "/#{client.api_version}/projects/#{client.project_id}/keys" 89 | end 90 | 91 | def key_object(name = "Test Access Key") 92 | { 93 | "key" => "SDKFJSDKFJSDKFJSDKFJDSK", 94 | "name" => name, 95 | "is_active" => true, 96 | "permitted" => ["queries", "cached_queries"], 97 | "options" => { 98 | "queries" => { 99 | "filters" => [ 100 | { 101 | "property_name" => "customer.id", 102 | "operator" => "eq", 103 | "property_value" => "asdf12345z" 104 | } 105 | ] 106 | }, 107 | "cached_queries" => { 108 | "allowed" => ["my_cached_query"] 109 | } 110 | } 111 | } 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/keen/cached_dataset_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe Keen do 4 | let(:client) do 5 | Keen::Client.new( 6 | project_id: "12341234", 7 | master_key: "abcdef", 8 | read_key: "ghijkl", 9 | api_url: "https://notreal.keen.io" 10 | ) 11 | end 12 | 13 | describe "#cached_datasets" do 14 | describe "#list" do 15 | 16 | it "returns cached dataset definitions" do 17 | example_result = { 18 | "datasets" => [{ 19 | "project_id" => "PROJECT_ID", 20 | "organization_id" => "ORGANIZATION_ID", 21 | "dataset_name" => "DATASET_NAME_1", 22 | "display_name" => "a first dataset wee", 23 | "query" => { 24 | "project_id" => "PROJECT_ID", 25 | "analysis_type" => "count", 26 | "event_collection" => "best collection", 27 | "filters" => [{ 28 | "property_name" => "request.foo", 29 | "operator" => "lt", 30 | "property_value" => 300 31 | }], 32 | "timeframe" => "this_500_hours", 33 | "timezone" => "US/Pacific", 34 | "interval" => "hourly", 35 | "group_by" => [ 36 | "exception.name" 37 | ] 38 | }, 39 | "index_by" => [ 40 | "project.id" 41 | ], 42 | "last_scheduled_date" => "2016-11-04T18:03:38.430Z", 43 | "latest_subtimeframe_available" => "2016-11-04T19:00:00.000Z", 44 | "milliseconds_behind" => 3600000 45 | }, { 46 | "project_id" => "PROJECT_ID", 47 | "organization_id" => "ORGANIZATION_ID", 48 | "dataset_name" => "DATASET_NAME_10", 49 | "display_name" => "tenth dataset wee", 50 | "query" => { 51 | "project_id" => "PROJECT_ID", 52 | "analysis_type" => "count", 53 | "event_collection" => "tenth best collection", 54 | "filters" => [], 55 | "timeframe" => "this_500_days", 56 | "timezone" => "UTC", 57 | "interval" => "daily", 58 | "group_by" => [ 59 | "analysis_type" 60 | ] 61 | }, 62 | "index_by" => [ 63 | "project.organization.id" 64 | ], 65 | "last_scheduled_date" => "2016-11-04T19:28:36.639Z", 66 | "latest_subtimeframe_available" => "2016-11-05T00:00:00.000Z", 67 | "milliseconds_behind" => 3600000 68 | }], 69 | "next_page_url" => nil 70 | } 71 | 72 | stub_keen_get(datasets_endpoint, 200, example_result) 73 | 74 | listed_cached_datasets_response = client.cached_datasets.list 75 | 76 | expect(listed_cached_datasets_response).to eq(example_result) 77 | end 78 | 79 | it 'accepts params for pagination' do 80 | example_result = { 81 | "datasets" => [{ 82 | "project_id" => "PROJECT_ID", 83 | "organization_id" => "ORGANIZATION_ID", 84 | "dataset_name" => "DATASET_NAME_10", 85 | "display_name" => "tenth dataset wee", 86 | "query" => { 87 | "project_id" => "PROJECT_ID", 88 | "analysis_type" => "count", 89 | "event_collection" => "tenth best collection", 90 | "filters" => [], 91 | "timeframe" => "this_500_days", 92 | "timezone" => "UTC", 93 | "interval" => "daily", 94 | "group_by" => [ 95 | "analysis_type" 96 | ] 97 | }, 98 | "index_by" => [ 99 | "project.organization.id" 100 | ], 101 | "last_scheduled_date" => "2016-11-04T19:28:36.639Z", 102 | "latest_subtimeframe_available" => "2016-11-05T00:00:00.000Z", 103 | "milliseconds_behind" => 3600000 104 | }], 105 | "next_page_url" => "https://api.keen.io/3.0/projects/PROJECT_ID/datasets?limit=1&after_name=DATASET_NAME_1" 106 | } 107 | 108 | stub_keen_get(datasets_endpoint + "?limit=1&after_name=DATASET_NAME_1", 200, example_result) 109 | 110 | listed_cached_datasets_response = client.cached_datasets.list(limit: 1, after_name: 'DATASET_NAME_1') 111 | 112 | expect(listed_cached_datasets_response).to eq(example_result) 113 | end 114 | end 115 | 116 | describe "#get_definition" do 117 | 118 | it "returns a cached dataset definition" do 119 | example_result = { 120 | "project_id" => "PROJECT_ID", 121 | "organization_id" => "ORGANIZATION_ID", 122 | "dataset_name" => "DATASET_NAME_1", 123 | "display_name" => "Count Daily Product Purchases Over $100 by Country", 124 | "query" => { 125 | "project_id" => "5011efa95f546f2ce2000000", 126 | "analysis_type" => "count", 127 | "event_collection" => "purchases", 128 | "filters" => [ 129 | { 130 | "property_name" => "price", 131 | "operator" => "gte", 132 | "property_value" => 100 133 | } 134 | ], 135 | "timeframe" => "this_500_days", 136 | "timezone" => nil, 137 | "interval" => "daily", 138 | "group_by" => ["ip_geo_info.country"] 139 | }, 140 | "index_by" => ["product.id"], 141 | "last_scheduled_date" => "2016-11-04T18:52:36.323Z", 142 | "latest_subtimeframe_available" => "2016-11-05T00:00:00.000Z", 143 | "milliseconds_behind" => 3600000 144 | } 145 | 146 | 147 | stub_keen_get(datasets_endpoint + '/DATASET_NAME_1', 200, example_result) 148 | 149 | cached_dataset_response = client.cached_datasets.get_definition('DATASET_NAME_1') 150 | 151 | expect(cached_dataset_response).to eq(example_result) 152 | end 153 | end 154 | 155 | describe "#get_results" do 156 | 157 | it "returns results for a cached dataset" do 158 | example_result = { 159 | "result" => [ 160 | { 161 | "timeframe" => { 162 | "start" => "2016-11-02T00:00:00.000Z", 163 | "end" => "2016-11-03T00:00:00.000Z" 164 | }, 165 | "value" => [ 166 | { 167 | "item.name" => "Golden Widget", 168 | "result" => 0 169 | }, 170 | { 171 | "item.name" => "Silver Widget", 172 | "result" => 18 173 | }, 174 | { 175 | "item.name" => "Bronze Widget", 176 | "result" => 1 177 | }, 178 | { 179 | "item.name" => "Platinum Widget", 180 | "result" => 9 181 | } 182 | ] 183 | }, 184 | { 185 | "timeframe" => { 186 | "start" => "2016-11-03T00:00:00.000Z", 187 | "end" => "2016-11-04T00:00:00.000Z" 188 | }, 189 | "value" => [ 190 | { 191 | "item.name" => "Golden Widget", 192 | "result" => 1 193 | }, 194 | { 195 | "item.name" => "Silver Widget", 196 | "result" => 13 197 | }, 198 | { 199 | "item.name" => "Bronze Widget", 200 | "result" => 0 201 | }, 202 | { 203 | "item.name" => "Platinum Widget", 204 | "result" => 3 205 | } 206 | ] 207 | } 208 | ] 209 | } 210 | 211 | stub_keen_get( 212 | datasets_endpoint + '/DATASET_NAME_1/results?index_by=some-user-id&timeframe=%7B%22start%22:%222012-08-13T19:00:00.000Z%22,%22end%22:%222013-09-20T19:00:00.000Z%22%7D', 213 | 200, 214 | example_result 215 | ) 216 | 217 | cached_dataset_response = client.cached_datasets.get_results('DATASET_NAME_1', { 218 | start: "2012-08-13T19:00:00.000Z", 219 | end: "2013-09-20T19:00:00.000Z" 220 | }, 'some-user-id') 221 | expect(cached_dataset_response).to eq(example_result) 222 | end 223 | 224 | it 'raises an error if cached dataset is not defined' do 225 | example_result = { 226 | message: "Resource not found", 227 | error_code: "ResourceNotFoundError" 228 | } 229 | stub_keen_get( 230 | datasets_endpoint + "/missing-dataset/results?index_by=some-user-id&timeframe=%7B%22start%22:%222012-08-13T19:00:00.000Z%22,%22end%22:%222013-09-20T19:00:00.000Z%22%7D", 231 | 404, 232 | example_result 233 | ) 234 | 235 | expect { 236 | cached_dataset_response = client.cached_datasets.get_results('missing-dataset', { 237 | start: "2012-08-13T19:00:00.000Z", 238 | end: "2013-09-20T19:00:00.000Z" 239 | }, 'some-user-id') 240 | }.to raise_error(Keen::NotFoundError) 241 | end 242 | end 243 | 244 | describe "#create" do 245 | it "returns the created dataset when creation is successful" do 246 | example_result = { 247 | "project_id" => "PROJECT ID", 248 | "organization_id" => "ORGANIZATION", 249 | "dataset_name" => "DATASET_NAME_1", 250 | "display_name" => "DS Display Name", 251 | "query" => { 252 | "project_id" => "PROJECT ID", 253 | "analysis_type" => "count", 254 | "event_collection" => "purchases", 255 | "filters" => [ 256 | { 257 | "property_name" => "price", 258 | "operator" => "gte", 259 | "property_value" => 100 260 | } 261 | ], 262 | "timeframe" => "this_500_days", 263 | "interval" => "daily", 264 | "group_by" => ["ip_geo_info.country"] 265 | }, 266 | "index_by" => "product.id", 267 | "last_scheduled_date" => "1970-01-01T00:00:00.000Z", 268 | "latest_subtimeframe_available" => "1970-01-01T00:00:00.000Z", 269 | "milliseconds_behind" => 3600000 270 | } 271 | stub_keen_put( 272 | datasets_endpoint + "/DATASET_NAME_1", 201, example_result 273 | ) 274 | 275 | create_response = client.cached_datasets.create('DATASET_NAME_1', 'product.id', example_result['query'], 'DS Display Name') 276 | 277 | expect(create_response).to eq(example_result) 278 | end 279 | 280 | it "raises an error when creation is unsuccessful" do 281 | stub_keen_put( 282 | datasets_endpoint + "/DATASET_NAME_1", 400, {} 283 | ) 284 | 285 | expect { 286 | client.cached_datasets.create("DATASET_NAME_1", 'product.id', {}, 'DS Display Name') 287 | }.to raise_error(Keen::BadRequestError) 288 | end 289 | end 290 | 291 | describe "#delete" do 292 | it "returns true with deletion is successful" do 293 | dataset_name = "dataset-to-be-deleted" 294 | stub_keen_delete( 295 | datasets_endpoint + "/dataset-to-be-deleted", 204 296 | ) 297 | 298 | result = client.cached_datasets.delete(dataset_name) 299 | 300 | expect(result).to eq(true) 301 | end 302 | end 303 | end 304 | 305 | def datasets_endpoint 306 | client.api_url + "/#{client.api_version}/projects/#{client.project_id}/datasets" 307 | end 308 | end 309 | -------------------------------------------------------------------------------- /spec/keen/client/maintenance_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../spec_helper", __FILE__) 2 | 3 | describe Keen::Client do 4 | let(:project_id) { "12345" } 5 | let(:master_key) { 'pastor_of_muppets' } 6 | let(:api_url) { "https://notreal.keen.io" } 7 | let(:api_version) { "3.0" } 8 | 9 | let(:client) { Keen::Client.new( 10 | :project_id => project_id, :master_key => master_key, 11 | :api_url => api_url ) } 12 | 13 | def delete_url(event_collection, filter_params=nil) 14 | "#{api_url}/#{api_version}/projects/#{project_id}/events/#{event_collection}#{filter_params ? "?filters=#{CGI.escape(MultiJson.encode(filter_params[:filters]))}" : ""}" 15 | end 16 | 17 | describe '#delete' do 18 | let(:event_collection) { :foodstuffs } 19 | 20 | it 'should not require filters' do 21 | url = delete_url(event_collection) 22 | stub_keen_delete(url, 204) 23 | expect(client.delete(event_collection)).to be true 24 | expect_keen_delete(url, "sync", master_key) 25 | end 26 | 27 | it "should accept and use filters" do 28 | filters = { 29 | :filters => [ 30 | { :property_name => "delete", :operator => "eq", :property_value => "me" } 31 | ] 32 | } 33 | url = delete_url(event_collection, filters) 34 | stub_keen_delete(url, 204) 35 | client.delete(event_collection, filters) 36 | expect_keen_delete(url, "sync", master_key) 37 | end 38 | end 39 | 40 | describe '#event_collections' do 41 | let(:events_url) { "#{api_url}/#{api_version}/projects/#{project_id}/events" } 42 | 43 | it "should fetch the project's event resource" do 44 | stub_keen_get(events_url, 200, [{ "a" => 1 }, { "b" => 2 }] ) 45 | expect(client.event_collections).to match_array([{ "a" => 1 }, { "b" => 2 }]) 46 | expect_keen_get(events_url, "sync", master_key) 47 | end 48 | end 49 | 50 | describe '#event_collection' do 51 | let(:event_collection) { "foodstuffs" } 52 | let(:events_url) { "#{api_url}/#{api_version}/projects/#{project_id}/events/#{event_collection}" } 53 | 54 | it "should fetch the project's named event resource" do 55 | stub_keen_get(events_url, 200, [{ "b" => 2 }] ) 56 | expect(client.event_collection(event_collection)).to match_array([{ "b" => 2 }]) 57 | expect_keen_get(events_url, "sync", master_key) 58 | end 59 | end 60 | 61 | describe '#project_info' do 62 | let(:project_url) { "#{api_url}/#{api_version}/projects/#{project_id}" } 63 | 64 | it "should fetch the project resource" do 65 | stub_keen_get(project_url, 200, [{ "a" => 1 }, { "b" => 2 }] ) 66 | expect(client.project_info).to match_array([{ "a" => 1 }, { "b" => 2 }]) 67 | expect_keen_get(project_url, "sync", master_key) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/keen/client/publishing_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../spec_helper", __FILE__) 2 | 3 | describe Keen::Client::PublishingMethods do 4 | let(:project_id) { "12345" } 5 | let(:write_key) { "abcde" } 6 | let(:api_url) { "https://unreal.keen.io" } 7 | let(:collection) { "some :actions_to.record" } 8 | let(:event_properties) { { "name" => "Bob" } } 9 | let(:api_success) { { "created" => true } } 10 | let(:client) { Keen::Client.new( 11 | :project_id => project_id, :write_key => write_key, 12 | :api_url => api_url) } 13 | 14 | describe "publish" do 15 | it "should post using the collection and properties" do 16 | stub_keen_post(api_event_collection_resource_url(api_url, collection), 201, "") 17 | client.publish(collection, event_properties) 18 | expect_keen_post(api_event_collection_resource_url(api_url, collection), event_properties, "sync", write_key) 19 | end 20 | 21 | it "should return the proper response" do 22 | api_response = { "created" => true } 23 | stub_keen_post(api_event_collection_resource_url(api_url, collection), 201, api_response) 24 | expect(client.publish(collection, event_properties)).to eq(api_response) 25 | end 26 | 27 | it "should raise an argument error if no event collection is specified" do 28 | expect { 29 | client.publish(nil, {}) 30 | }.to raise_error(ArgumentError) 31 | end 32 | 33 | it "should raise an argument error if no properties are specified" do 34 | expect { 35 | client.publish(collection, nil) 36 | }.to raise_error(ArgumentError) 37 | end 38 | 39 | it "should url encode the event collection" do 40 | stub_keen_post(api_event_collection_resource_url(api_url, "User%20posts.new%20)(*%26%5E%25%40!)%3A%3A%2520%2520"), 201, "") 41 | client.publish("User posts.new )(*&^%@!)::%20%20", event_properties) 42 | expect_keen_post(api_event_collection_resource_url(api_url, "User%20posts.new%20)(*%26%5E%25%40!)%3A%3A%2520%2520"), event_properties, "sync", write_key) 43 | end 44 | 45 | it "should wrap exceptions" do 46 | stub_request(:post, api_event_collection_resource_url(api_url, collection)).to_timeout 47 | e = nil 48 | begin 49 | client.publish(collection, event_properties) 50 | rescue Exception => exception 51 | e = exception 52 | end 53 | 54 | expect(e.class).to eq(Keen::HttpError) 55 | expect(e.original_error).to be_kind_of(Timeout::Error) 56 | expect(e.message).to eq("Keen IO Exception: HTTP publish failure: execution expired") 57 | end 58 | 59 | it "should raise an exception if client has no project_id" do 60 | expect { 61 | Keen::Client.new( 62 | :write_key => "abcde" 63 | ).publish(collection, event_properties) 64 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Project ID must be set") 65 | end 66 | 67 | it "should raise an exception if client has no write_key" do 68 | expect { 69 | Keen::Client.new( 70 | :project_id => "12345" 71 | ).publish(collection, event_properties) 72 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Write Key must be set for this operation") 73 | end 74 | 75 | context "when using proxy" do 76 | let(:client) do 77 | Keen::Client.new(:project_id => project_id, 78 | :write_key => write_key, 79 | :api_url => api_url, 80 | :proxy_url => "http://localhost:8888", 81 | :proxy_type => "socks5") 82 | end 83 | 84 | it "should return the proper response" do 85 | api_response = { "created" => true } 86 | stub_keen_post(api_event_collection_resource_url(api_url, collection), 201, api_response) 87 | expect(client.publish(collection, event_properties)).to eq(api_response) 88 | end 89 | end 90 | end 91 | 92 | describe "publish_batch" do 93 | let(:events) { 94 | { 95 | :purchases => [ 96 | { :price => 10 }, 97 | { :price => 11 } 98 | ], 99 | :signups => [ 100 | { :name => "bob" }, 101 | { :name => "bill" } 102 | ] 103 | } 104 | } 105 | 106 | it "should raise an exception if client has no project_id" do 107 | expect { 108 | Keen::Client.new( 109 | :write_key => "abcde" 110 | ).publish_batch(events) 111 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Project ID must be set") 112 | end 113 | 114 | it "should raise an exception if client has no write_key" do 115 | expect { 116 | Keen::Client.new( 117 | :project_id => "12345" 118 | ).publish_batch(events) 119 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Write Key must be set for this operation") 120 | end 121 | 122 | it "should publish a batch of events" do 123 | stub_keen_post(api_event_resource_url(api_url), 201, "") 124 | client.publish_batch(events) 125 | expect_keen_post( 126 | api_event_resource_url(api_url), 127 | events, "sync", write_key) 128 | end 129 | end 130 | 131 | describe "publish_async" do 132 | # no TLS support in EventMachine on jRuby 133 | unless defined?(JRUBY_VERSION) 134 | it "should require a running event loop" do 135 | expect { 136 | client.publish_async(collection, event_properties) 137 | }.to raise_error(Keen::Error) 138 | end 139 | 140 | it "should post the event data" do 141 | stub_keen_post(api_event_collection_resource_url(api_url, collection), 201, api_success) 142 | EM.run { 143 | client.publish_async(collection, event_properties).callback { 144 | begin 145 | expect_keen_post(api_event_collection_resource_url(api_url, collection), event_properties, "async", write_key) 146 | ensure 147 | EM.stop 148 | end 149 | }.errback { 150 | EM.stop 151 | fail 152 | } 153 | } 154 | end 155 | 156 | it "should url encode the event collection" do 157 | stub_keen_post(api_event_collection_resource_url(api_url, 'User%20posts.new%20)(*%26%5E%25%40!)%3A%3A%2520%2520'), 201, api_success) 158 | EM.run { 159 | client.publish_async('User posts.new )(*&^%@!)::%20%20', event_properties).callback { 160 | begin 161 | expect_keen_post(api_event_collection_resource_url(api_url, 'User%20posts.new%20)(*%26%5E%25%40!)%3A%3A%2520%2520'), event_properties, "async", write_key) 162 | ensure 163 | EM.stop 164 | end 165 | }.errback { 166 | EM.stop 167 | fail 168 | } 169 | } 170 | end 171 | 172 | it "should raise an argument error if no event collection is specified" do 173 | expect { 174 | client.publish_async(nil, {}) 175 | }.to raise_error(ArgumentError) 176 | end 177 | 178 | it "should raise an argument error if no properties are specified" do 179 | expect { 180 | client.publish_async(collection, nil) 181 | }.to raise_error(ArgumentError) 182 | end 183 | 184 | describe "deferrable callbacks" do 185 | it "should trigger callbacks" do 186 | stub_keen_post(api_event_collection_resource_url(api_url, collection), 201, api_success) 187 | EM.run { 188 | client.publish_async(collection, event_properties).callback { |response| 189 | begin 190 | expect(response).to eq(api_success) 191 | ensure 192 | EM.stop 193 | end 194 | } 195 | } 196 | end 197 | 198 | it "should trigger errbacks" do 199 | stub_request(:post, api_event_collection_resource_url(api_url, collection)).to_timeout 200 | EM.run { 201 | client.publish_async(collection, event_properties).errback { |error| 202 | begin 203 | expect(error).to_not be_nil 204 | expect(error.message).to eq("Keen IO Exception: HTTP publish_async failure: WebMock timeout error") 205 | ensure 206 | EM.stop 207 | end 208 | } 209 | } 210 | end 211 | 212 | it "should not trap exceptions in the client callback" do 213 | stub_keen_post(api_event_collection_resource_url(api_url, "foo%20bar"), 201, api_success) 214 | expect { 215 | EM.run { 216 | client.publish_async("foo bar", event_properties).callback { 217 | begin 218 | blowup 219 | ensure 220 | EM.stop 221 | end 222 | } 223 | } 224 | }.to raise_error(NameError) 225 | end 226 | end 227 | end 228 | end 229 | 230 | describe "publish_batch_async" do 231 | unless defined?(JRUBY_VERSION) 232 | let(:multi) { EventMachine::MultiRequest.new } 233 | let(:events) { 234 | { 235 | :purchases => [ 236 | { :price => 10 }, 237 | { :price => 11 } 238 | ], 239 | :signups => [ 240 | { :name => "bob" }, 241 | { :name => "bill" } 242 | ] 243 | } 244 | } 245 | 246 | it "should raise an exception if client has no project_id" do 247 | expect { 248 | Keen::Client.new( 249 | :write_key => "abcde" 250 | ).publish_batch_async(events) 251 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Project ID must be set") 252 | end 253 | 254 | it "should raise an exception if client has no write_key" do 255 | expect { 256 | Keen::Client.new( 257 | :project_id => "12345" 258 | ).publish_batch_async(events) 259 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Write Key must be set for this operation") 260 | end 261 | 262 | describe "deferrable callbacks" do 263 | it "should trigger callbacks" do 264 | stub_keen_post(api_event_resource_url(api_url), 201, api_success) 265 | EM.run { 266 | client.publish_batch_async(events).callback { |response| 267 | begin 268 | expect(response).to eq(api_success) 269 | ensure 270 | EM.stop 271 | end 272 | } 273 | } 274 | end 275 | 276 | it "should trigger errbacks" do 277 | stub_request(:post, api_event_resource_url(api_url)).to_timeout 278 | EM.run { 279 | client.publish_batch_async(events).errback { |error| 280 | begin 281 | expect(error).to_not be_nil 282 | expect(error.message).to eq("Keen IO Exception: HTTP publish_async failure: WebMock timeout error") 283 | ensure 284 | EM.stop 285 | end 286 | } 287 | } 288 | end 289 | 290 | it "should not trap exceptions in the client callback" do 291 | stub_keen_post(api_event_resource_url(api_url), 201, api_success) 292 | expect { 293 | EM.run { 294 | client.publish_batch_async(events).callback { 295 | begin 296 | blowup 297 | ensure 298 | EM.stop 299 | end 300 | } 301 | } 302 | }.to raise_error(NameError) 303 | end 304 | end 305 | end 306 | end 307 | 308 | it "should raise an exception if client has no project_id" do 309 | expect { 310 | Keen::Client.new.publish_async(collection, event_properties) 311 | }.to raise_error(Keen::ConfigurationError) 312 | end 313 | 314 | describe "#add_event" do 315 | it "should alias to publish" do 316 | expect(client).to receive(:publish).with(collection, {:a => 1}) 317 | client.add_event(collection, {:a => 1}, {:b => 2}) 318 | end 319 | end 320 | 321 | describe "beacon_url" do 322 | it "should return a url with a base-64 encoded json param" do 323 | expect(client.beacon_url("sign_ups", { :name => "Bob" })).to eq("#{api_url}/3.0/projects/12345/events/sign_ups?api_key=#{write_key}&data=eyJuYW1lIjoiQm9iIn0=") 324 | end 325 | end 326 | 327 | describe "redirect_url" do 328 | it "should return a url with a base-64 encoded json param and an encoded redirect url" do 329 | expect(client.redirect_url("sign_ups", { :name => "Bob" }, "http://keen.io/?foo=bar&bar=baz")).to eq("#{api_url}/3.0/projects/12345/events/sign_ups?api_key=#{write_key}&data=eyJuYW1lIjoiQm9iIn0=&redirect=http%3A%2F%2Fkeen.io%2F%3Ffoo%3Dbar%26bar%3Dbaz") 330 | end 331 | end 332 | 333 | end 334 | -------------------------------------------------------------------------------- /spec/keen/client/querying_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../spec_helper", __FILE__) 2 | 3 | describe Keen::Client do 4 | let(:project_id) { "12345" } 5 | let(:read_key) { "abcde" } 6 | let(:api_url) { "https://notreal.keen.io" } 7 | let(:api_version) { "3.0" } 8 | let(:event_collection) { "users" } 9 | let(:client) { Keen::Client.new( 10 | :project_id => project_id, :read_key => read_key, 11 | :api_url => api_url ) } 12 | 13 | def query_url(query_name, query_params = "") 14 | "#{api_url}/#{api_version}/projects/#{project_id}/queries/#{query_name}#{query_params}" 15 | end 16 | 17 | describe "querying names" do 18 | let(:params) { { :event_collection => "signups" } } 19 | 20 | ["minimum", "maximum", "sum", "average", "count", "count_unique", "select_unique", "extraction", "multi_analysis", "median", "percentile"].each do |query_name| 21 | it "should call keen query passing the query name" do 22 | expect(client).to receive(:query).with(query_name.to_sym, event_collection, params, {}) 23 | client.send(query_name, event_collection, params) 24 | end 25 | end 26 | 27 | describe "funnel" do 28 | it "should call keen query w/o event collection" do 29 | expect(client).to receive(:query).with(:funnel, nil, params, {}) 30 | client.funnel(params) 31 | end 32 | end 33 | end 34 | 35 | describe "#query" do 36 | describe "with an improperly configured client" do 37 | it "should require a project id" do 38 | expect { 39 | Keen::Client.new(:read_key => read_key).count("users", {}) 40 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Project ID must be set") 41 | end 42 | 43 | it "should require a read key" do 44 | expect { 45 | Keen::Client.new(:project_id => project_id).count("users", {}) 46 | }.to raise_error(Keen::ConfigurationError, "Keen IO Exception: Read Key must be set for this operation") 47 | end 48 | end 49 | 50 | describe "with a valid client" do 51 | let(:query) { client.method(:query) } 52 | let(:query_name) { "count" } 53 | let(:api_response) { { "result" => 1 } } 54 | 55 | def test_query(extra_query_params="", extra_query_hash={}) 56 | expected_query_params = "?event_collection=#{event_collection}" 57 | expected_query_params += extra_query_params 58 | expected_url = query_url(query_name, expected_query_params) 59 | stub_keen_get(expected_url, 200, :result => 1) 60 | response = query.call(query_name, event_collection, extra_query_hash) 61 | expect(response).to eq(api_response["result"]) 62 | expect_keen_get(expected_url, "sync", read_key) 63 | end 64 | 65 | it "should call the API w/ proper headers and return the processed json response" do 66 | test_query 67 | end 68 | 69 | it "should encode filters properly" do 70 | filters = [{ 71 | :property_name => "the+animal", 72 | :operator => "eq", 73 | :property_value => "dogs" 74 | }] 75 | filter_str = CGI.escape(MultiJson.encode(filters)) 76 | test_query("&filters=#{filter_str}", :filters => filters) 77 | end 78 | 79 | it "should encode a single group by property" do 80 | test_query("&group_by=one%20foo", :group_by => "one foo") 81 | end 82 | 83 | it "should encode multi-group by properly" do 84 | group_by = ["one", "two"] 85 | group_by_str = CGI.escape(MultiJson.encode(group_by)) 86 | test_query("&group_by=#{group_by_str}", :group_by => group_by) 87 | end 88 | 89 | it "should encode an array of property names property" do 90 | property_names = ["one", "two"] 91 | property_names_str = CGI.escape(MultiJson.encode(property_names)) 92 | test_query("&property_names=#{property_names_str}", :property_names => property_names) 93 | end 94 | 95 | it "should encode a percentile decimal properly" do 96 | test_query("&percentile=99.99", :percentile => 99.99) 97 | end 98 | 99 | it "should encode absolute timeframes properly" do 100 | timeframe = { 101 | :start => "2012-08-13T19:00Z+00:00", 102 | :end => "2012-08-13T19:00Z+00:00", 103 | } 104 | timeframe_str = CGI.escape(MultiJson.encode(timeframe)) 105 | test_query("&timeframe=#{timeframe_str}", :timeframe => timeframe) 106 | end 107 | 108 | it "should encode steps properly" do 109 | steps = [{ 110 | :event_collection => "sign ups", 111 | :actor_property => "user.id" 112 | }] 113 | steps_str = CGI.escape(MultiJson.encode(steps)) 114 | test_query("&steps=#{steps_str}", :steps => steps) 115 | end 116 | 117 | it "should not encode relative timeframes" do 118 | timeframe = "last_10_days" 119 | test_query("&timeframe=#{timeframe}", :timeframe => timeframe) 120 | end 121 | 122 | it "should raise a failed responses" do 123 | query_params = "?event_collection=#{event_collection}" 124 | url = query_url(query_name, query_params) 125 | 126 | stub_keen_get(url, 401, :error => {}) 127 | expect { 128 | query.call(query_name, event_collection, {}) 129 | }.to raise_error(Keen::AuthenticationError) 130 | expect_keen_get(url, "sync", read_key) 131 | end 132 | 133 | it "should not change the extra params" do 134 | timeframe = { 135 | :start => "2012-08-13T19:00Z+00:00", 136 | :end => "2012-08-13T19:00Z+00:00", 137 | } 138 | timeframe_str = CGI.escape(MultiJson.encode(timeframe)) 139 | 140 | test_query("&timeframe=#{timeframe_str}", options = {:timeframe => timeframe}) 141 | expect(options).to eq({:timeframe => timeframe}) 142 | end 143 | 144 | it "should return the full API response if the response option is set to all_keys" do 145 | expected_url = query_url("funnel", "?steps=#{MultiJson.encode([])}") 146 | stub_keen_get(expected_url, 200, :result => [1]) 147 | api_response = query.call("funnel", nil, { :steps => [] }, { :response => :all_keys }) 148 | expect(api_response).to eq({ "result" => [1] }) 149 | end 150 | 151 | context "if log_queries is true" do 152 | before(:each) { client.log_queries = true } 153 | 154 | it "logs the query" do 155 | expect(client).to receive(:log_query).with(query_url("count", "?event_collection=users")) 156 | test_query 157 | end 158 | 159 | after(:each) { client.log_queries = false } 160 | end 161 | 162 | context "if method option is set to post" do 163 | let(:steps) do 164 | [{ 165 | :event_collection => "sign ups", 166 | :actor_property => "user.id" 167 | }] 168 | end 169 | let(:expected_url) { query_url("funnel") } 170 | before(:each) { stub_keen_post(expected_url, 200, :result => 1) } 171 | 172 | it "should call API with post body" do 173 | response = query.call("funnel", nil, { :steps => steps }, { :method => :post }) 174 | 175 | expect_keen_post(expected_url, { :steps => steps }, "sync", read_key) 176 | expect(response).to eq(api_response["result"]) 177 | end 178 | 179 | context "if log_queries is true" do 180 | before(:each) { client.log_queries = true } 181 | 182 | it "logs the query" do 183 | expected_params = {:steps=>[{:event_collection=>"sign ups", :actor_property=>"user.id"}]} 184 | expect(client).to receive(:log_query).with(expected_url, 'POST', expected_params) 185 | query.call("funnel", nil, { :steps => steps }, { :method => :post }) 186 | end 187 | 188 | after(:each) { client.log_queries = false } 189 | end 190 | end 191 | 192 | it "should add extra headers if you supply them as an option" do 193 | url = query_url("count", "?event_collection=#{event_collection}") 194 | extra_headers = { 195 | "Keen-Flibbity-Flabbidy" => "foobar" 196 | } 197 | 198 | options = { 199 | :headers => extra_headers 200 | } 201 | 202 | stub_keen_get(url, 200, :result => 10) 203 | client.count(event_collection, {}, options) 204 | expect_keen_get(url, "sync", read_key, extra_headers) 205 | end 206 | end 207 | end 208 | 209 | describe "#count" do 210 | let(:query_params) { "?event_collection=#{event_collection}" } 211 | let(:url) { query_url("count", query_params) } 212 | before do 213 | stub_keen_get(url, 200, :result => 10) 214 | end 215 | 216 | it "should not require params" do 217 | expect(client.count(event_collection)).to eq(10) 218 | expect_keen_get(url, "sync", read_key) 219 | end 220 | 221 | context "with event collection as symbol" do 222 | let(:event_collection) { :users } 223 | it "should not require a string" do 224 | expect(client.count(event_collection)).to eq(10) 225 | end 226 | end 227 | end 228 | 229 | describe "#extraction" do 230 | it "should not require params" do 231 | query_params = "?event_collection=#{event_collection}" 232 | url = query_url("extraction", query_params) 233 | stub_keen_get(url, 200, :result => { "a" => 1 } ) 234 | expect(client.extraction(event_collection)).to eq({ "a" => 1 }) 235 | expect_keen_get(url, "sync", read_key) 236 | end 237 | end 238 | 239 | describe "#query_url" do 240 | let(:expected) { } 241 | 242 | it "should returns the URL for a query" do 243 | response = client.query_url('count', event_collection) 244 | expect(response).to eq 'https://notreal.keen.io/3.0/projects/12345/queries/count?event_collection=users&api_key=abcde' 245 | end 246 | 247 | it "should exclude the api key if option is passed" do 248 | response = client.query_url('count', event_collection, {}, :exclude_api_key => true) 249 | expect(response).to eq 'https://notreal.keen.io/3.0/projects/12345/queries/count?event_collection=users' 250 | end 251 | 252 | it "should not run the query" do 253 | expect(Keen::HTTP::Sync).to_not receive(:new) 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /spec/keen/client_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe Keen::Client do 4 | let(:project_id) { "12345" } 5 | let(:write_key) { "abcdewrite" } 6 | let(:read_key) { "abcderead" } 7 | let(:api_url) { "http://fake.keen.io:fakeport" } 8 | let(:client) { Keen::Client.new(:project_id => project_id) } 9 | let(:read_timeout) { 40 } 10 | let(:open_timeout) { 10 } 11 | 12 | before do 13 | ENV["KEEN_PROJECT_ID"] = nil 14 | ENV["KEEN_WRITE_KEY"] = nil 15 | ENV["KEEN_READ_KEY"] = nil 16 | ENV["KEEN_API_URL"] = nil 17 | ENV["KEEN_READ_TIMEOUT"] = nil 18 | ENV["KEEN_OPEN_TIMEOUT"] = nil 19 | end 20 | 21 | describe "#initialize" do 22 | context "deprecated" do 23 | it "should allow created via project_id and key args" do 24 | client = Keen::Client.new(project_id, write_key, read_key) 25 | expect(client.write_key).to eq(write_key) 26 | expect(client.read_key).to eq(read_key) 27 | expect(client.project_id).to eq(project_id) 28 | end 29 | end 30 | 31 | it "should initialize with options" do 32 | client = Keen::Client.new( 33 | :project_id => project_id, 34 | :write_key => write_key, 35 | :read_key => read_key, 36 | :api_url => api_url, 37 | :read_timeout => read_timeout, 38 | :open_timeout => open_timeout) 39 | expect(client.write_key).to eq(write_key) 40 | expect(client.read_key).to eq(read_key) 41 | expect(client.project_id).to eq(project_id) 42 | expect(client.api_url).to eq(api_url) 43 | expect(client.read_timeout).to eq(read_timeout) 44 | expect(client.open_timeout).to eq(open_timeout) 45 | end 46 | 47 | it "should set a default api_url" do 48 | expect(Keen::Client.new.api_url).to eq("https://api.keen.io") 49 | end 50 | end 51 | 52 | describe "process_response" do 53 | let (:body) { "{ \"wazzup\": 1 }" } 54 | let (:exception_body) { "Keen IO Exception: { \"wazzup\": 1 }" } 55 | let (:process_response) { client.method(:process_response) } 56 | 57 | it "should return encoded json for a 200" do 58 | expect(process_response.call(200, body)).to eq({ "wazzup" => 1 }) 59 | end 60 | 61 | it "should return encoded json for a 201" do 62 | expect(process_response.call(201, body)).to eq({ "wazzup" => 1 }) 63 | end 64 | 65 | it "should return empty for bad json on a 200/201" do 66 | expect(process_response.call(200, "invalid json")).to eq({}) 67 | end 68 | 69 | it "should raise a bad request error for a 400" do 70 | expect { 71 | process_response.call(400, body) 72 | }.to raise_error(Keen::BadRequestError, exception_body) 73 | end 74 | 75 | it "should raise a authentication error for a 401" do 76 | expect { 77 | process_response.call(401, body) 78 | }.to raise_error(Keen::AuthenticationError, exception_body) 79 | end 80 | 81 | it "should raise a not found error for a 404" do 82 | expect { 83 | process_response.call(404, body) 84 | }.to raise_error(Keen::NotFoundError, exception_body) 85 | end 86 | 87 | it "should raise an http error otherwise" do 88 | expect { 89 | process_response.call(420, body) 90 | }.to raise_error(Keen::HttpError, exception_body) 91 | end 92 | end 93 | 94 | describe "preprocess_params" do 95 | it "returns an empty string with no parameters" do 96 | params = {} 97 | expect( 98 | client.instance_eval{preprocess_params(params)} 99 | ).to eq("") 100 | end 101 | 102 | it "strips out nil parameters" do 103 | params = { :timeframe => nil, :group_by => "foo.bar" } 104 | expect( 105 | client.instance_eval{preprocess_params(params)} 106 | ).to eq("group_by=foo.bar") 107 | end 108 | end 109 | 110 | describe "preprocess_max_age" do 111 | it "stringifies numbers" do 112 | params = { :group_by => "foo.bar", :max_age => 3000 } 113 | expect( 114 | client.instance_eval{preprocess_params(params)} 115 | ).to eq("group_by=foo.bar&max_age=3000") 116 | end 117 | 118 | it "ignores non-numbers" do 119 | params = { :max_age => 'one hundred', :group_by => "foo.bar" } 120 | expect( 121 | client.instance_eval{preprocess_params(params)} 122 | ).to eq("group_by=foo.bar") 123 | end 124 | end 125 | 126 | describe "preprocess_timeframe" do 127 | it "does nothing for string values" do 128 | params = { :timeframe => 'this_3_days' } 129 | expect { 130 | client.instance_eval{preprocess_timeframe(params)} 131 | }.to_not change { params } 132 | end 133 | 134 | it "multi encodes for hash values" do 135 | params = {:timeframe => {:start => '2012-08-13T19:00:00.000Z', :end => '2013-09-20T19:00:00.000Z'} } 136 | expect { 137 | client.instance_eval{preprocess_timeframe(params)} 138 | }.to change {params}.to({:timeframe => "{\"start\":\"2012-08-13T19:00:00.000Z\",\"end\":\"2013-09-20T19:00:00.000Z\"}"}) 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/keen/keen_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe Keen do 4 | describe "default client" do 5 | describe "configuring from the environment" do 6 | before do 7 | Keen.instance_variable_set(:@default_client, nil) 8 | ENV["KEEN_PROJECT_ID"] = "12345" 9 | ENV["KEEN_WRITE_KEY"] = "abcdewrite" 10 | ENV["KEEN_READ_KEY"] = "abcderead" 11 | ENV["KEEN_MASTER_KEY"] = "lalalala" 12 | ENV["KEEN_API_URL"] = "http://fake.keen.io:fakeport" 13 | ENV["KEEN_PROXY_URL"] = "http://proxy.keen.io:proxyport" 14 | ENV["KEEN_PROXY_TYPE"] = "http" 15 | end 16 | 17 | let(:client) { Keen.send(:default_client) } 18 | 19 | it "should set a project id from the environment" do 20 | expect(client.project_id).to eq("12345") 21 | end 22 | 23 | it "should set a write key from the environment" do 24 | expect(client.write_key).to eq("abcdewrite") 25 | end 26 | 27 | it "should set a read key from the environment" do 28 | expect(client.read_key).to eq("abcderead") 29 | end 30 | 31 | it "should set a master key from the environment" do 32 | expect(client.master_key).to eq("lalalala") 33 | end 34 | 35 | it "should set an api host from the environment" do 36 | expect(client.api_url).to eq("http://fake.keen.io:fakeport") 37 | end 38 | 39 | it "should set an proxy host from the environment" do 40 | expect(client.proxy_url).to eq("http://proxy.keen.io:proxyport") 41 | end 42 | 43 | it "should set an proxy type from the environment" do 44 | expect(client.proxy_type).to eq("http") 45 | end 46 | end 47 | end 48 | 49 | describe "Keen delegation" do 50 | it "should memoize the default client, retaining settings" do 51 | Keen.project_id = "new-abcde" 52 | expect(Keen.project_id).to eq("new-abcde") 53 | end 54 | 55 | after do 56 | Keen.instance_variable_set(:@default_client, nil) 57 | end 58 | end 59 | 60 | describe "forwardable" do 61 | before do 62 | @default_client = double("client") 63 | allow(Keen).to receive(:default_client).and_return(@default_client) 64 | end 65 | 66 | [:project_id, :write_key, :read_key, :api_url, :proxy_url, :proxy_type].each do |_method| 67 | it "should forward the #{_method} method" do 68 | expect(@default_client).to receive(_method) 69 | Keen.send(_method) 70 | end 71 | end 72 | 73 | [:project_id, :write_key, :read_key, :master_key, :api_url].each do |_method| 74 | it "should forward the #{_method} method" do 75 | expect(@default_client).to receive(_method) 76 | Keen.send(_method) 77 | end 78 | end 79 | 80 | [:project_id=, :write_key=, :read_key=, :master_key=, :api_url=].each do |_method| 81 | it "should forward the #{_method} method" do 82 | expect(@default_client).to receive(_method).with("12345") 83 | Keen.send(_method, "12345") 84 | end 85 | end 86 | 87 | [:publish, :publish_async, :publish_batch, :publish_batch_async].each do |_method| 88 | it "should forward the #{_method} method" do 89 | expect(@default_client).to receive(_method).with("users", {}) 90 | Keen.send(_method, "users", {}) 91 | end 92 | end 93 | 94 | # pull the query methods list at runtime in order to ensure 95 | # any new methods have a corresponding delegator 96 | Keen::Client::QueryingMethods.instance_methods.each do |_method| 97 | it "should forward the #{_method} query method" do 98 | expect(@default_client).to receive(_method).with("users", {}) 99 | Keen.send(_method, "users", {}) 100 | end 101 | end 102 | end 103 | 104 | describe "logger" do 105 | it "should be set to info" do 106 | expect(Keen.logger.level).to eq(Logger::INFO) 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/keen/saved_query_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe Keen do 4 | let(:client) do 5 | Keen::Client.new( 6 | project_id: "12341234", 7 | master_key: "abcdef", 8 | api_url: "https://notreal.keen.io" 9 | ) 10 | end 11 | 12 | describe "#saved_queries" do 13 | describe "#all" do 14 | 15 | it "returns all saved queries" do 16 | all_saved_queries = [ { 17 | "refresh_rate" => 0, 18 | "last_modified_date" => "2015-10-19T20:14:29.797000+00:00", 19 | "query_name" => "Analysis-API-Calls-this-1-day", 20 | "query" => { 21 | "filters" => [], 22 | "analysis_type" => "count", 23 | "timezone" => "UTC", 24 | "timeframe" => "this_1_days", 25 | "event_collection" => "analysis_api_call" 26 | }, 27 | "metadata" => { 28 | "visualization" => { "chart_type" => "metric"} 29 | } 30 | } ] 31 | stub_keen_get(saved_query_endpoint, 200, all_saved_queries) 32 | 33 | all_saved_queries_response = client.saved_queries.all 34 | 35 | expect(all_saved_queries_response).to eq(all_saved_queries) 36 | end 37 | end 38 | 39 | describe "#get" do 40 | it "returns a specific saved query given a query id" do 41 | saved_query = { 42 | "refresh_rate" => 0, 43 | "last_modified_date" => "2015-10-19T20:14:29.797000+00:00", 44 | "query_name" => "Analysis-API-Calls-this-1-day", 45 | "query" => { 46 | "filters" => [], 47 | "analysis_type" => "count", 48 | "timezone" => "UTC", 49 | "timeframe" => "this_1_days", 50 | "event_collection" => "analysis_api_call" 51 | }, 52 | "metadata" => { 53 | "visualization" => { "chart_type" => "metric"} 54 | } 55 | } 56 | stub_keen_get( 57 | saved_query_endpoint + "/#{saved_query["query_name"]}", 58 | 200, 59 | saved_query 60 | ) 61 | 62 | saved_query_response = client.saved_queries.get("Analysis-API-Calls-this-1-day") 63 | 64 | expect(saved_query_response).to eq(saved_query) 65 | end 66 | 67 | it "throws an exception if service can't find saved query" do 68 | saved_query = { 69 | message: "Resource not found", 70 | error_code: "ResourceNotFoundError" 71 | } 72 | stub_keen_get( 73 | saved_query_endpoint + "/missing-query", 74 | 404, 75 | saved_query 76 | ) 77 | 78 | expect { 79 | client.saved_queries.get("missing-query") 80 | }.to raise_error(Keen::NotFoundError) 81 | end 82 | end 83 | 84 | describe "#create" do 85 | it "returns the created saved query when creation is successful" do 86 | saved_query = { 87 | "refresh_rate" => 0, 88 | "last_modified_date" => "2015-10-19T20:14:29.797000+00:00", 89 | "query_name" => "new-query", 90 | "query" => { 91 | "filters" => [], 92 | "analysis_type" => "count", 93 | "timezone" => "UTC", 94 | "timeframe" => "this_1_days", 95 | "event_collection" => "analysis_api_call" 96 | }, 97 | "metadata" => { 98 | "visualization" => { "chart_type" => "metric"} 99 | } 100 | } 101 | stub_keen_put( 102 | saved_query_endpoint + "/#{saved_query[:query_name]}", 201, saved_query 103 | ) 104 | 105 | saved_query_response = client.saved_queries.create(saved_query[:query_name], saved_query) 106 | 107 | expect(saved_query_response).to eq(saved_query) 108 | end 109 | 110 | it "raises an error when creation is unsuccessful" do 111 | stub_keen_put( 112 | saved_query_endpoint + "/saved-query-name", 400, {} 113 | ) 114 | 115 | expect { 116 | client.saved_queries.create("saved-query-name", {}) 117 | }.to raise_error(Keen::BadRequestError) 118 | end 119 | end 120 | 121 | describe "#update_full" do 122 | it "returns the created saved query when update is successful" do 123 | saved_query = { 124 | "refresh_rate" => 0, 125 | "last_modified_date" => "2015-10-19T20:14:29.797000+00:00", 126 | "query_name" => "new-query", 127 | "query" => { 128 | "filters" => [], 129 | "analysis_type" => "count", 130 | "timezone" => "UTC", 131 | "timeframe" => "this_1_days", 132 | "event_collection" => "analysis_api_call" 133 | }, 134 | "metadata" => { 135 | "visualization" => { "chart_type" => "metric"} 136 | } 137 | } 138 | stub_keen_put( 139 | saved_query_endpoint + "/#{saved_query[:query_name]}", 200, saved_query 140 | ) 141 | 142 | saved_query_response = client.saved_queries.update_full(saved_query[:query_name], saved_query) 143 | 144 | expect(saved_query_response).to eq(saved_query) 145 | end 146 | end 147 | 148 | describe "#delete" do 149 | it "returns true with deletion is successful" do 150 | query_name = "query-to-be-deleted" 151 | stub_keen_delete( 152 | saved_query_endpoint + "/#{query_name}", 204 153 | ) 154 | 155 | saved_query_response = client.saved_queries.delete(query_name) 156 | 157 | expect(saved_query_response).to eq(true) 158 | end 159 | end 160 | end 161 | 162 | def saved_query_endpoint 163 | client.api_url + "/#{client.api_version}/projects/#{client.project_id}/queries/saved" 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/keen/scoped_key_old_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Keen::ScopedKey do 4 | let(:api_key) { "ab428324dbdbcfe744" } 5 | let(:bad_api_key) { "badbadbadbad" } 6 | let(:data) { { 7 | "filters" => [{ 8 | "property_name" => "accountId", 9 | "operator" => "eq", 10 | "property_value" => "123456" 11 | }] 12 | }} 13 | let(:new_scoped_key) { Keen::ScopedKey.new(api_key, data) } 14 | 15 | describe "constructor" do 16 | it "should retain the api_key" do 17 | expect(new_scoped_key.api_key).to eq(api_key) 18 | end 19 | 20 | it "should retain the data" do 21 | expect(new_scoped_key.data).to eq(data) 22 | end 23 | end 24 | 25 | describe "encrypt! and decrypt!" do 26 | it "should encrypt and hex encode the data using the api key" do 27 | encrypted_str = new_scoped_key.encrypt! 28 | other_api_key = Keen::ScopedKey.decrypt!(api_key, encrypted_str) 29 | expect(other_api_key.data).to eq(data) 30 | end 31 | 32 | describe "when an IV is not provided" do 33 | it "should not produce the same encrypted key text" do 34 | expect(new_scoped_key.encrypt!).to_not eq(new_scoped_key.encrypt!) 35 | end 36 | end 37 | 38 | describe "when an IV is provided" do 39 | it "should produce the same encrypted key text for a " do 40 | iv = "\0" * 16 41 | expect(new_scoped_key.encrypt!(iv)).to eq(new_scoped_key.encrypt!(iv)) 42 | end 43 | 44 | it "should raise error when an invalid IV is supplied" do 45 | iv = "fakeiv" 46 | expect { new_scoped_key.encrypt!(iv) }.to raise_error(OpenSSL::Cipher::CipherError) 47 | end 48 | end 49 | 50 | it "should not decrypt the scoped key with a bad api key" do 51 | encrypted_str = new_scoped_key.encrypt! 52 | expect { 53 | other_api_key = Keen::ScopedKey.decrypt!(bad_api_key, encrypted_str) 54 | }.to raise_error(OpenSSL::Cipher::CipherError) 55 | end 56 | 57 | it "should properly encrypt with master keys that are 32 characters long" do 58 | master_key = "\0" * 32 59 | scoped_key = Keen::ScopedKey.new(master_key, data).encrypt! 60 | expect(Keen::ScopedKey.decrypt!(master_key, scoped_key).data).to eq(data) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/keen/scoped_key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Keen::ScopedKey do 4 | let(:api_key) { "24077ACBCB198BAAA2110EDDB673282F8E34909FD823A15C55A6253A664BE368" } 5 | let(:bad_api_key) { "24077ACBCB198BAAA2110EDDB673282F8E34909FD823A15C55A6253A664BE369" } 6 | let(:data) { { 7 | "filters" => [{ 8 | "property_name" => "accountId", 9 | "operator" => "eq", 10 | "property_value" => "123456" 11 | }] 12 | }} 13 | let(:new_scoped_key) { Keen::ScopedKey.new(api_key, data) } 14 | 15 | describe "constructor" do 16 | it "should retain the api_key" do 17 | expect(new_scoped_key.api_key).to eq(api_key) 18 | end 19 | 20 | it "should retain the data" do 21 | expect(new_scoped_key.data).to eq(data) 22 | end 23 | end 24 | 25 | describe "encrypt! and decrypt!" do 26 | it "should encrypt and hex encode the data using the api key" do 27 | encrypted_str = new_scoped_key.encrypt! 28 | other_api_key = Keen::ScopedKey.decrypt!(api_key, encrypted_str) 29 | expect(other_api_key.data).to eq(data) 30 | end 31 | 32 | describe "when an IV is not provided" do 33 | it "should not produce the same encrypted key text" do 34 | expect(new_scoped_key.encrypt!).to_not eq(new_scoped_key.encrypt!) 35 | end 36 | end 37 | 38 | describe "when an IV is provided" do 39 | it "should produce the same encrypted key text for a " do 40 | iv = "\0" * 16 41 | expect(new_scoped_key.encrypt!(iv)).to eq(new_scoped_key.encrypt!(iv)) 42 | end 43 | 44 | it "should raise error when an invalid IV is supplied" do 45 | iv = "fakeiv" 46 | expect { new_scoped_key.encrypt!(iv) }.to raise_error(OpenSSL::Cipher::CipherError) 47 | end 48 | end 49 | 50 | it "should not decrypt the scoped key with a bad api key" do 51 | encrypted_str = new_scoped_key.encrypt! 52 | expect { 53 | other_api_key = Keen::ScopedKey.decrypt!(bad_api_key, encrypted_str) 54 | }.to raise_error(OpenSSL::Cipher::CipherError) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/keen/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../spec_helper", __FILE__) 2 | 3 | require 'webmock/rspec' 4 | 5 | RSpec.configure do |config| 6 | config.before(:each) do 7 | WebMock.disable_net_connect! 8 | WebMock.reset! 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'Use of Bundler is recommended' 5 | end 6 | 7 | require 'rspec' 8 | require 'net/https' 9 | require 'em-http' 10 | 11 | require File.expand_path("../../lib/keen", __FILE__) 12 | 13 | module Keen::SpecHelpers 14 | def stub_keen_request(method, url, status, response_body) 15 | stub_request(method, url).to_return(:status => status, :body => response_body) 16 | end 17 | 18 | def stub_keen_post(url, status, response_body) 19 | stub_keen_request(:post, url, status, MultiJson.encode(response_body)) 20 | end 21 | 22 | def stub_keen_put(url, status, response_body) 23 | stub_keen_request(:put, url, status, MultiJson.encode(response_body)) 24 | end 25 | 26 | def stub_keen_get(url, status, response_body) 27 | stub_keen_request(:get, url, status, MultiJson.encode(response_body)) 28 | end 29 | 30 | def stub_keen_delete(url, status) 31 | stub_keen_request(:delete, url, status, "") 32 | end 33 | 34 | def expect_keen_request(method, url, body, sync_or_async_ua, read_or_write_key, extra_headers={}) 35 | user_agent = "keen-gem, v#{Keen::VERSION}, #{sync_or_async_ua}" 36 | user_agent += ", #{RUBY_VERSION}, #{RUBY_PLATFORM}, #{RUBY_PATCHLEVEL}" 37 | if defined?(RUBY_ENGINE) 38 | user_agent += ", #{RUBY_ENGINE}" 39 | end 40 | 41 | headers = { "Content-Type" => "application/json", 42 | "User-Agent" => user_agent, 43 | "Authorization" => read_or_write_key, 44 | "Keen-Sdk" => "ruby-#{Keen::VERSION}" } 45 | 46 | headers = headers.merge(extra_headers) if not extra_headers.empty? 47 | 48 | expect(WebMock).to have_requested(method, url).with( 49 | :body => body, 50 | :headers => headers) 51 | 52 | end 53 | 54 | def expect_keen_get(url, sync_or_async_ua, read_key, extra_headers={}) 55 | expect_keen_request(:get, url, "", sync_or_async_ua, read_key, extra_headers) 56 | end 57 | 58 | def expect_keen_post(url, event_properties, sync_or_async_ua, write_key, extra_headers={}) 59 | expect_keen_request(:post, url, MultiJson.encode(event_properties), sync_or_async_ua, write_key, extra_headers) 60 | end 61 | 62 | def expect_keen_delete(url, sync_or_async_ua, master_key, extra_headers={}) 63 | expect_keen_request(:delete, url, "", sync_or_async_ua, master_key, extra_headers) 64 | end 65 | 66 | def api_event_collection_resource_url(base_url, collection) 67 | "#{base_url}/3.0/projects/#{project_id}/events/#{collection}" 68 | end 69 | 70 | def api_event_resource_url(base_url) 71 | "#{base_url}/3.0/projects/#{project_id}/events" 72 | end 73 | end 74 | 75 | RSpec.configure do |config| 76 | config.include(Keen::SpecHelpers) 77 | 78 | config.color = true 79 | config.tty = true 80 | config.formatter = :progress # :progress, :documentation, :html, :textmate 81 | end 82 | -------------------------------------------------------------------------------- /spec/synchrony/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../../spec_helper", __FILE__) 2 | 3 | require 'em-synchrony' 4 | require 'em-synchrony/em-http' 5 | 6 | require 'webmock/rspec' 7 | 8 | -------------------------------------------------------------------------------- /spec/synchrony/synchrony_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("../spec_helper", __FILE__) 2 | 3 | describe Keen::HTTP::Async do 4 | let(:project_id) { "12345" } 5 | let(:write_key) { "abcdewrite" } 6 | let(:collection) { "users" } 7 | let(:api_url) { "https://fake.keen.io" } 8 | let(:event_properties) { { "name" => "Bob" } } 9 | let(:api_success) { { "created" => true } } 10 | let(:batch_api_success) { { "created" => true } } 11 | let(:events) { 12 | { 13 | :purchases => [ 14 | { :price => 10 }, 15 | { :price => 11 } 16 | ], 17 | :signups => [ 18 | { :name => "bob" }, 19 | { :name => "bill" } 20 | ] 21 | } 22 | } 23 | 24 | describe "synchrony" do 25 | before do 26 | @client = Keen::Client.new( 27 | :project_id => project_id, :write_key => write_key, 28 | :api_url => api_url) 29 | end 30 | 31 | describe "success" do 32 | it "should post the event data" do 33 | stub_keen_post(api_event_collection_resource_url(api_url, collection), 201, api_success) 34 | EM.synchrony { 35 | @client.publish_async(collection, event_properties) 36 | expect_keen_post(api_event_collection_resource_url(api_url, collection), event_properties, "async", write_key) 37 | EM.stop 38 | } 39 | end 40 | 41 | it "should receive the right response 'synchronously'" do 42 | stub_keen_post(api_event_collection_resource_url(api_url, collection), 201, api_success) 43 | EM.synchrony { 44 | @client.publish_async(collection, event_properties).should == api_success 45 | EM.stop 46 | } 47 | end 48 | end 49 | 50 | describe "batch success" do 51 | it "should post the event data" do 52 | stub_keen_post(api_event_resource_url(api_url), 201, api_success) 53 | EM.synchrony { 54 | @client.publish_batch_async(events) 55 | expect_keen_post(api_event_resource_url(api_url), events, "async", write_key) 56 | EM.stop 57 | } 58 | end 59 | 60 | it "should receive the right response 'synchronously'" do 61 | stub_keen_post(api_event_resource_url(api_url), 201, api_success) 62 | EM.synchrony { 63 | @client.publish_batch_async(events).should == api_success 64 | EM.stop 65 | } 66 | end 67 | end 68 | 69 | describe "failure" do 70 | it "should raise an exception" do 71 | stub_request(:post, api_event_collection_resource_url(api_url, collection)).to_timeout 72 | e = nil 73 | EM.synchrony { 74 | begin 75 | @client.publish_async(collection, event_properties).should == api_success 76 | rescue Exception => exception 77 | e = exception 78 | end 79 | e.class.should == Keen::HttpError 80 | e.message.should == "Keen IO Exception: HTTP em-synchrony publish_async error: WebMock timeout error" 81 | EM.stop 82 | } 83 | end 84 | end 85 | 86 | describe "batch failure" do 87 | it "should raise an exception" do 88 | stub_request(:post, api_event_resource_url(api_url)).to_timeout 89 | e = nil 90 | EM.synchrony { 91 | begin 92 | @client.publish_batch_async(events).should == api_success 93 | rescue Exception => exception 94 | e = exception 95 | end 96 | e.class.should == Keen::HttpError 97 | e.message.should == "Keen IO Exception: HTTP em-synchrony publish_async error: WebMock timeout error" 98 | EM.stop 99 | } 100 | end 101 | end 102 | end 103 | end 104 | --------------------------------------------------------------------------------