Packages
sentry
5.0.1
13.3.0
13.2.0
13.1.0
13.0.1
13.0.0
12.0.3
12.0.2
12.0.1
12.0.0
11.0.4
11.0.3
11.0.2
11.0.1
11.0.0
10.10.0
10.9.0
10.8.1
10.8.0
10.7.1
10.7.0
10.6.2
10.6.1
10.6.0
10.5.0
10.4.0
10.3.0
10.2.1
10.2.0
10.2.0-rc.2
10.2.0-rc.1
10.1.0
10.0.3
10.0.2
10.0.1
10.0.0
9.1.0
9.0.0
8.1.0
8.0.6
8.0.5
8.0.4
8.0.3
8.0.2
8.0.1
8.0.0
8.0.0-rc.2
8.0.0-rc.1
8.0.0-rc.0
retired
7.2.5
7.2.4
7.2.3
7.2.2
7.2.1
7.2.0
7.1.0
7.0.6
7.0.5
7.0.4
7.0.3
7.0.2
7.0.1
7.0.0
6.4.2
6.4.1
6.4.0
6.3.0
6.2.1
6.2.0
6.1.0
6.0.5
6.0.4
6.0.3
6.0.2
6.0.1
6.0.0
5.0.1
5.0.0
4.0.3
4.0.2
4.0.1
4.0.0
3.0.0
2.2.0
2.1.0
2.0.2
2.0.1
2.0.0
1.1.2
1.1.1
1.1.0
1.0.0
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
The Official Elixir client for Sentry
Current section
Files
Jump to
Current section
Files
lib/sentry/client.ex
defmodule Sentry.Client do
@behaviour Sentry.HTTPClient
@moduledoc ~S"""
This module is the default client for sending an event to Sentry via HTTP.
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.
### Configuration
* `:before_send_event` - allows performing operations on the event before
it is sent. Accepts an anonymous function or a {module, function} tuple, and
the event will be passed as the only argument.
* `:after_send_event` - callback that is called after attempting to send an event.
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.
The return value of the callback is not returned.
Example configuration of putting Logger metadata in the extra context:
config :sentry,
before_send_event: fn(event) ->
metadata = Map.new(Logger.metadata)
%{event | extra: Map.merge(event.extra, metadata)}
end,
after_send_event: fn(event, result) ->
case result do
{:ok, id} ->
Logger.info("Successfully sent event!")
_ ->
Logger.info(fn -> "Did not successfully send event! #{inspect(event)}" end)
end
end
"""
alias Sentry.{Event, Util, Config}
require Logger
@type get_dsn :: {String.t, String.t, Integer.t}
@sentry_version 5
@max_attempts 4
@hackney_pool_name :sentry_pool
quote do
unquote(@sentry_client "sentry-elixir/#{Mix.Project.config[:version]}")
end
@doc """
Attempts to send the event to the Sentry API up to 4 times with exponential backoff.
The event is dropped if it all retries fail.
### Options
* `: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 can be awaited upon to receive the result asynchronously. When used in an OTP behaviour like GenServer, the task will send a message that needs to be matched with `GenServer.handle_info/2`. See `Task.Supervisor.async_nolink/2` for more information. `:async` is the default.
* `: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.
"""
@spec send_event(Event.t) :: {:ok, Task.t | String.t} | :error | :unsampled
def send_event(%Event{} = event, opts \\ []) do
result = Keyword.get(opts, :result, :async)
sample_rate = Keyword.get(opts, :sample_rate) || Config.sample_rate()
event = maybe_call_before_send_event(event)
if sample_event?(sample_rate) do
encode_and_send(event, result)
else
:unsampled
end
end
defp encode_and_send(event, result) do
case Poison.encode(event) do
{:ok, body} ->
do_send_event(event, body, result)
{:error, error} ->
log_api_error("Unable to encode Sentry error - #{inspect(error)}")
:error
end
end
defp do_send_event(event, body, :async) do
{endpoint, public_key, secret_key} = get_dsn!()
auth_headers = authorization_headers(public_key, secret_key)
{:ok, Task.Supervisor.async_nolink(Sentry.TaskSupervisor, fn ->
try_request(:post, endpoint, auth_headers, body)
|> maybe_call_after_send_event(event)
end)}
end
defp do_send_event(event, body, :sync) do
{endpoint, public_key, secret_key} = get_dsn!()
auth_headers = authorization_headers(public_key, secret_key)
try_request(:post, endpoint, auth_headers, body)
|> maybe_call_after_send_event(event)
end
defp do_send_event(event, body, :none) do
{endpoint, public_key, secret_key} = get_dsn!()
auth_headers = authorization_headers(public_key, secret_key)
Task.Supervisor.start_child(Sentry.TaskSupervisor, fn ->
try_request(:post, endpoint, auth_headers, body)
|> maybe_call_after_send_event(event)
end)
{:ok, ""}
end
defp try_request(method, url, headers, body, current_attempt \\ 1)
defp try_request(_, _, _, _, current_attempt)
when current_attempt > @max_attempts, do: :error
defp try_request(method, url, headers, body, current_attempt) do
case request(method, url, headers, body) do
{:ok, id} -> {:ok, id}
_ ->
sleep(current_attempt)
try_request(method, url, headers, body, current_attempt + 1)
end
end
@doc """
Makes the HTTP request to Sentry using hackney.
Hackney options can be set via the `hackney_opts` configuration option.
"""
def request(method, url, headers, body) do
hackney_opts = Config.hackney_opts()
|> Keyword.put_new(:pool, @hackney_pool_name)
with {:ok, 200, _, client} <- :hackney.request(method, url, headers, body, hackney_opts),
{:ok, body} <- :hackney.body(client),
{:ok, json} <- Poison.decode(body) do
{:ok, Map.get(json, "id")}
else
{:ok, status, headers, client} ->
:hackney.skip_body(client)
error_header = :proplists.get_value("X-Sentry-Error", headers, "")
log_api_error("#{body}\nReceived #{status} from Sentry server: #{error_header}")
:error
_ ->
log_api_error(body)
:error
end
end
@doc """
Generates a Sentry API authorization header.
"""
@spec authorization_header(String.t, String.t) :: String.t
def authorization_header(public_key, secret_key) do
timestamp = Util.unix_timestamp()
data = [
sentry_version: @sentry_version,
sentry_client: @sentry_client,
sentry_timestamp: timestamp,
sentry_key: public_key,
sentry_secret: secret_key
]
query = data
|> Enum.map(fn {name, value} -> "#{name}=#{value}" end)
|> Enum.join(", ")
"Sentry " <> query
end
defp authorization_headers(public_key, secret_key) do
[
{"User-Agent", @sentry_client},
{"X-Sentry-Auth", authorization_header(public_key, secret_key)}
]
end
@doc """
Get a Sentry DSN which is simply a URI.
"""
@spec get_dsn! :: get_dsn
def get_dsn! do
# {PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}{PROJECT_ID}
%URI{userinfo: userinfo, host: host, port: port, path: path, scheme: protocol} = URI.parse(Config.dsn())
[public_key, secret_key] = String.split(userinfo, ":", parts: 2)
[_, binary_project_id] = String.split(path, "/")
project_id = String.to_integer(binary_project_id)
endpoint = "#{protocol}://#{host}:#{port}/api/#{project_id}/store/"
{endpoint, public_key, secret_key}
end
def maybe_call_after_send_event(result, event) do
case Config.after_send_event() do
function when is_function(function, 2) ->
function.(event, result)
{module, function} ->
apply(module, function, [event, result])
nil ->
nil
_ ->
raise ArgumentError, message: ":after_send_event must be an anonymous function or a {Module, Function} tuple"
end
result
end
def maybe_call_before_send_event(event) do
case Config.before_send_event do
function when is_function(function, 1) ->
function.(event)
{module, function} ->
apply(module, function, [event])
nil ->
event
_ ->
raise ArgumentError, message: ":before_send_event must be an anonymous function or a {Module, Function} tuple"
end
end
def hackney_pool_name do
@hackney_pool_name
end
defp log_api_error(body) do
Logger.warn(fn ->
["Failed to send Sentry event.", ?\n, body]
end)
end
defp sleep(attempt_number) do
# sleep 2^n seconds
:math.pow(2, attempt_number)
|> Kernel.*(1000)
|> Kernel.round()
|> :timer.sleep()
end
defp sample_event?(1), do: true
defp sample_event?(1.0), do: true
defp sample_event?(0), do: false
defp sample_event?(0.0), do: false
defp sample_event?(sample_rate) do
:rand.uniform < sample_rate
end
end