Sentry: use a Req-based HTTP client (hackney returns empty body)
dda7dbe · gmorell · 2026-06-26 20:21
Message
{commit_body(@commit)}
Files changed
modified
config/config.exs
+3
−0
@@ -252,6 +252,9 @@ config :phoenix, :json_library, Jason
| 252 | 252 | # runtime.exs; when unset (dev/test) Sentry no-ops. Source context lets |
| 253 | 253 | # stacktraces show the offending lines. |
| 254 | 254 | config :sentry, |
| 255 | + # Req-backed client; the default hackney one gets an empty response | |
| 256 | + # body from our GlitchTip and reports false send failures. | |
| 257 | + client: GitGud.SentryClient, | |
| 255 | 258 | environment_name: config_env(), |
| 256 | 259 | enable_source_code_context: true, |
| 257 | 260 | root_source_code_paths: [File.cwd!()] |
added
lib/git_gud/sentry_client.ex
+48
−0
@@ -0,0 +1,48 @@
| 1 | +defmodule GitGud.SentryClient do | |
| 2 | + @moduledoc """ | |
| 3 | + Sentry HTTP client backed by Req (already a dependency) instead of the | |
| 4 | + default hackney client. | |
| 5 | + | |
| 6 | + Against our self-hosted GlitchTip (s.gmp.io), hackney's | |
| 7 | + `:with_body` request returns an empty response body even though the | |
| 8 | + server replies `{"id": "..."}` (verified with curl over HTTP/1.1, /2, | |
| 9 | + and gzipped). The event still ingests, but the SDK can't read the | |
| 10 | + confirmation and reports a false `request_failure`, which also triggers | |
| 11 | + pointless retries. Req reads the body correctly. | |
| 12 | + """ | |
| 13 | + @behaviour Sentry.HTTPClient | |
| 14 | + | |
| 15 | + @finch GitGud.SentryFinch | |
| 16 | + | |
| 17 | + @impl true | |
| 18 | + def child_spec do | |
| 19 | + {Finch, name: @finch} | |
| 20 | + end | |
| 21 | + | |
| 22 | + @impl true | |
| 23 | + def post(url, headers, body) do | |
| 24 | + case Req.post(url, | |
| 25 | + headers: headers, | |
| 26 | + body: body, | |
| 27 | + finch: @finch, | |
| 28 | + # Sentry already gzipped the body + set Content-Encoding, and | |
| 29 | + # parses the response itself — don't let Req re-compress or | |
| 30 | + # decode, and let Sentry own retries. | |
| 31 | + compress_body: false, | |
| 32 | + decode_body: false, | |
| 33 | + retry: false | |
| 34 | + ) do | |
| 35 | + {:ok, %Req.Response{status: status, headers: resp_headers, body: resp_body}} -> | |
| 36 | + {:ok, status, flatten_headers(resp_headers), resp_body} | |
| 37 | + | |
| 38 | + {:error, exception} -> | |
| 39 | + {:error, exception} | |
| 40 | + end | |
| 41 | + end | |
| 42 | + | |
| 43 | + # Sentry.HTTPClient wants headers as a list of {key, value}; Req returns | |
| 44 | + # a map of key => [values]. | |
| 45 | + defp flatten_headers(headers) do | |
| 46 | + Enum.flat_map(headers, fn {k, vs} -> Enum.map(List.wrap(vs), &{k, &1}) end) | |
| 47 | + end | |
| 48 | +end |
Parents: a4b1452