Skip to content

Commit 7632cd9

Browse files
committed
wip - Add Span and Transaction support
1 parent c565390 commit 7632cd9

File tree

10 files changed

+468
-16
lines changed

10 files changed

+468
-16
lines changed

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: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ defmodule Sentry.Client do
1616
Interfaces,
1717
LoggerUtils,
1818
Transport,
19-
Options
19+
Options,
20+
Transaction
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 = validate_options!(opts)
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: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ 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.{Attachment, CheckIn, ClientReport, Config, Event, UUID, Transaction}
66

77
@type t() :: %__MODULE__{
88
event_id: UUID.t(),
9-
items: [Event.t() | Attachment.t() | CheckIn.t() | ClientReport.t(), ...]
9+
items: [
10+
Event.t() | Attachment.t() | CheckIn.t() | ClientReport.t() | Transaction.t()
11+
]
1012
}
1113

1214
@enforce_keys [:event_id, :items]
@@ -46,13 +48,31 @@ defmodule Sentry.Envelope do
4648
}
4749
end
4850

51+
@doc """
52+
Creates a new envelope containing a transaction with spans.
53+
"""
54+
@spec from_transaction(Sentry.Transaction.t()) :: t()
55+
def from_transaction(%Transaction{} = transaction) do
56+
%__MODULE__{
57+
event_id: transaction.event_id,
58+
items: [transaction]
59+
}
60+
end
61+
4962
@doc """
5063
Returns the "data category" of the envelope's contents (to be used in client reports and more).
5164
"""
5265
@doc since: "10.8.0"
53-
@spec get_data_category(Attachment.t() | CheckIn.t() | ClientReport.t() | Event.t()) ::
66+
@spec get_data_category(
67+
Attachment.t()
68+
| CheckIn.t()
69+
| ClientReport.t()
70+
| Event.t()
71+
| Transaction.t()
72+
) ::
5473
String.t()
5574
def get_data_category(%Attachment{}), do: "attachment"
75+
def get_data_category(%Transaction{}), do: "transaction"
5676
def get_data_category(%CheckIn{}), do: "monitor"
5777
def get_data_category(%ClientReport{}), do: "internal"
5878
def get_data_category(%Event{}), do: "error"
@@ -126,4 +146,15 @@ defmodule Sentry.Envelope do
126146
throw(error)
127147
end
128148
end
149+
150+
defp item_to_binary(json_library, %Transaction{} = transaction) do
151+
case transaction |> Sentry.Client.render_transaction() |> json_library.encode() do
152+
{:ok, encoded_transaction} ->
153+
header = ~s({"type": "transaction", "length": #{byte_size(encoded_transaction)}})
154+
[header, ?\n, encoded_transaction, ?\n]
155+
156+
{:error, _reason} = error ->
157+
throw(error)
158+
end
159+
end
129160
end

lib/sentry/test.ex

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ defmodule Sentry.Test do
7878

7979
@server __MODULE__.OwnershipServer
8080
@key :events
81+
@transaction_key :transactions
8182

8283
# Used internally when reporting an event, *before* reporting the actual event.
8384
@doc false
@@ -115,6 +116,48 @@ defmodule Sentry.Test do
115116
end
116117
end
117118

119+
# Used internally when reporting a transaction, *before* reporting the actual transaction.
120+
@doc false
121+
@spec maybe_collect(Sentry.Transaction.t()) :: :collected | :not_collecting
122+
def maybe_collect(%Sentry.Transaction{} = transaction) do
123+
if Sentry.Config.test_mode?() do
124+
dsn_set? = not is_nil(Sentry.Config.dsn())
125+
ensure_ownership_server_started()
126+
127+
case NimbleOwnership.fetch_owner(@server, callers(), @transaction_key) do
128+
{:ok, owner_pid} ->
129+
result =
130+
NimbleOwnership.get_and_update(
131+
@server,
132+
owner_pid,
133+
@transaction_key,
134+
fn transactions ->
135+
{:collected, (transactions || []) ++ [transaction]}
136+
end
137+
)
138+
139+
case result do
140+
{:ok, :collected} ->
141+
:collected
142+
143+
{:error, error} ->
144+
raise ArgumentError,
145+
"cannot collect Sentry transactions: #{Exception.message(error)}"
146+
end
147+
148+
:error when dsn_set? ->
149+
:not_collecting
150+
151+
# If the :dsn option is not set and we didn't capture the transaction, it's alright,
152+
# we can just swallow it.
153+
:error ->
154+
:collected
155+
end
156+
else
157+
:not_collecting
158+
end
159+
end
160+
118161
@doc """
119162
Starts collecting events from the current process.
120163
@@ -135,7 +178,8 @@ defmodule Sentry.Test do
135178
@doc since: "10.2.0"
136179
@spec start_collecting_sentry_reports(map()) :: :ok
137180
def start_collecting_sentry_reports(_context \\ %{}) do
138-
start_collecting()
181+
start_collecting(key: @key)
182+
start_collecting(key: @transaction_key)
139183
end
140184

141185
@doc """
@@ -177,6 +221,7 @@ defmodule Sentry.Test do
177221
@doc since: "10.2.0"
178222
@spec start_collecting(keyword()) :: :ok
179223
def start_collecting(options \\ []) when is_list(options) do
224+
key = Keyword.get(options, :key, @key)
180225
owner_pid = Keyword.get(options, :owner, self())
181226
cleanup? = Keyword.get(options, :cleanup, true)
182227

@@ -190,7 +235,7 @@ defmodule Sentry.Test do
190235
# Make sure the ownership server is started (this is idempotent).
191236
ensure_ownership_server_started()
192237

193-
case NimbleOwnership.fetch_owner(@server, callers, @key) do
238+
case NimbleOwnership.fetch_owner(@server, callers, key) do
194239
# No-op
195240
{tag, ^owner_pid} when tag in [:ok, :shared_owner] ->
196241
:ok
@@ -207,7 +252,7 @@ defmodule Sentry.Test do
207252
end
208253

209254
{:ok, _} =
210-
NimbleOwnership.get_and_update(@server, self(), @key, fn events ->
255+
NimbleOwnership.get_and_update(@server, self(), key, fn events ->
211256
{:ignored, events || []}
212257
end)
213258

@@ -302,6 +347,51 @@ defmodule Sentry.Test do
302347
end
303348
end
304349

350+
@doc """
351+
Pops all the collected transactions from the current process.
352+
353+
This function returns a list of all the transactions that have been collected from the current
354+
process and all the processes that were allowed through it. If the current process
355+
is not collecting transactions, this function raises an error.
356+
357+
After this function returns, the current process will still be collecting transactions, but
358+
the collected transactions will be reset to `[]`.
359+
360+
## Examples
361+
362+
iex> Sentry.Test.start_collecting_sentry_reports()
363+
:ok
364+
iex> Sentry.send_transaction(Sentry.Transaction.new(%{span_id: "123", spans: []}))
365+
{:ok, ""}
366+
iex> [%Sentry.Transaction{}] = Sentry.Test.pop_sentry_transactions()
367+
368+
"""
369+
@doc since: "10.2.0"
370+
@spec pop_sentry_transactions(pid()) :: [Sentry.Transaction.t()]
371+
def pop_sentry_transactions(owner_pid \\ self()) when is_pid(owner_pid) do
372+
result =
373+
try do
374+
NimbleOwnership.get_and_update(@server, owner_pid, @transaction_key, fn
375+
nil -> {:not_collecting, []}
376+
transactions when is_list(transactions) -> {transactions, []}
377+
end)
378+
catch
379+
:exit, {:noproc, _} ->
380+
raise ArgumentError, "not collecting reported transactions from #{inspect(owner_pid)}"
381+
end
382+
383+
case result do
384+
{:ok, :not_collecting} ->
385+
raise ArgumentError, "not collecting reported transactions from #{inspect(owner_pid)}"
386+
387+
{:ok, transactions} ->
388+
transactions
389+
390+
{:error, error} when is_exception(error) ->
391+
raise ArgumentError, "cannot pop Sentry transactions: #{Exception.message(error)}"
392+
end
393+
end
394+
305395
## Helpers
306396

307397
defp ensure_ownership_server_started do

0 commit comments

Comments
 (0)