Sentry: use a Req-based HTTP client (hackney returns empty body)

dda7dbe · gmorell · 2026-06-26 20:21

2 files +51 -0
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