Class: Railscope::Middleware
- Inherits:
-
Object
- Object
- Railscope::Middleware
- Defined in:
- lib/railscope/middleware.rb
Constant Summary collapse
- RESPONSE_SIZE_LIMIT =
Maximum size for response body capture (64KB like Telescope)
64 * 1024
- IGNORED_INSTANCE_VARS =
%w[ request response marked_for_same_origin_verification performed_redirect action_has_layout lookup_context view_context_class current_renderer view_renderer action_name pressed_key action_status response_body ].freeze
Class Method Summary collapse
- .extract_controller_data(controller) ⇒ Object
- .extract_session_from_env(env) ⇒ Object
- .extract_view_response(env) ⇒ Object
- .parse_response_body(response_body, content_type, env) ⇒ Object
- .resolve_view_path(controller) ⇒ Object
- .safe_serialize(value, depth: 0) ⇒ Object
- .update_entry_with_response(context_data, response_body) ⇒ Object
Instance Method Summary collapse
- #call(env) ⇒ Object
- #extract_body_from_env(env) ⇒ Object
-
#initialize(app) ⇒ Middleware
constructor
A new instance of Middleware.
- #update_entry_async(context_data, body_content) ⇒ Object
Constructor Details
#initialize(app) ⇒ Middleware
Returns a new instance of Middleware.
8 9 10 |
# File 'lib/railscope/middleware.rb', line 8 def initialize(app) @app = app end |
Class Method Details
.extract_controller_data(controller) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/railscope/middleware.rb', line 168 def self.extract_controller_data(controller) data = {} controller.instance_variables.each do |ivar| name = ivar.to_s.delete_prefix("@") # Skip internal Rails/controller variables next if name.start_with?("_") next if IGNORED_INSTANCE_VARS.include?(name) value = controller.instance_variable_get(ivar) data[name] = safe_serialize(value) end data.presence || {} rescue StandardError {} end |
.extract_session_from_env(env) ⇒ Object
221 222 223 224 225 226 227 228 229 230 |
# File 'lib/railscope/middleware.rb', line 221 def self.extract_session_from_env(env) return {} unless env session = env["rack.session"] || env["action_dispatch.request.session"] return {} unless session session.to_h.transform_keys(&:to_s).except("_csrf_token", "session_id") rescue StandardError {} end |
.extract_view_response(env) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/railscope/middleware.rb', line 134 def self.extract_view_response(env) return nil unless env controller = env["action_controller.instance"] return nil unless controller view_path = resolve_view_path(controller) return nil unless view_path { "view" => view_path, "data" => extract_controller_data(controller) } rescue StandardError nil end |
.parse_response_body(response_body, content_type, env) ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/railscope/middleware.rb', line 105 def self.parse_response_body(response_body, content_type, env) body_str = response_body.to_s # Empty response return "Empty Response" if body_str.blank? # Redirect if env && (location = env["action_dispatch.redirect_url"]) return "Redirected to #{location}" end # JSON response if content_type.include?("application/json") || body_str.match?(/\A\s*[\[{]/) begin return JSON.parse(body_str) rescue StandardError return body_str.truncate(2000) end end # Plain text response if content_type.include?("text/plain") return body_str.truncate(2000) end # HTML view response — extract template path and data like Telescope extract_view_response(env) || "HTML Response" end |
.resolve_view_path(controller) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/railscope/middleware.rb', line 151 def self.resolve_view_path(controller) # Try to get the actual rendered template from the controller if controller.respond_to?(:rendered_format, true) || controller.respond_to?(:controller_path) template_path = "app/views/#{controller.controller_path}/#{controller.action_name}" # Try to find the actual file with extension if defined?(Rails.root) candidates = Dir.glob(Rails.root.join("#{template_path}.*")) return candidates.first&.sub("#{Rails.root}/", "") if candidates.any? end template_path end rescue StandardError nil end |
.safe_serialize(value, depth: 0) ⇒ Object
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/railscope/middleware.rb', line 194 def self.safe_serialize(value, depth: 0) return "..." if depth > 5 case value when String, Numeric, TrueClass, FalseClass, NilClass value when Symbol value.to_s when Time, DateTime value.iso8601 when Date value.to_s when ActiveRecord::Relation value.limit(50).map { |record| safe_serialize(record, depth: depth + 1) } when ActiveRecord::Base Railscope.filter(value.attributes.transform_values { |v| safe_serialize(v, depth: depth + 1) }) when Array value.first(50).map { |v| safe_serialize(v, depth: depth + 1) } when Hash value.transform_values { |v| safe_serialize(v, depth: depth + 1) } else value.to_s end rescue StandardError value.class.name end |
.update_entry_with_response(context_data, response_body) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/railscope/middleware.rb', line 75 def self.update_entry_with_response(context_data, response_body) return unless Railscope.ready? headers = context_data[:headers] response_headers = begin headers.respond_to?(:to_h) ? headers.to_h : headers.to_hash rescue StandardError {} end session_data = extract_session_from_env(context_data[:env]) # Determine response type and handle accordingly (like Telescope) content_type = response_headers["Content-Type"] || response_headers["content-type"] || "" parsed_body = parse_response_body(response_body, content_type, context_data[:env]) payload_updates = { "response" => parsed_body.presence, "response_headers" => response_headers, "session" => session_data } Railscope.storage.update_by_batch( batch_id: context_data[:batch_id], entry_type: "request", payload_updates: payload_updates ) rescue StandardError => e Rails.logger.debug("[Railscope] Failed to update entry with response: #{e.}") end |
Instance Method Details
#call(env) ⇒ Object
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/railscope/middleware.rb', line 12 def call(env) return @app.call(env) unless Railscope.enabled? setup_context(env) status, headers, response = @app.call(env) # Capture response body for recording context = Context.current if context[:recording] # In conditional mode, skip persistence if trigger never fired unless Railscope.conditional_recording? && !context.triggered? # Read body from env (where Rails stores the response) body_content = extract_body_from_env(env) context_data = { batch_id: context.batch_id, env: env, headers: headers } # Update entry with response data update_entry_async(context_data, body_content) end end [status, headers, response] ensure Context.clear! end |
#extract_body_from_env(env) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/railscope/middleware.rb', line 42 def extract_body_from_env(env) body = "" # Try to get from ActionDispatch::Response stored in env if env["action_dispatch.response"] response = env["action_dispatch.response"] body = response.body.to_s if response.respond_to?(:body) end # Try action_controller.instance if body.empty? && env["action_controller.instance"] controller = env["action_controller.instance"] if controller.respond_to?(:response) && controller.response.respond_to?(:body) body = controller.response.body.to_s end end # Truncate if too large body = body.byteslice(0, RESPONSE_SIZE_LIMIT) if body.bytesize > RESPONSE_SIZE_LIMIT body rescue StandardError => e Rails.logger.debug("[Railscope] Failed to extract body: #{e.}") "" end |
#update_entry_async(context_data, body_content) ⇒ Object
68 69 70 71 72 73 |
# File 'lib/railscope/middleware.rb', line 68 def update_entry_async(context_data, body_content) # Update synchronously for now (could be made async later) self.class.update_entry_with_response(context_data, body_content) rescue StandardError => e Rails.logger.debug("[Railscope] Failed to update entry: #{e.}") end |