oopsie_exceptions 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 38724bd4e7e3113063179a4306f5207544e2c8933491ab429433d5294c00f04a
4
+ data.tar.gz: c42960e5299dfce450d2cd7992cc3fe5acf1c92ab667c41c555a2c8b90a258a5
5
+ SHA512:
6
+ metadata.gz: aa0c4ed76d142a98fc0b1097ad6956a637296eaa0da494762d839c2b94aecd4eabd061cc5d18e6c22113652ab401e2d74d6767ac86bccaed9c77ea620b990e7e
7
+ data.tar.gz: 39de699f9980f8151a55098257c75b50b000300718031baea8b9b8322cd96d8cb49f766a41dfddec615338d3835ece207b0192438f2730146df69d67a8427c48
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Troy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # OopsieExceptions
2
+
3
+ Lightweight exception capture and webhook delivery for Rails. Like Sentry/Rollbar, but self-hosted and webhook-driven.
4
+
5
+ Captures unhandled exceptions from web requests and background jobs, enriches them with request/user/server context, and delivers structured JSON payloads to configurable webhook endpoints.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "oopsie_exceptions"
13
+ ```
14
+
15
+ Then run the install generator:
16
+
17
+ ```bash
18
+ bin/rails generate oopsie_exceptions:install
19
+ ```
20
+
21
+ This creates:
22
+ - `config/initializers/oopsie_exceptions.rb` — configuration
23
+ - `app/jobs/oopsie_exceptions/delivery_job.rb` — async webhook delivery
24
+
25
+ ## Configuration
26
+
27
+ Edit `config/initializers/oopsie_exceptions.rb`:
28
+
29
+ ```ruby
30
+ OopsieExceptions.configure do |config|
31
+ config.add_webhook "https://your-endpoint.com/webhooks/exceptions",
32
+ headers: { "Authorization" => "Bearer #{ENV['OOPSIE_WEBHOOK_TOKEN']}" }
33
+
34
+ config.app_name = "MyApp"
35
+ config.environment = Rails.env
36
+ config.enabled = Rails.env.production?
37
+ end
38
+ ```
39
+
40
+ ## What happens automatically
41
+
42
+ - **Rack middleware** inserted after `DebugExceptions` catches unhandled exceptions
43
+ - **Rails.error subscriber** captures framework-reported errors
44
+ - Request context (URL, IP, params, headers) is collected automatically
45
+ - Webhooks are delivered async via ActiveJob
46
+
47
+ ## Adding user context
48
+
49
+ In your `ApplicationController`:
50
+
51
+ ```ruby
52
+ before_action :set_oopsie_context
53
+
54
+ private
55
+
56
+ def set_oopsie_context
57
+ OopsieExceptions.set_context(
58
+ user: current_user ? { id: current_user.id, email: current_user.email } : nil,
59
+ action: "#{self.class.name}##{action_name}"
60
+ )
61
+ end
62
+ ```
63
+
64
+ ## Manual reporting
65
+
66
+ ```ruby
67
+ begin
68
+ risky_operation
69
+ rescue => e
70
+ OopsieExceptions.report(e, context: { order_id: 123 }, handled: true)
71
+ end
72
+ ```
73
+
74
+ ## Multiple webhooks
75
+
76
+ ```ruby
77
+ config.add_webhook "https://your-api.com/exceptions"
78
+ config.add_webhook "https://hooks.slack.com/services/..."
79
+ config.add_webhook "https://discord.com/api/webhooks/..."
80
+ ```
81
+
82
+ Each endpoint receives every exception.
83
+
84
+ ## Payload format
85
+
86
+ Each webhook receives a JSON payload with:
87
+
88
+ - **exception** — class, message, backtrace, cause chain
89
+ - **request** — URL, method, IP, params, headers, user agent
90
+ - **context** — user info and custom data you set
91
+ - **server** — hostname, PID, Ruby/Rails versions
92
+ - **app** — name, environment
93
+ - **timestamp** — UTC ISO8601
94
+
95
+ ## Filtering
96
+
97
+ By default, common noise exceptions are ignored (404s, routing errors, `ActionController::BadRequest`, etc.). Sensitive parameters (`password`, `token`, `secret`, `api_key`) and headers (`Authorization`, `Cookie`) are automatically filtered from payloads.
98
+
99
+ Customize in your initializer:
100
+
101
+ ```ruby
102
+ config.ignored_exceptions += ["MyApp::IgnorableError"]
103
+ config.filter_parameters += [:credit_card]
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT — see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates an OopsieExceptions initializer and delivery job"
9
+
10
+ def create_initializer
11
+ template "initializer.rb", "config/initializers/oopsie_exceptions.rb"
12
+ end
13
+
14
+ def create_delivery_job
15
+ template "delivery_job.rb", "app/jobs/oopsie_exceptions/delivery_job.rb"
16
+ end
17
+
18
+ def show_post_install
19
+ say ""
20
+ say "OopsieExceptions installed!", :green
21
+ say ""
22
+ say "Next steps:"
23
+ say " 1. Edit config/initializers/oopsie_exceptions.rb to add your webhook URLs"
24
+ say " 2. Optionally add user context in ApplicationController:"
25
+ say ""
26
+ say " before_action :set_oopsie_context"
27
+ say ""
28
+ say " def set_oopsie_context"
29
+ say " OopsieExceptions.set_context("
30
+ say " user: current_user ? { id: current_user.id, email: current_user.email } : nil,"
31
+ say " action: \"\#{self.class.name}#\#{action_name}\""
32
+ say " )"
33
+ say " end"
34
+ say ""
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ module OopsieExceptions
2
+ class DeliveryJob < ApplicationJob
3
+ queue_as :default
4
+
5
+ discard_on StandardError do |job, error|
6
+ Rails.logger.error("[OopsieExceptions] DeliveryJob failed permanently: #{error.message}")
7
+ end
8
+
9
+ retry_on Net::OpenTimeout, Net::ReadTimeout, wait: 5.seconds, attempts: 3
10
+
11
+ def perform(payload_json, webhook_url, headers_json)
12
+ webhook = OopsieExceptions::Configuration::Webhook.new(
13
+ url: webhook_url,
14
+ headers: JSON.parse(headers_json),
15
+ name: webhook_url
16
+ )
17
+
18
+ OopsieExceptions::WebhookClient.post(webhook, payload_json)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ OopsieExceptions.configure do |config|
2
+ # Add webhook endpoints — exceptions get POSTed here as JSON
3
+ # config.add_webhook "https://your-endpoint.com/webhooks/exceptions",
4
+ # headers: { "Authorization" => "Bearer <%= "#{ENV['OOPSIE_WEBHOOK_TOKEN']}" %>" },
5
+ # name: "primary"
6
+
7
+ # config.add_webhook "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
8
+ # config.add_webhook "https://discord.com/api/webhooks/YOUR/DISCORD/WEBHOOK"
9
+
10
+ config.app_name = Rails.application.class.module_parent_name
11
+ config.environment = Rails.env
12
+
13
+ # Deliver webhooks async via ActiveJob (set false for sync delivery)
14
+ config.async_delivery = true
15
+
16
+ # Exceptions that won't be reported (404s, bot garbage, etc.)
17
+ # config.ignore_exception "ActionController::RoutingError"
18
+
19
+ # Filter sensitive params from payloads (inherits from Rails by default)
20
+ config.filter_parameters = Rails.application.config.filter_parameters.map(&:to_s)
21
+
22
+ # Set false to disable in dev/test
23
+ config.enabled = Rails.env.production? || Rails.env.staging?
24
+
25
+ # Optional: modify or drop payloads before sending
26
+ # config.before_notify = ->(payload) {
27
+ # payload[:context][:deploy_sha] = ENV["GIT_SHA"]
28
+ # payload # return nil to skip this notification
29
+ # }
30
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ class Configuration
5
+ attr_accessor :webhook_urls, :app_name, :environment,
6
+ :ignored_exceptions, :filter_parameters,
7
+ :filter_headers, :capture_request_body,
8
+ :async_delivery, :timeout, :open_timeout,
9
+ :backtrace_cleaner, :before_notify,
10
+ :enabled
11
+
12
+ def initialize
13
+ @webhook_urls = []
14
+ @app_name = defined?(Rails) ? (Rails.application.class.module_parent_name rescue "App") : "App"
15
+ @environment = defined?(Rails) ? (Rails.env rescue "development") : "development"
16
+ @ignored_exceptions = default_ignored_exceptions
17
+ @filter_parameters = %w[password password_confirmation secret token api_key]
18
+ @filter_headers = %w[Authorization Cookie Set-Cookie]
19
+ @capture_request_body = false
20
+ @async_delivery = true
21
+ @timeout = 10
22
+ @open_timeout = 5
23
+ @backtrace_cleaner = nil
24
+ @before_notify = nil
25
+ @enabled = true
26
+ end
27
+
28
+ def add_webhook(url, headers: {}, name: nil)
29
+ @webhook_urls << Webhook.new(url: url, headers: headers, name: name || url)
30
+ end
31
+
32
+ def ignore_exception(*class_names)
33
+ @ignored_exceptions.concat(class_names.map(&:to_s))
34
+ end
35
+
36
+ def ignored?(exception)
37
+ @ignored_exceptions.include?(exception.class.name)
38
+ end
39
+
40
+ private
41
+
42
+ def default_ignored_exceptions
43
+ %w[
44
+ ActionController::RoutingError
45
+ ActionController::UnknownFormat
46
+ ActionController::BadRequest
47
+ ActionDispatch::Http::MimeNegotiation::InvalidType
48
+ AbstractController::ActionNotFound
49
+ ActiveRecord::RecordNotFound
50
+ ActionController::UnknownHttpMethod
51
+ ]
52
+ end
53
+
54
+ Webhook = Data.define(:url, :headers, :name)
55
+ end
56
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ module Context
5
+ THREAD_KEY = :oopsie_exceptions_context
6
+
7
+ class << self
8
+ def current
9
+ Thread.current[THREAD_KEY] ||= {}
10
+ end
11
+
12
+ def merge(hash)
13
+ current.merge!(hash)
14
+ end
15
+
16
+ def replace(hash)
17
+ Thread.current[THREAD_KEY] = hash
18
+ end
19
+
20
+ def clear
21
+ Thread.current[THREAD_KEY] = nil
22
+ end
23
+
24
+ def from_rack_env(env)
25
+ request = ActionDispatch::Request.new(env)
26
+ config = OopsieExceptions.configuration
27
+
28
+ ctx = {
29
+ request: {
30
+ url: request.original_url,
31
+ method: request.method,
32
+ ip: request.remote_ip,
33
+ user_agent: request.user_agent,
34
+ referer: request.referer,
35
+ request_id: request.request_id,
36
+ params: sanitize_params(request.filtered_parameters),
37
+ headers: extract_headers(env, config)
38
+ }
39
+ }
40
+
41
+ if config.capture_request_body && request.content_type&.include?("application/json")
42
+ body = request.body.read
43
+ request.body.rewind
44
+ ctx[:request][:body] = body.truncate(10_000) if body.present?
45
+ end
46
+
47
+ ctx
48
+ end
49
+
50
+ private
51
+
52
+ def sanitize_params(params)
53
+ params.except("controller", "action").to_h
54
+ rescue
55
+ {}
56
+ end
57
+
58
+ def extract_headers(env, config)
59
+ headers = {}
60
+ env.each do |key, value|
61
+ next unless key.start_with?("HTTP_")
62
+ header_name = key.sub("HTTP_", "").split("_").map(&:capitalize).join("-")
63
+ next if config.filter_headers.any? { |h| h.casecmp(header_name) == 0 }
64
+ headers[header_name] = value
65
+ end
66
+ headers
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ class ErrorSubscriber
5
+ def report(error, handled:, severity:, context: {}, source: nil)
6
+ return if severity == :warning
7
+
8
+ merged_context = context.merge(
9
+ rails_error_reporter: true,
10
+ severity: severity.to_s,
11
+ source: source
12
+ ).compact
13
+
14
+ OopsieExceptions.report(error, context: merged_context, handled: handled)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ class Middleware
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ Context.clear
11
+
12
+ begin
13
+ request_context = Context.from_rack_env(env)
14
+ Context.merge(request_context)
15
+
16
+ response = @app.call(env)
17
+
18
+ if response[0].to_i >= 500
19
+ Context.merge(response_status: response[0].to_i)
20
+ end
21
+
22
+ response
23
+ rescue Exception => exception
24
+ Context.merge(response_status: 500)
25
+ OopsieExceptions.report(exception, handled: false)
26
+ raise
27
+ ensure
28
+ Context.clear
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ class Payload
5
+ MAX_CAUSE_DEPTH = 10
6
+
7
+ class << self
8
+ def build(exception, context: {}, handled: true)
9
+ config = OopsieExceptions.configuration
10
+ backtrace = clean_backtrace(exception, config)
11
+
12
+ {
13
+ notifier: "OopsieExceptions",
14
+ version: VERSION,
15
+ timestamp: Time.now.utc.iso8601(3),
16
+ app: {
17
+ name: config.app_name,
18
+ environment: config.environment
19
+ },
20
+ error: {
21
+ class_name: exception.class.name,
22
+ message: exception.message.to_s[0, 10_000],
23
+ backtrace: backtrace,
24
+ first_line: parse_backtrace_line(backtrace&.first),
25
+ causes: collect_causes(exception),
26
+ handled: handled
27
+ },
28
+ context: Context.current.merge(context),
29
+ server: server_info
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def server_info
36
+ info = {
37
+ hostname: Socket.gethostname,
38
+ pid: Process.pid,
39
+ ruby_version: RUBY_VERSION
40
+ }
41
+ info[:rails_version] = Rails::VERSION::STRING if defined?(Rails::VERSION)
42
+ info
43
+ end
44
+
45
+ def clean_backtrace(exception, config)
46
+ bt = exception.backtrace || []
47
+ cleaner = config.backtrace_cleaner || default_cleaner
48
+ cleaner ? cleaner.clean(bt) : bt
49
+ end
50
+
51
+ def default_cleaner
52
+ Rails.backtrace_cleaner if defined?(Rails)
53
+ rescue
54
+ nil
55
+ end
56
+
57
+ def collect_causes(exception)
58
+ causes = []
59
+ current = exception.cause
60
+ depth = 0
61
+
62
+ while current && depth < MAX_CAUSE_DEPTH
63
+ causes << {
64
+ class_name: current.class.name,
65
+ message: current.message.to_s[0, 1_000],
66
+ first_line: parse_backtrace_line(current.backtrace&.first)
67
+ }
68
+ current = current.cause
69
+ depth += 1
70
+ end
71
+
72
+ causes
73
+ end
74
+
75
+ def parse_backtrace_line(line)
76
+ return nil unless line
77
+ match = line.match(/\A(.+):(\d+):in [`'](.+)'\z/)
78
+ return { raw: line } unless match
79
+
80
+ {
81
+ file: match[1],
82
+ line: match[2].to_i,
83
+ method: match[3]
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ class Railtie < Rails::Railtie
5
+ initializer "oopsie_exceptions.middleware" do |app|
6
+ app.middleware.insert_after ActionDispatch::DebugExceptions, OopsieExceptions::Middleware
7
+ end
8
+
9
+ initializer "oopsie_exceptions.error_subscriber", after: :load_config_initializers do
10
+ if Rails.respond_to?(:error) && OopsieExceptions.configuration.enabled
11
+ Rails.error.subscribe(OopsieExceptions::ErrorSubscriber.new)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OopsieExceptions
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module OopsieExceptions
8
+ class WebhookClient
9
+ class << self
10
+ def post(webhook, payload)
11
+ uri = URI.parse(webhook.url)
12
+ config = OopsieExceptions.configuration
13
+
14
+ http = Net::HTTP.new(uri.host, uri.port)
15
+ http.use_ssl = uri.scheme == "https"
16
+ http.open_timeout = config.open_timeout
17
+ http.read_timeout = config.timeout
18
+
19
+ request = Net::HTTP::Post.new(uri.request_uri)
20
+ request["Content-Type"] = "application/json"
21
+ request["User-Agent"] = "OopsieExceptions/#{VERSION}"
22
+
23
+ webhook.headers.each { |k, v| request[k] = v }
24
+
25
+ request.body = payload.is_a?(String) ? payload : payload.to_json
26
+
27
+ response = http.request(request)
28
+
29
+ unless response.is_a?(Net::HTTPSuccess)
30
+ log_warn("Webhook #{webhook.name} responded #{response.code}: #{response.body.to_s[0, 500]}")
31
+ end
32
+
33
+ response
34
+ rescue => e
35
+ log_error("Failed to deliver to #{webhook.name}: #{e.message}")
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ def log_warn(message)
42
+ if defined?(Rails.logger) && Rails.logger
43
+ Rails.logger.warn("[OopsieExceptions] #{message}")
44
+ else
45
+ warn("[OopsieExceptions] #{message}")
46
+ end
47
+ end
48
+
49
+ def log_error(message)
50
+ if defined?(Rails.logger) && Rails.logger
51
+ Rails.logger.error("[OopsieExceptions] #{message}")
52
+ else
53
+ warn("[OopsieExceptions] ERROR: #{message}")
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "oopsie_exceptions/version"
4
+ require_relative "oopsie_exceptions/configuration"
5
+ require_relative "oopsie_exceptions/context"
6
+ require_relative "oopsie_exceptions/payload"
7
+ require_relative "oopsie_exceptions/webhook_client"
8
+ require_relative "oopsie_exceptions/middleware"
9
+ require_relative "oopsie_exceptions/error_subscriber"
10
+ require_relative "oopsie_exceptions/railtie" if defined?(Rails::Railtie)
11
+
12
+ module OopsieExceptions
13
+ REPORTED_MARKER = :@__oopsie_reported
14
+
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield configuration
22
+ end
23
+
24
+ def report(exception, context: {}, handled: true)
25
+ return unless configuration.enabled
26
+ return if configuration.ignored?(exception)
27
+ return if exception.instance_variable_get(REPORTED_MARKER)
28
+
29
+ exception.instance_variable_set(REPORTED_MARKER, true)
30
+
31
+ payload = Payload.build(exception, context: context, handled: handled)
32
+
33
+ if configuration.before_notify
34
+ payload = configuration.before_notify.call(payload)
35
+ return if payload.nil?
36
+ end
37
+
38
+ deliver(payload)
39
+ end
40
+
41
+ def set_context(hash)
42
+ Context.merge(hash)
43
+ end
44
+
45
+ def clear_context
46
+ Context.clear
47
+ end
48
+
49
+ def with_context(hash)
50
+ previous = Context.current.dup
51
+ Context.merge(hash)
52
+ yield
53
+ ensure
54
+ Context.replace(previous)
55
+ end
56
+
57
+ private
58
+
59
+ def deliver(payload)
60
+ configuration.webhook_urls.each do |webhook|
61
+ if configuration.async_delivery && defined?(OopsieExceptions::DeliveryJob)
62
+ DeliveryJob.perform_later(
63
+ payload.to_json,
64
+ webhook.url,
65
+ webhook.headers.to_json
66
+ )
67
+ else
68
+ WebhookClient.post(webhook, payload)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oopsie_exceptions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Troy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: actionpack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '7.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '7.0'
55
+ description: Captures unhandled exceptions from web requests and background jobs,
56
+ enriches them with request/user/server context, and delivers structured JSON payloads
57
+ to configurable webhook endpoints.
58
+ email:
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE.txt
64
+ - README.md
65
+ - lib/generators/oopsie_exceptions/install_generator.rb
66
+ - lib/generators/oopsie_exceptions/templates/delivery_job.rb
67
+ - lib/generators/oopsie_exceptions/templates/initializer.rb
68
+ - lib/oopsie_exceptions.rb
69
+ - lib/oopsie_exceptions/configuration.rb
70
+ - lib/oopsie_exceptions/context.rb
71
+ - lib/oopsie_exceptions/error_subscriber.rb
72
+ - lib/oopsie_exceptions/middleware.rb
73
+ - lib/oopsie_exceptions/payload.rb
74
+ - lib/oopsie_exceptions/railtie.rb
75
+ - lib/oopsie_exceptions/version.rb
76
+ - lib/oopsie_exceptions/webhook_client.rb
77
+ homepage: https://github.com/theinventor/oopsie_exceptions
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ source_code_uri: https://github.com/theinventor/oopsie_exceptions
82
+ changelog_uri: https://github.com/theinventor/oopsie_exceptions/releases
83
+ rubygems_mfa_required: 'true'
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '3.1'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.5.23
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Lightweight exception capture and webhook delivery for Rails
103
+ test_files: []