Skip to content

Commit 11d2662

Browse files
committed
Add support for sending transactions
- Add Transaction struct - Add Sentry.send_transaction - Add support for collecting transactions in tests
1 parent c565390 commit 11d2662

11 files changed

Lines changed: 504 additions & 50 deletions

File tree

lib/sentry.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,28 @@ defmodule Sentry do
362362
end
363363
end
364364

365+
def send_transaction(transaction, options \\ []) do
366+
# TODO: remove on v11.0.0, :included_environments was deprecated in 10.0.0.
367+
included_envs = Config.included_environments()
368+
369+
cond do
370+
Config.test_mode?() ->
371+
Client.send_transaction(transaction, options)
372+
373+
!Config.dsn() ->
374+
# We still validate options even if we're not sending the event. This aims at catching
375+
# configuration issues during development instead of only when deploying to production.
376+
_options = NimbleOptions.validate!(options, Options.send_event_schema())
377+
:ignored
378+
379+
included_envs == :all or to_string(Config.environment_name()) in included_envs ->
380+
Client.send_transaction(transaction, options)
381+
382+
true ->
383+
:ignored
384+
end
385+
end
386+
365387
@doc """
366388
Captures a check-in built with the given `options`.
367389

lib/sentry/client.ex

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ defmodule Sentry.Client do
1515
Event,
1616
Interfaces,
1717
LoggerUtils,
18-
Transport,
19-
Options
18+
Options,
19+
Transaction,
20+
Transport
2021
}
2122

2223
require Logger
@@ -107,6 +108,26 @@ defmodule Sentry.Client do
107108
|> Transport.encode_and_post_envelope(client, request_retries)
108109
end
109110

111+
def send_transaction(%Transaction{} = transaction, opts \\ []) do
112+
opts = NimbleOptions.validate!(opts, Options.send_transaction_schema())
113+
114+
result_type = Keyword.get_lazy(opts, :result, &Config.send_result/0)
115+
client = Keyword.get_lazy(opts, :client, &Config.client/0)
116+
117+
request_retries =
118+
Keyword.get_lazy(opts, :request_retries, fn ->
119+
Application.get_env(:sentry, :request_retries, Transport.default_retries())
120+
end)
121+
122+
case encode_and_send(transaction, result_type, client, request_retries) do
123+
{:ok, id} ->
124+
{:ok, id}
125+
126+
{:error, %ClientError{} = error} ->
127+
{:error, error}
128+
end
129+
end
130+
110131
defp sample_event(sample_rate) do
111132
cond do
112133
sample_rate == 1 -> :ok
@@ -205,6 +226,42 @@ defmodule Sentry.Client do
205226
end
206227
end
207228

229+
defp encode_and_send(
230+
%Transaction{} = transaction,
231+
_result_type = :sync,
232+
client,
233+
request_retries
234+
) do
235+
case Sentry.Test.maybe_collect(transaction) do
236+
:collected ->
237+
{:ok, ""}
238+
239+
:not_collecting ->
240+
send_result =
241+
transaction
242+
|> Envelope.from_transaction()
243+
|> Transport.encode_and_post_envelope(client, request_retries)
244+
245+
send_result
246+
end
247+
end
248+
249+
defp encode_and_send(
250+
%Transaction{} = transaction,
251+
_result_type = :none,
252+
client,
253+
_request_retries
254+
) do
255+
case Sentry.Test.maybe_collect(transaction) do
256+
:collected ->
257+
{:ok, ""}
258+
259+
:not_collecting ->
260+
:ok = Transport.Sender.send_async(client, transaction)
261+
{:ok, ""}
262+
end
263+
end
264+
208265
@spec render_event(Event.t()) :: map()
209266
def render_event(%Event{} = event) do
210267
json_library = Config.json_library()
@@ -225,6 +282,19 @@ defmodule Sentry.Client do
225282
|> update_if_present(:threads, fn list -> Enum.map(list, &render_thread/1) end)
226283
end
227284

285+
@spec render_transaction(%Transaction{}) :: map()
286+
def render_transaction(%Transaction{} = transaction) do
287+
transaction
288+
|> Transaction.to_map()
289+
|> Map.merge(%{
290+
platform: "elixir",
291+
sdk: %{
292+
name: "sentry.elixir",
293+
version: Application.spec(:sentry, :vsn)
294+
}
295+
})
296+
end
297+
228298
defp render_exception(%Interfaces.Exception{} = exception) do
229299
exception
230300
|> Map.from_struct()

lib/sentry/envelope.ex

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ defmodule Sentry.Envelope do
22
@moduledoc false
33
# https://develop.sentry.dev/sdk/envelopes/
44

5-
alias Sentry.{Attachment, CheckIn, ClientReport, Config, Event, UUID}
5+
alias Sentry.{
6+
Attachment,
7+
CheckIn,
8+
ClientReport,
9+
Config,
10+
Event,
11+
Transaction,
12+
UUID
13+
}
614

715
@type t() :: %__MODULE__{
816
event_id: UUID.t(),
9-
items: [Event.t() | Attachment.t() | CheckIn.t() | ClientReport.t(), ...]
17+
items: [
18+
Attachment.t() | CheckIn.t() | ClientReport.t() | Event.t() | Transaction.t()
19+
]
1020
}
1121

1222
@enforce_keys [:event_id, :items]
@@ -46,13 +56,31 @@ defmodule Sentry.Envelope do
4656
}
4757
end
4858

59+
@doc """
60+
Creates a new envelope containing a transaction with spans.
61+
"""
62+
@spec from_transaction(Sentry.Transaction.t()) :: t()
63+
def from_transaction(%Transaction{} = transaction) do
64+
%__MODULE__{
65+
event_id: transaction.event_id,
66+
items: [transaction]
67+
}
68+
end
69+
4970
@doc """
5071
Returns the "data category" of the envelope's contents (to be used in client reports and more).
5172
"""
5273
@doc since: "10.8.0"
53-
@spec get_data_category(Attachment.t() | CheckIn.t() | ClientReport.t() | Event.t()) ::
74+
@spec get_data_category(
75+
Attachment.t()
76+
| CheckIn.t()
77+
| ClientReport.t()
78+
| Event.t()
79+
| Transaction.t()
80+
) ::
5481
String.t()
5582
def get_data_category(%Attachment{}), do: "attachment"
83+
def get_data_category(%Transaction{}), do: "transaction"
5684
def get_data_category(%CheckIn{}), do: "monitor"
5785
def get_data_category(%ClientReport{}), do: "internal"
5886
def get_data_category(%Event{}), do: "error"
@@ -126,4 +154,15 @@ defmodule Sentry.Envelope do
126154
throw(error)
127155
end
128156
end
157+
158+
defp item_to_binary(json_library, %Transaction{} = transaction) do
159+
case transaction |> Sentry.Client.render_transaction() |> json_library.encode() do
160+
{:ok, encoded_transaction} ->
161+
header = ~s({"type": "transaction", "length": #{byte_size(encoded_transaction)}})
162+
[header, ?\n, encoded_transaction, ?\n]
163+
164+
{:error, _reason} = error ->
165+
throw(error)
166+
end
167+
end
129168
end

lib/sentry/options.ex

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Sentry.Options do
22
@moduledoc false
33

4-
@send_event_opts_schema_as_keyword [
4+
@common_opts_schema_as_keyword [
55
result: [
66
type: {:in, [:sync, :none]},
77
doc: """
@@ -15,29 +15,6 @@ defmodule Sentry.Options do
1515
call ends up being successful or not.
1616
"""
1717
],
18-
sample_rate: [
19-
type: :float,
20-
doc: """
21-
Same as the global `:sample_rate` configuration, but applied only to
22-
this call. See the module documentation. *Available since v10.0.0*.
23-
"""
24-
],
25-
before_send: [
26-
type: {:or, [{:fun, 1}, {:tuple, [:atom, :atom]}]},
27-
type_doc: "`t:before_send_event_callback/0`",
28-
doc: """
29-
Same as the global `:before_send` configuration, but
30-
applied only to this call. See the module documentation. *Available since v10.0.0*.
31-
"""
32-
],
33-
after_send_event: [
34-
type: {:or, [{:fun, 2}, {:tuple, [:atom, :atom]}]},
35-
type_doc: "`t:after_send_event_callback/0`",
36-
doc: """
37-
Same as the global `:after_send_event` configuration, but
38-
applied only to this call. See the module documentation. *Available since v10.0.0*.
39-
"""
40-
],
4118
client: [
4219
type: :atom,
4320
type_doc: "`t:module/0`",
@@ -54,6 +31,32 @@ defmodule Sentry.Options do
5431
]
5532
]
5633

34+
@send_event_opts_schema_as_keyword Keyword.merge(@common_opts_schema_as_keyword,
35+
sample_rate: [
36+
type: :float,
37+
doc: """
38+
Same as the global `:sample_rate` configuration, but applied only to
39+
this call. See the module documentation. *Available since v10.0.0*.
40+
"""
41+
],
42+
before_send: [
43+
type: {:or, [{:fun, 1}, {:tuple, [:atom, :atom]}]},
44+
type_doc: "`t:before_send_event_callback/0`",
45+
doc: """
46+
Same as the global `:before_send` configuration, but
47+
applied only to this call. See the module documentation. *Available since v10.0.0*.
48+
"""
49+
],
50+
after_send_event: [
51+
type: {:or, [{:fun, 2}, {:tuple, [:atom, :atom]}]},
52+
type_doc: "`t:after_send_event_callback/0`",
53+
doc: """
54+
Same as the global `:after_send_event` configuration, but
55+
applied only to this call. See the module documentation. *Available since v10.0.0*.
56+
"""
57+
]
58+
)
59+
5760
@create_event_opts_schema_as_keyword [
5861
exception: [
5962
type: {:custom, Sentry.Event, :__validate_exception__, [:exception]},
@@ -191,6 +194,10 @@ defmodule Sentry.Options do
191194

192195
@create_event_opts_schema NimbleOptions.new!(@create_event_opts_schema_as_keyword)
193196

197+
@send_transaction_opts_schema_as_keyword Keyword.merge(@common_opts_schema_as_keyword, [])
198+
199+
@send_transaction_opts_schema NimbleOptions.new!(@send_transaction_opts_schema_as_keyword)
200+
194201
@spec send_event_schema() :: NimbleOptions.t()
195202
def send_event_schema do
196203
@send_event_opts_schema
@@ -206,6 +213,11 @@ defmodule Sentry.Options do
206213
@create_event_opts_schema
207214
end
208215

216+
@spec send_transaction_schema() :: NimbleOptions.t()
217+
def send_transaction_schema do
218+
@send_transaction_opts_schema
219+
end
220+
209221
@spec docs_for(atom()) :: String.t()
210222
def docs_for(type)
211223

0 commit comments

Comments
 (0)