11defmodule Sentry.Opentelemetry.SpanProcessorTest do
22 use Sentry.Case , async: false
33
4+ require OpenTelemetry.Tracer , as: Tracer
5+ require OpenTelemetry.SemConv.Incubating.HTTPAttributes , as: HTTPAttributes
6+ require OpenTelemetry.SemConv.Incubating.URLAttributes , as: URLAttributes
7+
48 import Sentry.TestHelpers
59
610 alias Sentry.OpenTelemetry.SpanStorage
@@ -188,8 +192,6 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do
188192
189193 Sentry.Test . start_collecting_sentry_reports ( )
190194
191- require OpenTelemetry.Tracer , as: Tracer
192-
193195 Tracer . with_span "root_span" do
194196 Tracer . with_span "level_1_child" do
195197 Tracer . with_span "level_2_child" do
@@ -247,8 +249,6 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do
247249 tasks =
248250 Enum . map ( 1 .. 20 , fn i ->
249251 Task . async ( fn ->
250- require OpenTelemetry.Tracer , as: Tracer
251-
252252 Tracer . with_span "concurrent_root_#{ i } " do
253253 Tracer . with_span "concurrent_child_#{ i } " do
254254 Process . sleep ( 10 )
@@ -286,8 +286,6 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do
286286
287287 Sentry.Test . start_collecting_sentry_reports ( )
288288
289- require OpenTelemetry.Tracer , as: Tracer
290-
291289 Tracer . with_span "root_span" do
292290 Tracer . with_span "child_instrumented_function_one" do
293291 Process . sleep ( 10 )
@@ -310,5 +308,145 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do
310308
311309 Application . put_env ( :opentelemetry , :sampler , original_sampler )
312310 end
311+
312+ @ tag span_storage: true
313+ test "treats HTTP server request spans as transaction roots for distributed tracing" do
314+ put_test_config ( environment_name: "test" , traces_sample_rate: 1.0 )
315+
316+ Sentry.Test . start_collecting_sentry_reports ( )
317+
318+ # Simulate an incoming HTTP request with an external parent span ID (from browser/client)
319+ # This represents a distributed trace where the client started the trace
320+ external_trace_id = 0x1234567890ABCDEF1234567890ABCDEF
321+ external_parent_span_id = 0xABCDEF1234567890
322+
323+ # Create a remote parent span context using :otel_tracer.from_remote_span
324+ remote_parent = :otel_tracer . from_remote_span ( external_trace_id , external_parent_span_id , 1 )
325+
326+ ctx = Tracer . set_current_span ( :otel_ctx . new ( ) , remote_parent )
327+
328+ # Start an HTTP server span with the remote parent context
329+ Tracer . with_span ctx , "POST /api/users" , % {
330+ kind: :server ,
331+ attributes: % {
332+ HTTPAttributes . http_request_method ( ) => :POST ,
333+ URLAttributes . url_path ( ) => "/api/users" ,
334+ "http.route" => "/api/users" ,
335+ "server.address" => "localhost" ,
336+ "server.port" => 4000
337+ }
338+ } do
339+ # Simulate child spans (database queries, etc.)
340+ Tracer . with_span "db.query:users" , % {
341+ kind: :client ,
342+ attributes: % {
343+ "db.system" => :postgresql ,
344+ "db.statement" => "INSERT INTO users (name) VALUES ($1)"
345+ }
346+ } do
347+ Process . sleep ( 10 )
348+ end
349+
350+ Tracer . with_span "db.query:notifications" , % {
351+ kind: :client ,
352+ attributes: % {
353+ "db.system" => :postgresql ,
354+ "db.statement" => "INSERT INTO notifications (user_id) VALUES ($1)"
355+ }
356+ } do
357+ Process . sleep ( 10 )
358+ end
359+ end
360+
361+ # Should capture the HTTP request span as a transaction root despite having an external parent
362+ assert [ % Sentry.Transaction { } = transaction ] = Sentry.Test . pop_sentry_transactions ( )
363+
364+ # Verify transaction properties
365+ assert transaction . transaction == "POST /api/users"
366+ assert transaction . transaction_info == % { source: :custom }
367+ assert length ( transaction . spans ) == 2
368+
369+ # Verify child spans are properly included
370+ span_ops = Enum . map ( transaction . spans , & & 1 . op ) |> Enum . sort ( )
371+ assert span_ops == [ "db" , "db" ]
372+
373+ # Verify child spans have detailed data (like SQL queries)
374+ [ span1 , span2 ] = transaction . spans
375+ assert span1 . description =~ "INSERT INTO"
376+ assert span2 . description =~ "INSERT INTO"
377+ assert span1 . data [ "db.system" ] == :postgresql
378+ assert span2 . data [ "db.system" ] == :postgresql
379+ assert span1 . data [ "db.statement" ] =~ "INSERT INTO users"
380+ assert span2 . data [ "db.statement" ] =~ "INSERT INTO notifications"
381+
382+ # Verify all spans share the same trace ID (from the external parent)
383+ trace_id = transaction . contexts . trace . trace_id
384+
385+ Enum . each ( transaction . spans , fn span ->
386+ assert span . trace_id == trace_id
387+ end )
388+
389+ # The transaction should have the external parent's trace ID
390+ assert transaction . contexts . trace . trace_id ==
391+ "1234567890abcdef1234567890abcdef"
392+ end
393+
394+ @ tag span_storage: true
395+ test "cleans up HTTP server span and children after sending distributed trace transaction" , % {
396+ table_name: table_name
397+ } do
398+ put_test_config ( environment_name: "test" , traces_sample_rate: 1.0 )
399+
400+ Sentry.Test . start_collecting_sentry_reports ( )
401+
402+ # Simulate an incoming HTTP request with an external parent span ID (from browser/client)
403+ external_trace_id = 0x1234567890ABCDEF1234567890ABCDEF
404+ external_parent_span_id = 0xABCDEF1234567890
405+
406+ remote_parent = :otel_tracer . from_remote_span ( external_trace_id , external_parent_span_id , 1 )
407+ ctx = Tracer . set_current_span ( :otel_ctx . new ( ) , remote_parent )
408+
409+ # Start an HTTP server span with the remote parent context
410+ Tracer . with_span ctx , "POST /api/users" , % {
411+ kind: :server ,
412+ attributes: % {
413+ HTTPAttributes . http_request_method ( ) => :POST ,
414+ URLAttributes . url_path ( ) => "/api/users"
415+ }
416+ } do
417+ # Simulate child spans (database queries, etc.)
418+ Tracer . with_span "db.query:users" , % {
419+ kind: :client ,
420+ attributes: % {
421+ "db.system" => :postgresql ,
422+ "db.statement" => "INSERT INTO users (name) VALUES ($1)"
423+ }
424+ } do
425+ Process . sleep ( 10 )
426+ end
427+ end
428+
429+ # Should capture the HTTP request span as a transaction
430+ assert [ % Sentry.Transaction { } = transaction ] = Sentry.Test . pop_sentry_transactions ( )
431+
432+ # Verify the HTTP server span was removed from storage
433+ # (even though it was stored as a child span due to having a remote parent)
434+ http_server_span_id = transaction . contexts . trace . span_id
435+ remote_parent_span_id_str = "abcdef1234567890"
436+
437+ # The HTTP server span should not exist in storage anymore
438+ assert SpanStorage . get_root_span ( http_server_span_id , table_name: table_name ) == nil
439+
440+ # Check that it was also removed from child spans storage
441+ # We can't directly check if a specific child was removed, but we can verify
442+ # that get_child_spans for the remote parent returns empty (or doesn't include our span)
443+ remaining_children =
444+ SpanStorage . get_child_spans ( remote_parent_span_id_str , table_name: table_name )
445+
446+ refute Enum . any? ( remaining_children , fn span -> span . span_id == http_server_span_id end )
447+
448+ # Verify child spans of the HTTP server span were also removed
449+ assert [ ] == SpanStorage . get_child_spans ( http_server_span_id , table_name: table_name )
450+ end
313451 end
314452end
0 commit comments