diff --git a/lib/sentry/client.ex b/lib/sentry/client.ex index 1d11b781..337b8d5c 100644 --- a/lib/sentry/client.ex +++ b/lib/sentry/client.ex @@ -107,6 +107,11 @@ defmodule Sentry.Client do |> Transport.encode_and_post_envelope(client, request_retries) end + @spec send_transaction(Transaction.t(), keyword()) :: + {:ok, transaction_id :: String.t()} + | {:error, ClientError.t()} + | :unsampled + | :excluded def send_transaction(%Transaction{} = transaction, opts \\ []) do opts = NimbleOptions.validate!(opts, Options.send_transaction_schema()) @@ -190,12 +195,12 @@ defmodule Sentry.Client do """ end - defp maybe_call_after_send(event, result, callback) do + defp maybe_call_after_send(event_or_transaction, result, callback) do message = ":after_send_event must be an anonymous function or a {module, function} tuple" case callback do - function when is_function(function, 2) -> function.(event, result) - {module, function} -> apply(module, function, [event, result]) + function when is_function(function, 2) -> function.(event_or_transaction, result) + {module, function} -> apply(module, function, [event_or_transaction, result]) nil -> nil _ -> raise ArgumentError, message end @@ -294,13 +299,7 @@ defmodule Sentry.Client do def render_transaction(%Transaction{} = transaction) do transaction |> Transaction.to_payload() - |> Map.merge(%{ - platform: "elixir", - sdk: %{ - name: "sentry.elixir", - version: Application.spec(:sentry, :vsn) - } - }) + |> update_if_present(:sdk, &Map.from_struct/1) end defp render_exception(%Interfaces.Exception{} = exception) do diff --git a/lib/sentry/interfaces.ex b/lib/sentry/interfaces.ex index 3efdf9fc..eeb18b2d 100644 --- a/lib/sentry/interfaces.ex +++ b/lib/sentry/interfaces.ex @@ -272,24 +272,26 @@ defmodule Sentry.Interfaces do @moduledoc """ The struct for the **span** interface. - See . + See . """ @moduledoc since: "11.0.0" @typedoc since: "11.0.0" @type t() :: %__MODULE__{ - trace_id: String.t(), - span_id: String.t(), - start_timestamp: String.t(), - timestamp: String.t(), - parent_span_id: String.t(), - description: String.t(), - op: String.t(), - status: String.t(), - tags: map(), - data: map(), - origin: String.t() + trace_id: <<_::256>>, + span_id: <<_::128>>, + start_timestamp: String.t() | number(), + timestamp: String.t() | number(), + + # Optional + parent_span_id: <<_::128>> | nil, + description: String.t() | nil, + op: String.t() | nil, + status: String.t() | nil, + tags: %{optional(String.t()) => term()} | nil, + data: %{optional(String.t()) => term()} | nil, + origin: String.t() | nil } @enforce_keys [:trace_id, :span_id, :start_timestamp, :timestamp] @@ -304,10 +306,5 @@ defmodule Sentry.Interfaces do :data, :origin ] - - @doc false - def to_payload(%__MODULE__{} = span) do - Map.from_struct(span) - end end end diff --git a/lib/sentry/transaction.ex b/lib/sentry/transaction.ex index c0ebc619..efc43c20 100644 --- a/lib/sentry/transaction.ex +++ b/lib/sentry/transaction.ex @@ -2,12 +2,42 @@ defmodule Sentry.Transaction do @moduledoc """ The struct for the **transaction** interface. - See . + See . """ @moduledoc since: "11.0.0" - alias Sentry.{Config, UUID, Interfaces.Span, Interfaces.SDK} + alias Sentry.{Config, UUID, Interfaces, Interfaces.Span, Interfaces.SDK} + + @sdk %Interfaces.SDK{ + name: "sentry-elixir", + version: Mix.Project.config()[:version] + } + + @typedoc """ + A map of measurements. + + See + [here](https://github.com/getsentry/sentry/blob/a8c960a933d2ded5225841573d8fc426a482ca9c/static/app/utils/discover/fields.tsx#L654-L676) + for the list of supported keys (which could change in the future). + """ + @typedoc since: "11.0.0" + @type measurements() :: %{ + optional(key :: atom()) => %{ + required(:value) => number(), + optional(:unit) => String.t() + } + } + + @typedoc """ + Transaction information. + + Should only be set by integrations and not developers directly. + """ + @typedoc since: "11.0.0" + @type transaction_info() :: %{ + required(:source) => String.t() + } @typedoc since: "11.0.0" @type t() :: @@ -16,7 +46,8 @@ defmodule Sentry.Transaction do event_id: UUID.t(), start_timestamp: String.t() | number(), timestamp: String.t() | number(), - platform: :elixir, + platform: String.t(), + # See https://develop.sentry.dev/sdk/data-model/event-payloads/contexts/#trace-context contexts: %{ required(:trace) => %{ required(:trace_id) => String.t(), @@ -31,9 +62,8 @@ defmodule Sentry.Transaction do # Optional environment: String.t(), transaction: String.t(), - transaction_info: map(), - measurements: map(), - type: String.t(), + transaction_info: transaction_info(), + measurements: measurements(), tags: map(), data: map(), @@ -55,8 +85,7 @@ defmodule Sentry.Transaction do :platform, :environment, :tags, - :data, - type: "transaction" + :data ] @doc false @@ -66,6 +95,8 @@ defmodule Sentry.Transaction do attrs |> Map.put(:event_id, UUID.uuid4_hex()) |> Map.put(:environment, Config.environment_name()) + |> Map.put(:sdk, @sdk) + |> Map.put(:platform, "elixir") ) end @@ -73,6 +104,7 @@ defmodule Sentry.Transaction do def to_payload(%__MODULE__{} = transaction) do transaction |> Map.from_struct() - |> Map.update(:spans, [], fn spans -> Enum.map(spans, &Span.to_payload/1) end) + |> Map.put(:type, "transaction") + |> update_in([Access.key(:spans, []), Access.all()], &Map.from_struct/1) end end diff --git a/test/sentry/transaction_test.exs b/test/sentry/transaction_test.exs index bd2ee3c1..f6ec2854 100644 --- a/test/sentry/transaction_test.exs +++ b/test/sentry/transaction_test.exs @@ -6,13 +6,12 @@ defmodule Sentry.TransactionTest do import Sentry.TestHelpers describe "to_payload/1" do - setup do - {:ok, %{transaction: create_transaction()}} - end - - test "returns a map representation of the transaction", %{transaction: transaction} do + test "returns a map representation of the transaction" do + transaction = create_transaction() transaction_payload = Transaction.to_payload(transaction) + assert transaction_payload.type == "transaction" + [child_span] = transaction_payload[:spans] assert transaction_payload[:contexts][:trace][:trace_id] == "trace-312"