telebugs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Telebugs
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module Telebugs
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []