Skip to content

Commit 5c1a28d

Browse files
Merge pull request #177 from getsentry/app-frames
App frames
2 parents 21153ae + 15b0e50 commit 5c1a28d

File tree

8 files changed

+97
-13
lines changed

8 files changed

+97
-13
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ config :sentry,
7474
| `before_send_event` | False | | |
7575
| `after_send_event` | False | | |
7676
| `sample_rate` | False | 1.0 | |
77+
| `in_app_module_whitelist` | False | `[]` | |
7778
| `enable_source_code_context` | True | | |
7879
| `root_source_code_path` | Required if `enable_source_code_context` is enabled | | Should generally be set to `File.cwd!`|
7980
| `context_lines` | False | 3 | |
@@ -92,7 +93,8 @@ config :sentry,
9293
tags: %{
9394
env: "production"
9495
},
95-
hackney_opts: [pool: :my_pool]
96+
hackney_opts: [pool: :my_pool],
97+
in_app_module_whitelist: [MyApp]
9698
```
9799

98100
The `environment_name` and `included_environments` work together to determine

docs/config.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ Optional settings
9393

9494
The sampling factor to apply to events. A value of 0.0 will deny sending any events, and a value of 1.0 will send 100% of events.
9595

96+
.. describe:: in_app_module_whitelist
97+
98+
Expects a list of modules that is used to distinguish among stacktrace frames that belong to your app and ones that are part of libraries or core Elixir. This is used to better display the significant part of stacktraces. The logic is greedy, so if your app's root module is ``MyApp`` and your setting is ``[MyApp]``, that module as well as any submodules like ``MyApp.Submodule`` would be considered part of your app. Defaults to ``[]``.
99+
96100
.. describe:: context_lines
97101

98102
The number of lines of source code before and after the line that caused the exception to be included. Defaults to ``3``.

lib/sentry/event.ex

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ defmodule Sentry.Event do
22
@moduledoc """
33
Provides an Event Struct as well as transformation of Logger
44
entries into Sentry Events.
5+
6+
7+
### Configuration
8+
9+
* `:in_app_module_whitelist` - Expects a list of modules that is used to distinguish among stacktrace frames that belong to your app and ones that are part of libraries or core Elixir. This is used to better display the significant part of stacktraces. The logic is greedy, so if your app's root module is `MyApp` and your setting is `[MyApp]`, that module as well as any submodules like `MyApp.Submodule` would be considered part of your app. Defaults to `[]`.
10+
511
"""
612

713
defstruct event_id: nil,
@@ -154,6 +160,7 @@ defmodule Sentry.Event do
154160

155161
@spec stacktrace_to_frames(Exception.stacktrace) :: [map]
156162
def stacktrace_to_frames(stacktrace) do
163+
in_app_module_whitelist = Application.get_env(:sentry, :in_app_module_whitelist, [])
157164
stacktrace
158165
|> Enum.map(fn(line) ->
159166
{mod, function, arity, location} = line
@@ -167,6 +174,7 @@ defmodule Sentry.Event do
167174
function: Exception.format_mfa(mod, function, arity),
168175
module: mod,
169176
lineno: line_number,
177+
in_app: is_in_app?(mod, in_app_module_whitelist),
170178
}
171179
|> put_source_context(file, line_number)
172180
end)
@@ -191,4 +199,23 @@ defmodule Sentry.Event do
191199

192200
defp arity_to_integer(arity) when is_list(arity), do: Enum.count(arity)
193201
defp arity_to_integer(arity) when is_integer(arity), do: arity
202+
203+
defp is_in_app?(nil, _in_app_whitelist), do: false
204+
defp is_in_app?(_, []), do: false
205+
defp is_in_app?(module, in_app_module_whitelist) do
206+
split_modules = module_split(module)
207+
208+
Enum.any?(in_app_module_whitelist, fn(module) ->
209+
whitelisted_split_modules = module_split(module)
210+
211+
count = Enum.count(whitelisted_split_modules)
212+
Enum.take(split_modules, count) == whitelisted_split_modules
213+
end)
214+
end
215+
216+
defp module_split(module) when is_binary(module) do
217+
String.split(module, ".")
218+
|> Enum.reject(&(&1 == "Elixir"))
219+
end
220+
defp module_split(module), do: module_split(String.Chars.to_string(module))
194221
end

mix.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ defmodule Sentry.Mixfile do
3030
{:plug, "~> 1.0", optional: true},
3131

3232
{:dialyxir, "> 0.0.0", only: :dev},
33-
{:ex_doc, "~> 0.15.0", only: :dev},
33+
{:ex_doc, "~> 0.16.0", only: :dev},
3434
{:credo, "~> 0.8", only: [:dev, :test], runtime: false},
35-
{:bypass, "~> 0.6.0", only: [:test]}
35+
{:bypass, "~> 0.7.0", only: [:test]}
3636
]
3737
end
3838

mix.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
2-
"bypass": {:hex, :bypass, "0.6.0", "fd0a8004fada4464e2ba98497755310b892a097f2fd975f4f787cf264066a335", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]},
2+
"bypass": {:hex, :bypass, "0.7.0", "591d2ac1d87ba1bb84f62656a79de2fe3909993a211021a881825b59c9f7f58f", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]},
33
"certifi": {:hex, :certifi, "1.1.0", "c9b71a547016c2528a590ccfc28de786c7edb74aafa17446b84f54e04efc00ee", [:rebar3], []},
44
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]},
55
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
66
"credo": {:hex, :credo, "0.8.1", "137efcc99b4bc507c958ba9b5dff70149e971250813cbe7d4537ec7e36997402", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]},
77
"dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], []},
8-
"earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []},
9-
"ex_doc": {:hex, :ex_doc, "0.15.1", "d5f9d588fd802152516fccfdb96d6073753f77314fcfee892b15b6724ca0d596", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]},
8+
"earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], []},
9+
"ex_doc": {:hex, :ex_doc, "0.16.1", "b4b8a23602b4ce0e9a5a960a81260d1f7b29635b9652c67e95b0c2f7ccee5e81", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]},
1010
"hackney": {:hex, :hackney, "1.8.0", "8388a22f4e7eb04d171f2cf0285b217410f266d6c13a4c397a6c22ab823a486c", [:rebar3], [{:certifi, "1.1.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]},
1111
"idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []},
1212
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},

test/event_test.exs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ defmodule Sentry.EventTest do
2626
assert event.message == "(UndefinedFunctionError) function Sentry.Event.not_a_function/0 is undefined or private"
2727
assert is_binary(event.server_name)
2828
assert event.stacktrace == %{frames: Enum.reverse([
29-
%{filename: nil, function: "Sentry.Event.not_a_function/0", lineno: nil, module: Sentry.Event, context_line: nil, post_context: [], pre_context: []},
30-
%{filename: "test/event_test.exs", function: "Sentry.EventTest.event_generated_by_exception/1", lineno: 8, module: Sentry.EventTest, context_line: nil, post_context: [], pre_context: []},
31-
%{filename: "test/event_test.exs", function: "Sentry.EventTest.\"test parses error exception\"/1", lineno: 15, module: Sentry.EventTest, context_line: nil, post_context: [], pre_context: []},
32-
%{filename: "lib/ex_unit/runner.ex", function: "ExUnit.Runner.exec_test/1", lineno: 302, module: ExUnit.Runner, context_line: nil, post_context: [], pre_context: []},
33-
%{filename: "timer.erl", function: ":timer.tc/1", lineno: 166, module: :timer, context_line: nil, post_context: [], pre_context: []},
34-
%{filename: "lib/ex_unit/runner.ex", function: "anonymous fn/3 in ExUnit.Runner.spawn_test/3", lineno: 250, module: ExUnit.Runner, context_line: nil, post_context: [], pre_context: []}])
29+
%{filename: nil, function: "Sentry.Event.not_a_function/0", lineno: nil, module: Sentry.Event, context_line: nil, post_context: [], pre_context: [], in_app: false},
30+
%{filename: "test/event_test.exs", function: "Sentry.EventTest.event_generated_by_exception/1", lineno: 8, module: Sentry.EventTest, context_line: nil, post_context: [], pre_context: [], in_app: false},
31+
%{filename: "test/event_test.exs", function: "Sentry.EventTest.\"test parses error exception\"/1", lineno: 15, module: Sentry.EventTest, context_line: nil, post_context: [], pre_context: [], in_app: false},
32+
%{filename: "lib/ex_unit/runner.ex", function: "ExUnit.Runner.exec_test/1", lineno: 302, module: ExUnit.Runner, context_line: nil, post_context: [], pre_context: [], in_app: false},
33+
%{filename: "timer.erl", function: ":timer.tc/1", lineno: 166, module: :timer, context_line: nil, post_context: [], pre_context: [], in_app: false},
34+
%{filename: "lib/ex_unit/runner.ex", function: "anonymous fn/3 in ExUnit.Runner.spawn_test/3", lineno: 250, module: ExUnit.Runner, context_line: nil, post_context: [], pre_context: [], in_app: false}])
3535
}
3636
assert event.tags == %{}
3737
assert event.timestamp =~ ~r/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
@@ -70,9 +70,58 @@ defmodule Sentry.EventTest do
7070
event = Sentry.Event.transform_exception(exception, [fingerprint: ["hello", "world"]])
7171
assert event.fingerprint == ["hello", "world"]
7272
end
73+
7374
test "not sending fingerprint when unset" do
7475
exception = RuntimeError.exception("error")
7576
event = Sentry.Event.transform_exception(exception, [])
7677
assert event.fingerprint == ["{{ default }}"]
7778
end
79+
80+
test "sets app_frame to true when configured" do
81+
modify_env(:sentry, in_app_module_whitelist: [Sentry, :random, Sentry.Submodule])
82+
exception = RuntimeError.exception("error")
83+
event = Sentry.Event.transform_exception(exception, [stacktrace: [{Elixir.Sentry.Fun, :method, 2, []}, {Elixir.Sentry, :other_method, 4, []},
84+
{:other_module, :a_method, 8, []}, {:random, :uniform, 0, []},
85+
{Sentry.Submodule.Fun, :this_method, 0, []}]])
86+
assert %{frames: [
87+
%{
88+
module: Sentry.Submodule.Fun,
89+
function: "Sentry.Submodule.Fun.this_method/0",
90+
in_app: true,
91+
filename: nil, lineno: nil,
92+
context_line: nil, post_context: [], pre_context: []
93+
},
94+
%{
95+
module: :random,
96+
function: ":random.uniform/0",
97+
in_app: true,
98+
filename: nil, lineno: nil,
99+
context_line: nil, post_context: [], pre_context: []
100+
},
101+
%{
102+
module: :other_module,
103+
function: ":other_module.a_method/8",
104+
in_app: false,
105+
filename: nil, lineno: nil,
106+
context_line: nil, post_context: [], pre_context: []
107+
},
108+
%{
109+
module: Sentry,
110+
function: "Sentry.other_method/4",
111+
in_app: true,
112+
filename: nil, lineno: nil,
113+
context_line: nil, post_context: [], pre_context: []
114+
},
115+
%{
116+
filename: nil,
117+
function: "Sentry.Fun.method/2",
118+
module: Sentry.Fun,
119+
lineno: nil,
120+
in_app: true,
121+
context_line: nil,
122+
post_context: [],
123+
pre_context: []
124+
},
125+
]} == event.stacktrace
126+
end
78127
end

test/logger_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ defmodule Sentry.LoggerTest do
8888
assert List.first(json["exception"])["value"] == "** (exit) :function_clause"
8989
assert List.last(json["stacktrace"]["frames"]) == %{"filename" => "lib/calendar.ex",
9090
"function" => "NaiveDateTime.from_erl/2",
91+
"in_app" => false,
9192
"lineno" => 1214,
9293
"module" => "Elixir.NaiveDateTime",
9394
"context_line" => nil,

test/sentry_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ defmodule SentryTest do
2828
assert conn.method == "POST"
2929
Plug.Conn.send_resp(conn, 200, ~s<{"id": "340"}>)
3030
end
31-
Bypass.pass(bypass)
3231

3332
modify_env(:sentry, filter: Sentry.TestFilter, dsn: "http://public:secret@localhost:#{bypass.port}/1")
3433

3534
assert capture_log(fn ->
3635
assert :error = Sentry.capture_message("error", [])
3736
end) =~ "Failed to send Sentry event"
37+
38+
Bypass.pass(bypass)
3839
end
3940
end

0 commit comments

Comments
 (0)