bugstack 1.0.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: da0101ebd6b1cd809d68d700373e930e115813f35cc42f737e5963b18b9881fe
4
+ data.tar.gz: bef41397dd9156644ef76784feb204c0df41f491f469aafe9beb2f86692e18ea
5
+ SHA512:
6
+ metadata.gz: 3f1dad3f2f483114a07cfb8e3170455d8b82d678782653c4a2273ba2c3c19de46eb1be92782399184ebda9d3fe3bc051c1c8a0ca15bfe88962ef359c395c22a9
7
+ data.tar.gz: e52217eddf681a0c79e89ec20e449337915730eb3055a206a99bda2e101470aae0acaef4150342b6621e22294d382ae410f19e97c2719772ff25c6633f337c79
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.0.0] - 2026-02-13
6
+
7
+ ### Added
8
+
9
+ - Core SDK with `Bugstack.init` and `Bugstack.capture_exception`
10
+ - Background thread transport with retry and exponential backoff
11
+ - SHA-256 error fingerprinting and client-side deduplication
12
+ - `before_send` hook for event inspection/modification/filtering
13
+ - `ignored_errors` for skipping specific error types or messages
14
+ - `dry_run` mode for transparent debugging
15
+ - `enabled` kill switch
16
+ - Block-style configuration
17
+ - Rails integration via Railtie and Rack middleware
18
+ - Sinatra integration via `register`
19
+ - Generic integration via `at_exit` hook
20
+ - Zero runtime gem dependencies
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BugStack
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,174 @@
1
+ # bugstack-ruby
2
+
3
+ Official Ruby SDK for [BugStack](https://bugstack.dev) — capture, report, and auto-fix production errors.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/bugstack.svg)](https://rubygems.org/gems/bugstack)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ ## Installation
9
+
10
+ ```ruby
11
+ gem "bugstack"
12
+ ```
13
+
14
+ Or install directly:
15
+
16
+ ```bash
17
+ gem install bugstack
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```ruby
23
+ require "bugstack"
24
+
25
+ Bugstack.init(api_key: "bs_live_...")
26
+
27
+ begin
28
+ risky_operation
29
+ rescue => e
30
+ Bugstack.capture_exception(e)
31
+ end
32
+ ```
33
+
34
+ ## Block Configuration
35
+
36
+ ```ruby
37
+ Bugstack.init do |config|
38
+ config.api_key = "bs_live_..."
39
+ config.environment = "production"
40
+ config.auto_fix = true
41
+ config.debug = true
42
+ end
43
+ ```
44
+
45
+ ## Framework Integrations
46
+
47
+ ### Rails
48
+
49
+ Add to your Gemfile:
50
+
51
+ ```ruby
52
+ gem "bugstack"
53
+ ```
54
+
55
+ Create an initializer:
56
+
57
+ ```ruby
58
+ # config/initializers/bugstack.rb
59
+ Bugstack.init do |config|
60
+ config.api_key = Rails.application.credentials.bugstack_api_key
61
+ config.environment = Rails.env
62
+ config.auto_fix = true
63
+ end
64
+ ```
65
+
66
+ The Railtie automatically inserts Rack middleware for exception capture.
67
+
68
+ ### Sinatra
69
+
70
+ ```ruby
71
+ require "sinatra"
72
+ require "bugstack"
73
+ require "bugstack/integrations/sinatra"
74
+
75
+ Bugstack.init(api_key: "bs_live_...")
76
+
77
+ class MyApp < Sinatra::Base
78
+ register Bugstack::Integrations::Sinatra
79
+
80
+ get "/" do
81
+ "Hello!"
82
+ end
83
+ end
84
+ ```
85
+
86
+ ### Generic (at_exit hook)
87
+
88
+ ```ruby
89
+ require "bugstack"
90
+ require "bugstack/integrations/generic"
91
+
92
+ Bugstack.init(api_key: "bs_live_...")
93
+ Bugstack::Integrations::Generic.install!
94
+ ```
95
+
96
+ ## Configuration
97
+
98
+ ```ruby
99
+ Bugstack.init do |config|
100
+ config.api_key = "bs_live_..." # Required
101
+ config.environment = "production" # Default: "production"
102
+ config.auto_fix = true # Enable AI-powered auto-fix
103
+ config.debug = false # Log SDK activity
104
+ config.dry_run = false # Log without sending
105
+ config.enabled = true # Kill switch
106
+ config.deduplication_window = 300 # Seconds (default: 5 min)
107
+ config.timeout = 5.0 # HTTP timeout in seconds
108
+ config.max_retries = 3 # Retry attempts
109
+ config.ignored_errors = [ # Errors to skip
110
+ SystemExit,
111
+ SignalException,
112
+ "expected error message",
113
+ ]
114
+ config.before_send = ->(event) { # Inspect/modify/drop events
115
+ event # return nil to drop
116
+ }
117
+ end
118
+ ```
119
+
120
+ ## Data Transparency
121
+
122
+ ### `before_send` Hook
123
+
124
+ ```ruby
125
+ Bugstack.init do |config|
126
+ config.api_key = "bs_live_..."
127
+ config.before_send = ->(event) {
128
+ # Drop health check errors
129
+ return nil if event.request&.dig(:route)&.include?("/health")
130
+
131
+ # Redact sensitive data
132
+ event.metadata.delete("secret")
133
+
134
+ event
135
+ }
136
+ end
137
+ ```
138
+
139
+ ### `dry_run` Mode
140
+
141
+ ```ruby
142
+ Bugstack.init(api_key: "bs_live_...", dry_run: true)
143
+ # Prints: [BugStack DryRun] Would send: { ... }
144
+ ```
145
+
146
+ ## What Gets Sent
147
+
148
+ ```json
149
+ {
150
+ "apiKey": "bs_live_...",
151
+ "error": {
152
+ "message": "undefined method 'foo' for nil",
153
+ "stackTrace": "NoMethodError: undefined method...",
154
+ "file": "app/controllers/users_controller.rb",
155
+ "function": "show",
156
+ "fingerprint": "a1b2c3d4e5f6g7h8"
157
+ },
158
+ "environment": {
159
+ "language": "ruby",
160
+ "languageVersion": "3.2.0",
161
+ "framework": "rails",
162
+ "frameworkVersion": "7.1.0",
163
+ "os": "x86_64-linux",
164
+ "sdkVersion": "1.0.0"
165
+ },
166
+ "timestamp": "2026-01-15T08:30:00Z"
167
+ }
168
+ ```
169
+
170
+ Zero runtime gem dependencies. No cookies, IP addresses, or user data.
171
+
172
+ ## License
173
+
174
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugstack
4
+ # Core client for capturing and reporting errors to BugStack.
5
+ # Thread-safe. Handles deduplication, filtering, and transport.
6
+ class Client
7
+ attr_reader :config
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @deduplicator = Deduplicator.new(window: config.deduplication_window)
12
+ @transport = nil
13
+
14
+ if config.enabled && !config.dry_run
15
+ @transport = Transport.new(
16
+ endpoint: config.endpoint,
17
+ api_key: config.api_key,
18
+ timeout: config.timeout,
19
+ max_retries: config.max_retries,
20
+ debug: config.debug
21
+ )
22
+ end
23
+
24
+ log_debug("Client initialized (endpoint=#{config.endpoint}, dry_run=#{config.dry_run})")
25
+ end
26
+
27
+ # Capture an exception and send it to BugStack.
28
+ #
29
+ # @param exception [Exception]
30
+ # @param request [Hash, nil] { route:, method: }
31
+ # @param metadata [Hash, nil]
32
+ # @return [Boolean]
33
+ def capture_exception(exception, request: nil, metadata: nil)
34
+ do_capture(exception, request: request, metadata: metadata)
35
+ rescue => e
36
+ log_debug("Error during capture: #{e.message}")
37
+ false
38
+ end
39
+
40
+ # Shut down the client and flush pending events.
41
+ def shutdown
42
+ @transport&.shutdown
43
+ @transport = nil
44
+ end
45
+
46
+ private
47
+
48
+ def do_capture(exception, request: nil, metadata: nil)
49
+ return false unless @config.enabled
50
+
51
+ # Check ignored errors
52
+ if ignored?(exception)
53
+ log_debug("Error ignored: #{exception.class.name}")
54
+ return false
55
+ end
56
+
57
+ # Extract location info
58
+ exc_type, file, function, line = Fingerprint.extract_location(exception)
59
+ stack_trace = Fingerprint.format_backtrace(exception)
60
+
61
+ # Build event
62
+ event = Event.new(
63
+ message: exception.message,
64
+ stack_trace: stack_trace,
65
+ file: file,
66
+ function: function,
67
+ exception_type: exc_type,
68
+ fingerprint: Fingerprint.generate(exc_type, file, function, line),
69
+ request: request,
70
+ timestamp: Time.now.utc.iso8601,
71
+ metadata: metadata || {}
72
+ )
73
+
74
+ # before_send hook
75
+ if @config.before_send
76
+ event = @config.before_send.call(event)
77
+ if event.nil?
78
+ log_debug("Event dropped by before_send")
79
+ return false
80
+ end
81
+ end
82
+
83
+ # Deduplication
84
+ unless @deduplicator.should_send?(event.fingerprint)
85
+ log_debug("Event deduplicated: #{event.fingerprint}")
86
+ return false
87
+ end
88
+
89
+ # Build payload
90
+ payload = event.to_payload(@config)
91
+
92
+ # Dry run
93
+ if @config.dry_run
94
+ $stdout.puts "[BugStack DryRun] Would send: #{JSON.pretty_generate(payload)}"
95
+ return true
96
+ end
97
+
98
+ # Enqueue for sending
99
+ @transport&.enqueue(payload)
100
+ log_debug("Event queued: #{event.fingerprint}")
101
+ true
102
+ end
103
+
104
+ def ignored?(exception)
105
+ @config.ignored_errors.any? do |pattern|
106
+ case pattern
107
+ when Class
108
+ exception.is_a?(pattern)
109
+ when String
110
+ exception.message == pattern
111
+ else
112
+ false
113
+ end
114
+ end
115
+ end
116
+
117
+ def log_debug(msg)
118
+ return unless @config.debug
119
+
120
+ warn "[BugStack] #{msg}"
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugstack
4
+ # Configuration for the BugStack SDK.
5
+ #
6
+ # @example
7
+ # Bugstack.init do |config|
8
+ # config.api_key = "bs_live_..."
9
+ # config.environment = "production"
10
+ # config.auto_fix = true
11
+ # end
12
+ class Configuration
13
+ # @return [String] BugStack API key (required)
14
+ attr_accessor :api_key
15
+
16
+ # @return [String] BugStack API endpoint
17
+ attr_accessor :endpoint
18
+
19
+ # @return [String] Project identifier
20
+ attr_accessor :project_id
21
+
22
+ # @return [String] Environment name
23
+ attr_accessor :environment
24
+
25
+ # @return [Boolean] Enable autonomous error fixing
26
+ attr_accessor :auto_fix
27
+
28
+ # @return [Boolean] Kill switch — set to false to disable everything
29
+ attr_accessor :enabled
30
+
31
+ # @return [Boolean] Log SDK activity to console
32
+ attr_accessor :debug
33
+
34
+ # @return [Boolean] Log errors but don't send them
35
+ attr_accessor :dry_run
36
+
37
+ # @return [Float] Deduplication window in seconds
38
+ attr_accessor :deduplication_window
39
+
40
+ # @return [Float] HTTP timeout in seconds
41
+ attr_accessor :timeout
42
+
43
+ # @return [Integer] Max retry attempts
44
+ attr_accessor :max_retries
45
+
46
+ # @return [Array<Class, String>] Error types or messages to ignore
47
+ attr_accessor :ignored_errors
48
+
49
+ # @return [Proc, nil] Hook to inspect/modify/drop events before sending
50
+ attr_accessor :before_send
51
+
52
+ def initialize
53
+ @api_key = ""
54
+ @endpoint = "https://api.bugstack.dev/api/capture"
55
+ @project_id = ""
56
+ @environment = "production"
57
+ @auto_fix = false
58
+ @enabled = true
59
+ @debug = false
60
+ @dry_run = false
61
+ @deduplication_window = 300.0
62
+ @timeout = 5.0
63
+ @max_retries = 3
64
+ @ignored_errors = []
65
+ @before_send = nil
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Bugstack
7
+ # Represents an error event to be sent to BugStack.
8
+ class Event
9
+ attr_accessor :message, :stack_trace, :file, :function, :fingerprint,
10
+ :exception_type, :request, :environment, :timestamp, :metadata
11
+
12
+ def initialize(
13
+ message:,
14
+ stack_trace: "",
15
+ file: "",
16
+ function: "",
17
+ fingerprint: "",
18
+ exception_type: "",
19
+ request: nil,
20
+ environment: nil,
21
+ timestamp: nil,
22
+ metadata: {}
23
+ )
24
+ @message = message
25
+ @stack_trace = stack_trace
26
+ @file = file
27
+ @function = function
28
+ @fingerprint = fingerprint
29
+ @exception_type = exception_type
30
+ @request = request
31
+ @environment = environment || default_environment
32
+ @timestamp = timestamp || Time.now.utc.iso8601
33
+ @metadata = metadata || {}
34
+ end
35
+
36
+ # Serialize to the standard BugStack API payload.
37
+ #
38
+ # @param config [Bugstack::Configuration]
39
+ # @return [Hash]
40
+ def to_payload(config)
41
+ payload = {
42
+ "apiKey" => config.api_key,
43
+ "error" => {
44
+ "message" => @message,
45
+ "stackTrace" => @stack_trace,
46
+ "file" => @file,
47
+ "function" => @function,
48
+ "fingerprint" => @fingerprint
49
+ },
50
+ "environment" => {
51
+ "language" => @environment[:language],
52
+ "languageVersion" => @environment[:language_version],
53
+ "framework" => @environment[:framework].to_s,
54
+ "frameworkVersion" => @environment[:framework_version].to_s,
55
+ "os" => @environment[:os],
56
+ "sdkVersion" => @environment[:sdk_version]
57
+ },
58
+ "timestamp" => @timestamp
59
+ }
60
+
61
+ if @request
62
+ payload["request"] = {
63
+ "route" => @request[:route].to_s,
64
+ "method" => @request[:method].to_s
65
+ }
66
+ end
67
+
68
+ payload["projectId"] = config.project_id unless config.project_id.empty?
69
+
70
+ meta = @metadata.dup
71
+ meta["autoFix"] = true if config.auto_fix
72
+ payload["metadata"] = meta unless meta.empty?
73
+
74
+ payload
75
+ end
76
+
77
+ private
78
+
79
+ def default_environment
80
+ {
81
+ language: "ruby",
82
+ language_version: RUBY_VERSION,
83
+ framework: "",
84
+ framework_version: "",
85
+ os: RUBY_PLATFORM,
86
+ sdk_version: Bugstack::VERSION
87
+ }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha2"
4
+
5
+ module Bugstack
6
+ module Fingerprint
7
+ # Generate a stable SHA-256 fingerprint for an error.
8
+ #
9
+ # @param exception_type [String]
10
+ # @param file [String]
11
+ # @param function [String]
12
+ # @param line [Integer, nil]
13
+ # @return [String] 16-char hex fingerprint
14
+ def self.generate(exception_type, file, function, line = nil)
15
+ parts = [exception_type, file, function]
16
+ parts << line.to_s if line
17
+ key = parts.join(":")
18
+ Digest::SHA256.hexdigest(key)[0, 16]
19
+ end
20
+
21
+ # Extract file, function, and line from an exception's backtrace.
22
+ #
23
+ # @param exception [Exception]
24
+ # @return [Array<(String, String, String, Integer)>]
25
+ # [exception_type, file, function, line]
26
+ def self.extract_location(exception)
27
+ exception_type = exception.class.name
28
+
29
+ bt = exception.backtrace
30
+ return [exception_type, "", "", nil] if bt.nil? || bt.empty?
31
+
32
+ # Parse the first backtrace line: "file:line:in `method'"
33
+ first_line = bt.first
34
+ if first_line =~ /\A(.+):(\d+):in [`'](.+)'\z/
35
+ file = Regexp.last_match(1)
36
+ line = Regexp.last_match(2).to_i
37
+ function = Regexp.last_match(3)
38
+ [exception_type, file, function, line]
39
+ else
40
+ [exception_type, first_line, "", nil]
41
+ end
42
+ end
43
+
44
+ # Format an exception's backtrace as a string.
45
+ #
46
+ # @param exception [Exception]
47
+ # @return [String]
48
+ def self.format_backtrace(exception)
49
+ bt = exception.backtrace
50
+ return "#{exception.class}: #{exception.message}" if bt.nil? || bt.empty?
51
+
52
+ lines = ["#{exception.class}: #{exception.message}"]
53
+ bt.each { |frame| lines << " from #{frame}" }
54
+ lines.join("\n")
55
+ end
56
+ end
57
+
58
+ # Client-side error deduplicator.
59
+ # Prevents the same error (by fingerprint) from being reported
60
+ # more than once within a configurable time window.
61
+ class Deduplicator
62
+ def initialize(window: 300.0)
63
+ @cache = {}
64
+ @window = window
65
+ @mutex = Mutex.new
66
+ end
67
+
68
+ # Check if an error should be sent. Thread-safe.
69
+ #
70
+ # @param fingerprint [String]
71
+ # @return [Boolean]
72
+ def should_send?(fingerprint)
73
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
74
+
75
+ @mutex.synchronize do
76
+ last_sent = @cache[fingerprint]
77
+ if last_sent && (now - last_sent) < @window
78
+ return false
79
+ end
80
+
81
+ @cache[fingerprint] = now
82
+ cleanup(now)
83
+ true
84
+ end
85
+ end
86
+
87
+ def clear
88
+ @mutex.synchronize { @cache.clear }
89
+ end
90
+
91
+ private
92
+
93
+ def cleanup(now)
94
+ @cache.delete_if { |_, ts| (now - ts) >= @window }
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugstack
4
+ module Integrations
5
+ # Generic Ruby integration that hooks into at_exit
6
+ # to capture unhandled exceptions.
7
+ #
8
+ # Usage:
9
+ # require "bugstack"
10
+ # require "bugstack/integrations/generic"
11
+ #
12
+ # Bugstack.init(api_key: "bs_live_...")
13
+ # Bugstack::Integrations::Generic.install!
14
+ module Generic
15
+ @installed = false
16
+
17
+ # Install global exception hooks.
18
+ def self.install!
19
+ return if @installed
20
+
21
+ @installed = true
22
+
23
+ at_exit do
24
+ if $! && !$!.is_a?(SystemExit)
25
+ Bugstack.capture_exception($!)
26
+ end
27
+ end
28
+ end
29
+
30
+ # Check if hooks are installed.
31
+ def self.installed?
32
+ @installed
33
+ end
34
+
35
+ # Reset for testing.
36
+ def self.reset!
37
+ @installed = false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugstack
4
+ module Integrations
5
+ # Rails integration via Railtie for automatic setup
6
+ # and Rack middleware for exception capture.
7
+ #
8
+ # Add to your Gemfile:
9
+ # gem "bugstack"
10
+ #
11
+ # Configure in an initializer:
12
+ # # config/initializers/bugstack.rb
13
+ # Bugstack.init do |config|
14
+ # config.api_key = Rails.application.credentials.bugstack_api_key
15
+ # config.environment = Rails.env
16
+ # config.auto_fix = true
17
+ # end
18
+ class Railtie < ::Rails::Railtie
19
+ initializer "bugstack.middleware" do |app|
20
+ app.middleware.insert(0, Bugstack::Integrations::RackMiddleware)
21
+ end
22
+ end if defined?(::Rails::Railtie)
23
+
24
+ # Rack middleware that captures unhandled exceptions.
25
+ class RackMiddleware
26
+ def initialize(app)
27
+ @app = app
28
+ end
29
+
30
+ def call(env)
31
+ @app.call(env)
32
+ rescue Exception => e # rubocop:disable Lint/RescueException
33
+ capture_from_rack(e, env)
34
+ raise
35
+ end
36
+
37
+ private
38
+
39
+ def capture_from_rack(exception, env)
40
+ client = Bugstack.client
41
+ return unless client
42
+
43
+ request = build_request_context(env)
44
+ client.capture_exception(
45
+ exception,
46
+ request: request,
47
+ metadata: { "framework" => "rails" }
48
+ )
49
+ rescue => inner
50
+ warn "[BugStack] Error capturing exception: #{inner.message}" if client&.config&.debug
51
+ end
52
+
53
+ def build_request_context(env)
54
+ {
55
+ route: env["PATH_INFO"].to_s,
56
+ method: env["REQUEST_METHOD"].to_s
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugstack
4
+ module Integrations
5
+ # Sinatra integration for capturing unhandled exceptions.
6
+ #
7
+ # Usage:
8
+ # require "sinatra"
9
+ # require "bugstack"
10
+ # require "bugstack/integrations/sinatra"
11
+ #
12
+ # Bugstack.init(api_key: "bs_live_...")
13
+ #
14
+ # class MyApp < Sinatra::Base
15
+ # register Bugstack::Integrations::Sinatra
16
+ #
17
+ # get "/" do
18
+ # "Hello!"
19
+ # end
20
+ # end
21
+ module Sinatra
22
+ def self.registered(app)
23
+ app.error do |exception|
24
+ client = Bugstack.client
25
+ if client
26
+ client.capture_exception(
27
+ exception,
28
+ request: {
29
+ route: request.path_info,
30
+ method: request.request_method
31
+ },
32
+ metadata: { "framework" => "sinatra" }
33
+ )
34
+ end
35
+
36
+ raise exception
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Bugstack
8
+ # HTTP transport with background thread and retry logic.
9
+ # Uses stdlib net/http — zero gem dependencies.
10
+ class Transport
11
+ def initialize(endpoint:, api_key:, timeout: 5.0, max_retries: 3, debug: false)
12
+ @endpoint = URI.parse(endpoint)
13
+ @api_key = api_key
14
+ @timeout = timeout
15
+ @max_retries = max_retries
16
+ @debug = debug
17
+ @queue = Queue.new
18
+ @shutdown = false
19
+ @worker = start_worker
20
+ end
21
+
22
+ # Add a payload to the send queue (non-blocking).
23
+ #
24
+ # @param payload [Hash]
25
+ def enqueue(payload)
26
+ return if @shutdown
27
+
28
+ @queue << payload
29
+ rescue => e
30
+ log_debug("Enqueue failed: #{e.message}")
31
+ end
32
+
33
+ # Stop the worker thread and wait for it to finish.
34
+ def shutdown
35
+ @shutdown = true
36
+ @queue << :stop
37
+ @worker&.join(2)
38
+ end
39
+
40
+ private
41
+
42
+ def start_worker
43
+ Thread.new do
44
+ loop do
45
+ payload = @queue.pop
46
+ break if payload == :stop
47
+
48
+ send_with_retry(payload)
49
+ end
50
+ end.tap { |t| t.name = "bugstack-transport" if t.respond_to?(:name=) }
51
+ end
52
+
53
+ def send_with_retry(payload)
54
+ body = JSON.generate(payload)
55
+
56
+ @max_retries.times do |attempt|
57
+ begin
58
+ http = Net::HTTP.new(@endpoint.host, @endpoint.port)
59
+ http.use_ssl = @endpoint.scheme == "https"
60
+ http.open_timeout = @timeout
61
+ http.read_timeout = @timeout
62
+
63
+ request = Net::HTTP::Post.new(@endpoint.request_uri)
64
+ request["Content-Type"] = "application/json"
65
+ request["X-BugStack-API-Key"] = @api_key
66
+ request["X-BugStack-SDK-Version"] = Bugstack::VERSION
67
+ request.body = body
68
+
69
+ response = http.request(request)
70
+
71
+ if response.code.to_i < 400
72
+ log_debug("Event sent successfully")
73
+ return true
74
+ end
75
+
76
+ log_debug("HTTP #{response.code} (attempt #{attempt + 1})")
77
+ rescue => e
78
+ log_debug("Send failed (attempt #{attempt + 1}): #{e.message}")
79
+ end
80
+
81
+ # Exponential backoff: 1s, 2s, 4s
82
+ sleep(2**attempt) if attempt < @max_retries - 1
83
+ end
84
+
85
+ log_debug("Max retries exceeded, dropping event")
86
+ false
87
+ end
88
+
89
+ def log_debug(msg)
90
+ return unless @debug
91
+
92
+ warn "[BugStack] #{msg}"
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugstack
4
+ VERSION = "1.0.0"
5
+ end
data/lib/bugstack.rb ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bugstack/version"
4
+ require_relative "bugstack/configuration"
5
+ require_relative "bugstack/event"
6
+ require_relative "bugstack/fingerprint"
7
+ require_relative "bugstack/transport"
8
+ require_relative "bugstack/client"
9
+
10
+ # BugStack SDK for Ruby — capture, report, and auto-fix production errors.
11
+ #
12
+ # Usage:
13
+ # Bugstack.init(api_key: "bs_live_...")
14
+ #
15
+ # begin
16
+ # risky_operation
17
+ # rescue => e
18
+ # Bugstack.capture_exception(e)
19
+ # end
20
+ module Bugstack
21
+ class Error < StandardError; end
22
+
23
+ class << self
24
+ # @return [Bugstack::Client, nil]
25
+ attr_reader :client
26
+
27
+ # Initialize the BugStack SDK.
28
+ #
29
+ # @param api_key [String] Your BugStack API key (required)
30
+ # @yield [config] Optional block for configuration
31
+ # @return [Bugstack::Client]
32
+ def init(api_key: nil, **options, &block)
33
+ config = Configuration.new
34
+ config.api_key = api_key if api_key
35
+ options.each { |k, v| config.public_send(:"#{k}=", v) }
36
+ yield config if block_given?
37
+
38
+ @client&.shutdown
39
+ @client = Client.new(config)
40
+ end
41
+
42
+ # Capture an exception and send it to BugStack.
43
+ #
44
+ # @param exception [Exception]
45
+ # @param request [Hash, nil] Request context
46
+ # @param metadata [Hash, nil] Additional metadata
47
+ # @return [Boolean]
48
+ def capture_exception(exception, request: nil, metadata: nil)
49
+ unless @client
50
+ warn "[BugStack] Not initialized. Call Bugstack.init first."
51
+ return false
52
+ end
53
+
54
+ @client.capture_exception(exception, request: request, metadata: metadata)
55
+ end
56
+
57
+ # Flush pending events and shut down.
58
+ def shutdown
59
+ @client&.shutdown
60
+ @client = nil
61
+ end
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bugstack
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - BugStack
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: webmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.18'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.18'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.50'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.50'
69
+ description: Capture, report, and auto-fix production errors with BugStack. Zero runtime
70
+ dependencies. Framework integrations for Rails, Sinatra, and more.
71
+ email:
72
+ - team@bugstack.dev
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - CHANGELOG.md
78
+ - LICENSE
79
+ - README.md
80
+ - lib/bugstack.rb
81
+ - lib/bugstack/client.rb
82
+ - lib/bugstack/configuration.rb
83
+ - lib/bugstack/event.rb
84
+ - lib/bugstack/fingerprint.rb
85
+ - lib/bugstack/integrations/generic.rb
86
+ - lib/bugstack/integrations/rails.rb
87
+ - lib/bugstack/integrations/sinatra.rb
88
+ - lib/bugstack/transport.rb
89
+ - lib/bugstack/version.rb
90
+ homepage: https://bugstack.dev
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ homepage_uri: https://bugstack.dev
95
+ source_code_uri: https://github.com/MasonBachmann7/bugstack-ruby
96
+ changelog_uri: https://github.com/MasonBachmann7/bugstack-ruby/blob/main/CHANGELOG.md
97
+ bug_tracker_uri: https://github.com/MasonBachmann7/bugstack-ruby/issues
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '3.0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.4.19
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Official BugStack SDK for Ruby
117
+ test_files: []