Skip to content

Commit 67ef9f7

Browse files
authored
Allow saving occurrences with invalid context (#71)
This change updates how an occurrence is stored in order to validate if the context can be serialized to be stored on the database. If the context cannot be serialized, we emit a warning log message and store an error as the context. Extra tests have been added for the new functionality. **Note:** This is a proposal. I think it may be a good idea but it adds a bit more of complexity on occurrence building. On the other hand, we avoid having issues which are invisible to our users by not storing them. Closes #65 (again)
1 parent 233bf78 commit 67ef9f7

File tree

5 files changed

+107
-5
lines changed

5 files changed

+107
-5
lines changed

lib/error_tracker.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ defmodule ErrorTracker do
7070
import Ecto.Query
7171

7272
alias ErrorTracker.Error
73+
alias ErrorTracker.Occurrence
7374
alias ErrorTracker.Repo
7475
alias ErrorTracker.Telemetry
7576

@@ -212,11 +213,12 @@ defmodule ErrorTracker do
212213

213214
occurrence =
214215
error
215-
|> Ecto.build_assoc(:occurrences,
216+
|> Ecto.build_assoc(:occurrences)
217+
|> Occurrence.changeset(%{
216218
stacktrace: stacktrace,
217219
context: context,
218220
reason: reason
219-
)
221+
})
220222
|> Repo.insert!()
221223

222224
{error, occurrence}

lib/error_tracker/schemas/occurrence.ex

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ defmodule ErrorTracker.Occurrence do
88

99
use Ecto.Schema
1010

11+
require Logger
12+
import Ecto.Changeset
13+
1114
schema "error_tracker_occurrences" do
1215
field :context, :map
1316
field :reason, :string
@@ -17,4 +20,54 @@ defmodule ErrorTracker.Occurrence do
1720

1821
timestamps(type: :utc_datetime_usec, updated_at: false)
1922
end
23+
24+
@doc false
25+
def changeset(occurrence, attrs) do
26+
occurrence
27+
|> cast(attrs, [:context, :reason])
28+
|> maybe_put_stacktrace()
29+
|> validate_required([:reason, :stacktrace])
30+
|> validate_context()
31+
|> foreign_key_constraint(:error)
32+
end
33+
34+
# This function validates if the context can be serialized to JSON before
35+
# storing it to the DB.
36+
#
37+
# If it cannot be serialized a warning log message is emitted and an error
38+
# is stored in the context.
39+
#
40+
defp validate_context(changeset) do
41+
if changeset.valid? do
42+
context = get_field(changeset, :context, %{})
43+
44+
json_encoder =
45+
ErrorTracker.Repo.with_adapter(fn
46+
:postgres -> Application.get_env(:postgrex, :json_library, Jason)
47+
:sqlite -> Application.get_env(:ecto_sqlite3, :json_library, Jason)
48+
end)
49+
50+
case json_encoder.encode_to_iodata(context) do
51+
{:ok, _} ->
52+
put_change(changeset, :context, context)
53+
54+
{:error, _} ->
55+
Logger.warning(
56+
"[ErrorTracker] Context has been ignored: it is not serializable to JSON."
57+
)
58+
59+
put_change(changeset, :context, %{
60+
error: "Context not stored because it contains information not serializable to JSON."
61+
})
62+
end
63+
else
64+
changeset
65+
end
66+
end
67+
68+
defp maybe_put_stacktrace(changeset) do
69+
if stacktrace = Map.get(changeset.params, "stacktrace"),
70+
do: put_embed(changeset, :stacktrace, stacktrace),
71+
else: changeset
72+
end
2073
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule ErrorTracker.OccurrenceTest do
2+
use ErrorTracker.Test.Case
3+
4+
import Ecto.Changeset
5+
6+
alias ErrorTracker.Occurrence
7+
alias ErrorTracker.Stacktrace
8+
9+
describe inspect(&Occurrence.changeset/2) do
10+
test "works as expected with valid data" do
11+
attrs = %{context: %{foo: :bar}, reason: "Test reason", stacktrace: %Stacktrace{}}
12+
changeset = Occurrence.changeset(%Occurrence{}, attrs)
13+
14+
assert changeset.valid?
15+
end
16+
17+
test "validates required fields" do
18+
changeset = Occurrence.changeset(%Occurrence{}, %{})
19+
20+
refute changeset.valid?
21+
assert {_, [validation: :required]} = changeset.errors[:reason]
22+
assert {_, [validation: :required]} = changeset.errors[:stacktrace]
23+
end
24+
25+
@tag capture_log: true
26+
test "if context is not serializable, an error messgae is stored" do
27+
attrs = %{
28+
context: %{foo: %ErrorTracker.Error{}},
29+
reason: "Test reason",
30+
stacktrace: %Stacktrace{}
31+
}
32+
33+
changeset = Occurrence.changeset(%Occurrence{}, attrs)
34+
35+
assert %{error: err} = get_field(changeset, :context)
36+
assert err =~ "not serializable to JSON"
37+
end
38+
end
39+
end

test/error_tracker_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ defmodule ErrorTrackerTest do
6464
assert error.reason == "This is a test"
6565
assert error.source_line =~ @relative_file_path
6666
end
67+
68+
@tag capture_log: true
69+
test "reports errors with invalid context" do
70+
# It's invalid because cannot be serialized to JSON
71+
invalid_context = %{foo: %ErrorTracker.Error{}}
72+
73+
assert %Occurrence{} = report_error(fn -> raise "test" end, invalid_context)
74+
end
6775
end
6876

6977
describe inspect(&ErrorTracker.resolve/1) do

test/support/case.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ defmodule ErrorTracker.Test.Case do
1616
@doc """
1717
Reports the error produced by the given function.
1818
"""
19-
def report_error(fun) do
19+
def report_error(fun, context \\ %{}) do
2020
occurrence =
2121
try do
2222
fun.()
2323
rescue
2424
exception ->
25-
ErrorTracker.report(exception, __STACKTRACE__)
25+
ErrorTracker.report(exception, __STACKTRACE__, context)
2626
catch
2727
kind, reason ->
28-
ErrorTracker.report({kind, reason}, __STACKTRACE__)
28+
ErrorTracker.report({kind, reason}, __STACKTRACE__, context)
2929
end
3030

3131
repo().preload(occurrence, :error)

0 commit comments

Comments
 (0)