Repository: getsentry/sentry-python

Issue:
Improve debug message when dropping transaction
We log the following debug message when we drop a transaction that has no span recorder:

```
DEBUG: Discarding transaction because sampled = False
```

While having `sampled=False` is one potential cause for lacking a span recorder, transactions that have `sampled=True` can also output this debug message if they are not started with `start_transaction`. This behavior is confusing, and led to issue #2990 being opened, even though the behavior described in that issue is expected, since we require all transactions to be started with `start_transaction` in order to be sent to Sentry.

Therefore, before logging this message, we should check whether `sampled` is `False`. If it is, we can log the current message, otherwise we should log:

```
DEBUG: Discarding transaction because it was not started with `start_transaction`
```

The update needs to be made [here](https://github.com/getsentry/sentry-python/blob/411c9f31be419aa04a6fc5643716802453770bbb/sentry_sdk/tracing.py#L700).

We could also consider logging a warning when a user calls `__enter__` on a transaction that has not been started via `start_transaction` – the user likely intends to start the transaction in these cases.


Discussion:

Resolving patch:
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -747,9 +747,14 @@ def finish(self, hub=None, end_timestamp=None):
             # We have no active client and therefore nowhere to send this transaction.
             return None
 
-        # This is a de facto proxy for checking if sampled = False
         if self._span_recorder is None:
-            logger.debug("Discarding transaction because sampled = False")
+            # Explicit check against False needed because self.sampled might be None
+            if self.sampled is False:
+                logger.debug("Discarding transaction because sampled = False")
+            else:
+                logger.debug(
+                    "Discarding transaction because it was not started with sentry_sdk.start_transaction"
+                )
 
             # This is not entirely accurate because discards here are not
             # exclusively based on sample rate but also traces sampler, but


Test patch:
diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py
--- a/tests/tracing/test_misc.py
+++ b/tests/tracing/test_misc.py
@@ -362,3 +362,42 @@ def test_start_transaction_updates_scope_name_source(sentry_init):
     with start_transaction(name="foobar", source="route"):
         assert scope._transaction == "foobar"
         assert scope._transaction_info == {"source": "route"}
+
+
+@pytest.mark.parametrize("sampled", (True, None))
+def test_transaction_dropped_debug_not_started(sentry_init, sampled):
+    sentry_init(enable_tracing=True)
+
+    tx = Transaction(sampled=sampled)
+
+    with mock.patch("sentry_sdk.tracing.logger") as mock_logger:
+        with tx:
+            pass
+
+    mock_logger.debug.assert_any_call(
+        "Discarding transaction because it was not started with sentry_sdk.start_transaction"
+    )
+
+    with pytest.raises(AssertionError):
+        # We should NOT see the "sampled = False" message here
+        mock_logger.debug.assert_any_call(
+            "Discarding transaction because sampled = False"
+        )
+
+
+def test_transaction_dropeed_sampled_false(sentry_init):
+    sentry_init(enable_tracing=True)
+
+    tx = Transaction(sampled=False)
+
+    with mock.patch("sentry_sdk.tracing.logger") as mock_logger:
+        with sentry_sdk.start_transaction(tx):
+            pass
+
+    mock_logger.debug.assert_any_call("Discarding transaction because sampled = False")
+
+    with pytest.raises(AssertionError):
+        # We should not see the "not started" message here
+        mock_logger.debug.assert_any_call(
+            "Discarding transaction because it was not started with sentry_sdk.start_transaction"
+        )