Skip to content

Commit efec72f

Browse files
authored
Improve Sentry.Event struct definition (#609)
1 parent 49c20a6 commit efec72f

File tree

10 files changed

+214
-157
lines changed

10 files changed

+214
-157
lines changed

lib/sentry.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ defmodule Sentry do
467467
@spec send_event(Event.t(), keyword()) :: send_result
468468
def send_event(event, opts \\ [])
469469

470-
def send_event(%Event{message: nil, exception: nil}, _opts) do
470+
def send_event(%Event{message: nil, exception: []}, _opts) do
471471
Logger.log(Config.log_level(), "Sentry: unable to parse exception")
472472

473473
:ignored

lib/sentry/client.ex

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ defmodule Sentry.Client do
8989
send_result = Transport.post_envelope(envelope, request_retries)
9090

9191
if match?({:ok, _}, send_result) do
92-
Sentry.put_last_event_id_and_source(event.event_id, event.__source__)
92+
Sentry.put_last_event_id_and_source(event.event_id, event.source)
9393
end
9494

9595
_ = maybe_log_send_result(send_result, event)
@@ -98,7 +98,7 @@ defmodule Sentry.Client do
9898

9999
defp encode_and_send(%Event{} = event, _result_type = :none, _request_retries) do
100100
:ok = @sender_module.send_async(event)
101-
Sentry.put_last_event_id_and_source(event.event_id, event.__source__)
101+
Sentry.put_last_event_id_and_source(event.event_id, event.source)
102102
{:ok, ""}
103103
end
104104

@@ -107,15 +107,14 @@ defmodule Sentry.Client do
107107
json_library = Config.json_library()
108108

109109
event
110-
|> Map.from_struct()
110+
|> Event.remove_non_payload_keys()
111111
|> update_if_present(:message, &String.slice(&1, 0, @max_message_length))
112112
|> update_if_present(:breadcrumbs, fn bcs -> Enum.map(bcs, &Map.from_struct/1) end)
113113
|> update_if_present(:sdk, &Map.from_struct/1)
114114
|> update_if_present(:extra, &sanitize_non_jsonable_values(&1, json_library))
115115
|> update_if_present(:user, &sanitize_non_jsonable_values(&1, json_library))
116116
|> update_if_present(:tags, &sanitize_non_jsonable_values(&1, json_library))
117-
|> update_if_present(:exception, &[render_exception(&1)])
118-
|> Map.drop([:__source__, :__original_exception__])
117+
|> update_if_present(:exception, fn list -> Enum.map(list, &render_exception/1) end)
119118
end
120119

121120
defp render_exception(%Interfaces.Exception{} = exception) do
@@ -178,7 +177,7 @@ defmodule Sentry.Client do
178177
end
179178
end
180179

181-
defp maybe_log_send_result(_send_result, %Event{__source__: :logger}) do
180+
defp maybe_log_send_result(_send_result, %Event{source: :logger}) do
182181
:ok
183182
end
184183

lib/sentry/envelope.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,14 @@ defmodule Sentry.Envelope do
131131
culprit: fields["culprit"],
132132
environment: fields["environment"],
133133
event_id: fields["event_id"],
134-
__source__: fields["event_source"],
135-
exception: fields["exception"],
134+
source: fields["event_source"],
135+
exception: List.wrap(fields["exception"]),
136136
extra: fields["extra"],
137137
fingerprint: fields["fingerprint"],
138138
level: fields["level"],
139139
message: fields["message"],
140140
modules: fields["modules"],
141-
__original_exception__: fields["original_exception"],
141+
original_exception: fields["original_exception"],
142142
platform: fields["platform"],
143143
release: fields["release"],
144144
request: fields["request"],

lib/sentry/event.ex

Lines changed: 123 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,22 @@ defmodule Sentry.Event do
2929
@typedoc """
3030
The type for the event struct.
3131
32-
See [`%Sentry.Event{}`](`__struct__/0`) for more information.
32+
All of the fields in this struct map directly to the fields described in the
33+
[Sentry documentation](https://develop.sentry.dev/sdk/event-payloads). These fields
34+
are the exceptions, and are specific to the Elixir Sentry SDK:
35+
36+
* `:source` - the source of the event. `Sentry.LoggerBackend` and `Sentry.LoggerHandler`
37+
set this to `:logger`, while `Sentry.PlugCapture` and `Sentry.PlugContext` set it to
38+
`:plug`. You can set it to any atom. See the `:event_source` option in `create_event/1`
39+
and `transform_exception/2`.
40+
41+
* `:original_exception` - the original exception that is being reported, if there's one.
42+
The Elixir Sentry SDK manipulates reported exceptions to make them fit the payload
43+
required by the Sentry API, and these end up in the `:exception` field. The
44+
`:original_exception` field, instead, contains the original exception as the raw Elixir
45+
term (such as `%RuntimeError{...}`).
46+
47+
See also [`%Sentry.Event{}`](`__struct__/0`).
3348
"""
3449
@type t() :: %__MODULE__{
3550
# Required
@@ -53,113 +68,144 @@ defmodule Sentry.Event do
5368
# Interfaces.
5469
breadcrumbs: [Interfaces.Breadcrumb.t()],
5570
contexts: Interfaces.context(),
56-
exception: Interfaces.Exception.t() | nil,
71+
exception: [Interfaces.Exception.t()],
5772
message: String.t() | nil,
5873
request: Interfaces.request(),
5974
sdk: Interfaces.SDK.t() | nil,
6075
user: Interfaces.user() | nil,
6176

6277
# Non-payload fields.
63-
__source__: term(),
64-
__original_exception__: Exception.t() | nil
78+
source: atom(),
79+
original_exception: Exception.t() | nil
6580
}
6681

6782
@doc """
6883
The struct representing the event.
6984
70-
In general, you're not advised to manipulate this struct's fields directly. Instead,
71-
try to use functions such as `create_event/1` or `transform_exception/2` for creating
85+
You're not advised to manipulate this struct's fields directly. Instead,
86+
use functions such as `create_event/1` or `transform_exception/2` for creating
7287
events.
88+
89+
See the `t:t/0` type for information on the fields and their types.
7390
"""
7491
@enforce_keys [:event_id, :timestamp]
7592
defstruct [
7693
# Required. Hexadecimal string representing a uuid4 value. The length is exactly 32
7794
# characters. Dashes are not allowed. Has to be lowercase.
78-
:event_id,
95+
event_id: nil,
7996

8097
# Required. Indicates when the event was created in the Sentry SDK. The format is either a
8198
# string as defined in RFC 3339 or a numeric (integer or float) value representing the number
8299
# of seconds that have elapsed since the Unix epoch.
83-
:timestamp,
84-
85-
# Optional fields without defaults.
86-
:level,
87-
:logger,
88-
:transaction,
89-
:server_name,
90-
:release,
91-
:dist,
92-
93-
# Interfaces.
94-
:breadcrumbs,
95-
:contexts,
96-
:exception,
97-
:message,
98-
:request,
99-
:sdk,
100-
:user,
100+
timestamp: nil,
101+
102+
# Optional fields.
103+
breadcrumbs: [],
104+
contexts: nil,
105+
dist: nil,
106+
environment: "production",
107+
exception: [],
108+
extra: %{},
109+
fingerprint: [],
110+
level: nil,
111+
logger: nil,
112+
message: nil,
113+
modules: %{},
114+
platform: :elixir,
115+
release: nil,
116+
request: %{},
117+
sdk: nil,
118+
server_name: nil,
119+
tags: %{},
120+
transaction: nil,
121+
user: %{},
101122

102123
# "Culprit" is not documented anymore and we should move to transactions at some point.
103124
# https://forum.sentry.io/t/culprit-deprecated-in-favor-of-what/4871/9
104-
:culprit,
125+
culprit: nil,
105126

106127
# Non-payload "private" fields.
107-
:__source__,
108-
:__original_exception__,
109-
110-
# Required. Has to be "elixir".
111-
platform: :elixir,
112-
113-
# Optional fields with defaults.
114-
tags: %{},
115-
modules: %{},
116-
extra: %{},
117-
fingerprint: [],
118-
environment: "production"
128+
source: nil,
129+
original_exception: nil
119130
]
120131

132+
# Removes all the non-payload keys from the event so that the client can render
133+
@doc false
134+
@spec remove_non_payload_keys(t()) :: map()
135+
def remove_non_payload_keys(%__MODULE__{} = event) do
136+
event
137+
|> Map.from_struct()
138+
|> Map.drop([:original_exception, :source])
139+
end
140+
121141
@doc """
122142
Creates an event struct out of collected context and options.
123143
144+
> #### Merging Options with Context and Config {: .info}
145+
>
146+
> Some of the options documented below are **merged** with the Sentry context, or
147+
> with the Sentry context *and* the configuration. The option you pass here always
148+
> has higher precedence, followed by the context and finally by the configuration.
149+
>
150+
> See also `Sentry.Context` for information on the Sentry context and `Sentry` for
151+
> information on configuration.
152+
124153
## Options
125154
126-
* `:exception` - an `t:Exception.t/0`
155+
* `:exception` - an `t:Exception.t/0`. This is the exception that gets reported in the
156+
`:exception` field of `t:t/0`. The term passed here also ends up unchanged in the
157+
`:original_exception` field of `t:t/0`. This option is **required** unless the
158+
`:message` option is present. This is not present by default.
127159
128-
* `:stacktrace` - a stacktrace, as in `t:Exception.stacktrace/0`
160+
* `:stacktrace` - a stacktrace, as in `t:Exception.stacktrace/0`. This is not present
161+
by default.
129162
130-
* `:message` - a message (`t:String.t/0`)
163+
* `:message` - a message (`t:String.t/0`). This is not present by default.
131164
132165
* `:extra` - map of extra context, which gets merged with the current context
133-
(see `Sentry.Context`)
166+
(see `Sentry.Context.set_extra_context/1`). If fields collide, the ones
167+
in the map passed through this option have precedence over the ones in
168+
the context. Defaults to `%{}`.
134169
135170
* `:user` - map of user context, which gets merged with the current context
136-
(see `Sentry.Context`)
171+
(see `Sentry.Context.set_user_context/1`). If fields collide, the ones
172+
in the map passed through this option have precedence over the ones in
173+
the context. Defaults to `%{}`.
137174
138-
* `:tags` - map of tags context, which gets merged with the current context
139-
(see `Sentry.Context`)
175+
* `:tags` - map of tags context, which gets merged with the current context (see
176+
`Sentry.Context.set_tags_context/1`) and with the `:tags` option in the global
177+
Sentry configuration. If fields collide, the ones in the map passed through
178+
this option have precedence over the ones in the context, which have precedence
179+
over the ones in the configuration. Defaults to `%{}`.
140180
141181
* `:request` - map of request context, which gets merged with the current context
142-
(see `Sentry.Context`)
182+
(see `Sentry.Context.set_request_context/1`). If fields collide, the ones
183+
in the map passed through this option have precedence over the ones in
184+
the context. Defaults to `%{}`.
143185
144-
* `:breadcrumbs` - list of breadcrumbs
186+
* `:breadcrumbs` - list of breadcrumbs. This list gets **prepended** to the list
187+
in the context (see `Sentry.Context.add_breadcrumb/1`). Defaults to `[]`.
145188
146-
* `:level` - error level (see `t:t/0`)
189+
* `:level` - error level (see `t:t/0`). Defaults to `:error`.
147190
148-
* `:fingerprint` - list of the fingerprint for grouping this event (a list of `t:String.t/0`)
191+
* `:fingerprint` - list of the fingerprint for grouping this event (a list
192+
of `t:String.t/0`). Defaults to `["{{ default }}"]`.
149193
150-
* `:event_source` - the source of the event. This fills in the `:__source__` field of the
151-
returned struct.
194+
* `:event_source` - the source of the event. This fills in the `:source` field of the
195+
returned struct. This is not present by default.
152196
153197
## Examples
154198
155199
iex> event = create_event(exception: %RuntimeError{message: "oops"}, level: :warning)
156200
iex> event.level
157201
:warning
158-
iex> event.exception.type
202+
iex> hd(event.exception).type
159203
"RuntimeError"
204+
iex> event.original_exception
205+
%RuntimeError{message: "oops"}
160206
161-
iex> event = create_event(event_source: :plug)
162-
iex> event.__source__
207+
iex> event = create_event(message: "Unknown route", event_source: :plug)
208+
iex> event.source
163209
:plug
164210
165211
"""
@@ -173,7 +219,7 @@ defmodule Sentry.Event do
173219
| {:level, level()}
174220
| {:fingerprint, [String.t()]}
175221
| {:message, String.t()}
176-
| {:event_source, term()}
222+
| {:event_source, atom()}
177223
| {:exception, Exception.t()}
178224
| {:stacktrace, Exception.stacktrace()}
179225
def create_event(opts) when is_list(opts) do
@@ -191,25 +237,18 @@ defmodule Sentry.Event do
191237
request: request_context
192238
} = Sentry.Context.get_all()
193239

240+
level = Keyword.get(opts, :level, :error)
194241
fingerprint = Keyword.get(opts, :fingerprint, ["{{ default }}"])
195242

196-
extra =
197-
extra_context
198-
|> Map.merge(Keyword.get(opts, :extra, %{}))
199-
200-
user =
201-
user_context
202-
|> Map.merge(Keyword.get(opts, :user, %{}))
243+
extra = Map.merge(extra_context, Keyword.get(opts, :extra, %{}))
244+
user = Map.merge(user_context, Keyword.get(opts, :user, %{}))
245+
request = Map.merge(request_context, Keyword.get(opts, :request, %{}))
203246

204247
tags =
205248
Config.tags()
206249
|> Map.merge(tags_context)
207250
|> Map.merge(Keyword.get(opts, :tags, %{}))
208251

209-
request =
210-
request_context
211-
|> Map.merge(Keyword.get(opts, :request, %{}))
212-
213252
breadcrumbs =
214253
Keyword.get(opts, :breadcrumbs, [])
215254
|> Kernel.++(breadcrumbs_context)
@@ -218,31 +257,30 @@ defmodule Sentry.Event do
218257

219258
message = Keyword.get(opts, :message)
220259
exception = Keyword.get(opts, :exception)
260+
stacktrace = Keyword.get(opts, :stacktrace)
261+
source = Keyword.get(opts, :event_source)
221262

222263
%__MODULE__{
223-
event_id: UUID.uuid4_hex(),
224-
timestamp: timestamp,
225-
level: Keyword.get(opts, :level, :error),
226-
server_name: Config.server_name() || to_string(:net_adm.localhost()),
227-
release: Config.release(),
228-
sdk: @sdk,
229-
tags: tags,
230-
modules:
231-
Enum.reduce(@deps, %{}, fn app, acc ->
232-
Map.put(acc, app, to_string(Application.spec(app, :vsn)))
233-
end),
234-
culprit: culprit_from_stacktrace(Keyword.get(opts, :stacktrace, [])),
235-
extra: extra,
236264
breadcrumbs: breadcrumbs,
237265
contexts: generate_contexts(),
238-
exception: coerce_exception(exception, Keyword.get(opts, :stacktrace), message),
239-
message: message,
240-
fingerprint: fingerprint,
266+
culprit: culprit_from_stacktrace(Keyword.get(opts, :stacktrace, [])),
241267
environment: Config.environment_name(),
242-
user: user,
268+
event_id: UUID.uuid4_hex(),
269+
exception: List.wrap(coerce_exception(exception, stacktrace, message)),
270+
extra: extra,
271+
fingerprint: fingerprint,
272+
level: level,
273+
message: message,
274+
modules: Map.new(@deps, &{&1, to_string(Application.spec(&1, :vsn))}),
275+
original_exception: exception,
276+
release: Config.release(),
243277
request: request,
244-
__source__: Keyword.get(opts, :event_source),
245-
__original_exception__: exception
278+
sdk: @sdk,
279+
server_name: Config.server_name() || to_string(:net_adm.localhost()),
280+
source: source,
281+
tags: tags,
282+
timestamp: timestamp,
283+
user: user
246284
}
247285
end
248286

lib/sentry/transport/sender.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ defmodule Sentry.Transport.Sender do
8888
end
8989

9090
defp maybe_log_send_result(send_result, events) do
91-
if Enum.any?(events, &(&1.__source__ == :logger)) do
91+
if Enum.any?(events, &(&1.source == :logger)) do
9292
:ok
9393
else
9494
message =

0 commit comments

Comments
 (0)