rails_spotlight 0.1.7 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 507c2c676db5d30d6af1ec0f505e067f16c53514dfd875cd4554041304d95b87
4
- data.tar.gz: 8c990b6ec150e2a6c9e7b8e0c581d5b1e74987e5502907e847218a78d6324103
3
+ metadata.gz: 41eb3756c5f0405d7811d039c0953ac3a62b993486a4fc2e2085686c8c9cd54b
4
+ data.tar.gz: fd0d5d64ff53a660ade6867f05c152e36cd228ccb02b1d9006c3e4be8ed8e6cd
5
5
  SHA512:
6
- metadata.gz: c36b1e95facd5d437c4685a7ba66a02e45eaf152d8e4fc607468fffef5fad8d7fb9c15ed1b760feaa1501b1b2c8a27bd61166917083da90ccb5cf64529530a6c
7
- data.tar.gz: d1439a47bedd079561dcecb4a8f1f8cde964eea6555da2dc0e16e330bac2b89abc9e0f90a2a499bf50c1939e1b1e64a0622209a14affa1d951abb76ad4c9732a
6
+ metadata.gz: d3159584ad8395c2ea3a17fb969cd9a50c94a120c0db950ff11db8d52ce3357f443c7944a23dc8dc3521844f98e6cdc30da288f53a08e6308f8722c53a85907b
7
+ data.tar.gz: 2057d0386f21959a19b229ca2f3b4e46db2a0270d1c8f1262f551363eeda54c7127bc3b78c1c209b6e949a62dc2ce1c82989b17650eaa7c6dc02bb99874f3389
data/.rubocop.yml CHANGED
@@ -10,6 +10,7 @@ AllCops:
10
10
  - "exe/**/*"
11
11
  - "tmp/**/*"
12
12
  - "extensions/**/*"
13
+ - "spec/**/*"
13
14
 
14
15
  Style/StringLiterals:
15
16
  Enabled: true
data/Guardfile CHANGED
@@ -4,6 +4,7 @@ guard :rspec, cmd: 'rspec' do
4
4
  watch(%r{^lib/(.+).rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
5
5
  watch(%r{^spec/(.+).rb$}) { |m| "spec/#{m[1]}.rb" }
6
6
  watch('spec/spec_helper.rb') { 'spec' }
7
+ watch('spec/rails_helper.rb') { 'spec' }
7
8
  watch('Gemfile')
8
9
  end
9
10
 
data/README.md CHANGED
@@ -19,12 +19,44 @@ end
19
19
 
20
20
  ## Configuration
21
21
 
22
- environment variables
22
+ Generate configuration file by running:
23
23
 
24
+ ```bash
25
+ rails rails_spotlight:generate_config
24
26
  ```
25
- RAILS_SPOTLIGHT_PROJECT=MyProjectName
27
+
28
+ file will be created in `config/rails_spotlight.yml`
29
+
30
+ ### Configuration options
31
+
32
+ ```yaml
33
+ # Default configuration for RailsSpotlight
34
+ PROJECT_NAME: <%=Rails.application.class.respond_to?(:module_parent_name) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name%>
35
+ SOURCE_PATH: <%=Rails.root%>
36
+ STORAGE_PATH: <%=Rails.root.join('tmp', 'data', 'rails_spotlight')%>
37
+ STORAGE_POOL_SIZE: 20
38
+ LOGGER: <%=Logger.new(Rails.root.join('log', 'rails_spotlight.log'))%>
39
+ MIDDLEWARE_SKIPPED_PATHS: []
40
+ NOT_ENCODABLE_EVENT_VALUES:
41
+ # Rest of the configuration is required for ActionCable. It will be disabled automatically in when ActionCable is not available.
42
+ LIVE_CONSOLE_ENABLED: true
43
+ REQUEST_COMPLETED_BROADCAST_ENABLED: false
44
+ AUTO_MOUNT_ACTION_CABLE: true
45
+ ACTION_CABLE_MOUNT_PATH: /cable
26
46
  ```
27
47
 
48
+ ## Troubleshooting
49
+
50
+ Known issue:
51
+
52
+ Authentication error when using:
53
+ - Specific authentication method and action cable
54
+ - AUTO_MOUNT_ACTION_CABLE: true
55
+
56
+ Solution:
57
+ - Set AUTO_MOUNT_ACTION_CABLE: false
58
+ - Add manually `mount ActionCable.server => '/cable'` to `config/routes.rb` with proper authentication method
59
+
28
60
  ## Testing
29
61
 
30
62
  To run tests for all versions of Rails and Ruby, run:
@@ -35,19 +67,9 @@ docker-compose up
35
67
 
36
68
  ## Usage
37
69
 
38
- ## Development
39
-
40
- ## Contributing
41
-
42
- Bug reports and pull requests are welcome on GitHub at https://github.com/pniemczyk/rails_spotlight. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/rails_spotlight/blob/master/CODE_OF_CONDUCT.md).
70
+ Gem is created for the Chrome extension [Rails Spotlight](https://chrome.google.com/webstore/detail/rails-spotlight/kfacifkandemkdemkliponofajohhnbp?hl=en-US), but it can be used for any purpose.
43
71
 
44
72
  ## License
45
73
 
46
74
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
47
75
 
48
- ## Code of Conduct
49
-
50
- Everyone interacting in the RailsSpotlight project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/pniemczyk/rails_spotlight/blob/master/CODE_OF_CONDUCT.md).
51
-
52
-
53
- check https://github.com/alexrudall/ruby-openai
@@ -0,0 +1,39 @@
1
+ ## Privacy Policy for the Rails Spotlight Chrome Extension
2
+
3
+ **Last Updated:** October 18, 2023
4
+
5
+ ### 1. Introduction
6
+
7
+ Thank you for choosing Rails Spotlight Chrome Extension. We value your privacy and strive to protect your personal information. This Privacy Policy outlines how we collect, use, and share your data.
8
+
9
+ ### 2. Data Collection
10
+
11
+ We collect data strictly for the purpose of subscription and payment processing. No unnecessary information is harvested.
12
+
13
+ ### 3. Use of Data
14
+
15
+ Your data will be used for:
16
+ - Processing your subscription.
17
+ - Facilitating payments.
18
+ - Conducting anonymous analytics to improve our services.
19
+ - If you give your consent, for sending newsletters and promotional materials.
20
+
21
+ ### 4. Consent
22
+
23
+ By using Rails Spotlight, you agree to the collection and use of information in accordance with this policy. Should we decide to use your information for newsletter or promotional purposes, we will seek your explicit consent.
24
+
25
+ ### 5. Data Sharing
26
+
27
+ We do not sell or share your data with third parties for their promotional purposes. Data may only be shared with third parties that facilitate our service provision, such as payment processors.
28
+
29
+ ### 6. Data Security
30
+
31
+ We are committed to ensuring that your information is secure. To prevent unauthorized access or disclosure, we have put in place suitable physical, electronic, and managerial procedures to safeguard and secure the information we collect.
32
+
33
+ ### 7. Changes to This Privacy Policy
34
+
35
+ We may update our Privacy Policy from time to time. Thus, we advise you to review this document periodically for any changes. We will notify you of any changes by posting the new Privacy Policy in this location. Changes are effective immediately after they are posted.
36
+
37
+ ### 8. Contact Us
38
+
39
+ If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.
data/docker-compose.yml CHANGED
@@ -5,10 +5,6 @@ services:
5
5
  - 'docker-variables.env'
6
6
  build: .
7
7
  command: bundle exec rubocop
8
- spec-unit:
9
- env_file:
10
- - 'docker-variables.env'
11
- build: .
12
8
  spec-rails-5.2:
13
9
  env_file:
14
10
  - 'docker-variables.env'
@@ -21,7 +21,7 @@ RSpec.describe DummyController, type: :request do # rubocop:disable Metrics/Bloc
21
21
  post '/__rails_spotlight/file.json', params: { file: file, mode: :write, content: content }.to_json
22
22
 
23
23
  expect(response).to be_successful
24
- expect(response.body).to eq({ source: content }.to_json)
24
+ expect(response.body).to eq({ source: content, changed: true, project: 'App', new_content: 'TEST' }.to_json)
25
25
  expect(File.read(Rails.root.join(file))).to eq content
26
26
  end
27
27
 
@@ -33,14 +33,14 @@ RSpec.describe DummyController, type: :request do # rubocop:disable Metrics/Bloc
33
33
  it 'serve a sql result' do
34
34
  post '/__rails_spotlight/sql.json', params: { query: 'select sqlite_version();' }.to_json
35
35
  expect(response).to be_successful
36
- expect(JSON.parse(response.body).keys).to eq(%w[result logs])
36
+ expect(JSON.parse(response.body).keys).to eq(%w[query result logs error query_mode project])
37
37
  end
38
38
  end
39
39
 
40
- context 'meta_request specs' do
40
+ context 'meta request specs' do
41
41
  before do
42
42
  # clean up meta_request files
43
- FileUtils.rm_rf(Rails.root.join('tmp', 'data', 'meta_request'))
43
+ FileUtils.rm_rf(Rails.root.join('tmp', 'data', 'rails_spotlight'))
44
44
  get '/'
45
45
  @request_id = response.headers['X-Request-Id']
46
46
  end
@@ -52,15 +52,15 @@ RSpec.describe DummyController, type: :request do # rubocop:disable Metrics/Bloc
52
52
  end
53
53
 
54
54
  it 'should have a meta_request version header' do
55
- expect(response.headers['X-Meta-Request-Version']).to eq(MetaRequest::VERSION)
55
+ expect(response.headers['X-Rails-Spotlight-Version']).to eq(RailsSpotlight::VERSION)
56
56
  end
57
57
 
58
58
  it 'should create a request file' do
59
- expect(Dir[Rails.root.join('tmp/data/meta_request/*.json')].size).to eq(1)
59
+ expect(Dir[Rails.root.join('tmp/data/rails_spotlight/*.json')].size).to eq(1)
60
60
  end
61
61
 
62
- it 'should serve a meta_request' do
63
- get "/__meta_request/#{request_id}.json"
62
+ it 'should serve a meta request' do
63
+ get "/__rails_spotlight/meta.json?id=#{request_id}"
64
64
  expect(response).to be_successful
65
65
  end
66
66
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ class AppNotifications
5
+ # these are the specific keys in the cache payload that we display in the
6
+ # panel view
7
+ CACHE_KEY_COLUMNS = %i[key hit options type].freeze
8
+
9
+ # define this here so we can pass it in to all of our cache subscribe calls
10
+ CACHE_BLOCK = proc { |*args|
11
+ name, start, ending, transaction_id, payload = args
12
+
13
+ # from http://edgeguides.rubyonrails.org/active_support_instrumentation.html#cache-fetch-hit-active-support
14
+ #
15
+ # :super_operation :fetch is added when a read is used with #fetch
16
+ #
17
+ # so if :super_operation is present, we'll use it for the type. otherwise
18
+ # strip (say) 'cache_delete.active_support' down to 'delete'
19
+ payload[:type] = payload.delete(:super_operation) || name.sub(/cache_(.*?)\..*$/, '\1')
20
+
21
+ # anything that isn't in CACHE_KEY_COLUMNS gets shoved into :options
22
+ # instead
23
+ payload[:options] = {}
24
+ payload.each_key do |k|
25
+ payload[:options][k] = payload.delete(k) unless k.in? CACHE_KEY_COLUMNS
26
+ end
27
+
28
+ callsite = ::RailsSpotlight::Utils.dev_callsite(caller)
29
+ payload.merge!(callsite) if callsite
30
+
31
+ Event.new(name, start, ending, transaction_id, payload)
32
+ }
33
+
34
+ # sql processing block - used for sql.active_record and sql.sequel
35
+
36
+ # HACK: we hardcode the event name to 'sql.active_record' so that the ui will
37
+ # display sequel events without modification. otherwise the ui would need to
38
+ # be modified to support a sequel tab (or to change the display name on the
39
+ # active_record tab when necessary - which maybe makes more sense?)
40
+ SQL_EVENT_NAME = 'sql.active_record'
41
+
42
+ SQL_BLOCK = proc { |*args|
43
+ _name, start, ending, transaction_id, payload = args
44
+ callsite = ::RailsSpotlight::Utils.dev_callsite(caller)
45
+ payload.merge!(callsite) if callsite
46
+
47
+ Event.new(SQL_EVENT_NAME, start, ending, transaction_id, payload)
48
+ }
49
+
50
+ VIEW_BLOCK = proc { |*args|
51
+ name, start, ending, transaction_id, payload = args
52
+ payload[:identifier] = ::RailsSpotlight::Utils.sub_source_path(payload[:identifier])
53
+
54
+ Event.new(name, start, ending, transaction_id, payload)
55
+ }
56
+
57
+ # Subscribe to all events relevant to RailsPanel
58
+ #
59
+ def self.subscribe
60
+ new
61
+ .subscribe('rsl.notification.log')
62
+ .subscribe('sql.active_record', &SQL_BLOCK)
63
+ .subscribe('sql.sequel', &SQL_BLOCK)
64
+ .subscribe('render_partial.action_view', &VIEW_BLOCK)
65
+ .subscribe('render_template.action_view', &VIEW_BLOCK)
66
+ .subscribe('process_action.action_controller.exception')
67
+ .subscribe('process_action.action_controller') do |*args|
68
+ name, start, ending, transaction_id, payload = args
69
+ payload[:status] = '500' if payload[:exception]
70
+ Event.new(name, start, ending, transaction_id, payload)
71
+ end
72
+ .subscribe('cache_read.active_support', &CACHE_BLOCK)
73
+ .subscribe('cache_generate.active_support', &CACHE_BLOCK)
74
+ .subscribe('cache_fetch_hit.active_support', &CACHE_BLOCK)
75
+ .subscribe('cache_write.active_support', &CACHE_BLOCK)
76
+ .subscribe('cache_delete.active_support', &CACHE_BLOCK)
77
+ .subscribe('cache_exist?.active_support', &CACHE_BLOCK)
78
+ end
79
+
80
+ def subscribe(event_name)
81
+ ActiveSupport::Notifications.subscribe(event_name) do |*args|
82
+ event = block_given? ? yield(*args) : Event.new(*args)
83
+ AppRequest.current.events << event if AppRequest.current
84
+ end
85
+ self
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ class AppRequest
5
+ attr_reader :id, :events
6
+
7
+ def initialize(id)
8
+ @id = id
9
+ @events = []
10
+ end
11
+
12
+ def self.current
13
+ Thread.current[:rails_spotlight_request_id]
14
+ end
15
+
16
+ def current!
17
+ Thread.current[:rails_spotlight_request_id] = self
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module Channels
5
+ class LiveConsoleChannel < ActionCable::Channel::Base
6
+ def subscribed
7
+ stream_from 'rails_spotlight_live_console_channel'
8
+ publish({ message: "Welcome to the #{project} project Rails Spotlight Live Console" })
9
+ end
10
+
11
+ def unsubscribed
12
+ # Any cleanup needed when channel is unsubscribed
13
+ end
14
+
15
+ def receive(data)
16
+ command = data['command']
17
+ inspect_types = data['inspect_types']
18
+ for_project = data['project']
19
+ return publish({ error: project_mismatch_message(for_project) }) if for_project.present? && for_project != project
20
+
21
+ output = execute_command(command, { inspect_types: inspect_types })
22
+ publish(output)
23
+ end
24
+
25
+ private
26
+
27
+ def project_mismatch_message(for_project)
28
+ "Project mismatch, The command was intended for the #{for_project} project. This is #{project} project"
29
+ end
30
+
31
+ def publish(data)
32
+ transmit(data.merge(project: project))
33
+ end
34
+
35
+ # TODO: add possibility to change project name via ENV variable or RailsSpotlight config
36
+ def project
37
+ ::RailsSpotlight.config.project_name
38
+ end
39
+
40
+ def execute_command(command, opts = {})
41
+ output_stream = StringIO.new # Create a new StringIO object to capture output
42
+ inspect_types = opts[:inspect_types]
43
+ result = nil
44
+
45
+ begin
46
+ original_stdout = $stdout
47
+ $stdout = output_stream
48
+ result = eval(command) # rubocop:disable Security/Eval
49
+ ensure
50
+ $stdout = original_stdout
51
+ end
52
+
53
+ # result = eval(command)
54
+ {
55
+ result: {
56
+ inspect: result.inspect,
57
+ raw: result,
58
+ type: result.class.name,
59
+ types: result_inspect_types(inspect_types, result),
60
+ console: output_stream.string
61
+ }
62
+ }
63
+ rescue StandardError => e
64
+ { error: e.message }
65
+ end
66
+
67
+ def result_inspect_types(inspect_types, result)
68
+ return {} unless inspect_types
69
+
70
+ {
71
+ root: result.class.name,
72
+ items: result_types_items(result)
73
+ }
74
+ end
75
+
76
+ def result_types_items(result)
77
+ case result
78
+ when Array
79
+ # Create a hash with indices as keys and class names as values
80
+ result.each_with_index.to_h { |element, index| [index.to_s, element.class.name] }
81
+ when Hash
82
+ # Create a hash with string keys and class names as values
83
+ result.transform_keys(&:to_s).transform_values { |value| value.class.name }
84
+ else
85
+ # For non-collection types, there are no items
86
+ {}
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module Channels
5
+ class RequestCompletedChannel < ActionCable::Channel::Base
6
+ def subscribed
7
+ stream_from 'rails_spotlight_request_completed_channel'
8
+ end
9
+
10
+ def unsubscribed
11
+ # Any cleanup needed when channel is unsubscribed
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module Channels
5
+ autoload(:LiveConsoleChannel, 'rails_spotlight/channels/live_console_channel') if defined?(ActionCable)
6
+ autoload(:RequestCompletedChannel, 'rails_spotlight/channels/request_completed_channel') if defined?(ActionCable)
7
+ end
8
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'erb'
5
+
6
+ module RailsSpotlight
7
+ class Configuration
8
+ DEFAULT_NOT_ENCODABLE_EVENT_VALUES = {
9
+ 'ActiveRecord' => ['ActiveRecord::ConnectionAdapters::AbstractAdapter'],
10
+ 'ActionDispatch' => ['ActionDispatch::Request', 'ActionDispatch::Response']
11
+ }.freeze
12
+
13
+ attr_reader :project_name, :source_path, :logger, :storage_path, :storage_pool_size, :middleware_skipped_paths,
14
+ :not_encodable_event_values, :action_cable_mount_path
15
+
16
+ def initialize(opts = {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
17
+ @project_name = opts[:project_name] || detect_project_name
18
+ @source_path = opts[:source_path] || self.class.rails_root
19
+ @logger = opts[:logger] || Logger.new(File.join(self.class.rails_root, 'log', 'rails_spotlight.log'))
20
+ @storage_path = opts[:storage_path] || File.join(self.class.rails_root, 'tmp', 'data', 'rails_spotlight')
21
+ @storage_pool_size = opts[:storage_pool_size] || 20
22
+ @live_console_enabled = opts[:live_console_enabled].nil? ? true : is_true?(opts[:live_console_enabled])
23
+ @request_completed_broadcast_enabled = is_true?(opts[:request_completed_broadcast_enabled])
24
+ @middleware_skipped_paths = opts[:middleware_skipped_paths] || []
25
+ @not_encodable_event_values = DEFAULT_NOT_ENCODABLE_EVENT_VALUES.merge(opts[:not_encodable_event_values] || {})
26
+ @auto_mount_action_cable = opts[:auto_mount_action_cable].nil? ? true : is_true?(opts[:auto_mount_action_cable])
27
+ @action_cable_mount_path = opts[:action_cable_mount_path] || '/cable'
28
+ end
29
+
30
+ def live_console_enabled
31
+ @live_console_enabled && action_cable_present?
32
+ end
33
+
34
+ alias live_console_enabled? live_console_enabled
35
+
36
+ def request_completed_broadcast_enabled
37
+ @request_completed_broadcast_enabled && action_cable_present?
38
+ end
39
+
40
+ alias request_completed_broadcast_enabled? request_completed_broadcast_enabled
41
+
42
+ def auto_mount_action_cable
43
+ @auto_mount_action_cable && action_cable_present?
44
+ end
45
+
46
+ alias auto_mount_action_cable? auto_mount_action_cable
47
+
48
+ def action_cable_present?
49
+ defined?(ActionCable) && true
50
+ end
51
+
52
+ def self.load_config
53
+ config_file = File.join(rails_root, 'config', 'rails_spotlight.yml')
54
+ return new unless File.exist?(config_file)
55
+
56
+ erb_result = ERB.new(File.read(config_file)).result
57
+ data = YAML.safe_load(erb_result) || {}
58
+
59
+ # Support older versions of Ruby and Rails
60
+ opts = data.each_with_object({}) do |(key, value), memo|
61
+ new_key = key.is_a?(String) ? key.downcase.to_sym : key
62
+ memo[new_key] = value
63
+ end
64
+
65
+ new(opts)
66
+ end
67
+
68
+ def self.rails_root
69
+ @rails_root ||= (Rails.root.to_s.presence || Dir.pwd).freeze
70
+ end
71
+
72
+ def rails_root
73
+ self.class.rails_root
74
+ end
75
+
76
+ private
77
+
78
+ def is_true?(value)
79
+ value == true || value == 'true' || value == 1 || value == '1'
80
+ end
81
+
82
+ def detect_project_name
83
+ return ENV['RAILS_SPOTLIGHT_PROJECT'] if ENV['RAILS_SPOTLIGHT_PROJECT'].present?
84
+
85
+ if app_class.respond_to?(:module_parent_name)
86
+ app_class.module_parent_name
87
+ else
88
+ app_class.parent_name
89
+ end
90
+ end
91
+
92
+ def app_class
93
+ @app_class ||= Rails.application.class
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/json'
5
+ require 'active_support/core_ext'
6
+
7
+ module RailsSpotlight
8
+ # Subclass of ActiveSupport Event that is JSON encodable
9
+ #
10
+ class Event < ActiveSupport::Notifications::Event
11
+ NOT_JSON_ENCODABLE = 'Not JSON Encodable'
12
+
13
+ attr_reader :duration
14
+
15
+ def initialize(name, start, ending, transaction_id, payload)
16
+ super(name, start, ending, transaction_id, json_encodable(payload))
17
+ @duration = 1000.0 * (ending - start)
18
+ end
19
+
20
+ def self.events_for_exception(exception_wrapper)
21
+ if defined?(ActionDispatch::ExceptionWrapper)
22
+ exception = exception_wrapper.exception
23
+ trace = exception_wrapper.application_trace
24
+ trace = exception_wrapper.framework_trace if trace.empty?
25
+ else
26
+ exception = exception_wrapper
27
+ trace = exception.backtrace
28
+ end
29
+ trace.unshift "#{exception.class} (#{exception.message})"
30
+ trace.map do |call|
31
+ Event.new('process_action.action_controller.exception', 0, 0, nil, call: call)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def json_encodable(payload)
38
+ return {} unless payload.is_a?(Hash)
39
+
40
+ transform_hash(payload, deep: true) do |hash, key, value|
41
+ if value.class.to_s == 'ActionDispatch::Http::Headers'
42
+ value = value.to_h.select { |k, _| k.upcase == k }
43
+ elsif not_encodable?(value)
44
+ value = NOT_JSON_ENCODABLE
45
+ end
46
+
47
+ begin
48
+ value.to_json(methods: [:duration])
49
+ new_value = value
50
+ rescue StandardError
51
+ new_value = NOT_JSON_ENCODABLE
52
+ end
53
+ hash[key] = new_value
54
+ end.with_indifferent_access
55
+ end
56
+
57
+ def not_encodable?(value)
58
+ ::RailsSpotlight.config.not_encodable_event_values.any? do |module_name, class_names|
59
+ next unless defined?(module_name.constantize)
60
+
61
+ class_names.any? { |class_name| value.is_a?(class_name.constantize) }
62
+ end
63
+ end
64
+
65
+ def transform_hash(original, options = {}, &block)
66
+ options[:safe_descent] ||= {}.compare_by_identity
67
+
68
+ # Check if the hash has already been transformed to prevent infinite recursion.
69
+ return options[:safe_descent][original] if options[:safe_descent].key?(original)
70
+
71
+ # Create a new hash to store the transformed values.
72
+ new_hash = {}
73
+ # Store the new hash in safe_descent using the original's object_id to mark it as processed.
74
+ options[:safe_descent][original] = new_hash
75
+
76
+ # Iterate over each key-value pair in the original hash.
77
+ original.each do |key, value|
78
+ # If deep transformation is required and the value is a hash,
79
+ # recursively transform it, unless it's already been transformed.
80
+ if options[:deep] && Hash === value # rubocop:disable Style/CaseEquality
81
+ value = options[:safe_descent].fetch(value) do
82
+ transform_hash(value, options, &block)
83
+ end
84
+ end
85
+ # Apply the transformation block to the current key-value pair.
86
+ block.call(new_hash, key, value)
87
+ end
88
+
89
+ # Return the transformed hash.
90
+ new_hash
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ module LogInterceptor
5
+ def debug(message = nil, *args)
6
+ push_event(:debug, message)
7
+ super
8
+ end
9
+
10
+ def info(message = nil, *args)
11
+ push_event(:info, message)
12
+ super
13
+ end
14
+
15
+ def warn(message = nil, *args)
16
+ push_event(:warn, message)
17
+ super
18
+ end
19
+
20
+ def error(message = nil, *args)
21
+ push_event(:error, message)
22
+ super
23
+ end
24
+
25
+ def fatal(message = nil, *args)
26
+ push_event(:fatal, message)
27
+ super
28
+ end
29
+
30
+ def unknown(message = nil, *args)
31
+ push_event(:unknown, message)
32
+ super
33
+ end
34
+
35
+ private
36
+
37
+ def push_event(level, message)
38
+ callsite = AppRequest.current && Utils.dev_callsite(caller.drop(1))
39
+ if callsite
40
+ payload = callsite.merge(message: message, level: level)
41
+ AppRequest.current.events << Event.new('rsl.notification.log', 0, 0, 0, payload)
42
+ end
43
+ rescue StandardError => e
44
+ RailsSpotlight.config.logger.fatal("#{e.message}\n #{e.backtrace.join("\n ")}")
45
+ end
46
+ end
47
+ end