rails6-footnotes 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +25 -0
- data/.gitignore +15 -0
- data/.rspec.example +1 -0
- data/CHANGELOG +129 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +187 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +188 -0
- data/Rakefile +18 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/gemfiles/Gemfile.rails-3.2.22 +5 -0
- data/gemfiles/Gemfile.rails-4.0.x +6 -0
- data/gemfiles/Gemfile.rails-4.1.x +5 -0
- data/gemfiles/Gemfile.rails-4.2.x +5 -0
- data/gemfiles/Gemfile.rails-edge +5 -0
- data/lib/generators/rails_footnotes/install_generator.rb +14 -0
- data/lib/generators/templates/rails_footnotes.rb +26 -0
- data/lib/rails-footnotes.rb +68 -0
- data/lib/rails-footnotes/abstract_note.rb +178 -0
- data/lib/rails-footnotes/each_with_rescue.rb +36 -0
- data/lib/rails-footnotes/extension.rb +24 -0
- data/lib/rails-footnotes/filter.rb +359 -0
- data/lib/rails-footnotes/notes/all.rb +1 -0
- data/lib/rails-footnotes/notes/assigns_note.rb +60 -0
- data/lib/rails-footnotes/notes/controller_note.rb +55 -0
- data/lib/rails-footnotes/notes/cookies_note.rb +17 -0
- data/lib/rails-footnotes/notes/env_note.rb +24 -0
- data/lib/rails-footnotes/notes/files_note.rb +49 -0
- data/lib/rails-footnotes/notes/filters_note.rb +51 -0
- data/lib/rails-footnotes/notes/javascripts_note.rb +16 -0
- data/lib/rails-footnotes/notes/layout_note.rb +26 -0
- data/lib/rails-footnotes/notes/log_note.rb +47 -0
- data/lib/rails-footnotes/notes/log_note/note_logger.rb +59 -0
- data/lib/rails-footnotes/notes/params_note.rb +21 -0
- data/lib/rails-footnotes/notes/partials_note.rb +38 -0
- data/lib/rails-footnotes/notes/queries_note.rb +121 -0
- data/lib/rails-footnotes/notes/routes_note.rb +60 -0
- data/lib/rails-footnotes/notes/session_note.rb +27 -0
- data/lib/rails-footnotes/notes/stylesheets_note.rb +16 -0
- data/lib/rails-footnotes/notes/view_note.rb +41 -0
- data/lib/rails-footnotes/version.rb +3 -0
- data/lib/rails6-footnotes.rb +1 -0
- data/rails-footnotes.gemspec +22 -0
- data/spec/abstract_note_spec.rb +89 -0
- data/spec/app/assets/config/manifest.js +2 -0
- data/spec/app/assets/javascripts/foobar.js +1 -0
- data/spec/app/assets/stylesheets/foobar.css +0 -0
- data/spec/app/views/files/index.html.erb +1 -0
- data/spec/app/views/layouts/application.html.erb +12 -0
- data/spec/app/views/partials/_foo.html.erb +1 -0
- data/spec/app/views/partials/index.html.erb +1 -0
- data/spec/controllers/files_note_controller_spec.rb +38 -0
- data/spec/controllers/footnotes_controller_spec.rb +128 -0
- data/spec/controllers/log_note_controller_spec.rb +32 -0
- data/spec/controllers/partials_note_controller_spec.rb +28 -0
- data/spec/env_note_spec.rb +73 -0
- data/spec/fixtures/html_download.html +5 -0
- data/spec/footnotes_spec.rb +234 -0
- data/spec/notes/assigns_note_spec.rb +50 -0
- data/spec/notes/controller_note_spec.rb +12 -0
- data/spec/notes/files_note_spec.rb +26 -0
- data/spec/notes/javascripts_note_spec.rb +18 -0
- data/spec/notes/stylesheets_note_spec.rb +19 -0
- data/spec/notes/view_note_spec.rb +12 -0
- data/spec/spec_helper.rb +68 -0
- 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
|