rails-informant 0.0.1

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +340 -0
  4. data/Rakefile +10 -0
  5. data/VERSION +1 -0
  6. data/app/controllers/rails_informant/api/base_controller.rb +62 -0
  7. data/app/controllers/rails_informant/api/errors_controller.rb +72 -0
  8. data/app/controllers/rails_informant/api/occurrences_controller.rb +14 -0
  9. data/app/controllers/rails_informant/api/status_controller.rb +29 -0
  10. data/app/jobs/rails_informant/application_job.rb +4 -0
  11. data/app/jobs/rails_informant/notify_job.rb +34 -0
  12. data/app/jobs/rails_informant/purge_job.rb +29 -0
  13. data/app/models/rails_informant/application_record.rb +5 -0
  14. data/app/models/rails_informant/error_group.rb +175 -0
  15. data/app/models/rails_informant/occurrence.rb +22 -0
  16. data/config/routes.rb +14 -0
  17. data/db/migrate/20260227000000_create_informant_tables.rb +65 -0
  18. data/exe/informant-mcp +27 -0
  19. data/lib/generators/rails_informant/devin/templates/error-triage.devin.md +48 -0
  20. data/lib/generators/rails_informant/devin_generator.rb +12 -0
  21. data/lib/generators/rails_informant/install_generator.rb +20 -0
  22. data/lib/generators/rails_informant/skill/templates/SKILL.md +168 -0
  23. data/lib/generators/rails_informant/skill_generator.rb +12 -0
  24. data/lib/generators/rails_informant/templates/create_informant_tables.rb.erb +55 -0
  25. data/lib/generators/rails_informant/templates/initializer.rb.erb +33 -0
  26. data/lib/rails_informant/breadcrumb_buffer.rb +30 -0
  27. data/lib/rails_informant/breadcrumb_subscriber.rb +51 -0
  28. data/lib/rails_informant/configuration.rb +51 -0
  29. data/lib/rails_informant/context_builder.rb +142 -0
  30. data/lib/rails_informant/context_filter.rb +45 -0
  31. data/lib/rails_informant/current.rb +5 -0
  32. data/lib/rails_informant/engine.rb +86 -0
  33. data/lib/rails_informant/error_recorder.rb +47 -0
  34. data/lib/rails_informant/error_subscriber.rb +17 -0
  35. data/lib/rails_informant/fingerprint.rb +23 -0
  36. data/lib/rails_informant/mcp/base_tool.rb +38 -0
  37. data/lib/rails_informant/mcp/client.rb +123 -0
  38. data/lib/rails_informant/mcp/configuration.rb +90 -0
  39. data/lib/rails_informant/mcp/server.rb +29 -0
  40. data/lib/rails_informant/mcp/tools/annotate_error.rb +25 -0
  41. data/lib/rails_informant/mcp/tools/delete_error.rb +25 -0
  42. data/lib/rails_informant/mcp/tools/get_error.rb +24 -0
  43. data/lib/rails_informant/mcp/tools/get_informant_status.rb +22 -0
  44. data/lib/rails_informant/mcp/tools/ignore_error.rb +24 -0
  45. data/lib/rails_informant/mcp/tools/list_environments.rb +20 -0
  46. data/lib/rails_informant/mcp/tools/list_errors.rb +32 -0
  47. data/lib/rails_informant/mcp/tools/list_occurrences.rb +27 -0
  48. data/lib/rails_informant/mcp/tools/mark_duplicate.rb +25 -0
  49. data/lib/rails_informant/mcp/tools/mark_fix_pending.rb +27 -0
  50. data/lib/rails_informant/mcp/tools/reopen_error.rb +24 -0
  51. data/lib/rails_informant/mcp/tools/resolve_error.rb +24 -0
  52. data/lib/rails_informant/mcp.rb +22 -0
  53. data/lib/rails_informant/middleware/error_capture.rb +28 -0
  54. data/lib/rails_informant/middleware/rescued_exception_interceptor.rb +16 -0
  55. data/lib/rails_informant/notifiers/devin.rb +61 -0
  56. data/lib/rails_informant/notifiers/notification_policy.rb +85 -0
  57. data/lib/rails_informant/notifiers/slack.rb +77 -0
  58. data/lib/rails_informant/notifiers/webhook.rb +31 -0
  59. data/lib/rails_informant/structured_event_subscriber.rb +14 -0
  60. data/lib/rails_informant/version.rb +3 -0
  61. data/lib/rails_informant.rb +147 -0
  62. data/lib/tasks/rails_informant.rake +30 -0
  63. metadata +177 -0
@@ -0,0 +1,77 @@
1
+ module RailsInformant
2
+ module Notifiers
3
+ class Slack
4
+ include NotificationPolicy
5
+
6
+ def notify(error_group, occurrence)
7
+ post_json \
8
+ url: RailsInformant.slack_webhook_url,
9
+ body: build_payload(error_group, occurrence),
10
+ label: "Slack webhook"
11
+ end
12
+
13
+ private
14
+
15
+ def regression?(error_group)
16
+ error_group.status == "unresolved" && error_group.fix_deployed_at.present?
17
+ end
18
+
19
+ def build_payload(error_group, occurrence)
20
+ regression_tag = regression?(error_group) ? " [REGRESSION]" : ""
21
+
22
+ {
23
+ blocks: [
24
+ {
25
+ type: "header",
26
+ text: {
27
+ type: "plain_text",
28
+ text: "#{error_group.error_class}#{regression_tag}",
29
+ emoji: true
30
+ }
31
+ },
32
+ {
33
+ type: "section",
34
+ fields: [
35
+ { type: "mrkdwn", text: "*Message:*\n#{error_group.message.to_s.truncate(200)}" },
36
+ { type: "mrkdwn", text: "*Status:*\n#{error_group.status}" },
37
+ { type: "mrkdwn", text: "*Occurrences:*\n#{error_group.total_occurrences}" },
38
+ { type: "mrkdwn", text: "*First seen:*\n#{error_group.first_seen_at&.iso8601}" }
39
+ ]
40
+ },
41
+ {
42
+ type: "section",
43
+ fields: [
44
+ location_field(error_group),
45
+ { type: "mrkdwn", text: "*Severity:*\n#{error_group.severity}" }
46
+ ].compact
47
+ },
48
+ context_block(occurrence)
49
+ ].compact
50
+ }
51
+ end
52
+
53
+ def location_field(error_group)
54
+ location = error_group.controller_action || error_group.job_class || error_group.first_backtrace_line
55
+ return unless location
56
+
57
+ { type: "mrkdwn", text: "*Location:*\n`#{location.to_s.truncate(100)}`" }
58
+ end
59
+
60
+ def context_block(occurrence)
61
+ return unless occurrence
62
+
63
+ elements = []
64
+ if occurrence.git_sha
65
+ elements << { type: "mrkdwn", text: "Deploy: `#{occurrence.git_sha[0, 7]}`" }
66
+ end
67
+ if occurrence.environment_context&.dig("hostname")
68
+ elements << { type: "mrkdwn", text: "Host: `#{occurrence.environment_context["hostname"]}`" }
69
+ end
70
+
71
+ return if elements.empty?
72
+
73
+ { type: "context", elements: elements }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,31 @@
1
+ module RailsInformant
2
+ module Notifiers
3
+ class Webhook
4
+ include NotificationPolicy
5
+
6
+ def notify(error_group, occurrence)
7
+ post_json \
8
+ url: RailsInformant.webhook_url,
9
+ body: build_payload(error_group, occurrence),
10
+ label: "Webhook"
11
+ end
12
+
13
+ private
14
+
15
+ def build_payload(error_group, occurrence)
16
+ {
17
+ error_class: error_group.error_class,
18
+ fingerprint: error_group.fingerprint,
19
+ message: error_group.message,
20
+ severity: error_group.severity,
21
+ status: error_group.status,
22
+ total_occurrences: error_group.total_occurrences,
23
+ controller_action: error_group.controller_action,
24
+ job_class: error_group.job_class,
25
+ first_seen_at: error_group.first_seen_at&.iso8601,
26
+ last_seen_at: error_group.last_seen_at&.iso8601
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ module RailsInformant
2
+ class StructuredEventSubscriber
3
+ def emit(event)
4
+ return unless RailsInformant.initialized?
5
+
6
+ BreadcrumbBuffer.current.record(
7
+ category: event[:name],
8
+ message: event[:name],
9
+ metadata: event[:payload].is_a?(Hash) ? RailsInformant::ContextFilter.filter(event[:payload]) : {},
10
+ duration: nil
11
+ )
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module RailsInformant
2
+ VERSION = File.read(File.expand_path("../../../VERSION", __FILE__)).strip
3
+ end
@@ -0,0 +1,147 @@
1
+ require "rails_informant/version"
2
+ require "rails_informant/configuration"
3
+
4
+ module RailsInformant
5
+ InvalidParameterError = Class.new(StandardError)
6
+ NotifierError = Class.new(StandardError)
7
+
8
+ IGNORED_EXCEPTIONS_DEFAULT = %w[
9
+ AbstractController::ActionNotFound
10
+ ActionController::BadRequest
11
+ ActionController::InvalidAuthenticityToken
12
+ ActionController::InvalidCrossOriginRequest
13
+ ActionController::MethodNotAllowed
14
+ ActionController::NotImplemented
15
+ ActionController::ParameterMissing
16
+ ActionController::RoutingError
17
+ ActionController::UnknownAction
18
+ ActionController::UnknownFormat
19
+ ActionController::UnknownHttpMethod
20
+ ActionController::UrlGenerationError
21
+ ActionDispatch::Http::MimeNegotiation::InvalidType
22
+ ActiveRecord::RecordNotFound
23
+ CGI::Session::CookieStore::TamperedWithCookie
24
+ Mime::Type::InvalidMimeType
25
+ Rack::QueryParser::InvalidParameterError
26
+ Rack::QueryParser::ParameterTypeError
27
+ Rack::Utils::InvalidParameterError
28
+ SignalException
29
+ SystemExit
30
+ ].freeze
31
+
32
+ GIT_SHA_SOURCES = %w[GIT_SHA REVISION KAMAL_VERSION].freeze
33
+
34
+ autoload :BreadcrumbBuffer, "rails_informant/breadcrumb_buffer"
35
+ autoload :BreadcrumbSubscriber, "rails_informant/breadcrumb_subscriber"
36
+ autoload :ContextBuilder, "rails_informant/context_builder"
37
+ autoload :ContextFilter, "rails_informant/context_filter"
38
+ autoload :Current, "rails_informant/current"
39
+ autoload :ErrorRecorder, "rails_informant/error_recorder"
40
+ autoload :ErrorSubscriber, "rails_informant/error_subscriber"
41
+ autoload :Fingerprint, "rails_informant/fingerprint"
42
+ autoload :StructuredEventSubscriber, "rails_informant/structured_event_subscriber"
43
+
44
+ module Middleware
45
+ autoload :ErrorCapture, "rails_informant/middleware/error_capture"
46
+ autoload :RescuedExceptionInterceptor, "rails_informant/middleware/rescued_exception_interceptor"
47
+ end
48
+
49
+ module Notifiers
50
+ autoload :Devin, "rails_informant/notifiers/devin"
51
+ autoload :NotificationPolicy, "rails_informant/notifiers/notification_policy"
52
+ autoload :Slack, "rails_informant/notifiers/slack"
53
+ autoload :Webhook, "rails_informant/notifiers/webhook"
54
+ end
55
+
56
+ mattr_accessor :config
57
+ self.config = Configuration.new
58
+
59
+ class << self
60
+ delegate :api_token,
61
+ :capture_errors,
62
+ :capture_user_email,
63
+ :devin_api_key,
64
+ :devin_playbook_id,
65
+ :ignored_exceptions,
66
+ :notifiers,
67
+ :retention_days,
68
+ :slack_webhook_url,
69
+ :webhook_url,
70
+ to: :config
71
+
72
+ def configure
73
+ yield config
74
+ reset_caches!
75
+ end
76
+
77
+ def reset_caches!
78
+ @_initialized = nil
79
+ @_ignored_set = nil
80
+ config.reset_notifiers!
81
+ end
82
+
83
+ def initialized?
84
+ return @_initialized if defined?(@_initialized) && @_initialized
85
+
86
+ @_initialized = capture_errors && defined?(ActiveRecord::Base) && ActiveRecord::Base.connection_pool.connected?
87
+ rescue ActiveRecord::ConnectionNotEstablished
88
+ false
89
+ end
90
+
91
+ def ignored_exception?(exception)
92
+ ignored = ignored_exception_set
93
+ exception.class.ancestors.each do |ancestor|
94
+ name = ancestor.name or next
95
+ return true if ignored.include?(name)
96
+ end
97
+ false
98
+ end
99
+
100
+ def current_git_sha
101
+ @_current_git_sha ||= resolve_git_sha
102
+ end
103
+
104
+ def already_captured?(error)
105
+ error.instance_variable_get(:@__rails_informant_captured)
106
+ end
107
+
108
+ def capture(exception, context: {}, request: nil)
109
+ return if already_captured?(exception)
110
+ mark_captured!(exception)
111
+ ErrorRecorder.record exception, severity: "error", context:, env: request&.env
112
+ end
113
+
114
+ def mark_captured!(error)
115
+ error.instance_variable_set(:@__rails_informant_captured, true) unless error.frozen?
116
+ end
117
+
118
+ def server_mode?
119
+ defined?(Rails::Server)
120
+ end
121
+
122
+ private
123
+
124
+ def ignored_exception_set
125
+ @_ignored_set ||= Set.new(IGNORED_EXCEPTIONS_DEFAULT + Array(ignored_exceptions)).freeze
126
+ end
127
+
128
+ def resolve_git_sha
129
+ GIT_SHA_SOURCES.each do |key|
130
+ return ENV[key] if ENV[key].present?
131
+ end
132
+
133
+ head = Rails.root.join(".git", "HEAD").read.strip
134
+ if head.start_with?("ref: ")
135
+ ref = head.delete_prefix("ref: ")
136
+ raise ArgumentError if ref.include?("..") # path traversal guard
137
+ Rails.root.join(".git", ref).read.strip
138
+ else
139
+ head
140
+ end
141
+ rescue Errno::ENOENT, Errno::EACCES, ArgumentError
142
+ nil
143
+ end
144
+ end
145
+ end
146
+
147
+ require "rails_informant/engine"
@@ -0,0 +1,30 @@
1
+ namespace :informant do
2
+ desc "Purge resolved errors older than retention_days"
3
+ task purge: :environment do
4
+ RailsInformant::PurgeJob.perform_now
5
+ puts "Purge complete"
6
+ end
7
+
8
+ desc "Show error monitoring statistics"
9
+ task stats: :environment do
10
+ groups = RailsInformant::ErrorGroup
11
+ total = groups.count
12
+ unresolved = groups.where(status: "unresolved").count
13
+ fix_pending = groups.where(status: "fix_pending").count
14
+ resolved = groups.where(status: "resolved").count
15
+ ignored = groups.where(status: "ignored").count
16
+ duplicates = groups.where(status: "duplicate").count
17
+ occurrences = RailsInformant::Occurrence.count
18
+
19
+ puts "Rails Informant Statistics"
20
+ puts "-" * 30
21
+ puts "Error groups: #{total}"
22
+ puts " Unresolved: #{unresolved}"
23
+ puts " Fix pending: #{fix_pending}"
24
+ puts " Resolved: #{resolved}"
25
+ puts " Ignored: #{ignored}"
26
+ puts " Duplicates: #{duplicates}"
27
+ puts "Occurrences: #{occurrences}"
28
+ puts "Deploy SHA: #{RailsInformant.current_git_sha || 'unknown'}"
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-informant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel López Prat
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '8.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activejob
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '8.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '8.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activerecord
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '8.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '8.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: activesupport
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '8.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '8.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: railties
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '8.1'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '8.1'
82
+ description: Rails Engine that captures exceptions, stores them in your database with
83
+ rich context, and exposes error data via a bundled MCP server so AI agents can triage,
84
+ resolve, and fix errors autonomously.
85
+ email:
86
+ - daniel@6temes.cat
87
+ executables:
88
+ - informant-mcp
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - LICENSE
93
+ - README.md
94
+ - Rakefile
95
+ - VERSION
96
+ - app/controllers/rails_informant/api/base_controller.rb
97
+ - app/controllers/rails_informant/api/errors_controller.rb
98
+ - app/controllers/rails_informant/api/occurrences_controller.rb
99
+ - app/controllers/rails_informant/api/status_controller.rb
100
+ - app/jobs/rails_informant/application_job.rb
101
+ - app/jobs/rails_informant/notify_job.rb
102
+ - app/jobs/rails_informant/purge_job.rb
103
+ - app/models/rails_informant/application_record.rb
104
+ - app/models/rails_informant/error_group.rb
105
+ - app/models/rails_informant/occurrence.rb
106
+ - config/routes.rb
107
+ - db/migrate/20260227000000_create_informant_tables.rb
108
+ - exe/informant-mcp
109
+ - lib/generators/rails_informant/devin/templates/error-triage.devin.md
110
+ - lib/generators/rails_informant/devin_generator.rb
111
+ - lib/generators/rails_informant/install_generator.rb
112
+ - lib/generators/rails_informant/skill/templates/SKILL.md
113
+ - lib/generators/rails_informant/skill_generator.rb
114
+ - lib/generators/rails_informant/templates/create_informant_tables.rb.erb
115
+ - lib/generators/rails_informant/templates/initializer.rb.erb
116
+ - lib/rails_informant.rb
117
+ - lib/rails_informant/breadcrumb_buffer.rb
118
+ - lib/rails_informant/breadcrumb_subscriber.rb
119
+ - lib/rails_informant/configuration.rb
120
+ - lib/rails_informant/context_builder.rb
121
+ - lib/rails_informant/context_filter.rb
122
+ - lib/rails_informant/current.rb
123
+ - lib/rails_informant/engine.rb
124
+ - lib/rails_informant/error_recorder.rb
125
+ - lib/rails_informant/error_subscriber.rb
126
+ - lib/rails_informant/fingerprint.rb
127
+ - lib/rails_informant/mcp.rb
128
+ - lib/rails_informant/mcp/base_tool.rb
129
+ - lib/rails_informant/mcp/client.rb
130
+ - lib/rails_informant/mcp/configuration.rb
131
+ - lib/rails_informant/mcp/server.rb
132
+ - lib/rails_informant/mcp/tools/annotate_error.rb
133
+ - lib/rails_informant/mcp/tools/delete_error.rb
134
+ - lib/rails_informant/mcp/tools/get_error.rb
135
+ - lib/rails_informant/mcp/tools/get_informant_status.rb
136
+ - lib/rails_informant/mcp/tools/ignore_error.rb
137
+ - lib/rails_informant/mcp/tools/list_environments.rb
138
+ - lib/rails_informant/mcp/tools/list_errors.rb
139
+ - lib/rails_informant/mcp/tools/list_occurrences.rb
140
+ - lib/rails_informant/mcp/tools/mark_duplicate.rb
141
+ - lib/rails_informant/mcp/tools/mark_fix_pending.rb
142
+ - lib/rails_informant/mcp/tools/reopen_error.rb
143
+ - lib/rails_informant/mcp/tools/resolve_error.rb
144
+ - lib/rails_informant/middleware/error_capture.rb
145
+ - lib/rails_informant/middleware/rescued_exception_interceptor.rb
146
+ - lib/rails_informant/notifiers/devin.rb
147
+ - lib/rails_informant/notifiers/notification_policy.rb
148
+ - lib/rails_informant/notifiers/slack.rb
149
+ - lib/rails_informant/notifiers/webhook.rb
150
+ - lib/rails_informant/structured_event_subscriber.rb
151
+ - lib/rails_informant/version.rb
152
+ - lib/tasks/rails_informant.rake
153
+ homepage: https://github.com/6temes/rails-informant
154
+ licenses:
155
+ - MIT
156
+ metadata:
157
+ bug_tracker_uri: https://github.com/6temes/rails-informant/issues
158
+ rubygems_mfa_required: 'true'
159
+ source_code_uri: https://github.com/6temes/rails-informant
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '4.0'
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubygems_version: 4.0.6
175
+ specification_version: 4
176
+ summary: Self-hosted error monitoring for Rails with MCP server for agentic workflows
177
+ test_files: []