rulesio 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +154 -0
- data/Rakefile +1 -0
- data/lib/rulesio/active_record_extension.rb +29 -0
- data/lib/rulesio/exceptions.rb +79 -0
- data/lib/rulesio/girl_friday_queue.rb +34 -0
- data/lib/rulesio/helpers.rb +27 -0
- data/lib/rulesio/memory_queue.rb +11 -0
- data/lib/rulesio/railtie.rb +97 -0
- data/lib/rulesio/users.rb +67 -0
- data/lib/rulesio/version.rb +3 -0
- data/lib/rulesio.rb +137 -0
- data/rulesio.gemspec +22 -0
- metadata +114 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
rulesio gem for rules.io
|
2
|
+
=========
|
3
|
+
|
4
|
+
[rules.io](http://rules.io) is a rules engine that reacts to things users do or experience in your software, and makes things happen in 3rd party SaaS APIs -- without your having to write any code. Rather than implementing the most rapidly evolving parts of your application's business logic in code, your team can use the rules.io web app to specify "when", "how", and "who", with rules like these:
|
5
|
+
|
6
|
+
* when a user gets a form validation error three times in an hour, send an email to Frank
|
7
|
+
* when a premium customer hasn't logged in for a month, flag them in your CRM
|
8
|
+
* when a user gets a 500 response, create a ticket in Zendesk
|
9
|
+
* when a user invites ten friends, add them to the "well-connected" segment in MailChimp
|
10
|
+
|
11
|
+
This gem contains Rack middleware that automatically generates two event streams, one about exceptions and the other about user activity (pageviews, errors, attempts to save invalid models), that can be used to trigger rules in rules.io. You can also send more specific events manually.
|
12
|
+
|
13
|
+
Setup
|
14
|
+
-----
|
15
|
+
|
16
|
+
In your Gemfile:
|
17
|
+
|
18
|
+
gem 'rulesio'
|
19
|
+
|
20
|
+
###For Ruby on Rails
|
21
|
+
|
22
|
+
You should create two incoming channels (event streams) in rules.io, and configure their tokens in `config/rulesio.rb` (the available options are explained below). You may want to create additional channels to use in other environments, eg for staging.
|
23
|
+
|
24
|
+
token 'CHANNEL_TOKEN' # default channel (for user-centric events)
|
25
|
+
middleware :users # automatically generate events about user activity
|
26
|
+
middleware :exceptions do # automatically generate events for exceptions
|
27
|
+
token 'ERROR_CHANNEL_TOKEN' # separate channel for error-centric events
|
28
|
+
end
|
29
|
+
|
30
|
+
###As general-purpose Rack middleware, with or without Rails
|
31
|
+
|
32
|
+
config.middleware.insert 0, 'RulesIO::Rack', :token => 'CHANNEL_TOKEN'
|
33
|
+
config.middleware.use 'RulesIO::Users'
|
34
|
+
config.middleware.use 'RulesIO::Exceptions', :token => 'ERROR_CHANNEL_TOKEN'
|
35
|
+
|
36
|
+
The current user
|
37
|
+
----------------
|
38
|
+
|
39
|
+
rules.io can take advantage of knowing about the user behind each event, which is supplied by the \_actor field. This gem employs some to determine the current user, but you can also help it out (the default value is `current_user.to_param`). If, for example, you want to use current_user.id as the \_actor for every event (and 0 for the logged out user), you could do this via:
|
40
|
+
|
41
|
+
controller_data '{:_actor => current_user.try(:id) || 0}'
|
42
|
+
|
43
|
+
The string will be evaluated in the context of your controller.
|
44
|
+
|
45
|
+
Sending other events
|
46
|
+
--------------------
|
47
|
+
|
48
|
+
Every event must contain the \_actor, \_timestamp, \_domain and \_name fields. Beyond those fields, you can include any additional data you choose. See [docs](http://rules.io/docs) for more details.
|
49
|
+
|
50
|
+
To manually send an event when a user upgrades to a "premium" account:
|
51
|
+
|
52
|
+
RulesIO.send_event(
|
53
|
+
:_actor => current_user.unique_id,
|
54
|
+
:_timestamp => Time.now.to_f,
|
55
|
+
:_domain => 'account',
|
56
|
+
:_name => 'upgrade',
|
57
|
+
:user_email => current_user.email,
|
58
|
+
:plan => 'premium' )
|
59
|
+
|
60
|
+
Using girl_friday for asynchronous communication and persistence
|
61
|
+
-----------------
|
62
|
+
|
63
|
+
By default this gem sends a batch of events to the rules.io service synchronously, at the end of each request to your application. This means that each request to your app will be slowed down by the time it takes to do that communication. While this is fine for development or for low-volume sites, for those who wish to avoid this delay rulesio supports the use of the [girl_friday](https://github.com/mperham/girl_friday) gem, which you can enable in your rulesio.rb file:
|
64
|
+
|
65
|
+
queue RulesIO::GirlFridayQueue
|
66
|
+
|
67
|
+
Using the GirlFridayQueue also ensures that events are not lost should the rules.io service be temporarily unavailable.
|
68
|
+
|
69
|
+
You can also pass options to girl_friday. To avoid losing events when your app server instances restart, you can tell girl_friday to use Redis. In order to use the Redis backend, you must use the [connection_pool](https://github.com/mperham/connection_pool) gem to share a set of Redis connections with other threads and the GirlFriday queue. If you are not already using Redis in your application, add
|
70
|
+
|
71
|
+
gem 'connection_pool'
|
72
|
+
gem 'redis'
|
73
|
+
|
74
|
+
to your Gemfile, and add something like this to `config/rulesio.rb`:
|
75
|
+
|
76
|
+
require 'connection_pool'
|
77
|
+
|
78
|
+
redis_pool = ConnectionPool.new(:size => 5, :timeout => 5) { ::Redis.new }
|
79
|
+
queue RulesIO::GirlFridayQueue,
|
80
|
+
:store => GirlFriday::Store::Redis, :store_config => { :pool => redis_pool }
|
81
|
+
|
82
|
+
See the [girl_friday wiki](https://github.com/mperham/girl_friday/wiki) for more information on how to use girl_friday.
|
83
|
+
|
84
|
+
Options
|
85
|
+
-------
|
86
|
+
|
87
|
+
RulesIO::Rack accepts these options:
|
88
|
+
|
89
|
+
* `token` -- the token for a rules.io channel
|
90
|
+
* `webhook_url` -- defaults to 'http://www.rules.io/events'
|
91
|
+
* `middleware` -- takes the symbol for a middleware and a block, configuring it
|
92
|
+
* `queue` -- takes the class used for queuing (default: RulesIO::MemoryQueue), and an optional hash; see the section on girl_friday for examples
|
93
|
+
* `controller_data` -- a string evaluated in the context of the Rails controller (if any) handling the request; it should return a hash to be merged into every event (both automatically generated and manually triggered events)
|
94
|
+
|
95
|
+
The `exceptions` middleware accepts these options:
|
96
|
+
|
97
|
+
* `token` -- the token for a rules.io error channel
|
98
|
+
* `ignore_exceptions` -- an array of exception class names, defaults to ['ActiveRecord::RecordNotFound', 'AbstractController::ActionNotFound', 'ActionController::RoutingError']
|
99
|
+
* `ignore_crawlers` -- an array of strings to match against the user agent, includes a number of webcrawlers by default; matching requests do not generate events
|
100
|
+
* `ignore_if` -- this proc is passed env and an exception; if it returns true, the exception is not reported to rules.io
|
101
|
+
* `custom_data` -- this proc is passed env, and should return a hash to be merged into each automatically generated exception event
|
102
|
+
|
103
|
+
The `users` middleware accepts these options:
|
104
|
+
|
105
|
+
* `ignore_crawlers` -- an array of strings to match against the user agent, includes a number of webcrawlers by default; matching requests do not generate events
|
106
|
+
* `ignore_if` -- this proc is passed env; if it returns true, no automatic events are reported to rules.io for this request
|
107
|
+
* `ignore_if_controller` -- a string to be evaluated in the context of the Rails controller instance; if it evaluates to true, no automatic events are reported to rules.io for this request
|
108
|
+
* `custom_data` -- this proc is passed env, and should return a hash to be merged into each automatically generated event
|
109
|
+
|
110
|
+
The RulesIO::Users middleware uses the same token as RulesIO::Rack.
|
111
|
+
|
112
|
+
Here's an example of how to skip sending any user events for all requests to the SillyController:
|
113
|
+
|
114
|
+
middleware :users do
|
115
|
+
ignore_if lambda { |env| env['action_controller.instance'].is_a? SillyController }
|
116
|
+
end
|
117
|
+
|
118
|
+
To make life easier in the case where you want a condition evaluated in the context of a Rails controller, you can do the same thing like this. (Only the users middleware supports ignore_if_controller.)
|
119
|
+
|
120
|
+
middleware :users do
|
121
|
+
ignore_if_controller 'self.is_a?(EventsController)'
|
122
|
+
end
|
123
|
+
|
124
|
+
Or if you want to skip sending pageview events for requests from pingdom.com:
|
125
|
+
|
126
|
+
middleware :users do
|
127
|
+
ignore_crawlers RulesIO.default_ignored_crawlers + ['Pingdom.com_bot']
|
128
|
+
end
|
129
|
+
|
130
|
+
Use Cases
|
131
|
+
---------
|
132
|
+
|
133
|
+
### Example rule triggers
|
134
|
+
|
135
|
+
* whenever a `UserIsHavingAVeryBadDay` exception is raised
|
136
|
+
* the first time any particular exception occurs
|
137
|
+
* whenever a request takes more than 20 seconds to process
|
138
|
+
* whenever someone upgrades their account
|
139
|
+
* whenever someone does comment#create more than 10 times in a day
|
140
|
+
* whenever someone tagged 'active' doesn't login for a week
|
141
|
+
|
142
|
+
### Example rule actions
|
143
|
+
|
144
|
+
* send yourself an email or a mobile push message
|
145
|
+
* send a user an email or a mobile push message
|
146
|
+
* create a ticket in your ticketing system
|
147
|
+
* add a data point to a Librato or StatsMix graph
|
148
|
+
* tag a user in rules.io, or in your CRM
|
149
|
+
* segment a user in your email campaign tool
|
150
|
+
|
151
|
+
Compatibility
|
152
|
+
-------------
|
153
|
+
|
154
|
+
This gem can be used without Rails, but when used with Rails it depends on Rails 3 (we've tested with Rails 3.1 and 3.2). If you want to use girl_friday, you must use Ruby 1.9.2 or greater, JRuby, or Rubinius.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module RulesIOExtension
|
3
|
+
def save(*)
|
4
|
+
result = super
|
5
|
+
send_invalid_model_event if result == false
|
6
|
+
result
|
7
|
+
end
|
8
|
+
|
9
|
+
def save!(*)
|
10
|
+
begin
|
11
|
+
super
|
12
|
+
rescue ::ActiveRecord::RecordNotSaved
|
13
|
+
send_invalid_model_event
|
14
|
+
raise ::ActiveRecord::RecordNotSaved
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def send_invalid_model_event
|
20
|
+
event = {
|
21
|
+
:_domain => 'invalid_model',
|
22
|
+
:_name => self.class.name,
|
23
|
+
:attributes => self.attributes,
|
24
|
+
:errors => self.errors.full_messages.to_sentence
|
25
|
+
}
|
26
|
+
RulesIO.send_event event
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# With inspiration from
|
2
|
+
# https://github.com/smartinez87/exception_notification
|
3
|
+
# http://sharagoz.com/posts/1-rolling-your-own-exception-handler-in-rails-3
|
4
|
+
|
5
|
+
require 'action_dispatch'
|
6
|
+
|
7
|
+
module RulesIO
|
8
|
+
class Exceptions
|
9
|
+
include RulesIO::Helpers
|
10
|
+
|
11
|
+
def self.default_ignored_exceptions
|
12
|
+
[].tap do |exceptions|
|
13
|
+
exceptions << 'ActiveRecord::RecordNotFound'
|
14
|
+
exceptions << 'AbstractController::ActionNotFound'
|
15
|
+
exceptions << 'ActionController::RoutingError'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(app, options={})
|
20
|
+
@app, @options = app, options
|
21
|
+
@options[:ignore_exceptions] ||= self.class.default_ignored_exceptions
|
22
|
+
@options[:ignore_crawlers] ||= RulesIO.default_ignored_crawlers
|
23
|
+
@options[:ignore_if] ||= lambda { |env, e| false }
|
24
|
+
@options[:token] ||= RulesIO.token
|
25
|
+
@options[:custom_data] ||= lambda { |env| {} }
|
26
|
+
end
|
27
|
+
|
28
|
+
def call(env)
|
29
|
+
begin
|
30
|
+
@app.call(env)
|
31
|
+
rescue Exception => exception
|
32
|
+
env['rulesio.exception'] = exception
|
33
|
+
send_event_now event(env, exception), @options[:token], env unless should_be_ignored(env, exception)
|
34
|
+
raise exception
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def send_event_now(event, token, env)
|
40
|
+
prep = RulesIO.prepare_event(event, env)
|
41
|
+
RulesIO.post_payload_to_token prep, token
|
42
|
+
end
|
43
|
+
|
44
|
+
def should_be_ignored(env, exception)
|
45
|
+
ignored_exception(@options[:ignore_exceptions], exception) ||
|
46
|
+
from_crawler(@options[:ignore_crawlers], env['HTTP_USER_AGENT']) ||
|
47
|
+
conditionally_ignored(@options[:ignore_if], env, exception)
|
48
|
+
end
|
49
|
+
|
50
|
+
def ignored_exception(ignore_array, exception)
|
51
|
+
Array.wrap(ignore_array).map(&:to_s).include?(exception.class.name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def conditionally_ignored(ignore_proc, env, exception)
|
55
|
+
ignore_proc.call(env, exception)
|
56
|
+
rescue Exception => ex
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def event(env, exception)
|
61
|
+
backtrace = clean_backtrace(exception)
|
62
|
+
event = {
|
63
|
+
:_actor => actor_for_exception(exception),
|
64
|
+
:_from => '',
|
65
|
+
:_domain => exception.class.to_s,
|
66
|
+
:_name => fileline(exception),
|
67
|
+
:_message => exception.to_s,
|
68
|
+
:exception => exception.class.to_s,
|
69
|
+
:file => fileline(exception),
|
70
|
+
:backtrace => backtrace.join("\n")
|
71
|
+
}.with_indifferent_access
|
72
|
+
useractor = RulesIO.current_actor(env)
|
73
|
+
event[:_xactor] = useractor if useractor
|
74
|
+
event.merge!(@options[:custom_data].call(env))
|
75
|
+
event
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'girl_friday'
|
3
|
+
|
4
|
+
module RulesIO
|
5
|
+
class GirlFridayQueue < GirlFriday::WorkQueue
|
6
|
+
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super(:rulesio, {:size => 1}.merge(RulesIO.queue_options)) do |msg|
|
11
|
+
retries = 0
|
12
|
+
begin
|
13
|
+
RulesIO.post_payload_to_token msg[:payload], RulesIO.token
|
14
|
+
rescue Exception => e
|
15
|
+
if (retries += 1) % 6 == 5
|
16
|
+
RulesIO.logger.warn "RulesIO having trouble sending events; #{retries} attempts so far."
|
17
|
+
end
|
18
|
+
sleep [5, retries].max
|
19
|
+
retry
|
20
|
+
end
|
21
|
+
RulesIO.logger.warn "RulesIO resuming service after #{retries} retries." unless retries == 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.push *args
|
26
|
+
instance.push *args
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.status
|
30
|
+
instance.status
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RulesIO
|
2
|
+
module Helpers
|
3
|
+
def from_crawler(ignore_array, agent)
|
4
|
+
ignore_array.each do |crawler|
|
5
|
+
return true if (agent =~ /\b(#{crawler})\b/i)
|
6
|
+
end unless ignore_array.blank?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def clean_backtrace(exception)
|
11
|
+
if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
|
12
|
+
Rails.backtrace_cleaner.send(:filter, exception.backtrace)
|
13
|
+
else
|
14
|
+
exception.backtrace
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def fileline(exception)
|
19
|
+
fl = clean_backtrace(exception).first.match(/^(.*:.*):/)[1] rescue @app.to_s
|
20
|
+
fl.gsub(/ \((.*)\) /, '-\1-')
|
21
|
+
end
|
22
|
+
|
23
|
+
def actor_for_exception(exception)
|
24
|
+
"#{exception.class.to_s}:#{fileline(exception)}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
module RulesIO
|
5
|
+
class RailsConfigurator
|
6
|
+
attr_accessor :token, :webhook_url, :middlewares, :queue, :controller_data, :queue_options
|
7
|
+
def initialize
|
8
|
+
@webhook_url = 'http://www.rules.io/events/'
|
9
|
+
@middlewares = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def token(token)
|
13
|
+
@token = token
|
14
|
+
end
|
15
|
+
|
16
|
+
def webhook_url(webhook_url)
|
17
|
+
@webhook_url = webhook_url
|
18
|
+
end
|
19
|
+
|
20
|
+
def middleware(middleware, &block)
|
21
|
+
@middlewares[middleware] = MiddlewareConfigurator.apply(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def queue(queue, options)
|
25
|
+
@queue = queue
|
26
|
+
@queue_options = options
|
27
|
+
end
|
28
|
+
|
29
|
+
def controller_data(data)
|
30
|
+
@controller_data = data
|
31
|
+
end
|
32
|
+
|
33
|
+
def girl_friday_options(options)
|
34
|
+
@girl_friday_options = options
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class MiddlewareConfigurator
|
39
|
+
attr_accessor :configuration
|
40
|
+
|
41
|
+
def self.apply(&block)
|
42
|
+
x = new
|
43
|
+
x.configure(&block) if block_given?
|
44
|
+
x
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@configuration = {}
|
49
|
+
end
|
50
|
+
|
51
|
+
def configure(&block)
|
52
|
+
instance_eval &block
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(mid, *args, &block)
|
56
|
+
mname = mid.id2name
|
57
|
+
if block_given?
|
58
|
+
@configuration[mname.to_sym] = *block
|
59
|
+
else
|
60
|
+
if args.size == 1
|
61
|
+
@configuration[mname.to_sym] = args.first
|
62
|
+
else
|
63
|
+
@configuration[mname.to_sym] = args
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Railtie < Rails::Railtie
|
70
|
+
initializer :rulesio do |app|
|
71
|
+
filename = Rails.root.join('config/rulesio.rb')
|
72
|
+
if File.exists?(filename)
|
73
|
+
RulesIO::RailsConfigurator.new.instance_eval do
|
74
|
+
eval IO.read(filename), binding, filename.to_s, 1
|
75
|
+
if defined?(::Rails.configuration) && ::Rails.configuration.respond_to?(:middleware)
|
76
|
+
::Rails.configuration.middleware.insert_after 'ActionDispatch::Static', 'RulesIO::Rack',
|
77
|
+
:webhook_url => @webhook_url,
|
78
|
+
:token => @token,
|
79
|
+
:queue => @queue,
|
80
|
+
:queue_options => @queue_options,
|
81
|
+
:controller_data => @controller_data
|
82
|
+
::Rails.configuration.middleware.use('RulesIO::Users', @middlewares[:users].configuration) if @middlewares.has_key?(:users)
|
83
|
+
::Rails.configuration.middleware.use('RulesIO::Users', @middlewares[:pageviews].configuration) if @middlewares.has_key?(:pageviews)
|
84
|
+
::Rails.configuration.middleware.use('RulesIO::Exceptions', @middlewares[:exceptions].configuration) if @middlewares.has_key?(:exceptions)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
config.after_initialize do
|
91
|
+
ActiveSupport.on_load(:active_record) do
|
92
|
+
require 'rulesio/active_record_extension'
|
93
|
+
include ActiveRecord::RulesIOExtension
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# With inspiration from
|
2
|
+
# https://github.com/smartinez87/exception_notification
|
3
|
+
# http://sharagoz.com/posts/1-rolling-your-own-exception-handler-in-rails-3
|
4
|
+
|
5
|
+
require 'action_dispatch'
|
6
|
+
|
7
|
+
module RulesIO
|
8
|
+
class Users
|
9
|
+
include RulesIO::Helpers
|
10
|
+
|
11
|
+
def initialize(app, options={})
|
12
|
+
@app, @options = app, options
|
13
|
+
@options[:ignore_crawlers] ||= RulesIO.default_ignored_crawlers
|
14
|
+
@options[:ignore_if] ||= lambda { |env| false }
|
15
|
+
@options[:ignore_if_controller] ||= 'false'
|
16
|
+
@options[:custom_data] ||= lambda { |env| {} }
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
before = Time.now
|
21
|
+
status, headers, response = @app.call(env)
|
22
|
+
[status, headers, response]
|
23
|
+
rescue Exception => e
|
24
|
+
status = 500
|
25
|
+
raise e
|
26
|
+
ensure
|
27
|
+
after = Time.now
|
28
|
+
RulesIO.send_event event(env, status, after - before) unless should_be_ignored(env)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def rails_asset_request?(env)
|
33
|
+
defined?(Rails) && env['action_controller.instance'].nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def should_be_ignored(env)
|
37
|
+
rails_asset_request?(env) ||
|
38
|
+
from_crawler(@options[:ignore_crawlers], env['HTTP_USER_AGENT']) ||
|
39
|
+
conditionally_ignored(@options[:ignore_if], env) ||
|
40
|
+
conditionally_ignored_controller(@options[:ignore_if_controller], env)
|
41
|
+
end
|
42
|
+
|
43
|
+
def conditionally_ignored_controller(condition, env)
|
44
|
+
controller = env['action_controller.instance']
|
45
|
+
controller.instance_eval condition
|
46
|
+
end
|
47
|
+
|
48
|
+
def conditionally_ignored(ignore_proc, env)
|
49
|
+
ignore_proc.call(env)
|
50
|
+
end
|
51
|
+
|
52
|
+
def event(env, status, duration)
|
53
|
+
event = {
|
54
|
+
:_domain => (status.to_i >= 400) ? 'pageerror' : 'pageview',
|
55
|
+
:status => status,
|
56
|
+
:duration => "%.2f" % (duration * 1000)
|
57
|
+
}
|
58
|
+
if exception = env['rulesio.exception']
|
59
|
+
event[:_xactor] = actor_for_exception(exception)
|
60
|
+
event[:_message] = exception.to_s
|
61
|
+
end
|
62
|
+
event.merge!(@options[:custom_data].call(env))
|
63
|
+
event
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
data/lib/rulesio.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'rulesio/version'
|
2
|
+
require 'rulesio/helpers'
|
3
|
+
require 'rulesio/exceptions'
|
4
|
+
require 'rulesio/users'
|
5
|
+
require 'rulesio/girl_friday_queue'
|
6
|
+
require 'rulesio/memory_queue'
|
7
|
+
require 'net/http'
|
8
|
+
require 'uri'
|
9
|
+
require 'logger'
|
10
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
11
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
12
|
+
|
13
|
+
module RulesIO
|
14
|
+
mattr_accessor :filter_parameters, :buffer, :token, :webhook_url, :queue, :queue_options, :controller_data, :logger
|
15
|
+
|
16
|
+
def self.default_ignored_crawlers
|
17
|
+
%w(Baidu Gigabot Googlebot libwww-perl lwp-trivial msnbot SiteUptime Slurp WordPress ZIBB ZyBorg Yandex Jyxobot Huaweisymantecspider ApptusBot NewRelicPinger)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.send_event(event)
|
21
|
+
buffer << event
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.flush(env={})
|
25
|
+
return if (events = RulesIO.buffer).empty?
|
26
|
+
RulesIO.buffer = []
|
27
|
+
RulesIO.queue.push(:payload => events.map {|event| RulesIO.prepare_event(event, env)})
|
28
|
+
# RulesIO.post_payload_to_token events.to_json, RulesIO.token
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.post_payload_to_token(payload, token)
|
32
|
+
uri = URI(RulesIO.webhook_url + token)
|
33
|
+
req = Net::HTTP::Post.new(uri.path)
|
34
|
+
req.body = payload.to_json
|
35
|
+
req.content_type = 'application/json'
|
36
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
37
|
+
http.request(req)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.current_user(env)
|
42
|
+
if controller = env['action_controller.instance']
|
43
|
+
controller.instance_variable_get('@current_user') || controller.instance_eval('current_user')
|
44
|
+
end
|
45
|
+
rescue
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.current_actor(env)
|
50
|
+
if controller = env['action_controller.instance']
|
51
|
+
begin
|
52
|
+
data = controller.instance_eval(RulesIO.controller_data)
|
53
|
+
data = data.with_indifferent_access
|
54
|
+
return data[:_actor] if data[:_actor]
|
55
|
+
rescue
|
56
|
+
end
|
57
|
+
|
58
|
+
user = controller.instance_variable_get('@current_user') || controller.instance_eval('current_user')
|
59
|
+
[:to_param, :id].each do |method|
|
60
|
+
return user.send(method) if user && user.respond_to?(method)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def self.page_event_name(request, params)
|
68
|
+
if params && params['controller']
|
69
|
+
"#{params['controller']}##{params['action']}"
|
70
|
+
else
|
71
|
+
request.path.gsub('/', '-')[1..-1]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.prepare_event(event, env)
|
76
|
+
event = event.with_indifferent_access
|
77
|
+
|
78
|
+
if controller = env['action_controller.instance']
|
79
|
+
begin
|
80
|
+
data = controller.instance_eval(RulesIO.controller_data).with_indifferent_access
|
81
|
+
event = data.merge(event)
|
82
|
+
rescue
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
current_user = current_user(env)
|
87
|
+
actor = current_actor(env)
|
88
|
+
|
89
|
+
event[:_actor] = actor || 'anonymous' unless event[:_actor].present?
|
90
|
+
event[:_timestamp] ||= Time.now.to_f
|
91
|
+
event[:rails_env] = Rails.env if defined?(Rails)
|
92
|
+
|
93
|
+
unless env.empty?
|
94
|
+
env['rack.input'].rewind
|
95
|
+
request = defined?(Rails) ? ActionDispatch::Request.new(env) : ::Rack::Request.new(env)
|
96
|
+
params = request.params
|
97
|
+
action = page_event_name(request, params)
|
98
|
+
|
99
|
+
event[:_name] ||= action
|
100
|
+
event[:_from] ||= current_user.email if current_user && current_user.respond_to?(:email) && current_user.email != event[:_actor]
|
101
|
+
event[:action] = action
|
102
|
+
event[:request_url] = env['rulesio.request_url']
|
103
|
+
event[:request_method] = request.request_method
|
104
|
+
event[:user_agent] = request.user_agent
|
105
|
+
event[:referer_url] = request.referer
|
106
|
+
event[:params] = params.except(*RulesIO.filter_parameters)
|
107
|
+
event[:session] = request.session
|
108
|
+
end
|
109
|
+
|
110
|
+
event.reject! {|k, v| v.to_s.blank?}
|
111
|
+
event
|
112
|
+
end
|
113
|
+
|
114
|
+
class Rack
|
115
|
+
def initialize(app, options={})
|
116
|
+
@app = app
|
117
|
+
RulesIO.logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
118
|
+
RulesIO.webhook_url = options[:webhook_url] || 'http://www.rules.io/events/'
|
119
|
+
RulesIO.buffer = []
|
120
|
+
RulesIO.filter_parameters = defined?(Rails) ? Rails.application.config.filter_parameters : []
|
121
|
+
RulesIO.token = options[:token]
|
122
|
+
RulesIO.queue = options[:queue] || RulesIO::MemoryQueue
|
123
|
+
RulesIO.queue_options = options[:queue_options] || {}
|
124
|
+
RulesIO.controller_data = options[:controller_data] || '{}'
|
125
|
+
end
|
126
|
+
|
127
|
+
def call(env)
|
128
|
+
RulesIO.buffer = []
|
129
|
+
env['rulesio.request_url'] = ::Rack::Request.new(env).url
|
130
|
+
@app.call(env)
|
131
|
+
ensure
|
132
|
+
RulesIO.flush(env)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
require 'rulesio/railtie' if defined?(Rails)
|
data/rulesio.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'rulesio/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'rulesio'
|
7
|
+
s.version = RulesIO::VERSION
|
8
|
+
s.authors = ['David Anderson', 'Chris Weis']
|
9
|
+
s.email = ['david@alpinegizmo.com']
|
10
|
+
s.homepage = 'https://github.com/rulesio/rulesio'
|
11
|
+
s.summary = %q{Rack middleware for connecting to rules.io}
|
12
|
+
s.description = %q{Rack middleware for connecting Rack applications to rules.io, with extensions for Rails 3 applications.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ['lib']
|
18
|
+
|
19
|
+
s.add_runtime_dependency 'activesupport'
|
20
|
+
s.add_runtime_dependency 'actionpack'
|
21
|
+
s.add_runtime_dependency 'girl_friday', '~> 0.10.0'
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rulesio
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 9
|
8
|
+
- 2
|
9
|
+
version: 0.9.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- David Anderson
|
13
|
+
- Chris Weis
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-08-28 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activesupport
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: actionpack
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 0
|
42
|
+
version: "0"
|
43
|
+
type: :runtime
|
44
|
+
version_requirements: *id002
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: girl_friday
|
47
|
+
prerelease: false
|
48
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ~>
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
- 10
|
55
|
+
- 0
|
56
|
+
version: 0.10.0
|
57
|
+
type: :runtime
|
58
|
+
version_requirements: *id003
|
59
|
+
description: Rack middleware for connecting Rack applications to rules.io, with extensions for Rails 3 applications.
|
60
|
+
email:
|
61
|
+
- david@alpinegizmo.com
|
62
|
+
executables: []
|
63
|
+
|
64
|
+
extensions: []
|
65
|
+
|
66
|
+
extra_rdoc_files: []
|
67
|
+
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- Gemfile
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- lib/rulesio.rb
|
74
|
+
- lib/rulesio/active_record_extension.rb
|
75
|
+
- lib/rulesio/exceptions.rb
|
76
|
+
- lib/rulesio/girl_friday_queue.rb
|
77
|
+
- lib/rulesio/helpers.rb
|
78
|
+
- lib/rulesio/memory_queue.rb
|
79
|
+
- lib/rulesio/railtie.rb
|
80
|
+
- lib/rulesio/users.rb
|
81
|
+
- lib/rulesio/version.rb
|
82
|
+
- rulesio.gemspec
|
83
|
+
has_rdoc: true
|
84
|
+
homepage: https://github.com/rulesio/rulesio
|
85
|
+
licenses: []
|
86
|
+
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
segments:
|
97
|
+
- 0
|
98
|
+
version: "0"
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
requirements: []
|
107
|
+
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.3.6
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Rack middleware for connecting to rules.io
|
113
|
+
test_files: []
|
114
|
+
|