@@ -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
@@ -70,6 +92,10 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
7092 #
7193 # A span should NOT be a transaction root if:
7294 # - It has a LOCAL parent (parent span exists in our SpanStorage)
95+ #
96+ # This prevents LiveView spans from becoming separate transactions when
97+ # they occur within an HTTP request - they should be child spans of the
98+ # HTTP server span instead.
7399 is_transaction_root =
74100 cond do
75101 # No parent = definitely a root
@@ -142,6 +168,9 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
142168 end
143169
144170 # Clean up: remove the transaction root span and all its children
171+ # Note: For distributed tracing, the transaction root span may have been stored
172+ # as a child span (with a remote parent_span_id). In that case, we need to also
173+ # remove it from the child spans, not just look for it as a root span.
145174 :ok = SpanStorage . remove_root_span ( span_record . span_id )
146175
147176 # Also clean up any remaining pending children records (shouldn't be any, but be safe)
@@ -156,6 +185,17 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
156185 result
157186 end
158187
188+ # Check if this is a LiveView span from static render that should be filtered out
189+ defp static_render_liveview_span? ( span_record ) do
190+ # Check for the attribute we set in on_start
191+ Map . get ( span_record . attributes , :"sentry.liveview.static_render" , false ) == true
192+ end
193+
194+ # Check if the LiveViewPropagator module is loaded (only compiled when Phoenix.LiveView is available)
195+ defp liveview_propagator_loaded? do
196+ Code . ensure_loaded? ( Sentry.OpenTelemetry.LiveViewPropagator )
197+ end
198+
159199 @ impl :otel_span_processor
160200 def force_flush ( _config ) do
161201 :ok
@@ -170,11 +210,18 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
170210 end
171211
172212 # Helper function to detect if a span is a server span that should be
173- # treated as a transaction root for distributed tracing.
174- # This includes HTTP server request spans (have http.request.method attribute)
175- defp is_server_span? ( % { kind: :server , attributes: attributes } ) do
213+ # treated as a transaction root. This includes:
214+ # - HTTP server request spans (have http.request.method attribute)
215+ # - LiveView spans from OpentelemetryPhoenix (have kind: :server and origin: "opentelemetry_phoenix")
216+ # But NOT internal spans created with Tracer.with_span (which have kind: :internal)
217+ defp is_server_span? ( % { kind: :server , attributes: attributes } = span_record ) do
176218 # Check if it's an HTTP server request span (has http.request.method)
177- Map . has_key? ( attributes , to_string ( HTTPAttributes . http_request_method ( ) ) )
219+ has_http_method = Map . has_key? ( attributes , to_string ( HTTPAttributes . http_request_method ( ) ) )
220+
221+ # Check if it's a LiveView span from OpentelemetryPhoenix
222+ is_liveview_span = span_record . origin == "opentelemetry_phoenix"
223+
224+ has_http_method or is_liveview_span
178225 end
179226
180227 defp is_server_span? ( _ ) , do: false
0 commit comments