rails6-footnotes 5.0.0
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.
- 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
|