Skip to content

Commit fb9d960

Browse files
authored
Always return Sentry.ClientError when possible (#795)
1 parent dc8b119 commit fb9d960

File tree

8 files changed

+107
-77
lines changed

8 files changed

+107
-77
lines changed

lib/sentry.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ defmodule Sentry do
404404
"""
405405
@doc since: "10.2.0"
406406
@spec capture_check_in(keyword()) ::
407-
{:ok, check_in_id :: String.t()} | :ignored | {:error, term()}
407+
{:ok, check_in_id :: String.t()} | :ignored | {:error, ClientError.t()}
408408
def capture_check_in(options) when is_list(options) do
409409
if Config.dsn() do
410410
options

lib/sentry/client.ex

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule Sentry.Client do
2424
@max_message_length 8_192
2525

2626
@spec send_check_in(CheckIn.t(), keyword()) ::
27-
{:ok, check_in_id :: String.t()} | {:error, term()}
27+
{:ok, check_in_id :: String.t()} | {:error, ClientError.t()}
2828
def send_check_in(%CheckIn{} = check_in, opts) when is_list(opts) do
2929
client = Keyword.get_lazy(opts, :client, &Config.client/0)
3030

@@ -37,7 +37,7 @@ defmodule Sentry.Client do
3737
send_result =
3838
check_in
3939
|> Envelope.from_check_in()
40-
|> Transport.post_envelope(client, request_retries)
40+
|> Transport.encode_and_post_envelope(client, request_retries)
4141

4242
_ = maybe_log_send_result(send_result, check_in)
4343
send_result
@@ -87,11 +87,8 @@ defmodule Sentry.Client do
8787
:excluded ->
8888
:excluded
8989

90-
{:error, {status, headers, body}} ->
91-
{:error, ClientError.server_error(status, headers, body)}
92-
93-
{:error, reason} ->
94-
{:error, ClientError.new(reason)}
90+
{:error, %ClientError{} = error} ->
91+
{:error, error}
9592
end
9693
end
9794

@@ -176,7 +173,7 @@ defmodule Sentry.Client do
176173
send_result =
177174
event
178175
|> Envelope.from_event()
179-
|> Transport.post_envelope(client, request_retries)
176+
|> Transport.encode_and_post_envelope(client, request_retries)
180177

181178
_ = maybe_log_send_result(send_result, event)
182179
send_result
@@ -315,24 +312,8 @@ defmodule Sentry.Client do
315312
else
316313
message =
317314
case send_result do
318-
{:error, {:invalid_json, error}} ->
319-
"Failed to send Sentry event. Unable to encode JSON Sentry error - #{inspect(error)}"
320-
321-
{:error, {:request_failure, last_error}} ->
322-
case last_error do
323-
{kind, data, stacktrace}
324-
when kind in [:exit, :throw, :error] and is_list(stacktrace) ->
325-
Exception.format(kind, data, stacktrace)
326-
327-
_other ->
328-
"Failed to send Sentry event. Error in HTTP Request to Sentry - #{inspect(last_error)}"
329-
end
330-
331-
{:error, {status, headers, body}} ->
332-
ClientError.server_error(status, headers, body) |> ClientError.message()
333-
334-
{:error, reason} ->
335-
ClientError.new(reason) |> ClientError.message()
315+
{:error, %ClientError{} = error} ->
316+
"Failed to send Sentry event. #{Exception.message(error)}"
336317

337318
{:ok, _} ->
338319
nil

lib/sentry/client_error.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,18 @@ defmodule Sentry.ClientError do
7070
end
7171

7272
defp format(:too_many_retries) do
73-
"Sentry responded with status 429 - Too Many Requests"
73+
"Sentry responded with status 429 - Too Many Requests and the SDK exhausted the configured retries"
7474
end
7575

7676
defp format({:invalid_json, reason}) do
77-
"the Sentry SDK could not encode the event to JSON: #{Exception.message(reason)}"
77+
formatted =
78+
if is_exception(reason) do
79+
Exception.message(reason)
80+
else
81+
inspect(reason)
82+
end
83+
84+
"the Sentry SDK could not encode the event to JSON: #{formatted}"
7885
end
7986

8087
defp format({:request_failure, reason}) do

lib/sentry/transport.ex

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
defmodule Sentry.Transport do
22
@moduledoc false
33

4-
# This module is exclusively responsible for POSTing envelopes to Sentry.
4+
# This module is exclusively responsible for encoding and POSTing envelopes to Sentry.
55

6+
alias Sentry.ClientError
67
alias Sentry.Config
78
alias Sentry.Envelope
89

@@ -15,18 +16,24 @@ defmodule Sentry.Transport do
1516
@default_retries
1617
end
1718

18-
# The "retries" parameter is there for better testing.
19-
@spec post_envelope(Envelope.t(), module(), [non_neg_integer()]) ::
20-
{:ok, envelope_id :: String.t()} | {:error, term()}
21-
def post_envelope(%Envelope{} = envelope, client, retries \\ @default_retries)
19+
@doc """
20+
Encodes the given envelope and POSTs it to Sentry.
21+
22+
23+
The `retries` parameter is there for better testing. This function also logs
24+
a warning if there is an error encoding or posting the envelope.
25+
"""
26+
@spec encode_and_post_envelope(Envelope.t(), module(), [non_neg_integer()]) ::
27+
{:ok, envelope_id :: String.t()} | {:error, ClientError.t()}
28+
def encode_and_post_envelope(%Envelope{} = envelope, client, retries \\ @default_retries)
2229
when is_atom(client) and is_list(retries) do
2330
case Envelope.to_binary(envelope) do
2431
{:ok, body} ->
2532
{endpoint, headers} = get_endpoint_and_headers()
2633
post_envelope_with_retries(client, endpoint, headers, body, retries)
2734

2835
{:error, reason} ->
29-
{:error, {:invalid_json, reason}}
36+
{:error, ClientError.new({:invalid_json, reason})}
3037
end
3138
end
3239

@@ -42,18 +49,18 @@ defmodule Sentry.Transport do
4249
post_envelope_with_retries(client, endpoint, headers, payload, tl(retries_left))
4350

4451
{:retry_after, _delay_ms} ->
45-
{:error, :too_many_retries}
52+
{:error, ClientError.new(:too_many_retries)}
4653

4754
{:error, _reason} when retries_left != [] ->
4855
[sleep_interval | retries_left] = retries_left
4956
Process.sleep(sleep_interval)
5057
post_envelope_with_retries(client, endpoint, headers, payload, retries_left)
5158

52-
{:error, {status, headers, body}} ->
53-
{:error, {status, headers, body}}
59+
{:error, {:http, {status, headers, body}}} ->
60+
{:error, ClientError.server_error(status, headers, body)}
5461

5562
{:error, reason} ->
56-
{:error, {:request_failure, reason}}
63+
{:error, ClientError.new(reason)}
5764
end
5865
end
5966

@@ -78,10 +85,10 @@ defmodule Sentry.Transport do
7885
{:retry_after, delay_ms}
7986

8087
{:ok, status, headers, body} ->
81-
{:error, {status, headers, body}}
88+
{:error, {:http, {status, headers, body}}}
8289

8390
{:error, reason} ->
84-
{:error, reason}
91+
{:error, {:request_failure, reason}}
8592
end
8693
catch
8794
kind, data -> {:error, {kind, data, __STACKTRACE__}}

lib/sentry/transport/sender.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ defmodule Sentry.Transport.Sender do
4343
def handle_cast({:send, client, %Event{} = event}, %__MODULE__{} = state) do
4444
event
4545
|> Envelope.from_event()
46-
|> Transport.post_envelope(client)
46+
|> Transport.encode_and_post_envelope(client)
4747
|> Client.maybe_log_send_result([event])
4848

4949
# We sent an event, so we can decrease the number of queued events.

test/sentry/client_error_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule Sentry.ClientErrorTest do
55
describe "c:Exception.message/1" do
66
test "with an atom reason" do
77
assert message_for_reason(:too_many_retries) ==
8-
"Sentry responded with status 429 - Too Many Requests"
8+
"Sentry responded with status 429 - Too Many Requests and the SDK exhausted the configured retries"
99
end
1010

1111
test "with {:invalid_json, _} reason" do

test/sentry/client_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ defmodule Sentry.ClientTest do
342342

343343
assert capture_log(fn ->
344344
Client.send_event(event, result: :sync)
345-
end) =~ "Failed to send Sentry event. Unable to encode JSON"
345+
end) =~ "the Sentry SDK could not encode the event to JSON: :im_just_bad"
346346
end
347347

348348
test "uses the async sender pool when :result is :none", %{bypass: bypass} do

test/sentry/transport_test.exs

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ defmodule Sentry.TransportTest do
33

44
import Sentry.TestHelpers
55

6-
alias Sentry.{Envelope, Event, HackneyClient, Transport}
6+
alias Sentry.{ClientError, Envelope, Event, HackneyClient, Transport}
77

8-
describe "post_envelope/2" do
8+
describe "encode_and_post_envelope/2" do
99
setup do
1010
bypass = Bypass.open()
1111
put_test_config(dsn: "http://public:secret@localhost:#{bypass.port}/1")
@@ -33,31 +33,46 @@ defmodule Sentry.TransportTest do
3333
Plug.Conn.resp(conn, 200, ~s<{"id":"123"}>)
3434
end)
3535

36-
assert {:ok, "123"} = Transport.post_envelope(envelope, HackneyClient)
36+
assert {:ok, "123"} = Transport.encode_and_post_envelope(envelope, HackneyClient)
3737
end
3838

39-
defmodule InvalidHTTPClient do
40-
def post(_endpoint, _headers, _body) do
41-
{:ok, "not an integer", :badarg, %{}}
39+
test "returns an error if the HTTP client returns a badly-typed response" do
40+
defmodule InvalidHTTPClient do
41+
def post(_endpoint, _headers, _body) do
42+
Process.get(:invalid_http_return_value) ||
43+
raise "missing :invalid_http_return_value from pdict"
44+
end
4245
end
43-
end
4446

45-
test "returns an error with an invalid response from a POST request" do
4647
envelope = Envelope.from_event(Event.create_event(message: "Hello 1"))
4748

48-
assert {:error,
49-
{:request_failure,
50-
{:malformed_http_client_response, "not an integer", :badarg, %{}}}} =
51-
Transport.post_envelope(envelope, InvalidHTTPClient, _retries = [])
49+
for {:ok, status, headers, body} = invalid_return_value <- [
50+
{:ok, 10000, [], ""},
51+
{:ok, 200, %{}, ""},
52+
{:ok, 200, [], :not_a_binary}
53+
] do
54+
Process.put(:invalid_http_return_value, invalid_return_value)
55+
56+
assert {:request_failure, {:malformed_http_client_response, ^status, ^headers, ^body}} =
57+
error(fn ->
58+
Transport.encode_and_post_envelope(envelope, InvalidHTTPClient, _retries = [])
59+
end)
60+
end
61+
after
62+
Process.delete(:invalid_http_return_value)
63+
:code.delete(InvalidHTTPClient)
64+
:code.purge(InvalidHTTPClient)
5265
end
5366

5467
test "returns the HTTP client's error if the HTTP client returns one", %{bypass: bypass} do
5568
envelope = Envelope.from_event(Event.create_event(message: "Hello"))
5669

5770
Bypass.down(bypass)
5871

59-
assert {:error, {:request_failure, :econnrefused}} =
60-
Transport.post_envelope(envelope, HackneyClient, _retries = [])
72+
assert {:request_failure, :econnrefused} =
73+
error(fn ->
74+
Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [])
75+
end)
6176
end
6277

6378
test "returns an error if the response from Sentry is not 200", %{bypass: bypass} do
@@ -69,10 +84,15 @@ defmodule Sentry.TransportTest do
6984
|> Plug.Conn.resp(400, ~s<{}>)
7085
end)
7186

72-
{:error, {_status, headers, _body}} =
73-
Transport.post_envelope(envelope, HackneyClient, _retries = [])
87+
{:error, %ClientError{} = error} =
88+
Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [])
7489

90+
assert error.reason == :server_error
91+
assert {400, headers, "{}"} = error.http_response
7592
assert :proplists.get_value("x-sentry-error", headers, nil) == "some error"
93+
94+
assert Exception.message(error) =~
95+
"the Sentry server responded with an error, the details are below."
7696
end
7797

7898
test "returns an error if the HTTP client raises an error when making the request",
@@ -85,10 +105,10 @@ defmodule Sentry.TransportTest do
85105
end
86106
end
87107

88-
assert {:error, {:error, %RuntimeError{} = exception, _stacktrace}} =
89-
Transport.post_envelope(envelope, RaisingHTTPClient, _retries = [])
90-
91-
assert exception.message == "I'm a really bad HTTP client"
108+
assert {:error, %RuntimeError{message: "I'm a really bad HTTP client"}, _stacktrace} =
109+
error(fn ->
110+
Transport.encode_and_post_envelope(envelope, RaisingHTTPClient, _retries = [])
111+
end)
92112
after
93113
:code.delete(RaisingHTTPClient)
94114
:code.purge(RaisingHTTPClient)
@@ -104,8 +124,10 @@ defmodule Sentry.TransportTest do
104124
end
105125
end
106126

107-
assert {:error, {:exit, :through_the_window, _stacktrace}} =
108-
Transport.post_envelope(envelope, ExitingHTTPClient, _retries = [])
127+
assert {:exit, :through_the_window, _stacktrace} =
128+
error(fn ->
129+
Transport.encode_and_post_envelope(envelope, ExitingHTTPClient, _retries = [])
130+
end)
109131
after
110132
:code.delete(ExitingHTTPClient)
111133
:code.purge(ExitingHTTPClient)
@@ -121,8 +143,10 @@ defmodule Sentry.TransportTest do
121143
end
122144
end
123145

124-
assert {:error, {:throw, :catch_me_if_you_can, _stacktrace}} =
125-
Transport.post_envelope(envelope, ThrowingHTTPClient, _retries = [])
146+
assert {:throw, :catch_me_if_you_can, _stacktrace} =
147+
error(fn ->
148+
Transport.encode_and_post_envelope(envelope, ThrowingHTTPClient, _retries = [])
149+
end)
126150
after
127151
:code.delete(ThrowingHTTPClient)
128152
:code.purge(ThrowingHTTPClient)
@@ -145,10 +169,10 @@ defmodule Sentry.TransportTest do
145169

146170
put_test_config(json_library: CrashingJSONLibrary)
147171

148-
assert {:error, {:error, %RuntimeError{} = exception, _stacktrace}} =
149-
Transport.post_envelope(envelope, HackneyClient, _retries = [])
150-
151-
assert exception.message == "I'm a really bad JSON library"
172+
assert {:error, %RuntimeError{message: "I'm a really bad JSON library"}, _stacktrace} =
173+
error(fn ->
174+
Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [])
175+
end)
152176
after
153177
:code.delete(CrashingJSONLibrary)
154178
:code.purge(CrashingJSONLibrary)
@@ -165,8 +189,10 @@ defmodule Sentry.TransportTest do
165189
Plug.Conn.resp(conn, 200, ~s<invalid JSON>)
166190
end)
167191

168-
assert {:error, {:request_failure, %Jason.DecodeError{}}} =
169-
Transport.post_envelope(envelope, HackneyClient, _retries = [0])
192+
assert {:request_failure, %Jason.DecodeError{}} =
193+
error(fn ->
194+
Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [0])
195+
end)
170196

171197
assert_received {:request, ^ref}
172198
assert_received {:request, ^ref}
@@ -191,7 +217,8 @@ defmodule Sentry.TransportTest do
191217
end
192218
end)
193219

194-
assert {:ok, "123"} = Transport.post_envelope(envelope, HackneyClient, _retries = [10, 25])
220+
assert {:ok, "123"} =
221+
Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [10, 25])
195222

196223
assert System.system_time(:millisecond) - start_time >= 35
197224

@@ -213,10 +240,18 @@ defmodule Sentry.TransportTest do
213240
|> Plug.Conn.resp(429, ~s<{}>)
214241
end)
215242

216-
assert {:error, :too_many_retries} =
217-
Transport.post_envelope(envelope, HackneyClient, _retries = [])
243+
assert :too_many_retries =
244+
error(fn ->
245+
Transport.encode_and_post_envelope(envelope, HackneyClient, _retries = [])
246+
end)
218247

219248
assert_received {:request, ^ref}
220249
end
221250
end
251+
252+
defp error(fun) do
253+
assert {:error, %ClientError{} = error} = fun.()
254+
assert is_binary(Exception.message(error))
255+
error.reason
256+
end
222257
end

0 commit comments

Comments
 (0)