Skip to content

Commit 1c33a5e

Browse files
Merge pull request #400 from josevalim/jv-thin-http-client
Make hackney an optional dependency
2 parents 0dcf5ff + a635a60 commit 1c33a5e

File tree

14 files changed

+152
-146
lines changed

14 files changed

+152
-146
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ If you would like to upgrade a project to use Sentry 7.x, see [here](https://gis
1717

1818
## Installation
1919

20-
To use Sentry with your projects, edit your mix.exs file and add it as a dependency. Sentry does not install a JSON library itself, and requires users to have one available. Sentry will default to trying to use Jason for JSON operations, but can be configured to use other ones.
20+
To use Sentry with your projects, edit your mix.exs file and add it as a dependency. Sentry does not install a JSON library nor HTTP client by itself. Sentry will default to trying to use Jason for JSON operations and Hackney for HTTP requests, but can be configured to use other ones. To use the default ones, do:
2121

2222
```elixir
2323
defp deps do
2424
[
2525
# ...
2626
{:sentry, "~> 7.0"},
2727
{:jason, "~> 1.1"},
28+
{:hackney, "~> 1.8"}
2829
]
2930
end
3031
```
@@ -116,13 +117,15 @@ The full range of options is the following:
116117
| `tags` | False | `%{}` | |
117118
| `release` | False | None | |
118119
| `server_name` | False | None | |
119-
| `client` | False | `Sentry.Client` | If you need different functionality for the HTTP client, you can define your own module that implements the `Sentry.HTTPClient` behaviour and set `client` to that module |
120+
| `client` | False | `Sentry.HackneyClient` | If you need different functionality for the HTTP client, you can define your own module that implements the `Sentry.HTTPClient` behaviour and set `client` to that module |
120121
| `hackney_opts` | False | `[pool: :sentry_pool]` | |
121122
| `hackney_pool_max_connections` | False | 50 | |
122123
| `hackney_pool_timeout` | False | 5000 | |
123124
| `before_send_event` | False | | |
124125
| `after_send_event` | False | | |
125126
| `sample_rate` | False | 1.0 | |
127+
| `send_result` | False | `:none` | You may want to set it to `:sync` if testing your Sentry integration. See "Testing with Sentry" |
128+
| `send_max_attempts` | False | 4 | |
126129
| `in_app_module_whitelist` | False | `[]` | |
127130
| `report_deps` | False | True | Will attempt to load Mix dependencies at compile time to report alongside events |
128131
| `enable_source_code_context` | False | False | |
@@ -242,10 +245,13 @@ test "add/2 does not raise but sends an event to Sentry when given bad input" do
242245
end)
243246

244247
Application.put_env(:sentry, :dsn, "http://public:secret@localhost:#{bypass.port}/1")
248+
Application.put_env(:sentry, :send_result, :sync)
245249
MyModule.add(1, "a")
246250
end
247251
```
248252

253+
When testing, you will also want to set the `send_result` type to `:sync`, so the request is done synchronously.
254+
249255
## License
250256

251257
This project is Licensed under the [MIT License](https://github.com/getsentry/sentry-elixir/blob/master/LICENSE).

config/test.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ use Mix.Config
33
config :sentry,
44
environment_name: :test,
55
included_environments: [:test],
6-
client: Sentry.TestClient,
7-
hackney_opts: [recv_timeout: 50]
6+
hackney_opts: [recv_timeout: 50],
7+
send_result: :sync,
8+
send_max_attempts: 1
89

910
config :ex_unit,
1011
assert_receive_timeout: 500

lib/mix/tasks/sentry.send_test_event.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule Mix.Tasks.Sentry.SendTestEvent do
99

1010
def run(args) do
1111
unless "--no-compile" in args do
12-
Mix.Project.compile(args)
12+
Mix.Task.run("compile", args)
1313
end
1414

1515
Application.ensure_all_started(:sentry)

lib/sentry.ex

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,17 @@ defmodule Sentry do
7575
7676
## Capturing Exceptions
7777
78-
Simply calling `capture_exception/2` will send the event. By default, the event is sent asynchronously and the result can be awaited upon. The `:result` option can be used to change this behavior. See `Sentry.Client.send_event/2` for more information.
78+
Simply calling `capture_exception/2` will send the event. By default, the event
79+
is sent asynchronously and the result can be awaited upon. The `:result` option
80+
can be used to change this behavior. See `Sentry.Client.send_event/2` for more
81+
information.
7982
8083
{:ok, task} = Sentry.capture_exception(my_exception)
8184
{:ok, event_id} = Task.await(task)
82-
8385
{:ok, another_event_id} = Sentry.capture_exception(other_exception, [event_source: :my_source, result: :sync])
8486
8587
### Options
88+
8689
* `:event_source` - The source passed as the first argument to `c:Sentry.EventFilter.exclude_exception?/2`
8790
8891
## Configuring The `Logger` Backend
@@ -95,11 +98,7 @@ defmodule Sentry do
9598
def start(_type, _opts) do
9699
children = [
97100
{Task.Supervisor, name: Sentry.TaskSupervisor},
98-
:hackney_pool.child_spec(
99-
Sentry.Client.hackney_pool_name(),
100-
timeout: Config.hackney_timeout(),
101-
max_connections: Config.max_hackney_connections()
102-
)
101+
Config.client().child_spec()
103102
]
104103

105104
validate_json_config!()
@@ -157,10 +156,9 @@ defmodule Sentry do
157156
def send_event(%Event{} = event, opts) do
158157
included_environments = Config.included_environments()
159158
environment_name = Config.environment_name()
160-
client = Config.client()
161159

162160
if environment_name in included_environments do
163-
client.send_event(event, opts)
161+
Sentry.Client.send_event(event, opts)
164162
else
165163
:ignored
166164
end

lib/sentry/client.ex

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,49 @@
11
defmodule Sentry.Client do
2-
@behaviour Sentry.HTTPClient
3-
# Max message length per https://github.com/getsentry/sentry/blob/0fcec33ac94ad81a205f86f208072b0f57b39ff4/src/sentry/conf/server.py#L1021
4-
@max_message_length 8_192
5-
62
@moduledoc ~S"""
7-
This module is the default client for sending an event to Sentry via HTTP.
3+
This module interfaces directly with Sentry via HTTP.
84
9-
It makes use of `Task.Supervisor` to allow sending tasks synchronously or asynchronously, and defaulting to asynchronous. See `Sentry.Client.send_event/2` for more information.
5+
The client itself can be configured via the :client
6+
configuration. It must implement the `Sentry.HTTPClient`
7+
behaviour and it defaults to `Sentry.HackneyClient`.
8+
9+
It makes use of `Task.Supervisor` to allow sending tasks
10+
synchronously or asynchronously, and defaulting to asynchronous.
11+
See `send_event/2` for more information.
1012
1113
### Configuration
1214
13-
* `:before_send_event` - allows performing operations on the event before
14-
it is sent. Accepts an anonymous function or a {module, function} tuple, and
15-
the event will be passed as the only argument.
15+
* `:before_send_event` - allows performing operations on the event before
16+
it is sent. Accepts an anonymous function or a `{module, function}` tuple,
17+
and the event will be passed as the only argument.
1618
17-
* `:after_send_event` - callback that is called after attempting to send an event.
18-
Accepts an anonymous function or a {module, function} tuple. The result of the HTTP call as well as the event will be passed as arguments.
19-
The return value of the callback is not returned.
19+
* `:after_send_event` - callback that is called after attempting to send an event.
20+
Accepts an anonymous function or a `{module, function}` tuple. The result of the
21+
HTTP call as well as the event will be passed as arguments. The return value of
22+
the callback is not returned.
2023
2124
Example configuration of putting Logger metadata in the extra context:
2225
2326
config :sentry,
24-
before_send_event: fn(event) ->
27+
before_send_event: {MyModule, :before_send},
28+
before_send_event: {MyModule, :after_send}
29+
30+
where:
31+
32+
defmodule MyModule do
33+
def before_send(event) do
2534
metadata = Map.new(Logger.metadata)
2635
%{event | extra: Map.merge(event.extra, metadata)}
27-
end,
36+
end
2837
29-
after_send_event: fn(event, result) ->
38+
def after_send_event(event, result) do
3039
case result do
3140
{:ok, id} ->
3241
Logger.info("Successfully sent event!")
3342
_ ->
3443
Logger.info(fn -> "Did not successfully send event! #{inspect(event)}" end)
3544
end
3645
end
46+
end
3747
"""
3848

3949
alias Sentry.{Config, Event, Util}
@@ -45,27 +55,39 @@ defmodule Sentry.Client do
4555
@type dsn :: {String.t(), String.t(), String.t()}
4656
@type result :: :sync | :none | :async
4757
@sentry_version 5
48-
@max_attempts 4
49-
@hackney_pool_name :sentry_pool
58+
@sentry_client "sentry-elixir/#{Mix.Project.config()[:version]}"
5059

51-
quote do
52-
unquote(@sentry_client "sentry-elixir/#{Mix.Project.config()[:version]}")
53-
end
60+
# Max message length per https://github.com/getsentry/sentry/blob/0fcec33ac94ad81a205f86f208072b0f57b39ff4/src/sentry/conf/server.py#L1021
61+
@max_message_length 8_192
5462

5563
@doc """
5664
Attempts to send the event to the Sentry API up to 4 times with exponential backoff.
5765
5866
The event is dropped if it all retries fail.
59-
Errors will be logged unless the source is the Sentry.LoggerBackend, which can deadlock by logging within a logger.
67+
Errors will be logged unless the source is the Sentry.LoggerBackend, which can
68+
deadlock by logging within a logger.
6069
6170
### Options
62-
* `:result` - Allows specifying how the result should be returned. Options include `:sync`, `:none`, and `:async`. `:sync` will make the API call synchronously, and return `{:ok, event_id}` if successful. `:none` sends the event from an unlinked child process under `Sentry.TaskSupervisor` and will return `{:ok, ""}` regardless of the result. `:async` will start an unlinked task and return a tuple of `{:ok, Task.t}` on success where the Task should be awaited upon to receive the result asynchronously. If you do not call `Task.await/2`, messages will be leaked to the inbox of the current process. See `Task.Supervisor.async_nolink/2` for more information. `:none` is the default.
63-
* `:sample_rate` - The sampling factor to apply to events. A value of 0.0 will deny sending any events, and a value of 1.0 will send 100% of events.
64-
* Other options, such as `:stacktrace` or `:extra` will be passed to `Sentry.Event.create_event/1` downstream. See `Sentry.Event.create_event/1` for available options.
71+
72+
* `:result` - Allows specifying how the result should be returned. Options include
73+
`:sync`, `:none`, and `:async`. `:sync` will make the API call synchronously, and
74+
return `{:ok, event_id}` if successful. `:none` sends the event from an unlinked
75+
child process under `Sentry.TaskSupervisor` and will return `{:ok, ""}` regardless
76+
of the result. `:async` will start an unlinked task and return a tuple of `{:ok, Task.t}`
77+
on success where the Task should be awaited upon to receive the result asynchronously.
78+
If you do not call `Task.await/2`, messages will be leaked to the inbox of the current
79+
process. See `Task.Supervisor.async_nolink/2` for more information. `:none` is the default.
80+
81+
* `:sample_rate` - The sampling factor to apply to events. A value of 0.0 will deny sending
82+
any events, and a value of 1.0 will send 100% of events.
83+
84+
* Other options, such as `:stacktrace` or `:extra` will be passed to `Sentry.Event.create_event/1`
85+
downstream. See `Sentry.Event.create_event/1` for available options.
86+
6587
"""
6688
@spec send_event(Event.t()) :: send_event_result
6789
def send_event(%Event{} = event, opts \\ []) do
68-
result = Keyword.get(opts, :result, :none)
90+
result = Keyword.get(opts, :result, Config.send_result())
6991
sample_rate = Keyword.get(opts, :sample_rate) || Config.sample_rate()
7092
should_log = event.event_source != :logger
7193

@@ -110,7 +132,7 @@ defmodule Sentry.Client do
110132
{endpoint, auth_headers} when is_binary(endpoint) ->
111133
{:ok,
112134
Task.Supervisor.async_nolink(Sentry.TaskSupervisor, fn ->
113-
try_request(endpoint, auth_headers, {event, body})
135+
try_request(endpoint, auth_headers, {event, body}, Config.send_max_attempts())
114136
|> maybe_call_after_send_event(event)
115137
end)}
116138

@@ -123,7 +145,7 @@ defmodule Sentry.Client do
123145
defp do_send_event(event, body, :sync) do
124146
case get_headers_and_endpoint() do
125147
{endpoint, auth_headers} when is_binary(endpoint) ->
126-
try_request(endpoint, auth_headers, {event, body})
148+
try_request(endpoint, auth_headers, {event, body}, Config.send_max_attempts())
127149
|> maybe_call_after_send_event(event)
128150

129151
{:error, :invalid_dsn} ->
@@ -137,7 +159,7 @@ defmodule Sentry.Client do
137159
case get_headers_and_endpoint() do
138160
{endpoint, auth_headers} when is_binary(endpoint) ->
139161
Task.Supervisor.start_child(Sentry.TaskSupervisor, fn ->
140-
try_request(endpoint, auth_headers, {event, body})
162+
try_request(endpoint, auth_headers, {event, body}, Config.send_max_attempts())
141163
|> maybe_call_after_send_event(event)
142164
end)
143165

@@ -152,47 +174,39 @@ defmodule Sentry.Client do
152174
String.t(),
153175
list({String.t(), String.t()}),
154176
{Event.t(), String.t()},
177+
pos_integer(),
155178
{pos_integer(), any()}
156179
) :: {:ok, String.t()} | {:error, {:request_failure, any()}}
157-
defp try_request(url, headers, event_body_tuple, current_attempt_and_error \\ {1, nil})
180+
defp try_request(url, headers, event_body_tuple, max_attempts, current \\ {1, nil})
158181

159-
defp try_request(_url, _headers, {_event, _body}, {current_attempt, last_error})
160-
when current_attempt > @max_attempts,
182+
defp try_request(_url, _headers, {_event, _body}, max_attempts, {current_attempt, last_error})
183+
when current_attempt > max_attempts,
161184
do: {:error, {:request_failure, last_error}}
162185

163-
defp try_request(url, headers, {event, body}, {current_attempt, _last_error}) do
186+
defp try_request(url, headers, {event, body}, max_attempts, {current_attempt, _last_error}) do
164187
case request(url, headers, body) do
165188
{:ok, id} ->
166189
{:ok, id}
167190

168191
{:error, error} ->
169-
if current_attempt < @max_attempts, do: sleep(current_attempt)
170-
171-
try_request(url, headers, {event, body}, {current_attempt + 1, error})
192+
if current_attempt < max_attempts, do: sleep(current_attempt)
193+
try_request(url, headers, {event, body}, max_attempts, {current_attempt + 1, error})
172194
end
173195
end
174196

175197
@doc """
176-
Makes the HTTP request to Sentry using hackney.
177-
178-
Hackney options can be set via the `hackney_opts` configuration option.
198+
Makes the HTTP request to Sentry using the configured HTTP client.
179199
"""
180200
@spec request(String.t(), list({String.t(), String.t()}), String.t()) ::
181201
{:ok, String.t()} | {:error, term()}
182202
def request(url, headers, body) do
183203
json_library = Config.json_library()
184204

185-
hackney_opts =
186-
Config.hackney_opts()
187-
|> Keyword.put_new(:pool, @hackney_pool_name)
188-
189-
with {:ok, 200, _, client} <- :hackney.request(:post, url, headers, body, hackney_opts),
190-
{:ok, body} <- :hackney.body(client),
205+
with {:ok, 200, _, body} <- Config.client().post(url, headers, body),
191206
{:ok, json} <- json_library.decode(body) do
192207
{:ok, Map.get(json, "id")}
193208
else
194-
{:ok, status, headers, client} ->
195-
:hackney.skip_body(client)
209+
{:ok, status, headers, _body} ->
196210
error_header = :proplists.get_value("X-Sentry-Error", headers, "")
197211
error = "Received #{status} from Sentry server: #{error_header}"
198212
{:error, error}
@@ -288,10 +302,6 @@ defmodule Sentry.Client do
288302
end
289303
end
290304

291-
def hackney_pool_name do
292-
@hackney_pool_name
293-
end
294-
295305
@doc """
296306
Transform the Event struct into JSON map.
297307

lib/sentry/config.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ defmodule Sentry.Config do
1313
@default_path_pattern "**/*.ex"
1414
@default_context_lines 3
1515
@default_sample_rate 1.0
16+
@default_send_result :none
17+
@default_send_max_attempts 4
1618

1719
@permitted_log_level_values ~w(debug info warn error)a
1820

@@ -68,7 +70,7 @@ defmodule Sentry.Config do
6870
end
6971

7072
def client do
71-
get_config(:client, default: Sentry.Client, check_dsn: false)
73+
get_config(:client, default: Sentry.HackneyClient, check_dsn: false)
7274
end
7375

7476
def enable_source_code_context do
@@ -105,6 +107,14 @@ defmodule Sentry.Config do
105107
get_config(:in_app_module_whitelist, default: [], check_dsn: false)
106108
end
107109

110+
def send_result do
111+
get_config(:send_result, default: @default_send_result, check_dsn: false)
112+
end
113+
114+
def send_max_attempts do
115+
get_config(:send_max_attempts, default: @default_send_max_attempts, check_dsn: false)
116+
end
117+
108118
def sample_rate do
109119
get_config(:sample_rate, default: @default_sample_rate, check_dsn: false)
110120
end

0 commit comments

Comments
 (0)