defmodule Sentry do use Application alias Sentry.{Config, Event} require Logger @moduledoc """ Provides the basic functionality to submit a `Sentry.Event` to the Sentry Service. ## Configuration Add the following to your production config config :sentry, dsn: "https://public:secret@app.getsentry.com/1", included_environments: [:prod], environment_name: :prod, tags: %{ env: "production" } The `environment_name` and `included_environments` work together to determine if and when Sentry should record exceptions. The `environment_name` is the name of the current environment. In the example above, we have explicitly set the environment to `:prod` which works well if you are inside an environment specific configuration `config/prod.exs`. An alternative is to use `Mix.env` in your general configuration file: config :sentry, dsn: "https://public:secret@app.getsentry.com/1", included_environments: [:prod], environment_name: Mix.env This will set the environment name to whatever the current Mix environment atom is, but it will only send events if the current environment is `:prod`, since that is the only entry in the `included_environments` key. You can even rely on more custom determinations of the environment name. It's not uncommmon for most applications to have a "staging" environment. In order to handle this without adding an additional Mix environment, you can set an environment variable that determines the release level. config :sentry, dsn: "https://public:secret@app.getsentry.com/1", included_environments: ~w(production staging), environment_name: System.get_env("RELEASE_LEVEL") || "development" In this example, we are getting the environment name from the `RELEASE_LEVEL` environment variable. If that variable does not exist, we default to `"development"`. Now, on our servers, we can set the environment variable appropriately. On our local development machines, exceptions will never be sent, because the default value is not in the list of `included_environments`. ## Filtering Exceptions If you would like to prevent certain exceptions, the `:filter` configuration option allows you to implement the `Sentry.EventFilter` behaviour. The first argument is the exception to be sent, and the second is the source of the event. `Sentry.Plug` will have a source of `:plug`, `Sentry.LoggerBackend` will have a source of `:logger`, and `Sentry.Phoenix.Endpoint` will have a source of `:endpoint`. If an exception does not come from either of those sources, the source will be nil unless the `:event_source` option is passed to `Sentry.capture_exception/2` A configuration like below will prevent sending `Phoenix.Router.NoRouteError` from `Sentry.Plug`, but allows other exceptions to be sent. # sentry_event_filter.ex defmodule MyApp.SentryEventFilter do @behaviour Sentry.EventFilter def exclude_exception?(%Elixir.Phoenix.Router.NoRouteError{}, :plug), do: true def exclude_exception?(_exception, _source), do: false end # config.exs config :sentry, filter: MyApp.SentryEventFilter, included_environments: ~w(production staging), environment_name: System.get_env("RELEASE_LEVEL") || "development" ## Capturing Exceptions 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. {:ok, task} = Sentry.capture_exception(my_exception) {:ok, event_id} = Task.await(task) {:ok, another_event_id} = Sentry.capture_exception(other_exception, [event_source: :my_source, result: :sync]) ### Options * `:event_source` - The source passed as the first argument to `c:Sentry.EventFilter.exclude_exception?/2` ## Configuring The `Logger` Backend See `Sentry.LoggerBackend` """ @type send_result :: Sentry.Client.send_event_result() | :excluded | :ignored def start(_type, _opts) do children = [ {Task.Supervisor, name: Sentry.TaskSupervisor}, Config.client().child_spec() ] if Config.client() == Sentry.HackneyClient do unless Code.ensure_loaded?(:hackney) do raise """ cannot start the :sentry application because the HTTP client is set to \ Sentry.HackneyClient (which is the default), but the Hackney library is not loaded. \ Add :hackney to your dependencies to fix this. """ end case Application.ensure_all_started(:hackney) do {:ok, _apps} -> :ok {:error, reason} -> raise "failed to start the :hackney application: #{inspect(reason)}" end end validate_json_config!() validate_log_level_config!() opts = [strategy: :one_for_one, name: Sentry.Supervisor] Supervisor.start_link(children, opts) end @doc """ Parses and submits an exception to Sentry if current environment is in included_environments. `opts` argument is passed as the second argument to `Sentry.send_event/2`. """ @spec capture_exception(Exception.t(), Keyword.t()) :: send_result def capture_exception(exception, opts \\ []) do filter_module = Config.filter() event_source = Keyword.get(opts, :event_source) if filter_module.exclude_exception?(exception, event_source) do :excluded else exception |> Event.transform_exception(opts) |> send_event(opts) end end @doc """ Puts the last event ID sent to the server for the current process in the process dictionary. """ @spec put_last_event_id_and_source(String.t()) :: {String.t(), atom() | nil} | nil def put_last_event_id_and_source(event_id, source \\ nil) when is_binary(event_id) do Process.put(:sentry_last_event_id_and_source, {event_id, source}) end @doc """ Gets the last event ID sent to the server from the process dictionary. Since it uses the process dictionary, it will only return the last event ID sent within the current process. """ @spec get_last_event_id_and_source() :: {String.t(), atom() | nil} | nil def get_last_event_id_and_source do Process.get(:sentry_last_event_id_and_source) end @doc """ Reports a message to Sentry. `opts` argument is passed as the second argument to `Sentry.send_event/2`. """ @spec capture_message(String.t(), Keyword.t()) :: send_result def capture_message(message, opts \\ []) when is_binary(message) do opts |> Keyword.put(:message, message) |> Event.create_event() |> send_event(opts) end @doc """ Sends a `Sentry.Event` `opts` argument is passed as the second argument to `send_event/2` of the configured `Sentry.HTTPClient`. See `Sentry.Client.send_event/2` for more information. """ @spec send_event(Event.t(), Keyword.t()) :: send_result def send_event(event, opts \\ []) def send_event(%Event{message: nil, exception: nil}, _opts) do Logger.log(Config.log_level(), "Sentry: unable to parse exception") :ignored end def send_event(%Event{} = event, opts) do included_environments = Config.included_environments() environment_name = Config.environment_name() if environment_name in included_environments do Sentry.Client.send_event(event, opts) else :ignored end end defp validate_json_config!() do case Config.json_library() do nil -> raise ArgumentError.exception("nil is not a valid :json_library configuration") library -> try do with {:ok, %{}} <- library.decode("{}"), {:ok, "{}"} <- library.encode(%{}) do :ok else _ -> raise ArgumentError.exception( "configured :json_library #{inspect(library)} does not implement decode/1 and encode/1" ) end rescue UndefinedFunctionError -> reraise ArgumentError.exception(""" configured :json_library #{inspect(library)} is not available or does not implement decode/1 and encode/1. Do you need to add #{inspect(library)} to your mix.exs? """), __STACKTRACE__ end end end defp validate_log_level_config!() do value = Config.log_level() if value in Config.permitted_log_level_values() do :ok else raise ArgumentError.exception("#{inspect(value)} is not a valid :log_level configuration") end end end