rails_spotlight 0.5.0 → 0.5.3
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/README.md +120 -4
- data/fake_spec_res/config/rails_spotlight.yml +5 -0
- data/lib/rails_spotlight/app_notifications.rb +27 -4
- data/lib/rails_spotlight/configuration.rb +21 -4
- data/lib/rails_spotlight/event.rb +92 -52
- data/lib/rails_spotlight/log_interceptor.rb +6 -5
- data/lib/rails_spotlight/middlewares/header_marker.rb +3 -1
- data/lib/rails_spotlight/middlewares/request_completed.rb +4 -7
- data/lib/rails_spotlight/notification_extension.rb +14 -0
- data/lib/rails_spotlight/railtie.rb +15 -7
- data/lib/rails_spotlight/utils.rb +6 -8
- data/lib/rails_spotlight/version.rb +1 -1
- data/lib/rails_spotlight.rb +12 -10
- data/lib/tasks/init.rake +7 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a1028b180c27405b30fc503ed2885835496d23cbbe20043b83ca5f1ec0c0363
|
4
|
+
data.tar.gz: b5eb35ba72c5fc178a3bff967bfc6424af3548899dbaa121642716e2c91a96db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2818a4e5b2564c846ae3cdf85765c95e8dbfb1b00088a9491fa21664603ad9255e477c8857dc00466750a4adbfd0daba97fb8bef877e4d7b8d9844c565bd98b
|
7
|
+
data.tar.gz: e69963faff65bfe0ef59c56cd6674ded77261c190d87713f82e0518bbd994910cc8184e284f18bf0e36411aea63bd59b9355b9864c6bec92cc6c98879141bcbb
|
data/README.md
CHANGED
@@ -25,6 +25,89 @@ group :development do
|
|
25
25
|
end
|
26
26
|
```
|
27
27
|
|
28
|
+
## Using gems locally with Gemfile.local (No Git Pollution)
|
29
|
+
|
30
|
+
This guide shows you how to use any gem **locally in development** without modifying your app’s main `Gemfile`. It’s perfect for plugin development, debugging, or testing gems privately.
|
31
|
+
|
32
|
+
The setup also works with **`puma-dev`** out of the box and ensures your local changes stay out of Git.
|
33
|
+
|
34
|
+
---
|
35
|
+
|
36
|
+
### 1. Add a bundle Wrapper to Your Shell
|
37
|
+
|
38
|
+
Paste this function into your `~/.zshrc`, `~/.bashrc`, or `~/.profile`:
|
39
|
+
|
40
|
+
```bash
|
41
|
+
bundle() {
|
42
|
+
local gemfile_local="Gemfile.local"
|
43
|
+
local lockfile_local="Gemfile.local.lock"
|
44
|
+
local lockfile_default="Gemfile.lock"
|
45
|
+
|
46
|
+
if [[ "$1" == "install" ]]; then
|
47
|
+
echo "[bundle] Running standard install with Gemfile"
|
48
|
+
command bundle install "${@:2}"
|
49
|
+
|
50
|
+
if [ -f "$gemfile_local" ]; then
|
51
|
+
echo "[bundle] Removing $lockfile_local if it exists"
|
52
|
+
rm -f "$lockfile_local"
|
53
|
+
|
54
|
+
echo "[bundle] Running install with Gemfile.local"
|
55
|
+
BUNDLE_GEMFILE="$gemfile_local" command bundle install "${@:2}"
|
56
|
+
fi
|
57
|
+
else
|
58
|
+
if [ -f "$gemfile_local" ]; then
|
59
|
+
BUNDLE_GEMFILE="$gemfile_local" command bundle "$@"
|
60
|
+
else
|
61
|
+
command bundle "$@"
|
62
|
+
fi
|
63
|
+
fi
|
64
|
+
}
|
65
|
+
```
|
66
|
+
|
67
|
+
### 2. Add the setup script
|
68
|
+
|
69
|
+
Paste this function into your `~/.zshrc`, `~/.bashrc`, or `~/.profile`:
|
70
|
+
```bash
|
71
|
+
setup_local_gemfile() {
|
72
|
+
echo 'export BUNDLE_GEMFILE=Gemfile.local' > .pumaenv
|
73
|
+
|
74
|
+
cat > Gemfile.local <<'RUBY'
|
75
|
+
gemfile = File.join(File.dirname(__FILE__), 'Gemfile')
|
76
|
+
if File.readable?(gemfile)
|
77
|
+
puts "Loading #{gemfile}..." if $DEBUG
|
78
|
+
instance_eval(File.read(gemfile))
|
79
|
+
end
|
80
|
+
RUBY
|
81
|
+
|
82
|
+
{
|
83
|
+
echo .pumaenv
|
84
|
+
echo Gemfile.local
|
85
|
+
echo Gemfile.local.lock
|
86
|
+
} >> .git/info/exclude
|
87
|
+
|
88
|
+
echo "[setup] Local Gemfile environment ready!"
|
89
|
+
}
|
90
|
+
```
|
91
|
+
|
92
|
+
### 3. Go to your app folder modify Gemfile.local
|
93
|
+
|
94
|
+
Just add your gem like this
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
group :development do
|
98
|
+
gem 'rails_spotlight'
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
### 4. Install all and restart puma.dev if needed
|
103
|
+
```bash
|
104
|
+
# if you didn't reload you bash/zsh just load rc or profile file here by source {my_file}
|
105
|
+
setup_local_gemfile
|
106
|
+
bundle install
|
107
|
+
puma-dev -stop
|
108
|
+
```
|
109
|
+
|
110
|
+
|
28
111
|
## Configuration
|
29
112
|
|
30
113
|
Generate configuration file by running:
|
@@ -38,6 +121,7 @@ file will be created in `config/rails_spotlight.yml`
|
|
38
121
|
### Configuration options
|
39
122
|
|
40
123
|
```yaml
|
124
|
+
ENABLED: true
|
41
125
|
# Default configuration for RailsSpotlight
|
42
126
|
PROJECT_NAME: <%=Rails.application.class.respond_to?(:module_parent_name) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name%>
|
43
127
|
SOURCE_PATH: <%=Rails.root%>
|
@@ -50,16 +134,21 @@ file will be created in `config/rails_spotlight.yml`
|
|
50
134
|
RAILS_SPOTLIGHT_PROJECT:
|
51
135
|
|
52
136
|
# Prevent from processing and sending some data to the extension
|
53
|
-
MIDDLEWARE_SKIPPED_PATHS: []
|
137
|
+
MIDDLEWARE_SKIPPED_PATHS: ['/rails']
|
54
138
|
NOT_ENCODABLE_EVENT_VALUES:
|
55
139
|
SKIP_RENDERED_IVARS: []
|
56
140
|
|
57
141
|
# Features
|
142
|
+
LOGS_ENABLED: true
|
143
|
+
SIDEKIQ_LOGS_ENABLED: false
|
58
144
|
FILE_MANAGER_ENABLED: true
|
59
145
|
RUBOCOP_ENABLED: true
|
60
146
|
SQL_CONSOLE_ENABLED: true
|
61
147
|
IRB_CONSOLE_ENABLED: true
|
62
148
|
|
149
|
+
# Disable ActiveSupport subscriptions
|
150
|
+
DISABLE_ACTIVE_SUPPORT_SUBSCRIPTIONS: []
|
151
|
+
|
63
152
|
# File manager configuration
|
64
153
|
BLOCK_EDITING_FILES: false
|
65
154
|
BLOCK_EDITING_FILES_OUTSIDE_OF_THE_PROJECT: true
|
@@ -143,12 +232,40 @@ You can add to your Initializers `config/initializers/rails_spotlight.rb` file w
|
|
143
232
|
end
|
144
233
|
```
|
145
234
|
|
146
|
-
|
147
235
|
## Troubleshooting
|
148
236
|
|
149
237
|
Known issue:
|
150
238
|
|
151
|
-
|
239
|
+
### Stack too deep:
|
240
|
+
|
241
|
+
Usually happens when you have a lot of nested partials or locals containing complex objects.
|
242
|
+
|
243
|
+
Solution:
|
244
|
+
|
245
|
+
- first try to add `render_partial.action_view` and `render_template.action_view` to the `DISABLE_ACTIVE_SUPPORT_SUBSCRIPTIONS` in the config file.
|
246
|
+
- if it doesn't help, try to disable more of the subscriptions full list:
|
247
|
+
* sql.active_record
|
248
|
+
* sql.sequel
|
249
|
+
* render_partial.action_view
|
250
|
+
* render_template.action_view
|
251
|
+
* process_action.action_controller.exception
|
252
|
+
* process_action.action_controller
|
253
|
+
* cache_read.active_support
|
254
|
+
* cache_generate.active_support
|
255
|
+
* cache_fetch_hit.active_support
|
256
|
+
* cache_write.active_support
|
257
|
+
* cache_delete.active_support
|
258
|
+
* cache_exist?.active_support
|
259
|
+
* render_view.locals
|
260
|
+
- last you can disable `LOGS_ENABLED` or entire gem via `ENABLED` flag
|
261
|
+
- If you are sure what in you app can cause issue you can add it to the `NOT_ENCODABLE_EVENT_VALUES` list in the config file like this:
|
262
|
+
```yaml
|
263
|
+
NOT_ENCODABLE_EVENT_VALUES:
|
264
|
+
Lookbook:
|
265
|
+
- Lookbook::Param
|
266
|
+
```
|
267
|
+
|
268
|
+
### Authentication error when using:
|
152
269
|
- Specific authentication method and action cable
|
153
270
|
- AUTO_MOUNT_CABLE: true
|
154
271
|
|
@@ -188,4 +305,3 @@ Gem is created for the Chrome extension [Rails Spotlight](https://chrome.google.
|
|
188
305
|
## License
|
189
306
|
|
190
307
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
191
|
-
|
@@ -1,3 +1,4 @@
|
|
1
|
+
ENABLED: true
|
1
2
|
# Default configuration for RailsSpotlight
|
2
3
|
PROJECT_NAME: <%=Rails.application.class.respond_to?(:module_parent_name) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name%>
|
3
4
|
SOURCE_PATH: <%=Rails.root%>
|
@@ -15,10 +16,14 @@
|
|
15
16
|
SKIP_RENDERED_IVARS: []
|
16
17
|
|
17
18
|
# Features
|
19
|
+
LOGS_ENABLED: true
|
18
20
|
FILE_MANAGER_ENABLED: true
|
19
21
|
RUBOCOP_ENABLED: true
|
20
22
|
SQL_CONSOLE_ENABLED: true
|
21
23
|
IRB_CONSOLE_ENABLED: true
|
24
|
+
|
25
|
+
# Disable ActiveSupport subscriptions
|
26
|
+
DISABLE_ACTIVE_SUPPORT_SUBSCRIPTIONS: []
|
22
27
|
|
23
28
|
# File manager configuration
|
24
29
|
BLOCK_EDITING_FILES: false
|
@@ -25,7 +25,7 @@ module RailsSpotlight
|
|
25
25
|
payload[:options][k] = payload.delete(k) unless k.in? CACHE_KEY_COLUMNS
|
26
26
|
end
|
27
27
|
|
28
|
-
callsite = ::RailsSpotlight::Utils.dev_callsite(
|
28
|
+
callsite = payload[:original_callsite] || ::RailsSpotlight::Utils.dev_callsite(caller_locations)
|
29
29
|
payload.merge!(callsite) if callsite
|
30
30
|
|
31
31
|
Event.new(name, start, ending, transaction_id, payload)
|
@@ -46,7 +46,7 @@ module RailsSpotlight
|
|
46
46
|
|
47
47
|
SQL_BLOCK = proc { |*args|
|
48
48
|
_name, start, ending, transaction_id, payload = args
|
49
|
-
callsite = ::RailsSpotlight::Utils.dev_callsite(
|
49
|
+
callsite = payload[:original_callsite] || ::RailsSpotlight::Utils.dev_callsite(caller_locations)
|
50
50
|
payload.merge!(callsite) if callsite
|
51
51
|
|
52
52
|
Event.new(SQL_EVENT_NAME, start, ending, transaction_id, payload)
|
@@ -59,13 +59,30 @@ module RailsSpotlight
|
|
59
59
|
Event.new(name, start, ending, transaction_id, payload)
|
60
60
|
}
|
61
61
|
|
62
|
+
CONTROLLER_BLOCK = proc { |*args|
|
63
|
+
name, start, ending, transaction_id, payload = args
|
64
|
+
payload[:identifier] = ::RailsSpotlight::Utils.sub_source_path(payload[:identifier])
|
65
|
+
# Payload of redirect_to
|
66
|
+
# { status: 302, location: "http://localhost:3000/posts/new", request: <ActionDispatch::Request:0x00007ff1cb9bd7b8> }
|
67
|
+
# Payload of process_action
|
68
|
+
# { controller: "PostsController", action: "index", params: {"action" => "index", "controller" => "posts"}, format: :html, method: "GET", path: "/posts",
|
69
|
+
# headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>, request: #<ActionDispatch::Request:0x00007ff1cb9bd7b8>, response: #<ActionDispatch::Response:0x00007f8521841ec8>,
|
70
|
+
# status: 200, view_runtime: 46.848, db_runtime: 0.157
|
71
|
+
# }
|
72
|
+
# Payload of send_stream.action_controller
|
73
|
+
# { filename: "subscribers.csv", type: "text/csv", disposition: "attachment" }
|
74
|
+
|
75
|
+
Event.new(name, start, ending, transaction_id, payload)
|
76
|
+
}
|
77
|
+
|
78
|
+
|
62
79
|
# Subscribe to all relevant events
|
63
80
|
def self.subscribe
|
64
81
|
# Skip RailsSpotlight subscriptions during migrations
|
65
82
|
return if migrating?
|
66
83
|
|
67
84
|
new
|
68
|
-
|
85
|
+
.subscribe('rsl.notification.log') # We do not publish events to this channel for now
|
69
86
|
.subscribe('sql.active_record', &SQL_BLOCK)
|
70
87
|
.subscribe('sql.sequel', &SQL_BLOCK)
|
71
88
|
.subscribe('render_partial.action_view', &VIEW_BLOCK)
|
@@ -83,6 +100,9 @@ module RailsSpotlight
|
|
83
100
|
.subscribe('cache_delete.active_support', &CACHE_BLOCK)
|
84
101
|
.subscribe('cache_exist?.active_support', &CACHE_BLOCK)
|
85
102
|
.subscribe('render_view.locals', &VIEW_LOCALS_BLOCK)
|
103
|
+
# .subscribe('start_processing.action_controller', &CONTROLLER_BLOCK)
|
104
|
+
# .subscribe('redirect_to.action_controller', &CONTROLLER_BLOCK)
|
105
|
+
# .subscribe('send_file.action_controller', &CONTROLLER_BLOCK)
|
86
106
|
|
87
107
|
# TODO: Consider adding these events
|
88
108
|
# start_processing.action_controller: Triggered when a controller action starts processing a request.
|
@@ -96,12 +116,15 @@ module RailsSpotlight
|
|
96
116
|
|
97
117
|
def self.migrating?
|
98
118
|
defined?(Rake) && Rake.application.top_level_tasks.any? do |task|
|
99
|
-
task.start_with?('db:
|
119
|
+
task.start_with?('db:')
|
100
120
|
end
|
101
121
|
end
|
102
122
|
|
103
123
|
def subscribe(event_name)
|
124
|
+
# Look for details about instrumentation => https://guides.rubyonrails.org/active_support_instrumentation.html#railties
|
104
125
|
ActiveSupport::Notifications.subscribe(event_name) do |*args|
|
126
|
+
next if ::RailsSpotlight.config.disable_active_support_subscriptions.include?(event_name)
|
127
|
+
|
105
128
|
event = block_given? ? yield(*args) : Event.new(*args)
|
106
129
|
AppRequest.current.events << event if AppRequest.current
|
107
130
|
end
|
@@ -16,6 +16,8 @@ module RailsSpotlight
|
|
16
16
|
'ActionDispatch' => ['ActionDispatch::Request', 'ActionDispatch::Response']
|
17
17
|
}.freeze
|
18
18
|
|
19
|
+
MAXIMUM_EVENT_VALUE_SIZE = 100000
|
20
|
+
|
19
21
|
DEFAULT_DIRECTORY_INDEX_IGNORE = %w[
|
20
22
|
/.git **/*.lock **/.DS_Store /app/assets/images/** /app/assets/fonts/** /app/assets/builds/** **/.keep
|
21
23
|
].freeze
|
@@ -40,13 +42,17 @@ module RailsSpotlight
|
|
40
42
|
@devise_mapping
|
41
43
|
].freeze
|
42
44
|
|
43
|
-
attr_reader :project_name, :source_path, :logger, :storage_path, :storage_pool_size, :middleware_skipped_paths,
|
44
|
-
:not_encodable_event_values, :cable_mount_path,
|
45
|
+
attr_reader :enabled, :project_name, :source_path, :logger, :storage_path, :storage_pool_size, :middleware_skipped_paths,
|
46
|
+
:not_encodable_event_values, :cable_mount_path, :logs_enabled, :sidekiq_logs_enabled,
|
45
47
|
:file_manager_enabled, :block_editing_files, :block_editing_files_outside_of_the_project, :skip_rendered_ivars,
|
46
48
|
:directory_index_ignore, :rubocop_enabled, :rubocop_config_path, :use_cable, :default_rs_src,
|
47
|
-
:form_js_execution_token, :sql_console_enabled, :irb_console_enabled, :data_access_token
|
49
|
+
:form_js_execution_token, :sql_console_enabled, :irb_console_enabled, :data_access_token,
|
50
|
+
:disable_active_support_subscriptions, :maximum_event_value_size
|
48
51
|
|
49
52
|
def initialize(opts = {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
53
|
+
@enabled = bool_val(:enabled, opts, default: false)
|
54
|
+
@sidekiq_logs_enabled = bool_val(:logs_enabled, opts, default: false)
|
55
|
+
@logs_enabled = bool_val(:logs_enabled, opts, default: true)
|
50
56
|
@project_name = opts[:project_name] || detect_project_name
|
51
57
|
@source_path = opts[:source_path] || self.class.rails_root
|
52
58
|
@logger = opts[:logger] || Logger.new(File.join(self.class.rails_root, 'log', 'rails_spotlight.log'))
|
@@ -63,7 +69,7 @@ module RailsSpotlight
|
|
63
69
|
@block_editing_files_outside_of_the_project = bool_val(:block_editing_files_outside_of_the_project, opts, default: true)
|
64
70
|
@file_manager_enabled = bool_val(:file_manager_enabled, opts, default: true)
|
65
71
|
@skip_rendered_ivars = SKIP_RENDERED_IVARS + (opts[:skip_rendered_ivars] || []).map(&:to_sym)
|
66
|
-
@directory_index_ignore = opts[:directory_index_ignore] || DEFAULT_DIRECTORY_INDEX_IGNORE
|
72
|
+
@directory_index_ignore = Array(opts[:directory_index_ignore] || DEFAULT_DIRECTORY_INDEX_IGNORE)
|
67
73
|
@rubocop_enabled = bool_val(:rubocop_enabled, opts, default: true)
|
68
74
|
@rubocop_config_path = opts[:rubocop_config_path] ? File.join(self.class.rails_root, opts[:rubocop_config_path]) : nil
|
69
75
|
@cable_logs_enabled = bool_val(:cable_logs_enabled, opts)
|
@@ -72,6 +78,8 @@ module RailsSpotlight
|
|
72
78
|
@sql_console_enabled = bool_val(:sql_console_enabled, opts, default: true)
|
73
79
|
@irb_console_enabled = bool_val(:irb_console_enabled, opts, default: true)
|
74
80
|
@data_access_token = opts[:data_access_token].present? ? opts[:data_access_token] : nil
|
81
|
+
@disable_active_support_subscriptions = Array(opts[:disable_active_support_subscriptions] || [])
|
82
|
+
@maximum_event_value_size = opts[:maximum_event_value_size] || MAXIMUM_EVENT_VALUE_SIZE
|
75
83
|
end
|
76
84
|
|
77
85
|
def cable_console_enabled = @cable_console_enabled && use_cable && action_cable_present?
|
@@ -80,6 +88,8 @@ module RailsSpotlight
|
|
80
88
|
def auto_mount_cable = @auto_mount_cable && use_cable && action_cable_present?
|
81
89
|
def action_cable_present? = defined?(ActionCable) && true
|
82
90
|
|
91
|
+
alias logs_enabled? logs_enabled
|
92
|
+
alias sidekiq_logs_enabled? sidekiq_logs_enabled
|
83
93
|
alias cable_console_enabled? cable_console_enabled
|
84
94
|
alias cable_logs_enabled? cable_logs_enabled
|
85
95
|
alias use_cable? use_cable
|
@@ -90,6 +100,13 @@ module RailsSpotlight
|
|
90
100
|
alias sql_console_enabled? sql_console_enabled
|
91
101
|
alias irb_console_enabled? irb_console_enabled
|
92
102
|
|
103
|
+
# We do not recommend using Rails Spotlight in production. However, if you still want to do it, a data_access_token is required
|
104
|
+
def enabled?
|
105
|
+
return enabled unless Rails.env.production?
|
106
|
+
|
107
|
+
enabled && data_access_token.present?
|
108
|
+
end
|
109
|
+
|
93
110
|
def self.load_config
|
94
111
|
config_file = File.join(rails_root, 'config', 'rails_spotlight.yml')
|
95
112
|
return new unless File.exist?(config_file)
|
@@ -6,14 +6,17 @@ require 'active_support/core_ext'
|
|
6
6
|
|
7
7
|
module RailsSpotlight
|
8
8
|
# Subclass of ActiveSupport Event that is JSON encodable
|
9
|
-
#
|
10
9
|
class Event < ActiveSupport::Notifications::Event
|
11
10
|
NOT_JSON_ENCODABLE = 'Not JSON Encodable'
|
11
|
+
NON_SERIALIZABLE_CLASSES = [Proc, Binding, Method, UnboundMethod, Thread, IO, Class, Module].freeze
|
12
12
|
|
13
|
-
attr_reader :duration
|
13
|
+
attr_reader :duration, :seen_not_encodable
|
14
14
|
|
15
15
|
def initialize(name, start, ending, transaction_id, payload)
|
16
|
-
|
16
|
+
@seen_not_encodable = Set.new
|
17
|
+
raw_payload = json_encodable(payload)
|
18
|
+
raw_payload.merge!(raw_payload[:original_callsite]) if raw_payload[:original_callsite].present? && raw_payload[:filename].blank?
|
19
|
+
super(name, start, ending, transaction_id, raw_payload)
|
17
20
|
@duration = 1000.0 * (ending - start)
|
18
21
|
rescue # rubocop:disable Lint/RedundantCopDisableDirective, Style/RescueStandardError
|
19
22
|
@duration = 0
|
@@ -36,30 +39,85 @@ module RailsSpotlight
|
|
36
39
|
|
37
40
|
private
|
38
41
|
|
39
|
-
def json_encodable(payload)
|
42
|
+
def json_encodable(payload)
|
40
43
|
return {} unless payload.is_a?(Hash)
|
41
44
|
|
42
45
|
transform_hash(payload, deep: true) do |hash, key, value|
|
43
|
-
|
44
|
-
value = value.to_h.select { |k, _| k.upcase == k }
|
45
|
-
elsif value.is_a?(Array) && defined?(ActiveRecord::Relation::QueryAttribute) && value.first.is_a?(ActiveRecord::Relation::QueryAttribute)
|
46
|
-
value = value.map(&method(:map_relation_query_attribute))
|
47
|
-
elsif !value.respond_to?(:to_json) || not_encodable?(value)
|
48
|
-
value = NOT_JSON_ENCODABLE
|
49
|
-
end
|
50
|
-
|
51
|
-
begin
|
52
|
-
value.to_json(methods: [:duration])
|
53
|
-
new_value = value
|
54
|
-
rescue # rubocop:disable Lint/RedundantCopDisableDirective, Style/RescueStandardError
|
55
|
-
new_value = NOT_JSON_ENCODABLE
|
56
|
-
end
|
57
|
-
hash[key] = new_value # encode_value(value)
|
46
|
+
hash[key] = encode_json_safe_value(value)
|
58
47
|
end.with_indifferent_access
|
59
|
-
rescue
|
48
|
+
rescue
|
60
49
|
{}
|
61
50
|
end
|
62
51
|
|
52
|
+
def not_json_encodable_and_seen(value)
|
53
|
+
seen_not_encodable.add(value.__id__)
|
54
|
+
NOT_JSON_ENCODABLE
|
55
|
+
end
|
56
|
+
|
57
|
+
def encode_json_safe_value(value)
|
58
|
+
return not_json_encodable_and_seen(value) if not_encodable?(value)
|
59
|
+
return NOT_JSON_ENCODABLE unless value.respond_to?(:to_json)
|
60
|
+
|
61
|
+
case value
|
62
|
+
when ActionDispatch::Http::Headers
|
63
|
+
value = value.to_h.select { |k, _| k.upcase == k }
|
64
|
+
when Array
|
65
|
+
value = value.map { |v| encode_json_safe_value(v) rescue NOT_JSON_ENCODABLE }
|
66
|
+
when Hash
|
67
|
+
value = transform_hash(value, deep: true) { |h, k, v| h[k] = encode_json_safe_value(v) }
|
68
|
+
when ActiveRecord::Relation::QueryAttribute
|
69
|
+
map_relation_query_attribute(value)
|
70
|
+
# when ActionController::Parameters
|
71
|
+
# value = value.to_unsafe_h rescue value.to_h
|
72
|
+
else
|
73
|
+
if defined?(ActiveRecord::Relation::QueryAttribute) && value.is_a?(ActiveRecord::Relation::QueryAttribute)
|
74
|
+
value = map_relation_query_attribute(value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
begin
|
79
|
+
value.to_json(methods: [:duration])
|
80
|
+
value
|
81
|
+
rescue
|
82
|
+
NOT_JSON_ENCODABLE
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def transform_hash(original, options = {}, &block)
|
87
|
+
options[:safe_descent] ||= {}.compare_by_identity
|
88
|
+
|
89
|
+
return cached_transformation(original, options) if already_transformed?(original, options)
|
90
|
+
|
91
|
+
new_hash = {}
|
92
|
+
cache_transformation(original, new_hash, options)
|
93
|
+
|
94
|
+
original.each do |key, value|
|
95
|
+
value = deep_transform_value(value, options, &block) if options[:deep]
|
96
|
+
block.call(new_hash, key, value)
|
97
|
+
end
|
98
|
+
|
99
|
+
new_hash
|
100
|
+
end
|
101
|
+
|
102
|
+
def already_transformed?(object, options)
|
103
|
+
options[:safe_descent].key?(object)
|
104
|
+
end
|
105
|
+
|
106
|
+
def cached_transformation(object, options)
|
107
|
+
options[:safe_descent][object]
|
108
|
+
end
|
109
|
+
|
110
|
+
def cache_transformation(original, transformed, options)
|
111
|
+
options[:safe_descent][original] = transformed
|
112
|
+
end
|
113
|
+
|
114
|
+
def deep_transform_value(value, options, &block)
|
115
|
+
return value unless value.is_a?(Hash)
|
116
|
+
return cached_transformation(value, options) if already_transformed?(value, options)
|
117
|
+
|
118
|
+
transform_hash(value, options, &block)
|
119
|
+
end
|
120
|
+
|
63
121
|
# ActiveRecord::Relation::QueryAttribute implementation changed in Rails 7.1 it getting binds need to be manually added
|
64
122
|
def map_relation_query_attribute(attr)
|
65
123
|
{
|
@@ -74,52 +132,34 @@ module RailsSpotlight
|
|
74
132
|
end
|
75
133
|
|
76
134
|
def not_encodable?(value)
|
135
|
+
return true if value_too_large?(value)
|
136
|
+
return true if NON_SERIALIZABLE_CLASSES.any? { |klass| value.is_a?(klass) }
|
137
|
+
|
77
138
|
::RailsSpotlight.config.not_encodable_event_values.any? do |module_name, class_names|
|
78
|
-
next unless safe_constantize(module_name)
|
139
|
+
next true unless safe_constantize(module_name)
|
79
140
|
|
80
141
|
class_names.any? do |class_name|
|
81
142
|
klass = safe_constantize(class_name)
|
82
|
-
|
83
|
-
|
84
|
-
value.is_a?(klass)
|
143
|
+
klass && value.is_a?(klass)
|
85
144
|
end
|
86
145
|
rescue # rubocop:disable Style/RescueStandardError
|
87
146
|
true
|
88
147
|
end
|
89
148
|
end
|
90
149
|
|
150
|
+
def value_too_large?(value)
|
151
|
+
return false unless defined?(ObjectSpace) && defined?(ObjectSpace.memsize_of)
|
152
|
+
return false unless RailsSpotlight.config.maximum_event_value_size
|
153
|
+
|
154
|
+
(ObjectSpace.memsize_of(value) > RailsSpotlight.config.maximum_event_value_size)
|
155
|
+
rescue
|
156
|
+
false
|
157
|
+
end
|
158
|
+
|
91
159
|
def safe_constantize(name)
|
92
160
|
name.constantize
|
93
161
|
rescue NameError
|
94
162
|
nil
|
95
163
|
end
|
96
|
-
|
97
|
-
def transform_hash(original, options = {}, &block)
|
98
|
-
options[:safe_descent] ||= {}.compare_by_identity
|
99
|
-
|
100
|
-
# Check if the hash has already been transformed to prevent infinite recursion.
|
101
|
-
return options[:safe_descent][original] if options[:safe_descent].key?(original)
|
102
|
-
|
103
|
-
# Create a new hash to store the transformed values.
|
104
|
-
new_hash = {}
|
105
|
-
# Store the new hash in safe_descent using the original's object_id to mark it as processed.
|
106
|
-
options[:safe_descent][original] = new_hash
|
107
|
-
|
108
|
-
# Iterate over each key-value pair in the original hash.
|
109
|
-
original.each do |key, value|
|
110
|
-
# If deep transformation is required and the value is a hash,
|
111
|
-
# recursively transform it, unless it's already been transformed.
|
112
|
-
if options[:deep] && Hash === value # rubocop:disable Style/CaseEquality
|
113
|
-
value = options[:safe_descent].fetch(value) do
|
114
|
-
transform_hash(value, options, &block)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
# Apply the transformation block to the current key-value pair.
|
118
|
-
block.call(new_hash, key, value)
|
119
|
-
end
|
120
|
-
|
121
|
-
# Return the transformed hash.
|
122
|
-
new_hash
|
123
|
-
end
|
124
164
|
end
|
125
165
|
end
|
@@ -38,9 +38,9 @@ module RailsSpotlight
|
|
38
38
|
|
39
39
|
private
|
40
40
|
|
41
|
-
def
|
41
|
+
def _skip_cable_logging?(message)
|
42
42
|
return false unless ::RailsSpotlight.config.use_cable?
|
43
|
-
return false unless
|
43
|
+
return false unless ActionCable.server.config&.cable&.dig(:adapter).present?
|
44
44
|
|
45
45
|
message.include?(::RailsSpotlight::Channels::SPOTLIGHT_CHANNEL)
|
46
46
|
end
|
@@ -60,13 +60,14 @@ module RailsSpotlight
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
-
return
|
63
|
+
return unless message.is_a?(String)
|
64
64
|
|
65
|
-
callsite = Utils.dev_callsite(
|
65
|
+
callsite = Utils.dev_callsite(caller_locations.drop(1))
|
66
66
|
name = progname.is_a?(String) || progname.is_a?(Symbol) ? progname : nil
|
67
|
-
message = yield if message.nil? && block_given?
|
68
67
|
|
69
68
|
AppRequest.current.events << Event.new('rsl.notification.log', 0, 0, 0, (callsite || {}).merge(message:, level: severity, progname: name)) if AppRequest.current
|
69
|
+
return if _skip_cable_logging?(message)
|
70
|
+
|
70
71
|
::RailsSpotlight::Channels::SpotlightChannel.broadcast_log(message, level, callsite, name)
|
71
72
|
rescue StandardError => e
|
72
73
|
RailsSpotlight.config.logger.fatal("#{e.message}\n #{e.backtrace&.join("\n ")}")
|
@@ -15,8 +15,10 @@ module RailsSpotlight
|
|
15
15
|
|
16
16
|
def call(env)
|
17
17
|
request_path = env['PATH_INFO']
|
18
|
+
return app.call(env) if skip?(request_path)
|
19
|
+
|
18
20
|
middleware = Rack::ResponseHeaders.new(app) do |headers|
|
19
|
-
headers['X-Rails-Spotlight-Version'] = RailsSpotlight::VERSION
|
21
|
+
headers['X-Rails-Spotlight-Version'] = RailsSpotlight::VERSION
|
20
22
|
end
|
21
23
|
middleware.call(env)
|
22
24
|
end
|
@@ -14,15 +14,12 @@ module RailsSpotlight
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def call(env)
|
17
|
+
return app.call(env) if skip?(env['PATH_INFO']) || (env['HTTP_CONNECTION'] == 'Upgrade' && env['HTTP_UPGRADE'] == 'websocket')
|
17
18
|
return app.call(env) unless ::RailsSpotlight.config.request_completed_broadcast_enabled?
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
status, headers, body = app.call(env)
|
23
|
-
publish_event(status, headers, env)
|
24
|
-
[status, headers, body]
|
25
|
-
end
|
20
|
+
status, headers, body = app.call(env)
|
21
|
+
publish_event(status, headers, env)
|
22
|
+
[status, headers, body]
|
26
23
|
rescue => e # rubocop:disable Style/RescueStandardError
|
27
24
|
::RailsSpotlight.config.logger.error "Error in RailsSpotlight::Middlewares::RequestCompletedHandler instrumentation: #{e.message}"
|
28
25
|
app.call(env)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module RailsSpotlight
|
2
|
+
module NotificationExtension
|
3
|
+
def instrument(name, payload = {}, &block)
|
4
|
+
if payload.is_a?(Hash) && !payload.key?(:original_callsite)
|
5
|
+
callsite = ::RailsSpotlight::Utils.dev_callsite(caller_locations)
|
6
|
+
if callsite && callsite[:filename].present?
|
7
|
+
payload[:original_callsite] = callsite
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
super(name, payload, &block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -6,22 +6,29 @@ require_relative 'log_interceptor'
|
|
6
6
|
module RailsSpotlight
|
7
7
|
class Railtie < ::Rails::Railtie
|
8
8
|
initializer 'rails_spotlight.inject_middlewares' do
|
9
|
-
|
9
|
+
next unless ::RailsSpotlight.config.enabled?
|
10
|
+
|
11
|
+
insert_base_middlewares
|
10
12
|
end
|
11
13
|
|
12
14
|
initializer 'rails_spotlight.log_interceptor' do
|
13
|
-
unless
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
next unless ::RailsSpotlight.config.enabled?
|
16
|
+
next unless ::RailsSpotlight.config.logs_enabled?
|
17
|
+
|
18
|
+
Rails.logger&.extend(LogInterceptor)
|
19
|
+
::RailsSpotlight.config.sidekiq_logs_enabled? && defined?(Sidekiq::Logger) && Sidekiq.logger&.extend(LogInterceptor)
|
17
20
|
end
|
18
21
|
|
19
22
|
initializer 'rails_spotlight.subscribe_to_notifications' do
|
20
|
-
|
23
|
+
next unless ::RailsSpotlight.config.enabled?
|
24
|
+
|
25
|
+
AppNotifications.subscribe
|
21
26
|
end
|
22
27
|
|
23
28
|
initializer 'rails_spotlight.action_cable_setup' do
|
24
|
-
|
29
|
+
next unless ::RailsSpotlight.config.enabled?
|
30
|
+
|
31
|
+
insert_action_cable_helpers
|
25
32
|
end
|
26
33
|
|
27
34
|
def insert_action_cable_helpers
|
@@ -53,6 +60,7 @@ module RailsSpotlight
|
|
53
60
|
app.middleware.use ::RailsSpotlight::Middlewares::MainRequestHandler
|
54
61
|
|
55
62
|
return unless ::RailsSpotlight.config.request_completed_broadcast_enabled?
|
63
|
+
return unless ActionCable.server.config&.cable&.dig(:adapter).present?
|
56
64
|
|
57
65
|
# app.middleware.insert_after ::RailsSpotlight::Middlewares::HeaderMarker, RailsSpotlight::Middlewares::RequestCompleted, app.config
|
58
66
|
if defined? ActionDispatch::Executor
|
@@ -4,16 +4,14 @@ module RailsSpotlight
|
|
4
4
|
module Utils
|
5
5
|
module_function
|
6
6
|
|
7
|
-
def dev_callsite(
|
8
|
-
|
9
|
-
return nil unless
|
10
|
-
|
11
|
-
_, filename, _, line, _, method = app_line.split(/^(.*?)(:(\d+))(:in `(.*)')?$/)
|
7
|
+
def dev_callsite(caller_locations)
|
8
|
+
loc = caller_locations.detect { |c| c.path.start_with? RailsSpotlight.config.rails_root }
|
9
|
+
return nil unless loc
|
12
10
|
|
13
11
|
{
|
14
|
-
filename: sub_source_path(
|
15
|
-
line:
|
16
|
-
method:
|
12
|
+
filename: sub_source_path(loc.path),
|
13
|
+
line: loc.lineno,
|
14
|
+
method: loc.label
|
17
15
|
}
|
18
16
|
rescue # rubocop:disable Style/RescueStandardError, Lint/SuppressedException
|
19
17
|
end
|
data/lib/rails_spotlight.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RailsSpotlight
|
4
|
-
autoload :VERSION,
|
5
|
-
autoload :Configuration,
|
6
|
-
autoload :Storage,
|
7
|
-
autoload :Event,
|
8
|
-
autoload :AppRequest,
|
9
|
-
autoload :Middlewares,
|
10
|
-
autoload :LogInterceptor,
|
11
|
-
autoload :
|
12
|
-
autoload :
|
13
|
-
autoload :
|
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 :NotificationExtension, 'rails_spotlight/notification_extension'
|
12
|
+
autoload :AppNotifications, 'rails_spotlight/app_notifications'
|
13
|
+
autoload :Utils, 'rails_spotlight/utils'
|
14
|
+
autoload :RenderViewReporter, 'rails_spotlight/render_view_reporter'
|
15
|
+
|
14
16
|
|
15
17
|
class << self
|
16
18
|
def config
|
data/lib/tasks/init.rake
CHANGED
@@ -10,6 +10,7 @@ namespace :rails_spotlight do # rubocop:disable Metrics/BlockLength
|
|
10
10
|
config_path = Rails.root.join('config', 'rails_spotlight.yml')
|
11
11
|
|
12
12
|
default_config = <<~YAML
|
13
|
+
ENABLED: true
|
13
14
|
# Default configuration for RailsSpotlight
|
14
15
|
PROJECT_NAME: <%=Rails.application.class.respond_to?(:module_parent_name) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name%>
|
15
16
|
SOURCE_PATH: <%=Rails.root%>
|
@@ -27,10 +28,14 @@ namespace :rails_spotlight do # rubocop:disable Metrics/BlockLength
|
|
27
28
|
SKIP_RENDERED_IVARS: []
|
28
29
|
|
29
30
|
# Features
|
31
|
+
LOGS_ENABLED: true
|
30
32
|
FILE_MANAGER_ENABLED: true
|
31
33
|
RUBOCOP_ENABLED: true
|
32
34
|
SQL_CONSOLE_ENABLED: true
|
33
35
|
IRB_CONSOLE_ENABLED: true
|
36
|
+
|
37
|
+
# Disable ActiveSupport subscriptions
|
38
|
+
DISABLE_ACTIVE_SUPPORT_SUBSCRIPTIONS: []
|
34
39
|
|
35
40
|
# File manager configuration
|
36
41
|
BLOCK_EDITING_FILES: false
|
@@ -123,9 +128,9 @@ namespace :rails_spotlight do # rubocop:disable Metrics/BlockLength
|
|
123
128
|
|
124
129
|
case layout_format
|
125
130
|
when 'slim', 'haml'
|
126
|
-
puts "- if Rails.env.development?\n = render 'layouts/#{partial_name.split('.').first}'" # rubocop:disable Style/StringLiteralsInInterpolation
|
131
|
+
puts "- if defined?(RailsSpotlight) && Rails.env.development?\n = render 'layouts/#{partial_name.split('.').first}'" # rubocop:disable Style/StringLiteralsInInterpolation
|
127
132
|
else
|
128
|
-
puts "<% if Rails.env.development? %>\n <%= render 'layouts/#{partial_name.split('.').first}' %>\n<% end %>" # rubocop:disable Style/StringLiteralsInInterpolation
|
133
|
+
puts "<% if defined?(RailsSpotlight) && Rails.env.development? %>\n <%= render 'layouts/#{partial_name.split('.').first}' %>\n<% end %>" # rubocop:disable Style/StringLiteralsInInterpolation
|
129
134
|
end
|
130
135
|
end
|
131
136
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_spotlight
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pawel Niemczyk
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-06-20 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rack-contrib
|
@@ -265,6 +265,7 @@ files:
|
|
265
265
|
- lib/rails_spotlight/middlewares/main_request_handler.rb
|
266
266
|
- lib/rails_spotlight/middlewares/request_completed.rb
|
267
267
|
- lib/rails_spotlight/middlewares/request_handler.rb
|
268
|
+
- lib/rails_spotlight/notification_extension.rb
|
268
269
|
- lib/rails_spotlight/rails_command_executor.rb
|
269
270
|
- lib/rails_spotlight/railtie.rb
|
270
271
|
- lib/rails_spotlight/render_view_reporter.rb
|