rails_spotlight 0.1.7 → 0.2.1

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Guardfile +1 -0
  4. data/README.md +35 -13
  5. data/chrome_ext_private_policy.md +39 -0
  6. data/docker-compose.yml +0 -4
  7. data/fake_spec_res/rails_spotlight_spec.rb +8 -8
  8. data/lib/rails_spotlight/app_notifications.rb +88 -0
  9. data/lib/rails_spotlight/app_request.rb +20 -0
  10. data/lib/rails_spotlight/channels/live_console_channel.rb +91 -0
  11. data/lib/rails_spotlight/channels/request_completed_channel.rb +15 -0
  12. data/lib/rails_spotlight/channels.rb +8 -0
  13. data/lib/rails_spotlight/configuration.rb +96 -0
  14. data/lib/rails_spotlight/event.rb +93 -0
  15. data/lib/rails_spotlight/log_interceptor.rb +47 -0
  16. data/lib/rails_spotlight/middlewares/concerns/skip_request_paths.rb +35 -0
  17. data/lib/rails_spotlight/middlewares/handlers/file_action_handler.rb +13 -6
  18. data/lib/rails_spotlight/middlewares/handlers/meta_action_handler.rb +28 -0
  19. data/lib/rails_spotlight/middlewares/handlers/sql_action_handler.rb +3 -5
  20. data/lib/rails_spotlight/middlewares/handlers/verify_action_handler.rb +4 -1
  21. data/lib/rails_spotlight/middlewares/header_marker.rb +33 -0
  22. data/lib/rails_spotlight/middlewares/main_request_handler.rb +35 -0
  23. data/lib/rails_spotlight/middlewares/request_completed.rb +71 -0
  24. data/lib/rails_spotlight/middlewares/request_handler.rb +2 -0
  25. data/lib/rails_spotlight/middlewares.rb +10 -0
  26. data/lib/rails_spotlight/railtie.rb +41 -12
  27. data/lib/rails_spotlight/storage.rb +47 -0
  28. data/lib/rails_spotlight/utils.rb +28 -0
  29. data/lib/rails_spotlight/version.rb +1 -1
  30. data/lib/rails_spotlight.rb +22 -3
  31. data/lib/tasks/init.rake +36 -0
  32. metadata +83 -10
  33. data/lib/rails_spotlight/support/project.rb +0 -21
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module Middlewares
5
+ module SkipRequestPaths
6
+ PATHS_TO_SKIP = %w[/__better_errors /__rails_spotlight /__meta_request].freeze
7
+
8
+ private
9
+
10
+ def skip?(path)
11
+ skip_paths.any? { |skip_path| path.start_with?(skip_path) } || asset?(path)
12
+ end
13
+
14
+ def default_skip_paths
15
+ PATHS_TO_SKIP
16
+ end
17
+
18
+ def additional_skip_paths
19
+ []
20
+ end
21
+
22
+ def skip_paths
23
+ additional_skip_paths + default_skip_paths + ::RailsSpotlight.config.middleware_skipped_paths
24
+ end
25
+
26
+ def asset?(path)
27
+ app_config.respond_to?(:assets) && path.start_with?(assets_prefix)
28
+ end
29
+
30
+ def assets_prefix
31
+ "/#{app_config.assets.prefix[%r{\A/?(.*?)/?\z}, 1]}/"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../support/project'
4
-
5
3
  module RailsSpotlight
6
4
  module Middlewares
7
5
  module Handlers
@@ -9,7 +7,7 @@ module RailsSpotlight
9
7
  def execute
10
8
  raise NotFound, 'File not found' unless path_valid?
11
9
 
12
- File.write(file_path, json_request_body.fetch('content')) if write_mode?
10
+ File.write(file_path, new_content) if write_mode?
13
11
  rescue => e # rubocop:disable Style/RescueStandardError
14
12
  raise UnprocessableEntity, e.message
15
13
  end
@@ -20,11 +18,16 @@ module RailsSpotlight
20
18
  File.read(file_path)
21
19
  end
22
20
 
21
+ def new_content
22
+ json_request_body.fetch('content')
23
+ end
24
+
23
25
  def json_response_body
24
26
  {
25
27
  source: text_response_body,
26
- project: ::RailsSpotlight::Support::Project.instance.name
27
- }
28
+ changed: write_mode?,
29
+ project: ::RailsSpotlight.config.project_name
30
+ }.merge(write_mode? ? { new_content: new_content } : {})
28
31
  end
29
32
 
30
33
  def write_mode?
@@ -40,7 +43,11 @@ module RailsSpotlight
40
43
  end
41
44
 
42
45
  def file_path
43
- @file_path ||= Rails.root.join(json_request_body.fetch('file'))
46
+ @file_path ||= if json_request_body.fetch('file').start_with?(::RailsSpotlight.config.rails_root)
47
+ json_request_body.fetch('file')
48
+ else
49
+ File.join(::RailsSpotlight.config.rails_root, json_request_body.fetch('file'))
50
+ end
44
51
  end
45
52
  end
46
53
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module Middlewares
5
+ module Handlers
6
+ class MetaActionHandler < BaseActionHandler
7
+ def execute; end
8
+
9
+ private
10
+
11
+ def json_response_body
12
+ {
13
+ events: events,
14
+ project: ::RailsSpotlight.config.project_name
15
+ }
16
+ end
17
+
18
+ def id
19
+ @id ||= request.params['id']
20
+ end
21
+
22
+ def events
23
+ @events ||= Storage.new(id).read || []
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../support/project'
4
-
5
3
  module RailsSpotlight
6
4
  module Middlewares
7
5
  module Handlers
@@ -20,9 +18,9 @@ module RailsSpotlight
20
18
  def validate_project!
21
19
  Rails.logger.warn required_projects
22
20
  return if required_projects.blank?
23
- return if required_projects.include?(::RailsSpotlight::Support::Project.instance.name)
21
+ return if required_projects.include?(::RailsSpotlight.config.project_name)
24
22
 
25
- raise UnprocessableEntity, "Check your connetction settings the current query is not allowed to be executed on the #{::RailsSpotlight::Support::Project.instance.name} project"
23
+ raise UnprocessableEntity, "Check your connetction settings the current query is not allowed to be executed on the #{::RailsSpotlight.config.project_name} project"
26
24
  end
27
25
 
28
26
  def transaction
@@ -59,7 +57,7 @@ module RailsSpotlight
59
57
  logs: logs,
60
58
  error: error.present? ? error.inspect : nil,
61
59
  query_mode: force_execution? ? 'force' : 'default',
62
- project: ::RailsSpotlight::Support::Project.instance.name
60
+ project: ::RailsSpotlight.config.project_name
63
61
  }
64
62
  end
65
63
 
@@ -18,7 +18,10 @@ module RailsSpotlight
18
18
  body: request.body.read,
19
19
  content_type: request.content_type,
20
20
  request_method: request.request_method,
21
- version: request.get_header('HTTP_X_RAILS_SPOTLIGHT')
21
+ version: request.get_header('HTTP_X_RAILS_SPOTLIGHT'),
22
+ current_gem_version: ::RailsSpotlight::VERSION,
23
+ project: ::RailsSpotlight.config.project_name,
24
+ action_cable_path: defined?(ActionCable) ? ActionCable&.server&.config&.mount_path : nil
22
25
  }
23
26
  end
24
27
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/contrib/response_headers'
4
+ require_relative 'concerns/skip_request_paths'
5
+
6
+ module RailsSpotlight
7
+ module Middlewares
8
+ class HeaderMarker
9
+ include ::RailsSpotlight::Middlewares::SkipRequestPaths
10
+
11
+ def initialize(app, app_config)
12
+ @app = app
13
+ @app_config = app_config
14
+ end
15
+
16
+ def call(env)
17
+ request_path = env['PATH_INFO']
18
+ middleware = Rack::ResponseHeaders.new(app) do |headers|
19
+ headers['X-Rails-Spotlight-Version'] = RailsSpotlight::VERSION unless skip?(request_path)
20
+ end
21
+ middleware.call(env)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :app, :app_config
27
+
28
+ def default_skip_paths
29
+ %w[/__better_errors]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RailsSpotlight
6
+ module Middlewares
7
+ class MainRequestHandler
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ attr_reader :app
13
+
14
+ def call(env)
15
+ app_request = AppRequest.new env['action_dispatch.request_id']
16
+ app_request.current!
17
+ app.call(env)
18
+ rescue StandardError => e
19
+ if defined?(ActionDispatch::ExceptionWrapper)
20
+ wrapper = if ActionDispatch::ExceptionWrapper.method_defined? :env
21
+ ActionDispatch::ExceptionWrapper.new(env, e)
22
+ else
23
+ ActionDispatch::ExceptionWrapper.new(env['action_dispatch.backtrace_cleaner'], e)
24
+ end
25
+ app_request.events.push(*Event.events_for_exception(wrapper))
26
+ else
27
+ app_request.events.push(*Event.events_for_exception(e))
28
+ end
29
+ raise
30
+ ensure
31
+ Storage.new(app_request.id).write(app_request.events.to_json) unless app_request.events.empty?
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/skip_request_paths'
4
+
5
+ module RailsSpotlight
6
+ module Middlewares
7
+ class RequestCompleted
8
+ include ::RailsSpotlight::Middlewares::SkipRequestPaths
9
+
10
+ def initialize(app, app_config)
11
+ @app = app
12
+ @app_config = app_config
13
+ end
14
+
15
+ def call(env)
16
+ if skip?(env['PATH_INFO']) || (env['HTTP_CONNECTION'] == 'Upgrade' && env['HTTP_UPGRADE'] == 'websocket')
17
+ app.call(env)
18
+ else
19
+ status, headers, body = app.call(env)
20
+ publish_event(status, headers, env)
21
+ [status, headers, body]
22
+ end
23
+ rescue => e # rubocop:disable Style/RescueStandardError
24
+ ::RailsSpotlight.config.logger.error "Error in RailsSpotlight::Middlewares::RequestCompletedHandler instrumentation: #{e.message}"
25
+ app.call(env)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :app, :app_config
31
+
32
+ def rails_spotlight_request_id
33
+ Thread.current[:rails_spotlight_request_id]&.id
34
+ end
35
+
36
+ def publish_event(status, _headers, env)
37
+ return if status < 100
38
+ return unless rails_spotlight_request_id
39
+
40
+ request = ActionDispatch::Request.new(env)
41
+
42
+ host, url = host_and_url(env)
43
+ ActionCable.server.broadcast(
44
+ 'rails_spotlight_request_completed_channel',
45
+ {
46
+ rails_spotlight_version: RailsSpotlight::VERSION,
47
+ id: rails_spotlight_request_id,
48
+ http_method: env['REQUEST_METHOD'],
49
+ host: host,
50
+ url: url,
51
+ format: request.format.symbol,
52
+ controller: request.path_parameters[:controller],
53
+ action: request.path_parameters[:action]
54
+ }
55
+ )
56
+ end
57
+
58
+ def host_and_url(env)
59
+ scheme = env['rack.url_scheme']
60
+ host = env['HTTP_HOST']
61
+ path = env['PATH_INFO']
62
+ query_string = env['QUERY_STRING']
63
+
64
+ host_url = "#{scheme}://#{host}"
65
+ full_url = "#{host_url}#{path}"
66
+ full_url += "?#{query_string}" unless query_string.empty?
67
+ [host_url, full_url]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -5,6 +5,7 @@ require_relative 'handlers/file_action_handler'
5
5
  require_relative 'handlers/sql_action_handler'
6
6
  require_relative 'handlers/verify_action_handler'
7
7
  require_relative 'handlers/not_found_action_handler'
8
+ require_relative 'handlers/meta_action_handler'
8
9
 
9
10
  module RailsSpotlight
10
11
  module Middlewares
@@ -31,6 +32,7 @@ module RailsSpotlight
31
32
  when 'file' then Handlers::FileActionHandler.new(*args).call
32
33
  when 'sql' then Handlers::SqlActionHandler.new(*args).call
33
34
  when 'verify' then Handlers::VerifyActionHandler.new(*args).call
35
+ when 'meta' then Handlers::MetaActionHandler.new(*args).call
34
36
  else
35
37
  Handlers::NotFoundActionHandler.new(*args).call
36
38
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module Middlewares
5
+ autoload :RequestHandler, 'rails_spotlight/middlewares/request_handler'
6
+ autoload :RequestCompleted, 'rails_spotlight/middlewares/request_completed'
7
+ autoload :HeaderMarker, 'rails_spotlight/middlewares/header_marker'
8
+ autoload :MainRequestHandler, 'rails_spotlight/middlewares/main_request_handler'
9
+ end
10
+ end
@@ -1,28 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/railtie'
4
- require 'meta_request'
5
4
 
6
5
  module RailsSpotlight
7
6
  class Railtie < ::Rails::Railtie
8
7
  initializer 'rails_spotlight.inject_middlewares' do
9
- insert_middleware unless Rails.env.production?
8
+ insert_base_middlewares unless Rails.env.production?
10
9
  end
11
10
 
12
- # initializer 'rails_spotlight.log_interceptor' do
13
- # Rails.logger&.extend(LogInterceptor)
14
- # end
15
- #
16
- # initializer 'rails_spotlight.subscribe_to_notifications' do
17
- # AppNotifications.subscribe
18
- # end
11
+ initializer 'rails_spotlight.log_interceptor' do
12
+ Rails.logger&.extend(LogInterceptor) unless Rails.env.production?
13
+ end
14
+
15
+ initializer 'rails_spotlight.subscribe_to_notifications' do
16
+ AppNotifications.subscribe unless Rails.env.production?
17
+ end
18
+
19
+ initializer 'rails_spotlight.action_cable_setup' do
20
+ insert_action_cable_helpers unless Rails.env.production?
21
+ end
22
+
23
+ def insert_action_cable_helpers
24
+ return unless ::RailsSpotlight.config.action_cable_present?
25
+
26
+ app.config.after_initialize do
27
+ update_actioncable_allowed_request_origins!
28
+
29
+ require 'rails_spotlight/channels/request_completed_channel' if ::RailsSpotlight.config.request_completed_broadcast_enabled?
30
+ require 'rails_spotlight/channels/live_console_channel' if ::RailsSpotlight.config.live_console_enabled?
31
+
32
+ app.routes.draw { mount ActionCable.server => '/cable' } if ::RailsSpotlight.config.auto_mount_action_cable?
33
+ end
34
+ end
35
+
36
+ def update_actioncable_allowed_request_origins!
37
+ existing_origins = Array(app.config.action_cable.allowed_request_origins)
38
+ app.config.action_cable.allowed_request_origins = existing_origins | [%r{\Achrome-extension://.*\z}]
39
+ end
40
+
41
+ def insert_base_middlewares
42
+ app.middleware.use ::RailsSpotlight::Middlewares::RequestHandler
19
43
 
20
- def insert_middleware
21
44
  if defined? ActionDispatch::DebugExceptions
22
- app.middleware.insert_before ActionDispatch::DebugExceptions, RailsSpotlight::Middlewares::RequestHandler
45
+ app.middleware.insert_before ActionDispatch::DebugExceptions, ::RailsSpotlight::Middlewares::HeaderMarker, app.config
23
46
  else
24
- app.middleware.use RailsSpotlight::Middlewares::RequestHandler
47
+ app.middleware.use ::RailsSpotlight::Middlewares::HeaderMarker, app.config
25
48
  end
49
+
50
+ app.middleware.use ::RailsSpotlight::Middlewares::MainRequestHandler
51
+
52
+ return unless ::RailsSpotlight.config.request_completed_broadcast_enabled?
53
+
54
+ app.middleware.insert_after ::RailsSpotlight::Middlewares::HeaderMarker, RailsSpotlight::Middlewares::RequestCompleted, app.config
26
55
  end
27
56
 
28
57
  def app
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ class Storage
5
+ attr_reader :key
6
+
7
+ def initialize(key)
8
+ @key = key
9
+ end
10
+
11
+ def write(value)
12
+ FileUtils.mkdir_p dir_path
13
+ # Use File.binwrite instead File.open(json_file, 'wb') { |file| file.write(value) }
14
+ File.binwrite(json_file, value)
15
+ maintain_file_pool(RailsSpotlight.config.storage_pool_size)
16
+ end
17
+
18
+ def read
19
+ # avoid FileNotFound error
20
+ File.exist?(json_file) ? File.read(json_file) : '[]'
21
+ end
22
+
23
+ private
24
+
25
+ def maintain_file_pool(size)
26
+ files = Dir["#{dir_path}/*.json"]
27
+ files = files.sort_by { |f| -file_ctime(f) }
28
+ (files[size..] || []).each do |file|
29
+ FileUtils.rm_f(file)
30
+ end
31
+ end
32
+
33
+ def file_ctime(file)
34
+ File.stat(file).ctime.to_i
35
+ rescue Errno::ENOENT
36
+ 0
37
+ end
38
+
39
+ def json_file
40
+ File.join(dir_path, "#{key}.json")
41
+ end
42
+
43
+ def dir_path
44
+ @dir_path ||= RailsSpotlight.config.storage_path
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module Utils
5
+ module_function
6
+
7
+ def dev_callsite(caller)
8
+ app_line = caller.detect { |c| c.start_with? RailsSpotlight.config.rails_root }
9
+ return nil unless app_line
10
+
11
+ _, filename, _, line, _, method = app_line.split(/^(.*?)(:(\d+))(:in `(.*)')?$/)
12
+
13
+ {
14
+ filename: sub_source_path(filename),
15
+ line: line.to_i,
16
+ method: method
17
+ }
18
+ end
19
+
20
+ def sub_source_path(path)
21
+ rails_root = RailsSpotlight.config.rails_root
22
+ source_path = RailsSpotlight.config.source_path
23
+ return path if rails_root == source_path
24
+
25
+ path.sub(rails_root, source_path)
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsSpotlight
4
- VERSION = '0.1.7'
4
+ VERSION = '0.2.1'
5
5
  end
@@ -1,9 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'rails_spotlight/version'
4
- require_relative 'rails_spotlight/middlewares/request_handler'
5
-
6
3
  module RailsSpotlight
4
+ autoload :VERSION, 'rails_spotlight/version'
5
+ autoload :Configuration, 'rails_spotlight/configuration'
6
+ autoload :Storage, 'rails_spotlight/storage'
7
+ autoload :Event, 'rails_spotlight/event'
8
+ autoload :AppRequest, 'rails_spotlight/app_request'
9
+ autoload :Middlewares, 'rails_spotlight/middlewares'
10
+ autoload :LogInterceptor, 'rails_spotlight/log_interceptor'
11
+ autoload :AppNotifications, 'rails_spotlight/app_notifications'
12
+ autoload :Utils, 'rails_spotlight/utils'
13
+
14
+ class << self
15
+ def config
16
+ @config ||= Configuration.load_config
17
+ end
18
+ end
19
+
20
+ autoload :Channels, 'rails_spotlight/channels'
7
21
  end
8
22
 
9
23
  require_relative 'rails_spotlight/railtie'
24
+
25
+ if defined?(Rake)
26
+ spec = Gem::Specification.find_by_name 'rails_spotlight'
27
+ load "#{spec.gem_dir}/lib/tasks/init.rake"
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+
5
+ namespace :rails_spotlight do
6
+ desc 'Generate rails_spotlight configuration file'
7
+ task generate_config: :environment do
8
+ require 'fileutils'
9
+
10
+ config_path = Rails.root.join('config', 'rails_spotlight.yml')
11
+
12
+ default_config = <<~YAML
13
+ # Default configuration for RailsSpotlight
14
+ PROJECT_NAME: <%=Rails.application.class.respond_to?(:module_parent_name) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name%>
15
+ SOURCE_PATH: <%=Rails.root%>
16
+ STORAGE_PATH: <%=Rails.root.join('tmp', 'data', 'rails_spotlight')%>
17
+ STORAGE_POOL_SIZE: 20
18
+ LOGGER: <%=Logger.new(Rails.root.join('log', 'rails_spotlight.log'))%>
19
+ MIDDLEWARE_SKIPPED_PATHS: []
20
+ NOT_ENCODABLE_EVENT_VALUES:
21
+ # Rest of the configuration is required for ActionCable. It will be disabled automatically in when ActionCable is not available.
22
+ LIVE_CONSOLE_ENABLED: true
23
+ REQUEST_COMPLETED_BROADCAST_ENABLED: false
24
+ AUTO_MOUNT_ACTION_CABLE: true
25
+ ACTION_CABLE_MOUNT_PATH: /cable
26
+ YAML
27
+
28
+ if File.exist?(config_path)
29
+ puts 'Config file already exists: config/rails_spotlight.yml'
30
+ else
31
+ FileUtils.mkdir_p(File.dirname(config_path))
32
+ File.write(config_path, default_config)
33
+ puts 'Created config file: config/rails_spotlight.yml'
34
+ end
35
+ end
36
+ end