appmap 0.42.1 → 0.46.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 +4 -4
- data/.releaserc.yml +11 -0
- data/.travis.yml +33 -2
- data/CHANGELOG.md +44 -0
- data/README.md +74 -16
- data/README_CI.md +29 -0
- data/Rakefile +4 -2
- data/appmap.gemspec +5 -3
- data/lib/appmap.rb +3 -7
- data/lib/appmap/class_map.rb +11 -22
- data/lib/appmap/command/record.rb +1 -1
- data/lib/appmap/config.rb +180 -67
- data/lib/appmap/cucumber.rb +1 -1
- data/lib/appmap/event.rb +46 -27
- data/lib/appmap/handler/function.rb +19 -0
- data/lib/appmap/handler/net_http.rb +107 -0
- data/lib/appmap/handler/rails/request_handler.rb +124 -0
- data/lib/appmap/handler/rails/sql_handler.rb +152 -0
- data/lib/appmap/handler/rails/template.rb +149 -0
- data/lib/appmap/hook.rb +111 -70
- data/lib/appmap/hook/method.rb +6 -8
- data/lib/appmap/middleware/remote_recording.rb +1 -1
- data/lib/appmap/minitest.rb +22 -20
- data/lib/appmap/railtie.rb +5 -5
- data/lib/appmap/record.rb +1 -1
- data/lib/appmap/rspec.rb +22 -21
- data/lib/appmap/trace.rb +47 -6
- data/lib/appmap/util.rb +47 -2
- data/lib/appmap/version.rb +2 -2
- data/package-lock.json +3 -3
- data/release.sh +17 -0
- data/spec/abstract_controller_base_spec.rb +140 -34
- data/spec/class_map_spec.rb +5 -13
- data/spec/config_spec.rb +33 -1
- data/spec/fixtures/hook/custom_instance_method.rb +11 -0
- data/spec/fixtures/hook/method_named_call.rb +11 -0
- data/spec/fixtures/rails5_users_app/Gemfile +7 -3
- data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
- data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails5_users_app/create_app +8 -2
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
- data/spec/fixtures/rails6_users_app/Gemfile +5 -4
- data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
- data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails6_users_app/create_app +8 -2
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
- data/spec/hook_spec.rb +143 -22
- data/spec/record_net_http_spec.rb +160 -0
- data/spec/record_sql_rails_pg_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -0
- data/test/expectations/openssl_test_key_sign1.json +2 -4
- data/test/gem_test.rb +1 -1
- data/test/rspec_test.rb +0 -13
- metadata +20 -14
- data/exe/appmap +0 -154
- data/lib/appmap/rails/request_handler.rb +0 -109
- data/lib/appmap/rails/sql_handler.rb +0 -150
- data/test/cli_test.rb +0 -116
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'appmap/event'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module AppMap
         | 
| 6 | 
            +
              module Handler
         | 
| 7 | 
            +
                module Function
         | 
| 8 | 
            +
                  class << self
         | 
| 9 | 
            +
                    def handle_call(defined_class, hook_method, receiver, args)
         | 
| 10 | 
            +
                      AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def handle_return(call_event_id, elapsed, return_value, exception)
         | 
| 14 | 
            +
                      AppMap::Event::MethodReturn.build_from_invocation(call_event_id, return_value, exception, elapsed: elapsed)
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,107 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'appmap/event'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module AppMap
         | 
| 6 | 
            +
              module Handler
         | 
| 7 | 
            +
                class HTTPClientRequest < AppMap::Event::MethodEvent
         | 
| 8 | 
            +
                  attr_accessor :request_method, :url, :params, :headers
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def initialize(http, request)
         | 
| 11 | 
            +
                    super AppMap::Event.next_id_counter, :call, Thread.current.object_id
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    path, query = request.path.split('?')
         | 
| 14 | 
            +
                    query ||= ''
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    protocol = http.use_ssl? ? 'https' : 'http'
         | 
| 17 | 
            +
                    port = if http.use_ssl? && http.port == 443
         | 
| 18 | 
            +
                      nil
         | 
| 19 | 
            +
                    elsif !http.use_ssl? && http.port == 80
         | 
| 20 | 
            +
                      nil
         | 
| 21 | 
            +
                    else
         | 
| 22 | 
            +
                      ":#{http.port}"
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    url = [ protocol, '://', http.address, port, path ].compact.join
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    self.request_method = request.method
         | 
| 28 | 
            +
                    self.url = url
         | 
| 29 | 
            +
                    self.headers = AppMap::Util.select_headers(NetHTTP.request_headers(request))
         | 
| 30 | 
            +
                    self.params = Rack::Utils.parse_nested_query(query)
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def to_h
         | 
| 34 | 
            +
                    super.tap do |h|
         | 
| 35 | 
            +
                      h[:http_client_request] = {
         | 
| 36 | 
            +
                        request_method: request_method,
         | 
| 37 | 
            +
                        url: url,
         | 
| 38 | 
            +
                        headers: headers
         | 
| 39 | 
            +
                      }.compact
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                      unless params.blank?
         | 
| 42 | 
            +
                        h[:message] = params.keys.map do |key|
         | 
| 43 | 
            +
                          val = params[key]
         | 
| 44 | 
            +
                          {
         | 
| 45 | 
            +
                            name: key,
         | 
| 46 | 
            +
                            class: val.class.name,
         | 
| 47 | 
            +
                            value: self.class.display_string(val),
         | 
| 48 | 
            +
                            object_id: val.__id__,
         | 
| 49 | 
            +
                          }
         | 
| 50 | 
            +
                        end
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                class HTTPClientResponse < AppMap::Event::MethodReturnIgnoreValue
         | 
| 57 | 
            +
                  attr_accessor :status, :mime_type, :headers
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  def initialize(response, parent_id, elapsed)
         | 
| 60 | 
            +
                    super AppMap::Event.next_id_counter, :return, Thread.current.object_id
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    self.status = response.code.to_i
         | 
| 63 | 
            +
                    self.parent_id = parent_id
         | 
| 64 | 
            +
                    self.elapsed = elapsed
         | 
| 65 | 
            +
                    self.headers = AppMap::Util.select_headers(NetHTTP.response_headers(response))
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def to_h
         | 
| 69 | 
            +
                    super.tap do |h|
         | 
| 70 | 
            +
                      h[:http_client_response] = {
         | 
| 71 | 
            +
                        status_code: status,
         | 
| 72 | 
            +
                        mime_type: mime_type,
         | 
| 73 | 
            +
                        headers: headers
         | 
| 74 | 
            +
                      }.compact
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                class NetHTTP
         | 
| 80 | 
            +
                  class << self
         | 
| 81 | 
            +
                    def request_headers(request)
         | 
| 82 | 
            +
                      {}.tap do |headers|
         | 
| 83 | 
            +
                        request.each_header do |k,v|
         | 
| 84 | 
            +
                          key = [ 'HTTP', k.underscore.upcase ].join('_')
         | 
| 85 | 
            +
                          headers[key] = v
         | 
| 86 | 
            +
                        end
         | 
| 87 | 
            +
                      end
         | 
| 88 | 
            +
                    end
         | 
| 89 | 
            +
                
         | 
| 90 | 
            +
                    alias response_headers request_headers
         | 
| 91 | 
            +
                
         | 
| 92 | 
            +
                    def handle_call(defined_class, hook_method, receiver, args)
         | 
| 93 | 
            +
                      # request will call itself again in a start block if it's not already started.
         | 
| 94 | 
            +
                      return unless receiver.started?
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                      http = receiver
         | 
| 97 | 
            +
                      request = args.first
         | 
| 98 | 
            +
                      HTTPClientRequest.new(http, request)
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    def handle_return(call_event_id, elapsed, return_value, exception)
         | 
| 102 | 
            +
                      HTTPClientResponse.new(return_value, call_event_id, elapsed)
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
            end
         | 
| @@ -0,0 +1,124 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'appmap/event'
         | 
| 4 | 
            +
            require 'appmap/hook'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module AppMap
         | 
| 7 | 
            +
              module Handler
         | 
| 8 | 
            +
                module Rails
         | 
| 9 | 
            +
                  module RequestHandler
         | 
| 10 | 
            +
                    class HTTPServerRequest < AppMap::Event::MethodEvent
         | 
| 11 | 
            +
                      attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                      def initialize(request)
         | 
| 14 | 
            +
                        super AppMap::Event.next_id_counter, :call, Thread.current.object_id
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                        self.request_method = request.request_method
         | 
| 17 | 
            +
                        self.normalized_path_info = normalized_path(request)
         | 
| 18 | 
            +
                        self.mime_type = request.headers['Content-Type']
         | 
| 19 | 
            +
                        self.headers = AppMap::Util.select_headers(request.env)
         | 
| 20 | 
            +
                        self.authorization = request.headers['Authorization']
         | 
| 21 | 
            +
                        self.path_info = request.path_info.split('?')[0]
         | 
| 22 | 
            +
                        # ActionDispatch::Http::ParameterFilter is deprecated
         | 
| 23 | 
            +
                        parameter_filter_cls = \
         | 
| 24 | 
            +
                          if defined?(ActiveSupport::ParameterFilter)
         | 
| 25 | 
            +
                            ActiveSupport::ParameterFilter
         | 
| 26 | 
            +
                          else
         | 
| 27 | 
            +
                            ActionDispatch::Http::ParameterFilter
         | 
| 28 | 
            +
                          end
         | 
| 29 | 
            +
                        self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
         | 
| 30 | 
            +
                      end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      def to_h
         | 
| 33 | 
            +
                        super.tap do |h|
         | 
| 34 | 
            +
                          h[:http_server_request] = {
         | 
| 35 | 
            +
                            request_method: request_method,
         | 
| 36 | 
            +
                            path_info: path_info,
         | 
| 37 | 
            +
                            mime_type: mime_type,
         | 
| 38 | 
            +
                            normalized_path_info: normalized_path_info,
         | 
| 39 | 
            +
                            authorization: authorization,
         | 
| 40 | 
            +
                            headers: headers,
         | 
| 41 | 
            +
                          }.compact
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                          unless params.blank?
         | 
| 44 | 
            +
                            h[:message] = params.keys.map do |key|
         | 
| 45 | 
            +
                              val = params[key]
         | 
| 46 | 
            +
                              {
         | 
| 47 | 
            +
                                name: key,
         | 
| 48 | 
            +
                                class: val.class.name,
         | 
| 49 | 
            +
                                value: self.class.display_string(val),
         | 
| 50 | 
            +
                                object_id: val.__id__,
         | 
| 51 | 
            +
                              }.tap do |message|
         | 
| 52 | 
            +
                                properties = object_properties(val)
         | 
| 53 | 
            +
                                message[:properties] = properties if properties
         | 
| 54 | 
            +
                              end
         | 
| 55 | 
            +
                            end
         | 
| 56 | 
            +
                          end
         | 
| 57 | 
            +
                        end
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      private
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      def normalized_path(request, router = ::Rails.application.routes.router)
         | 
| 63 | 
            +
                        router.recognize request do |route, _|
         | 
| 64 | 
            +
                          app = route.app
         | 
| 65 | 
            +
                          next unless app.matches? request
         | 
| 66 | 
            +
                          return normalized_path request, app.rack_app.routes.router if app.engine?
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                          return route.path.spec.to_s
         | 
| 69 | 
            +
                        end
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
         | 
| 74 | 
            +
                      attr_accessor :status, :mime_type, :headers
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                      def initialize(response, parent_id, elapsed)
         | 
| 77 | 
            +
                        super AppMap::Event.next_id_counter, :return, Thread.current.object_id
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                        self.status = response.status
         | 
| 80 | 
            +
                        self.mime_type = response.headers['Content-Type']
         | 
| 81 | 
            +
                        self.parent_id = parent_id
         | 
| 82 | 
            +
                        self.elapsed = elapsed
         | 
| 83 | 
            +
                        self.headers = AppMap::Util.select_headers(response.headers)
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                      def to_h
         | 
| 87 | 
            +
                        super.tap do |h|
         | 
| 88 | 
            +
                          h[:http_server_response] = {
         | 
| 89 | 
            +
                            status_code: status,
         | 
| 90 | 
            +
                            mime_type: mime_type,
         | 
| 91 | 
            +
                            headers: headers
         | 
| 92 | 
            +
                          }.compact
         | 
| 93 | 
            +
                        end
         | 
| 94 | 
            +
                      end
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    class HookMethod < AppMap::Hook::Method
         | 
| 98 | 
            +
                      def initialize
         | 
| 99 | 
            +
                        # ActionController::Instrumentation has issued start_processing.action_controller and
         | 
| 100 | 
            +
                        # process_action.action_controller since Rails 3. Therefore it's a stable place to hook
         | 
| 101 | 
            +
                        # the request. Rails controller notifications can't be used directly because they don't
         | 
| 102 | 
            +
                        # provide response headers, and we want the Content-Type.
         | 
| 103 | 
            +
                        super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
         | 
| 104 | 
            +
                      end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                      protected
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                      def before_hook(receiver, defined_class, _) # args
         | 
| 109 | 
            +
                        call_event = HTTPServerRequest.new(receiver.request)
         | 
| 110 | 
            +
                        # http_server_request events are i/o and do not require a package name.
         | 
| 111 | 
            +
                        AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
         | 
| 112 | 
            +
                        [ call_event, TIME_NOW.call ]
         | 
| 113 | 
            +
                      end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                      def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
         | 
| 116 | 
            +
                        elapsed = TIME_NOW.call - start_time
         | 
| 117 | 
            +
                        return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
         | 
| 118 | 
            +
                        AppMap.tracing.record_event return_event
         | 
| 119 | 
            +
                      end
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
              end
         | 
| 124 | 
            +
            end
         | 
| @@ -0,0 +1,152 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'appmap/event'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module AppMap
         | 
| 6 | 
            +
              module Handler
         | 
| 7 | 
            +
                module Rails
         | 
| 8 | 
            +
                  class SQLHandler
         | 
| 9 | 
            +
                    class SQLCall < AppMap::Event::MethodCall
         | 
| 10 | 
            +
                      attr_accessor :payload
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      def initialize(payload)
         | 
| 13 | 
            +
                        super AppMap::Event.next_id_counter, :call, Thread.current.object_id
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                        self.payload = payload
         | 
| 16 | 
            +
                      end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      def to_h
         | 
| 19 | 
            +
                        super.tap do |h|
         | 
| 20 | 
            +
                          h[:sql_query] = {
         | 
| 21 | 
            +
                            sql: payload[:sql],
         | 
| 22 | 
            +
                            database_type: payload[:database_type]
         | 
| 23 | 
            +
                          }.tap do |sql_query|
         | 
| 24 | 
            +
                            %i[server_version].each do |attribute|
         | 
| 25 | 
            +
                              sql_query[attribute] = payload[attribute] if payload[attribute]
         | 
| 26 | 
            +
                            end
         | 
| 27 | 
            +
                          end
         | 
| 28 | 
            +
                        end
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
         | 
| 33 | 
            +
                      def initialize(parent_id, elapsed)
         | 
| 34 | 
            +
                        super AppMap::Event.next_id_counter, :return, Thread.current.object_id
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                        self.parent_id = parent_id
         | 
| 37 | 
            +
                        self.elapsed = elapsed
         | 
| 38 | 
            +
                      end
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    module SQLExaminer
         | 
| 42 | 
            +
                      class << self
         | 
| 43 | 
            +
                        def examine(payload, sql:)
         | 
| 44 | 
            +
                          return unless (examiner = build_examiner)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                          payload[:server_version] = examiner.server_version
         | 
| 47 | 
            +
                          payload[:database_type] = examiner.database_type.to_s
         | 
| 48 | 
            +
                        end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                        protected
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                        def build_examiner
         | 
| 53 | 
            +
                          if defined?(Sequel)
         | 
| 54 | 
            +
                            SequelExaminer.new
         | 
| 55 | 
            +
                          elsif defined?(ActiveRecord)
         | 
| 56 | 
            +
                            ActiveRecordExaminer.new
         | 
| 57 | 
            +
                          end
         | 
| 58 | 
            +
                        end
         | 
| 59 | 
            +
                      end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                      class SequelExaminer
         | 
| 62 | 
            +
                        def server_version
         | 
| 63 | 
            +
                          Sequel::Model.db.server_version
         | 
| 64 | 
            +
                        end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                        def database_type
         | 
| 67 | 
            +
                          Sequel::Model.db.database_type.to_sym
         | 
| 68 | 
            +
                        end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                        def execute_query(sql)
         | 
| 71 | 
            +
                          Sequel::Model.db[sql].all
         | 
| 72 | 
            +
                        end
         | 
| 73 | 
            +
                      end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                      class ActiveRecordExaminer
         | 
| 76 | 
            +
                        @@db_version_warning_issued = {}
         | 
| 77 | 
            +
                        
         | 
| 78 | 
            +
                        def issue_warning
         | 
| 79 | 
            +
                          db_type = database_type
         | 
| 80 | 
            +
                          return if @@db_version_warning_issued[db_type]
         | 
| 81 | 
            +
                          warn("AppMap: Unable to determine database version for #{db_type.inspect}") 
         | 
| 82 | 
            +
                          @@db_version_warning_issued[db_type] = true
         | 
| 83 | 
            +
                        end
         | 
| 84 | 
            +
                        
         | 
| 85 | 
            +
                        def server_version
         | 
| 86 | 
            +
                          ActiveRecord::Base.connection.try(:database_version) || issue_warning
         | 
| 87 | 
            +
                        end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                        def database_type
         | 
| 90 | 
            +
                          type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
         | 
| 91 | 
            +
                          type = :postgres if type == :postgresql
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                          type
         | 
| 94 | 
            +
                        end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                        def execute_query(sql)
         | 
| 97 | 
            +
                          ActiveRecord::Base.connection.execute(sql).inject([]) { |memo, r| memo << r; memo }
         | 
| 98 | 
            +
                        end
         | 
| 99 | 
            +
                      end
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
         | 
| 103 | 
            +
                      return if AppMap.tracing.empty?
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                      reentry_key = "#{self.class.name}#call"
         | 
| 106 | 
            +
                      return if Thread.current[reentry_key] == true
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                      Thread.current[reentry_key] = true
         | 
| 109 | 
            +
                      begin
         | 
| 110 | 
            +
                        sql = payload[:sql].strip
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                        # Detect whether a function call within a specified filename is present in the call stack.
         | 
| 113 | 
            +
                        find_in_backtrace = lambda do |file_name, function_name = nil|
         | 
| 114 | 
            +
                          Thread.current.backtrace.find do |line|
         | 
| 115 | 
            +
                            tokens = line.split(':')
         | 
| 116 | 
            +
                            matches_file = tokens.find { |t| t.rindex(file_name) == (t.length - file_name.length) }
         | 
| 117 | 
            +
                            matches_function = function_name.nil? || tokens.find { |t| t == "in `#{function_name}'" }
         | 
| 118 | 
            +
                            matches_file && matches_function
         | 
| 119 | 
            +
                          end
         | 
| 120 | 
            +
                        end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                        # Ignore SQL calls which are made while establishing a new connection.
         | 
| 123 | 
            +
                        #
         | 
| 124 | 
            +
                        # Example:
         | 
| 125 | 
            +
                        # /path/to/ruby/2.6.0/gems/sequel-5.20.0/lib/sequel/connection_pool.rb:122:in `make_new'
         | 
| 126 | 
            +
                        return if find_in_backtrace.call('lib/sequel/connection_pool.rb', 'make_new')
         | 
| 127 | 
            +
                        # lib/active_record/connection_adapters/abstract/connection_pool.rb:811:in `new_connection'
         | 
| 128 | 
            +
                        return if find_in_backtrace.call('lib/active_record/connection_adapters/abstract/connection_pool.rb', 'new_connection')
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                        # Ignore SQL calls which are made while inspecting the DB schema.
         | 
| 131 | 
            +
                        #
         | 
| 132 | 
            +
                        # Example:
         | 
| 133 | 
            +
                        # /path/to/ruby/2.6.0/gems/sequel-5.20.0/lib/sequel/model/base.rb:812:in `get_db_schema'
         | 
| 134 | 
            +
                        return if find_in_backtrace.call('lib/sequel/model/base.rb', 'get_db_schema')
         | 
| 135 | 
            +
                        # /usr/local/bundle/gems/activerecord-5.2.3/lib/active_record/model_schema.rb:466:in `load_schema!'
         | 
| 136 | 
            +
                        return if find_in_backtrace.call('lib/active_record/model_schema.rb', 'load_schema!')
         | 
| 137 | 
            +
                        return if find_in_backtrace.call('lib/active_model/attribute_methods.rb', 'define_attribute_methods')
         | 
| 138 | 
            +
                        return if find_in_backtrace.call('lib/active_record/connection_adapters/schema_cache.rb')
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                        SQLExaminer.examine payload, sql: sql
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                        call = SQLCall.new(payload)
         | 
| 143 | 
            +
                        AppMap.tracing.record_event(call)
         | 
| 144 | 
            +
                        AppMap.tracing.record_event(SQLReturn.new(call.id, finished - started))
         | 
| 145 | 
            +
                      ensure
         | 
| 146 | 
            +
                        Thread.current[reentry_key] = nil
         | 
| 147 | 
            +
                      end
         | 
| 148 | 
            +
                    end
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
              end
         | 
| 152 | 
            +
            end
         | 
| @@ -0,0 +1,149 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'appmap/event'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module AppMap
         | 
| 6 | 
            +
              module Handler
         | 
| 7 | 
            +
                module Rails
         | 
| 8 | 
            +
                  class Template
         | 
| 9 | 
            +
                    LOG = (ENV['APPMAP_TEMPLATE_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    # All the code which is touched by the AppMap is recorded in the classMap.
         | 
| 12 | 
            +
                    # This duck-typed 'method' is used to represent a view template as a package, 
         | 
| 13 | 
            +
                    # class, and method in the classMap.
         | 
| 14 | 
            +
                    # The class name is generated from the template path. The package name is
         | 
| 15 | 
            +
                    # 'app/views', and the method name is 'render'. The source location of the method
         | 
| 16 | 
            +
                    # is, of course, the path to the view template.
         | 
| 17 | 
            +
                    TemplateMethod = Struct.new(:path) do
         | 
| 18 | 
            +
                      private_instance_methods :path
         | 
| 19 | 
            +
                      attr_reader :class_name
         | 
| 20 | 
            +
             
         | 
| 21 | 
            +
                      def initialize(path)
         | 
| 22 | 
            +
                        super
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                        @class_name = path.parameterize.underscore
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
              
         | 
| 27 | 
            +
                      def package
         | 
| 28 | 
            +
                        'app/views'
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
              
         | 
| 31 | 
            +
                      def name
         | 
| 32 | 
            +
                        'render'
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
              
         | 
| 35 | 
            +
                      def source_location
         | 
| 36 | 
            +
                        path
         | 
| 37 | 
            +
                      end
         | 
| 38 | 
            +
              
         | 
| 39 | 
            +
                      def static
         | 
| 40 | 
            +
                        true
         | 
| 41 | 
            +
                      end
         | 
| 42 | 
            +
              
         | 
| 43 | 
            +
                      def comment
         | 
| 44 | 
            +
                        nil
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
              
         | 
| 47 | 
            +
                      def labels
         | 
| 48 | 
            +
                        [ 'mvc.template' ]
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
              
         | 
| 52 | 
            +
                    # TemplateCall is a type of function call which is specialized to view template rendering. Since
         | 
| 53 | 
            +
                    # there isn't really a perfect method in Rails to hook, this one is synthesized from the available
         | 
| 54 | 
            +
                    # information. 
         | 
| 55 | 
            +
                    class TemplateCall < AppMap::Event::MethodEvent
         | 
| 56 | 
            +
                      # This is basically the +self+ parameter.
         | 
| 57 | 
            +
                      attr_reader :render_instance
         | 
| 58 | 
            +
                      # Path to the view template.
         | 
| 59 | 
            +
                      attr_accessor :path
         | 
| 60 | 
            +
              
         | 
| 61 | 
            +
                      def initialize(render_instance)
         | 
| 62 | 
            +
                        super :call
         | 
| 63 | 
            +
              
         | 
| 64 | 
            +
                        AppMap::Event::MethodEvent.build_from_invocation(:call, event: self)
         | 
| 65 | 
            +
                        @render_instance = render_instance
         | 
| 66 | 
            +
                      end
         | 
| 67 | 
            +
              
         | 
| 68 | 
            +
                      def static?
         | 
| 69 | 
            +
                        true
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
                
         | 
| 72 | 
            +
                      def to_h
         | 
| 73 | 
            +
                        super.tap do |h|
         | 
| 74 | 
            +
                          h[:defined_class] = path ? path.parameterize.underscore : 'inline_template'
         | 
| 75 | 
            +
                          h[:method_id] = 'render'
         | 
| 76 | 
            +
                          h[:path] = path
         | 
| 77 | 
            +
                          h[:static] = static?
         | 
| 78 | 
            +
                          h[:parameters] = []
         | 
| 79 | 
            +
                          h[:receiver] = {
         | 
| 80 | 
            +
                            class: AppMap::Event::MethodEvent.best_class_name(render_instance),
         | 
| 81 | 
            +
                            object_id: render_instance.__id__,
         | 
| 82 | 
            +
                            value: AppMap::Event::MethodEvent.display_string(render_instance)
         | 
| 83 | 
            +
                          }
         | 
| 84 | 
            +
                          h.compact
         | 
| 85 | 
            +
                        end
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             
         | 
| 89 | 
            +
                    TEMPLATE_RENDERER = 'appmap.handler.rails.template.renderer'
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    # Hooks the ActionView::Resolver methods +find_all+, +find_all_anywhere+. The resolver is used
         | 
| 92 | 
            +
                    # during template rendering to lookup the template file path from parameters such as the
         | 
| 93 | 
            +
                    # template name, prefix, and partial (boolean).
         | 
| 94 | 
            +
                    class ResolverHandler
         | 
| 95 | 
            +
                      class << self
         | 
| 96 | 
            +
                        # Handled as a normal function call.
         | 
| 97 | 
            +
                        def handle_call(defined_class, hook_method, receiver, args)
         | 
| 98 | 
            +
                          name, prefix, partial = args
         | 
| 99 | 
            +
                          warn "Resolver: #{{ name: name, prefix: prefix, partial: partial }}" if LOG
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                          AppMap::Handler::Function.handle_call(defined_class, hook_method, receiver, args)
         | 
| 102 | 
            +
                        end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                        # When the resolver returns, look to see if there is template rendering underway.
         | 
| 105 | 
            +
                        # If so, populate the template path. In all cases, add a TemplateMethod so that the
         | 
| 106 | 
            +
                        # template will be recorded in the classMap.
         | 
| 107 | 
            +
                        def handle_return(call_event_id, elapsed, return_value, exception)
         | 
| 108 | 
            +
                          warn "Resolver return: #{return_value.inspect}" if LOG
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                          renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
         | 
| 111 | 
            +
                          path = Array(return_value).first&.inspect
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                          if path
         | 
| 114 | 
            +
                            AppMap.tracing.record_method(TemplateMethod.new(path))
         | 
| 115 | 
            +
                            renderer.path ||= path if renderer
         | 
| 116 | 
            +
                          end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                          AppMap::Handler::Function.handle_return(call_event_id, elapsed, return_value, exception)
         | 
| 119 | 
            +
                        end
         | 
| 120 | 
            +
                      end
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    # Hooks the ActionView::Renderer method +render+. This method is used by Rails to perform
         | 
| 124 | 
            +
                    # template rendering. The TemplateCall event which is emitted by this handler has a
         | 
| 125 | 
            +
                    # +path+ parameter, which is nil until it's filled in by a ResolverHandler. 
         | 
| 126 | 
            +
                    class RenderHandler
         | 
| 127 | 
            +
                      class << self
         | 
| 128 | 
            +
                        def handle_call(defined_class, hook_method, receiver, args)
         | 
| 129 | 
            +
                          context, options = args
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                          warn "Renderer: #{options}" if LOG
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                          TemplateCall.new(receiver).tap do |call|
         | 
| 134 | 
            +
                            Thread.current[TEMPLATE_RENDERER] ||= []
         | 
| 135 | 
            +
                            Thread.current[TEMPLATE_RENDERER] << call
         | 
| 136 | 
            +
                          end
         | 
| 137 | 
            +
                        end
         | 
| 138 | 
            +
              
         | 
| 139 | 
            +
                        def handle_return(call_event_id, elapsed, return_value, exception)
         | 
| 140 | 
            +
                          Array(Thread.current[TEMPLATE_RENDERER]).pop
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                          AppMap::Event::MethodReturnIgnoreValue.build_from_invocation(call_event_id, elapsed: elapsed)
         | 
| 143 | 
            +
                        end
         | 
| 144 | 
            +
                      end
         | 
| 145 | 
            +
                    end
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
              end
         | 
| 149 | 
            +
            end
         |