Skip to content

Commit 6767cec

Browse files
Merge pull request #385 from getsentry/feedback
allow gathering feedback for sentry errors
2 parents 6952fec + e2d1937 commit 6767cec

File tree

2 files changed

+99
-7
lines changed

2 files changed

+99
-7
lines changed

lib/sentry/plug.ex

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ if Code.ensure_loaded?(Plug) do
9797
which Plug.RequestId (and therefore Phoenix) also default to.
9898
9999
use Sentry.Plug, request_id_header: "application-request-id"
100+
101+
### Collect User Feedback after Error
102+
103+
Sentry allows collecting user feedback after they hit an error.
104+
Information about it is available [here](https://docs.sentry.io/enriching-error-data/user-feedback).
105+
If a Plug request experiences an error that Sentry is reporting, and the request
106+
accepts the content-type "text/html" or "*/*", the feedback form will be rendered.
107+
The configuration is limited to the defaults at the moment, but it can be enabled with:
108+
109+
use Sentry.Plug, collect_feedback: [enabled: true]
100110
"""
101111

102112
@default_plug_request_id_header "x-request-id"
@@ -108,6 +118,8 @@ if Code.ensure_loaded?(Plug) do
108118
cookie_scrubber = Keyword.get(env, :cookie_scrubber, {__MODULE__, :default_cookie_scrubber})
109119

110120
request_id_header = Keyword.get(env, :request_id_header)
121+
collect_feedback = Keyword.get(env, :collect_feedback, [])
122+
collect_feedback_enabled = Keyword.get(collect_feedback, :enabled, false)
111123

112124
quote do
113125
# Ignore 404s for Plug routes
@@ -130,18 +142,63 @@ if Code.ensure_loaded?(Plug) do
130142
request_id_header: unquote(request_id_header)
131143
]
132144

145+
collect_feedback_enabled = unquote(collect_feedback_enabled)
133146
request = Sentry.Plug.build_request_interface_data(conn, opts)
134147
exception = Exception.normalize(kind, reason, stack)
135148

136-
Sentry.capture_exception(
137-
exception,
138-
stacktrace: stack,
139-
request: request,
140-
event_source: :plug,
141-
error_type: kind
142-
)
149+
accept_html =
150+
Plug.Conn.get_req_header(conn, "accept")
151+
|> Enum.any?(fn header ->
152+
String.split(header, ",")
153+
|> Enum.any?(&(&1 == "text/html" || &1 == "*/*"))
154+
end)
155+
156+
if accept_html && collect_feedback_enabled do
157+
result =
158+
Sentry.capture_exception(
159+
exception,
160+
stacktrace: stack,
161+
request: request,
162+
event_source: :plug,
163+
error_type: kind,
164+
result: :sync
165+
)
166+
167+
render_sentry_feedback(conn, result)
168+
else
169+
Sentry.capture_exception(
170+
exception,
171+
stacktrace: stack,
172+
request: request,
173+
event_source: :plug,
174+
error_type: kind
175+
)
176+
end
143177
end
144178

179+
defp render_sentry_feedback(conn, {:ok, id}) do
180+
html = """
181+
<!DOCTYPE HTML>
182+
<html lang="en">
183+
<head>
184+
<meta charset="utf-8">
185+
<script src="https://browser.sentry-cdn.com/5.9.1/bundle.min.js" integrity="sha384-/x1aHz0nKRd6zVUazsV6CbQvjJvr6zQL2CHbQZf3yoLkezyEtZUpqUNnOLW9Nt3v" crossorigin="anonymous"></script>
186+
<script>
187+
Sentry.init({ dsn: '#{Sentry.Config.dsn()}' });
188+
Sentry.showReportDialog({ eventId: '#{id}' })
189+
</script>
190+
</head>
191+
<body>
192+
</body>
193+
</html>
194+
"""
195+
196+
Plug.Conn.put_resp_header(conn, "content-type", "text/html")
197+
|> Plug.Conn.send_resp(conn.status, html)
198+
end
199+
200+
defp render_sentry_feedback(_conn, _result), do: nil
201+
145202
defoverridable handle_errors: 2
146203
end
147204
end

test/plug_test.exs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,41 @@ defmodule Sentry.PlugTest do
162162
end)
163163
end
164164

165+
test "collects feedback" do
166+
Code.compile_string("""
167+
defmodule CollectFeedbackApp do
168+
use Plug.Router
169+
use Plug.ErrorHandler
170+
use Sentry.Plug, collect_feedback: [enabled: true]
171+
plug :match
172+
plug :dispatch
173+
forward("/", to: Sentry.ExampleApp)
174+
end
175+
""")
176+
177+
bypass = Bypass.open()
178+
179+
Bypass.expect(bypass, fn conn ->
180+
{:ok, _body, _conn} = Plug.Conn.read_body(conn)
181+
Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>)
182+
end)
183+
184+
modify_env(:sentry, dsn: "http://public:secret@localhost:#{bypass.port}/1")
185+
186+
conn =
187+
conn(:get, "/error_route")
188+
|> Plug.Conn.put_req_header("accept", "text/html")
189+
190+
assert_raise(Plug.Conn.WrapperError, "** (RuntimeError) Error", fn ->
191+
CollectFeedbackApp.call(conn, [])
192+
end)
193+
194+
assert_received {:plug_conn, :sent}
195+
assert {500, _headers, body} = sent_resp(conn)
196+
assert body =~ "340"
197+
assert body =~ "sentry-cdn"
198+
end
199+
165200
defp update_req_cookie(conn, name, value) do
166201
req_headers =
167202
conn.req_headers

0 commit comments

Comments
 (0)