-
-
Notifications
You must be signed in to change notification settings - Fork 198
/
Copy pathsentry.ex
490 lines (384 loc) · 18.6 KB
/
sentry.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
defmodule Sentry do
@moduledoc """
Provides the functionality to submit events to [Sentry](https://sentry.io).
This library can be used to submit events to Sentry from any Elixir application.
It supports several ways of reporting events:
* Manually — see `capture_exception/2` and `capture_message/2`.
* Through an [Erlang `:logger`](https://www.erlang.org/doc/apps/kernel/logger.html) handler —
see `Sentry.LoggerHandler`.
* Through an Elixir `Logger` backend — see `Sentry.LoggerBackend`.
* Automatically for Plug/Phoenix applications — see the
[*Setup with Plug and Phoenix* guide](setup-with-plug-and-phoenix.html), and the
`Sentry.PlugCapture` and `Sentry.PlugContext` modules.
* Through integrations for various ecosystem tools, like [Oban](oban-integration.html)
or [Quantum](quantum-integration.html).
## Usage
Add the following to your production configuration:
# In config/prod.exs
config :sentry, dsn: "https://public:[email protected]/1",
environment_name: :prod,
tags: %{
env: "production"
}
Sentry uses the `:dsn` option to determine whether it should record exceptions. If
`:dsn` is set, then Sentry records exceptions. If it's not set or set to `nil`,
then simply no events are sent to Sentry.
> #### Included Environments {: .warning}
>
> Before v10.0.0, the recommended way to control whether to report events to Sentry
> was the `:included_environments` option (a list of environments to report events for).
> This was used together with the `:environment_name` option to determine whether to
> send events. `:included_environments` is deprecated in v10.0.0 in favor of setting
> or not setting `:dsn`. It will be removed in v11.0.0.
You can even rely on more specific logic to determine the environment name. It's
not uncommon for most applications to have a "staging" environment. In order
to handle this without adding an additional Mix environment, you can set an
environment variable that determines the release level. By default, Sentry
picks up the `SENTRY_ENVIRONMENT` variable (*at runtime, when starging*).
Otherwise, you can read the variable at runtime. Do this only in
`config/runtime.exs` so that it will work both for local development as well
as Mix releases.
# In config/runtime.exs
if config_env() == :prod do
config :sentry, dsn: "https://public:[email protected]/1",
environment_name: System.fetch_env!("RELEASE_LEVEL")
end
In this example, we are getting the environment name from the `RELEASE_LEVEL`
environment variable. Now, on our servers, we can set the environment variable
appropriately. The `config_env() == :prod` check ensures that we only set
`:dsn` in production, effectively only enabling reporting in production-like
environments.
Sentry supports many configuration options. See the [*Configuration*
section](#module-configuration) for complete documentation.
## Configuration
*See also the [official Sentry
documentation](https://docs.sentry.io/platforms/elixir/configuration/).*
You can configure Sentry through the application environment, under the `:sentry` application.
For example, you can do this in `config/config.exs`:
# config/config.exs
config :sentry,
# ...
**Sentry reads the configuration when the `:sentry` application starts**, and
will not pick up any changes after that. This is in line with how other
Sentry SDKs (and many other Erlang/Elixir libraries) work. The reason
for this choice is performance: the SDK performs validation on application
start and then caches the configuration (in [`:persistent_term`](`:persistent_term`)).
> #### Updating Configuration at Runtime {: .tip}
>
> If you *must* update configuration at runtime, use `put_config/2`. This
> function is not efficient (since it updates terms in `:persistent_term`),
> but it works in a pinch. For example, it's useful if you're verifying
> that you send the right events to Sentry in your test suite, so you need to
> change the `:dsn` configuration to point to a local server that you can verify
> requests on.
Below you can find all the available configuration options.
#{Sentry.Config.docs()}
> #### Configuration Through System Environment {: .info}
>
> Sentry supports loading some configuration from the system environment.
> The supported environment variables are: `SENTRY_RELEASE`, `SENTRY_ENVIRONMENT`,
> and `SENTRY_DSN`. See the `:release`, `:environment_name`, and `:dsn` configuration
> options respectively for more information.
## Filtering Exceptions
If you would like to prevent Sentry from sending certain exceptions, you can
use the `:before_send` configuration option. See the [*Event Callbacks*
section](#module-event-callbacks) below.
Before v9.0.0, the recommended way to filter out exceptions was to use a *filter*,
that is, a module implementing the `Sentry.EventFilter` behaviour. This is still supported,
but is now deprecated. See `Sentry.EventFilter` for more information.
## Event Callbacks
You can configure the `:before_send` and `:after_send_event` options to
customize what happens before and/or after sending an event. The `:before_send`
callback must be of type `t:before_send_event_callback/0` and the `:after_send_event`
callback must be of type `t:after_send_event_callback/0`. For example, you
can set:
config :sentry,
before_send: {MyModule, :before_send},
after_send_event: {MyModule, :after_send}
`MyModule` could look like this:
defmodule MyModule do
def before_send(event) do
metadata = Map.new(Logger.metadata())
%Sentry.Event{event | extra: Map.merge(event.extra, metadata)}
end
def after_send_event(event, result) do
case result do
{:ok, id} ->
Logger.info("Successfully sent event!")
{:error, _reason} ->
Logger.info(fn -> "Did not successfully send event! \#{inspect(event)}" end)
end
end
end
## Reporting Source Code
Sentry supports reporting the source code of (and around) the line that
caused an issue. An example configuration to enable this functionality is:
config :sentry,
dsn: "https://public:[email protected]/1",
enable_source_code_context: true,
root_source_code_paths: [File.cwd!()],
context_lines: 5
To support this functionality, Sentry needs to **package** source code
and store it so that it's available in the compiled application. Packaging source
code is an active step you have to take; use the [`mix
sentry.package_source_code`](`Mix.Tasks.Sentry.PackageSourceCode`) Mix task to do that.
Sentry stores the packaged source code in its `priv` directory. This is included by
default in [Mix releases](`Mix.Tasks.Release`). Once the source code is packaged
and ready to ship with your release, Sentry will load it when the `:sentry` application
starts. If there are issues with loading the packaged code, Sentry will log some warnings
but will boot up normally and it just won't report source code context.
> #### Prune Large File Trees {: .tip}
>
> Due to Sentry reading the file system and defaulting to a recursive search
> of directories, it is important to check your configuration and compilation
> environment to avoid a folder recursion issue. You might see problems when
> deploying to the root folder, so it is best to follow the practice of
> compiling your application in its own folder. Modifying the
> `:source_code_path_pattern` configuration option from its default is also
> an avenue to avoid compile problems, as well as pruning unnecessary files
> with `:source_code_exclude_patterns`.
"""
alias Sentry.{CheckIn, Client, ClientError, ClientReport, Config, Event, LoggerUtils, Options}
require Logger
@typedoc """
A callback to use with the `:before_send` configuration option.
configuration options.k
If this is `{module, function_name}`, then `module.function_name(event)` will
be called, where `event` is of type `t:Sentry.Event.t/0`.
See the [*Configuration* section](#module-configuration) in the module documentation
for more information on configuration.
"""
@typedoc since: "9.0.0"
@type before_send_event_callback() ::
(Sentry.Event.t() -> as_boolean(Sentry.Event.t()))
| {module(), function_name :: atom()}
@typedoc """
A callback to use with the `:after_send_event` configuration option.
If this is `{module, function_name}`, then `module.function_name(event, result)` will
be called, where `event` is of type `t:Sentry.Event.t/0`.
"""
@typedoc since: "9.0.0"
@type after_send_event_callback() ::
(Sentry.Event.t(), result :: term() -> term())
| {module(), function_name :: atom()}
@typedoc """
The strategy to use when sending an event to Sentry.
"""
@typedoc since: "9.0.0"
@type send_type() :: :sync | :none
@type send_result() ::
{:ok, event_or_envelope_id :: String.t()}
| {:error, ClientError.t()}
| :ignored
| :unsampled
| :excluded
@doc """
Parses and submits an exception to Sentry.
This only sends the exception if the `:dsn` configuration option is set
and is not `nil`. See the [*Configuration* section](#module-configuration)
in the module documentation.
## Options
#{Options.docs_for(:capture_exception)}
"""
@spec capture_exception(Exception.t(), keyword()) :: send_result()
def capture_exception(exception, options \\ []) do
filter_module = Config.filter()
event_source = Keyword.get(options, :event_source)
{send_opts, create_event_opts} = Options.split_send_event_options(options)
if filter_module.exclude_exception?(exception, event_source) do
:excluded
else
exception
|> Event.transform_exception(create_event_opts)
|> send_event(send_opts)
end
end
@doc """
Puts the last event ID sent to the server for the current process in
the process dictionary.
"""
@spec put_last_event_id_and_source(String.t()) :: {String.t(), atom() | nil} | nil
def put_last_event_id_and_source(event_id, source \\ nil) when is_binary(event_id) do
Process.put(:sentry_last_event_id_and_source, {event_id, source})
end
@doc """
Gets the last event ID sent to the server from the process dictionary.
Since it uses the process dictionary, it will only return the last event
ID sent within the current process.
"""
@spec get_last_event_id_and_source() :: {String.t(), atom() | nil} | nil
def get_last_event_id_and_source do
Process.get(:sentry_last_event_id_and_source)
end
@doc """
Reports a message to Sentry.
## Options
#{Options.docs_for(:capture_message)}
## Interpolation (since v10.1.0)
The `message` argument supports interpolation. You can pass a string with formatting
markers as `%s`, and then pass in the `:interpolation_parameters` option as a list
of positional parameters to interpolate. For example:
Sentry.capture_message("Error with user %s", interpolation_parameters: ["John"])
This way, Sentry will group the messages based on the non-interpolated string, but it
will show the interpolated string in the UI.
> #### Missing or Extra Parameters {: .neutral}
>
> If the message string has more `%s` markers than parameters, the extra `%s` markers
> are included as is and the SDK doesn't raise any error. If you pass in more interpolation
> parameters than `%s` markers, the extra parameters are ignored as well. This is because
> the SDK doesn't want to be the cause of even more errors in your application when what
> you're trying to do is report an error in the first place.
"""
@spec capture_message(String.t(), keyword()) :: send_result
def capture_message(message, opts \\ []) when is_binary(message) do
{send_opts, create_event_opts} =
opts
|> Keyword.put(:message, message)
|> Options.split_send_event_options()
event = Event.create_event(create_event_opts)
send_event(event, send_opts)
end
@doc """
Sends an event to Sentry.
An **event** is the most generic payload you can send to Sentry. It encapsulates
information about an exception, a message, or any other event that you want to
report. To manually build events, see the functions in `Sentry.Event`.
This function doesn't build the event for you, it only *sends* it. Most of the time,
you'll want to use `capture_exception/2` or `capture_message/2`. To manually create events,
see `Sentry.Event.create_event/1`.
## Options
#{Options.docs_for(:send_event)}
> #### Async Send {: .error}
>
> Before v9.0.0 of this library, the `:result` option also supported the `:async` value.
> This would spawn a `Task` to make the API call, and would return a `{:ok, Task.t()}` tuple.
> You could use `Task` operations to wait for the result asynchronously. Since v9.0.0, this
> option is not present anymore. Instead, you can spawn a task yourself that then calls this
> function with `result: :sync`. The effect is exactly the same.
> #### Sending Exceptions and Messages {: .tip}
>
> This function is **low-level**, and mostly intended for library developers,
> or folks that want to have full control on what they report to Sentry. For most
> use cases, use `capture_exception/2` or `capture_message/2`.
"""
@spec send_event(Event.t(), keyword()) :: send_result
def send_event(event, options \\ []) do
# TODO: remove on v11.0.0, :included_environments was deprecated in 10.0.0.
included_envs = Config.included_environments()
cond do
is_nil(event.message) and event.exception == [] ->
LoggerUtils.log("Cannot report event without message or exception: #{inspect(event)}")
ClientReport.Sender.record_discarded_events(:event_processor, [event])
:ignored
# If we're in test mode, let's send the event down the pipeline anyway.
Config.test_mode?() ->
Client.send_event(event, options)
!Config.dsn() ->
# We still validate options even if we're not sending the event. This aims at catching
# configuration issues during development instead of only when deploying to production.
_options = NimbleOptions.validate!(options, Options.send_event_schema())
:ignored
included_envs == :all or to_string(Config.environment_name()) in included_envs ->
Client.send_event(event, options)
true ->
:ignored
end
end
def send_transaction(transaction, options \\ []) do
# TODO: remove on v11.0.0, :included_environments was deprecated in 10.0.0.
included_envs = Config.included_environments()
cond do
Config.test_mode?() ->
Client.send_transaction(transaction, options)
!Config.dsn() ->
# We still validate options even if we're not sending the event. This aims at catching
# configuration issues during development instead of only when deploying to production.
_options = NimbleOptions.validate!(options, Options.send_event_schema())
:ignored
included_envs == :all or to_string(Config.environment_name()) in included_envs ->
Client.send_transaction(transaction, options)
true ->
:ignored
end
end
@doc """
Captures a check-in built with the given `options`.
Check-ins are used to report the status of a monitor to Sentry. This is used
to track the health and progress of **cron jobs**. This function is somewhat
low level, and mostly useful when you want to report the status of a cron
but you are not using any common library to manage your cron jobs.
This function performs a *synchronous* HTTP request to Sentry. If the request
performs successfully, it returns `{:ok, check_in_id}` where `check_in_id` is
the ID of the check-in that was sent to Sentry. You can use this ID to send
updates about the same check-in. If the request fails, it returns
`{:error, reason}`.
> #### Setting the DSN {: .warning}
>
> If the `:dsn` configuration is not set, this function won't report the check-in
> to Sentry and will instead return `:ignored`. This behaviour is consistent with
> the rest of the SDK (such as `capture_exception/2`).
## Examples
Say you have a GenServer which periodically sends a message to itself to execute some
job. You could monitor the health of this GenServer by reporting a check-in to Sentry.
For example:
@impl GenServer
def handle_info(:execute_periodic_job, state) do
# Report that the job started.
{:ok, check_in_id} = Sentry.capture_check_in(status: :in_progress, monitor_slug: "genserver-job")
:ok = do_job(state)
# Report that the job ended successfully.
Sentry.capture_check_in(check_in_id: check_in_id, status: :ok, monitor_slug: "genserver-job")
{:noreply, state}
end
"""
@doc since: "10.2.0"
@spec capture_check_in(keyword()) ::
{:ok, check_in_id :: String.t()} | :ignored | {:error, ClientError.t()}
def capture_check_in(options) when is_list(options) do
if Config.dsn() do
options
|> CheckIn.new()
|> Client.send_check_in(options)
else
:ignored
end
end
@doc ~S"""
Updates the value of `key` in the configuration *at runtime*.
Once the `:sentry` application starts, it validates and caches the value of the
configuration options you start it with. Because of this, updating configuration
at runtime requires this function as opposed to just changing the application
environment.
> #### This Function Is Slow {: .warning}
>
> This function updates terms in [`:persistent_term`](`:persistent_term`), which is what
> this SDK uses to cache configuration. Updating terms in `:persistent_term` is slow
> and can trigger full GC sweeps. We recommend only using this function in rare cases,
> or during tests.
## Examples
For example, if you're using [`Bypass`](https://github.com/PSPDFKit-labs/bypass) to test
that you send the correct events to Sentry:
test "reports the correct event to Sentry" do
bypass = Bypass.open()
Bypass.expect(...)
Sentry.put_config(:dsn, "http://public:secret@localhost:#{bypass.port}/1")
Sentry.put_config(:send_result, :sync)
my_function_to_test()
end
"""
@doc since: "10.0.0"
@spec put_config(atom(), term()) :: :ok
defdelegate put_config(key, value), to: Config
@doc """
Returns the currently-set Sentry DSN, *if set* (or `nil` otherwise).
This is useful in situations like capturing user feedback.
"""
@doc since: "10.6.0"
@spec get_dsn() :: String.t() | nil
def get_dsn do
case Config.dsn() do
%Sentry.DSN{original_dsn: original_dsn} -> original_dsn
nil -> nil
end
end
end