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