rails_spotlight 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Guardfile +1 -0
- data/README.md +22 -2
- data/chrome_ext_private_policy.md +39 -0
- data/docker-compose.yml +0 -4
- data/fake_spec_res/rails_spotlight_spec.rb +8 -8
- data/lib/rails_spotlight/app_notifications.rb +88 -0
- data/lib/rails_spotlight/app_request.rb +20 -0
- data/lib/rails_spotlight/channels/live_console_channel.rb +91 -0
- data/lib/rails_spotlight/channels/request_completed_channel.rb +15 -0
- data/lib/rails_spotlight/channels.rb +8 -0
- data/lib/rails_spotlight/configuration.rb +91 -0
- data/lib/rails_spotlight/event.rb +110 -0
- data/lib/rails_spotlight/log_interceptor.rb +47 -0
- data/lib/rails_spotlight/middlewares/concerns/skip_request_paths.rb +35 -0
- data/lib/rails_spotlight/middlewares/handlers/file_action_handler.rb +13 -6
- data/lib/rails_spotlight/middlewares/handlers/meta_action_handler.rb +28 -0
- data/lib/rails_spotlight/middlewares/handlers/sql_action_handler.rb +3 -5
- data/lib/rails_spotlight/middlewares/handlers/verify_action_handler.rb +4 -1
- data/lib/rails_spotlight/middlewares/header_marker.rb +33 -0
- data/lib/rails_spotlight/middlewares/main_request_handler.rb +35 -0
- data/lib/rails_spotlight/middlewares/request_completed.rb +71 -0
- data/lib/rails_spotlight/middlewares/request_handler.rb +2 -0
- data/lib/rails_spotlight/middlewares.rb +10 -0
- data/lib/rails_spotlight/railtie.rb +30 -10
- data/lib/rails_spotlight/storage.rb +47 -0
- data/lib/rails_spotlight/utils.rb +28 -0
- data/lib/rails_spotlight/version.rb +1 -1
- data/lib/rails_spotlight.rb +22 -3
- data/lib/tasks/init.rake +36 -0
- metadata +83 -10
- data/lib/rails_spotlight/support/project.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df87e15beee1f5b3706be05e0e6642d56165eeb7ae667cab7dad6514abcfe728
|
4
|
+
data.tar.gz: 50d7dc4b7cefae1cf565f021aea4d398757ff6a09bd5bdce3dd8e2b56ed24cee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5050e1f700b52d97e62cf26d1fc1cd37f2f5eb1fe5ffc815f030aea8a67e137b0eb4030aff8ca52c06586e754f327477a6ae93fe3502d2e876e2c6e61de9333
|
7
|
+
data.tar.gz: 0ae4a9d1b8d806f0c8bc33297c3d941e683c4d7731a672a4804889a3700b65229fd2334a35ca94874ec0ba76a1a820a549ffddff05c8f651753ed6e6b6e32f27
|
data/.rubocop.yml
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -19,10 +19,30 @@ end
|
|
19
19
|
|
20
20
|
## Configuration
|
21
21
|
|
22
|
-
|
22
|
+
Generate configuration file by running:
|
23
23
|
|
24
|
+
```bash
|
25
|
+
rails rails_spotlight:generate_config
|
24
26
|
```
|
25
|
-
|
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
|
|
28
48
|
## Testing
|
@@ -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
@@ -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 '
|
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', '
|
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-
|
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/
|
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
|
63
|
-
get "/
|
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,91 @@
|
|
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] || true
|
23
|
+
@request_completed_broadcast_enabled = opts[:request_completed_broadcast_enabled] || false
|
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] || true
|
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
|
+
config = YAML.safe_load(erb_result) || {}
|
58
|
+
# Support older versions of Ruby and Rails
|
59
|
+
opts = config.each_with_object({}) do |(key, value), memo|
|
60
|
+
new_key = key.is_a?(String) ? key.downcase.to_sym : key
|
61
|
+
memo[new_key] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
new(opts)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.rails_root
|
68
|
+
@rails_root ||= (Rails.root.to_s.presence || Dir.pwd).freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
def rails_root
|
72
|
+
self.class.rails_root
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def detect_project_name
|
78
|
+
return ENV['RAILS_SPOTLIGHT_PROJECT'] if ENV['RAILS_SPOTLIGHT_PROJECT'].present?
|
79
|
+
|
80
|
+
if app_class.respond_to?(:module_parent_name)
|
81
|
+
app_class.module_parent_name
|
82
|
+
else
|
83
|
+
app_class.parent_name
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def app_class
|
88
|
+
@app_class ||= Rails.application.class
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,110 @@
|
|
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] ||= {}
|
67
|
+
# new_hash = {}
|
68
|
+
# options[:safe_descent][original.object_id] = new_hash
|
69
|
+
# original.each_with_object(new_hash) do |(key, value), result|
|
70
|
+
# if options[:deep] && Hash === value
|
71
|
+
# value = options[:safe_descent].fetch(value.object_id) do
|
72
|
+
# transform_hash(value, options, &block)
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
# block.call(result, key, value)
|
76
|
+
# end
|
77
|
+
|
78
|
+
# Initialize safe_descent hash to keep track of hashes already transformed.
|
79
|
+
# options[:safe_descent] ||= {}
|
80
|
+
options[:safe_descent] ||= {}.compare_by_identity
|
81
|
+
|
82
|
+
# Check if the hash has already been transformed to prevent infinite recursion.
|
83
|
+
# return options[:safe_descent][original.object_id] if options[:safe_descent].key?(original.object_id)
|
84
|
+
return options[:safe_descent][original] if options[:safe_descent].key?(original)
|
85
|
+
|
86
|
+
# Create a new hash to store the transformed values.
|
87
|
+
new_hash = {}
|
88
|
+
# Store the new hash in safe_descent using the original's object_id to mark it as processed.
|
89
|
+
# options[:safe_descent][original.object_id] = new_hash
|
90
|
+
options[:safe_descent][original] = new_hash
|
91
|
+
|
92
|
+
# Iterate over each key-value pair in the original hash.
|
93
|
+
original.each do |key, value|
|
94
|
+
# If deep transformation is required and the value is a hash,
|
95
|
+
# recursively transform it, unless it's already been transformed.
|
96
|
+
if options[:deep] && Hash === value # rubocop:disable Style/CaseEquality
|
97
|
+
# value = options[:safe_descent].fetch(value.object_id) do
|
98
|
+
value = options[:safe_descent].fetch(value) do
|
99
|
+
transform_hash(value, options, &block)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
# Apply the transformation block to the current key-value pair.
|
103
|
+
block.call(new_hash, key, value)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return the transformed hash.
|
107
|
+
new_hash
|
108
|
+
end
|
109
|
+
end
|
110
|
+
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
|
@@ -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
|