@@ -22,6 +22,14 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
2222
2323 @ impl :otel_span_processor
2424 def on_start ( _ctx , otel_span , _config ) do
25+ # Check if this is a LiveView span during static render
26+ # If so, mark it so we can filter it out in on_end
27+ if liveview_propagator_loaded? ( ) and
28+ Sentry.OpenTelemetry.LiveViewPropagator . static_render? ( ) do
29+ # Set an attribute on the span to mark it as from static render
30+ :otel_span . set_attribute ( otel_span , :"sentry.liveview.static_render" , true )
31+ end
32+
2533 # Track pending children: when a span starts with a parent, register it
2634 # as a pending child. This allows us to wait for all children when
2735 # the parent ends, solving the race condition where parent.on_end
@@ -44,7 +52,21 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
4452 @ impl :otel_span_processor
4553 def on_end ( otel_span , _config ) do
4654 span_record = SpanRecord . new ( otel_span )
47- process_span ( span_record )
55+
56+ # Skip LiveView spans from static render - they're redundant since:
57+ # 1. The HTTP span already covers the static render phase
58+ # 2. The same LiveView callbacks run again over WebSocket (the meaningful ones)
59+ if static_render_liveview_span? ( span_record ) do
60+ # Don't store or process this span
61+ # But still remove from pending children if it was tracked
62+ if span_record . parent_span_id != nil do
63+ SpanStorage . remove_pending_child ( span_record . parent_span_id , span_record . span_id )
64+ end
65+
66+ true
67+ else
68+ process_span ( span_record )
69+ end
4870 end
4971
5072 defp process_span ( span_record ) do
@@ -69,6 +91,10 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
6991 #
7092 # A span should NOT be a transaction root if:
7193 # - It has a LOCAL parent (parent span exists in our SpanStorage)
94+ #
95+ # This prevents LiveView spans from becoming separate transactions when
96+ # they occur within an HTTP request - they should be child spans of the
97+ # HTTP server span instead.
7298 is_transaction_root =
7399 cond do
74100 # No parent = definitely a root
@@ -141,6 +167,9 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
141167 end
142168
143169 # Clean up: remove the transaction root span and all its children
170+ # Note: For distributed tracing, the transaction root span may have been stored
171+ # as a child span (with a remote parent_span_id). In that case, we need to also
172+ # remove it from the child spans, not just look for it as a root span.
144173 :ok = SpanStorage . remove_root_span ( span_record . span_id )
145174
146175 # Also clean up any remaining pending children records (shouldn't be any, but be safe)
@@ -155,6 +184,17 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
155184 result
156185 end
157186
187+ # Check if this is a LiveView span from static render that should be filtered out
188+ defp static_render_liveview_span? ( span_record ) do
189+ # Check for the attribute we set in on_start
190+ Map . get ( span_record . attributes , :"sentry.liveview.static_render" , false ) == true
191+ end
192+
193+ # Check if the LiveViewPropagator module is loaded (only compiled when Phoenix.LiveView is available)
194+ defp liveview_propagator_loaded? do
195+ Code . ensure_loaded? ( Sentry.OpenTelemetry.LiveViewPropagator )
196+ end
197+
158198 @ impl :otel_span_processor
159199 def force_flush ( _config ) do
160200 :ok
@@ -169,11 +209,18 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
169209 end
170210
171211 # Helper function to detect if a span is a server span that should be
172- # treated as a transaction root for distributed tracing.
173- # This includes HTTP server request spans (have http.request.method attribute)
174- defp is_server_span? ( % { kind: :server , attributes: attributes } ) do
212+ # treated as a transaction root. This includes:
213+ # - HTTP server request spans (have http.request.method attribute)
214+ # - LiveView spans from OpentelemetryPhoenix (have kind: :server and origin: "opentelemetry_phoenix")
215+ # But NOT internal spans created with Tracer.with_span (which have kind: :internal)
216+ defp is_server_span? ( % { kind: :server , attributes: attributes } = span_record ) do
175217 # Check if it's an HTTP server request span (has http.request.method)
176- Map . has_key? ( attributes , to_string ( HTTPAttributes . http_request_method ( ) ) )
218+ has_http_method = Map . has_key? ( attributes , to_string ( HTTPAttributes . http_request_method ( ) ) )
219+
220+ # Check if it's a LiveView span from OpentelemetryPhoenix
221+ is_liveview_span = span_record . origin == "opentelemetry_phoenix"
222+
223+ has_http_method or is_liveview_span
177224 end
178225
179226 defp is_server_span? ( _ ) , do: false
0 commit comments