error_radar 1.0.5 → 1.0.7

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: c45267122a7ded92caa20eb6eb1d34f40ac5691222a8f69f0d32e7418f3f9934
4
- data.tar.gz: d51638ec8c8e79d536c9b81bee62fb09381d9ce1dc3096bfe3161dc74566e3a5
3
+ metadata.gz: 97d73a27edbc8351ce6ff95be373a45ac9213057b332affd769980223a643b37
4
+ data.tar.gz: d079439643d93715219b0297184f3b595d8cf609a6bb8cd74633d714aa142033
5
5
  SHA512:
6
- metadata.gz: 74f8c559c90cb25db39413355a61f987fc2c6cb4ca406327a6bb47a379b68e4ddaed048b4b9da538ce9aabfec3c1556fceecc6b9c9580a6c5acf15c2446b067d
7
- data.tar.gz: 5feb61f2843942481bb718f69617a3ebf0a1b64d21aea0c5f4c1ea33f60775ace0afb127d6eee708fb4efb3396a3976a814f0d1ccd929045ebac598a98a76831
6
+ metadata.gz: f55e7e0ca86fb47c6d739fdc13816380cfbc58378f8fea6b75b2db8867393a537ced7d50bca30e81025c5fb5cc2c5fe5ff9c12956b7314eaa16ab8962690da09
7
+ data.tar.gz: 77610793d856a3dbc46621ad99edb937724a8280edaa90e03bc3a65e7b2e180b4ddc77ae1476699b5129d11b63b00aa5daca4f5db88f8596ae7322b21db04957
@@ -10,13 +10,13 @@ module ErrorRadar
10
10
  SEVERITY_ORDER = { 'critical' => 0, 'error' => 1, 'warning' => 2, 'info' => 3 }.freeze
11
11
 
12
12
  def index
13
+ # group(:status).count in Rails 7 returns string enum name keys:
14
+ # {"open"=>100, "resolved"=>97070, ...}
13
15
  counts_by_status = ErrorLog.group(:status).count
14
- # statuses returns {"open"=>0, "in_progress"=>1, "resolved"=>2, "ignored"=>3}
15
- inv = ErrorLog.statuses.invert
16
- @open_count = counts_by_status[ErrorLog.statuses['open']] || 0
17
- @in_progress = counts_by_status[ErrorLog.statuses['in_progress']] || 0
18
- @resolved_count = counts_by_status[ErrorLog.statuses['resolved']] || 0
19
- @ignored_count = counts_by_status[ErrorLog.statuses['ignored']] || 0
16
+ @open_count = counts_by_status['open'] || 0
17
+ @in_progress = counts_by_status['in_progress'] || 0
18
+ @resolved_count = counts_by_status['resolved'] || 0
19
+ @ignored_count = counts_by_status['ignored'] || 0
20
20
  @total = counts_by_status.values.sum
21
21
  @unresolved = @open_count + @in_progress
22
22
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ class SettingsController < ApplicationController
5
+ before_action :authenticate_request!
6
+
7
+ CHATWORK_KEYS = %w[chatwork_api_token chatwork_room_id chatwork_notify_sources].freeze
8
+
9
+ def show
10
+ @settings = CHATWORK_KEYS.each_with_object({}) do |key, h|
11
+ h[key] = Setting.get(key)
12
+ end
13
+ # notify_sources is stored as an array; join for the textarea
14
+ @settings['chatwork_notify_sources'] = Array(@settings['chatwork_notify_sources']).join("\n")
15
+ end
16
+
17
+ def update
18
+ CHATWORK_KEYS.each do |key|
19
+ value = settings_params[key]
20
+ next if value.nil?
21
+
22
+ if key == 'chatwork_notify_sources'
23
+ lines = value.split(/[\n,]+/).map(&:strip).reject(&:empty?)
24
+ Setting.set(key, lines)
25
+ else
26
+ Setting.set(key, value.strip.presence)
27
+ end
28
+ end
29
+
30
+ redirect_to settings_path, notice: 'Settings saved.'
31
+ end
32
+
33
+ private
34
+
35
+ def settings_params
36
+ params.require(:settings).permit(*CHATWORK_KEYS)
37
+ rescue ActionController::ParameterMissing
38
+ {}
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErrorRadar
4
+ # Key-value store for UI-configurable settings (e.g. ChatWork credentials).
5
+ # Values are JSON-encoded so arrays/booleans survive the round-trip.
6
+ class Setting < ApplicationRecord
7
+ validates :key, presence: true, uniqueness: true
8
+
9
+ CHATWORK_KEYS = %w[
10
+ chatwork_api_token
11
+ chatwork_room_id
12
+ chatwork_notify_sources
13
+ ].freeze
14
+
15
+ def self.get(key)
16
+ row = find_by(key: key.to_s)
17
+ return nil if row.nil? || row.value.nil?
18
+
19
+ JSON.parse(row.value)
20
+ rescue JSON::ParserError
21
+ row.value
22
+ end
23
+
24
+ def self.set(key, value)
25
+ row = find_or_initialize_by(key: key.to_s)
26
+ row.value = value.nil? ? nil : value.to_json
27
+ row.save!
28
+ end
29
+
30
+ def self.chatwork_settings
31
+ CHATWORK_KEYS.each_with_object({}) do |key, hash|
32
+ hash[key] = get(key)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ <h1>Settings</h1>
2
+ <p class="sub">Configure notification channels. Changes take effect immediately — no deploy needed.</p>
3
+
4
+ <% if notice.present? %>
5
+ <div style="background:#d1fae5;border:1px solid #6ee7b7;color:#065f46;padding:10px 14px;border-radius:8px;margin-bottom:16px;font-size:13px;">
6
+ <%= notice %>
7
+ </div>
8
+ <% end %>
9
+
10
+ <%= form_with url: settings_path, method: :patch, html: { style: 'max-width:640px' } do |f| %>
11
+ <div class="panel" style="margin-bottom:20px;">
12
+ <h2>ChatWork Notifications</h2>
13
+ <p style="font-size:12px;color:#6b7280;margin:0 0 16px;">
14
+ Send error alerts to a ChatWork room. Leave Token or Room ID blank to disable.
15
+ </p>
16
+
17
+ <div style="margin-bottom:14px;">
18
+ <label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px;" for="chatwork_api_token">
19
+ API Token
20
+ </label>
21
+ <%= f.text_field 'settings[chatwork_api_token]',
22
+ value: @settings['chatwork_api_token'],
23
+ id: 'chatwork_api_token',
24
+ placeholder: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
25
+ style: 'width:100%;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;font-family:monospace;' %>
26
+ </div>
27
+
28
+ <div style="margin-bottom:14px;">
29
+ <label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px;" for="chatwork_room_id">
30
+ Room ID
31
+ </label>
32
+ <%= f.text_field 'settings[chatwork_room_id]',
33
+ value: @settings['chatwork_room_id'],
34
+ id: 'chatwork_room_id',
35
+ placeholder: '123456789',
36
+ style: 'width:100%;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;font-family:monospace;' %>
37
+ </div>
38
+
39
+ <div style="margin-bottom:20px;">
40
+ <label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px;" for="chatwork_notify_sources">
41
+ Notify Sources <span style="font-weight:400;color:#6b7280;">(one per line — leave blank to notify all sources)</span>
42
+ </label>
43
+ <%= f.text_area 'settings[chatwork_notify_sources]',
44
+ value: @settings['chatwork_notify_sources'],
45
+ id: 'chatwork_notify_sources',
46
+ placeholder: "Token::ValidatorService\nPostScheduler::",
47
+ rows: 5,
48
+ style: 'width:100%;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;font-family:monospace;resize:vertical;' %>
49
+ <p style="font-size:11px;color:#6b7280;margin:4px 0 0;">
50
+ Substring match. An error is sent to ChatWork only when its <strong>source</strong> contains one of these strings.
51
+ </p>
52
+ </div>
53
+
54
+ <div>
55
+ <%= f.submit 'Save settings',
56
+ style: 'background:#2563eb;color:#fff;border:none;padding:9px 20px;border-radius:6px;font-size:13px;cursor:pointer;font-weight:600;' %>
57
+ </div>
58
+ </div>
59
+ <% end %>
@@ -57,6 +57,7 @@
57
57
  <a href="<%= root_path %>" class="nav-link <%= 'active' if controller_name == 'dashboard' %>">Dashboard</a>
58
58
  <a href="<%= errors_path %>" class="nav-link <%= 'active' if controller_name == 'errors' %>">All Errors</a>
59
59
  <a href="<%= guide_path %>" class="nav-link <%= 'active' if controller_name == 'guide' %>">Guide</a>
60
+ <a href="<%= settings_path %>" class="nav-link <%= 'active' if controller_name == 'settings' %>">Settings</a>
60
61
  </div>
61
62
  </nav>
62
63
  <div class="wrap"><%= yield %></div>
data/config/routes.rb CHANGED
@@ -22,4 +22,7 @@ ErrorRadar::Engine.routes.draw do
22
22
  delete 'errors/:id', to: 'errors#destroy'
23
23
 
24
24
  post 'maintenance/purge', to: 'dashboard#purge', as: :maintenance_purge
25
+
26
+ get 'settings', to: 'settings#show', as: :settings
27
+ patch 'settings', to: 'settings#update'
25
28
  end
@@ -57,6 +57,16 @@ module ErrorRadar
57
57
  # One or more plain HTTPS URLs that receive a POST with JSON body on each alert.
58
58
  attr_accessor :webhook_urls
59
59
 
60
+ # ChatWork ────────────────────────────────────────────────────────────────
61
+ # API token from https://www.chatwork.com/service/packages/chatwork/subpackages/api/apply_token.php
62
+ attr_accessor :chatwork_api_token
63
+ # Room ID to post messages into (numeric string).
64
+ attr_accessor :chatwork_room_id
65
+ # Filter by source: array of strings/regexps. Only errors whose `source`
66
+ # matches at least one entry are sent to ChatWork. nil or [] = notify all.
67
+ # Example: ['Maintain::CheckSidekiqServ', /FetchInsights/]
68
+ attr_accessor :chatwork_notify_sources
69
+
60
70
  # App name shown in notification subject/title. Defaults to Rails app name.
61
71
  attr_accessor :app_name
62
72
  # Base URL used to build deep-links (e.g. "https://myapp.com").
@@ -165,6 +175,9 @@ module ErrorRadar
165
175
  @email_recipients = []
166
176
  @email_from = nil
167
177
  @webhook_urls = []
178
+ @chatwork_api_token = nil
179
+ @chatwork_room_id = nil
180
+ @chatwork_notify_sources = []
168
181
  @app_name = nil
169
182
  @app_host = nil
170
183
  @error_callbacks = []
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module ErrorRadar
7
+ module Notifications
8
+ module Chatwork
9
+ API_BASE = 'https://api.chatwork.com/v2'.freeze
10
+
11
+ def self.deliver(log, event = :recurring)
12
+ cfg = ErrorRadar.config
13
+ token, room_id, = ErrorRadar::Notifier.chatwork_db_settings(cfg)
14
+ return if token.empty? || room_id.empty?
15
+
16
+ uri = URI("#{API_BASE}/rooms/#{room_id}/messages")
17
+ http = Net::HTTP.new(uri.host, uri.port)
18
+ http.use_ssl = true
19
+ http.open_timeout = 5
20
+ http.read_timeout = 5
21
+
22
+ req = Net::HTTP::Post.new(uri)
23
+ req['X-ChatWorkToken'] = token
24
+ req['Content-Type'] = 'application/x-www-form-urlencoded'
25
+ req.body = URI.encode_www_form(body: build_message(log, event))
26
+
27
+ http.request(req)
28
+ rescue StandardError => e
29
+ ErrorRadar::Tracking.warn_internal("ChatWork notification failed: #{e.message}")
30
+ end
31
+
32
+ def self.build_message(log, event)
33
+ app = ErrorRadar::Notifier.app_name
34
+ link = ErrorRadar::Notifier.error_url(log)
35
+
36
+ title = case event
37
+ when :spike
38
+ spike = log.instance_variable_get(:@spike_data)
39
+ "Spike — #{spike[:count]} hits in #{spike[:window_minutes]} min: #{log.error_class}"
40
+ when :new_error
41
+ "New error: #{log.error_class}"
42
+ else
43
+ "#{log.severity.capitalize} error: #{log.error_class}"
44
+ end
45
+
46
+ body_lines = []
47
+ body_lines << "Source: #{log.source || 'unknown'}"
48
+ body_lines << "Category: #{log.category}"
49
+ body_lines << "Severity: #{log.severity}"
50
+ body_lines << "Occurrences: #{log.occurrences}"
51
+ body_lines << "Message: #{log.message.to_s.truncate(300)}"
52
+ body_lines << "URL: #{link}" if link
53
+
54
+ "[info][title][#{app}] #{title}[/title]#{body_lines.join("\n")}[/info]"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -82,6 +82,7 @@ module ErrorRadar
82
82
  send_slack(log, event) if cfg.slack_webhook_url.to_s.start_with?('http')
83
83
  send_discord(log, event) if cfg.discord_webhook_url.to_s.start_with?('http')
84
84
  send_email(log, event) if cfg.email_recipients.any?
85
+ send_chatwork(log, event) if chatwork_enabled?(cfg, log)
85
86
  cfg.webhook_urls.each { |url| send_webhook(log, url, event) }
86
87
  cfg.error_callbacks.each { |cb| safe_call(cb, log) }
87
88
  end
@@ -106,6 +107,42 @@ module ErrorRadar
106
107
  Notifications::Webhook.deliver(log, url: url, event: event)
107
108
  end
108
109
 
110
+ def self.send_chatwork(log, event)
111
+ require 'error_radar/notifications/chatwork'
112
+ Notifications::Chatwork.deliver(log, event)
113
+ end
114
+
115
+ def self.chatwork_enabled?(cfg, log)
116
+ token, room_id, sources = chatwork_db_settings(cfg)
117
+ return false if token.empty? || room_id.empty?
118
+
119
+ return true if sources.empty?
120
+
121
+ source = log.source.to_s
122
+ sources.any? { |filter| filter.is_a?(Regexp) ? filter.match?(source) : source.include?(filter.to_s) }
123
+ end
124
+
125
+ # Returns [token, room_id, sources_array] — DB settings take priority over config.
126
+ def self.chatwork_db_settings(cfg)
127
+ if defined?(ErrorRadar::Setting) && ErrorRadar::Setting.table_exists?
128
+ token = ErrorRadar::Setting.get('chatwork_api_token').to_s
129
+ room_id = ErrorRadar::Setting.get('chatwork_room_id').to_s
130
+ sources = Array(ErrorRadar::Setting.get('chatwork_notify_sources')).compact
131
+ # Fall back to config when DB row is empty
132
+ token = cfg.chatwork_api_token.to_s if token.empty?
133
+ room_id = cfg.chatwork_room_id.to_s if room_id.empty?
134
+ sources = Array(cfg.chatwork_notify_sources).compact if sources.empty?
135
+ else
136
+ token = cfg.chatwork_api_token.to_s
137
+ room_id = cfg.chatwork_room_id.to_s
138
+ sources = Array(cfg.chatwork_notify_sources).compact
139
+ end
140
+ [token, room_id, sources]
141
+ rescue StandardError
142
+ [cfg.chatwork_api_token.to_s, cfg.chatwork_room_id.to_s,
143
+ Array(cfg.chatwork_notify_sources).compact]
144
+ end
145
+
109
146
  def self.safe_call(cb, log)
110
147
  cb.call(log)
111
148
  rescue StandardError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErrorRadar
4
- VERSION = '1.0.5'
4
+ VERSION = '1.0.7'
5
5
  end
@@ -0,0 +1,10 @@
1
+ class CreateErrorRadarSettings < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :error_radar_settings do |t|
4
+ t.string :key, null: false
5
+ t.text :value
6
+ t.timestamps
7
+ end
8
+ add_index :error_radar_settings, :key, unique: true
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record'
4
+
5
+ module ErrorRadar
6
+ module Generators
7
+ class UpgradeV107Generator < ActiveRecord::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ argument :name, type: :string, default: 'upgrade_v107'
11
+
12
+ desc 'Creates the error_radar_settings table for UI-configurable notification settings (v1.0.7).'
13
+
14
+ def create_migration
15
+ migration_template 'create_error_radar_settings.rb.tt',
16
+ 'db/migrate/create_error_radar_settings.rb'
17
+ end
18
+ end
19
+ end
20
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: error_radar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - chienbn9x
@@ -64,6 +64,7 @@ files:
64
64
  - app/controllers/error_radar/dashboard_controller.rb
65
65
  - app/controllers/error_radar/errors_controller.rb
66
66
  - app/controllers/error_radar/guide_controller.rb
67
+ - app/controllers/error_radar/settings_controller.rb
67
68
  - app/mailers/error_radar/digest_mailer.rb
68
69
  - app/mailers/error_radar/error_mailer.rb
69
70
  - app/models/error_radar/application_record.rb
@@ -71,6 +72,7 @@ files:
71
72
  - app/models/error_radar/error_comment.rb
72
73
  - app/models/error_radar/error_log.rb
73
74
  - app/models/error_radar/error_occurrence.rb
75
+ - app/models/error_radar/setting.rb
74
76
  - app/views/error_radar/dashboard/index.html.erb
75
77
  - app/views/error_radar/dashboard/show.html.erb
76
78
  - app/views/error_radar/digest_mailer/digest.html.erb
@@ -80,6 +82,7 @@ files:
80
82
  - app/views/error_radar/errors/index.html.erb
81
83
  - app/views/error_radar/errors/show.html.erb
82
84
  - app/views/error_radar/guide/index.html.erb
85
+ - app/views/error_radar/settings/show.html.erb
83
86
  - app/views/layouts/error_radar/application.html.erb
84
87
  - config/routes.rb
85
88
  - lib/error_radar.rb
@@ -94,6 +97,7 @@ files:
94
97
  - lib/error_radar/integrations/rake.rb
95
98
  - lib/error_radar/integrations/sidekiq.rb
96
99
  - lib/error_radar/middleware.rb
100
+ - lib/error_radar/notifications/chatwork.rb
97
101
  - lib/error_radar/notifications/discord.rb
98
102
  - lib/error_radar/notifications/email.rb
99
103
  - lib/error_radar/notifications/slack.rb
@@ -112,6 +116,8 @@ files:
112
116
  - lib/generators/error_radar/upgrade_v060/upgrade_v060_generator.rb
113
117
  - lib/generators/error_radar/upgrade_v100/templates/upgrade_to_v100.rb.tt
114
118
  - lib/generators/error_radar/upgrade_v100/upgrade_v100_generator.rb
119
+ - lib/generators/error_radar/upgrade_v107/templates/create_error_radar_settings.rb.tt
120
+ - lib/generators/error_radar/upgrade_v107/upgrade_v107_generator.rb
115
121
  - lib/tasks/error_radar.rake
116
122
  homepage: https://github.com/chienbn9x/error_radar
117
123
  licenses: