Repository: getsentry/sentry-go

Issue:
Add beforeSendTransaction
Problem Statement
Similar to beforeSend, we will add beforeSendTransaction to allow people to strip Pii or drop transactions before they are sent to Sentry.

Solution Brainstorm
https://github.com/getsentry/sentry-javascript/pull/6121

Hints from issue comments:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4442cb5..81eff27 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 ## Unreleased
 
+### Features
+
+- Add `BeforeSendTransaction` hook to `ClientOptions` ([#517](https://github.com/getsentry/sentry-go/pull/517))
+  - Here's [an example](https://github.com/getsentry/sentry-go/blob/master/_examples/http/main.go#L56-L66) of how BeforeSendTransaction can be used to modify or drop transaction events.
+
 ### Bug Fixes
 
 - fix(dynamic-sampling): Do not crash in Span.Finish() when Client is empty [#520](https://github.com/getsentry/sentry-go/pull/520)
diff --git a/_examples/http/main.go b/_examples/http/main.go
index 25c1bbb..5c0af03 100644
--- a/_examples/http/main.go
+++ b/_examples/http/main.go
@@ -48,11 +48,22 @@ func run() error {
 		// Useful when getting started or trying to figure something out.
 		Debug: true,
 		BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
-			// Here you can inspect/modify events before they are sent.
+			// Here you can inspect/modify non-transaction events (for example, errors) before they are sent.
 			// Returning nil drops the event.
 			log.Printf("BeforeSend event [%s]", event.EventID)
 			return event
 		},
+		BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+			// Here you can inspect/modify transaction events before they are sent.
+			// Returning nil drops the event.
+			if strings.Contains(event.Message, "test-transaction") {
+				// Drop the transaction
+				return nil
+			}
+			event.Message += " [example]"
+			log.Printf("BeforeSendTransaction event [%s]", event.EventID)
+			return event
+		},
 		// Enable tracing
 		EnableTracing: true,
 		// Specify either a TracesSampleRate...
diff --git a/client.go b/client.go
index 2c580b0..49e2cd5 100644
--- a/client.go
+++ b/client.go
@@ -140,8 +140,10 @@ type ClientOptions struct {
 	SendDefaultPII bool
 	// BeforeSend is called before error events are sent to Sentry.
 	// Use it to mutate the event or return nil to discard the event.
-	// See EventProcessor if you need to mutate transactions.
 	BeforeSend func(event *Event, hint *EventHint) *Event
+	// BeforeSendTransaction is called before transaction events are sent to Sentry.
+	// Use it to mutate the transaction or return nil to discard the transaction.
+	BeforeSendTransaction func(event *Event, hint *EventHint) *Event
 	// Before breadcrumb add callback.
 	BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
 	// Integrations to be installed on the current Client, receives default
@@ -570,11 +572,18 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
 		return nil
 	}
 
-	// As per spec, transactions do not go through BeforeSend.
-	if event.Type != transactionType && options.BeforeSend != nil {
-		if hint == nil {
-			hint = &EventHint{}
+	// Apply beforeSend* processors
+	if hint == nil {
+		hint = &EventHint{}
+	}
+	if event.Type == transactionType && options.BeforeSendTransaction != nil {
+		// Transaction events
+		if event = options.BeforeSendTransaction(event, hint); event == nil {
+			Logger.Println("Transaction dropped due to BeforeSendTransaction callback.")
+			return nil
 		}
+	} else if event.Type != transactionType && options.BeforeSend != nil {
+		// All other events
 		if event = options.BeforeSend(event, hint); event == nil {
 			Logger.Println("Event dropped due to BeforeSend callback.")
 			return nil



Test patch:
diff --git a/client_test.go b/client_test.go
index 73f1f12..5afce8a 100644
--- a/client_test.go
+++ b/client_test.go
@@ -383,6 +383,60 @@ func TestBeforeSendGetAccessToEventHint(t *testing.T) {
 	assertEqual(t, transport.lastEvent.Message, "customComplexError: Foo 42")
 }
 
+func TestBeforeSendTransactionCanDropTransaction(t *testing.T) {
+	transport := &TransportMock{}
+	ctx := NewTestContext(ClientOptions{
+		EnableTracing:    true,
+		TracesSampleRate: 1.0,
+		Transport:        transport,
+		BeforeSend: func(event *Event, hint *EventHint) *Event {
+			t.Error("beforeSend should not be called")
+			return event
+		},
+		BeforeSendTransaction: func(event *Event, hint *EventHint) *Event {
+			assertEqual(t, event.Transaction, "Foo")
+			return nil
+		},
+	})
+
+	transaction := StartTransaction(ctx,
+		"Foo",
+	)
+	transaction.Finish()
+
+	if transport.lastEvent != nil {
+		t.Error("expected event to be dropped")
+	}
+}
+
+func TestBeforeSendTransactionIsCalled(t *testing.T) {
+	transport := &TransportMock{}
+	ctx := NewTestContext(ClientOptions{
+		EnableTracing:    true,
+		TracesSampleRate: 1.0,
+		Transport:        transport,
+		BeforeSend: func(event *Event, hint *EventHint) *Event {
+			t.Error("beforeSend should not be called")
+			return event
+		},
+		BeforeSendTransaction: func(event *Event, hint *EventHint) *Event {
+			assertEqual(t, event.Transaction, "Foo")
+			event.Transaction = "Bar"
+			return event
+		},
+	})
+
+	transaction := StartTransaction(ctx,
+		"Foo",
+	)
+	transaction.Finish()
+
+	lastEvent := transport.lastEvent
+	assertEqual(t, lastEvent.Transaction, "Bar")
+	// Make sure it's the same span
+	assertEqual(t, lastEvent.Contexts["trace"]["span_id"], transaction.SpanID)
+}
+
 func TestSampleRate(t *testing.T) {
 	tests := []struct {
 		SampleRate float64