Repository: getsentry/sentry-python

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
See https://github.com/getsentry/sentry-javascript/pull/6121



Discussion:
antonpirker: Docs: https://github.com/getsentry/sentry-docs/pull/6089

Patch:
diff --git a/codecov.yml b/codecov.yml
index 1989f1cd..1811996a 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -7,3 +7,6 @@ coverage:
       python:
         target: 90%
 comment: false
+ignore:
+  - "tests"
+  - "sentry_sdk/_types.py"
diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py
index 3c985f21..70641929 100644
--- a/sentry_sdk/_types.py
+++ b/sentry_sdk/_types.py
@@ -30,6 +30,7 @@ if MYPY:
     EventProcessor = Callable[[Event, Hint], Optional[Event]]
     ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]]
     BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]]
+    TransactionProcessor = Callable[[Event, Hint], Optional[Event]]
 
     TracesSampler = Callable[[SamplingContext], Union[float, int, bool]]
 
diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py
index 8af70031..e5df64fb 100644
--- a/sentry_sdk/client.py
+++ b/sentry_sdk/client.py
@@ -248,6 +248,19 @@ class _Client(object):
                     )
             event = new_event  # type: ignore
 
+        before_send_transaction = self.options["before_send_transaction"]
+        if before_send_transaction is not None and event.get("type") == "transaction":
+            new_event = None
+            with capture_internal_exceptions():
+                new_event = before_send_transaction(event, hint or {})
+            if new_event is None:
+                logger.info("before send transaction dropped event (%s)", event)
+                if self.transport:
+                    self.transport.record_lost_event(
+                        "before_send", data_category="transaction"
+                    )
+            event = new_event  # type: ignore
+
         return event
 
     def _is_ignored_error(self, event, hint):
diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
index eeca4cba..db50e058 100644
--- a/sentry_sdk/consts.py
+++ b/sentry_sdk/consts.py
@@ -20,6 +20,7 @@ if MYPY:
         Event,
         EventProcessor,
         TracesSampler,
+        TransactionProcessor,
     )
 
     # Experiments are feature flags to enable and disable certain unstable SDK
@@ -117,6 +118,7 @@ class ClientConstructor(object):
         _experiments={},  # type: Experiments  # noqa: B006
         proxy_headers=None,  # type: Optional[Dict[str, str]]
         instrumenter=INSTRUMENTER.SENTRY,  # type: Optional[str]
+        before_send_transaction=None,  # type: Optional[TransactionProcessor]
     ):
         # type: (...) -> None
         pass


Test patch:
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 8657231f..0d87e049 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -91,7 +91,79 @@ def test_event_id(sentry_init, capture_events):
     assert Hub.current.last_event_id() == event_id
 
 
-def test_option_callback(sentry_init, capture_events, monkeypatch):
+def test_option_before_send(sentry_init, capture_events):
+    def before_send(event, hint):
+        event["extra"] = {"before_send_called": True}
+        return event
+
+    def do_this():
+        try:
+            raise ValueError("aha!")
+        except Exception:
+            capture_exception()
+
+    sentry_init(before_send=before_send)
+    events = capture_events()
+
+    do_this()
+
+    (event,) = events
+    assert event["extra"] == {"before_send_called": True}
+
+
+def test_option_before_send_discard(sentry_init, capture_events):
+    def before_send_discard(event, hint):
+        return None
+
+    def do_this():
+        try:
+            raise ValueError("aha!")
+        except Exception:
+            capture_exception()
+
+    sentry_init(before_send=before_send_discard)
+    events = capture_events()
+
+    do_this()
+
+    assert len(events) == 0
+
+
+def test_option_before_send_transaction(sentry_init, capture_events):
+    def before_send_transaction(event, hint):
+        assert event["type"] == "transaction"
+        event["extra"] = {"before_send_transaction_called": True}
+        return event
+
+    sentry_init(
+        before_send_transaction=before_send_transaction,
+        traces_sample_rate=1.0,
+    )
+    events = capture_events()
+    transaction = start_transaction(name="foo")
+    transaction.finish()
+
+    (event,) = events
+    assert event["transaction"] == "foo"
+    assert event["extra"] == {"before_send_transaction_called": True}
+
+
+def test_option_before_send_transaction_discard(sentry_init, capture_events):
+    def before_send_transaction_discard(event, hint):
+        return None
+
+    sentry_init(
+        before_send_transaction=before_send_transaction_discard,
+        traces_sample_rate=1.0,
+    )
+    events = capture_events()
+    transaction = start_transaction(name="foo")
+    transaction.finish()
+
+    assert len(events) == 0
+
+
+def test_option_before_breadcrumb(sentry_init, capture_events, monkeypatch):
     drop_events = False
     drop_breadcrumbs = False
     reports = []