Skip to content

Commit 4bbf9ee

Browse files
committed
wip - integrate LiveView detection into SpanProcessor
1 parent 21d5d21 commit 4bbf9ee

File tree

1 file changed

+52
-5
lines changed

1 file changed

+52
-5
lines changed

lib/sentry/opentelemetry/span_processor.ex

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)