Repository: getsentry/sentry-go

Issue:
Add StartTransaction Function
## Summary
Today the Sentry Go SDK only provides support for [StartSpan](https://github.com/getsentry/sentry-go/blob/8fbb027528f9464e0233afa756cef0f99bd70906/doc.go#L34). However according to the Sentry model span(s) are sent to Sentry in transactions. 

StartTransaction function should be added to Go according to our develop documentation for SDKs.

[Guidelines for Performance Monitoring](https://develop.sentry.dev/sdk/performance/)

[Sentry.startTransaction function requirements](https://develop.sentry.dev/sdk/performance/#static-api-changes)

Consider [proposed changes](https://github.com/getsentry/develop/issues/690) currently being proposed here, regarding new manually created transactions automatically being attached to an existing trace or not

## Motivation
To improve the developer experience when instrument you application for Performance monitoring we want to provide functions so that developer do not need to manual construct HTTP requests.

## Additional Context
This is a crucial piece on the road to getting the GO SDK to have more feature parity for the Performance similar to other SDKs.


Discussion:

Solution patch:
diff --git a/http/sentryhttp.go b/http/sentryhttp.go
index a41b04a..86c5df3 100644
--- a/http/sentryhttp.go
+++ b/http/sentryhttp.go
@@ -86,15 +86,21 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc {
 			hub = sentry.CurrentHub().Clone()
 			ctx = sentry.SetHubOnContext(ctx, hub)
 		}
-		span := sentry.StartSpan(ctx, "http.server",
-			sentry.TransactionName(fmt.Sprintf("%s %s", r.Method, r.URL.Path)),
+		options := []sentry.SpanOption{
+			sentry.OpName("http.server"),
 			sentry.ContinueFromRequest(r),
+		}
+		// We don't mind getting an existing transaction back so we don't need to
+		// check if it is.
+		transaction := sentry.StartTransaction(ctx,
+			fmt.Sprintf("%s %s", r.Method, r.URL.Path),
+			options...,
 		)
-		defer span.Finish()
+		defer transaction.Finish()
 		// TODO(tracing): if the next handler.ServeHTTP panics, store
 		// information on the transaction accordingly (status, tag,
 		// level?, ...).
-		r = r.WithContext(span.Context())
+		r = r.WithContext(transaction.Context())
 		hub.Scope().SetRequest(r)
 		defer h.recoverWithSentry(hub, r)
 		// TODO(tracing): use custom response writer to intercept
diff --git a/tracing.go b/tracing.go
index 6c992fa..77666d4 100644
--- a/tracing.go
+++ b/tracing.go
@@ -567,6 +567,13 @@ func TransactionName(name string) SpanOption {
 	}
 }
 
+// OpName sets the operation name for a given span.
+func OpName(name string) SpanOption {
+	return func(s *Span) {
+		s.Op = name
+	}
+}
+
 // ContinueFromRequest returns a span option that updates the span to continue
 // an existing trace. If it cannot detect an existing trace in the request, the
 // span will be left unchanged.
@@ -626,3 +633,23 @@ func spanFromContext(ctx context.Context) *Span {
 	}
 	return nil
 }
+
+// StartTransaction will create a transaction (root span) if there's no existing
+// transaction in the context otherwise, it will return the existing transaction.
+func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span {
+	currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span)
+	if exists {
+		return currentTransaction
+	}
+	hub := GetHubFromContext(ctx)
+	if hub == nil {
+		hub = CurrentHub().Clone()
+		ctx = SetHubOnContext(ctx, hub)
+	}
+	options = append(options, TransactionName(name))
+	return StartSpan(
+		ctx,
+		"",
+		options...,
+	)
+}



Test patch:
diff --git a/tracing_test.go b/tracing_test.go
index 69819da..347a6dd 100644
--- a/tracing_test.go
+++ b/tracing_test.go
@@ -226,6 +226,78 @@ func TestStartChild(t *testing.T) {
 	}
 }
 
+func TestStartTransaction(t *testing.T) {
+	transport := &TransportMock{}
+	ctx := NewTestContext(ClientOptions{
+		Transport: transport,
+	})
+	transactionName := "Test Transaction"
+	description := "A Description"
+	status := SpanStatusOK
+	sampled := SampledTrue
+	startTime := time.Now()
+	endTime := startTime.Add(3 * time.Second)
+	data := map[string]interface{}{
+		"k": "v",
+	}
+	transaction := StartTransaction(ctx,
+		transactionName,
+		func(s *Span) {
+			s.Description = description
+			s.Status = status
+			s.Sampled = sampled
+			s.StartTime = startTime
+			s.EndTime = endTime
+			s.Data = data
+		},
+	)
+	transaction.Finish()
+
+	SpanCheck{
+		Sampled:     sampled,
+		RecorderLen: 1,
+	}.Check(t, transaction)
+
+	events := transport.Events()
+	if got := len(events); got != 1 {
+		t.Fatalf("sent %d events, want 1", got)
+	}
+	want := &Event{
+		Type:        transactionType,
+		Transaction: transactionName,
+		Contexts: map[string]Context{
+			"trace": TraceContext{
+				TraceID:     transaction.TraceID,
+				SpanID:      transaction.SpanID,
+				Description: description,
+				Status:      status,
+			}.Map(),
+		},
+		Tags: nil,
+		// TODO(tracing): the root span / transaction data field is
+		// mapped into Event.Extra for now, pending spec clarification.
+		// https://github.com/getsentry/develop/issues/244#issuecomment-778694182
+		Extra:     transaction.Data,
+		Timestamp: endTime,
+		StartTime: startTime,
+	}
+	opts := cmp.Options{
+		cmpopts.IgnoreFields(Event{},
+			"Contexts", "EventID", "Level", "Platform",
+			"Release", "Sdk", "ServerName", "Modules",
+		),
+		cmpopts.EquateEmpty(),
+	}
+	if diff := cmp.Diff(want, events[0], opts); diff != "" {
+		t.Fatalf("Event mismatch (-want +got):\n%s", diff)
+	}
+	// Check trace context explicitly, as we ignored all contexts above to
+	// disregard other contexts.
+	if diff := cmp.Diff(want.Contexts["trace"], events[0].Contexts["trace"]); diff != "" {
+		t.Fatalf("TraceContext mismatch (-want +got):\n%s", diff)
+	}
+}
+
 // testContextKey is used to store a value in a context so that we can check
 // that SDK operations on that context preserve the original context values.
 type testContextKey struct{}