Repository: getsentry/sentry-python

Issue:
Continue trace from ENV vars
It should be able to continue a trace with trace information that was given to the python process over environment variables. 

See this RFC for the spec: https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md

Discussion:

Patch:
diff --git a/Makefile b/Makefile
index a4d07279..2011b1b6 100644
--- a/Makefile
+++ b/Makefile
@@ -51,7 +51,7 @@ lint: .venv
 apidocs: .venv
 	@$(VENV_PATH)/bin/pip install --editable .
 	@$(VENV_PATH)/bin/pip install -U -r ./docs-requirements.txt
-	@$(VENV_PATH)/bin/sphinx-build -W -b html docs/ docs/_build
+	@$(VENV_PATH)/bin/sphinx-build -vv -W -b html docs/ docs/_build
 .PHONY: apidocs
 
 apidocs-hotfix: apidocs
diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py
index 4fa48956..0e56608d 100644
--- a/sentry_sdk/_compat.py
+++ b/sentry_sdk/_compat.py
@@ -82,7 +82,10 @@ def check_thread_support():
     if "threads" in opt:
         return
 
-    if str(opt.get("enable-threads", "0")).lower() in ("false", "off", "no", "0"):
+    # put here because of circular import
+    from sentry_sdk.consts import FALSE_VALUES
+
+    if str(opt.get("enable-threads", "0")).lower() in FALSE_VALUES:
         from warnings import warn
 
         warn(
diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
index ebe57194..0f276e05 100644
--- a/sentry_sdk/consts.py
+++ b/sentry_sdk/consts.py
@@ -45,6 +45,14 @@ DEFAULT_MAX_BREADCRUMBS = 100
 
 MATCH_ALL = r".*"
 
+FALSE_VALUES = [
+    "false",
+    "no",
+    "off",
+    "n",
+    "0",
+]
+
 
 class INSTRUMENTER:
     SENTRY = "sentry"
diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py
index c7ff1500..3ad61d31 100644
--- a/sentry_sdk/scope.py
+++ b/sentry_sdk/scope.py
@@ -1,6 +1,7 @@
 from copy import copy
 from collections import deque
 from itertools import chain
+import os
 import uuid
 
 from sentry_sdk.attachments import Attachment
@@ -19,6 +20,8 @@ from sentry_sdk.tracing import (
 from sentry_sdk._types import TYPE_CHECKING
 from sentry_sdk.utils import logger, capture_internal_exceptions
 
+from sentry_sdk.consts import FALSE_VALUES
+
 
 if TYPE_CHECKING:
     from typing import Any
@@ -122,7 +125,36 @@ class Scope(object):
         self._propagation_context = None  # type: Optional[Dict[str, Any]]
 
         self.clear()
-        self.generate_propagation_context()
+
+        incoming_trace_information = self._load_trace_data_from_env()
+        self.generate_propagation_context(incoming_data=incoming_trace_information)
+
+    def _load_trace_data_from_env(self):
+        # type: () -> Optional[Dict[str, str]]
+        """
+        Load Sentry trace id and baggage from environment variables.
+        Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false".
+        """
+        incoming_trace_information = None
+
+        sentry_use_environment = (
+            os.environ.get("SENTRY_USE_ENVIRONMENT") or ""
+        ).lower()
+        use_environment = sentry_use_environment not in FALSE_VALUES
+        if use_environment:
+            incoming_trace_information = {}
+
+            if os.environ.get("SENTRY_TRACE"):
+                incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = (
+                    os.environ.get("SENTRY_TRACE") or ""
+                )
+
+            if os.environ.get("SENTRY_BAGGAGE"):
+                incoming_trace_information[BAGGAGE_HEADER_NAME] = (
+                    os.environ.get("SENTRY_BAGGAGE") or ""
+                )
+
+        return incoming_trace_information or None
 
     def _extract_propagation_context(self, data):
         # type: (Dict[str, Any]) -> Optional[Dict[str, Any]]
@@ -141,6 +173,12 @@ class Scope(object):
             if sentrytrace_data is not None:
                 context.update(sentrytrace_data)
 
+        only_baggage_no_sentry_trace = (
+            "dynamic_sampling_context" in context and "trace_id" not in context
+        )
+        if only_baggage_no_sentry_trace:
+            context.update(self._create_new_propagation_context())
+
         if context:
             if not context.get("span_id"):
                 context["span_id"] = uuid.uuid4().hex[16:]


Test patch:
diff --git a/tests/test_scope.py b/tests/test_scope.py
index d90a89f4..8bdd46e0 100644
--- a/tests/test_scope.py
+++ b/tests/test_scope.py
@@ -1,7 +1,14 @@
 import copy
+import os
+import pytest
 from sentry_sdk import capture_exception
 from sentry_sdk.scope import Scope
 
+try:
+    from unittest import mock  # python 3.3 and above
+except ImportError:
+    import mock  # python < 3.3
+
 
 def test_copying():
     s1 = Scope()
@@ -62,3 +69,91 @@ def test_common_args():
     assert s2._extras == {"k": "v", "foo": "bar"}
     assert s2._tags == {"a": "b", "x": "y"}
     assert s2._contexts == {"os": {"name": "Blafasel"}, "device": {"a": "b"}}
+
+
+BAGGAGE_VALUE = (
+    "other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, "
+    "sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, "
+    "sentry-user_id=Am%C3%A9lie, other-vendor-value-2=foo;bar;"
+)
+
+SENTRY_TRACE_VALUE = "771a43a4192642f0b136d5159a501700-1234567890abcdef-1"
+
+
+@pytest.mark.parametrize(
+    "env,excepted_value",
+    [
+        (
+            {
+                "SENTRY_TRACE": SENTRY_TRACE_VALUE,
+            },
+            {
+                "sentry-trace": SENTRY_TRACE_VALUE,
+            },
+        ),
+        (
+            {
+                "SENTRY_BAGGAGE": BAGGAGE_VALUE,
+            },
+            {
+                "baggage": BAGGAGE_VALUE,
+            },
+        ),
+        (
+            {
+                "SENTRY_TRACE": SENTRY_TRACE_VALUE,
+                "SENTRY_BAGGAGE": BAGGAGE_VALUE,
+            },
+            {
+                "sentry-trace": SENTRY_TRACE_VALUE,
+                "baggage": BAGGAGE_VALUE,
+            },
+        ),
+        (
+            {
+                "SENTRY_USE_ENVIRONMENT": "",
+                "SENTRY_TRACE": SENTRY_TRACE_VALUE,
+                "SENTRY_BAGGAGE": BAGGAGE_VALUE,
+            },
+            {
+                "sentry-trace": SENTRY_TRACE_VALUE,
+                "baggage": BAGGAGE_VALUE,
+            },
+        ),
+        (
+            {
+                "SENTRY_USE_ENVIRONMENT": "True",
+                "SENTRY_TRACE": SENTRY_TRACE_VALUE,
+                "SENTRY_BAGGAGE": BAGGAGE_VALUE,
+            },
+            {
+                "sentry-trace": SENTRY_TRACE_VALUE,
+                "baggage": BAGGAGE_VALUE,
+            },
+        ),
+        (
+            {
+                "SENTRY_USE_ENVIRONMENT": "no",
+                "SENTRY_TRACE": SENTRY_TRACE_VALUE,
+                "SENTRY_BAGGAGE": BAGGAGE_VALUE,
+            },
+            None,
+        ),
+        (
+            {
+                "SENTRY_USE_ENVIRONMENT": "True",
+                "MY_OTHER_VALUE": "asdf",
+                "SENTRY_RELEASE": "1.0.0",
+            },
+            None,
+        ),
+    ],
+)
+def test_load_trace_data_from_env(env, excepted_value):
+    new_env = os.environ.copy()
+    new_env.update(env)
+
+    with mock.patch.dict(os.environ, new_env):
+        s = Scope()
+        incoming_trace_data = s._load_trace_data_from_env()
+        assert incoming_trace_data == excepted_value