Skip to content

Commit c5dd1fc

Browse files
solnicwhatyouhide
andauthored
Add support for sending transactions (#842)
Co-Authored-By: Andrea Leopardi <[email protected]>
1 parent 652e1dc commit c5dd1fc

15 files changed

+661
-37
lines changed

lib/sentry.ex

+22
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

+83-5
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
@@ -99,14 +100,42 @@ defmodule Sentry.Client do
99100
client = Config.client()
100101

101102
# This is a "private" option, only really used in testing.
102-
request_retries =
103-
Application.get_env(:sentry, :request_retries, Transport.default_retries())
103+
request_retries = Application.get_env(:sentry, :request_retries, Transport.default_retries())
104104

105105
client_report
106106
|> Envelope.from_client_report()
107107
|> Transport.encode_and_post_envelope(client, request_retries)
108108
end
109109

110+
def send_transaction(%Transaction{} = transaction, opts \\ []) do
111+
opts = NimbleOptions.validate!(opts, Options.send_transaction_schema())
112+
113+
result_type = Keyword.get_lazy(opts, :result, &Config.send_result/0)
114+
client = Keyword.get_lazy(opts, :client, &Config.client/0)
115+
sample_rate = Keyword.get_lazy(opts, :sample_rate, &Config.sample_rate/0)
116+
before_send = Keyword.get_lazy(opts, :before_send, &Config.before_send/0)
117+
after_send_event = Keyword.get_lazy(opts, :after_send_event, &Config.after_send_event/0)
118+
119+
request_retries =
120+
Keyword.get_lazy(opts, :request_retries, fn ->
121+
Application.get_env(:sentry, :request_retries, Transport.default_retries())
122+
end)
123+
124+
with :ok <- sample_event(sample_rate),
125+
{:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send) do
126+
send_result = encode_and_send(transaction, result_type, client, request_retries)
127+
_ignored = maybe_call_after_send(transaction, send_result, after_send_event)
128+
send_result
129+
else
130+
:unsampled ->
131+
ClientReport.Sender.record_discarded_events(:sample_rate, [transaction])
132+
:unsampled
133+
134+
:excluded ->
135+
:excluded
136+
end
137+
end
138+
110139
defp sample_event(sample_rate) do
111140
cond do
112141
sample_rate == 1 -> :ok
@@ -161,7 +190,7 @@ defmodule Sentry.Client do
161190
"""
162191
end
163192

164-
defp maybe_call_after_send(%Event{} = event, result, callback) do
193+
defp maybe_call_after_send(event, result, callback) do
165194
message = ":after_send_event must be an anonymous function or a {module, function} tuple"
166195

167196
case callback do
@@ -205,6 +234,42 @@ defmodule Sentry.Client do
205234
end
206235
end
207236

237+
defp encode_and_send(
238+
%Transaction{} = transaction,
239+
_result_type = :sync,
240+
client,
241+
request_retries
242+
) do
243+
case Sentry.Test.maybe_collect(transaction) do
244+
:collected ->
245+
{:ok, ""}
246+
247+
:not_collecting ->
248+
send_result =
249+
transaction
250+
|> Envelope.from_transaction()
251+
|> Transport.encode_and_post_envelope(client, request_retries)
252+
253+
send_result
254+
end
255+
end
256+
257+
defp encode_and_send(
258+
%Transaction{} = transaction,
259+
_result_type = :none,
260+
client,
261+
_request_retries
262+
) do
263+
case Sentry.Test.maybe_collect(transaction) do
264+
:collected ->
265+
{:ok, ""}
266+
267+
:not_collecting ->
268+
:ok = Transport.Sender.send_async(client, transaction)
269+
{:ok, ""}
270+
end
271+
end
272+
208273
@spec render_event(Event.t()) :: map()
209274
def render_event(%Event{} = event) do
210275
json_library = Config.json_library()
@@ -225,6 +290,19 @@ defmodule Sentry.Client do
225290
|> update_if_present(:threads, fn list -> Enum.map(list, &render_thread/1) end)
226291
end
227292

293+
@spec render_transaction(%Transaction{}) :: map()
294+
def render_transaction(%Transaction{} = transaction) do
295+
transaction
296+
|> Transaction.to_payload()
297+
|> Map.merge(%{
298+
platform: "elixir",
299+
sdk: %{
300+
name: "sentry.elixir",
301+
version: Application.spec(:sentry, :vsn)
302+
}
303+
})
304+
end
305+
228306
defp render_exception(%Interfaces.Exception{} = exception) do
229307
exception
230308
|> Map.from_struct()

lib/sentry/client_report/sender.ex

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ defmodule Sentry.ClientReport.Sender do
2323
| Sentry.CheckIn.t()
2424
| ClientReport.t()
2525
| Sentry.Event.t()
26+
| Sentry.Transaction.t()
2627
def record_discarded_events(reason, event_items, genserver \\ __MODULE__)
2728
when is_list(event_items) do
2829
# We silently ignore events whose reasons aren't valid because we have to add it to the allowlist in Snuba

lib/sentry/envelope.ex

+43-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@ 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+
...
20+
]
1021
}
1122

1223
@enforce_keys [:event_id, :items]
@@ -46,13 +57,31 @@ defmodule Sentry.Envelope do
4657
}
4758
end
4859

60+
@doc """
61+
Creates a new envelope containing a transaction with spans.
62+
"""
63+
@spec from_transaction(Transaction.t()) :: t()
64+
def from_transaction(%Transaction{} = transaction) do
65+
%__MODULE__{
66+
event_id: transaction.event_id,
67+
items: [transaction]
68+
}
69+
end
70+
4971
@doc """
5072
Returns the "data category" of the envelope's contents (to be used in client reports and more).
5173
"""
5274
@doc since: "10.8.0"
53-
@spec get_data_category(Attachment.t() | CheckIn.t() | ClientReport.t() | Event.t()) ::
75+
@spec get_data_category(
76+
Attachment.t()
77+
| CheckIn.t()
78+
| ClientReport.t()
79+
| Event.t()
80+
| Transaction.t()
81+
) ::
5482
String.t()
5583
def get_data_category(%Attachment{}), do: "attachment"
84+
def get_data_category(%Transaction{}), do: "transaction"
5685
def get_data_category(%CheckIn{}), do: "monitor"
5786
def get_data_category(%ClientReport{}), do: "internal"
5887
def get_data_category(%Event{}), do: "error"
@@ -126,4 +155,15 @@ defmodule Sentry.Envelope do
126155
throw(error)
127156
end
128157
end
158+
159+
defp item_to_binary(json_library, %Transaction{} = transaction) do
160+
case transaction |> Sentry.Client.render_transaction() |> Sentry.JSON.encode(json_library) do
161+
{:ok, encoded_transaction} ->
162+
header = ~s({"type":"transaction","length":#{byte_size(encoded_transaction)}})
163+
[header, ?\n, encoded_transaction, ?\n]
164+
165+
{:error, _reason} = error ->
166+
throw(error)
167+
end
168+
end
129169
end

lib/sentry/interfaces.ex

+43
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,47 @@ defmodule Sentry.Interfaces do
267267
:stacktrace
268268
]
269269
end
270+
271+
defmodule Span do
272+
@moduledoc """
273+
The struct for the **span** interface.
274+
275+
See <https://develop.sentry.dev/sdk/event-payloads/spans>.
276+
"""
277+
278+
@moduledoc since: "11.0.0"
279+
280+
@typedoc since: "11.0.0"
281+
@type t() :: %__MODULE__{
282+
trace_id: String.t(),
283+
span_id: String.t(),
284+
start_timestamp: String.t(),
285+
timestamp: String.t(),
286+
parent_span_id: String.t(),
287+
description: String.t(),
288+
op: String.t(),
289+
status: String.t(),
290+
tags: map(),
291+
data: map(),
292+
origin: String.t()
293+
}
294+
295+
@enforce_keys [:trace_id, :span_id, :start_timestamp, :timestamp]
296+
297+
defstruct @enforce_keys ++
298+
[
299+
:parent_span_id,
300+
:description,
301+
:op,
302+
:status,
303+
:tags,
304+
:data,
305+
:origin
306+
]
307+
308+
@doc false
309+
def to_payload(%__MODULE__{} = span) do
310+
Map.from_struct(span)
311+
end
312+
end
270313
end

lib/sentry/options.ex

+13-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ defmodule Sentry.Options do
1515
call ends up being successful or not.
1616
"""
1717
],
18+
client: [
19+
type: :atom,
20+
type_doc: "`t:module/0`",
21+
doc: """
22+
Same as the global `:client` configuration, but
23+
applied only to this call. See the module documentation. *Available since v10.0.0*.
24+
"""
25+
],
1826
sample_rate: [
1927
type: :float,
2028
doc: """
@@ -38,14 +46,6 @@ defmodule Sentry.Options do
3846
applied only to this call. See the module documentation. *Available since v10.0.0*.
3947
"""
4048
],
41-
client: [
42-
type: :atom,
43-
type_doc: "`t:module/0`",
44-
doc: """
45-
Same as the global `:client` configuration, but
46-
applied only to this call. See the module documentation. *Available since v10.0.0*.
47-
"""
48-
],
4949

5050
# Private options, only used in testing.
5151
request_retries: [
@@ -206,6 +206,11 @@ defmodule Sentry.Options do
206206
@create_event_opts_schema
207207
end
208208

209+
@spec send_transaction_schema() :: NimbleOptions.t()
210+
def send_transaction_schema do
211+
@send_event_opts_schema
212+
end
213+
209214
@spec docs_for(atom()) :: String.t()
210215
def docs_for(type)
211216

0 commit comments

Comments
 (0)