Packages
sentry
2.1.0
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
alias Sentry.{Event, Util}
require Logger
@type parsed_dsn :: {String.t, String.t, Integer.t}
@sentry_version 5
@max_attempts 4
quote do
unquote(@sentry_client "sentry-elixir/#{Mix.Project.config[:version]}")
end
@moduledoc """
Provides basic HTTP client request and response handling for the Sentry API.
"""
@doc """
Starts an unlinked asynchronous task that will attempt to send the event to the Sentry
API up to 4 times with exponential backoff.
The event is dropped if it all retries fail.
"""
@spec send_event(%Event{}) :: {:ok, String.t} | :error
def send_event(%Event{} = event) do
{endpoint, public_key, secret_key} = parse_dsn!(Application.fetch_env!(:sentry, :dsn))
auth_headers = authorization_headers(public_key, secret_key)
body = Poison.encode!(event)
Task.Supervisor.async_nolink(Sentry.TaskSupervisor, fn ->
try_request(:post, endpoint, auth_headers, body)
end)
end
defp try_request(method, url, headers, body) do
do_try_request(method, url, headers, body, 1)
end
defp do_try_request(_method, _url, _headers, _body, current_attempt) when current_attempt > @max_attempts do
:error
end
defp do_try_request(method, url, headers, body, current_attempt) when current_attempt <= @max_attempts do
case request(method, url, headers, body) do
{:ok, id} -> {:ok, id}
_ ->
sleep(current_attempt)
do_try_request(method, url, headers, body, current_attempt + 1)
end
end
def request(method, url, headers, body) do
case :hackney.request(method, url, headers, body, []) do
{:ok, 200, _headers, client} ->
case :hackney.body(client) do
{:ok, body} ->
id = body
|> Poison.decode!()
|> Map.get("id")
{:ok, id}
_ ->
log_api_error(body)
:error
end
{:ok, status, headers, _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()
"Sentry sentry_version=#{@sentry_version}, sentry_client=#{@sentry_client}, sentry_timestamp=#{timestamp}, sentry_key=#{public_key}, sentry_secret=#{secret_key}"
end
def authorization_headers(public_key, secret_key) do
[
{"User-Agent", @sentry_client},
{"X-Sentry-Auth", authorization_header(public_key, secret_key)}
]
end
@doc """
Parses a Sentry DSN which is simply a URI.
"""
@spec parse_dsn!(String.t) :: parsed_dsn
def parse_dsn!(dsn) do
# {PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}{PROJECT_ID}
%URI{userinfo: userinfo, host: host, port: port, path: path, scheme: protocol} = URI.parse(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
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(attempt_number, 2)
|> Kernel.*(1000)
|> Kernel.round()
|> :timer.sleep()
end
end