Skip to content

Commit a8e67d8

Browse files
authored
Improve option validation in functions (#671)
1 parent de0bbb1 commit a8e67d8

File tree

7 files changed

+219
-93
lines changed

7 files changed

+219
-93
lines changed

lib/sentry.ex

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ defmodule Sentry do
171171
> with `:source_code_exclude_patterns`.
172172
"""
173173

174-
alias Sentry.{Config, Event, LoggerUtils}
174+
alias Sentry.{Client, Config, Event, LoggerUtils}
175175

176176
require Logger
177177

@@ -227,13 +227,14 @@ defmodule Sentry do
227227
def capture_exception(exception, opts \\ []) do
228228
filter_module = Config.filter()
229229
event_source = Keyword.get(opts, :event_source)
230+
{send_opts, create_event_opts} = Client.split_send_event_opts(opts)
230231

231232
if filter_module.exclude_exception?(exception, event_source) do
232233
:excluded
233234
else
234235
exception
235-
|> Event.transform_exception(opts)
236-
|> send_event(opts)
236+
|> Event.transform_exception(create_event_opts)
237+
|> send_event(send_opts)
237238
end
238239
end
239240

@@ -263,10 +264,13 @@ defmodule Sentry do
263264
"""
264265
@spec capture_message(String.t(), keyword()) :: send_result
265266
def capture_message(message, opts \\ []) when is_binary(message) do
266-
opts
267-
|> Keyword.put(:message, message)
268-
|> Event.create_event()
269-
|> send_event(opts)
267+
{send_opts, create_event_opts} =
268+
opts
269+
|> Keyword.put(:message, message)
270+
|> Client.split_send_event_opts()
271+
272+
event = Event.create_event(create_event_opts)
273+
send_event(event, send_opts)
270274
end
271275

272276
@doc """
@@ -278,30 +282,7 @@ defmodule Sentry do
278282
279283
## Options
280284
281-
The supported options are:
282-
283-
* `:result` - Allows specifying how the result should be returned. The possible values are:
284-
285-
* `:sync` - Sentry will make an API call synchronously (including retries) and will
286-
return `{:ok, event_id}` if successful.
287-
288-
* `:none` - Sentry will send the event in the background, in a *fire-and-forget*
289-
fashion. The function will return `{:ok, ""}` regardless of whether the API
290-
call ends up being successful or not.
291-
292-
* `:async` - **Not supported anymore**, see the information below.
293-
294-
* `:sample_rate` - same as the global `:sample_rate` configuration, but applied only to
295-
this call. See the module documentation. *Available since v10.0.0*.
296-
297-
* `:before_send` - same as the global `:before_send` configuration, but
298-
applied only to this call. See the module documentation. *Available since v10.0.0*.
299-
300-
* `:after_send_event` - same as the global `:after_send_event` configuration, but
301-
applied only to this call. See the module documentation. *Available since v10.0.0*.
302-
303-
* `:client` - same as the global `:client` configuration, but
304-
applied only to this call. See the module documentation. *Available since v10.0.0*.
285+
#{NimbleOptions.docs(Client.send_events_opts_schema())}
305286
306287
> #### Async Send {: .error}
307288
>
@@ -331,7 +312,7 @@ defmodule Sentry do
331312
:ignored
332313

333314
included_envs == :all or to_string(Config.environment_name()) in included_envs ->
334-
Sentry.Client.send_event(event, opts)
315+
Client.send_event(event, opts)
335316

336317
true ->
337318
:ignored

lib/sentry/client.ex

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,79 @@ defmodule Sentry.Client do
1212
# Max message length per https://github.com/getsentry/sentry/blob/0fcec33ac94ad81a205f86f208072b0f57b39ff4/src/sentry/conf/server.py#L1021
1313
@max_message_length 8_192
1414

15+
# The docs for the options here are generated in the Sentry module, so you can refer to types
16+
# and functions and so on like if you were writing these docs in the Sentry module itself.
17+
send_event_opts_schema = [
18+
result: [
19+
type: {:in, [:sync, :none]},
20+
doc: """
21+
Allows specifying how the result should be returned. The possible values are:
22+
23+
* `:sync` - Sentry will make an API call synchronously (including retries) and will
24+
return `{:ok, event_id}` if successful.
25+
26+
* `:none` - Sentry will send the event in the background, in a *fire-and-forget*
27+
fashion. The function will return `{:ok, ""}` regardless of whether the API
28+
call ends up being successful or not.
29+
"""
30+
],
31+
sample_rate: [
32+
type: :float,
33+
doc: """
34+
Same as the global `:sample_rate` configuration, but applied only to
35+
this call. See the module documentation. *Available since v10.0.0*.
36+
"""
37+
],
38+
before_send: [
39+
type: {:or, [{:fun, 1}, {:tuple, [:atom, :atom]}]},
40+
type_doc: "`t:before_send_event_callback/0`",
41+
doc: """
42+
Same as the global `:before_send` configuration, but
43+
applied only to this call. See the module documentation. *Available since v10.0.0*.
44+
"""
45+
],
46+
after_send_event: [
47+
type: {:or, [{:fun, 2}, {:tuple, [:atom, :atom]}]},
48+
type_doc: "`t:after_send_event_callback/1`",
49+
doc: """
50+
Same as the global `:after_send_event` configuration, but
51+
applied only to this call. See the module documentation. *Available since v10.0.0*.
52+
"""
53+
],
54+
client: [
55+
type: :atom,
56+
type_doc: "`t:module/0`",
57+
doc: """
58+
Same as the global `:client` configuration, but
59+
applied only to this call. See the module documentation. *Available since v10.0.0*.
60+
"""
61+
],
62+
63+
# Private options, only used in testing.
64+
request_retries: [
65+
type: {:list, :integer},
66+
doc: false
67+
]
68+
]
69+
70+
@send_event_opts_schema NimbleOptions.new!(send_event_opts_schema)
71+
@send_event_opts_keys Keyword.keys(send_event_opts_schema)
72+
73+
@spec send_events_opts_schema() :: NimbleOptions.t()
74+
def send_events_opts_schema, do: @send_event_opts_schema
75+
76+
@spec split_send_event_opts(keyword()) :: {keyword(), keyword()}
77+
def split_send_event_opts(options) when is_list(options) do
78+
Keyword.split(options, @send_event_opts_keys)
79+
end
80+
1581
# This is what executes the "Event Pipeline".
1682
# See: https://develop.sentry.dev/sdk/unified-api/#event-pipeline
1783
@spec send_event(Event.t(), keyword()) ::
1884
{:ok, event_id :: String.t()} | {:error, term()} | :unsampled | :excluded
1985
def send_event(%Event{} = event, opts) when is_list(opts) do
86+
opts = NimbleOptions.validate!(opts, @send_event_opts_schema)
87+
2088
result_type = Keyword.get_lazy(opts, :result, &Config.send_result/0)
2189
sample_rate = Keyword.get_lazy(opts, :sample_rate, &Config.sample_rate/0)
2290
before_send = Keyword.get_lazy(opts, :before_send, &Config.before_send/0)

lib/sentry/event.ex

Lines changed: 127 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -135,61 +135,129 @@ defmodule Sentry.Event do
135135
|> Map.drop([:original_exception, :source])
136136
end
137137

138-
@doc """
139-
Creates an event struct out of collected context and options.
140-
141-
> #### Merging Options with Context and Config {: .info}
142-
>
143-
> Some of the options documented below are **merged** with the Sentry context, or
144-
> with the Sentry context *and* the configuration. The option you pass here always
145-
> has higher precedence, followed by the context and finally by the configuration.
146-
>
147-
> See also `Sentry.Context` for information on the Sentry context and `Sentry` for
148-
> information on configuration.
149-
150-
## Options
151-
152-
* `:exception` - an `t:Exception.t/0`. This is the exception that gets reported in the
138+
create_event_opts_schema = [
139+
exception: [
140+
type: {:custom, __MODULE__, :__validate_exception__, [:exception]},
141+
type_doc: "`t:Exception.t/0`",
142+
doc: """
143+
This is the exception that gets reported in the
153144
`:exception` field of `t:t/0`. The term passed here also ends up unchanged in the
154145
`:original_exception` field of `t:t/0`. This option is **required** unless the
155-
`:message` option is present. This is not present by default.
156-
157-
* `:stacktrace` - a stacktrace, as in `t:Exception.stacktrace/0`. This is not present
158-
by default.
159-
160-
* `:message` - a message (`t:String.t/0`). This is not present by default.
161-
162-
* `:extra` - map of extra context, which gets merged with the current context
146+
`:message` option is present. Not present by default.
147+
"""
148+
],
149+
stacktrace: [
150+
type: {:list, :any},
151+
type_doc: "`t:Exception.stacktrace/0`",
152+
doc: """
153+
The exception's stacktrace. This can also be used with messages (`:message`). Not
154+
present by default.
155+
"""
156+
],
157+
message: [
158+
type: :string,
159+
doc: """
160+
A message to report. The string can contain interpolation markers (`%s`). In that
161+
case, you can pass the `:interpolation_parameters` option as well to fill
162+
in those parameters. See `Sentry.capture_message/2` for more information on
163+
message interpolation. Not present by default.
164+
"""
165+
],
166+
extra: [
167+
type: {:map, {:or, [:atom, :string]}, :any},
168+
type_doc: "`t:Sentry.Context.extra/0`",
169+
default: %{},
170+
doc: """
171+
Map of extra context, which gets merged with the current context
163172
(see `Sentry.Context.set_extra_context/1`). If fields collide, the ones
164173
in the map passed through this option have precedence over the ones in
165-
the context. Defaults to `%{}`.
166-
167-
* `:user` - map of user context, which gets merged with the current context
174+
the context.
175+
"""
176+
],
177+
user: [
178+
type: :map,
179+
type_doc: "`t:Sentry.Context.user_context/0`",
180+
default: %{},
181+
doc: """
182+
Map of user context, which gets merged with the current context
168183
(see `Sentry.Context.set_user_context/1`). If fields collide, the ones
169184
in the map passed through this option have precedence over the ones in
170-
the context. Defaults to `%{}`.
171-
172-
* `:tags` - map of tags context, which gets merged with the current context (see
185+
the context.
186+
"""
187+
],
188+
tags: [
189+
type: {:map, {:or, [:atom, :string]}, :any},
190+
type_doc: "`t:Sentry.Context.tags/0`",
191+
default: %{},
192+
doc: """
193+
Map of tags context, which gets merged with the current context (see
173194
`Sentry.Context.set_tags_context/1`) and with the `:tags` option in the global
174195
Sentry configuration. If fields collide, the ones in the map passed through
175196
this option have precedence over the ones in the context, which have precedence
176-
over the ones in the configuration. Defaults to `%{}`.
177-
178-
* `:request` - map of request context, which gets merged with the current context
197+
over the ones in the configuration.
198+
"""
199+
],
200+
request: [
201+
type: :map,
202+
type_doc: "`t:Sentry.Context.request_context/0`",
203+
default: %{},
204+
doc: """
205+
Map of request context, which gets merged with the current context
179206
(see `Sentry.Context.set_request_context/1`). If fields collide, the ones
180207
in the map passed through this option have precedence over the ones in
181-
the context. Defaults to `%{}`.
208+
the context.
209+
"""
210+
],
211+
breadcrumbs: [
212+
type: {:list, {:or, [:keyword_list, :map]}},
213+
type_doc: "list of `t:keyword/0` or `t:Sentry.Context.breadcrumb/0`",
214+
default: [],
215+
doc: """
216+
List of breadcrumbs. This list gets **prepended** to the list
217+
in the context (see `Sentry.Context.add_breadcrumb/1`).
218+
"""
219+
],
220+
level: [
221+
type: {:in, [:fatal, :error, :warning, :info, :debug]},
222+
type_doc: "`t:level/0`",
223+
default: :error,
224+
doc: """
225+
The level of the event.
226+
"""
227+
],
228+
fingerprint: [
229+
type: {:list, :string},
230+
default: ["{{ default }}"],
231+
doc: """
232+
List of the fingerprint for grouping this event.
233+
"""
234+
],
235+
event_source: [
236+
type: :atom,
237+
doc: """
238+
The source of the event. This fills in the `:source` field of the
239+
returned struct. This is not present by default.
240+
"""
241+
]
242+
]
182243

183-
* `:breadcrumbs` - list of breadcrumbs. This list gets **prepended** to the list
184-
in the context (see `Sentry.Context.add_breadcrumb/1`). Defaults to `[]`.
244+
@create_event_opts_schema NimbleOptions.new!(create_event_opts_schema)
185245

186-
* `:level` - error level (see `t:t/0`). Defaults to `:error`.
246+
@doc """
247+
Creates an event struct out of collected context and options.
187248
188-
* `:fingerprint` - list of the fingerprint for grouping this event (a list
189-
of `t:String.t/0`). Defaults to `["{{ default }}"]`.
249+
> #### Merging Options with Context and Config {: .info}
250+
>
251+
> Some of the options documented below are **merged** with the Sentry context, or
252+
> with the Sentry context *and* the configuration. The option you pass here always
253+
> has higher precedence, followed by the context and finally by the configuration.
254+
>
255+
> See also `Sentry.Context` for information on the Sentry context and `Sentry` for
256+
> information on configuration.
190257
191-
* `:event_source` - the source of the event. This fills in the `:source` field of the
192-
returned struct. This is not present by default.
258+
## Options
259+
260+
#{NimbleOptions.docs(@create_event_opts_schema)}
193261
194262
## Examples
195263
@@ -220,6 +288,8 @@ defmodule Sentry.Event do
220288
| {:exception, Exception.t()}
221289
| {:stacktrace, Exception.stacktrace()}
222290
def create_event(opts) when is_list(opts) do
291+
opts = NimbleOptions.validate!(opts, @create_event_opts_schema)
292+
223293
timestamp =
224294
DateTime.utc_now()
225295
|> DateTime.truncate(:microsecond)
@@ -234,20 +304,18 @@ defmodule Sentry.Event do
234304
request: request_context
235305
} = Sentry.Context.get_all()
236306

237-
level = Keyword.get(opts, :level, :error)
238-
fingerprint = Keyword.get(opts, :fingerprint, ["{{ default }}"])
239-
240-
extra = Map.merge(extra_context, Keyword.get(opts, :extra, %{}))
241-
user = Map.merge(user_context, Keyword.get(opts, :user, %{}))
242-
request = Map.merge(request_context, Keyword.get(opts, :request, %{}))
307+
extra = Map.merge(extra_context, Keyword.fetch!(opts, :extra))
308+
user = Map.merge(user_context, Keyword.fetch!(opts, :user))
309+
request = Map.merge(request_context, Keyword.fetch!(opts, :request))
243310

244311
tags =
245312
Config.tags()
246313
|> Map.merge(tags_context)
247-
|> Map.merge(Keyword.get(opts, :tags, %{}))
314+
|> Map.merge(Keyword.fetch!(opts, :tags))
248315

249316
breadcrumbs =
250-
Keyword.get(opts, :breadcrumbs, [])
317+
opts
318+
|> Keyword.fetch!(:breadcrumbs)
251319
|> Kernel.++(breadcrumbs_context)
252320
|> Enum.take(-1 * Config.max_breadcrumbs())
253321
|> Enum.map(&struct(Interfaces.Breadcrumb, &1))
@@ -265,8 +333,8 @@ defmodule Sentry.Event do
265333
event_id: UUID.uuid4_hex(),
266334
exception: List.wrap(coerce_exception(exception, stacktrace, message)),
267335
extra: extra,
268-
fingerprint: fingerprint,
269-
level: level,
336+
fingerprint: Keyword.fetch!(opts, :fingerprint),
337+
level: Keyword.fetch!(opts, :level),
270338
message: message,
271339
modules: :persistent_term.get({:sentry, :loaded_applications}),
272340
original_exception: exception,
@@ -491,4 +559,13 @@ defmodule Sentry.Event do
491559
event.fingerprint
492560
])
493561
end
562+
563+
@doc false
564+
def __validate_exception__(term, key) do
565+
if is_exception(term) do
566+
{:ok, term}
567+
else
568+
{:error, "expected #{inspect(key)} to be an exception, got: #{inspect(term)}"}
569+
end
570+
end
494571
end

0 commit comments

Comments
 (0)