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 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: []