telebugs 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/.standard.yml +1 -0
- data/LICENSE.md +20 -0
- data/README.md +95 -0
- data/Rakefile +8 -0
- data/lib/telebugs/backtrace.rb +156 -0
- data/lib/telebugs/code_hunk.rb +44 -0
- data/lib/telebugs/config.rb +42 -0
- data/lib/telebugs/error_message.rb +23 -0
- data/lib/telebugs/file_cache.rb +41 -0
- data/lib/telebugs/middleware.rb +17 -0
- data/lib/telebugs/middleware_stack.rb +30 -0
- data/lib/telebugs/notifier.rb +30 -0
- data/lib/telebugs/promise.rb +39 -0
- data/lib/telebugs/report.rb +87 -0
- data/lib/telebugs/sender.rb +50 -0
- data/lib/telebugs/truncator.rb +111 -0
- data/lib/telebugs/version.rb +5 -0
- data/lib/telebugs/wrapped_error.rb +22 -0
- data/lib/telebugs.rb +37 -0
- data/sig/telebugs.rbs +4 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 14237978a4696093518f0a95eefa25363b7f6adaa2acc1c02d5e8cfcddb63b93
|
4
|
+
data.tar.gz: 61c664b448c4098fabdf4d49684f49b18494ec0e16e8d6dea5e7043cad19e8bf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eef345a4aaf248ce14462ed2cfeaf6ef6b410c42b377b911bf47a48dd2742c401af8c16fa3680c229e1a6235e963937fcdfd17494dae039fc39b93f7785f783e
|
7
|
+
data.tar.gz: ebe99794663078f5d9a8528eeb71d5dec959bb937787fde07ae242e4060738ac013fbbf0a9710506fd0e0bf04b7fb003ac8ed8938857181b4e61e3ee345eb397
|
data/.standard.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby_version: 3.0
|
data/LICENSE.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# The MIT License
|
2
|
+
|
3
|
+
Copyright © 2024 Telebugs
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the 'Software'), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
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, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# Telebugs for Ruby
|
2
|
+
|
3
|
+
Simple error monitoring for developers. Monitor production errors in real-time
|
4
|
+
and get them reported to Telegram with Telebugs.
|
5
|
+
|
6
|
+
- [FAQ](https://telebugs.com/faq)
|
7
|
+
- [Telebugs News](https://t.me/TelebugsNews)
|
8
|
+
- [Telebugs Community](https://t.me/TelebugsCommunity)
|
9
|
+
|
10
|
+
## Introduction
|
11
|
+
|
12
|
+
Any Ruby application or script can be integrated with
|
13
|
+
[Telebugs](https://telebugs.com) using the `telebugs` gem. The gem is designed
|
14
|
+
to be simple and easy to use. It provides a simple API to send errors to
|
15
|
+
Telebugs, which will then be reported to your Telegram project. This guide will
|
16
|
+
help you get started with Telebugs for Ruby.
|
17
|
+
|
18
|
+
For full details, please refer to the [Telebugs documentation](https://telebugs.com/new/docs/integrations/ruby).
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Install the gem and add to the application's Gemfile by executing:
|
23
|
+
|
24
|
+
```sh
|
25
|
+
bundle add telebugs
|
26
|
+
```
|
27
|
+
|
28
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
29
|
+
|
30
|
+
```sh
|
31
|
+
gem install telebugs
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
This is the minimal example that you can use to test Telebugs for Ruby with your
|
37
|
+
project:
|
38
|
+
|
39
|
+
```rb
|
40
|
+
require "telebugs"
|
41
|
+
|
42
|
+
Telebugs.configure do |c|
|
43
|
+
c.api_key = "YOUR_API_KEY"
|
44
|
+
end
|
45
|
+
|
46
|
+
begin
|
47
|
+
1 / 0
|
48
|
+
rescue ZeroDivisionError => e
|
49
|
+
Telebugs.notify(error: e)
|
50
|
+
end
|
51
|
+
|
52
|
+
sleep 2
|
53
|
+
|
54
|
+
puts "An error was sent to Telebugs asynchronously.",
|
55
|
+
"It will appear in your dashboard shortly.",
|
56
|
+
"A notification was also sent to your Telegram chat."
|
57
|
+
```
|
58
|
+
|
59
|
+
Replace `YOUR_API_KEY` with your actual API key. You can ask
|
60
|
+
[@TelebugsBot](http://t.me/TelebugsBot) for your API key or find it in
|
61
|
+
your project's dashboard.
|
62
|
+
|
63
|
+
## Telebugs for Ruby integrations
|
64
|
+
|
65
|
+
Telebugs for Ruby is a standalone gem that can be used with any Ruby application
|
66
|
+
or script. It can be integrated with any Ruby framework or library.
|
67
|
+
|
68
|
+
We provide official integrations for the following Ruby platforms:
|
69
|
+
|
70
|
+
- [Ruby on Rails](https://github.com/telebugs/telebugs-rails)
|
71
|
+
|
72
|
+
## Ruby support policy
|
73
|
+
|
74
|
+
Telebugs for Ruby supports the following Ruby versions:
|
75
|
+
|
76
|
+
- Ruby 3.0+
|
77
|
+
|
78
|
+
If you need support older rubies or other Ruby implementations, please contact
|
79
|
+
us at [help@telebugs.com](mailto:help@telebugs.com).
|
80
|
+
|
81
|
+
## Development
|
82
|
+
|
83
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
84
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
85
|
+
prompt that will allow you to experiment.
|
86
|
+
|
87
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
88
|
+
release a new version, update the version number in `version.rb`, and then run
|
89
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
90
|
+
git commits and the created tag, and push the `.gem` file to
|
91
|
+
[rubygems.org](https://rubygems.org).
|
92
|
+
|
93
|
+
## Contributing
|
94
|
+
|
95
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/telebugs/telebugs-ruby.
|
data/Rakefile
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Represents a cross-Ruby backtrace from exceptions (including JRuby Java
|
5
|
+
# exceptions). Provides information about stack frames (such as line number,
|
6
|
+
# file and method) in convenient for Telebugs format.
|
7
|
+
module Backtrace
|
8
|
+
module Patterns
|
9
|
+
# The pattern that matches standard Ruby stack frames, such as
|
10
|
+
# ./spec/report_spec.rb:43:in `block (3 levels) in <top (required)>'
|
11
|
+
RUBY = %r{\A
|
12
|
+
(?<file>.+) # Matches './spec/report_spec.rb'
|
13
|
+
:
|
14
|
+
(?<line>\d+) # Matches '43'
|
15
|
+
:in\s
|
16
|
+
`(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
|
17
|
+
\z}x
|
18
|
+
|
19
|
+
# The pattern that matches JRuby Java stack frames, such as
|
20
|
+
# org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
|
21
|
+
JAVA = %r{\A
|
22
|
+
(?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret'
|
23
|
+
\(
|
24
|
+
(?<file>
|
25
|
+
(?:uri:classloader:/.+(?=:)) # Matches '/META-INF/jruby.home/protocol.rb'
|
26
|
+
|
|
27
|
+
(?:uri_3a_classloader_3a_.+(?=:)) # Matches 'uri_3a_classloader_3a_/gems/...'
|
28
|
+
|
|
29
|
+
[^:]+ # Matches 'NewlineNode.java'
|
30
|
+
)
|
31
|
+
:?
|
32
|
+
(?<line>\d+)? # Matches '105'
|
33
|
+
\)
|
34
|
+
\z}x
|
35
|
+
|
36
|
+
# The pattern that tries to assume what a generic stack frame might look
|
37
|
+
# like, when exception's backtrace is set manually.
|
38
|
+
GENERIC = %r{\A
|
39
|
+
(?:from\s)?
|
40
|
+
(?<file>.+) # Matches '/foo/bar/baz.ext'
|
41
|
+
:
|
42
|
+
(?<line>\d+)? # Matches '43' or nothing
|
43
|
+
(?:
|
44
|
+
in\s`(?<function>.+)' # Matches "in `func'"
|
45
|
+
|
|
46
|
+
:in\s(?<function>.+) # Matches ":in func"
|
47
|
+
)? # ... or nothing
|
48
|
+
\z}x
|
49
|
+
|
50
|
+
# The pattern that matches exceptions from PL/SQL such as
|
51
|
+
# ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945
|
52
|
+
# @note This is raised by https://github.com/kubo/ruby-oci8
|
53
|
+
OCI = /\A
|
54
|
+
(?:
|
55
|
+
ORA-\d{5}
|
56
|
+
:\sat\s
|
57
|
+
(?:"(?<function>.+)",\s)?
|
58
|
+
line\s(?<line>\d+)
|
59
|
+
|
|
60
|
+
#{GENERIC}
|
61
|
+
)
|
62
|
+
\z/x
|
63
|
+
|
64
|
+
# The pattern that matches CoffeeScript backtraces usually coming from
|
65
|
+
# Rails & ExecJS
|
66
|
+
EXECJS = /\A
|
67
|
+
(?:
|
68
|
+
# Matches 'compile ((execjs):6692:19)'
|
69
|
+
(?<function>.+)\s\((?<file>.+):(?<line>\d+):\d+\)
|
70
|
+
|
|
71
|
+
# Matches 'bootstrap_node.js:467:3'
|
72
|
+
(?<file>.+):(?<line>\d+):\d+(?<function>)
|
73
|
+
|
|
74
|
+
# Matches the Ruby part of the backtrace
|
75
|
+
#{RUBY}
|
76
|
+
)
|
77
|
+
\z/x
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.parse(error)
|
81
|
+
return [] if error.backtrace.nil? || error.backtrace.none?
|
82
|
+
|
83
|
+
parse_backtrace(error)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Checks whether the given exception was generated by JRuby's VM.
|
87
|
+
def self.java_exception?(error)
|
88
|
+
if defined?(Java::JavaLang::Throwable) &&
|
89
|
+
error.is_a?(Java::JavaLang::Throwable)
|
90
|
+
return true
|
91
|
+
end
|
92
|
+
|
93
|
+
return false unless error.respond_to?(:backtrace)
|
94
|
+
|
95
|
+
(Patterns::JAVA =~ error.backtrace.first) != nil
|
96
|
+
end
|
97
|
+
|
98
|
+
class << self
|
99
|
+
private
|
100
|
+
|
101
|
+
def best_regexp_for(error)
|
102
|
+
if java_exception?(error)
|
103
|
+
Patterns::JAVA
|
104
|
+
elsif oci_exception?(error)
|
105
|
+
Patterns::OCI
|
106
|
+
elsif execjs_exception?(error)
|
107
|
+
Patterns::EXECJS
|
108
|
+
else
|
109
|
+
Patterns::RUBY
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def oci_exception?(error)
|
114
|
+
defined?(OCIError) && error.is_a?(OCIError)
|
115
|
+
end
|
116
|
+
|
117
|
+
def execjs_exception?(error)
|
118
|
+
return false unless defined?(ExecJS::RuntimeError)
|
119
|
+
return true if error.is_a?(ExecJS::RuntimeError)
|
120
|
+
return true if error.cause&.is_a?(ExecJS::RuntimeError)
|
121
|
+
|
122
|
+
false
|
123
|
+
end
|
124
|
+
|
125
|
+
def stack_frame(regexp, stackframe)
|
126
|
+
if (match = match_frame(regexp, stackframe))
|
127
|
+
return {
|
128
|
+
file: match[:file],
|
129
|
+
line: (Integer(match[:line]) if match[:line]),
|
130
|
+
function: match[:function]
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
{file: nil, line: nil, function: stackframe}
|
135
|
+
end
|
136
|
+
|
137
|
+
def match_frame(regexp, stackframe)
|
138
|
+
match = regexp.match(stackframe)
|
139
|
+
return match if match
|
140
|
+
|
141
|
+
Patterns::GENERIC.match(stackframe)
|
142
|
+
end
|
143
|
+
|
144
|
+
def parse_backtrace(error)
|
145
|
+
regexp = best_regexp_for(error)
|
146
|
+
|
147
|
+
error.backtrace.map.with_index do |stackframe, i|
|
148
|
+
frame = stack_frame(regexp, stackframe)
|
149
|
+
next(frame) unless frame[:file]
|
150
|
+
|
151
|
+
frame
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Represents a small hunk of code consisting of a base line and a couple lines
|
5
|
+
# around it
|
6
|
+
class CodeHunk
|
7
|
+
MAX_LINE_LEN = 200
|
8
|
+
|
9
|
+
# How many lines should be read around the base line.
|
10
|
+
AROUND_LINES = 2
|
11
|
+
|
12
|
+
def self.get(file, line)
|
13
|
+
start_line = [line - AROUND_LINES, 1].max
|
14
|
+
|
15
|
+
lines = get_lines(file, start_line, line + AROUND_LINES)
|
16
|
+
return {start_line: 0, lines: []} if lines.empty?
|
17
|
+
|
18
|
+
{
|
19
|
+
start_line: start_line,
|
20
|
+
lines: lines
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
private_class_method def self.get_lines(file, start_line, end_line)
|
25
|
+
lines = []
|
26
|
+
return lines unless (cached_file = get_from_cache(file))
|
27
|
+
|
28
|
+
cached_file.with_index(1) do |l, i|
|
29
|
+
next if i < start_line
|
30
|
+
break if i > end_line
|
31
|
+
|
32
|
+
lines << l[0...MAX_LINE_LEN].rstrip
|
33
|
+
end
|
34
|
+
|
35
|
+
lines
|
36
|
+
end
|
37
|
+
|
38
|
+
private_class_method def self.get_from_cache(file)
|
39
|
+
Telebugs::FileCache[file] ||= File.foreach(file)
|
40
|
+
rescue
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Represents the Telebugs config. A config contains all the options that you
|
5
|
+
# can use to configure a +Telebugs::Notifier+ instance.
|
6
|
+
class Config
|
7
|
+
ERROR_API_URL = "https://api.telebugs.com/2024-03-28/errors"
|
8
|
+
|
9
|
+
attr_accessor :api_key,
|
10
|
+
:root_directory,
|
11
|
+
:middleware
|
12
|
+
|
13
|
+
attr_reader :api_url
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_writer :instance
|
17
|
+
|
18
|
+
def instance
|
19
|
+
@instance ||= new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
reset
|
25
|
+
end
|
26
|
+
|
27
|
+
def api_url=(url)
|
28
|
+
@api_url = URI(url)
|
29
|
+
end
|
30
|
+
|
31
|
+
def reset
|
32
|
+
self.api_key = nil
|
33
|
+
self.api_url = ERROR_API_URL
|
34
|
+
self.root_directory = File.realpath(
|
35
|
+
(defined?(Bundler) && Bundler.root) ||
|
36
|
+
Dir.pwd
|
37
|
+
)
|
38
|
+
|
39
|
+
@middleware = MiddlewareStack.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Parses error messages to make them more consistent.
|
5
|
+
module ErrorMessage
|
6
|
+
# On Ruby 3.1+, the error highlighting gem can produce messages that can
|
7
|
+
# span over multiple lines. We don't want to display multiline error titles.
|
8
|
+
# Therefore, we want to strip out the higlighting part so that the errors
|
9
|
+
# look consistent.
|
10
|
+
RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n"
|
11
|
+
|
12
|
+
# The options for +String#encode+
|
13
|
+
ENCODING_OPTIONS = {invalid: :replace, undef: :replace}.freeze
|
14
|
+
|
15
|
+
def self.parse(error)
|
16
|
+
return unless (msg = error.message)
|
17
|
+
|
18
|
+
msg.encode(Encoding::UTF_8, **ENCODING_OPTIONS)
|
19
|
+
.split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER)
|
20
|
+
.first
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
module FileCache
|
5
|
+
MAX_SIZE = 50
|
6
|
+
MUTEX = Mutex.new
|
7
|
+
|
8
|
+
# Associates the value given by +value+ with the key given by +key+. Deletes
|
9
|
+
# entries that exceed +MAX_SIZE+.
|
10
|
+
def self.[]=(key, value)
|
11
|
+
MUTEX.synchronize do
|
12
|
+
data[key] = value
|
13
|
+
data.delete(data.keys.first) if data.size > MAX_SIZE
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Retrieve an object from the cache.
|
18
|
+
def self.[](key)
|
19
|
+
MUTEX.synchronize do
|
20
|
+
data[key]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Checks whether the cache is empty. Needed only for the test suite.
|
25
|
+
def self.empty?
|
26
|
+
MUTEX.synchronize do
|
27
|
+
data.empty?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.reset
|
32
|
+
MUTEX.synchronize do
|
33
|
+
@data = {}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private_class_method def self.data
|
38
|
+
@data ||= {}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Represents a middleware that can be used to filter out errors.
|
5
|
+
# You must inherit from this class and implement the #call method.
|
6
|
+
class Middleware
|
7
|
+
DEFAULT_WEIGHT = 0
|
8
|
+
|
9
|
+
def weight
|
10
|
+
DEFAULT_WEIGHT
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(_report)
|
14
|
+
raise NotImplementedError, "You must implement the #call method"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# MiddlewareStack represents an ordered array of middleware.
|
5
|
+
#
|
6
|
+
# A middleware is an object that responds to <b>#call</b> (typically a Proc or a
|
7
|
+
# class that implements the call method). The <b>#call</b> method must accept
|
8
|
+
# exactly one argument: the report object.
|
9
|
+
#
|
10
|
+
# When you add a new middleware to the stack, it gets inserted according to its
|
11
|
+
# <b>weight</b>. Smaller weight means the middleware will be somewhere in the
|
12
|
+
# beginning of the array. Larger - in the end.
|
13
|
+
class MiddlewareStack
|
14
|
+
attr_reader :middlewares
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@middlewares = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def use(new_middleware)
|
21
|
+
@middlewares = (@middlewares << new_middleware).sort_by(&:weight).reverse
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(report)
|
25
|
+
@middlewares.each do |middleware|
|
26
|
+
middleware.call(report)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Notifier is reponsible for sending reports to Telebugs.
|
5
|
+
class Notifier
|
6
|
+
class << self
|
7
|
+
attr_writer :instance
|
8
|
+
|
9
|
+
def instance
|
10
|
+
@instance ||= new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@sender = Sender.new
|
16
|
+
@middleware = Config.instance.middleware
|
17
|
+
end
|
18
|
+
|
19
|
+
def notify(error)
|
20
|
+
Telebugs::Promise.new(error) do
|
21
|
+
report = Report.new(error)
|
22
|
+
|
23
|
+
@middleware.call(report)
|
24
|
+
next if report.ignored
|
25
|
+
|
26
|
+
@sender.send(report)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Wraps Concurrent::Promise to provide a consistent API for promises that we
|
5
|
+
# can control.
|
6
|
+
class Promise
|
7
|
+
def initialize(...)
|
8
|
+
@future = Concurrent::Promises.future(...)
|
9
|
+
end
|
10
|
+
|
11
|
+
def value
|
12
|
+
@future.value
|
13
|
+
end
|
14
|
+
|
15
|
+
def reason
|
16
|
+
@future.reason
|
17
|
+
end
|
18
|
+
|
19
|
+
def wait
|
20
|
+
@future.wait
|
21
|
+
end
|
22
|
+
|
23
|
+
def fulfilled?
|
24
|
+
@future.fulfilled?
|
25
|
+
end
|
26
|
+
|
27
|
+
def rejected?
|
28
|
+
@future.rejected?
|
29
|
+
end
|
30
|
+
|
31
|
+
def then(...)
|
32
|
+
@future.then(...)
|
33
|
+
end
|
34
|
+
|
35
|
+
def rescue(...)
|
36
|
+
@future.rescue(...)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Represents a piece of information that will be sent to Telebugs API to
|
5
|
+
# report an error.
|
6
|
+
class Report
|
7
|
+
# The list of possible exceptions that might be raised when an object is
|
8
|
+
# converted to JSON
|
9
|
+
JSON_EXCEPTIONS = [
|
10
|
+
IOError,
|
11
|
+
NotImplementedError,
|
12
|
+
JSON::GeneratorError,
|
13
|
+
Encoding::UndefinedConversionError
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
# The maxium size of the JSON data in bytes
|
17
|
+
MAX_REPORT_SIZE = 64000
|
18
|
+
|
19
|
+
# The maximum size of hashes, arrays and strings in the report.
|
20
|
+
DATA_MAX_SIZE = 10000
|
21
|
+
|
22
|
+
attr_reader :data
|
23
|
+
attr_accessor :ignored
|
24
|
+
|
25
|
+
def initialize(error)
|
26
|
+
@ignored = false
|
27
|
+
@truncator = Truncator.new(DATA_MAX_SIZE)
|
28
|
+
|
29
|
+
@data = {
|
30
|
+
errors: errors_as_json(error)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Converts the report to JSON. Calls +to_json+ on each object inside the
|
35
|
+
# reports' data. Truncates report data, JSON representation of which is
|
36
|
+
# bigger than {MAX_REPORT_SIZE}.
|
37
|
+
def to_json(*_args)
|
38
|
+
loop do
|
39
|
+
begin
|
40
|
+
json = @data.to_json
|
41
|
+
rescue *JSON_EXCEPTIONS
|
42
|
+
# TODO: log the error
|
43
|
+
else
|
44
|
+
return json if json && json.bytesize <= MAX_REPORT_SIZE
|
45
|
+
end
|
46
|
+
|
47
|
+
break if truncate == 0
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def errors_as_json(error)
|
54
|
+
WrappedError.new(error).unwrap.map do |e|
|
55
|
+
{
|
56
|
+
type: e.class.name,
|
57
|
+
message: ErrorMessage.parse(e),
|
58
|
+
backtrace: attach_code(Backtrace.parse(e))
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def attach_code(backtrace)
|
64
|
+
backtrace.each do |frame|
|
65
|
+
next unless frame[:file]
|
66
|
+
next unless File.exist?(frame[:file])
|
67
|
+
next unless frame[:line]
|
68
|
+
next unless frame_belogns_to_root_directory?(frame)
|
69
|
+
next if %r{vendor/bundle}.match?(frame[:file])
|
70
|
+
|
71
|
+
frame[:code] = CodeHunk.get(frame[:file], frame[:line])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def frame_belogns_to_root_directory?(frame)
|
76
|
+
frame[:file].start_with?(Telebugs::Config.instance.root_directory)
|
77
|
+
end
|
78
|
+
|
79
|
+
def truncate
|
80
|
+
@data.each_key do |key|
|
81
|
+
@data[key] = @truncator.truncate(@data[key])
|
82
|
+
end
|
83
|
+
|
84
|
+
@truncator.reduce_max_size
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# Responsible for sending HTTP requests to Telebugs.
|
5
|
+
class Sender
|
6
|
+
CONTENT_TYPE = "application/json"
|
7
|
+
|
8
|
+
USER_AGENT = "telebugs-ruby/#{Telebugs::VERSION} (#{RUBY_ENGINE}/#{RUBY_VERSION})"
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@config = Config.instance
|
12
|
+
@authorization = "Bearer #{@config.api_key}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def send(data)
|
16
|
+
req = build_request(@config.api_url, data)
|
17
|
+
|
18
|
+
resp = build_https(@config.api_url).request(req)
|
19
|
+
if resp.code_type == Net::HTTPCreated
|
20
|
+
return JSON.parse(resp.body)
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
reason = JSON.parse(resp.body)
|
25
|
+
rescue JSON::ParserError
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
raise HTTPError, "#{resp.code_type} (#{resp.code}): #{reason}"
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def build_request(uri, data)
|
35
|
+
Net::HTTP::Post.new(uri.request_uri).tap do |req|
|
36
|
+
req["Authorization"] = @authorization
|
37
|
+
req["Content-Type"] = CONTENT_TYPE
|
38
|
+
req["User-Agent"] = USER_AGENT
|
39
|
+
|
40
|
+
req.body = data.to_json
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_https(uri)
|
45
|
+
Net::HTTP.new(uri.host, uri.port).tap do |https|
|
46
|
+
https.use_ssl = uri.is_a?(URI::HTTPS)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Telebugs
|
4
|
+
# This class is responsible for the truncation of too-big objects. Mainly, you
|
5
|
+
# should use it for simple objects such as strings, hashes, & arrays.
|
6
|
+
class Truncator
|
7
|
+
# The options for +String#encode+
|
8
|
+
ENCODING_OPTIONS = {invalid: :replace, undef: :replace}.freeze
|
9
|
+
|
10
|
+
# The temporary encoding to be used when fixing invalid strings with
|
11
|
+
# +ENCODING_OPTIONS+
|
12
|
+
TEMP_ENCODING = "utf-16"
|
13
|
+
|
14
|
+
# Encodings that are eligible for fixing invalid characters
|
15
|
+
SUPPORTED_ENCODINGS = [Encoding::UTF_8, Encoding::ASCII].freeze
|
16
|
+
|
17
|
+
# What to append when something is a circular reference
|
18
|
+
CIRCULAR = "[Circular]"
|
19
|
+
|
20
|
+
# What to append when something is truncated
|
21
|
+
TRUNCATED = "[Truncated]"
|
22
|
+
|
23
|
+
# The types that can contain references to itself
|
24
|
+
CIRCULAR_TYPES = [Array, Hash, Set].freeze
|
25
|
+
|
26
|
+
# Maximum size of hashes, arrays and strings
|
27
|
+
def initialize(max_size)
|
28
|
+
@max_size = max_size
|
29
|
+
end
|
30
|
+
|
31
|
+
# Performs deep truncation of arrays, hashes, sets & strings. Uses a
|
32
|
+
# placeholder for recursive objects (`[Circular]`).
|
33
|
+
def truncate(object, seen = Set.new)
|
34
|
+
if seen.include?(object.object_id)
|
35
|
+
return CIRCULAR if CIRCULAR_TYPES.any? { |t| object.is_a?(t) }
|
36
|
+
|
37
|
+
return object
|
38
|
+
end
|
39
|
+
truncate_object(object, seen << object.object_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Reduces maximum allowed size of hashes, arrays, sets & strings by half.
|
43
|
+
def reduce_max_size
|
44
|
+
@max_size /= 2
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def truncate_object(object, seen)
|
50
|
+
case object
|
51
|
+
when Hash then truncate_hash(object, seen)
|
52
|
+
when Array then truncate_array(object, seen)
|
53
|
+
when Set then truncate_set(object, seen)
|
54
|
+
when String then truncate_string(object)
|
55
|
+
when Numeric, TrueClass, FalseClass, Symbol, NilClass then object
|
56
|
+
else
|
57
|
+
truncate_string(stringify_object(object))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def truncate_string(str)
|
62
|
+
fixed_str = replace_invalid_characters(str)
|
63
|
+
return fixed_str if fixed_str.length <= @max_size
|
64
|
+
|
65
|
+
(fixed_str.slice(0, @max_size) + TRUNCATED).freeze
|
66
|
+
end
|
67
|
+
|
68
|
+
def stringify_object(object)
|
69
|
+
object.to_json
|
70
|
+
rescue *Report::JSON_EXCEPTIONS
|
71
|
+
object.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def truncate_hash(hash, seen)
|
75
|
+
truncated_hash = {}
|
76
|
+
hash.each_with_index do |(key, val), idx|
|
77
|
+
break if idx + 1 > @max_size
|
78
|
+
|
79
|
+
truncated_hash[key] = truncate(val, seen)
|
80
|
+
end
|
81
|
+
|
82
|
+
truncated_hash.freeze
|
83
|
+
end
|
84
|
+
|
85
|
+
def truncate_array(array, seen)
|
86
|
+
array.slice(0, @max_size).map! { |elem| truncate(elem, seen) }.freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
def truncate_set(set, seen)
|
90
|
+
truncated_set = Set.new
|
91
|
+
|
92
|
+
set.each do |elem|
|
93
|
+
truncated_set << truncate(elem, seen)
|
94
|
+
break if truncated_set.size >= @max_size
|
95
|
+
end
|
96
|
+
|
97
|
+
truncated_set.freeze
|
98
|
+
end
|
99
|
+
|
100
|
+
# Replaces invalid characters in a string with arbitrary encoding.
|
101
|
+
# @see https://github.com/flori/json/commit/3e158410e81f94dbbc3da6b7b35f4f64983aa4e3
|
102
|
+
def replace_invalid_characters(str)
|
103
|
+
utf8_string = SUPPORTED_ENCODINGS.include?(str.encoding)
|
104
|
+
return str if utf8_string && str.valid_encoding?
|
105
|
+
|
106
|
+
temp_str = str.dup
|
107
|
+
temp_str.encode!(TEMP_ENCODING, **ENCODING_OPTIONS) if utf8_string
|
108
|
+
temp_str.encode!("utf-8", **ENCODING_OPTIONS)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Telebugs
|
2
|
+
# WrappedError unwraps an error and its causes up to a certain depth.
|
3
|
+
class WrappedError
|
4
|
+
MAX_NESTED_ERRORS = 3
|
5
|
+
|
6
|
+
def initialize(error)
|
7
|
+
@error = error
|
8
|
+
end
|
9
|
+
|
10
|
+
def unwrap
|
11
|
+
error_list = []
|
12
|
+
error = @error
|
13
|
+
|
14
|
+
while error && error_list.size < MAX_NESTED_ERRORS
|
15
|
+
error_list << error
|
16
|
+
error = error.cause
|
17
|
+
end
|
18
|
+
|
19
|
+
error_list
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/telebugs.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
require "net/https"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
require_relative "telebugs/version"
|
8
|
+
require_relative "telebugs/config"
|
9
|
+
require_relative "telebugs/promise"
|
10
|
+
require_relative "telebugs/notifier"
|
11
|
+
require_relative "telebugs/sender"
|
12
|
+
require_relative "telebugs/wrapped_error"
|
13
|
+
require_relative "telebugs/report"
|
14
|
+
require_relative "telebugs/error_message"
|
15
|
+
require_relative "telebugs/backtrace"
|
16
|
+
require_relative "telebugs/file_cache"
|
17
|
+
require_relative "telebugs/code_hunk"
|
18
|
+
require_relative "telebugs/middleware"
|
19
|
+
require_relative "telebugs/middleware_stack"
|
20
|
+
require_relative "telebugs/truncator"
|
21
|
+
|
22
|
+
module Telebugs
|
23
|
+
# The general error that this library uses when it wants to raise.
|
24
|
+
Error = Class.new(StandardError)
|
25
|
+
|
26
|
+
HTTPError = Class.new(Error)
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def configure
|
30
|
+
yield Config.instance
|
31
|
+
end
|
32
|
+
|
33
|
+
def notify(error:)
|
34
|
+
Notifier.instance.notify(error)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/sig/telebugs.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: telebugs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kyrylo Silin
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
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.23'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.23'
|
41
|
+
description: |
|
42
|
+
Telebugs Ruby is an SDK for Telebugs (https://telebugs.com/), a simple error monitoring tool for developers. With
|
43
|
+
Telebugs, you can track production errors in real-time and report them to Telegram.
|
44
|
+
email:
|
45
|
+
- help@telebugs.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- ".standard.yml"
|
51
|
+
- LICENSE.md
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- lib/telebugs.rb
|
55
|
+
- lib/telebugs/backtrace.rb
|
56
|
+
- lib/telebugs/code_hunk.rb
|
57
|
+
- lib/telebugs/config.rb
|
58
|
+
- lib/telebugs/error_message.rb
|
59
|
+
- lib/telebugs/file_cache.rb
|
60
|
+
- lib/telebugs/middleware.rb
|
61
|
+
- lib/telebugs/middleware_stack.rb
|
62
|
+
- lib/telebugs/notifier.rb
|
63
|
+
- lib/telebugs/promise.rb
|
64
|
+
- lib/telebugs/report.rb
|
65
|
+
- lib/telebugs/sender.rb
|
66
|
+
- lib/telebugs/truncator.rb
|
67
|
+
- lib/telebugs/version.rb
|
68
|
+
- lib/telebugs/wrapped_error.rb
|
69
|
+
- sig/telebugs.rbs
|
70
|
+
homepage: https://telebugs.com
|
71
|
+
licenses: []
|
72
|
+
metadata:
|
73
|
+
homepage_uri: https://telebugs.com
|
74
|
+
source_code_uri: https://github.com/telebugs/telebugs-ruby
|
75
|
+
rubygems_mfa_required: 'true'
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: 3.0.0
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubygems_version: 3.5.3
|
92
|
+
signing_key:
|
93
|
+
specification_version: 4
|
94
|
+
summary: Report errors to Telebugs with the offical Ruby SDK
|
95
|
+
test_files: []
|