Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
56016ba
Track errors last occurrence
crbelaus Jul 15, 2024
8eb7ee2
WIP: error dashboard index
crbelaus Jul 15, 2024
ae89054
Filters and pagination
crbelaus Jul 17, 2024
b01a326
Remove unused callback
crbelaus Jul 18, 2024
adaecd2
Layout changes
odarriba Jul 18, 2024
76cd0a0
WIP dashboard and components
odarriba Jul 18, 2024
68f75e8
Add pagination
odarriba Jul 18, 2024
af418f9
Navigate to error detail
crbelaus Jul 18, 2024
8c9a638
Load occurrences list
odarriba Jul 20, 2024
f0f8282
Don't hash last_occurrence_at
crbelaus Jul 20, 2024
6113ddb
Fix warning
crbelaus Jul 20, 2024
efca1aa
Wrap Ecto.Repo
crbelaus Jul 20, 2024
1af5929
Fix Credo
crbelaus Jul 20, 2024
851f78b
Fix warnings
crbelaus Jul 20, 2024
47390b0
Full width filters
crbelaus Jul 20, 2024
22ffb52
fixup! Wrap Ecto.Repo
crbelaus Jul 20, 2024
9e93a23
WIP: styles
crbelaus Jul 20, 2024
3afac95
Show occurrence stacktrace and context
crbelaus Jul 21, 2024
552c655
Show only app frames in stacktrace
crbelaus Jul 21, 2024
f9425dc
Add button as links
odarriba Jul 21, 2024
645faf6
Set configured dashboard path on the assigns
odarriba Jul 21, 2024
14cbbde
Fix button as link
odarriba Jul 21, 2024
2c027ed
Implement routes generator helper
odarriba Jul 21, 2024
e9c4202
Happy credo
odarriba Jul 21, 2024
1bab26b
Refactor how related occurrences are loaded
odarriba Jul 21, 2024
d9aa818
WIP UI
odarriba Jul 21, 2024
e06e688
Update navbar and layout
odarriba Jul 21, 2024
5bf3cc1
More UI changes
odarriba Jul 21, 2024
337cf3c
Happy credo and fix compile warnings
odarriba Jul 21, 2024
89ef67f
Update how timestamps are shown
odarriba Jul 21, 2024
3e416b0
Format sections
odarriba Jul 21, 2024
d713724
Sanitize module names and format datetimes
odarriba Jul 21, 2024
e56c5d6
Better checkbox placement
odarriba Jul 21, 2024
ce424dc
Happy credo
odarriba Jul 21, 2024
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
2 changes: 1 addition & 1 deletion .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.FunctionArity, []},
Expand Down Expand Up @@ -189,6 +188,7 @@
{Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, []},
{Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []},
Expand Down
26 changes: 16 additions & 10 deletions lib/error_tracker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,36 @@ defmodule ErrorTracker do
"""
@type context :: %{String.t() => any()}

alias ErrorTracker.Error
alias ErrorTracker.Repo

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

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

error =
repo().insert!(error,
on_conflict: [set: [status: :unresolved]],
conflict_target: :fingerprint,
prefix: prefix()
Repo.insert!(error,
on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
conflict_target: :fingerprint
)

error
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context)
|> repo().insert!(prefix: prefix())
|> Repo.insert!()
end

def repo do
Application.fetch_env!(:error_tracker, :repo)
def resolve(error = %Error{status: :unresolved}) do
changeset = Ecto.Changeset.change(error, status: :resolved)

Repo.update(changeset)
end

def prefix do
Application.get_env(:error_tracker, :prefix, "public")
def unresolve(error = %Error{status: :resolved}) do
changeset = Ecto.Changeset.change(error, status: :unresolved)

Repo.update(changeset)
end

@spec set_context(context()) :: context()
Expand Down
2 changes: 1 addition & 1 deletion lib/error_tracker/migrations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ defmodule ErrorTracker.Migration do
end

defp migrator do
case ErrorTracker.repo().__adapter__() do
case ErrorTracker.Repo.__adapter__() do
Ecto.Adapters.Postgres -> ErrorTracker.Migrations.Postgres
adapter -> raise "ErrorTracker does not support #{adapter}"
end
Expand Down
1 change: 1 addition & 0 deletions lib/error_tracker/migrations/postgres/v01.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule ErrorTracker.Migrations.Postgres.V01 do
add :source_function, :text, null: false
add :status, :string, null: false
add :fingerprint, :string, null: false
add :last_occurrence_at, :utc_datetime_usec, null: false

timestamps(type: :utc_datetime_usec)
end
Expand Down
41 changes: 41 additions & 0 deletions lib/error_tracker/repo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule ErrorTracker.Repo do
@moduledoc """
Wraps Ecto.Repo calls and applies default options.
"""
def insert!(struct_or_changeset, opts \\ []) do
dispatch(:insert!, [struct_or_changeset], opts)
end

def update(changeset, opts \\ []) do
dispatch(:update, [changeset], opts)
end

def get(queryable, id, opts \\ []) do
dispatch(:get, [queryable, id], opts)
end

def get!(queryable, id, opts \\ []) do
dispatch(:get!, [queryable, id], opts)
end

def all(queryable, opts \\ []) do
dispatch(:all, [queryable], opts)
end

def aggregate(queryable, aggregate, opts \\ []) do
dispatch(:aggregate, [queryable, aggregate], opts)
end

def __adapter__, do: repo().__adapter__()

defp dispatch(action, args, opts) do
defaults = [prefix: Application.get_env(:error_tracker, :prefix, "public")]
opts_w_defaults = Keyword.merge(defaults, opts)

apply(repo(), action, args ++ [opts_w_defaults])
end

defp repo do
Application.fetch_env!(:error_tracker, :repo)
end
end
2 changes: 2 additions & 0 deletions lib/error_tracker/schemas/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule ErrorTracker.Error do
field :source_function, :string
field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved
field :fingerprint, :binary
field :last_occurrence_at, :utc_datetime_usec

has_many :occurrences, ErrorTracker.Occurrence

Expand Down Expand Up @@ -48,6 +49,7 @@ defmodule ErrorTracker.Error do
%__MODULE__{}
|> Ecto.Changeset.change(params)
|> Ecto.Changeset.put_change(:fingerprint, Base.encode16(fingerprint))
|> Ecto.Changeset.put_change(:last_occurrence_at, DateTime.utc_now())
|> Ecto.Changeset.apply_action(:new)
end
end
4 changes: 4 additions & 0 deletions lib/error_tracker/web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ defmodule ErrorTracker.Web do
import Phoenix.HTML
import Phoenix.LiveView.Helpers

import ErrorTracker.Web.CoreComponents
import ErrorTracker.Web.Helpers
import ErrorTracker.Web.Router.Routes

alias Phoenix.LiveView.JS
end
end
Expand Down
127 changes: 127 additions & 0 deletions lib/error_tracker/web/components/core_components.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
defmodule ErrorTracker.Web.CoreComponents do
@moduledoc false
use Phoenix.Component

@doc """
Renders a button.

## Examples

<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value href patch navigate)

slot :inner_block, required: true

def button(assigns = %{type: "link"}) do
~H"""
<.link
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-600 hover:bg-zinc-400 py-[11.5px] px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</.link>
"""
end

def button(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-600 hover:bg-zinc-400 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end

@doc """
Renders a badge.

## Examples

<.badge>Info</.badge>
<.badge color={:red}>Error</.badge>
"""
attr :color, :atom, default: :blue
attr :rest, :global

slot :inner_block, required: true

def badge(assigns) do
color_class =
case assigns.color do
:blue -> "bg-blue-900 text-blue-300"
:gray -> "bg-gray-700 text-gray-300"
:red -> "bg-red-900 text-red-300"
:green -> "bg-green-900 text-green-300"
:yellow -> "bg-yellow-900 text-yellow-300"
:indigo -> "bg-indigo-900 text-indigo-300"
:purple -> "bg-purple-900 text-purple-300"
:pink -> "bg-pink-900 text-pink-300"
end

assigns = Map.put(assigns, :color_class, color_class)

~H"""
<span class={["text-sm font-medium me-2 px-2.5 py-1.5 rounded", @color_class]} {@rest}>
<%= render_slot(@inner_block) %>
</span>
"""
end

attr :page, :integer, required: true
attr :total_pages, :integer, required: true
attr :event_previous, :string, default: "prev-page"
attr :event_next, :string, default: "next-page"

def pagination(assigns) do
~H"""
<div class="mt-10 w-full flex">
<button
:if={@page > 1}
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 hover:text-white"
phx-click={@event_previous}
>
Previous page
</button>
<button
:if={@page < @total_pages}
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 bg-gray-800 border border-gray-700 rounded-lg hover:bg-gray-700 hover:text-white"
phx-click={@event_next}
>
Next page
</button>
</div>
"""
end

attr :title, :string
attr :title_class, :string, default: nil
attr :rest, :global

slot :inner_block, required: true

def section(assigns) do
~H"""
<div>
<h2 :if={assigns[:title]} class={["text-sm font-bold mb-2 uppercase", @title_class]}>
<%= @title %>
</h2>
<%= render_slot(@inner_block) %>
</div>
"""
end
end
2 changes: 2 additions & 0 deletions lib/error_tracker/web/components/layouts.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule ErrorTracker.Web.Layouts do
use ErrorTracker.Web, :html

alias ErrorTracker.Web.Layouts.Navbar

@css_path :code.priv_dir(:error_tracker) |> Path.join("static/app.css")
@js_path :code.priv_dir(:error_tracker) |> Path.join("static/app.js")

Expand Down
3 changes: 2 additions & 1 deletion lib/error_tracker/web/components/layouts/live.html.heex
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<main>
<.live_component module={Navbar} id="navbar" {assigns} />
<main class="container px-4 mx-auto mt-4 mb-4">
<%= @inner_content %>
</main>
67 changes: 67 additions & 0 deletions lib/error_tracker/web/components/layouts/navbar.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule ErrorTracker.Web.Layouts.Navbar do
@moduledoc false
use ErrorTracker.Web, :live_component

def render(assigns) do
~H"""
<nav class="border-gray-200 bg-gray-800" phx-click-away={JS.hide(to: "#navbar-main")}>
<div class="container flex flex-wrap items-center justify-between mx-auto p-4">
<.link
href={dashboard_path(assigns)}
class="self-center text-2xl font-semibold whitespace-nowrap text-white"
>
ErrorTracker
</.link>
<button
type="button"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded -lg md:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-600 focus:ring-gray-500"
aria-controls="navbar-main"
aria-expanded="false"
phx-click={JS.toggle(to: "#navbar-main")}
>
<span class="sr-only">Open main menu</span>
<svg
class="w-5 h-5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 17 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 1h15M1 7h15M1 13h15"
/>
</svg>
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-main">
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-600 rounded-lg bg-gray-700 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-gray-800">
<.navbar_item to="https://github.com" target="_blank">GitHub</.navbar_item>
</ul>
</div>
</div>
</nav>
"""
end

attr :to, :string, required: true
attr :rest, :global

slot :inner_block, required: true

def navbar_item(assigns) do
~H"""
<li>
<a
href={@to}
class="block py-2 px-3 text-gray-900 rounded text-white hover:text-white hover:bg-gray-700 md:hover:bg-transparent md:border-0 md:hover:text-blue-500 md:p-0"
{@rest}
>
<%= render_slot(@inner_block) %>
</a>
</li>
"""
end
end
2 changes: 1 addition & 1 deletion lib/error_tracker/web/components/layouts/root.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</script>
</head>

<body>
<body class="bg-gray-700 text-white">
<%= @inner_content %>
</body>
</html>
10 changes: 10 additions & 0 deletions lib/error_tracker/web/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule ErrorTracker.Web.Helpers do
@moduledoc false

@doc false
def sanitize_module(<<"Elixir.", str::binary>>), do: str
def sanitize_module(str), do: str

@doc false
def format_datetime(dt = %DateTime{}), do: Calendar.strftime(dt, "%c %Z")
end
10 changes: 10 additions & 0 deletions lib/error_tracker/web/hooks/set_assigns.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule ErrorTracker.Web.Hooks.SetAssigns do
@moduledoc """
Mounting hooks to set environment configuration on the socket.
"""
import Phoenix.Component

def on_mount({:set_dashboard_path, path}, _params, _session, socket) do
{:cont, assign(socket, :dashboard_path, path)}
end
end
Loading