Repository: getsentry/sentry-python

Issue:
parent_span_id being attached to root celerybeat transactions
### Environment

How do you use Sentry?
Sentry SaaS (sentry.io)

Which SDK and version?
sentry-python==1.0.0

### Steps to Reproduce

https://gist.github.com/wmak/fffab40bdbe5329f2ef8bd0a4ff44659
1. Repro project attached, a very basic flask app with an endpoint that starts a celery task + a celerybeat that starts the same task
2. Setup the repro project
3. Run redis on 6379
4. Start the celery worker `celery -A main.celery worker`
5. Start celery beat `celery -A main.celery beat --loglevel=INFO --pidfile=''`
6. Wait for the task to be scheduled

### Expected Result

I don't expect for celery tasks started by celerybeat to have the parent_span_id set to a span_id, especially when there isn't a corresponding transaction with that span to make the association, the repro attached here is flask + celerybeat. But also happens with Django + celerybeat

### Actual Result

The parent_span_id was set in the trace context, so the transaction appears to be orphaned, when it in fact should be a root event instead
<img width="1283" alt="image" src="https://user-images.githubusercontent.com/4205004/115637367-fd88b880-a2dd-11eb-875d-592aae657bd8.png">



Discussion:
This issue has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you label it `Status: Backlog` or `Status: In Progress`, I will leave it alone ... forever!

----

"A weed is but an unloved flower." ― _Ella Wheeler Wilcox_ 🥀
Hello @wmak 
Thanks for writting in. Sorry this issue slipped through the cracks.
This is a really good point, and I think this is a bug that we should address.

As this bug report is already quite old, could you please verify this with the latest Sentry SDK? Thanks!
This issue has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you label it `Status: Backlog` or `Status: In Progress`, I will leave it alone ... forever!

----

"A weed is but an unloved flower." ― _Ella Wheeler Wilcox_ 🥀

Resolving patch:
diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py
--- a/sentry_sdk/integrations/celery.py
+++ b/sentry_sdk/integrations/celery.py
@@ -30,6 +30,7 @@
     from typing import TypeVar
     from typing import Union
 
+    from sentry_sdk.tracing import Span
     from sentry_sdk._types import EventProcessor, Event, Hint, ExcInfo
 
     F = TypeVar("F", bound=Callable[..., Any])
@@ -133,6 +134,16 @@ def _now_seconds_since_epoch():
     return time.time()
 
 
+class NoOpMgr:
+    def __enter__(self):
+        # type: () -> None
+        return None
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        # type: (Any, Any, Any) -> None
+        return None
+
+
 def _wrap_apply_async(f):
     # type: (F) -> F
     @wraps(f)
@@ -154,11 +165,26 @@ def apply_async(*args, **kwargs):
         if not propagate_traces:
             return f(*args, **kwargs)
 
-        with hub.start_span(
-            op=OP.QUEUE_SUBMIT_CELERY, description=args[0].name
-        ) as span:
+        try:
+            task_started_from_beat = args[1][0] == "BEAT"
+        except IndexError:
+            task_started_from_beat = False
+
+        task = args[0]
+
+        span_mgr = (
+            hub.start_span(op=OP.QUEUE_SUBMIT_CELERY, description=task.name)
+            if not task_started_from_beat
+            else NoOpMgr()
+        )  # type: Union[Span, NoOpMgr]
+
+        with span_mgr as span:
             with capture_internal_exceptions():
-                headers = dict(hub.iter_trace_propagation_headers(span))
+                headers = (
+                    dict(hub.iter_trace_propagation_headers(span))
+                    if span is not None
+                    else {}
+                )
                 if integration.monitor_beat_tasks:
                     headers.update(
                         {


Test patch:
diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py
--- a/tests/integrations/celery/test_celery.py
+++ b/tests/integrations/celery/test_celery.py
@@ -3,7 +3,11 @@
 import pytest
 
 from sentry_sdk import Hub, configure_scope, start_transaction, get_current_span
-from sentry_sdk.integrations.celery import CeleryIntegration, _get_headers
+from sentry_sdk.integrations.celery import (
+    CeleryIntegration,
+    _get_headers,
+    _wrap_apply_async,
+)
 
 from sentry_sdk._compat import text_type
 
@@ -555,3 +559,37 @@ def dummy_task(self, message):
             headers={"sentry-propagate-traces": False},
         ).get()
         assert transaction_trace_id != task_transaction_id
+
+
+def test_apply_async_manually_span(sentry_init):
+    sentry_init(
+        integrations=[CeleryIntegration()],
+    )
+
+    def dummy_function(*args, **kwargs):
+        headers = kwargs.get("headers")
+        assert "sentry-trace" in headers
+        assert "baggage" in headers
+
+    wrapped = _wrap_apply_async(dummy_function)
+    wrapped(mock.MagicMock(), (), headers={})
+
+
+def test_apply_async_from_beat_no_span(sentry_init):
+    sentry_init(
+        integrations=[CeleryIntegration()],
+    )
+
+    def dummy_function(*args, **kwargs):
+        headers = kwargs.get("headers")
+        assert "sentry-trace" not in headers
+        assert "baggage" not in headers
+
+    wrapped = _wrap_apply_async(dummy_function)
+    wrapped(
+        mock.MagicMock(),
+        [
+            "BEAT",
+        ],
+        headers={},
+    )