rails_console_pro 0.1.1 โ†’ 0.1.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +261 -240
  3. data/CHANGELOG.md +4 -0
  4. data/QUICK_START.md +8 -0
  5. data/README.md +16 -0
  6. data/docs/FORMATTING.md +5 -0
  7. data/docs/MODEL_STATISTICS.md +4 -0
  8. data/docs/OBJECT_DIFFING.md +6 -0
  9. data/docs/PROFILING.md +91 -0
  10. data/docs/QUEUE_INSIGHTS.md +82 -0
  11. data/docs/SCHEMA_INSPECTION.md +5 -0
  12. data/docs/SNIPPETS.md +71 -0
  13. data/lib/rails_console_pro/commands/base_command.rb +1 -1
  14. data/lib/rails_console_pro/commands/jobs_command.rb +212 -0
  15. data/lib/rails_console_pro/commands/profile_command.rb +84 -0
  16. data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
  17. data/lib/rails_console_pro/commands.rb +15 -0
  18. data/lib/rails_console_pro/configuration.rb +39 -0
  19. data/lib/rails_console_pro/format_exporter.rb +8 -0
  20. data/lib/rails_console_pro/global_methods.rb +12 -0
  21. data/lib/rails_console_pro/initializer.rb +29 -4
  22. data/lib/rails_console_pro/model_validator.rb +1 -1
  23. data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
  24. data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
  25. data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
  26. data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
  27. data/lib/rails_console_pro/profile_result.rb +109 -0
  28. data/lib/rails_console_pro/pry_commands.rb +106 -0
  29. data/lib/rails_console_pro/queue_insights_result.rb +110 -0
  30. data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
  31. data/lib/rails_console_pro/services/profile_collector.rb +245 -0
  32. data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
  33. data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
  34. data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
  35. data/lib/rails_console_pro/snippets/collection_result.rb +44 -0
  36. data/lib/rails_console_pro/snippets/single_result.rb +30 -0
  37. data/lib/rails_console_pro/snippets/snippet.rb +112 -0
  38. data/lib/rails_console_pro/snippets.rb +12 -0
  39. data/lib/rails_console_pro/version.rb +1 -1
  40. data/rails_console_pro.gemspec +1 -1
  41. metadata +26 -8
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Printers
5
+ class QueueInsightsPrinter < BasePrinter
6
+ def print
7
+ print_header
8
+ print_meta
9
+ print_warnings
10
+ print_section("๐Ÿ“ฌ Enqueued Jobs", value.enqueued_jobs) { |job| format_job(job) }
11
+ print_section("๐Ÿ” Retry Set", value.retry_jobs) { |job| format_job(job, include_attempts: true) }
12
+ print_section("โš™๏ธ Recent Executions", value.recent_executions) { |execution| format_execution(execution) }
13
+ print_footer
14
+ end
15
+
16
+ private
17
+
18
+ def print_header
19
+ header_color = config.get_color(:header)
20
+ output.puts bold_color(header_color, "โ•" * config.header_width)
21
+ output.puts bold_color(header_color, "๐Ÿงต QUEUE INSIGHTS: #{display_label}")
22
+ output.puts bold_color(header_color, "โ•" * config.header_width)
23
+ end
24
+
25
+ def print_meta
26
+ return if value.meta.empty?
27
+
28
+ output.puts color(config.get_color(:info), "\nโ„น๏ธ Adapter Stats:")
29
+ value.meta.each do |key, data|
30
+ formatted_key = key.to_s.tr('_', ' ').capitalize
31
+ line = "#{formatted_key.ljust(18)} #{color(config.get_color(:attribute_value_numeric), data.to_s)}"
32
+ output.puts " #{line}"
33
+ end
34
+ end
35
+
36
+ def print_warnings
37
+ return unless value.warnings?
38
+
39
+ warning_color = config.get_color(:warning)
40
+ value.warnings.each do |warning|
41
+ output.puts bold_color(warning_color, "โš ๏ธ #{warning}")
42
+ end
43
+ end
44
+
45
+ def print_section(title, collection)
46
+ output.puts bold_color(config.get_color(:warning), "\n#{title}:")
47
+ if collection.empty?
48
+ output.puts color(:dim, " (none)")
49
+ return
50
+ end
51
+
52
+ collection.each_with_index do |entry, index|
53
+ output.puts color(:dim, " #{index + 1}. ")
54
+ formatted = yield(entry)
55
+ formatted.each { |line| output.puts " #{line}" }
56
+ end
57
+ end
58
+
59
+ def print_footer
60
+ footer_color = config.get_color(:footer)
61
+ output.puts bold_color(footer_color, "\n" + "โ•" * config.header_width)
62
+ output.puts color(:dim, "Captured at: #{format_time(value.captured_at)}")
63
+ end
64
+
65
+ def format_job(job, include_attempts: false)
66
+ lines = []
67
+ lines << "#{bold_color(config.get_color(:attribute_key), job.job_class || job.queue || 'Job')} (#{job.id || 'unknown'})"
68
+ lines << "Queue: #{color(config.get_color(:info), job.queue || 'default')}"
69
+ lines << "Enqueued: #{color(config.get_color(:attribute_value_time), format_time(job.enqueued_at) || 'n/a')}"
70
+ if job.scheduled_at
71
+ lines << "Scheduled: #{color(config.get_color(:attribute_value_time), format_time(job.scheduled_at))}"
72
+ end
73
+ if include_attempts && job.attempts
74
+ lines << "Attempts: #{color(config.get_color(:attribute_value_numeric), job.attempts.to_s)}"
75
+ end
76
+ if job.error
77
+ lines << "Error: #{color(config.get_color(:error), job.error)}"
78
+ end
79
+ if job.args && !job.args.empty?
80
+ serialized_args = safe_truncate(job.args.inspect)
81
+ lines << "Args: #{color(config.get_color(:attribute_value_string), serialized_args)}"
82
+ end
83
+ if present?(job.metadata)
84
+ job.metadata.each do |key, value|
85
+ lines << "#{key.to_s.tr('_', ' ').capitalize}: #{color(config.get_color(:attribute_value_string), value.to_s)}"
86
+ end
87
+ end
88
+ lines
89
+ end
90
+
91
+ def format_execution(execution)
92
+ lines = []
93
+ lines << "#{bold_color(config.get_color(:attribute_key), execution.job_class || 'Execution')} (#{execution.id || 'unknown'})"
94
+ lines << "Queue: #{color(config.get_color(:info), execution.queue || 'default')}"
95
+ lines << "Started: #{color(config.get_color(:attribute_value_time), format_time(execution.started_at) || 'n/a')}"
96
+ if execution.runtime_ms
97
+ lines << "Runtime: #{color(config.get_color(:attribute_value_numeric), "#{execution.runtime_ms.to_f.round(2)} ms")}"
98
+ end
99
+ if execution.worker || execution.hostname
100
+ lines << "Worker: #{color(config.get_color(:attribute_value_string), [execution.worker, execution.hostname].compact.join('@'))}"
101
+ end
102
+ if present?(execution.metadata)
103
+ execution.metadata.each do |key, value|
104
+ lines << "#{key.to_s.tr('_', ' ').capitalize}: #{color(config.get_color(:attribute_value_string), value.to_s)}"
105
+ end
106
+ end
107
+ lines
108
+ end
109
+
110
+ def format_time(time)
111
+ return unless time
112
+
113
+ case time
114
+ when Time
115
+ time.strftime("%Y-%m-%d %H:%M:%S")
116
+ when Integer, Float
117
+ Time.at(time).strftime("%Y-%m-%d %H:%M:%S")
118
+ else
119
+ time.to_s
120
+ end
121
+ rescue
122
+ time.to_s
123
+ end
124
+
125
+ def safe_truncate(text, max = 120)
126
+ return text if text.length <= max
127
+
128
+ "#{text[0, max]}โ€ฆ"
129
+ end
130
+
131
+ def display_label
132
+ label = value.adapter_label.to_s.strip
133
+ label.empty? ? "ActiveJob" : label
134
+ end
135
+
136
+ def present?(value)
137
+ case value
138
+ when nil
139
+ false
140
+ when String
141
+ !value.empty?
142
+ else
143
+ value.respond_to?(:empty?) ? !value.empty? : true
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Printers
5
+ # Printer for snippet collection results
6
+ class SnippetCollectionPrinter < BasePrinter
7
+ PREVIEW_WIDTH = 80
8
+
9
+ def print
10
+ collection = value
11
+ print_header(collection)
12
+
13
+ if collection.empty?
14
+ output.puts color(config.get_color(:warning), "No snippets yet. Capture one with snippets(:add, \"User.count\")")
15
+ else
16
+ collection.each_with_index do |snippet, index|
17
+ print_row(snippet, index: index)
18
+ end
19
+ end
20
+
21
+ print_footer(collection)
22
+ collection
23
+ end
24
+
25
+ private
26
+
27
+ def print_header(collection)
28
+ border
29
+ title = "๐Ÿ“š SNIPPETS"
30
+ filters = []
31
+ filters << "query: #{collection.query.inspect}" if collection.query
32
+ filters << "tags: #{collection.tags.join(', ')}" if collection.tags.any?
33
+ filters << "limit: #{collection.limit}" if collection.limit
34
+
35
+ header_text = filters.any? ? "#{title} (#{filters.join(' ยท ')})" : title
36
+ output.puts bold_color(config.get_color(:header), header_text)
37
+ border
38
+ end
39
+
40
+ def print_row(snippet, index:)
41
+ index_label = color(config.get_color(:info), (index + 1).to_s.rjust(2))
42
+ id_label = bold_color(config.get_color(:attribute_key), snippet.id)
43
+ tags_label = snippet.tags.any? ? color(config.get_color(:info), "[#{snippet.tags.join(', ')}]") : nil
44
+ favorite_marker = snippet.favorite? ? color(config.get_color(:success), "โ˜…") : " "
45
+ summary = truncate(snippet.summary)
46
+
47
+ output.puts "#{index_label} #{favorite_marker} #{id_label} #{summary}"
48
+ if tags_label
49
+ output.puts color(:dim, " #{tags_label}")
50
+ end
51
+ end
52
+
53
+ def print_footer(collection)
54
+ border
55
+ output.puts color(:dim, "Showing #{collection.size} #{collection.size == 1 ? 'snippet' : 'snippets'}")
56
+ border
57
+ end
58
+
59
+ def truncate(text)
60
+ return '' unless text
61
+ return text if text.length <= PREVIEW_WIDTH
62
+
63
+ "#{text[0, PREVIEW_WIDTH - 1]}โ€ฆ"
64
+ end
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Printers
5
+ # Printer for individual snippet results
6
+ class SnippetPrinter < BasePrinter
7
+ def print
8
+ result = value
9
+ snippet = result.snippet
10
+
11
+ border
12
+ header_text = header_title(result)
13
+ output.puts bold_color(config.get_color(:header), header_text)
14
+ border
15
+
16
+ output.puts format_metadata(snippet, result)
17
+ output.puts
18
+
19
+ snippet.body.each_line.with_index(1) do |line, number|
20
+ output.puts format_line(number, line)
21
+ end
22
+
23
+ border
24
+ output.puts result.message if result.message
25
+ border
26
+
27
+ snippet
28
+ end
29
+
30
+ private
31
+
32
+ def header_title(result)
33
+ case result.action
34
+ when :add
35
+ "โœจ Captured snippet #{result.snippet.id}"
36
+ when :favorite
37
+ "โญ Favorite snippet #{result.snippet.id}"
38
+ when :unfavorite
39
+ "โ˜† Snippet #{result.snippet.id}"
40
+ else
41
+ "๐Ÿ“„ Snippet #{result.snippet.id}"
42
+ end
43
+ end
44
+
45
+ def format_metadata(snippet, result)
46
+ tags = snippet.tags.any? ? "tags: #{snippet.tags.join(', ')}" : nil
47
+ details = []
48
+ details << "description: #{snippet.description}" if snippet.description
49
+ details << tags if tags
50
+ details << "favorite: #{snippet.favorite?}"
51
+ details << "created: #{snippet.created_at.strftime('%Y-%m-%d %H:%M')}"
52
+ details << "updated: #{snippet.updated_at.strftime('%Y-%m-%d %H:%M')}"
53
+
54
+ color(config.get_color(:info), details.compact.join(' ยท '))
55
+ end
56
+
57
+ def format_line(number, line)
58
+ number_label = color(config.get_color(:attribute_key), number.to_s.rjust(3))
59
+ "#{number_label} โ”‚ #{line.rstrip}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ # Value object representing a profiling session summary
5
+ class ProfileResult
6
+ QuerySample = Struct.new(
7
+ :sql,
8
+ :duration_ms,
9
+ :cached,
10
+ :name,
11
+ :binds,
12
+ keyword_init: true
13
+ )
14
+
15
+ DuplicateQuery = Struct.new(
16
+ :fingerprint,
17
+ :sql,
18
+ :count,
19
+ :total_duration_ms,
20
+ keyword_init: true
21
+ )
22
+
23
+ attr_reader :label,
24
+ :duration_ms,
25
+ :result,
26
+ :error,
27
+ :query_count,
28
+ :cached_query_count,
29
+ :write_query_count,
30
+ :total_sql_duration_ms,
31
+ :slow_queries,
32
+ :duplicate_queries,
33
+ :query_samples,
34
+ :instantiation_count,
35
+ :cache_hits,
36
+ :cache_misses,
37
+ :cache_writes,
38
+ :started_at,
39
+ :finished_at
40
+
41
+ def initialize(label:, duration_ms:, result:, error:, query_count:, cached_query_count:,
42
+ write_query_count:, total_sql_duration_ms:, slow_queries:, duplicate_queries:,
43
+ query_samples:, instantiation_count:, cache_hits:, cache_misses:,
44
+ cache_writes:, started_at:, finished_at:)
45
+ @label = label
46
+ @duration_ms = duration_ms
47
+ @result = result
48
+ @error = error
49
+ @query_count = query_count
50
+ @cached_query_count = cached_query_count
51
+ @write_query_count = write_query_count
52
+ @total_sql_duration_ms = total_sql_duration_ms
53
+ @slow_queries = Array(slow_queries)
54
+ @duplicate_queries = Array(duplicate_queries)
55
+ @query_samples = Array(query_samples)
56
+ @instantiation_count = instantiation_count
57
+ @cache_hits = cache_hits
58
+ @cache_misses = cache_misses
59
+ @cache_writes = cache_writes
60
+ @started_at = started_at
61
+ @finished_at = finished_at
62
+ end
63
+
64
+ def label?
65
+ !(label.nil? || (label.respond_to?(:empty?) && label.empty?))
66
+ end
67
+
68
+ def error?
69
+ !error.nil?
70
+ end
71
+
72
+ def cache_activity?
73
+ cache_hits.positive? || cache_misses.positive? || cache_writes.positive?
74
+ end
75
+
76
+ def slow_queries?
77
+ slow_queries.any?
78
+ end
79
+
80
+ def duplicate_queries?
81
+ duplicate_queries.any?
82
+ end
83
+
84
+ def query_samples?
85
+ query_samples.any?
86
+ end
87
+
88
+ def read_query_count
89
+ query_count - write_query_count
90
+ end
91
+
92
+ def to_json(pretty: true)
93
+ FormatExporter.to_json(self, pretty: pretty)
94
+ end
95
+
96
+ def to_yaml
97
+ FormatExporter.to_yaml(self)
98
+ end
99
+
100
+ def to_html(style: :default)
101
+ FormatExporter.to_html(self, title: "Profile: #{label || 'Session'}", style: style)
102
+ end
103
+
104
+ def export_to_file(file_path, format: nil)
105
+ FormatExporter.export_to_file(self, file_path, format: format)
106
+ end
107
+ end
108
+ end
109
+
@@ -32,6 +32,47 @@ if defined?(Pry)
32
32
  end
33
33
  end
34
34
 
35
+ Pry::Commands.create_command "profile" do
36
+ description "Profile a block, callable, or relation and report query stats"
37
+
38
+ def process
39
+ pastel = RailsConsolePro::ColorHelper.pastel
40
+ unless RailsConsolePro.config.profile_command_enabled
41
+ output.puts pastel.yellow("Profile command is disabled. Enable it with: RailsConsolePro.configure { |c| c.profile_command_enabled = true }")
42
+ return
43
+ end
44
+
45
+ if args.empty?
46
+ show_usage
47
+ return
48
+ end
49
+
50
+ begin
51
+ expression = args.join(' ')
52
+ profile_target = eval(expression, target)
53
+ result = RailsConsolePro::Commands.profile(profile_target)
54
+ RailsConsolePro.call(output, result, pry_instance) if result
55
+ rescue SyntaxError => e
56
+ output.puts pastel.red("Syntax Error: #{e.message}")
57
+ show_usage
58
+ rescue => e
59
+ output.puts pastel.red("Error: #{e.message}")
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def show_usage
66
+ pastel = RailsConsolePro::ColorHelper.pastel
67
+ output.puts pastel.red("Usage: profile expression")
68
+ output.puts pastel.yellow("Examples:")
69
+ output.puts pastel.cyan(" profile User.active.limit(10)")
70
+ output.puts pastel.cyan(" profile -> { User.includes(:posts).each { |u| u.posts.load } }")
71
+ output.puts pastel.yellow("")
72
+ output.puts pastel.yellow("Tip: For blocks, call the helper method directly: profile('Load') { User.limit(5).to_a }")
73
+ end
74
+ end
75
+
35
76
  Pry::Commands.create_command "explain" do
36
77
  description "Analyze SQL query execution plan"
37
78
 
@@ -186,6 +227,71 @@ if defined?(Pry)
186
227
  end
187
228
  end
188
229
 
230
+ Pry::Commands.create_command "jobs" do
231
+ description "Inspect ActiveJob queue insights (enqueued, retries, recent executions)"
232
+
233
+ def process
234
+ pastel = RailsConsolePro::ColorHelper.pastel
235
+ unless RailsConsolePro.config.queue_command_enabled
236
+ output.puts pastel.yellow("Jobs command is disabled. Enable it with: RailsConsolePro.configure { |c| c.queue_command_enabled = true }")
237
+ return
238
+ end
239
+
240
+ begin
241
+ options = parse_options
242
+ result = RailsConsolePro::Commands.jobs(options)
243
+ if result
244
+ RailsConsolePro.call(output, result, pry_instance)
245
+ else
246
+ output.puts pastel.yellow("No queue insights available.")
247
+ end
248
+ rescue => e
249
+ output.puts pastel.red("Error: #{e.message}")
250
+ end
251
+ end
252
+
253
+ private
254
+
255
+ def parse_options
256
+ return {} if args.empty?
257
+
258
+ options = {}
259
+
260
+ args.each do |token|
261
+ case token
262
+ when /\A(limit|queue)=(.+)\z/i
263
+ key = Regexp.last_match(1).downcase.to_sym
264
+ value = Regexp.last_match(2)
265
+ value = value.to_i if key == :limit
266
+ options[key] = value
267
+ when /\Astatus=(.+)\z/i, /\A--status=(.+)\z/i
268
+ statuses = Regexp.last_match(1).split(',').map(&:strip).reject(&:empty?)
269
+ options[:status] = Array(options[:status]) + statuses
270
+ when /\A(class|job_class)=(.+)\z/i, /\A--class=(.+)\z/i
271
+ options[:job_class] = Regexp.last_match(2)
272
+ when /\Aretry=(.+)\z/i, /\A--retry=(.+)\z/i
273
+ options[:retry] = Regexp.last_match(1)
274
+ when /\Adelete=(.+)\z/i, /\A--delete=(.+)\z/i
275
+ options[:delete] = Regexp.last_match(1)
276
+ when /\Adetails?=(.+)\z/i, /\A--details?=(.+)\z/i
277
+ options[:details] = Regexp.last_match(1)
278
+ when '--retry-only'
279
+ options[:status] = Array(options[:status]) + ['retry']
280
+ when '--enqueued-only'
281
+ options[:status] = Array(options[:status]) + ['enqueued']
282
+ when '--recent-only', '--executing-only', '--running-only'
283
+ options[:status] = Array(options[:status]) + ['recent']
284
+ when /\A\d+\z/
285
+ options[:limit] = token.to_i
286
+ else
287
+ options[:queue] ||= token
288
+ end
289
+ end
290
+
291
+ options
292
+ end
293
+ end
294
+
189
295
  Pry::Commands.create_command "diff" do
190
296
  description "Compare two objects and highlight differences"
191
297
 
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ class QueueInsightsResult
5
+ JobSummary = Struct.new(
6
+ :id,
7
+ :job_class,
8
+ :queue,
9
+ :args,
10
+ :enqueued_at,
11
+ :scheduled_at,
12
+ :attempts,
13
+ :error,
14
+ :metadata,
15
+ keyword_init: true
16
+ )
17
+
18
+ ExecutionSummary = Struct.new(
19
+ :id,
20
+ :job_class,
21
+ :queue,
22
+ :started_at,
23
+ :runtime_ms,
24
+ :worker,
25
+ :hostname,
26
+ :metadata,
27
+ keyword_init: true
28
+ )
29
+
30
+ attr_reader :adapter_name,
31
+ :adapter_type,
32
+ :enqueued_jobs,
33
+ :retry_jobs,
34
+ :recent_executions,
35
+ :meta,
36
+ :warnings,
37
+ :captured_at
38
+
39
+ def initialize(adapter_name:, adapter_type:, enqueued_jobs:, retry_jobs:, recent_executions:, meta: {}, warnings: [], captured_at: Time.current)
40
+ @adapter_name = adapter_name
41
+ @adapter_type = adapter_type
42
+ @enqueued_jobs = Array(enqueued_jobs)
43
+ @retry_jobs = Array(retry_jobs)
44
+ @recent_executions = Array(recent_executions)
45
+ @meta = meta || {}
46
+ @warnings = Array(warnings).compact
47
+ @captured_at = captured_at || Time.current
48
+ end
49
+
50
+ def adapter_label
51
+ [adapter_name, adapter_type].compact.uniq.join(" ")
52
+ end
53
+
54
+ def has_enqueued?
55
+ enqueued_jobs.any?
56
+ end
57
+
58
+ def has_retry_jobs?
59
+ retry_jobs.any?
60
+ end
61
+
62
+ def has_recent_executions?
63
+ recent_executions.any?
64
+ end
65
+
66
+ def empty?
67
+ !has_enqueued? && !has_retry_jobs? && !has_recent_executions?
68
+ end
69
+
70
+ def warnings?
71
+ warnings.any?
72
+ end
73
+
74
+ def total_enqueued
75
+ enqueued_jobs.size
76
+ end
77
+
78
+ def total_retry
79
+ retry_jobs.size
80
+ end
81
+
82
+ def total_recent
83
+ recent_executions.size
84
+ end
85
+
86
+ def totals
87
+ {
88
+ enqueued: total_enqueued,
89
+ retry: total_retry,
90
+ recent: total_recent
91
+ }
92
+ end
93
+
94
+ def with_overrides(overrides = {})
95
+ self.class.new(
96
+ adapter_name: overrides.fetch(:adapter_name, adapter_name),
97
+ adapter_type: overrides.fetch(:adapter_type, adapter_type),
98
+ enqueued_jobs: overrides.fetch(:enqueued_jobs, enqueued_jobs),
99
+ retry_jobs: overrides.fetch(:retry_jobs, retry_jobs),
100
+ recent_executions: overrides.fetch(:recent_executions, recent_executions),
101
+ meta: overrides.fetch(:meta, meta),
102
+ warnings: overrides.fetch(:warnings, warnings),
103
+ captured_at: overrides.fetch(:captured_at, captured_at)
104
+ )
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Serializers
5
+ # Serializer for ProfileResult objects
6
+ class ProfileSerializer < BaseSerializer
7
+ def serialize(profile)
8
+ {
9
+ label: profile.label,
10
+ duration_ms: profile.duration_ms,
11
+ total_sql_duration_ms: profile.total_sql_duration_ms,
12
+ query_count: profile.query_count,
13
+ read_query_count: profile.read_query_count,
14
+ write_query_count: profile.write_query_count,
15
+ cached_query_count: profile.cached_query_count,
16
+ instantiation_count: profile.instantiation_count,
17
+ cache_stats: serialize_cache(profile),
18
+ slow_queries: serialize_queries(profile.slow_queries),
19
+ duplicate_queries: serialize_duplicates(profile.duplicate_queries),
20
+ query_samples: serialize_queries(profile.query_samples),
21
+ error: serialize_error(profile.error),
22
+ started_at: profile.started_at&.iso8601,
23
+ finished_at: profile.finished_at&.iso8601,
24
+ result: serialize_data(profile.result)
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def serialize_cache(profile)
31
+ {
32
+ hits: profile.cache_hits,
33
+ misses: profile.cache_misses,
34
+ writes: profile.cache_writes
35
+ }
36
+ end
37
+
38
+ def serialize_queries(queries)
39
+ Array(queries).map do |query|
40
+ {
41
+ sql: query.sql,
42
+ duration_ms: query.duration_ms,
43
+ cached: query.cached,
44
+ name: query.name,
45
+ binds: query.binds
46
+ }
47
+ end
48
+ end
49
+
50
+ def serialize_duplicates(duplicates)
51
+ Array(duplicates).map do |duplicate|
52
+ {
53
+ fingerprint: duplicate.fingerprint,
54
+ sql: duplicate.sql,
55
+ count: duplicate.count,
56
+ total_duration_ms: duplicate.total_duration_ms
57
+ }
58
+ end
59
+ end
60
+
61
+ def serialize_error(error)
62
+ return nil unless error
63
+
64
+ {
65
+ class: error.class.name,
66
+ message: error.message,
67
+ backtrace: Array(error.backtrace).first(10)
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
73
+