revelio 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d57c3092796a024f71491ecccb201e2e18eec378a1854212d72400b7d1a4d7d2
4
+ data.tar.gz: 14f760356c957b4e2e5d36d8530866bbee70a3983c3d73ad221a1c05268d6f0d
5
+ SHA512:
6
+ metadata.gz: 0cee26631803029474d627620cdb62693350efbcd8ede3086d7198355a7eac7000e02de9add776ca1f749ff30d7176de27f872871a05a5f55b90f48efee426b8
7
+ data.tar.gz: 7d85dd1539a39200675a48cfa92c9a7148bd7627dd4fd7ab3d137c242899b8d4b591403434b04de65d780389abcfff12b525c2bd2116d55695b456dbcbbae1c0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Jose Galisteo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revelio
4
+ class Config
5
+ attr_accessor :debug_mode, :project_root, :inject_overlay,
6
+ :duration_threshold, :queries_threshold, :gc_objects_threshold
7
+
8
+ def initialize
9
+ @debug_mode = false
10
+ @project_root = nil
11
+ @inject_overlay = true
12
+ @duration_threshold = 200
13
+ @queries_threshold = 20
14
+ @gc_objects_threshold = 100_000
15
+ end
16
+
17
+ def thresholds
18
+ {
19
+ duration: @duration_threshold,
20
+ queries: @queries_threshold,
21
+ gc_objects: @gc_objects_threshold
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revelio
4
+ module ErbTemplateExtension
5
+ include TemplateType
6
+
7
+ def call(template, source = nil)
8
+ compiled = super
9
+ return compiled unless Revelio.config.debug_mode
10
+ return compiled unless template.format == :html
11
+
12
+ relative, type, short = resolve_template_info(template)
13
+
14
+ preamble = %{<!-- revelio-begin file="#{relative}" type="#{type}" short="#{short}" -->}
15
+ postamble = %{<!-- revelio-end file="#{relative}" -->}
16
+
17
+ # ERB's output buffer is already initialized by ActionView before the template runs
18
+ "@output_buffer.safe_append='#{preamble}';#{compiled}\n@output_buffer.safe_append='#{postamble}';\n@output_buffer"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revelio
4
+ module HamlTagCompilerExtension
5
+ include TemplateType
6
+
7
+ def initialize(identity, options)
8
+ @revelio_filename = options[:filename] || "unknown"
9
+ super
10
+ end
11
+
12
+ def compile(node, &block)
13
+ result = super
14
+ return result unless Revelio.config.debug_mode
15
+
16
+ inject_debug_attrs(result, node)
17
+ end
18
+
19
+ private
20
+
21
+ def inject_debug_attrs(temple, node)
22
+ attrs = temple[3]
23
+ return temple unless attrs.is_a?(Array) && attrs[0] == :html && attrs[1] == :attrs
24
+
25
+ filename = @revelio_filename.dup
26
+ root = Revelio.config.project_root
27
+ if root && filename.start_with?(root)
28
+ filename = filename.delete_prefix(root).delete_prefix("/")
29
+ end
30
+
31
+ debug_attrs = [
32
+ [:html, :attr, "data-revelio-file", [:static, filename]],
33
+ [:html, :attr, "data-revelio-line", [:static, node.line.to_s]],
34
+ [:html, :attr, "data-revelio-type", [:static, template_type(filename)]]
35
+ ]
36
+
37
+ temple[3] = [:html, :attrs, *attrs[2..], *debug_attrs]
38
+ temple
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revelio
4
+ module HamlTemplateExtension
5
+ include TemplateType
6
+
7
+ def call(template, source = nil)
8
+ return super unless Revelio.config.debug_mode
9
+
10
+ source ||= template.source
11
+ options = Haml::RailsTemplate.options
12
+
13
+ if template.respond_to?(:identifier)
14
+ options = options.merge(filename: template.identifier)
15
+ end
16
+
17
+ if template.respond_to?(:type) && template.type == "text/xml"
18
+ options = options.merge(format: :xhtml)
19
+ end
20
+
21
+ relative, type, short = resolve_template_info(template)
22
+
23
+ preamble_parts = []
24
+ postamble_parts = []
25
+
26
+ if ActionView::Base.try(:annotate_rendered_view_with_filenames) && template.format == :html
27
+ preamble_parts << "<!-- BEGIN #{template.short_identifier} -->"
28
+ postamble_parts << "<!-- END #{template.short_identifier} -->"
29
+ end
30
+
31
+ marker_attrs = "file=\"#{relative}\" type=\"#{type}\" short=\"#{short}\""
32
+ preamble_parts << "<!-- revelio-begin #{marker_attrs} -->"
33
+ postamble_parts.unshift("<!-- revelio-end file=\"#{relative}\" -->")
34
+
35
+ options = options.merge(
36
+ preamble: preamble_parts.join("\n"),
37
+ postamble: postamble_parts.join("\n")
38
+ )
39
+
40
+ Haml::Engine.new(options).call(source)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "render_subscriber"
5
+
6
+ module Revelio
7
+ class Middleware
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ ensure_installed!
14
+
15
+ status = nil
16
+ headers = nil
17
+ response = nil
18
+
19
+ metrics = collect_metrics do
20
+ status, headers, response = @app.call(env)
21
+ end
22
+
23
+ return [status, headers, response] unless injectable?(status, headers)
24
+
25
+ body = +""
26
+ response.each { |chunk| body << chunk }
27
+ response.close if response.respond_to?(:close)
28
+
29
+ if body.include?("</body>")
30
+ body.sub!("</body>", "#{devtools_injection(metrics)}\n</body>")
31
+ headers["content-length"] = body.bytesize.to_s if headers["content-length"]
32
+ end
33
+
34
+ [status, headers, [body]]
35
+ end
36
+
37
+ private
38
+
39
+ def collect_metrics
40
+ all_queries = []
41
+ subscribers = []
42
+
43
+ if defined?(ActiveSupport::Notifications)
44
+ # SQL tracking -- Rails 8.1 passes a single Event object
45
+ subscribers << ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
46
+ next if event.payload[:name] == "SCHEMA"
47
+ all_queries << {
48
+ name: event.payload[:name],
49
+ duration: event.duration.round(2),
50
+ allocations: event.allocations
51
+ }
52
+ end
53
+
54
+ # Render tracking with start/finish stack
55
+ render_tracker = RenderSubscriber.new(all_queries)
56
+ %w[render_template.action_view render_partial.action_view render.view_component].each do |event|
57
+ subscribers << ActiveSupport::Notifications.subscribe(event, render_tracker)
58
+ end
59
+ end
60
+
61
+ gc_before = GC.stat(:total_allocated_objects)
62
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
63
+
64
+ yield
65
+
66
+ duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(1)
67
+ gc_allocated = GC.stat(:total_allocated_objects) - gc_before
68
+
69
+ subscribers.compact.each { |s| ActiveSupport::Notifications.unsubscribe(s) }
70
+
71
+ renders = defined?(render_tracker) ? render_tracker.renders : []
72
+ query_time = all_queries.sum { |q| q[:duration] }
73
+
74
+ {
75
+ duration: duration,
76
+ queries: all_queries.size,
77
+ query_time: query_time.round(1),
78
+ renders: renders.size,
79
+ render_time: renders.sum { |r| r[:duration] }.round(1),
80
+ gc_objects: gc_allocated,
81
+ thresholds: Revelio.config.thresholds,
82
+ details: {
83
+ queries: all_queries.first(50),
84
+ renders: renders
85
+ }
86
+ }
87
+ end
88
+
89
+ def ensure_installed!
90
+ # Always call install! — it's idempotent per-engine and handles
91
+ # late-defined constants like Slim::RailsTemplate
92
+ Revelio.install!
93
+ end
94
+
95
+ def injectable?(status, headers)
96
+ status == 200 &&
97
+ !headers["content-disposition"]&.start_with?("attachment") &&
98
+ headers["content-type"]&.include?("text/html")
99
+ end
100
+
101
+ def devtools_injection(metrics)
102
+ meta = "<meta name=\"revelio-project-path\" content=\"#{Revelio.config.project_root}\">"
103
+ metrics_tag = "<script type=\"application/json\" id=\"revelio-metrics\">#{metrics.to_json}</script>"
104
+ "#{meta}\n#{metrics_tag}\n#{self.class.devtools_script}"
105
+ end
106
+
107
+ def self.devtools_script
108
+ js = File.read(File.expand_path("overlay.js", __dir__))
109
+ "<script id=\"revelio\">\n#{js}\n</script>"
110
+ end
111
+ end
112
+ end