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 +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/lib/bugstack/client.rb +123 -0
- data/lib/bugstack/configuration.rb +68 -0
- data/lib/bugstack/event.rb +90 -0
- data/lib/bugstack/fingerprint.rb +97 -0
- data/lib/bugstack/integrations/generic.rb +41 -0
- data/lib/bugstack/integrations/rails.rb +61 -0
- data/lib/bugstack/integrations/sinatra.rb +41 -0
- data/lib/bugstack/transport.rb +95 -0
- data/lib/bugstack/version.rb +5 -0
- data/lib/bugstack.rb +63 -0
- metadata +117 -0
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
|
+
[](https://rubygems.org/gems/bugstack)
|
|
6
|
+
[](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
|
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: []
|