Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion lib/sentry/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,16 @@ defmodule Sentry.Event do
> See also `Sentry.Context` for information on the Sentry context and `Sentry` for
> information on configuration.

> #### Trace Context {: .info}
>
> When the calling process has an active OpenTelemetry span, the event gets a
> `contexts.trace` with the `trace_id` and `span_id` of that span, so that Sentry
> links the error to the trace it happened in. The key is omitted when there is no
> active span or when OpenTelemetry is not available.
>
> The span is read from the process that creates the event, so errors reported from
> a process that did not inherit the OpenTelemetry context are not linked to a trace.

## Options

#{NimbleOptions.docs(@create_event_opts_schema)}
Expand Down Expand Up @@ -485,10 +495,41 @@ defmodule Sentry.Event do
version_string -> version_string
end

%{
contexts = %{
os: %{name: Atom.to_string(os_name), version: os_version},
runtime: %{name: "elixir", version: System.build_info().build}
}

case current_trace_context() do
nil -> contexts
trace -> Map.put(contexts, :trace, trace)
end
end

if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
defp current_trace_context do
case :otel_tracer.current_span_ctx() do
:undefined ->
nil

span_ctx ->
trace_id = :otel_span.trace_id(span_ctx)
span_id = :otel_span.span_id(span_ctx)

if trace_id != 0 and span_id != 0 do
%{trace_id: format_id(trace_id, 32), span_id: format_id(span_id, 16)}
end
end
end
Comment thread
solnic marked this conversation as resolved.

defp format_id(id, length) do
id
|> Integer.to_string(16)
|> String.pad_leading(length, "0")
|> String.downcase()
end
else
defp current_trace_context, do: nil
end

# Used to compare events for deduplication. See "Sentry.Dedupe".
Expand Down
52 changes: 52 additions & 0 deletions test/sentry_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule SentryTest do
use Sentry.Case

require OpenTelemetry.Tracer, as: Tracer

import ExUnit.CaptureLog
import Sentry.Test.Assertions
import Sentry.TestHelpers
Expand Down Expand Up @@ -394,6 +396,56 @@ defmodule SentryTest do
end
end

describe "trace context on captured errors without tracing" do
test "sends the error without a trace context when nothing is being traced", %{bypass: bypass} do
ref = SentryTest.setup_bypass_envelope_collector(bypass, type: "event")

assert {:ok, _} = Sentry.capture_message("standalone failure", result: :sync)

assert [event] = extract_events(collect_envelopes(ref, 1))
refute event["contexts"]["trace"]
end
end

describe "trace context on captured errors" do
setup %{bypass: bypass} do
put_test_config(traces_sample_rate: 1.0)
%{ref: SentryTest.setup_bypass_envelope_collector(bypass)}
end

test "sends the error and the transaction of the same operation with the same trace_id", %{
ref: ref
} do
Tracer.with_span "checkout" do
assert {:ok, _} = Sentry.capture_message("checkout failed", result: :sync)
end

envelopes = collect_envelopes(ref, 2)

assert [event] = extract_events(envelopes)
assert [transaction] = extract_transactions(envelopes)

assert event["contexts"]["trace"]["trace_id"] ==
transaction["contexts"]["trace"]["trace_id"]
end

test "points the error at the span it was captured in", %{ref: ref} do
Tracer.with_span "checkout" do
Tracer.with_span "charge_card" do
assert {:ok, _} = Sentry.capture_message("charge failed", result: :sync)
end
end

envelopes = collect_envelopes(ref, 2)

assert [event] = extract_events(envelopes)
assert [transaction] = extract_transactions(envelopes)
assert [child_span] = transaction["spans"]

assert event["contexts"]["trace"]["span_id"] == child_span["span_id"]
end
end

describe "flush/1" do
test "warns and returns :ok when the TelemetryProcessor is not running" do
# The default TelemetryProcessor runs under the application supervisor, so it has
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ defmodule PhoenixAppWeb.PageController do
raise "Test exception"
end

def traced_exception(conn, _params) do
Tracer.with_span "process_order" do
try do
raise "Traced exception"
rescue
exception -> Sentry.capture_exception(exception, stacktrace: __STACKTRACE__)
end
end

render(conn, :home, layout: false)
end

def function_clause_error(_conn, %{"required" => _value}) do
:ok
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule PhoenixAppWeb.Router do

get "/", PageController, :home
get "/exception", PageController, :exception
get "/traced-exception", PageController, :traced_exception
get "/transaction", PageController, :transaction
get "/nested-spans", PageController, :nested_spans
get "/metrics", PageController, :metrics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ defmodule Sentry.Integrations.Phoenix.ExceptionTest do
import Sentry.Test.Assertions

setup do
Sentry.Test.setup_sentry(traces_sample_rate: 1.0)
:ok
Sentry.Test.setup_sentry(collect_envelopes: true, traces_sample_rate: 1.0)
end

test "GET /exception sends exception to Sentry", %{conn: conn} do
Expand All @@ -21,4 +20,21 @@ defmodule Sentry.Integrations.Phoenix.ExceptionTest do

assert is_binary(event.event_id)
end

test "GET /traced-exception links the exception to the trace it was reported in", %{
conn: conn,
ref: ref
} do
get(conn, ~p"/traced-exception")

event =
assert_sentry_report(:event,
original_exception: %RuntimeError{message: "Traced exception"}
)

transaction = find_sentry_transaction!(ref, count: 2, transaction: "process_order")

assert event.contexts.trace.trace_id == transaction["contexts"]["trace"]["trace_id"]
assert event.contexts.trace.span_id == transaction["contexts"]["trace"]["span_id"]
end
end
Loading