telebugs 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|