@@ -16,10 +16,6 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
1616 alias Sentry . { Transaction , OpenTelemetry.SpanStorage , OpenTelemetry.SpanRecord }
1717 alias Sentry.Interfaces.Span
1818
19- # Extract span record fields to access parent_span_id in on_start
20- @ span_fields Record . extract ( :span , from_lib: "opentelemetry/include/otel_span.hrl" )
21- Record . defrecordp ( :span , @ span_fields )
22-
2319 @ impl :otel_span_processor
2420 def on_start ( _ctx , otel_span , _config ) do
2521 span_record = SpanRecord . new ( otel_span )
@@ -32,23 +28,92 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
3228 span_record = SpanRecord . new ( otel_span )
3329 SpanStorage . update_span ( span_record )
3430
35- if is_transaction_root? ( span_record ) do
31+ # Filter redundant LiveView spans from static renders.
32+ # During static render, the HTTP server span already covers the request,
33+ # so LiveView lifecycle spans (mount, handle_params, etc.) are duplicates.
34+ if skip_static_render_liveview_span? ( span_record ) do
35+ true
36+ else
37+ process_span ( span_record )
38+ end
39+ end
40+
41+ # Skip LiveView spans that occur during static render (HTTP request phase).
42+ # These are redundant because the HTTP server span already covers this phase.
43+ # We detect this by checking if:
44+ # 1. The span is a LiveView lifecycle span (mount, handle_params, handle_event)
45+ # 2. It has a local parent span that is an HTTP server span
46+ defp skip_static_render_liveview_span? ( span_record ) do
47+ is_liveview_span? ( span_record ) and has_local_http_parent? ( span_record )
48+ end
49+
50+ # Check if span name matches LiveView lifecycle patterns from opentelemetry_phoenix
51+ # Span names are like "MyAppWeb.SomeLive.mount", "MyAppWeb.SomeLive.handle_params", etc.
52+ # Note: handle_event spans have the event name appended like "Module.handle_event#event_name"
53+ defp is_liveview_span? ( % { name: name } ) when is_binary ( name ) do
54+ is_liveview_lifecycle_span? ( name )
55+ end
56+
57+ defp is_liveview_span? ( _ ) , do: false
58+
59+ # Check if the span has a local parent that is an HTTP server span
60+ defp has_local_http_parent? ( % { parent_span_id: nil } ) , do: false
61+
62+ defp has_local_http_parent? ( % { parent_span_id: parent_span_id } ) do
63+ case SpanStorage . get_span ( parent_span_id ) do
64+ nil -> false
65+ parent_span -> is_http_server_span? ( parent_span )
66+ end
67+ end
68+
69+ defp is_http_server_span? ( % { kind: :server , attributes: attributes } ) do
70+ Map . has_key? ( attributes , to_string ( HTTPAttributes . http_request_method ( ) ) )
71+ end
72+
73+ defp is_http_server_span? ( _ ) , do: false
74+
75+ defp process_span ( span_record ) do
76+ SpanStorage . store_span ( span_record )
77+
78+ # Check if this is a root span (no parent) or a transaction root
79+ #
80+ # A span should be a transaction root if:
81+ # 1. It has no parent (true root span)
82+ # 2. OR it's a server span with only a REMOTE parent (distributed tracing)
83+ #
84+ # A span should NOT be a transaction root if:
85+ # - It has a LOCAL parent (parent span exists in our SpanStorage)
86+ #
87+ # Note: LiveView spans during static render are filtered earlier by
88+ # skip_static_render_liveview_span?/1, so we don't need to handle them here.
89+ is_transaction_root =
90+ cond do
91+ # No parent = definitely a root
92+ span_record . parent_span_id == nil ->
93+ true
94+
95+ # Has a parent - check if it's local or remote
96+ true ->
97+ has_local_parent = has_local_parent_span? ( span_record . parent_span_id )
98+
99+ if has_local_parent do
100+ # Parent exists locally - this is a child span, not a transaction root
101+ false
102+ else
103+ # Parent is remote (distributed tracing) - treat server spans as transaction roots
104+ is_server_span? ( span_record )
105+ end
106+ end
107+
108+ if is_transaction_root do
36109 build_and_send_transaction ( span_record )
37110 else
38111 true
39112 end
40113 end
41114
42- # Check if this is a root span (no parent) or a transaction root
43- #
44- # A span should be a transaction root if:
45- #
46- # 1. It has no parent (true root span)
47- # 2. OR it's a span with a remote parent span
48- #
49- defp is_transaction_root? ( span_record ) do
50- span_record . parent_span_id == nil or
51- not SpanStorage . span_exists? ( span_record . parent_span_id )
115+ defp has_local_parent_span? ( parent_span_id ) do
116+ SpanStorage . span_exists? ( parent_span_id )
52117 end
53118
54119 defp build_and_send_transaction ( span_record ) do
@@ -82,6 +147,31 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
82147 :ok
83148 end
84149
150+ # Helper function to detect if a span is a server span that should be
151+ # treated as a transaction root for distributed tracing.
152+ # This includes HTTP server request spans (have http.request.method attribute)
153+ # and LiveView lifecycle spans (mount, handle_params, handle_event)
154+ defp is_server_span? ( % { kind: :server , attributes: attributes , name: name } ) do
155+ # Check if it's an HTTP server request span (has http.request.method)
156+ # LiveView lifecycle spans (opentelemetry_phoenix uses kind: :server)
157+ Map . has_key? ( attributes , to_string ( HTTPAttributes . http_request_method ( ) ) ) or
158+ is_liveview_lifecycle_span? ( name )
159+ end
160+
161+ defp is_server_span? ( _ ) , do: false
162+
163+ # Check if span name matches LiveView lifecycle patterns
164+ # Note: handle_event spans have the event name appended like "Module.handle_event#event_name"
165+ # So we check if the name contains these patterns, not just ends with them
166+ defp is_liveview_lifecycle_span? ( name ) when is_binary ( name ) do
167+ String . ends_with? ( name , ".mount" ) or
168+ String . ends_with? ( name , ".handle_params" ) or
169+ String . contains? ( name , ".handle_event#" ) or
170+ String . ends_with? ( name , ".handle_event" )
171+ end
172+
173+ defp is_liveview_lifecycle_span? ( _ ) , do: false
174+
85175 defp build_transaction ( root_span_record , child_span_records ) do
86176 root_span = build_span ( root_span_record )
87177 child_spans = Enum . map ( child_span_records , & build_span ( & 1 ) )
@@ -134,7 +224,13 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
134224 client_address =
135225 Map . get ( span_record . attributes , to_string ( ClientAttributes . client_address ( ) ) )
136226
137- url_path = Map . get ( span_record . attributes , to_string ( URLAttributes . url_path ( ) ) )
227+ # Try multiple attributes for the URL path
228+ url_path =
229+ Map . get ( span_record . attributes , to_string ( URLAttributes . url_path ( ) ) ) ||
230+ Map . get ( span_record . attributes , "url.full" ) ||
231+ Map . get ( span_record . attributes , "http.target" ) ||
232+ Map . get ( span_record . attributes , "http.route" ) ||
233+ span_record . name
138234
139235 # Build description with method and path
140236 description =
0 commit comments