rails_customerbeats 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. data/CHANGELOG.rdoc +3 -0
  2. data/Gemfile +10 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +2 -0
  5. data/Rakefile +58 -0
  6. data/TODO.rdoc +1 -0
  7. data/app/controllers/rails_customerbeats_controller.rb +82 -0
  8. data/app/helpers/rails_customerbeats_helper.rb +164 -0
  9. data/app/views/layouts/rails_customerbeats.html.erb +21 -0
  10. data/app/views/rails_customerbeats/_request.html.erb +21 -0
  11. data/app/views/rails_customerbeats/_row.html.erb +28 -0
  12. data/app/views/rails_customerbeats/all.html.erb +26 -0
  13. data/app/views/rails_customerbeats/chart.html.erb +49 -0
  14. data/app/views/rails_customerbeats/index.html.erb +21 -0
  15. data/app/views/rails_customerbeats/show.html.erb +41 -0
  16. data/config/routes.rb +10 -0
  17. data/lib/generators/rails_customerbeats_generator.rb +33 -0
  18. data/lib/rails_customerbeats/async_consumer.rb +54 -0
  19. data/lib/rails_customerbeats/engine.rb +43 -0
  20. data/lib/rails_customerbeats/middleware.rb +27 -0
  21. data/lib/rails_customerbeats/orm/active_record.rb +79 -0
  22. data/lib/rails_customerbeats/orm/data_mapper.rb +88 -0
  23. data/lib/rails_customerbeats/payload_parser.rb +134 -0
  24. data/lib/rails_customerbeats/store.rb +137 -0
  25. data/lib/rails_customerbeats/version.rb +3 -0
  26. data/lib/rails_customerbeats.rb +121 -0
  27. data/public/images/rails_customerbeats/arrow_down.png +0 -0
  28. data/public/images/rails_customerbeats/arrow_up.png +0 -0
  29. data/public/images/rails_customerbeats/cancel.png +0 -0
  30. data/public/images/rails_customerbeats/chart_pie.png +0 -0
  31. data/public/images/rails_customerbeats/page_white_delete.png +0 -0
  32. data/public/images/rails_customerbeats/page_white_go.png +0 -0
  33. data/public/images/rails_customerbeats/tick.png +0 -0
  34. data/public/javascripts/rails_customerbeats/g.pie-min.js +6 -0
  35. data/public/javascripts/rails_customerbeats/g.raphael-min.js +5 -0
  36. data/public/javascripts/rails_customerbeats/raphael-min.js +5 -0
  37. data/public/stylesheets/rails_customerbeats.css +135 -0
  38. data/test/dummy/app/controllers/application_controller.rb +4 -0
  39. data/test/dummy/app/controllers/users_controller.rb +43 -0
  40. data/test/dummy/app/helpers/application_helper.rb +2 -0
  41. data/test/dummy/app/models/metric.rb +3 -0
  42. data/test/dummy/app/models/notification.rb +7 -0
  43. data/test/dummy/app/models/user.rb +2 -0
  44. data/test/dummy/config/application.rb +52 -0
  45. data/test/dummy/config/boot.rb +9 -0
  46. data/test/dummy/config/environment.rb +5 -0
  47. data/test/dummy/config/environments/development.rb +19 -0
  48. data/test/dummy/config/environments/production.rb +33 -0
  49. data/test/dummy/config/environments/test.rb +31 -0
  50. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  51. data/test/dummy/config/initializers/cookie_verification_secret.rb +7 -0
  52. data/test/dummy/config/initializers/session_store.rb +8 -0
  53. data/test/dummy/config/routes.rb +60 -0
  54. data/test/dummy/db/migrate/20100106152343_create_metrics.rb +17 -0
  55. data/test/dummy/db/migrate/20100108120821_create_users.rb +13 -0
  56. data/test/integration/instrumentation_test.rb +100 -0
  57. data/test/integration/navigation_test.rb +103 -0
  58. data/test/orm/active_record_test.rb +47 -0
  59. data/test/payload_parser_test.rb +36 -0
  60. data/test/rails_customerbeats_test.rb +43 -0
  61. data/test/store_test.rb +81 -0
  62. data/test/support/helpers.rb +16 -0
  63. data/test/support/instrumentation.rb +18 -0
  64. data/test/support/mock_store.rb +34 -0
  65. data/test/support/webrat/integrations/rails.rb +31 -0
  66. data/test/test_helper.rb +25 -0
  67. metadata +142 -0
@@ -0,0 +1,43 @@
1
+ module RailsCustomerbeats
2
+ class Engine < Rails::Engine
3
+
4
+ # Initialize configure parameters
5
+ config.rails_customerbeats = ActiveSupport::OrderedOptions.new
6
+
7
+ config.rails_customerbeats.ignore_lambdas = {}
8
+ config.rails_customerbeats.ignore_patterns = [ "start_processing.action_controller",
9
+ "sql.active_record",
10
+ "!render_template.action_view",
11
+ "render_partial.action_view",
12
+ "render_template.action_view" ]
13
+
14
+ config.rails_customerbeats.ignore_lambdas['rack.request'] = lambda { |name, payload|
15
+ payload[:path] =~ /assets/
16
+ }
17
+
18
+ initializer "rails_customerbeats.add_middleware" do |app|
19
+ app.config.middleware.use RailsCustomerbeats::Middleware
20
+ end
21
+
22
+ initializer "static assets" do |app|
23
+ app.middleware.insert_before ::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public/assets"
24
+ end
25
+
26
+ initializer "rails_customerbeats.set_ignores" do |app|
27
+ RailsCustomerbeats.ignore_lambdas.merge!(app.config.rails_customerbeats.ignore_lambdas)
28
+ RailsCustomerbeats.ignore_patterns.concat(app.config.rails_customerbeats.ignore_patterns)
29
+ end
30
+
31
+ initializer "rails_customerbeats.set_store" do |app|
32
+ if app.config.rails_customerbeats.set_store
33
+ RailsCustomerbeats.set_store(&app.config.rails_customerbeats.set_store)
34
+ end
35
+ end
36
+
37
+ initializer "rails_customerbeats.start_subscriber" do
38
+ ActiveSupport::Notifications.subscribe /[^!]$/ do |*args|
39
+ RailsCustomerbeats.events.push(args) if RailsCustomerbeats.valid_for_storing?(args)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ module RailsCustomerbeats
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ if env["PATH_INFO"] =~ /^\/rails_customerbeats/
9
+ @app.call(env)
10
+ else
11
+ RailsCustomerbeats.listen_request do
12
+ response = notifications.instrument "rack.request",
13
+ :path => env["PATH_INFO"], :method => env["REQUEST_METHOD"],
14
+ :instrumenter_id => notifications.instrumenter.id do
15
+ @app.call(env)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ def notifications
24
+ ActiveSupport::Notifications
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,79 @@
1
+ # Setup to ignore any query which is not a SELECT, INSERT, UPDATE
2
+ # or DELETE and queries made by the own store.
3
+ RailsCustomerbeats.ignore :invalid_queries do |name, payload|
4
+ name == "active_record.sql" &&
5
+ payload[:sql] !~ /^(SELECT|INSERT|UPDATE|DELETE)/
6
+ end
7
+
8
+ module RailsCustomerbeats
9
+ module ORM
10
+ # Include in your model to store metrics. For ActiveRecord, you need the
11
+ # following setup:
12
+ #
13
+ # script/generate model Metric script/generate name:string duration:integer
14
+ # request_id:integer parent_id:integer payload:text started_at:datetime created_at:datetime --skip-timestamps
15
+ #
16
+ # You can use any model name you wish. Next, you need to include
17
+ # RailsCustomerbeats::ORM::ActiveRecord:
18
+ #
19
+ # class Metric < ActiveRecord::Base
20
+ # include RailsCustomerbeats::ORM::ActiveRecord
21
+ # end
22
+ #
23
+
24
+ ORM.primary_key_finder = :find
25
+ ORM.delete_all = :delete_all
26
+
27
+ ORM.metric_model_properties = %w[
28
+ name:string
29
+ duration:integer
30
+ request_id:integer
31
+ parent_id:integer
32
+ payload:text
33
+ started_at:datetime
34
+ created_at:datetime
35
+ ]
36
+
37
+ def self.add_metric_model_config(generator, file_name, class_name)
38
+ generator.inject_into_class "app/models/#{file_name}.rb", class_name, <<-CONTENT
39
+ include RailsCustomerbeats::ORM::#{Rails::Generators.options[:rails][:orm].to_s.camelize}
40
+ CONTENT
41
+ end
42
+
43
+ module ActiveRecord
44
+ extend ActiveSupport::Concern
45
+ include RailsCustomerbeats::Store
46
+
47
+ included do
48
+ # Create a new connection pool just for the given resource
49
+ establish_connection(Rails.env)
50
+
51
+ # Set required validations
52
+ validates_presence_of :name, :started_at, :duration
53
+
54
+ # Serialize payload data
55
+ serialize :payload
56
+
57
+ # Select scopes
58
+ scope :requests, where(:name => "rack.request")
59
+ scope :by_name, lambda { |name| where(:name => name) }
60
+ scope :by_request_id, lambda { |request_id| where(:request_id => request_id) }
61
+
62
+ # Order scopes
63
+ # We need to add the id in the earliest and latest scope since the database
64
+ # does not store miliseconds. The id then comes as second criteria, since
65
+ # the ones started first are first saved in the database.
66
+ scope :earliest, order("started_at ASC, id ASC")
67
+ scope :latest, order("started_at DESC, id DESC")
68
+ scope :slowest, order("duration DESC")
69
+ scope :fastest, order("duration ASC")
70
+ end
71
+
72
+ protected
73
+
74
+ def save_metric!
75
+ save!
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,88 @@
1
+ # Setup to ignore any query which is not a SELECT, INSERT, UPDATE
2
+ # or DELETE and queries made by the own store.
3
+ RailsCustomerbeats.ignore :invalid_queries do |name, payload|
4
+ name == "data_mapper.sql" &&
5
+ payload[:sql] !~ /^(SELECT|INSERT|UPDATE|DELETE)/
6
+ end
7
+
8
+ module RailsCustomerbeats
9
+ module ORM
10
+
11
+ # Include in your model to store metrics. For DataMapper, you need the
12
+ # following setup:
13
+ #
14
+ # script/generate model Metric script/generate name:string duration:integer
15
+ # request_id:integer parent_id:integer payload:object started_at:datetime created_at:datetime --skip-timestamps
16
+ #
17
+ # You can use any model name you wish. Next, you need to include
18
+ # RailsCustomerbeats::ORM::DataMapper:
19
+ #
20
+ # class Metric
21
+ # include DataMapper::Resource
22
+ # include RailsCustomerbeats::ORM::DataMapper
23
+ # end
24
+ #
25
+
26
+ ORM.primary_key_finder = :get
27
+ ORM.delete_all = :destroy! # use bang version here cause we don't need no hooks
28
+
29
+ ORM.metric_model_properties = %w[
30
+ name:string
31
+ duration:integer
32
+ request_id:integer
33
+ parent_id:integer
34
+ payload:object
35
+ started_at:datetime
36
+ created_at:datetime
37
+ ]
38
+
39
+ def self.add_metric_model_config(generator, file_name, class_name)
40
+ generator.inject_into_file "app/models/#{file_name}.rb",
41
+ " include RailsCustomerbeats::ORM::DataMapper\n",
42
+ {:after => " include DataMapper::Resource\n"}
43
+ end
44
+
45
+ module DataMapper
46
+ extend ActiveSupport::Concern
47
+ include RailsCustomerbeats::Store
48
+
49
+ included do
50
+ # Set required validations
51
+ validates_presence_of :name, :started_at, :duration
52
+ end
53
+
54
+ module ClassMethods
55
+
56
+ # Select scopes
57
+
58
+ def requests; all(:name => 'rack.request') end
59
+ def by_name(name); all(:name => name ) end
60
+ def by_request_id(request_id); all(:request_id => request_id ) end
61
+
62
+ # Order scopes
63
+ # We need to add the id in the earliest and latest scope since the database
64
+ # does not store miliseconds. The id then comes as second criteria, since
65
+ # the ones started first are first saved in the database.
66
+
67
+ def earliest; all(:order => [:started_at.asc, :id.asc ]) end
68
+ def latest; all(:order => [:started_at.desc, :id.desc]) end
69
+ def slowest; all(:order => [:duration.desc ]) end
70
+ def fastest; all(:order => [:duration.asc ]) end
71
+
72
+ end
73
+
74
+ # Destroy all children if it's a request metric.
75
+ def destroy
76
+ self.class.by_request_id(self.id).destroy! if rack_request?
77
+ super
78
+ end
79
+
80
+ protected
81
+
82
+ def save_metric!
83
+ save!
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,134 @@
1
+ module RailsCustomerbeats
2
+ # ActiveSupport::Notifications usually comes with extra information as the
3
+ # SQL query, response status and many others. This information is called payload.
4
+ #
5
+ # By default, RailsCustomerbeats stores the whole payload in the database but it allows
6
+ # you to manipulate it or even ignore some through the add and ignore methods.
7
+ #
8
+ # For example, "activerecord.sql" has as paylaod a hash with :name (like "Product
9
+ # Load"), the :sql to be performed and the :connection_id. We can remove the connection
10
+ # from the hash by simply providing :except:
11
+ #
12
+ # RailsCustomerbeats::PayloadParser.add "active_record.sql", :except => :name
13
+ #
14
+ # Or, we could also:
15
+ #
16
+ # RailsCustomerbeats::PayloadParser.add "active_record.sql", :slice => [:name, :sql]
17
+ #
18
+ # Finally, in some cases manipulating the hash is not enough and you might need
19
+ # to customize it further, as in "action_view.render_template". You can do
20
+ # that by giving a block which will receive the payload as argument:
21
+ #
22
+ # RailsCustomerbeats::PayloadParser.add "action_view.render_template" do |payload|
23
+ # payload = payload.dup
24
+ # payload[:template] = payload[:template].gsub("RAILS_ROOT", Rails.root)
25
+ # payload
26
+ # end
27
+ #
28
+ # ATTENTION: if you need to modify the payload or any of its values, be sure to
29
+ # .dup if first, as in the example above.
30
+ #
31
+ # If you want to ignore any payload, you can use the ignore method:
32
+ #
33
+ # RailsCustomerbeats::PayloadParser.ignore "active_record.sql"
34
+ #
35
+ module PayloadParser
36
+ # Holds the parsers used by RailsCustomerbeats.
37
+ def self.parsers
38
+ @parsers ||= {}
39
+ end
40
+
41
+ # Holds the mapped paths used in prunning.
42
+ def self.mapped_paths
43
+ @mapped_paths ||= {}
44
+ end
45
+
46
+ # Add a new parser.
47
+ def self.add(*names, &block)
48
+ options = names.extract_options!
49
+
50
+ names.each do |name|
51
+ parsers[name.to_s] = if block_given?
52
+ block
53
+ elsif options.present?
54
+ options.to_a.flatten
55
+ else
56
+ true
57
+ end
58
+ end
59
+ end
60
+
61
+ # Delete a previous parser
62
+ def self.ignore(*names)
63
+ names.each { |name| parsers[name.to_s] = false }
64
+ end
65
+
66
+ # Filter the given payload based on the name given and configured parsers
67
+ def self.filter(name, payload)
68
+ parser = parsers[name]
69
+ case parser
70
+ when Array
71
+ payload.send(*parser)
72
+ when Proc
73
+ parser.call(payload)
74
+ when TrueClass, NilClass
75
+ payload
76
+ when FalseClass
77
+ nil
78
+ end
79
+ end
80
+
81
+ # Prune paths based on the mapped paths set.
82
+ def self.prune_path(raw_path)
83
+ mapped_paths.each do |path, replacement|
84
+ raw_path = raw_path.gsub(path, replacement)
85
+ end
86
+ raw_path
87
+ end
88
+
89
+ # Make Rails.root appear as APP in pruned paths.
90
+ mapped_paths[Rails.root.to_s] = "RAILS_ROOT"
91
+
92
+ # Make Gem paths appear as GEM in pruned paths.
93
+ Gem.path.each do |path|
94
+ mapped_paths[File.join(path, "gems")] = "GEMS_ROOT"
95
+ end if defined?(Gem)
96
+
97
+ # ActiveRecord
98
+ add "sql.active_record" do |payload|
99
+ payload = payload.dup
100
+ payload[:sql] = payload[:sql].squeeze(" ")
101
+ payload.delete(:connection_id)
102
+ payload
103
+ end
104
+
105
+ # ActionController - process action
106
+ add "process_action.action_controller" do |payload|
107
+ payload = payload.dup
108
+ #payload = payload.except(:path, :method, :params, :db_runtime, :view_runtime)
109
+ payload[:end_point] = "#{payload.delete(:controller)}##{payload.delete(:action)}"
110
+ #new_data = { 'session_id' => session.session_id , 'browser' => request.env['HTTP_USER_AGENT'] , 'ip_address' => request.env['REMOTE_ADDR'] }
111
+ #payload.merge!(new_data)
112
+ payload
113
+ end
114
+
115
+ # ActionView
116
+ add "render_template.action_view", "render_partial.action_view",
117
+ "render_collection.action_view" do |payload|
118
+ returning Hash.new do |new_payload|
119
+ payload.each do |key, value|
120
+ case value
121
+ when NilClass
122
+ when String
123
+ new_payload[key] = prune_path(value)
124
+ else
125
+ new_payload[key] = value
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ # ActionMailer
132
+ add "action_mailer.deliver", "action_mailer.receive", :except => :mail
133
+ end
134
+ end
@@ -0,0 +1,137 @@
1
+ module RailsCustomerbeats
2
+ # This module contains the default API for storing notifications.
3
+ # Imagine that you configure your store to be the Metric class:
4
+ #
5
+ # RailsCustomerbeats.set_store { Metric }
6
+ #
7
+ # Whenever a notification comes, RailsCustomerbeats instantiates a new
8
+ # store and call configure on it with the instrumentation arguments:
9
+ #
10
+ # metric = Metric.new
11
+ # metric.configure(args)
12
+ # metric
13
+ #
14
+ # After all metrics are configured they are nested and save_metrics! is called,
15
+ # where each metric saves itself and its children.
16
+ #
17
+ # The method save_metrics! is implemented below and it requires the method
18
+ # save_metric! to be implemented in the target class.
19
+ #
20
+ module Store
21
+ VALID_ORDERS = %w(earliest latest slowest fastest).freeze
22
+ extend ActiveSupport::Concern
23
+
24
+ module ClassMethods
25
+ def mount_tree(metrics)
26
+ while metric = metrics.shift
27
+ if parent = metrics.find { |n| n.parent_of?(metric) }
28
+ parent.children << metric
29
+ elsif metrics.empty?
30
+ return metric if metric.rack_request?
31
+ raise %(Expected tree root to be a "rack.request", got #{metric.name.inspect})
32
+ end
33
+ end
34
+ end
35
+
36
+ def events_to_metrics_tree(events)
37
+ verify_active_connections! if respond_to?(:verify_active_connections!)
38
+
39
+ metrics = events.map do |event|
40
+ metric = new
41
+ metric.configure(event)
42
+ metric
43
+ end
44
+
45
+ mount_tree(metrics)
46
+ end
47
+ end
48
+
49
+ # Configure the current metric by setting the values yielded by
50
+ # the instrumentation event.
51
+ def configure(args)
52
+ self.payload = RailsCustomerbeats::PayloadParser.filter(args[0].to_s, args[4])
53
+ self.name = args[0].to_s
54
+ self.started_at = args[1]
55
+ self.duration = normalized_duration(self.payload, args)
56
+ end
57
+
58
+ def duration_in_us
59
+ self.duration
60
+ end
61
+
62
+ def duration_in_ms
63
+ self.duration * 0.001
64
+ end
65
+
66
+ def exclusive_duration
67
+ @exclusive_duration ||= self.duration - children.sum(&:duration)
68
+ end
69
+
70
+ def exclusive_duration_in_us
71
+ self.exclusive_duration
72
+ end
73
+
74
+ def exclusive_duration_in_ms
75
+ self.exclusive_duration * 0.001
76
+ end
77
+
78
+ # Stores the children of this metric when a tree is created.
79
+ def children
80
+ @children ||= []
81
+ end
82
+
83
+ def rack_request?
84
+ self.name == "rack.request"
85
+ end
86
+
87
+ # Returns if the current node is the parent of the given node.
88
+ # If this is a new record, we can use started_at values to detect parenting.
89
+ # However, if it was already saved, we lose microseconds information from
90
+ # timestamps and we must rely solely in id and parent_id information.
91
+ def parent_of?(node)
92
+ if !persisted?
93
+ start = (self.started_at - node.started_at) * 1000000
94
+ start <= 0 && (start + self.duration >= node.duration)
95
+ else
96
+ self.id == node.parent_id
97
+ end
98
+ end
99
+
100
+ def child_of?(node)
101
+ node.parent_of?(self)
102
+ end
103
+
104
+ # Save the current metric and all of its children by properly setting
105
+ # the request and parent ids.
106
+ def save_metrics!(request_id=nil, parent_id=nil)
107
+ self.request_id, self.parent_id = request_id, parent_id
108
+ save_metric!
109
+
110
+ children.each do |child|
111
+ child.save_metrics!(request_id || self.id, self.id)
112
+ end
113
+
114
+ unless self.request_id
115
+ self.request_id ||= self.id
116
+ save_metric!
117
+ end
118
+ end
119
+
120
+ # Destroy all children if it's a request metric.
121
+ def destroy
122
+ self.class.by_request_id(self.id).delete_all if rack_request?
123
+ super
124
+ end
125
+
126
+ protected
127
+
128
+ def save_metric!
129
+ raise NotImplementedError
130
+ end
131
+
132
+ def normalized_duration(payload, args)
133
+ payload[:duration] ? payload[:duration] : (args[2] - args[1]) * 1000000
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,3 @@
1
+ module RailsCustomerbeats
2
+ VERSION = "0.0.4"
3
+ end
@@ -0,0 +1,121 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'active_support/core_ext/class/attribute'
3
+
4
+ Thread.abort_on_exception = Rails.env.development? || Rails.env.test?
5
+
6
+ module RailsCustomerbeats
7
+ autoload :AsyncConsumer, 'rails_customerbeats/async_consumer'
8
+ autoload :Middleware, 'rails_customerbeats/middleware'
9
+ autoload :PayloadParser, 'rails_customerbeats/payload_parser'
10
+ autoload :Store, 'rails_customerbeats/store'
11
+ autoload :VERSION, 'rails_customerbeats/version'
12
+ autoload :VoidInstrumenter, 'rails_customerbeats/async_consumer'
13
+
14
+ module ORM
15
+ autoload :ActiveRecord, 'rails_customerbeats/orm/active_record'
16
+ autoload :DataMapper, 'rails_customerbeats/orm/data_mapper'
17
+
18
+ class << self
19
+ class_attribute :primary_key_finder
20
+ class_attribute :delete_all
21
+ class_attribute :metric_model_properties
22
+ end
23
+ end
24
+
25
+ # Set which store to use in RailsCustomerbeats.
26
+ #
27
+ # RailsCustomerbeats.set_store { Metric }
28
+ #
29
+ def self.set_store(&block)
30
+ singleton_class.send :define_method, :store, &block
31
+ end
32
+
33
+ # Place holder for the store.
34
+ def self.store; end
35
+
36
+ # Holds the events for a specific thread.
37
+ def self.events
38
+ Thread.current[:rails_customerbeats_events] ||= []
39
+ end
40
+
41
+ # Turn RailsCustomerbeats on, i.e. make it listen to notifications during the block.
42
+ # At the end, it pushes notifications to the async consumer.
43
+ def self.listen_request
44
+ events = RailsCustomerbeats.events
45
+ events.clear
46
+
47
+ Thread.current[:rails_customerbeats_listening] = true
48
+ result = yield
49
+
50
+ RailsCustomerbeats.async_consumer.push(events.dup)
51
+ result
52
+ ensure
53
+ Thread.current[:rails_customerbeats_listening] = false
54
+ RailsCustomerbeats.events.clear
55
+ end
56
+
57
+ # Returns if events are being registered or not.
58
+ def self.listening?
59
+ Thread.current[:rails_customerbeats_listening] || false
60
+ end
61
+
62
+ # Allow you to specify a condition to ignore a notification based
63
+ # on its name and/or payload. For example, if you want to ignore
64
+ # all notifications with empty payload, one can do:
65
+ #
66
+ # RailsCustomerbeats.ignore :with_empty_payload do |name, payload|
67
+ # payload.empty?
68
+ # end
69
+ #
70
+ # However, if you want to ignore something based solely on its
71
+ # name, you can use ignore_patterns instead:
72
+ #
73
+ # RailsCustomerbeats.ignore_patterns << /^some_noise_plugin/
74
+ #
75
+ def self.ignore(name, &block)
76
+ raise ArgumentError, "ignore expects a block" unless block_given?
77
+ ignore_lambdas[name] = block
78
+ end
79
+
80
+ # Stores the blocks given to ignore with their respective identifier in a hash.
81
+ def self.ignore_lambdas
82
+ @@ignore_lambdas ||= {}
83
+ end
84
+
85
+ # Stores ignore patterns that can be given as strings or regexps.
86
+ def self.ignore_patterns
87
+ @@ignore_patterns ||= []
88
+ end
89
+
90
+ # Holds the queue which store stuff in the database.
91
+ def self.async_consumer
92
+ @@async_consumer ||= AsyncConsumer.new do |events|
93
+ next if events.empty?
94
+ root = RailsCustomerbeats.store.events_to_metrics_tree(events)
95
+ root.save_metrics!
96
+ end
97
+ end
98
+
99
+ # Wait until the async queue is consumed.
100
+ def self.wait
101
+ sleep(0.01) until async_consumer.finished?
102
+ end
103
+
104
+ # A notification is valid for storing if two conditions are met:
105
+ #
106
+ # 1) The instrumenter id which created the notification is not the same
107
+ # instrumenter id of this thread. This means that notifications generated
108
+ # inside this thread are stored in the database;
109
+ #
110
+ # 2) If the notification name does not match any ignored pattern;
111
+ #
112
+ def self.valid_for_storing?(args) #:nodoc:
113
+ name, payload = args[0].to_s, args[4]
114
+
115
+ RailsCustomerbeats.listening? && RailsCustomerbeats.store &&
116
+ !self.ignore_patterns.find { |p| String === p ? name == p : name =~ p } &&
117
+ !self.ignore_lambdas.values.any? { |b| b.call(name, payload) }
118
+ end
119
+ end
120
+
121
+ require 'rails_customerbeats/engine'
@@ -0,0 +1,6 @@
1
+ /* g.Raphael 0.4 - Charting library, based on Raphaël
2
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
3
+ * Licensed under the MIT License.
4
+ * This code was modified by José Valim to not include "Others" neither sort values.
5
+ */
6
+ Raphael.fn.g.piechart=function(d,c,n,b,k){k=k||{};var j=this,l=[],f=this.set(),m=this.set(),h=this.set(),t=[],v=b.length,w=0,x=0;m.covers=f;if(v==1){h.push(this.circle(d,c,n).attr({fill:this.g.colors[0],stroke:opt.stroke||"#fff","stroke-width":k.strokewidth==null?1:k.strokewidth}));f.push(this.circle(d,c,n).attr(this.g.shim));x=b[0];b[0]={value:b[0],order:0,valueOf:function(){return this.value}};h[0].middle={x:d,y:c};h[0].mangle=180}else{function s(C,B,i,E,A,J){var G=Math.PI/180,y=C+i*Math.cos(-E*G),p=C+i*Math.cos(-A*G),D=C+i/2*Math.cos(-(E+(A-E)/2)*G),I=B+i*Math.sin(-E*G),H=B+i*Math.sin(-A*G),z=B+i/2*Math.sin(-(E+(A-E)/2)*G),F=["M",C,B,"L",y,I,"A",i,i,0,+(Math.abs(A-E)>180),1,p,H,"z"];F.middle={x:D,y:z};return F}for(var u=0;u<v;u++){x+=b[u];b[u]={value:b[u],order:u,valueOf:function(){return this.value}}}for(var u=0;u<v;u++){var e=w-360*b[u]/x/2;if(!u){w=90-e;e=w-360*b[u]/x/2}if(k.init){var g=s(d,c,1,w,w-360*b[u]/x).join(",")}var q=s(d,c,n,w,w-=360*b[u]/x);var o=this.path(k.init?g:q).attr({fill:k.colors&&k.colors[u]||this.g.colors[u]||"#666",stroke:k.stroke||"#fff","stroke-width":(k.strokewidth==null?1:k.strokewidth),"stroke-linejoin":"round"});o.value=b[u];o.middle=q.middle;o.mangle=e;l.push(o);h.push(o);k.init&&o.animate({path:q.join(",")},(+k.init-1)||1000,">")}for(var u=0;u<v;u++){var o=j.path(l[u].attr("path")).attr(this.g.shim);k.href&&k.href[u]&&o.attr({href:k.href[u]});o.attr=function(){};f.push(o);h.push(o)}}m.hover=function(z,r){r=r||function(){};var y=this;for(var p=0;p<v;p++){(function(A,B,i){var C={sector:A,cover:B,cx:d,cy:c,mx:A.middle.x,my:A.middle.y,mangle:A.mangle,r:n,value:b[i],total:x,label:y.labels&&y.labels[i]};B.mouseover(function(){z.call(C)}).mouseout(function(){r.call(C)})})(h[p],f[p],p)}return this};m.each=function(y){var r=this;for(var p=0;p<v;p++){(function(z,A,i){var B={sector:z,cover:A,cx:d,cy:c,x:z.middle.x,y:z.middle.y,mangle:z.mangle,r:n,value:b[i],total:x,label:r.labels&&r.labels[i]};y.call(B)})(h[p],f[p],p)}return this};m.click=function(y){var r=this;for(var p=0;p<v;p++){(function(z,A,i){var B={sector:z,cover:A,cx:d,cy:c,mx:z.middle.x,my:z.middle.y,mangle:z.mangle,r:n,value:b[i],total:x,label:r.labels&&r.labels[i]};A.click(function(){y.call(B)})})(h[p],f[p],p)}return this};m.inject=function(i){i.insertBefore(f[0])};var a=function(E,z,r,p){var I=d+n+n/5,H=c,D=H+10;E=E||[];p=(p&&p.toLowerCase&&p.toLowerCase())||"east";r=j.g.markers[r&&r.toLowerCase()]||"disc";m.labels=j.set();for(var C=0;C<v;C++){var J=h[C].attr("fill"),A=b[C].order,B;E[A]=j.g.labelise(E[A],b[C],x);m.labels.push(j.set());m.labels[C].push(j.g[r](I+5,D,5).attr({fill:J,stroke:"none"}));m.labels[C].push(B=j.text(I+20,D,E[A]||b[A]).attr(j.g.txtattr).attr({fill:k.legendcolor||"#000","text-anchor":"start"}));f[C].label=m.labels[C];D+=B.getBBox().height*1.2}var F=m.labels.getBBox(),G={east:[0,-F.height/2],west:[-F.width-2*n-20,-F.height/2],north:[-n-F.width/2,-n-F.height-10],south:[-n-F.width/2,n+10]}[p];m.labels.translate.apply(m.labels,G);m.push(m.labels)};if(k.legend){a(k.legend,k.legendothers,k.legendmark,k.legendpos)}m.push(h,f);m.series=h;m.covers=f;return m};