Skip to content
23 changes: 22 additions & 1 deletion lib/error_tracker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ defmodule ErrorTracker do
En Elixir based built-in error tracking solution.
"""

def report(exception, stacktrace, context \\ %{}) do
@typedoc """
A map containing the relevant context for a particular error.
"""
@type context :: %{String.t() => any()}

def report(exception, stacktrace, given_context \\ %{}) do
{:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace)
{:ok, error} = ErrorTracker.Error.new(exception, stacktrace)

context = Map.merge(get_context(), given_context)

error =
repo().insert!(error,
on_conflict: [set: [status: :unresolved]],
Expand All @@ -26,4 +33,18 @@ defmodule ErrorTracker do
def prefix do
Application.get_env(:error_tracker, :prefix, "public")
end

@spec set_context(context()) :: context()
def set_context(params) when is_map(params) do
current_context = Process.get(:error_tracker_context, %{})

Process.put(:error_tracker_context, Map.merge(current_context, params))

params
end

@spec get_context() :: context()
def get_context do
Process.get(:error_tracker_context, %{})
end
end
30 changes: 22 additions & 8 deletions lib/error_tracker/integrations/oban.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,34 @@ defmodule ErrorTracker.Integrations.Oban do
modify anything on your application.
"""

# https://hexdocs.pm/oban/Oban.Telemetry.html
@events [
[:oban, :job, :start],
[:oban, :job, :exception]
]

def attach do
if Application.spec(:oban) do
:telemetry.attach(__MODULE__, [:oban, :job, :exception], &handle_event/4, :no_config)
:telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config)
end
end

def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do
%{job: job, reason: exception, stacktrace: stacktrace} = metadata
def handle_event([:oban, :job, :start], _measurements, metadata, :no_config) do
%{job: job} = metadata

ErrorTracker.report(exception, stacktrace, %{
job_id: job.id,
job_attempt: job.attempt,
job_queue: job.queue,
job_worker: job.worker
ErrorTracker.set_context(%{
"job.args" => job.args,
"job.attempt" => job.attempt,
"job.id" => job.id,
"job.priority" => job.priority,
"job.queue" => job.queue,
"job.worker" => job.worker
})
end

def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do
%{reason: exception, stacktrace: stacktrace} = metadata

ErrorTracker.report(exception, stacktrace)
end
end
40 changes: 20 additions & 20 deletions lib/error_tracker/integrations/phoenix.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,32 @@ defmodule ErrorTracker.Integrations.Phoenix do

alias ErrorTracker.Integrations.Plug, as: PlugIntegration

# https://hexdocs.pm/phoenix/Phoenix.Logger.html#module-instrumentation
@events [
[:phoenix, :router_dispatch, :start],
[:phoenix, :router_dispatch, :exception]
]

def attach do
if Application.spec(:phoenix) do
:telemetry.attach(
__MODULE__,
[:phoenix, :router_dispatch, :exception],
&__MODULE__.handle_exception/4,
[]
)
:telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config)
end
end

def handle_exception(
[:phoenix, :router_dispatch, :exception],
_measurements,
%{reason: %Plug.Conn.WrapperError{conn: conn, reason: reason, stack: stack}},
_opts
) do
PlugIntegration.report_error(conn, reason, stack)
def handle_event([:phoenix, :router_dispatch, :start], _measurements, metadata, :no_config) do
PlugIntegration.set_context(metadata.conn)
end

def handle_exception(
[:phoenix, :router_dispatch, :exception],
_measurements,
%{reason: reason, stacktrace: stack, conn: conn},
_opts
) do
PlugIntegration.report_error(conn, reason, stack)
def handle_event([:phoenix, :router_dispatch, :exception], _measurements, metadata, :no_config) do
{reason, stack} =
case metadata do
%{reason: %Plug.Conn.WrapperError{reason: reason, stack: stack}} ->
{reason, stack}

%{reason: reason, stacktrace: stack} ->
{reason, stack}
end

PlugIntegration.report_error(metadata.conn, reason, stack)
end
end
40 changes: 37 additions & 3 deletions lib/error_tracker/integrations/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ defmodule ErrorTracker.Integrations.Plug do
defoverridable call: 2

def call(conn, opts) do
unquote(__MODULE__).set_context(conn)
super(conn, opts)
rescue
e in Plug.Conn.WrapperError ->
unquote(__MODULE__).report_error(conn, e, e.stack)
unquote(__MODULE__).report_error(e.conn, e.reason, e.stack)

Plug.Conn.WrapperError.reraise(e)

Expand All @@ -68,13 +69,46 @@ defmodule ErrorTracker.Integrations.Plug do
end
end

def report_error(_conn, reason, stack) do
def report_error(conn, reason, stack) do
unless Process.get(:error_tracker_router_exception_reported) do
try do
ErrorTracker.report(reason, stack)
ErrorTracker.report(reason, stack, build_context(conn))
after
Process.put(:error_tracker_router_exception_reported, true)
end
end
end

def set_context(conn = %Plug.Conn{}) do
conn |> build_context |> ErrorTracker.set_context()
end

defp build_context(conn = %Plug.Conn{}) do
%{
"request.host" => conn.host,
"request.path" => conn.request_path,
"request.query" => conn.query_string,
"request.method" => conn.method,
"request.ip" => remote_ip(conn),
"request.headers" => Map.new(conn.req_headers),
# Depending on the error source, the request params may have not been fetched yet
"request.params" => unless(is_struct(conn.params, Plug.Conn.Unfetched), do: conn.params)
}
end

defp remote_ip(conn = %Plug.Conn{}) do
remote_ip =
case Plug.Conn.get_req_header(conn, "x-forwarded-for") do
[x_forwarded_for | _] ->
x_forwarded_for |> String.split(",", parts: 2) |> List.first()

[] ->
case :inet.ntoa(conn.remote_ip) do
{:error, _} -> ""
address -> to_string(address)
end
end

String.trim(remote_ip)
end
end