Repository: getsentry/sentry-go

Issue:
[OTel] Dynamic Sampling Context propagation does not work for nested spans
Extracted from #537.

### Problem

Dynamic Sampling Context propagation from nested spans doesn't work properly in OpenTelemetry integration.
Main reason: no publicly exposed mechanism to get the transaction (as in, a root span) for the given nested span. 
At the moment we're [doing this](https://github.com/getsentry/sentry-go/blob/2ece4821c5ab58cb10a385c8bbdc1270a5a0b432/otel/propagator.go#L45-L49), meaning that if we're doing a cross-service request from a nested span, the DSC won't be properly set.

### Potential Solutions

* Expose a method on Span that allows to get the root span (transaction) of the given span.


Discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eeba188..fcf654b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
 - Add Span.IsTransaction() ([#543](https://github.com/getsentry/sentry-go/pull/543))
 - Add TraceParentContext structure ([#545](https://github.com/getsentry/sentry-go/pull/545))
 - Add a new SpanOption: `SpanSampled` ([#546](https://github.com/getsentry/sentry-go/pull/546))
+- Add `Span.GetTransaction()` method ([#558](https://github.com/getsentry/sentry-go/pull/558))
 
 ## 0.17.0
 
diff --git a/tracing.go b/tracing.go
index 64734ef..3eeccef 100644
--- a/tracing.go
+++ b/tracing.go
@@ -241,6 +241,26 @@ func (s *Span) IsTransaction() bool {
 	return s.isTransaction
 }
 
+// GetTransaction returns the transaction that contains this span.
+//
+// For transaction spans it returns itself. For spans that were created manually
+// the method returns "nil".
+func (s *Span) GetTransaction() *Span {
+	spanRecorder := s.spanRecorder()
+	if spanRecorder == nil {
+		// This probably means that the Span was created manually (not via
+		// StartTransaction/StartSpan or StartChild).
+		// Return "nil" to indicate that it's not a normal situation.
+		return nil
+	}
+	recorderRoot := spanRecorder.root()
+	if recorderRoot == nil {
+		// Same as above: manually created Span.
+		return nil
+	}
+	return recorderRoot
+}
+
 // TODO(tracing): maybe add shortcuts to get/set transaction name. Right now the
 // transaction name is in the Scope, as it has existed there historically, prior
 // to tracing.



Test patch:
diff --git a/tracing_test.go b/tracing_test.go
index 256152d..dbd55f0 100644
--- a/tracing_test.go
+++ b/tracing_test.go
@@ -764,6 +764,7 @@ func TestParseTraceParentContext(t *testing.T) {
 	}
 
 	for _, tt := range tests {
+		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			traceParentContext, valid := ParseTraceParentContext([]byte(tt.sentryTrace))
 
@@ -776,3 +777,36 @@ func TestParseTraceParentContext(t *testing.T) {
 		})
 	}
 }
+
+func TestGetTransactionWithProperTransactionsSpans(t *testing.T) {
+	ctx := NewTestContext(ClientOptions{
+		EnableTracing: true,
+	})
+	transaction := StartTransaction(ctx, "transaction")
+	child1 := transaction.StartChild("child1")
+	child2 := transaction.StartChild("child2")
+	grandchild := child1.StartChild("grandchild")
+
+	assertEqual(t, transaction.GetTransaction(), transaction)
+	assertEqual(t, child1.GetTransaction(), transaction)
+	assertEqual(t, child2.GetTransaction(), transaction)
+	assertEqual(t, grandchild.GetTransaction(), transaction)
+
+	// Another transaction, unrelated to the first one
+	anotherTransaction := StartTransaction(ctx, "another transaction")
+
+	assertNotEqual(t, transaction, anotherTransaction)
+	assertEqual(t, anotherTransaction.GetTransaction(), anotherTransaction)
+}
+
+func TestGetTransactionReturnsNilOnManuallyCreatedSpans(t *testing.T) {
+	span1 := Span{}
+	if span1.GetTransaction() != nil {
+		t.Errorf("GetTransaction() should return nil on manually created Spans")
+	}
+
+	span2 := Span{isTransaction: true}
+	if span2.GetTransaction() != nil {
+		t.Errorf("GetTransaction() should return nil on manually created Spans")
+	}
+}