@@ -821,4 +821,196 @@ defmodule Sentry.Opentelemetry.SpanProcessorTest do
821821 refute SpanStorage . span_exists? ( "completed_child" , table_name: table_name )
822822 end
823823 end
824+
825+ describe "span links" do
826+ @ tag span_storage: true
827+ test "root span with links includes links in trace context" do
828+ put_test_config ( environment_name: "test" , traces_sample_rate: 1.0 )
829+ Sentry.Test . start_collecting_sentry_reports ( )
830+
831+ # Create a source span and capture its context
832+ source_ctx =
833+ Tracer . with_span "source_span" do
834+ OpenTelemetry.Tracer . current_span_ctx ( )
835+ end
836+
837+ link = OpenTelemetry . link ( source_ctx )
838+
839+ # Create a new root span with a link to the source span
840+ Tracer . with_span "GET /api/linked" , % {
841+ kind: :server ,
842+ attributes: % {
843+ HTTPAttributes . http_request_method ( ) => :GET ,
844+ URLAttributes . url_path ( ) => "/api/linked"
845+ } ,
846+ links: [ link ]
847+ } do
848+ Process . sleep ( 10 )
849+ end
850+
851+ transactions = Sentry.Test . pop_sentry_transactions ( )
852+
853+ linked_tx =
854+ Enum . find ( transactions , fn tx -> tx . transaction == "GET /api/linked" end )
855+
856+ assert linked_tx != nil
857+
858+ trace_links = linked_tx . contexts . trace . links
859+ assert is_list ( trace_links )
860+ assert length ( trace_links ) == 1
861+
862+ [ span_link ] = trace_links
863+ assert String . match? ( span_link . trace_id , ~r/ ^[a-f0-9]{32}$/ )
864+ assert String . match? ( span_link . span_id , ~r/ ^[a-f0-9]{16}$/ )
865+ refute Map . has_key? ( span_link , :attributes )
866+ end
867+
868+ @ tag span_storage: true
869+ test "root span with links preserves link attributes" do
870+ put_test_config ( environment_name: "test" , traces_sample_rate: 1.0 )
871+ Sentry.Test . start_collecting_sentry_reports ( )
872+
873+ source_ctx =
874+ Tracer . with_span "source_span" do
875+ OpenTelemetry.Tracer . current_span_ctx ( )
876+ end
877+
878+ link = OpenTelemetry . link ( source_ctx , % { "my.key" => "my.value" } )
879+
880+ Tracer . with_span "GET /api/linked" , % {
881+ kind: :server ,
882+ attributes: % {
883+ HTTPAttributes . http_request_method ( ) => :GET ,
884+ URLAttributes . url_path ( ) => "/api/linked"
885+ } ,
886+ links: [ link ]
887+ } do
888+ Process . sleep ( 10 )
889+ end
890+
891+ transactions = Sentry.Test . pop_sentry_transactions ( )
892+
893+ linked_tx =
894+ Enum . find ( transactions , fn tx -> tx . transaction == "GET /api/linked" end )
895+
896+ [ span_link ] = linked_tx . contexts . trace . links
897+ assert span_link . attributes == % { "my.key" => "my.value" }
898+ end
899+
900+ @ tag span_storage: true
901+ test "child span with links includes links in the span struct" do
902+ put_test_config ( environment_name: "test" , traces_sample_rate: 1.0 )
903+ Sentry.Test . start_collecting_sentry_reports ( )
904+
905+ source_ctx =
906+ Tracer . with_span "source_span" do
907+ OpenTelemetry.Tracer . current_span_ctx ( )
908+ end
909+
910+ link = OpenTelemetry . link ( source_ctx )
911+
912+ Tracer . with_span "GET /api/parent" , % {
913+ kind: :server ,
914+ attributes: % {
915+ HTTPAttributes . http_request_method ( ) => :GET ,
916+ URLAttributes . url_path ( ) => "/api/parent"
917+ }
918+ } do
919+ Tracer . with_span "child_with_link" , % { links: [ link ] } do
920+ Process . sleep ( 10 )
921+ end
922+ end
923+
924+ transactions = Sentry.Test . pop_sentry_transactions ( )
925+
926+ parent_tx =
927+ Enum . find ( transactions , fn tx -> tx . transaction == "GET /api/parent" end )
928+
929+ assert length ( parent_tx . spans ) == 1
930+ [ child_span ] = parent_tx . spans
931+
932+ assert is_list ( child_span . links )
933+ assert length ( child_span . links ) == 1
934+
935+ [ span_link ] = child_span . links
936+ assert String . match? ( span_link . trace_id , ~r/ ^[a-f0-9]{32}$/ )
937+ assert String . match? ( span_link . span_id , ~r/ ^[a-f0-9]{16}$/ )
938+ end
939+
940+ @ tag span_storage: true
941+ test "spans without links have nil links" do
942+ put_test_config ( environment_name: "test" , traces_sample_rate: 1.0 )
943+ Sentry.Test . start_collecting_sentry_reports ( )
944+
945+ Tracer . with_span "GET /api/no-links" , % {
946+ kind: :server ,
947+ attributes: % {
948+ HTTPAttributes . http_request_method ( ) => :GET ,
949+ URLAttributes . url_path ( ) => "/api/no-links"
950+ }
951+ } do
952+ Tracer . with_span "child_span" do
953+ Process . sleep ( 10 )
954+ end
955+ end
956+
957+ [ transaction ] = Sentry.Test . pop_sentry_transactions ( )
958+
959+ refute Map . has_key? ( transaction . contexts . trace , :links )
960+ assert [ child_span ] = transaction . spans
961+ assert child_span . links == nil
962+ end
963+
964+ @ tag span_storage: true
965+ test "span with multiple links preserves all links" do
966+ put_test_config ( environment_name: "test" , traces_sample_rate: 1.0 )
967+ Sentry.Test . start_collecting_sentry_reports ( )
968+
969+ source_ctx_1 =
970+ Tracer . with_span "source_1" do
971+ OpenTelemetry.Tracer . current_span_ctx ( )
972+ end
973+
974+ source_ctx_2 =
975+ Tracer . with_span "source_2" do
976+ OpenTelemetry.Tracer . current_span_ctx ( )
977+ end
978+
979+ link_1 = OpenTelemetry . link ( source_ctx_1 )
980+ link_2 = OpenTelemetry . link ( source_ctx_2 , % { "order" => "second" } )
981+
982+ Tracer . with_span "GET /api/multi-linked" , % {
983+ kind: :server ,
984+ attributes: % {
985+ HTTPAttributes . http_request_method ( ) => :GET ,
986+ URLAttributes . url_path ( ) => "/api/multi-linked"
987+ } ,
988+ links: [ link_1 , link_2 ]
989+ } do
990+ Process . sleep ( 10 )
991+ end
992+
993+ transactions = Sentry.Test . pop_sentry_transactions ( )
994+
995+ linked_tx =
996+ Enum . find ( transactions , fn tx -> tx . transaction == "GET /api/multi-linked" end )
997+
998+ trace_links = linked_tx . contexts . trace . links
999+ assert length ( trace_links ) == 2
1000+
1001+ # Both links should have valid trace/span IDs
1002+ Enum . each ( trace_links , fn link ->
1003+ assert String . match? ( link . trace_id , ~r/ ^[a-f0-9]{32}$/ )
1004+ assert String . match? ( link . span_id , ~r/ ^[a-f0-9]{16}$/ )
1005+ end )
1006+
1007+ # The two links should point to different spans
1008+ span_ids = Enum . map ( trace_links , & & 1 . span_id )
1009+ assert length ( Enum . uniq ( span_ids ) ) == 2
1010+
1011+ # The link with attributes should preserve them
1012+ link_with_attrs = Enum . find ( trace_links , & Map . has_key? ( & 1 , :attributes ) )
1013+ assert link_with_attrs . attributes == % { "order" => "second" }
1014+ end
1015+ end
8241016end
0 commit comments