A few weeks ago I added enabled support for Sentry inside Tune, my Spotify browser/client. Even if I'm pretty much the only user (I built it for myself after all), having exception tracking has already proved to be useful - band and song names can really create all sorts of issues.

The official Sentry package works as advertised and by default it communicates using Hackney as a http client. As I've been using Finch in the project, I was pleased to see that Sentry exposed a client configuration option that allowed using your own module, as long as it implemented the Sentry.HTTPClient behaviour.

The advantage in swapping the http client library (on top of uniforming the building blocks of the application) is that Finch has support for Telemetry metrics.

Update #1: Thanks to Wojtek Mach for a more streamlined implementation.

The module I wrote is quite short:

defmodule Sentry.FinchClient do
  @moduledoc """
  Defines a small shim to use `Finch` as a `Sentry.HTTPClient`.
  """

  @behaviour Sentry.HTTPClient

  @impl true
  def child_spec do
    Finch.child_spec(name: Sentry.Finch)
  end

  @impl true
  def post(url, headers, body) do
    request = Finch.build(:post, url, headers, body)

    case Finch.request(request, Sentry.Finch) do
      {:ok, response} ->
        {:ok, response.status, response.headers, response.body}

      error ->
        error
    end
  end
end

The trickiest bit was to get the child_spec/0 callback right while keeping dialyzer happy. The first implementation I wrote was simply {Finch, name: Sentry.Finch}, but that would fail to satisfy the typespec defined for child_spec/0. I then switched to a more verbose version:

  def child_spec do
    opts = [name: Sentry.Finch]

    Supervisor.child_spec(
      %{
        id: __MODULE__,
        start: {Finch, :start_link, [opts]},
        type: :supervisor
      },
      []
    )
  end

This version satisfied dialyzer, but turns out there's a simpler way. After publishing this blog post, Wojtek Mach reached out and submitted a PR to streamline the specification to the version shown in the full example above.

I also updated my production configuration:

config :sentry,
  dsn: {:system, "SENTRY_DSN"},
  environment_name: :prod,
  enable_source_code_context: true,
  root_source_code_path: File.cwd!(),
  client: Sentry.FinchClient,
  included_environments: [:prod]

You can of course expand on this implementation if you need to pass more options to the Finch child specification - I found that for my use case, defaults are fine, so for now I don't need to add any configuration hooks.

To see the change in context, this is the original PR, with the follow-up by Wojtek Mach.