rails6-footnotes 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +25 -0
  3. data/.gitignore +15 -0
  4. data/.rspec.example +1 -0
  5. data/CHANGELOG +129 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +187 -0
  8. data/MIT-LICENSE +21 -0
  9. data/README.rdoc +188 -0
  10. data/Rakefile +18 -0
  11. data/bin/rake +29 -0
  12. data/bin/rspec +29 -0
  13. data/gemfiles/Gemfile.rails-3.2.22 +5 -0
  14. data/gemfiles/Gemfile.rails-4.0.x +6 -0
  15. data/gemfiles/Gemfile.rails-4.1.x +5 -0
  16. data/gemfiles/Gemfile.rails-4.2.x +5 -0
  17. data/gemfiles/Gemfile.rails-edge +5 -0
  18. data/lib/generators/rails_footnotes/install_generator.rb +14 -0
  19. data/lib/generators/templates/rails_footnotes.rb +26 -0
  20. data/lib/rails-footnotes.rb +68 -0
  21. data/lib/rails-footnotes/abstract_note.rb +178 -0
  22. data/lib/rails-footnotes/each_with_rescue.rb +36 -0
  23. data/lib/rails-footnotes/extension.rb +24 -0
  24. data/lib/rails-footnotes/filter.rb +359 -0
  25. data/lib/rails-footnotes/notes/all.rb +1 -0
  26. data/lib/rails-footnotes/notes/assigns_note.rb +60 -0
  27. data/lib/rails-footnotes/notes/controller_note.rb +55 -0
  28. data/lib/rails-footnotes/notes/cookies_note.rb +17 -0
  29. data/lib/rails-footnotes/notes/env_note.rb +24 -0
  30. data/lib/rails-footnotes/notes/files_note.rb +49 -0
  31. data/lib/rails-footnotes/notes/filters_note.rb +51 -0
  32. data/lib/rails-footnotes/notes/javascripts_note.rb +16 -0
  33. data/lib/rails-footnotes/notes/layout_note.rb +26 -0
  34. data/lib/rails-footnotes/notes/log_note.rb +47 -0
  35. data/lib/rails-footnotes/notes/log_note/note_logger.rb +59 -0
  36. data/lib/rails-footnotes/notes/params_note.rb +21 -0
  37. data/lib/rails-footnotes/notes/partials_note.rb +38 -0
  38. data/lib/rails-footnotes/notes/queries_note.rb +121 -0
  39. data/lib/rails-footnotes/notes/routes_note.rb +60 -0
  40. data/lib/rails-footnotes/notes/session_note.rb +27 -0
  41. data/lib/rails-footnotes/notes/stylesheets_note.rb +16 -0
  42. data/lib/rails-footnotes/notes/view_note.rb +41 -0
  43. data/lib/rails-footnotes/version.rb +3 -0
  44. data/lib/rails6-footnotes.rb +1 -0
  45. data/rails-footnotes.gemspec +22 -0
  46. data/spec/abstract_note_spec.rb +89 -0
  47. data/spec/app/assets/config/manifest.js +2 -0
  48. data/spec/app/assets/javascripts/foobar.js +1 -0
  49. data/spec/app/assets/stylesheets/foobar.css +0 -0
  50. data/spec/app/views/files/index.html.erb +1 -0
  51. data/spec/app/views/layouts/application.html.erb +12 -0
  52. data/spec/app/views/partials/_foo.html.erb +1 -0
  53. data/spec/app/views/partials/index.html.erb +1 -0
  54. data/spec/controllers/files_note_controller_spec.rb +38 -0
  55. data/spec/controllers/footnotes_controller_spec.rb +128 -0
  56. data/spec/controllers/log_note_controller_spec.rb +32 -0
  57. data/spec/controllers/partials_note_controller_spec.rb +28 -0
  58. data/spec/env_note_spec.rb +73 -0
  59. data/spec/fixtures/html_download.html +5 -0
  60. data/spec/footnotes_spec.rb +234 -0
  61. data/spec/notes/assigns_note_spec.rb +50 -0
  62. data/spec/notes/controller_note_spec.rb +12 -0
  63. data/spec/notes/files_note_spec.rb +26 -0
  64. data/spec/notes/javascripts_note_spec.rb +18 -0
  65. data/spec/notes/stylesheets_note_spec.rb +19 -0
  66. data/spec/notes/view_note_spec.rb +12 -0
  67. data/spec/spec_helper.rb +68 -0
  68. metadata +153 -0
@@ -0,0 +1,17 @@
1
+ module Footnotes
2
+ module Notes
3
+ class CookiesNote < AbstractNote
4
+ def initialize(controller)
5
+ @cookies = controller.request.cookies
6
+ end
7
+
8
+ def title
9
+ "Cookies (#{@cookies.length})"
10
+ end
11
+
12
+ def content
13
+ mount_table_for_hash(@cookies, :summary => "Debug information for #{title}")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ module Footnotes
2
+ module Notes
3
+ class EnvNote < AbstractNote
4
+ def initialize(controller)
5
+ @env = controller.request.env.dup
6
+ end
7
+
8
+ def content
9
+ env_data = @env.map { |k, v|
10
+ case k
11
+ when 'HTTP_COOKIE'
12
+ # Replace HTTP_COOKIE for a link
13
+ [k.to_s, '<a href="#" style="color:#009" onclick="Footnotes.hideAllAndToggle(\'cookies_debug_info\');return false;">See cookies on its tab</a>']
14
+ else
15
+ [k.to_s, escape(v.to_s)]
16
+ end
17
+ }.sort.unshift([ :key, escape('value') ])
18
+
19
+ # Create the env table
20
+ mount_table(env_data)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ module Footnotes
2
+ module Notes
3
+ class FilesNote < AbstractNote
4
+ def initialize(controller)
5
+ @files = scan_text(controller.response.body)
6
+ parse_files!
7
+ end
8
+
9
+ def row
10
+ :edit
11
+ end
12
+
13
+ def content
14
+ if @files.empty?
15
+ ""
16
+ else
17
+ "<ul><li>%s</li></ul>" % @files.join("</li><li>")
18
+ end
19
+ end
20
+
21
+ def valid?
22
+ prefix?
23
+ end
24
+
25
+ protected
26
+ def scan_text(text)
27
+ raise NotImplementedError, "implement this in your subclass"
28
+ end
29
+
30
+ def parse_files!
31
+ asset_paths = Rails.application.config.try(:assets).try(:paths) || []
32
+ linked_files = []
33
+
34
+ @files.collect do |file|
35
+ file.gsub!(/-[a-f0-9]{64}\./, '.')
36
+ base_name = File.basename(file)
37
+ asset_paths.each do |asset_path|
38
+ results = Dir[File.expand_path(base_name, asset_path) + '*']
39
+ results.each do |r|
40
+ linked_files << %[<a href="#{Footnotes::Filter.prefix(r, 1, 1)}">#{File.basename(r)}</a>]
41
+ end
42
+ break if results.present?
43
+ end
44
+ end
45
+ @files = linked_files
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,51 @@
1
+ module Footnotes
2
+ module Notes
3
+ class FiltersNote < AbstractNote
4
+ def initialize(controller)
5
+ @controller = controller
6
+ @parsed_filters = parse_filters
7
+ end
8
+
9
+ def legend
10
+ "Filter chain for #{@controller.class.to_s}"
11
+ end
12
+
13
+ def content
14
+ mount_table(@parsed_filters.unshift([:name, :type, :actions]), :summary => "Debug information for #{title}")
15
+ end
16
+
17
+ protected
18
+ # Get controller filter chain
19
+ #
20
+ def parse_filters
21
+ return [] # TODO @controller.class.filter_chain.collect do |filter|
22
+ # [parse_method(filter.method), filter.type.inspect, controller_filtered_actions(filter).inspect]
23
+ # end
24
+ end
25
+
26
+ # This receives a filter, creates a mock controller and check in which
27
+ # actions the filter is performed
28
+ #
29
+ def controller_filtered_actions(filter)
30
+ mock_controller = Footnotes::Extensions::MockController.new
31
+
32
+ return @controller.class.action_methods.select { |action|
33
+ mock_controller.action_name = action
34
+
35
+ #remove conditions (this would call a Proc on the mock_controller)
36
+ filter.options.merge!(:if => nil, :unless => nil)
37
+
38
+ filter.__send__(:should_run_callback?, mock_controller)
39
+ }.map(&:to_sym)
40
+ end
41
+
42
+ def parse_method(method = '')
43
+ escape(method.inspect.gsub(RAILS_ROOT, ''))
44
+ end
45
+ end
46
+ end
47
+
48
+ module Extensions
49
+ class MockController < Struct.new(:action_name); end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ require "rails-footnotes/notes/files_note"
2
+
3
+ module Footnotes
4
+ module Notes
5
+ class JavascriptsNote < FilesNote
6
+ def title
7
+ "Javascripts (#{@files.length})"
8
+ end
9
+
10
+ protected
11
+ def scan_text(text)
12
+ text.scan(/<script[^>]+src\s*=\s*['"]([^>?'"]+\.js)/im).flatten
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module Footnotes
2
+ module Notes
3
+ class LayoutNote < AbstractNote
4
+ def initialize(controller)
5
+ @controller = controller
6
+ end
7
+
8
+ def row
9
+ :edit
10
+ end
11
+
12
+ def link
13
+ escape(Footnotes::Filter.prefix(filename, 1, 1))
14
+ end
15
+
16
+ def valid?
17
+ prefix? && nil#@controller.active_layout TODO doesn't work with Rails 3
18
+ end
19
+
20
+ protected
21
+ def filename
22
+ File.join(File.expand_path(Rails.root), 'app', 'layouts', "#{@controller.active_layout.to_s.underscore}").sub('/layouts/layouts/', '/views/layouts/')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ module Footnotes
2
+ module Notes
3
+ class LogNote < AbstractNote
4
+
5
+ autoload :NoteLogger, 'rails-footnotes/notes/log_note/note_logger'
6
+
7
+ cattr_accessor :logs
8
+ cattr_accessor :original_logger
9
+
10
+ def self.start!(controller)
11
+ self.logs = []
12
+ self.original_logger = Rails.logger
13
+ note_logger = NoteLogger.new(self.logs)
14
+ note_logger.level = self.original_logger.level
15
+ note_logger.formatter =
16
+ if self.original_logger.kind_of?(Logger)
17
+ self.original_logger.formatter
18
+ else
19
+ defined?(ActiveSupport::Logger) ? ActiveSupport::Logger::SimpleFormatter.new : Logger::SimpleFormatter.new
20
+ end
21
+ # Rails 3 don't have ActiveSupport::Logger#broadcast so we backported it
22
+ extend_module = defined?(ActiveSupport::Logger) ? ActiveSupport::Logger.broadcast(note_logger) : NoteLogger.broadcast(note_logger)
23
+ Rails.logger = self.original_logger.clone.extend(extend_module)
24
+ end
25
+
26
+ def title
27
+ "Log (#{log.count})"
28
+ end
29
+
30
+ def content
31
+ result = '<table>'
32
+ log.compact.each do |l|
33
+ result << "<tr><td>#{l.gsub(/\e\[.+?m/, '')}</td></tr>"
34
+ end
35
+ result << '</table>'
36
+ # Restore formatter
37
+ Rails.logger = self.class.original_logger
38
+ result
39
+ end
40
+
41
+ def log
42
+ self.class.logs
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,59 @@
1
+ module Footnotes
2
+ module Notes
3
+ class LogNote
4
+ class NoteLogger < Logger
5
+
6
+ def initialize(logs)
7
+ @logs = logs
8
+ end
9
+
10
+ def add(severity, message = nil, progname = nil, &block)
11
+ severity ||= UNKNOWN
12
+ if severity < level
13
+ return true
14
+ end
15
+ formatter = @formatter || Logger::Formatter.new
16
+ @logs << formatter.call(format_severity(severity), Time.now, message, progname)
17
+ end
18
+
19
+ ## Backport from rails 4 for handling logging broadcast, should be removed when rails 3 is deprecated :
20
+
21
+ # Broadcasts logs to multiple loggers.
22
+ def self.broadcast(logger) # :nodoc:
23
+ Module.new do
24
+ define_method(:add) do |*args, &block|
25
+ logger.add(*args, &block)
26
+ super(*args, &block)
27
+ end
28
+
29
+ define_method(:<<) do |x|
30
+ logger << x
31
+ super(x)
32
+ end
33
+
34
+ define_method(:close) do
35
+ logger.close
36
+ super()
37
+ end
38
+
39
+ define_method(:progname=) do |name|
40
+ logger.progname = name
41
+ super(name)
42
+ end
43
+
44
+ define_method(:formatter=) do |formatter|
45
+ logger.formatter = formatter
46
+ super(formatter)
47
+ end
48
+
49
+ define_method(:level=) do |level|
50
+ logger.level = level
51
+ super(level)
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ module Footnotes
2
+ module Notes
3
+ class ParamsNote < AbstractNote
4
+ def initialize(controller)
5
+ @params = if Rails::VERSION::MAJOR >= 5
6
+ controller.params.to_unsafe_h
7
+ else
8
+ controller.params
9
+ end
10
+ end
11
+
12
+ def title
13
+ "Params (#{@params.length})"
14
+ end
15
+
16
+ def content
17
+ mount_table_for_hash(@params, :summary => "Debug information for #{title}")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ module Footnotes
2
+ module Notes
3
+ class PartialsNote < AbstractNote
4
+
5
+ cattr_accessor :partials
6
+
7
+ def self.start!(controller)
8
+ self.partials = []
9
+ @subscriber ||= ActiveSupport::Notifications.subscribe('render_partial.action_view') do |*args|
10
+ event = ActiveSupport::Notifications::Event.new *args
11
+ self.partials << {:file => event.payload[:identifier], :duration => event.duration}
12
+ end
13
+ end
14
+
15
+ def initialize(controller)
16
+ @controller = controller
17
+ end
18
+
19
+ def row
20
+ :edit
21
+ end
22
+
23
+ def title
24
+ "Partials (#{partials.size})"
25
+ end
26
+
27
+ def content
28
+ rows = self.class.partials.map do |partial|
29
+ href = Footnotes::Filter.prefix(partial[:file],1,1)
30
+ shortened_name = partial[:file].gsub(File.join(Rails.root,"app/views/"),"")
31
+ [%{<a href="#{href}">#{shortened_name}</a>},"#{partial[:duration]}ms"]
32
+ end
33
+ mount_table(rows.unshift(%w(Partial Time)), :summary => "Partials for #{title}")
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,121 @@
1
+ module Footnotes
2
+ module Notes
3
+ class QueriesNote < AbstractNote
4
+ cattr_accessor :alert_db_time, :alert_sql_number, :orm, :ignored_regexps, :instance_writer => false
5
+ @@alert_db_time = 16.0
6
+ @@alert_sql_number = 8
7
+ @@query_subscriber = nil
8
+ @@orm = [:active_record, :data_mapper]
9
+ @@ignored_regexps = [%r{(pg_table|pg_attribute|pg_namespace|show\stables|pragma|sqlite_master)}i]
10
+
11
+ def self.start!(controller)
12
+ self.query_subscriber.reset!
13
+ end
14
+
15
+ def self.query_subscriber
16
+ @@query_subscriber ||= Footnotes::Notes::QuerySubscriber.new(self.orm)
17
+ end
18
+
19
+ def events
20
+ self.class.query_subscriber.events
21
+ end
22
+
23
+ def title
24
+ queries = self.events.count
25
+ total_time = self.events.map(&:duration).sum
26
+ query_color = alert_color(self.events.count, alert_sql_number)
27
+ db_color = alert_color(total_time, alert_db_time)
28
+
29
+ <<-TITLE
30
+ <span style="background-color:#{query_color}">Queries (#{queries})</span>
31
+ <span style="background-color:#{db_color}">DB (#{"%.3f" % total_time}ms)</span>
32
+ TITLE
33
+ end
34
+
35
+ def content
36
+ html = '<table>'
37
+ self.events.each_with_index do |event, index|
38
+ sql_links = []
39
+ sql_links << "<a href=\"javascript:Footnotes.toggle('qtrace_#{index}')\" style=\"color:#00A;\">trace</a>"
40
+
41
+ html << <<-HTML
42
+ <tr>
43
+ <td>
44
+ <b id="qtitle_#{index}">#{escape(event.type.to_s.upcase)}</b> (#{sql_links.join(' | ')})
45
+ <p id="qtrace_#{index}" style="display:none;">#{parse_trace(event.trace)}</p><br />
46
+ </td>
47
+ <td>
48
+ <span id="sql_#{index}">#{print_query(event.payload[:sql])}</span>
49
+ </td>
50
+ <td>#{print_name_and_time(event.payload[:name], event.duration)}</td>
51
+ </tr>
52
+ HTML
53
+ end
54
+ html << '</table>'
55
+ return html
56
+ end
57
+
58
+ protected
59
+ def print_name_and_time(name, time)
60
+ "<span style='background-color:#{alert_color(time, alert_ratio)}'>#{escape(name || 'SQL')} (#{'%.3fms' % time})</span>"
61
+ end
62
+
63
+ def print_query(query)
64
+ escape(query.to_s.gsub(/(\s)+/, ' ').gsub('`', ''))
65
+ end
66
+
67
+ def alert_color(value, threshold)
68
+ return 'transparent' if value < threshold
69
+ '#ffff00'
70
+ end
71
+
72
+ def alert_ratio
73
+ alert_db_time / alert_sql_number
74
+ end
75
+
76
+ def parse_trace(trace)
77
+ trace.map do |t|
78
+ s = t.split(':')
79
+ %[<a href="#{escape(Footnotes::Filter.prefix("#{Rails.root.to_s}/#{s[0]}", s[1].to_i, 1))}">#{escape(t)}</a><br />]
80
+ end.join
81
+ end
82
+ end
83
+
84
+ class QuerySubscriberNotifactionEvent
85
+ attr_reader :event, :trace, :query
86
+ delegate :name, :payload, :duration, :time, :type, :to => :event
87
+
88
+ def initialize(event, ctrace)
89
+ @event, @ctrace, @query = event, ctrace, event.payload[:sql]
90
+ end
91
+
92
+ def trace
93
+ @trace ||= @ctrace.collect(&:strip).select{|i| i.gsub!(/^#{Rails.root.to_s}\//, '') } || []
94
+ end
95
+
96
+ def type
97
+ @type ||= self.query.match(/^(\s*)(select|insert|update|delete|alter)\b/im) || 'Unknown'
98
+ end
99
+ end
100
+
101
+ class QuerySubscriber < ActiveSupport::LogSubscriber
102
+ attr_accessor :events, :ignore_regexps
103
+
104
+ def initialize(orm)
105
+ super()
106
+ @events = []
107
+ orm.each {|adapter| ActiveSupport::LogSubscriber.attach_to adapter, self}
108
+ end
109
+
110
+ def reset!
111
+ self.events.clear
112
+ end
113
+
114
+ def sql(event)
115
+ unless QueriesNote.ignored_regexps.any? {|rex| event.payload[:sql] =~ rex }
116
+ @events << QuerySubscriberNotifactionEvent.new(event.dup, caller)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end