errbit-ruby 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 +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +20 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +1 -0
- data/CLAUDE.md +79 -0
- data/LICENSE.txt +21 -0
- data/README.md +92 -0
- data/Rakefile +8 -0
- data/lib/errbit/backtrace.rb +28 -0
- data/lib/errbit/client.rb +86 -0
- data/lib/errbit/configuration.rb +36 -0
- data/lib/errbit/error_report.rb +19 -0
- data/lib/errbit/errors.rb +19 -0
- data/lib/errbit/result.rb +40 -0
- data/lib/errbit/version.rb +5 -0
- data/lib/errbit.rb +39 -0
- data/openapi/errbit-api.yaml +139 -0
- metadata +103 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bf16e360e4211384b9c7064f3981b586810521f44102067186e63b7cba2522ba
|
|
4
|
+
data.tar.gz: 31ffc9af3350871133ee0e6c92684736b3e56b76021e5d083246de24fd2314e9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 656b9b99cb0a66d575ebe7825ee5e8d0c995707aebba25dc6ca69c603149be50235d530aea9a4c23ca98f3554717be99ba6aa7d3fb1f4d064abf774e7dd243f6
|
|
7
|
+
data.tar.gz: 291339ec37a09eb42bf877a50bbede6f3aeaff8bf8621658a3c254c93f9e020d4a4c00f65f85604dd771381d75c88836e08dd1a9991d9e527ce83925c501bb60
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 4.0
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
Exclude:
|
|
6
|
+
- "bin/*"
|
|
7
|
+
|
|
8
|
+
Style/StringLiterals:
|
|
9
|
+
EnforcedStyle: double_quotes
|
|
10
|
+
|
|
11
|
+
Style/Documentation:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Metrics/MethodLength:
|
|
15
|
+
Max: 20
|
|
16
|
+
|
|
17
|
+
Metrics/BlockLength:
|
|
18
|
+
Exclude:
|
|
19
|
+
- "spec/**/*"
|
|
20
|
+
- "errbit-ruby.gemspec"
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.7
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
See [CLAUDE.md](./CLAUDE.md) for agent instructions for this project.
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# errbit-ruby
|
|
2
|
+
|
|
3
|
+
A dependency-free Ruby client for reporting exceptions to an Errbit server.
|
|
4
|
+
Plain Ruby — no Rails, no Rack. Targets Ruby >= 4.0.
|
|
5
|
+
|
|
6
|
+
## Design constraints (do not violate without asking)
|
|
7
|
+
|
|
8
|
+
- **Zero runtime dependencies.** The gem may only use Ruby's standard
|
|
9
|
+
library (`net/http`, `json`, `uri`, ...) at runtime. `rspec`, `webmock`,
|
|
10
|
+
`rubocop`, `rake`, `irb` are development-only (`add_development_dependency`
|
|
11
|
+
in `errbit-ruby.gemspec`, or plain `gem` lines in the `Gemfile`).
|
|
12
|
+
- **No Rails, no Rack.** Nothing in `lib/` may assume either is loaded.
|
|
13
|
+
- **This is a new, custom API** — not Airbrake- or Sentry-compatible. The
|
|
14
|
+
wire format is defined in `openapi/errbit-api.yaml` and that file is the
|
|
15
|
+
source of truth for the request/response shape the client builds and
|
|
16
|
+
parses. If you change the payload shape in `lib/errbit/error_report.rb`
|
|
17
|
+
or `lib/errbit/client.rb`, update the OpenAPI spec to match (and vice
|
|
18
|
+
versa).
|
|
19
|
+
- **Synchronous delivery only.** `Errbit::Client#notify` makes a blocking
|
|
20
|
+
`Net::HTTP` call on the caller's thread. No background thread/queue.
|
|
21
|
+
- Target Ruby version is 4.0 (`.ruby-version` pins the local dev version to
|
|
22
|
+
4.0.7 via rbenv; `required_ruby_version` in the gemspec is `>= 4.0`).
|
|
23
|
+
|
|
24
|
+
## Project layout
|
|
25
|
+
|
|
26
|
+
- `lib/errbit.rb` — public entry point: `Errbit.configure`, `Errbit.notify`.
|
|
27
|
+
- `lib/errbit/configuration.rb` — `host`, `project_id`, `api_key`, `ignore`
|
|
28
|
+
list, timeouts.
|
|
29
|
+
- `lib/errbit/backtrace.rb` — parses `exception.backtrace` lines into
|
|
30
|
+
`{file:, line:, method:}` hashes. Handles both the pre-3.4 backtick/quote
|
|
31
|
+
format and the 3.4+ single-quote format.
|
|
32
|
+
- `lib/errbit/error_report.rb` — builds the JSON payload from an
|
|
33
|
+
`Exception`, matching the `ErrorReport` schema in the OpenAPI spec.
|
|
34
|
+
- `lib/errbit/client.rb` — POSTs to
|
|
35
|
+
`{host}/api/v1/projects/{project_id}/errors/{api_key}` via `Net::HTTP`,
|
|
36
|
+
returns an `Errbit::Result`.
|
|
37
|
+
- `lib/errbit/result.rb` — outcome object: `success?` / `ignored?` /
|
|
38
|
+
`failure?`.
|
|
39
|
+
- `lib/errbit/errors.rb` — `Errbit::Error`, `ConfigurationError`,
|
|
40
|
+
`DeliveryError` (raised only on network failure, never on a well-formed
|
|
41
|
+
4xx/5xx from the server — those come back as a failed `Result`).
|
|
42
|
+
- `openapi/errbit-api.yaml` — OpenAPI 3.1 spec for the single ingestion
|
|
43
|
+
endpoint this gem calls. Update alongside any payload/response change.
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bundle install
|
|
49
|
+
bundle exec rspec # run the test suite (spec/)
|
|
50
|
+
bundle exec rubocop # lint
|
|
51
|
+
bundle exec rake # defaults to spec
|
|
52
|
+
bin/console # IRB session with Errbit pre-configured
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Note: a `rubocop --server` daemon can get stuck/orphaned on this machine
|
|
56
|
+
and hang `bundle exec rubocop`. If it hangs, check `ps aux | grep rubocop`
|
|
57
|
+
and confirm with the user before killing anything; `--no-server` may not
|
|
58
|
+
help if the daemon itself is wedged.
|
|
59
|
+
|
|
60
|
+
## Testing conventions
|
|
61
|
+
|
|
62
|
+
- RSpec, with WebMock stubbing all HTTP (`WebMock.disable_net_connect!` in
|
|
63
|
+
`spec/spec_helper.rb` — real network calls in specs are a bug, not a
|
|
64
|
+
feature).
|
|
65
|
+
- WebMock's `hash_including` body matchers compare against the
|
|
66
|
+
**JSON-decoded body with string keys**, not symbols — write
|
|
67
|
+
`hash_including("error" => hash_including("class" => ...))`, not
|
|
68
|
+
`hash_including(error: hash_including(class: ...))`. Getting this wrong
|
|
69
|
+
is a common mistake here and fails silently-looking WebMock diffs.
|
|
70
|
+
- Every spec file lives under `spec/`, mirroring `lib/errbit/*` files
|
|
71
|
+
1:1 (`lib/errbit/client.rb` <-> `spec/errbit/client_spec.rb`).
|
|
72
|
+
|
|
73
|
+
## Style
|
|
74
|
+
|
|
75
|
+
- `# frozen_string_literal: true` at the top of every Ruby file.
|
|
76
|
+
- Double-quoted string literals (enforced by `.rubocop.yml`).
|
|
77
|
+
- No comments explaining *what* code does; only *why*, when non-obvious
|
|
78
|
+
(see the `DeliveryError` note above for an example of the kind of thing
|
|
79
|
+
worth a comment).
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Zubkov
|
|
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,92 @@
|
|
|
1
|
+
# errbit-ruby
|
|
2
|
+
|
|
3
|
+
A dependency-free Ruby client for reporting exceptions to an Errbit server.
|
|
4
|
+
Plain Ruby only — no Rails, no Rack required. Targets Ruby >= 4.0 and has
|
|
5
|
+
**zero runtime dependencies** (uses only `net/http`, `json`, and `uri` from
|
|
6
|
+
the standard library).
|
|
7
|
+
|
|
8
|
+
This gem talks to a new, purpose-built error-ingestion API — it is **not**
|
|
9
|
+
compatible with the Airbrake or Sentry APIs. The API is specified in
|
|
10
|
+
[`openapi/errbit-api.yaml`](openapi/errbit-api.yaml).
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add to your Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "errbit-ruby", require: "errbit"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or install directly:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
gem install errbit-ruby
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Configure once, near your application's boot:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "errbit"
|
|
32
|
+
|
|
33
|
+
Errbit.configure do |config|
|
|
34
|
+
config.host = "https://errbit.example.com"
|
|
35
|
+
config.project_id = ENV.fetch("ERRBIT_PROJECT_ID")
|
|
36
|
+
config.api_key = ENV.fetch("ERRBIT_API_KEY")
|
|
37
|
+
|
|
38
|
+
# Optional: exception classes, class-name strings, or regexps to skip.
|
|
39
|
+
config.ignore = [ArgumentError, /expected, harmless/i]
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then report exceptions where you rescue them:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
begin
|
|
47
|
+
risky_operation
|
|
48
|
+
rescue => e
|
|
49
|
+
Errbit.notify(e)
|
|
50
|
+
raise
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`Errbit.notify` returns an `Errbit::Result`:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
result = Errbit.notify(e)
|
|
58
|
+
|
|
59
|
+
result.success? # => true/false
|
|
60
|
+
result.ignored? # => true if the exception matched config.ignore
|
|
61
|
+
result.failure? # => true on a 4xx/5xx response from the server
|
|
62
|
+
result.id # => server-assigned id, on success
|
|
63
|
+
result.error # => server error message, on failure
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A report is delivered synchronously over HTTP(S) on the calling thread. A
|
|
67
|
+
network failure (timeout, connection refused, etc.) raises
|
|
68
|
+
`Errbit::DeliveryError`; a well-formed error response from the server
|
|
69
|
+
(400/401/404) does not raise — check `result.failure?` instead.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
bundle install
|
|
75
|
+
bundle exec rspec
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`bin/console` starts an IRB session with the gem pre-configured against a
|
|
79
|
+
placeholder host for manual experimentation.
|
|
80
|
+
|
|
81
|
+
## API
|
|
82
|
+
|
|
83
|
+
See [`openapi/errbit-api.yaml`](openapi/errbit-api.yaml) for the full
|
|
84
|
+
OpenAPI 3.1 definition of the single endpoint this gem calls:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
POST /api/v1/projects/{project_id}/errors/{api_key}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errbit
|
|
4
|
+
module Backtrace
|
|
5
|
+
# Matches both backtrace line formats Ruby has used:
|
|
6
|
+
# "file.rb:12:in `method'" (Ruby <= 3.3)
|
|
7
|
+
# "file.rb:12:in 'method'" (Ruby >= 3.4)
|
|
8
|
+
# and lines with no method info at all: "file.rb:12"
|
|
9
|
+
LINE_PATTERN = /\A(?<file>.+):(?<line>\d+)(?::in\s+[`'](?<method>.+)')?\z/.freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def parse(exception)
|
|
14
|
+
Array(exception&.backtrace).map { |line| parse_line(line) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_line(line)
|
|
18
|
+
match = LINE_PATTERN.match(line.to_s)
|
|
19
|
+
return { file: line.to_s, line: nil, method: nil } unless match
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
file: match[:file],
|
|
23
|
+
line: match[:line].to_i,
|
|
24
|
+
method: match[:method]
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Errbit
|
|
8
|
+
class Client
|
|
9
|
+
NETWORK_ERRORS = [
|
|
10
|
+
Errno::ECONNREFUSED,
|
|
11
|
+
Errno::ECONNRESET,
|
|
12
|
+
Errno::EHOSTUNREACH,
|
|
13
|
+
SocketError,
|
|
14
|
+
Timeout::Error,
|
|
15
|
+
Net::OpenTimeout,
|
|
16
|
+
Net::ReadTimeout,
|
|
17
|
+
OpenSSL::SSL::SSLError
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(configuration)
|
|
21
|
+
@configuration = configuration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def notify(exception)
|
|
25
|
+
return Result.ignored if configuration.ignored?(exception)
|
|
26
|
+
|
|
27
|
+
configuration.validate!
|
|
28
|
+
deliver(ErrorReport.build(exception))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :configuration
|
|
34
|
+
|
|
35
|
+
def deliver(payload)
|
|
36
|
+
uri = endpoint_uri
|
|
37
|
+
request = Net::HTTP::Post.new(uri)
|
|
38
|
+
request["Content-Type"] = "application/json"
|
|
39
|
+
request["Accept"] = "application/json"
|
|
40
|
+
request.body = JSON.generate(payload)
|
|
41
|
+
|
|
42
|
+
response = http_client(uri).request(request)
|
|
43
|
+
build_result(response)
|
|
44
|
+
rescue *NETWORK_ERRORS => e
|
|
45
|
+
raise DeliveryError, e
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def http_client(uri)
|
|
49
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
50
|
+
http.use_ssl = uri.scheme == "https"
|
|
51
|
+
http.open_timeout = configuration.open_timeout
|
|
52
|
+
http.read_timeout = configuration.read_timeout
|
|
53
|
+
http
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def endpoint_uri
|
|
57
|
+
base = configuration.host.to_s.chomp("/")
|
|
58
|
+
URI.parse(
|
|
59
|
+
"#{base}/api/v1/projects/#{escape(configuration.project_id)}/errors/#{escape(configuration.api_key)}"
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def escape(value)
|
|
64
|
+
URI.encode_www_form_component(value.to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_result(response)
|
|
68
|
+
body = parse_json(response.body)
|
|
69
|
+
|
|
70
|
+
case response
|
|
71
|
+
when Net::HTTPAccepted
|
|
72
|
+
Result.success(status_code: response.code.to_i, id: body["id"], created_at: body["created_at"])
|
|
73
|
+
else
|
|
74
|
+
Result.failure(status_code: response.code.to_i, error: body["error"] || response.message)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_json(raw)
|
|
79
|
+
return {} if raw.nil? || raw.empty?
|
|
80
|
+
|
|
81
|
+
JSON.parse(raw)
|
|
82
|
+
rescue JSON::ParserError
|
|
83
|
+
{}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errbit
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :host, :project_id, :api_key, :ignore, :open_timeout, :read_timeout
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@host = nil
|
|
9
|
+
@project_id = nil
|
|
10
|
+
@api_key = nil
|
|
11
|
+
@ignore = []
|
|
12
|
+
@open_timeout = 2
|
|
13
|
+
@read_timeout = 2
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ignored?(exception)
|
|
17
|
+
ignore.any? do |pattern|
|
|
18
|
+
case pattern
|
|
19
|
+
when Module
|
|
20
|
+
exception.is_a?(pattern)
|
|
21
|
+
when Regexp
|
|
22
|
+
pattern.match?(exception.message.to_s) || pattern.match?(exception.class.name.to_s)
|
|
23
|
+
else
|
|
24
|
+
pattern.to_s == exception.class.name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate!
|
|
30
|
+
missing = %i[host project_id api_key].select { |attr| public_send(attr).nil? || public_send(attr).to_s.empty? }
|
|
31
|
+
return if missing.empty?
|
|
32
|
+
|
|
33
|
+
raise ConfigurationError, "Errbit is missing required configuration: #{missing.join(', ')}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errbit
|
|
4
|
+
module ErrorReport
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Builds a payload matching the ErrorReport schema in
|
|
8
|
+
# openapi/errbit-api.yaml.
|
|
9
|
+
def build(exception)
|
|
10
|
+
{
|
|
11
|
+
error: {
|
|
12
|
+
class: exception.class.name,
|
|
13
|
+
message: exception.message.to_s,
|
|
14
|
+
backtrace: Backtrace.parse(exception)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errbit
|
|
4
|
+
Error = Class.new(StandardError)
|
|
5
|
+
ConfigurationError = Class.new(Error)
|
|
6
|
+
|
|
7
|
+
# Raised when the exception could not be delivered at all (network
|
|
8
|
+
# failure, timeout, connection refused, ...). A well-formed HTTP error
|
|
9
|
+
# response from the server (4xx/5xx) is *not* raised as this error; it is
|
|
10
|
+
# surfaced via a failed Result instead.
|
|
11
|
+
class DeliveryError < Error
|
|
12
|
+
attr_reader :cause_error
|
|
13
|
+
|
|
14
|
+
def initialize(cause_error)
|
|
15
|
+
@cause_error = cause_error
|
|
16
|
+
super("Failed to deliver error report: #{cause_error.class}: #{cause_error.message}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errbit
|
|
4
|
+
# Outcome of a Client#notify call.
|
|
5
|
+
class Result
|
|
6
|
+
attr_reader :status, :status_code, :id, :created_at, :error
|
|
7
|
+
|
|
8
|
+
def self.ignored
|
|
9
|
+
new(status: :ignored)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.success(status_code:, id:, created_at:)
|
|
13
|
+
new(status: :success, status_code: status_code, id: id, created_at: created_at)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.failure(status_code:, error:)
|
|
17
|
+
new(status: :failure, status_code: status_code, error: error)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(status:, status_code: nil, id: nil, created_at: nil, error: nil)
|
|
21
|
+
@status = status
|
|
22
|
+
@status_code = status_code
|
|
23
|
+
@id = id
|
|
24
|
+
@created_at = created_at
|
|
25
|
+
@error = error
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def success?
|
|
29
|
+
status == :success
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ignored?
|
|
33
|
+
status == :ignored
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def failure?
|
|
37
|
+
status == :failure
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/errbit.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errbit/version"
|
|
4
|
+
require_relative "errbit/errors"
|
|
5
|
+
require_relative "errbit/result"
|
|
6
|
+
require_relative "errbit/backtrace"
|
|
7
|
+
require_relative "errbit/error_report"
|
|
8
|
+
require_relative "errbit/configuration"
|
|
9
|
+
require_relative "errbit/client"
|
|
10
|
+
|
|
11
|
+
module Errbit
|
|
12
|
+
class << self
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def configure
|
|
18
|
+
yield configuration
|
|
19
|
+
@client = nil
|
|
20
|
+
configuration
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def notify(exception)
|
|
24
|
+
client.notify(exception)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Resets configuration and memoized client. Primarily useful in tests.
|
|
28
|
+
def reset!
|
|
29
|
+
@configuration = nil
|
|
30
|
+
@client = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def client
|
|
36
|
+
@client ||= Client.new(configuration)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
info:
|
|
3
|
+
title: Errbit Error Ingestion API
|
|
4
|
+
description: >
|
|
5
|
+
A minimal API for reporting application exceptions to an Errbit server.
|
|
6
|
+
This is a new API design, not compatible with the Airbrake or Sentry
|
|
7
|
+
APIs. v1 covers error ingestion only.
|
|
8
|
+
version: "1.0.0"
|
|
9
|
+
|
|
10
|
+
servers:
|
|
11
|
+
- url: https://errbit.example.com
|
|
12
|
+
description: Example Errbit server
|
|
13
|
+
|
|
14
|
+
paths:
|
|
15
|
+
/api/v1/projects/{project_id}/errors/{api_key}:
|
|
16
|
+
post:
|
|
17
|
+
operationId: createError
|
|
18
|
+
summary: Report an exception
|
|
19
|
+
description: >
|
|
20
|
+
Submits a single exception occurrence for a project. Authenticates
|
|
21
|
+
the request via the project's API key embedded in the URL path.
|
|
22
|
+
parameters:
|
|
23
|
+
- $ref: '#/components/parameters/ProjectId'
|
|
24
|
+
- $ref: '#/components/parameters/ApiKey'
|
|
25
|
+
requestBody:
|
|
26
|
+
required: true
|
|
27
|
+
content:
|
|
28
|
+
application/json:
|
|
29
|
+
schema:
|
|
30
|
+
$ref: '#/components/schemas/ErrorReport'
|
|
31
|
+
responses:
|
|
32
|
+
'202':
|
|
33
|
+
description: Error report accepted.
|
|
34
|
+
content:
|
|
35
|
+
application/json:
|
|
36
|
+
schema:
|
|
37
|
+
$ref: '#/components/schemas/ErrorAck'
|
|
38
|
+
'400':
|
|
39
|
+
description: Malformed request body.
|
|
40
|
+
content:
|
|
41
|
+
application/json:
|
|
42
|
+
schema:
|
|
43
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
44
|
+
'401':
|
|
45
|
+
description: Invalid project_id/api_key combination.
|
|
46
|
+
content:
|
|
47
|
+
application/json:
|
|
48
|
+
schema:
|
|
49
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
50
|
+
'404':
|
|
51
|
+
description: Unknown project_id.
|
|
52
|
+
content:
|
|
53
|
+
application/json:
|
|
54
|
+
schema:
|
|
55
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
56
|
+
|
|
57
|
+
components:
|
|
58
|
+
parameters:
|
|
59
|
+
ProjectId:
|
|
60
|
+
name: project_id
|
|
61
|
+
in: path
|
|
62
|
+
required: true
|
|
63
|
+
description: Identifier of the project the error belongs to.
|
|
64
|
+
schema:
|
|
65
|
+
type: string
|
|
66
|
+
ApiKey:
|
|
67
|
+
name: api_key
|
|
68
|
+
in: path
|
|
69
|
+
required: true
|
|
70
|
+
description: Secret API key authorizing writes to this project.
|
|
71
|
+
schema:
|
|
72
|
+
type: string
|
|
73
|
+
|
|
74
|
+
schemas:
|
|
75
|
+
BacktraceFrame:
|
|
76
|
+
type: object
|
|
77
|
+
properties:
|
|
78
|
+
file:
|
|
79
|
+
type: string
|
|
80
|
+
description: Source file path for this stack frame.
|
|
81
|
+
line:
|
|
82
|
+
type: integer
|
|
83
|
+
description: Line number within the file, if known.
|
|
84
|
+
method:
|
|
85
|
+
type: string
|
|
86
|
+
description: Method or block name the frame was executing in.
|
|
87
|
+
additionalProperties: false
|
|
88
|
+
|
|
89
|
+
ErrorReport:
|
|
90
|
+
type: object
|
|
91
|
+
required:
|
|
92
|
+
- error
|
|
93
|
+
properties:
|
|
94
|
+
error:
|
|
95
|
+
type: object
|
|
96
|
+
required:
|
|
97
|
+
- class
|
|
98
|
+
- message
|
|
99
|
+
properties:
|
|
100
|
+
class:
|
|
101
|
+
type: string
|
|
102
|
+
description: Fully qualified exception class name.
|
|
103
|
+
examples:
|
|
104
|
+
- RuntimeError
|
|
105
|
+
- "Net::OpenTimeout"
|
|
106
|
+
message:
|
|
107
|
+
type: string
|
|
108
|
+
description: Exception message.
|
|
109
|
+
backtrace:
|
|
110
|
+
type: array
|
|
111
|
+
description: Ordered stack frames, innermost first.
|
|
112
|
+
items:
|
|
113
|
+
$ref: '#/components/schemas/BacktraceFrame'
|
|
114
|
+
additionalProperties: false
|
|
115
|
+
additionalProperties: false
|
|
116
|
+
|
|
117
|
+
ErrorAck:
|
|
118
|
+
type: object
|
|
119
|
+
required:
|
|
120
|
+
- id
|
|
121
|
+
- created_at
|
|
122
|
+
properties:
|
|
123
|
+
id:
|
|
124
|
+
type: string
|
|
125
|
+
description: Server-assigned identifier for the stored error occurrence.
|
|
126
|
+
created_at:
|
|
127
|
+
type: string
|
|
128
|
+
format: date-time
|
|
129
|
+
additionalProperties: false
|
|
130
|
+
|
|
131
|
+
ErrorResponse:
|
|
132
|
+
type: object
|
|
133
|
+
required:
|
|
134
|
+
- error
|
|
135
|
+
properties:
|
|
136
|
+
error:
|
|
137
|
+
type: string
|
|
138
|
+
description: Human-readable error message.
|
|
139
|
+
additionalProperties: false
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: errbit-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Igor Zubkov
|
|
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: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.13'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: webmock
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.23'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.23'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rubocop
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.65'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.65'
|
|
54
|
+
description: |
|
|
55
|
+
A dependency-free Ruby gem that catches and reports exceptions to an
|
|
56
|
+
Errbit server over its error-ingestion API. No Rails, no Rack required.
|
|
57
|
+
email:
|
|
58
|
+
- igor.zubkov@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".rspec"
|
|
64
|
+
- ".rubocop.yml"
|
|
65
|
+
- ".ruby-version"
|
|
66
|
+
- AGENTS.md
|
|
67
|
+
- CLAUDE.md
|
|
68
|
+
- LICENSE.txt
|
|
69
|
+
- README.md
|
|
70
|
+
- Rakefile
|
|
71
|
+
- lib/errbit.rb
|
|
72
|
+
- lib/errbit/backtrace.rb
|
|
73
|
+
- lib/errbit/client.rb
|
|
74
|
+
- lib/errbit/configuration.rb
|
|
75
|
+
- lib/errbit/error_report.rb
|
|
76
|
+
- lib/errbit/errors.rb
|
|
77
|
+
- lib/errbit/result.rb
|
|
78
|
+
- lib/errbit/version.rb
|
|
79
|
+
- openapi/errbit-api.yaml
|
|
80
|
+
homepage: https://github.com/errbit/errbit-ruby
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
homepage_uri: https://github.com/errbit/errbit-ruby
|
|
85
|
+
source_code_uri: https://github.com/errbit/errbit-ruby
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '4.0'
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 4.0.21
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: Plain-Ruby client for reporting exceptions to an Errbit server.
|
|
103
|
+
test_files: []
|