Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/dashing-baron-otso.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
hex/posthog: minor
---

Improve Error Tracking for complex errors. If an error has a `crash_reason`, which is common for OTP reports, the SDK will report it as a chain of two exceptions. Additionally, some valuable information, such as process label, genserver state or last message, will be extracted from the report and put into event properties.
228 changes: 209 additions & 19 deletions lib/posthog/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,35 +40,84 @@ defmodule PostHog.Handler do
end

defp properties(log_event, config) do
exception =
Enum.reduce(
[&type/1, &value/1, &stacktrace(&1, config.in_app_modules)],
%{
mechanism: %{handled: not Map.has_key?(log_event.meta, :crash_reason), type: "generic"}
},
fn fun, acc ->
Map.merge(acc, fun.(log_event))
end
)
exceptions = exceptions(log_event, config)

metadata =
log_event.meta
|> then(fn metadata ->
if config.metadata == :all do
Map.delete(metadata, Context.logger_metadata_key())
metadata
|> Map.delete(Context.logger_metadata_key())
|> Map.drop([
:crash_reason,
:error_logger,
:gl,
:report_cb
])
else
Map.take(metadata, [:distinct_id | config.metadata])
end
end)
|> Map.drop(["$exception_list"])
|> enrich_metadata(log_event)
|> maybe_update_file_key()

Context.get(config.supervisor_name, "$exception")
|> enrich_context(log_event)
|> Map.put(:"$exception_list", [exception])
|> Map.put(:"$exception_list", exceptions)
|> Map.merge(metadata)
end

if Version.match?(System.version(), ">= 1.19.0") and
System.otp_release() |> String.to_integer() >= 27 do
# Reports, such as GenServer crash, should be seen as downstream effects of
# initial exceptions
defp exceptions(%{meta: %{crash_reason: _}, msg: {:report, _}} = log_event, config) do
initial_exception = exception(log_event, config)

reporter_exception =
log_event |> Map.update!(:meta, &Map.delete(&1, :crash_reason)) |> exception(config)

[initial_exception, reporter_exception]
end

# Bare process crash shaped like complex error but it really isnt
defp exceptions(
%{meta: %{crash_reason: _, error_logger: %{emulator: true}}, msg: {:string, _}} =
log_event,
config
) do
[exception(log_event, config)]
end

# Non-reports, such as log messages with attached crash_reason, should be seen
# as primary errors and define grouping.
defp exceptions(%{meta: %{crash_reason: _}} = log_event, config) do
initial_exception = exception(log_event, config)

reporter_exception =
log_event |> Map.update!(:meta, &Map.delete(&1, :crash_reason)) |> exception(config)

[reporter_exception, initial_exception]
end
end

defp exceptions(log_event, config) do
[exception(log_event, config)]
end

defp exception(log_event, config) do
[&type/1, &value/1, &stacktrace(&1, config.in_app_modules)]
|> Enum.reduce(
%{
mechanism: %{handled: not Map.has_key?(log_event.meta, :crash_reason), type: "generic"}
},
fn fun, acc ->
Map.merge(acc, fun.(log_event))
end
)
end

defp type(log_event) do
log_event
|> do_type()
Expand All @@ -87,6 +136,22 @@ defmodule PostHog.Handler do

defp do_type(%{msg: {:string, chardata}}), do: IO.chardata_to_string(chardata)

defp do_type(%{msg: {:report, %{label: {:gen_server, :terminate}}}}) do
"GenServer terminating"
end

defp do_type(%{msg: {:report, %{label: {Task.Supervisor, :terminating}}}}) do
"Task terminating"
end

defp do_type(%{msg: {:report, %{label: {:gen_statem, :terminate}}}}) do
":gen_statem terminating"
end

defp do_type(%{msg: {:report, %{label: {:proc_lib, :crash}}}}) do
"Process terminating"
end
Comment on lines +139 to +153
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means we'll group all of this under the same issue. I assume this is not a problem because this is the underlying reason and we'll group under the actual error, but we might wanna change this in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this shouldn't be a problem, because by default fingerprinting uses not just type but also the latest application frame, which should be different 🤔


defp do_type(%{msg: {:report, report}, meta: %{report_cb: report_cb}})
when is_function(report_cb, 1) do
{io_format, data} = report_cb.(report)
Expand Down Expand Up @@ -123,7 +188,18 @@ defmodule PostHog.Handler do
defp value(%{msg: {io_format, data}}),
do: io_format |> :io_lib.format(data) |> IO.chardata_to_string() |> then(&%{value: &1})

defp stacktrace(%{meta: %{crash_reason: {_reason, [_ | _] = stacktrace}}}, in_app_modules) do
defp stacktrace(%{meta: %{crash_reason: {_reason, [_ | _] = stacktrace}}}, in_app_modules),
do: %{stacktrace: do_stacktrace(stacktrace, in_app_modules)}

defp stacktrace(
%{msg: {:report, %{client_info: {_, {_, [_ | _] = stacktrace}}}}},
in_app_modules
),
do: %{stacktrace: do_stacktrace(stacktrace, in_app_modules)}

defp stacktrace(_event, _), do: %{}

defp do_stacktrace([_ | _] = stacktrace, in_app_modules) do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like an extra indirection for no reason, could've inlined this under stacktrace/2 above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But stacktrace/2 has two heads at this point? We could use case, sure, but that kind of goes against the grain of all other functions in the module, esp. type + do_type. Or am I misunderstanding you comment?

frames =
for {module, function, arity_or_args, location} <- stacktrace do
in_app = module in in_app_modules
Expand All @@ -141,15 +217,11 @@ defmodule PostHog.Handler do
end

%{
stacktrace: %{
type: "raw",
frames: frames
}
type: "raw",
frames: frames
}
end

defp stacktrace(_event, _), do: %{}

defp enrich_context(context, %{meta: %{conn: conn}}) when is_struct(conn, Plug.Conn) do
case context do
# Context was set and survived
Expand All @@ -172,4 +244,122 @@ defmodule PostHog.Handler do
end

defp maybe_update_file_key(metadata), do: metadata

defp enrich_metadata(metadata, log_event) do
[
:genserver_name,
:genserver_state,
:genserver_last_message,
:genserver_process_label,
:genserver_client,
:task_name,
:task_process_label,
:task_starter,
:gen_statem_name,
:gen_statem_state,
:gen_statem_queue,
:gen_statem_client,
:gen_statem_process_label,
:gen_statem_callback_mode,
:gen_statem_postponed,
:gen_statem_state_enter
]
|> Enum.reduce(metadata, fn key, acc ->
key |> extract_extra_meta(log_event) |> Map.merge(acc)
end)
end

defp extract_extra_meta(:genserver_name, %{
msg: {:report, %{label: {:gen_server, :terminate}, name: name}}
}),
do: %{genserver_name: name}

defp extract_extra_meta(:genserver_state, %{
msg: {:report, %{label: {:gen_server, :terminate}, state: state}}
}),
do: %{genserver_state: state}

defp extract_extra_meta(:genserver_last_message, %{
msg: {:report, %{label: {:gen_server, :terminate}, last_message: last_message}}
}),
do: %{genserver_last_message: last_message}

defp extract_extra_meta(:genserver_process_label, %{
msg: {:report, %{label: {:gen_server, :terminate}, process_label: process_label}}
}),
do: %{genserver_process_label: process_label}

defp extract_extra_meta(:genserver_client, %{
msg: {:report, %{label: {:gen_server, :terminate}, client_info: {_, {client, _}}}}
}),
do: %{genserver_client: client}

defp extract_extra_meta(:genserver_client, %{
msg: {:report, %{label: {:gen_server, :terminate}, client_info: {client, _}}}
}),
do: %{genserver_client: client}

defp extract_extra_meta(:task_name, %{
msg: {:report, %{label: {Task.Supervisor, :terminating}, report: %{name: name}}}
}),
do: %{task_name: name}

defp extract_extra_meta(:task_process_label, %{
msg:
{:report,
%{label: {Task.Supervisor, :terminating}, report: %{process_label: process_label}}}
}),
do: %{task_process_label: process_label}

defp extract_extra_meta(:task_starter, %{
msg: {:report, %{label: {Task.Supervisor, :terminating}, report: %{starter: starter}}}
}),
do: %{task_starter: starter}

defp extract_extra_meta(:gen_statem_name, %{
msg: {:report, %{label: {:gen_statem, :terminate}, name: name}}
}),
do: %{gen_statem_name: name}

defp extract_extra_meta(:gen_statem_state, %{
msg: {:report, %{label: {:gen_statem, :terminate}, state: state}}
}),
do: %{gen_statem_state: state}

defp extract_extra_meta(:gen_statem_queue, %{
msg: {:report, %{label: {:gen_statem, :terminate}, queue: queue}}
}),
do: %{gen_statem_queue: queue}

defp extract_extra_meta(:gen_statem_client, %{
msg: {:report, %{label: {:gen_statem, :terminate}, client_info: {_, {client, _}}}}
}),
do: %{gen_statem_client: client}

defp extract_extra_meta(:gen_statem_client, %{
msg: {:report, %{label: {:gen_statem, :terminate}, client_info: {client, _}}}
}),
do: %{gen_statem_client: client}

defp extract_extra_meta(:gen_statem_process_label, %{
msg: {:report, %{label: {:gen_statem, :terminate}, process_label: process_label}}
}),
do: %{gen_statem_process_label: process_label}

defp extract_extra_meta(:gen_statem_callback_mode, %{
msg: {:report, %{label: {:gen_statem, :terminate}, callback_mode: callback_mode}}
}),
do: %{gen_statem_callback_mode: callback_mode}

defp extract_extra_meta(:gen_statem_postponed, %{
msg: {:report, %{label: {:gen_statem, :terminate}, postponed: postponed}}
}),
do: %{gen_statem_postponed: postponed}

defp extract_extra_meta(:gen_statem_state_enter, %{
msg: {:report, %{label: {:gen_statem, :terminate}, state_enter: state_enter}}
}),
do: %{gen_statem_state_enter: state_enter}

defp extract_extra_meta(_, _), do: %{}
end
11 changes: 11 additions & 0 deletions test/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ defmodule PostHog.IntegrationTest do
wait.()
end

test "log message with crash_reason", %{wait_fun: wait} do
try do
raise "Crashing for no reason"
rescue
e ->
Logger.warning("No crash, just crash shaped", crash_reason: {e, __STACKTRACE__})
end

wait.()
end

test "genserver crash exception", %{wait_fun: wait} do
LoggerHandlerKit.Act.genserver_crash(:exception)
wait.()
Expand Down
Loading
Loading