Skip to content

Commit

Permalink
Implement Notifier
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo committed Jun 6, 2024
1 parent a41720c commit cf1d9bd
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 17 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
8 changes: 6 additions & 2 deletions lib/telebugs/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ def instance
end

def initialize
self.api_key = nil
self.api_url = ERROR_API_URL
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
4 changes: 3 additions & 1 deletion lib/telebugs/sender.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ def send(data)
req = build_request(@config.api_url, data)

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

raise HTTPError, "#{resp.code_type} (#{resp.code}): #{JSON.parse(resp.body)}"
end
Expand Down
4 changes: 4 additions & 0 deletions test/test_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
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" }

Expand Down
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
21 changes: 13 additions & 8 deletions test/test_sender.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,37 @@
require "test_helper"

class TestSender < Minitest::Test
def teardown
Telebugs::Config.instance.reset
WebMock.reset!
end

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

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

Telebugs::Sender.new.send({"error" => "error"})
Telebugs::Sender.new.send({"errors" => []})

assert_requested stub.with(
headers: {
"Authorization" => "Bearer 12345:abcdef",
"Content-Type" => "application/json",
"User-Agent" => "telebugs-ruby/#{Telebugs::VERSION} (ruby/#{RUBY_VERSION})"
},
body: {"error" => "error"}.to_json
body: {"errors" => []}.to_json
)
end

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

stub_request(:post, Telebugs::Config.instance.api_url)
.to_return(status: 500, body: {"error" => "error"}.to_json)
stub = stub_request(:post, Telebugs::Config.instance.api_url)
.to_return(status: 500, body: {"error" => "oops"}.to_json)

assert_raises(Telebugs::HTTPError) do
Telebugs::Sender.new.send({"error" => "error"})
Telebugs::Sender.new.send({"errors" => []})
end

assert_requested stub
end
end
24 changes: 21 additions & 3 deletions test/test_telebugs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
require "test_helper"

class TestTelebugs < Minitest::Test
def teardown
Telebugs::Config.instance.reset
WebMock.reset!
end

def test_that_it_has_a_version_number
refute_nil ::Telebugs::VERSION
end
Expand All @@ -17,9 +22,22 @@ def test_configure_configures_project_key
assert_equal key, Telebugs::Config.instance.api_key
end

def test_notify_returns_a_future
future = Telebugs.notify(error: StandardError.new)
def test_notify_returns_a_fullfilled_promise_when_request_succeeds
stub_request(:post, Telebugs::Config.instance.api_url)
.to_return(status: 201, body: {id: "123"}.to_json)

p = Telebugs.notify(error: StandardError.new)
p.wait

assert p.fulfilled?
end

def test_notify_returns_a_rejected_promise_when_request_fails
stub_request(:post, Telebugs::Config.instance.api_url).to_return(status: 500)

p = Telebugs.notify(error: StandardError.new)
p.wait

assert_instance_of Telebugs::Promise, future
assert p.rejected?
end
end

0 comments on commit cf1d9bd

Please sign in to comment.