Skip to content

Commit

Permalink
Merge pull request #6 from telebugs/notifier
Browse files Browse the repository at this point in the history
Implement notice sending
  • Loading branch information
kyrylo committed Jun 6, 2024
2 parents bb89286 + cf1d9bd commit c4db672
Show file tree
Hide file tree
Showing 14 changed files with 394 additions and 10 deletions.
12 changes: 9 additions & 3 deletions lib/telebugs.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
# frozen_string_literal: true

require "concurrent"
require "net/https"
require "json"

require_relative "telebugs/version"
require_relative "telebugs/config"
require_relative "telebugs/promise"
require_relative "telebugs/notifier"
require_relative "telebugs/sender"
require_relative "telebugs/wrapped_error"
require_relative "telebugs/notice"

module Telebugs
# The general error that this library uses when it wants to raise.
Error = Class.new(StandardError)

HTTPError = Class.new(Error)

class << self
def configure
yield Telebugs::Config.instance
yield Config.instance
end

def notify(error:)
Telebugs::Promise.new(error) do
end
Notifier.instance.notify(error)
end
end
end
14 changes: 13 additions & 1 deletion lib/telebugs/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ module Telebugs
# Represents the Telebugs config. A config contains all the options that you
# can use to configure a +Telebugs::Notifier+ instance.
class Config
ERROR_API_URL = "https://api.telebugs.com/2024-03-28/errors"

attr_accessor :api_key
attr_reader :api_url

class << self
attr_writer :instance
Expand All @@ -15,7 +18,16 @@ def instance
end

def initialize
@api_key = nil
reset
end

def api_url=(url)
@api_url = URI(url)
end

def reset
self.api_key = nil
self.api_url = ERROR_API_URL
end
end
end
74 changes: 74 additions & 0 deletions lib/telebugs/notice.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

module Telebugs
# Represents a piece of information that will be sent to Telebugs API to
# report an error.
class Notice
# The list of possible exceptions that might be raised when an object is
# converted to JSON
JSON_EXCEPTIONS = [
IOError,
NotImplementedError,
JSON::GeneratorError,
Encoding::UndefinedConversionError
].freeze

# On Ruby 3.1+, the error highlighting gem can produce messages that can
# span over multiple lines. We don't want to display multiline error titles.
# Therefore, we want to strip out the higlighting part so that the errors
# look consistent.
RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n"

# The options for +String#encode+
ENCODING_OPTIONS = {invalid: :replace, undef: :replace}.freeze

# The maxium size of the JSON payload in bytes
MAX_NOTICE_SIZE = 64000

def initialize(error)
@payload = {
errors: errors_as_json(error)
}
end

# Converts the notice to JSON. Calls +to_json+ on each object inside
# notice's payload. Truncates notices, JSON representation of which is
# bigger than {MAX_NOTICE_SIZE}.
def to_json(*_args)
loop do
begin
json = @payload.to_json
rescue *JSON_EXCEPTIONS
# TODO
else
return json if json && json.bytesize <= MAX_NOTICE_SIZE
end

break if truncate == 0
end
end

private

def errors_as_json(error)
WrappedError.new(error).unwrap.map do |e|
{
type: e.class.name,
message: message(e)
}
end
end

def message(error)
return unless (msg = error.message)

msg.encode(Encoding::UTF_8, **ENCODING_OPTIONS)
.split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER)
.first
end

def truncate
0
end
end
end
24 changes: 24 additions & 0 deletions lib/telebugs/notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Telebugs
# Notifier is reponsible for sending notices to Telebugs.
class Notifier
class << self
attr_writer :instance

def instance
@instance ||= new
end
end

def initialize
@sender = Sender.new
end

def notify(error)
Telebugs::Promise.new(error) do
@sender.send(error)
end
end
end
end
16 changes: 16 additions & 0 deletions lib/telebugs/promise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,21 @@ def initialize(...)
def value
@future.value
end

def reason
@future.reason
end

def wait
@future.wait
end

def fulfilled?
@future.fulfilled?
end

def rejected?
@future.rejected?
end
end
end
44 changes: 44 additions & 0 deletions lib/telebugs/sender.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Telebugs
# Responsible for sending HTTP requests to Telebugs.
class Sender
CONTENT_TYPE = "application/json"

USER_AGENT = "telebugs-ruby/#{Telebugs::VERSION} (#{RUBY_ENGINE}/#{RUBY_VERSION})"

def initialize
@config = Config.instance
@authorization = "Bearer #{@config.api_key}"
end

def send(data)
req = build_request(@config.api_url, data)

resp = build_https(@config.api_url).request(req)
if resp.code_type == Net::HTTPCreated
return JSON.parse(resp.body)
end

raise HTTPError, "#{resp.code_type} (#{resp.code}): #{JSON.parse(resp.body)}"
end

private

def build_request(uri, data)
Net::HTTP::Post.new(uri.request_uri).tap do |req|
req["Authorization"] = @authorization
req["Content-Type"] = CONTENT_TYPE
req["User-Agent"] = USER_AGENT

req.body = data.to_json
end
end

def build_https(uri)
Net::HTTP.new(uri.host, uri.port).tap do |https|
https.use_ssl = uri.is_a?(URI::HTTPS)
end
end
end
end
2 changes: 2 additions & 0 deletions telebugs.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Gem::Specification.new do |spec|

spec.add_dependency "concurrent-ruby", "~> 1.3"

spec.add_development_dependency "webmock", "~> 3.23"

# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
spec.metadata["rubygems_mfa_required"] = "true"
Expand Down
21 changes: 21 additions & 0 deletions test/test_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require "test_helper"

class TestConfig < Minitest::Test
def teardown
Telebugs::Config.instance.reset
end

def test_api_key
Telebugs.configure { |c| c.api_key = "12345:abcdef" }

assert_equal "12345:abcdef", Telebugs::Config.instance.api_key
end

def test_error_api_url
Telebugs.configure { |c| c.api_url = "example.com" }

assert_equal URI("example.com"), Telebugs::Config.instance.api_url
end
end
1 change: 1 addition & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
require "telebugs"

require "minitest/autorun"
require "webmock/minitest"
89 changes: 89 additions & 0 deletions test/test_notice.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

require "test_helper"

class TestNotice < Minitest::Test
def test_to_json_with_nested_errors
begin
raise StandardError.new("error 1")
rescue => _
begin
raise StandardError.new("error 2")
rescue => e2
n = Telebugs::Notice.new(e2)
end
end

assert_equal(
{
"errors" => [
{
"type" => "StandardError",
"message" => "error 2"
},
{
"type" => "StandardError",
"message" => "error 1"
}
]
},
JSON.parse(n.to_json)
)
end

def test_to_json_with_error_highlighting_in_messages
begin
raise "undefined method `[]' for nil:NilClass\n\n " \
"data[:result].first[:first_name]\n ^^^^^^^^^^^^^"
rescue => e
end

n = Telebugs::Notice.new(e)

assert_equal(
{
"errors" => [
{
"type" => "RuntimeError",
"message" => "undefined method `[]' for nil:NilClass"
}
]
},
JSON.parse(n.to_json)
)
end

def test_to_json_when_error_message_contains_invalid_characters
begin
JSON.parse(Marshal.dump(Time.now))
rescue JSON::ParserError => e
end

n = Telebugs::Notice.new(e)
json = JSON.parse(n.to_json)

assert_equal "JSON::ParserError", json["errors"].first["type"]
assert_match(/unexpected token at/, json["errors"].first["message"])
end

def test_to_json_when_error_message_is_nil
error = Class.new(StandardError) {
def message
end
}.new

n = Telebugs::Notice.new(error)

assert_equal(
{
"errors" => [
{
"type" => nil,
"message" => nil
}
]
},
JSON.parse(n.to_json)
)
end
end
18 changes: 18 additions & 0 deletions test/test_notifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require "test_helper"

class TestNotifier < Minitest::Test
def teardown
WebMock.reset!
end

def test_notify_returns_a_promise_that_resolves_to_a_hash
stub_request(:post, Telebugs::Config.instance.api_url)
.to_return(status: 201, body: {id: "123"}.to_json)

p = Telebugs::Notifier.new.notify(StandardError.new)

assert_equal({"id" => "123"}, p.value)
end
end
Loading

0 comments on commit c4db672

Please sign in to comment.