diff --git a/lib/treblle.rb b/lib/treblle.rb index b36394e..0f69480 100644 --- a/lib/treblle.rb +++ b/lib/treblle.rb @@ -2,6 +2,7 @@ require 'treblle/middleware' require 'treblle/configuration' +require 'treblle/rails/railtie' # Treblle middleware for request interception and gathering. module Treblle diff --git a/lib/treblle/generate_payload.rb b/lib/treblle/generate_payload.rb index 43c34d7..d71d22b 100644 --- a/lib/treblle/generate_payload.rb +++ b/lib/treblle/generate_payload.rb @@ -8,7 +8,7 @@ class GeneratePayload SDK_LANG = 'ruby' TIME_FORMAT = '%Y-%m-%d %H:%M:%S' - def initialize(request:, response:, started_at:, exception: nil, configuration: Treblle.configuration) + def initialize(request:, response:, started_at:, exception: false, configuration: Treblle.configuration) @request = request @response = response @started_at = started_at @@ -73,7 +73,7 @@ def payload code: response.status, size: response.size, load_time: load_time, - body: sanitize(response.body), + body: response.body, errors: build_error_object } } @@ -81,16 +81,30 @@ def payload end def build_error_object - return [] if exception.blank? + return [] if exception == false + + trace = response.body.dig("traces", "Application Trace")&.first&.[]("trace") + file_path, line_number = get_exception_path_and_line(trace) [ { source: 'onError', - type: exception.class.to_s || 'Unhandled error', - message: exception.message, - file: exception.backtrace + type: response.body["error"] || response.body["errors"] || 'Unhandled error', + message: response.body["exception"] || response.body["error"] || response.body["errors"], + file: file_path, + line: line_number } ] end + + def get_exception_path_and_line(trace) + return [nil, nil] if trace.nil? + + match_data = trace.match(/^(.*):(\d+):in `.*'$/) + file_path = match_data[1] + line_number = match_data[2] + + [file_path, line_number] + end end end diff --git a/lib/treblle/middleware.rb b/lib/treblle/middleware.rb index 6ff8784..76b9230 100644 --- a/lib/treblle/middleware.rb +++ b/lib/treblle/middleware.rb @@ -31,25 +31,20 @@ def call(env) def call_with_treblle_monitoring(env) started_at = Time.now - begin - response = @app.call(env) - rescue Exception => e - handle_monitoring(env, response, started_at, e) - raise e - end + response = @app.call(env) + status, _headers, _rack_response = response - handle_monitoring(env, response, started_at) + handle_monitoring(env, response, started_at) if status < 400 response end - def handle_monitoring(env, rack_response, started_at, exception: nil) + def handle_monitoring(env, rack_response, started_at) configuration.validate_credentials! request = RequestBuilder.new(env).build response = ResponseBuilder.new(rack_response).build - payload = GeneratePayload.new(request: request, response: response, started_at: started_at, - exception: exception).call + payload = GeneratePayload.new(request: request, response: response, started_at: started_at).call Dispatcher.new(payload: payload).call rescue StandardError => e diff --git a/lib/treblle/rails/capture_exceptions.rb b/lib/treblle/rails/capture_exceptions.rb new file mode 100644 index 0000000..d9eb50f --- /dev/null +++ b/lib/treblle/rails/capture_exceptions.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'treblle/dispatcher' +require 'treblle/request_builder' +require 'treblle/response_builder' +require 'treblle/generate_payload' +require 'treblle/logging' +require 'treblle' + +module Treblle + module Rails + class CaptureExceptions + include Logging + + def initialize(app, configuration: Treblle.configuration) + @app = app + @configuration = configuration + end + + def call(env) + started_at = Time.now + response = @app.call(env) + status, _headers, _rack_response = response + + handle_monitoring(env, response, started_at) if status >= 400 + + response + end + + private + + attr_reader :configuration + + def handle_monitoring(env, rack_response, started_at) + configuration.validate_credentials! + + request = RequestBuilder.new(env).build + response = ResponseBuilder.new(rack_response).build + payload = GeneratePayload.new(request: request, response: response, started_at: started_at, + exception: true).call + + Dispatcher.new(payload: payload).call + rescue StandardError => e + log_error(e.message) + end + end + end +end diff --git a/lib/treblle/rails/railtie.rb b/lib/treblle/rails/railtie.rb new file mode 100644 index 0000000..6f12c9a --- /dev/null +++ b/lib/treblle/rails/railtie.rb @@ -0,0 +1,14 @@ +require 'rails' +require 'treblle/rails/capture_exceptions' + +module Treblle + module Init + module Rails + class Railtie < ::Rails::Railtie + initializer 'treblle.install_middleware' do |app| + app.config.middleware.insert_after ActionDispatch::ShowExceptions, Treblle::Rails::CaptureExceptions + end + end + end + end +end diff --git a/lib/treblle/response_builder.rb b/lib/treblle/response_builder.rb index 2f087c6..1c75e0e 100644 --- a/lib/treblle/response_builder.rb +++ b/lib/treblle/response_builder.rb @@ -18,14 +18,14 @@ def build private - attr_reader :rack_response + attr_reader :rack_response, :handle_errors def apply_to_response(response) status, headers, response_data = rack_response || [500, [], nil] response.status = status response.headers = headers - response.body = parse_body(response_data) + response.body = parse_body(response_data) || parse_error_body(response_data) response.size = calculate_size(response.body, response.headers) response end @@ -40,6 +40,16 @@ def parse_body(response_data) return nil unless response_data.respond_to?(:body) JSON.parse(response_data.body) + rescue JSON::ParserError + response_data.body + end + + def parse_error_body(response_data) + return nil unless response_data.is_a?(Array) && !response_data.empty? + + JSON.parse(response_data.first) + rescue JSON::ParserError + response_data.body end end end diff --git a/lib/treblle/utils/hash_sanitizer.rb b/lib/treblle/utils/hash_sanitizer.rb index e19d1f7..93c5f35 100644 --- a/lib/treblle/utils/hash_sanitizer.rb +++ b/lib/treblle/utils/hash_sanitizer.rb @@ -4,18 +4,36 @@ module Treblle module Utils class HashSanitizer class << self - def sanitize(hash, sensitive_attrs) - return {} if hash.nil? || hash.empty? - return hash unless hash.is_a?(Hash) || hash.is_a?(Array) + def sanitize(body, sensitive_attrs) + return {} if body.nil? || body.empty? + return hash unless body.is_a?(Hash) || body.is_a?(Array) + if body.is_a?(Hash) + sanitize_hash(body, sensitive_attrs) + elsif body.is_a?(Array) + sanitize_array(body, sensitive_attrs) + end + end + + private + + def sanitize_hash(hash, sensitive_attrs) hash.each_with_object({}) do |(key, value), result| result[key] = if value.is_a?(Hash) || value.is_a?(Array) - sanitize_hash(value, sensitive_attrs) + sanitize(value, sensitive_attrs) else - sensitive_attrs.include?(key.to_s) ? '*' * value.to_s.length : value + sanitize_value(key, value, sensitive_attrs) end end end + + def sanitize_array(array, sensitive_attrs) + array.map { |item| sanitize(item, sensitive_attrs) } + end + + def sanitize_value(key, value, sensitive_attrs) + sensitive_attrs.include?(key.to_s) ? '*' * value.to_s.length : value + end end end end diff --git a/spec/lib/request_builder_spec.rb b/spec/lib/request_builder_spec.rb index 1342b8a..acd3ec9 100644 --- a/spec/lib/request_builder_spec.rb +++ b/spec/lib/request_builder_spec.rb @@ -72,9 +72,9 @@ result = subject.new(env).build - expect(result.body).to eq '{"something":"everything"}' - expect(result.body.encoding).to eq Encoding::UTF_8 - expect(result.body.valid_encoding?).to be true + expect(result.body).to eq({ "something" => "everything" }) + expect(result.body.to_json.encoding).to eq Encoding::UTF_8 + expect(result.body.to_json.valid_encoding?).to be true end end end