newshound 0.2.0 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed2c9eae65ac8681fc1c72890cbd421ca9dd73130c37fa86c04643cb74c03856
4
- data.tar.gz: 7ccb875f08607e1478245384376a621243fdd44e40443fb191064210cff94ba6
3
+ metadata.gz: 65debb3ae756e51f870de0872ff54d2332ee71e9c05aa6ac174c9525336c46b5
4
+ data.tar.gz: 8667bbc9ff8695b5191bccbbe066190bb9e6f86f7c7ef0d6508a312420258167
5
5
  SHA512:
6
- metadata.gz: a78bd31f883c89116c83b7c77b680faba172b41e0bc3f375e4a81a075503d33860ebd4778e71f11d868a794fe04effd815e240ebd3d5c6135b0ed80599c87469
7
- data.tar.gz: 4985d3d128688284f4734391b12e7f6e34d1c69857d713d3995d2078cc4df210b64ca5f84d889841e52b1b3a9086bcf0f19e7d35bef646b60e08020379527ad6
6
+ metadata.gz: 7bb18eaf72c010172a7c80c13f782b6736620cef9855ee81f0faaa2ad32d1659b02d0970a4381adad7b42d72eb80663b8c93e0eefebd58d61ec75d45c2c31081
7
+ data.tar.gz: '07722335323043284857b615205ec4def0f167f4f77308ffce3eeb1911afe85c963e6a62987f88d88d59e8385a4998d30e33835211c6c7d70f52e111f46ae5bb'
data/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.2] - 2025-10-23
9
+
10
+ ### Added
11
+
12
+ - CI workflow to run tests on Ruby 3.0-3.4 (b3ebc7a)
13
+ - Separate linter job for RuboCop (b3ebc7a)
14
+ - Coverage reporting with SimpleCov (b3ebc7a)
15
+ - CI workflow to run tests on Ruby 3.0-3.4 (782cc57)
16
+ - Separate linter job for RuboCop (782cc57)
17
+ - Coverage reporting with SimpleCov (782cc57)
18
+ - .claude/settings.local.json to .gitignore (701d802)
19
+ - Reissue gem dependency for automated versioning (feac98a)
20
+ - Git trailer support for changelog management (feac98a)
21
+ - Initial CHANGELOG.md with Keep a Changelog format (feac98a)
22
+ - CODEOWNERS file for SOFware/engineers (1a0a1f5)
23
+ - Newshound::Exceptions::Base for standard API to interact with exception data (9dce846)
24
+ - Configuration exception_source to allow for future alternative exception backends (9dce846)
25
+ - SolidErrors support (8b5b48a)
26
+
27
+ ### Changed
28
+
29
+ - Replaced RuboCop with StandardRB for simpler linting (09292ed)
30
+ - Updated GitHub Actions workflow to use standardrb (09292ed)
31
+ - Auto-corrected all StandardRB violations (09292ed)
32
+ - Simplified test setup by removing unused mocks (701d802)
33
+ - Rakefile to use reissue tasks instead of manual versioning (feac98a)
34
+ - Removed Ruby 3.0 from test matrix (now testing 3.1-3.4) (4beafd2)
35
+ - Updated gemspec required_ruby_version to >= 3.1.0 (4beafd2)
36
+
37
+ ### Fixed
38
+
39
+ - Workflow now only runs once per PR (removed duplicate triggers) (09292ed)
40
+ - Mock ActiveRecord::Base.connection and its methods (701d802)
41
+ - Mock connection.execute to return database-like results (701d802)
42
+ - Mock connection.quote and connection.select_value properly (701d802)
43
+ - Name and email in gemspec authors (ab44f30)
44
+
45
+ ### Removed
46
+
47
+ - Dependency on exception-track (8b5b48a)
48
+
49
+ ## [0.2.2] - Unreleased
data/Gemfile CHANGED
@@ -4,12 +4,11 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- group :development, :test do
8
- gem "bundler", "~> 2.0"
9
- gem "rake", "~> 13.0"
10
- gem "rspec", "~> 3.0"
11
- gem "rubocop", "~> 1.0"
12
- gem "pg"
13
- gem "pry"
14
- gem "simplecov", require: false
15
- end
7
+ gem "bundler"
8
+ gem "rake"
9
+ gem "rspec"
10
+ gem "reissue"
11
+ gem "standard"
12
+ gem "pg"
13
+ gem "pry"
14
+ gem "simplecov"
data/README.md CHANGED
@@ -201,6 +201,21 @@ bundle exec rubocop
201
201
  bin/console
202
202
  ```
203
203
 
204
+ ## Release Management
205
+
206
+ This gem uses [Reissue](https://github.com/rails/reissue) for release management. To release a new version, perform
207
+ the following steps as you would with any other ruby gem:
208
+
209
+ ```bash
210
+ bundle exec rake bump:checksum
211
+ ```
212
+ And then create a new release:
213
+ ```bash
214
+ bundle exec rake release
215
+ ```
216
+
217
+ The final step is to push your version bump branch, open a PR, and merge it.
218
+
204
219
  ## Dependencies
205
220
 
206
221
  - **Rails** >= 6.0
data/Rakefile CHANGED
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "reissue/gem"
4
5
  require "rspec/core/rake_task"
5
- require "rubocop/rake_task"
6
+ require "standard/rake"
7
+
8
+ Reissue::Task.create :reissue do |task|
9
+ task.version_file = "lib/newshound/version.rb"
10
+ task.fragment = :git
11
+ end
6
12
 
7
13
  RSpec::Core::RakeTask.new(:spec)
8
- RuboCop::RakeTask.new
9
14
 
10
- task default: %i[spec rubocop]
15
+ task default: %i[spec standard]
@@ -37,4 +37,4 @@ module Newshound
37
37
  end
38
38
  end
39
39
  end
40
- end
40
+ end
@@ -10,6 +10,10 @@ Newshound.configure do |config|
10
10
  # Default is 10
11
11
  config.exception_limit = 10
12
12
 
13
+ # Exception source to use for exceptions
14
+ # Default is :exception_track
15
+ config.exception_source = :exception_track # or :solid_errors
16
+
13
17
  # User roles that are authorized to view the Newshound banner
14
18
  # These should match the role values in your User model
15
19
  # Default is [:developer, :super_user]
@@ -29,4 +33,4 @@ end
29
33
  # # Your custom logic here
30
34
  # # Return true to show the banner, false to hide it
31
35
  # controller.current_user&.admin?
32
- # end
36
+ # end
@@ -3,7 +3,7 @@
3
3
  module Newshound
4
4
  class Configuration
5
5
  attr_accessor :exception_limit, :enabled, :authorized_roles,
6
- :current_user_method, :authorization_block
6
+ :current_user_method, :authorization_block, :exception_source
7
7
 
8
8
  def initialize
9
9
  @exception_limit = 10
@@ -11,6 +11,7 @@ module Newshound
11
11
  @authorized_roles = [:developer, :super_user]
12
12
  @current_user_method = :current_user
13
13
  @authorization_block = nil
14
+ @exception_source = :exception_track
14
15
  end
15
16
 
16
17
  # Allow custom authorization logic
@@ -22,4 +23,4 @@ module Newshound
22
23
  enabled
23
24
  end
24
25
  end
25
- end
26
+ end
@@ -5,8 +5,8 @@ module Newshound
5
5
  attr_reader :exception_source, :configuration, :time_range
6
6
 
7
7
  def initialize(exception_source: nil, configuration: nil, time_range: 24.hours)
8
- @exception_source = exception_source || (defined?(ExceptionTrack::Log) ? ExceptionTrack::Log : nil)
9
8
  @configuration = configuration || Newshound.configuration
9
+ @exception_source = resolve_exception_source(exception_source)
10
10
  @time_range = time_range
11
11
  end
12
12
 
@@ -24,20 +24,24 @@ module Newshound
24
24
  *format_exceptions
25
25
  ]
26
26
  end
27
+ alias_method :report, :generate_report
28
+
29
+ # Returns data formatted for the banner UI
30
+ def banner_data
31
+ {
32
+ exceptions: recent_exceptions.map { |exception| exception_source.format_for_banner(exception) }
33
+ }
34
+ end
27
35
 
28
36
  private
29
37
 
30
- def recent_exceptions
31
- @recent_exceptions ||= fetch_recent_exceptions
38
+ def resolve_exception_source(source)
39
+ source ||= @configuration.exception_source
40
+ source.is_a?(Symbol) ? Exceptions.source(source) : source
32
41
  end
33
42
 
34
- def fetch_recent_exceptions
35
- return [] unless exception_source
36
-
37
- exception_source
38
- .where("created_at >= ?", time_range.ago)
39
- .order(created_at: :desc)
40
- .limit(configuration.exception_limit)
43
+ def recent_exceptions
44
+ @recent_exceptions ||= exception_source.recent(time_range: time_range, limit: configuration.exception_limit)
41
45
  end
42
46
 
43
47
  def format_exceptions
@@ -46,38 +50,12 @@ module Newshound
46
50
  type: "section",
47
51
  text: {
48
52
  type: "mrkdwn",
49
- text: format_exception_text(exception, index + 1)
53
+ text: exception_source.format_for_report(exception, index + 1)
50
54
  }
51
55
  }
52
56
  end
53
57
  end
54
58
 
55
- def format_exception_text(exception, number)
56
- <<~TEXT
57
- *#{number}. #{exception.title || exception.exception_class}*
58
- • *Time:* #{exception.created_at.strftime('%I:%M %p')}
59
- • *Controller:* #{exception.controller_name}##{exception.action_name}
60
- • *Count:* #{exception_count(exception)}
61
- #{format_message(exception)}
62
- TEXT
63
- end
64
-
65
- def format_message(exception)
66
- return "" unless exception.message.present?
67
-
68
- message = exception.message.truncate(100)
69
- "• *Message:* `#{message}`"
70
- end
71
-
72
- def exception_count(exception)
73
- return 0 unless exception_source
74
-
75
- exception_source
76
- .where(exception_class: exception.exception_class)
77
- .where("created_at >= ?", time_range.ago)
78
- .count
79
- end
80
-
81
59
  def no_exceptions_block
82
60
  [
83
61
  {
@@ -90,4 +68,4 @@ module Newshound
90
68
  ]
91
69
  end
92
70
  end
93
- end
71
+ end
@@ -0,0 +1,40 @@
1
+ module Newshound
2
+ module Exceptions
3
+ # Base class for exception source adapters
4
+ # Each adapter is responsible for:
5
+ # 1. Fetching recent exceptions from its specific exception tracking system
6
+ # 2. Formatting exception data for reports and banners
7
+ #
8
+ # Subclasses must implement:
9
+ # - #recent(time_range:, limit:) - Returns a collection of exception records
10
+ # - #format_for_report(exception) - Formats a single exception for Slack/report display
11
+ # - #format_for_banner(exception) - Formats a single exception for banner UI
12
+ class Base
13
+ # Fetches recent exceptions from the exception tracking system
14
+ #
15
+ # @param time_range [ActiveSupport::Duration] Time duration to look back (e.g., 24.hours)
16
+ # @param limit [Integer] Maximum number of exceptions to return
17
+ # @return [Array] Collection of exception records
18
+ def recent(time_range:, limit:)
19
+ raise NotImplementedError, "#{self.class} must implement #recent"
20
+ end
21
+
22
+ # Formats an exception for report/Slack display
23
+ #
24
+ # @param exception [Object] Exception record from the tracking system
25
+ # @param number [Integer] Position number in the list
26
+ # @return [String] Formatted markdown text for display
27
+ def format_for_report(exception, number)
28
+ raise NotImplementedError, "#{self.class} must implement #format_for_report"
29
+ end
30
+
31
+ # Formats an exception for banner UI display
32
+ #
33
+ # @param exception [Object] Exception record from the tracking system
34
+ # @return [Hash] Hash with keys: :title, :message, :location, :time
35
+ def format_for_banner(exception)
36
+ raise NotImplementedError, "#{self.class} must implement #format_for_banner"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,92 @@
1
+ module Newshound
2
+ module Exceptions
3
+ class ExceptionTrack < Base
4
+ def recent(time_range:, limit:)
5
+ ::ExceptionTrack::Log
6
+ .where("created_at >= ?", time_range.ago)
7
+ .order(created_at: :desc)
8
+ .limit(limit)
9
+ end
10
+
11
+ def format_for_report(exception, number)
12
+ details = parse_exception_details(exception)
13
+
14
+ <<~TEXT
15
+ *#{number}. #{exception_title(exception)}*
16
+ • *Time:* #{exception.created_at.strftime("%I:%M %p")}
17
+ #{format_controller(details)}
18
+ #{format_message(exception, details)}
19
+ TEXT
20
+ end
21
+
22
+ def format_for_banner(exception)
23
+ details = parse_exception_details(exception)
24
+
25
+ # Extract message
26
+ message = if details["message"].present?
27
+ details["message"].to_s
28
+ elsif exception.respond_to?(:message) && exception.message.present?
29
+ exception.message.to_s
30
+ else
31
+ +""
32
+ end
33
+
34
+ # Extract location
35
+ location = if details["controller_name"] && details["action_name"]
36
+ "#{details["controller_name"]}##{details["action_name"]}"
37
+ else
38
+ +""
39
+ end
40
+
41
+ {
42
+ title: exception_title(exception),
43
+ message: message.truncate(100),
44
+ location: location,
45
+ time: exception.created_at.strftime("%I:%M %p")
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ def exception_title(exception)
52
+ if exception.respond_to?(:title) && exception.title.present?
53
+ exception.title
54
+ elsif exception.respond_to?(:exception_class) && exception.exception_class.present?
55
+ exception.exception_class
56
+ else
57
+ "Unknown Exception"
58
+ end
59
+ end
60
+
61
+ def parse_exception_details(exception)
62
+ return {} unless exception.respond_to?(:body) && exception.body.present?
63
+
64
+ JSON.parse(exception.body)
65
+ rescue JSON::ParserError
66
+ {}
67
+ end
68
+
69
+ def format_controller(details)
70
+ return +"" unless details["controller_name"] && details["action_name"]
71
+
72
+ "• *Controller:* #{details["controller_name"]}##{details["action_name"]}\n"
73
+ end
74
+
75
+ def format_message(exception, details = nil)
76
+ details ||= parse_exception_details(exception)
77
+
78
+ # Try to get message from different sources
79
+ message = if details["message"].present?
80
+ details["message"]
81
+ elsif exception.respond_to?(:message) && exception.message.present?
82
+ exception.message
83
+ end
84
+
85
+ return +"" unless message.present?
86
+
87
+ message = message.to_s.truncate(100)
88
+ "• *Message:* `#{message}`"
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,97 @@
1
+ module Newshound
2
+ module Exceptions
3
+ class SolidErrors < Base
4
+ def recent(time_range:, limit:)
5
+ ::SolidErrors::Occurrence
6
+ .where("created_at >= ?", time_range.ago)
7
+ .order(created_at: :desc)
8
+ .limit(limit)
9
+ end
10
+
11
+ def format_for_report(exception, number)
12
+ context = parse_context(exception)
13
+
14
+ <<~TEXT
15
+ *#{number}. #{exception_title(exception)}*
16
+ • *Time:* #{exception.created_at.strftime("%I:%M %p")}
17
+ #{format_controller(context)}
18
+ #{format_message(exception, context)}
19
+ TEXT
20
+ end
21
+
22
+ def format_for_banner(exception)
23
+ context = parse_context(exception)
24
+
25
+ # Extract message
26
+ message = if exception.respond_to?(:message) && exception.message.present?
27
+ exception.message.to_s
28
+ elsif context["message"].present?
29
+ context["message"].to_s
30
+ else
31
+ +""
32
+ end
33
+
34
+ # Extract location from context
35
+ location = if context["controller"] && context["action"]
36
+ "#{context["controller"]}##{context["action"]}"
37
+ else
38
+ +""
39
+ end
40
+
41
+ {
42
+ title: exception_title(exception),
43
+ message: message.truncate(100),
44
+ location: location,
45
+ time: exception.created_at.strftime("%I:%M %p")
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ def exception_title(exception)
52
+ if exception.respond_to?(:error_class) && exception.error_class.present?
53
+ exception.error_class
54
+ else
55
+ "Unknown Exception"
56
+ end
57
+ end
58
+
59
+ def parse_context(exception)
60
+ return {} unless exception.respond_to?(:context) && exception.context.present?
61
+
62
+ # SolidErrors context might be a hash or JSON string
63
+ if exception.context.is_a?(Hash)
64
+ exception.context
65
+ elsif exception.context.is_a?(String)
66
+ JSON.parse(exception.context)
67
+ else
68
+ {}
69
+ end
70
+ rescue JSON::ParserError
71
+ {}
72
+ end
73
+
74
+ def format_controller(context)
75
+ return +"" unless context["controller"] && context["action"]
76
+
77
+ "• *Controller:* #{context["controller"]}##{context["action"]}\n"
78
+ end
79
+
80
+ def format_message(exception, context = nil)
81
+ context ||= parse_context(exception)
82
+
83
+ # Try to get message from different sources
84
+ message = if exception.respond_to?(:message) && exception.message.present?
85
+ exception.message
86
+ elsif context["message"].present?
87
+ context["message"]
88
+ end
89
+
90
+ return +"" unless message.present?
91
+
92
+ message = message.to_s.truncate(100)
93
+ "• *Message:* `#{message}`"
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,12 @@
1
+ Dir[File.join(__dir__, "exceptions", "*.rb")].each { |file| require file }
2
+
3
+ module Newshound
4
+ module Exceptions
5
+ def self.source(source)
6
+ constant = constants.find { |c| c.to_s.gsub(/(?<!^)([A-Z])/, "_\\1").downcase == source.to_s }
7
+ raise "Invalid exception source: #{source}" unless constant
8
+
9
+ const_get(constant).new
10
+ end
11
+ end
12
+ end
@@ -15,7 +15,7 @@ module Newshound
15
15
  return [status, headers, response] unless status == 200
16
16
 
17
17
  # Check authorization
18
- controller = env['action_controller.instance']
18
+ controller = env["action_controller.instance"]
19
19
  return [status, headers, response] unless controller
20
20
  return [status, headers, response] unless Newshound::Authorization.authorized?(controller)
21
21
 
@@ -26,8 +26,8 @@ module Newshound
26
26
  new_response = inject_banner(response, banner_html)
27
27
 
28
28
  # Update Content-Length header
29
- if headers['Content-Length']
30
- headers['Content-Length'] = new_response.bytesize.to_s
29
+ if headers["Content-Length"]
30
+ headers["Content-Length"] = new_response.bytesize.to_s
31
31
  end
32
32
 
33
33
  [status, headers, [new_response]]
@@ -36,8 +36,8 @@ module Newshound
36
36
  private
37
37
 
38
38
  def html_response?(headers)
39
- content_type = headers['Content-Type']
40
- content_type && content_type.include?('text/html')
39
+ content_type = headers["Content-Type"]
40
+ content_type&.include?("text/html")
41
41
  end
42
42
 
43
43
  def inject_banner(response, banner_html)
@@ -48,7 +48,7 @@ module Newshound
48
48
  end
49
49
 
50
50
  def response_body(response)
51
- body = ""
51
+ body = +""
52
52
  response.each { |part| body << part }
53
53
  body
54
54
  end
@@ -57,8 +57,8 @@ module Newshound
57
57
  exception_reporter = Newshound::ExceptionReporter.new
58
58
  que_reporter = Newshound::QueReporter.new
59
59
 
60
- exception_data = exception_reporter.report
61
- job_data = que_reporter.report
60
+ exception_data = exception_reporter.banner_data
61
+ job_data = que_reporter.banner_data
62
62
 
63
63
  # Generate HTML from template
64
64
  render_banner(exception_data, job_data)
@@ -68,7 +68,7 @@ module Newshound
68
68
  <<~HTML
69
69
  <div id="newshound-banner" class="newshound-banner newshound-collapsed">
70
70
  #{render_styles}
71
- <div class="newshound-header" onclick="document.getElementById('newshound-banner').classList.toggle('newshound-collapsed')">
71
+ <div class="newshound-header" onclick="document.getElementById('newshound-banner').classList.toggle('newshound-collapsed'); window.newshoundUpdatePadding();">
72
72
  <span class="newshound-title">
73
73
  🐕 Newshound
74
74
  #{summary_badge(exception_data, job_data)}
@@ -80,12 +80,46 @@ module Newshound
80
80
  #{render_jobs(job_data)}
81
81
  </div>
82
82
  </div>
83
+ #{render_script}
83
84
  HTML
84
85
  end
85
86
 
87
+ def render_script
88
+ <<~JS
89
+ <script>
90
+ (function() {
91
+ window.newshoundUpdatePadding = function() {
92
+ // Wait for transition to complete
93
+ setTimeout(function() {
94
+ var banner = document.getElementById('newshound-banner');
95
+ if (banner) {
96
+ var height = banner.offsetHeight;
97
+ document.body.style.paddingTop = height + 'px';
98
+ }
99
+ }, 300);
100
+ };
101
+
102
+ // Set initial padding after page load
103
+ if (document.readyState === 'loading') {
104
+ document.addEventListener('DOMContentLoaded', window.newshoundUpdatePadding);
105
+ } else {
106
+ window.newshoundUpdatePadding();
107
+ }
108
+
109
+ // Also update on window resize
110
+ window.addEventListener('resize', window.newshoundUpdatePadding);
111
+ })();
112
+ </script>
113
+ JS
114
+ end
115
+
86
116
  def render_styles
87
117
  <<~CSS
88
118
  <style>
119
+ body {
120
+ padding-top: 0;
121
+ transition: padding-top 0.3s ease-out;
122
+ }
89
123
  .newshound-banner {
90
124
  position: fixed;
91
125
  top: 0;
@@ -275,13 +309,13 @@ module Newshound
275
309
  end
276
310
 
277
311
  def escape_html(text)
278
- return "" unless text
312
+ return +"" unless text.present?
279
313
  text.to_s
280
- .gsub('&', '&amp;')
281
- .gsub('<', '&lt;')
282
- .gsub('>', '&gt;')
283
- .gsub('"', '&quot;')
284
- .gsub("'", '&#39;')
314
+ .gsub("&", "&amp;")
315
+ .gsub("<", "&lt;")
316
+ .gsub(">", "&gt;")
317
+ .gsub('"', "&quot;")
318
+ .gsub("'", "&#39;")
285
319
  end
286
320
  end
287
321
  end
@@ -6,7 +6,7 @@ module Newshound
6
6
 
7
7
  def initialize(job_source: nil, logger: nil)
8
8
  @job_source = job_source || (defined?(::Que::Job) ? ::Que::Job : nil)
9
- @logger = logger || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT))
9
+ @logger = logger || (defined?(Rails) ? Rails.logger : Logger.new($stdout))
10
10
  end
11
11
 
12
12
  def generate_report
@@ -22,14 +22,29 @@ module Newshound
22
22
  queue_health_section
23
23
  ].compact
24
24
  end
25
+ alias_method :report, :generate_report
26
+
27
+ # Returns data formatted for the banner UI
28
+ def banner_data
29
+ stats = queue_statistics
30
+
31
+ {
32
+ queue_stats: {
33
+ ready_to_run: stats[:ready],
34
+ scheduled: stats[:scheduled],
35
+ failed: stats[:failed],
36
+ completed_today: stats[:finished_today]
37
+ }
38
+ }
39
+ end
25
40
 
26
41
  private
27
42
 
28
43
  def job_counts_section
29
44
  counts = job_counts_by_type
30
-
45
+
31
46
  return no_jobs_section if counts.empty?
32
-
47
+
33
48
  {
34
49
  type: "section",
35
50
  text: {
@@ -42,37 +57,49 @@ module Newshound
42
57
  def job_counts_by_type
43
58
  return {} unless job_source
44
59
 
45
- job_source
46
- .group(:job_class)
47
- .group(:error_count)
48
- .count
49
- .each_with_object({}) do |((job_class, error_count), count), hash|
50
- hash[job_class] ||= { success: 0, failed: 0, total: 0 }
51
-
52
- if error_count.zero?
53
- hash[job_class][:success] += count
54
- else
55
- hash[job_class][:failed] += count
56
- end
57
-
58
- hash[job_class][:total] += count
60
+ # Use raw SQL since Que::Job may not support ActiveRecord's .group method
61
+ results = ActiveRecord::Base.connection.execute(<<~SQL)
62
+ SELECT job_class, error_count, COUNT(*) as count
63
+ FROM que_jobs
64
+ WHERE finished_at IS NULL
65
+ GROUP BY job_class, error_count
66
+ ORDER BY job_class
67
+ SQL
68
+
69
+ results.each_with_object({}) do |row, hash|
70
+ job_class = row["job_class"]
71
+ error_count = row["error_count"].to_i
72
+ count = row["count"].to_i
73
+
74
+ hash[job_class] ||= {success: 0, failed: 0, total: 0}
75
+
76
+ if error_count.zero?
77
+ hash[job_class][:success] += count
78
+ else
79
+ hash[job_class][:failed] += count
59
80
  end
81
+
82
+ hash[job_class][:total] += count
83
+ end
84
+ rescue => e
85
+ logger.error "Failed to fetch job counts: #{e.message}"
86
+ {}
60
87
  end
61
88
 
62
89
  def format_job_counts(counts)
63
90
  lines = ["*Job Counts by Type:*"]
64
-
91
+
65
92
  counts.each do |job_class, stats|
66
- status_emoji = stats[:failed] > 0 ? "⚠️" : "✅"
93
+ status_emoji = (stats[:failed] > 0) ? "⚠️" : "✅"
67
94
  lines << "• #{status_emoji} *#{job_class}*: #{stats[:total]} total (#{stats[:success]} success, #{stats[:failed]} failed)"
68
95
  end
69
-
96
+
70
97
  lines.join("\n")
71
98
  end
72
99
 
73
100
  def queue_health_section
74
101
  stats = queue_statistics
75
-
102
+
76
103
  {
77
104
  type: "section",
78
105
  text: {
@@ -85,27 +112,38 @@ module Newshound
85
112
  def queue_statistics
86
113
  return default_stats unless job_source
87
114
 
88
- current_time = Time.now
89
- beginning_of_day = Date.today.to_time
115
+ conn = ActiveRecord::Base.connection
116
+ current_time = conn.quote(Time.now)
117
+ beginning_of_day = conn.quote(Date.today.to_time)
90
118
 
91
119
  {
92
- ready: job_source.where(finished_at: nil, expired_at: nil).where("run_at <= ?", current_time).count,
93
- scheduled: job_source.where(finished_at: nil, expired_at: nil).where("run_at > ?", current_time).count,
94
- failed: job_source.where.not(error_count: 0).where(finished_at: nil).count,
95
- finished_today: job_source.where("finished_at >= ?", beginning_of_day).count
120
+ ready: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at <= #{current_time}"),
121
+ scheduled: count_jobs("finished_at IS NULL AND expired_at IS NULL AND run_at > #{current_time}"),
122
+ failed: count_jobs("error_count > 0 AND finished_at IS NULL"),
123
+ finished_today: count_jobs("finished_at >= #{beginning_of_day}")
96
124
  }
97
- rescue StandardError => e
125
+ rescue => e
98
126
  logger.error "Failed to fetch Que statistics: #{e.message}"
99
127
  default_stats
100
128
  end
101
129
 
130
+ def count_jobs(where_clause)
131
+ ActiveRecord::Base.connection.select_value(
132
+ "SELECT COUNT(*) FROM que_jobs WHERE #{where_clause}"
133
+ ).to_i
134
+ end
135
+
102
136
  def default_stats
103
- { ready: 0, scheduled: 0, failed: 0, finished_today: 0 }
137
+ {ready: 0, scheduled: 0, failed: 0, finished_today: 0}
104
138
  end
105
139
 
106
140
  def format_queue_health(stats)
107
- health_emoji = stats[:failed] > 10 ? "🔴" : stats[:failed] > 5 ? "🟡" : "🟢"
108
-
141
+ health_emoji = if stats[:failed] > 10
142
+ "🔴"
143
+ else
144
+ (stats[:failed] > 5) ? "🟡" : "🟢"
145
+ end
146
+
109
147
  <<~TEXT
110
148
  *Queue Health #{health_emoji}*
111
149
  • *Ready to Run:* #{stats[:ready]}
@@ -125,4 +163,4 @@ module Newshound
125
163
  }
126
164
  end
127
165
  end
128
- end
166
+ end
@@ -19,9 +19,9 @@ module Newshound
19
19
  puts "Newshound Configuration:"
20
20
  puts " Enabled: #{config.enabled}"
21
21
  puts " Exception Limit: #{config.exception_limit}"
22
- puts " Authorized Roles: #{config.authorized_roles.join(', ')}"
22
+ puts " Authorized Roles: #{config.authorized_roles.join(", ")}"
23
23
  puts " Current User Method: #{config.current_user_method}"
24
- puts " Custom Authorization: #{config.authorization_block.present? ? 'Yes' : 'No'}"
24
+ puts " Custom Authorization: #{config.authorization_block.present? ? "Yes" : "No"}"
25
25
  end
26
26
 
27
27
  desc "Test exception reporter"
@@ -56,4 +56,4 @@ module Newshound
56
56
  end
57
57
  end
58
58
  end
59
- end
59
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Newshound
4
- VERSION = "0.2.0"
5
- end
4
+ VERSION = "0.2.2"
5
+ end
data/lib/newshound.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "newshound/version"
4
4
  require_relative "newshound/configuration"
5
+ require_relative "newshound/exceptions"
5
6
  require_relative "newshound/exception_reporter"
6
7
  require_relative "newshound/que_reporter"
7
8
  require_relative "newshound/authorization"
@@ -25,4 +26,4 @@ module Newshound
25
26
  configuration.authorize_with(&block)
26
27
  end
27
28
  end
28
- end
29
+ end
data/newshound.gemspec CHANGED
@@ -5,14 +5,14 @@ require_relative "lib/newshound/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "newshound"
7
7
  spec.version = Newshound::VERSION
8
- spec.authors = ["salbanez"]
9
- spec.email = ["salbanez@example.com"]
8
+ spec.authors = ["Savannah Moore"]
9
+ spec.email = ["savannah.albanez@sofwarellc.com"]
10
10
 
11
11
  spec.summary = "Real-time web UI banner for monitoring Que jobs and exception tracking"
12
12
  spec.description = "Newshound displays exceptions and job statuses in a collapsible banner for authorized users in your Rails app"
13
- spec.homepage = "https://github.com/salbanez/newshound"
13
+ spec.homepage = "https://github.com/SOFware/newshound"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7.0"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
@@ -30,5 +30,4 @@ Gem::Specification.new do |spec|
30
30
  # Runtime dependencies
31
31
  spec.add_dependency "rails", ">= 6.0"
32
32
  spec.add_dependency "que", ">= 1.0"
33
- spec.add_dependency "exception-track", ">= 0.1"
34
- end
33
+ end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: newshound
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
- - salbanez
7
+ - Savannah Moore
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -37,30 +37,17 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.0'
40
- - !ruby/object:Gem::Dependency
41
- name: exception-track
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0.1'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0.1'
54
40
  description: Newshound displays exceptions and job statuses in a collapsible banner
55
41
  for authorized users in your Rails app
56
42
  email:
57
- - salbanez@example.com
43
+ - savannah.albanez@sofwarellc.com
58
44
  executables: []
59
45
  extensions: []
60
46
  extra_rdoc_files: []
61
47
  files:
62
48
  - ".rspec"
63
49
  - ".tool-versions"
50
+ - CHANGELOG.md
64
51
  - Gemfile
65
52
  - README.md
66
53
  - Rakefile
@@ -71,18 +58,22 @@ files:
71
58
  - lib/newshound/authorization.rb
72
59
  - lib/newshound/configuration.rb
73
60
  - lib/newshound/exception_reporter.rb
61
+ - lib/newshound/exceptions.rb
62
+ - lib/newshound/exceptions/base.rb
63
+ - lib/newshound/exceptions/exception_track.rb
64
+ - lib/newshound/exceptions/solid_errors.rb
74
65
  - lib/newshound/middleware/banner_injector.rb
75
66
  - lib/newshound/que_reporter.rb
76
67
  - lib/newshound/railtie.rb
77
68
  - lib/newshound/version.rb
78
69
  - newshound.gemspec
79
- homepage: https://github.com/salbanez/newshound
70
+ homepage: https://github.com/SOFware/newshound
80
71
  licenses:
81
72
  - MIT
82
73
  metadata:
83
- homepage_uri: https://github.com/salbanez/newshound
84
- source_code_uri: https://github.com/salbanez/newshound
85
- changelog_uri: https://github.com/salbanez/newshound/blob/main/CHANGELOG.md
74
+ homepage_uri: https://github.com/SOFware/newshound
75
+ source_code_uri: https://github.com/SOFware/newshound
76
+ changelog_uri: https://github.com/SOFware/newshound/blob/main/CHANGELOG.md
86
77
  rdoc_options: []
87
78
  require_paths:
88
79
  - lib
@@ -90,7 +81,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
81
  requirements:
91
82
  - - ">="
92
83
  - !ruby/object:Gem::Version
93
- version: 2.7.0
84
+ version: 3.1.0
94
85
  required_rubygems_version: !ruby/object:Gem::Requirement
95
86
  requirements:
96
87
  - - ">="