From 7f53ab3f70dcc48666d2182b8e2d9033da6daf01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:05:55 +0200 Subject: [PATCH 01/11] build(deps): bump actions/cache from 2 to 3 (#1478) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38ec4b9834..1f8ad34d98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: with: python-version: 3.9 - name: Setup build cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: build_cache with: path: ${{ env.CACHED_BUILD_PATHS }} From 8ce4194848165a51a15a5af09a2bdb912eef750b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:30:41 +0200 Subject: [PATCH 02/11] build(deps): bump mypy from 0.950 to 0.961 (#1464) --- linter-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linter-requirements.txt b/linter-requirements.txt index ec736a59c5..edabda68c3 100644 --- a/linter-requirements.txt +++ b/linter-requirements.txt @@ -1,7 +1,7 @@ black==22.3.0 flake8==3.9.2 flake8-import-order==0.18.1 -mypy==0.950 +mypy==0.961 types-certifi types-redis types-setuptools From 8926abfe62841772ab9c45a36ab61ae68239fae5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jun 2022 16:04:13 +0000 Subject: [PATCH 03/11] build(deps): bump actions/setup-python from 3 to 4 (#1465) --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f8ad34d98..8007cdaa7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: python-version: 3.9 @@ -86,7 +86,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -118,7 +118,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: 3.9 - name: Setup build cache @@ -160,7 +160,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: 3.9 From b8f4eeece1692895d54efb94a889a6d2cd166728 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jun 2022 19:03:03 +0200 Subject: [PATCH 04/11] build(deps): bump pep8-naming from 0.11.1 to 0.13.0 (#1457) --- linter-requirements.txt | 2 +- sentry_sdk/_queue.py | 26 +++++++++++++------------- sentry_sdk/integrations/__init__.py | 2 +- sentry_sdk/utils.py | 2 +- sentry_sdk/worker.py | 6 +++--- tests/test_client.py | 14 +++++++------- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/linter-requirements.txt b/linter-requirements.txt index edabda68c3..53edc6477f 100644 --- a/linter-requirements.txt +++ b/linter-requirements.txt @@ -6,5 +6,5 @@ types-certifi types-redis types-setuptools flake8-bugbear==21.4.3 -pep8-naming==0.11.1 +pep8-naming==0.13.0 pre-commit # local linting diff --git a/sentry_sdk/_queue.py b/sentry_sdk/_queue.py index e368da2229..fc845f70d1 100644 --- a/sentry_sdk/_queue.py +++ b/sentry_sdk/_queue.py @@ -21,15 +21,15 @@ if MYPY: from typing import Any -__all__ = ["Empty", "Full", "Queue"] +__all__ = ["EmptyError", "FullError", "Queue"] -class Empty(Exception): +class EmptyError(Exception): "Exception raised by Queue.get(block=0)/get_nowait()." pass -class Full(Exception): +class FullError(Exception): "Exception raised by Queue.put(block=0)/put_nowait()." pass @@ -134,16 +134,16 @@ def put(self, item, block=True, timeout=None): If optional args 'block' is true and 'timeout' is None (the default), block if necessary until a free slot is available. If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises - the Full exception if no free slot was available within that time. + the FullError exception if no free slot was available within that time. Otherwise ('block' is false), put an item on the queue if a free slot - is immediately available, else raise the Full exception ('timeout' + is immediately available, else raise the FullError exception ('timeout' is ignored in that case). """ with self.not_full: if self.maxsize > 0: if not block: if self._qsize() >= self.maxsize: - raise Full() + raise FullError() elif timeout is None: while self._qsize() >= self.maxsize: self.not_full.wait() @@ -154,7 +154,7 @@ def put(self, item, block=True, timeout=None): while self._qsize() >= self.maxsize: remaining = endtime - time() if remaining <= 0.0: - raise Full + raise FullError() self.not_full.wait(remaining) self._put(item) self.unfinished_tasks += 1 @@ -166,15 +166,15 @@ def get(self, block=True, timeout=None): If optional args 'block' is true and 'timeout' is None (the default), block if necessary until an item is available. If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises - the Empty exception if no item was available within that time. + the EmptyError exception if no item was available within that time. Otherwise ('block' is false), return an item if one is immediately - available, else raise the Empty exception ('timeout' is ignored + available, else raise the EmptyError exception ('timeout' is ignored in that case). """ with self.not_empty: if not block: if not self._qsize(): - raise Empty() + raise EmptyError() elif timeout is None: while not self._qsize(): self.not_empty.wait() @@ -185,7 +185,7 @@ def get(self, block=True, timeout=None): while not self._qsize(): remaining = endtime - time() if remaining <= 0.0: - raise Empty() + raise EmptyError() self.not_empty.wait(remaining) item = self._get() self.not_full.notify() @@ -195,7 +195,7 @@ def put_nowait(self, item): """Put an item into the queue without blocking. Only enqueue the item if a free slot is immediately available. - Otherwise raise the Full exception. + Otherwise raise the FullError exception. """ return self.put(item, block=False) @@ -203,7 +203,7 @@ def get_nowait(self): """Remove and return an item from the queue without blocking. Only get an item if one is immediately available. Otherwise - raise the Empty exception. + raise the EmptyError exception. """ return self.get(block=False) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 114a3a1f41..68445d3416 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -146,7 +146,7 @@ def setup_integrations( return integrations -class DidNotEnable(Exception): +class DidNotEnable(Exception): # noqa: N818 """ The integration could not be enabled due to a trivial user error like `flask` not being installed for the `FlaskIntegration`. diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 0a735a1e20..38ba4d7857 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -931,7 +931,7 @@ def transaction_from_function(func): disable_capture_event = ContextVar("disable_capture_event") -class ServerlessTimeoutWarning(Exception): +class ServerlessTimeoutWarning(Exception): # noqa: N818 """Raised when a serverless method is about to reach its timeout.""" pass diff --git a/sentry_sdk/worker.py b/sentry_sdk/worker.py index a06fb8f0d1..310ba3bfb4 100644 --- a/sentry_sdk/worker.py +++ b/sentry_sdk/worker.py @@ -3,7 +3,7 @@ from time import sleep, time from sentry_sdk._compat import check_thread_support -from sentry_sdk._queue import Queue, Full +from sentry_sdk._queue import Queue, FullError from sentry_sdk.utils import logger from sentry_sdk.consts import DEFAULT_QUEUE_SIZE @@ -81,7 +81,7 @@ def kill(self): if self._thread: try: self._queue.put_nowait(_TERMINATOR) - except Full: + except FullError: logger.debug("background worker queue full, kill failed") self._thread = None @@ -114,7 +114,7 @@ def submit(self, callback): try: self._queue.put_nowait(callback) return True - except Full: + except FullError: return False def _target(self): diff --git a/tests/test_client.py b/tests/test_client.py index ffdb831e39..5523647870 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -35,13 +35,13 @@ from collections.abc import Mapping -class EventCaptured(Exception): +class EventCapturedError(Exception): pass class _TestTransport(Transport): def capture_event(self, event): - raise EventCaptured(event) + raise EventCapturedError(event) def test_transport_option(monkeypatch): @@ -273,7 +273,7 @@ def e(exc): e(ZeroDivisionError()) e(MyDivisionError()) - pytest.raises(EventCaptured, lambda: e(ValueError())) + pytest.raises(EventCapturedError, lambda: e(ValueError())) def test_with_locals_enabled(sentry_init, capture_events): @@ -400,8 +400,8 @@ def test_attach_stacktrace_disabled(sentry_init, capture_events): def test_capture_event_works(sentry_init): sentry_init(transport=_TestTransport()) - pytest.raises(EventCaptured, lambda: capture_event({})) - pytest.raises(EventCaptured, lambda: capture_event({})) + pytest.raises(EventCapturedError, lambda: capture_event({})) + pytest.raises(EventCapturedError, lambda: capture_event({})) @pytest.mark.parametrize("num_messages", [10, 20]) @@ -744,10 +744,10 @@ def test_errno_errors(sentry_init, capture_events): sentry_init() events = capture_events() - class Foo(Exception): + class FooError(Exception): errno = 69 - capture_exception(Foo()) + capture_exception(FooError()) (event,) = events From 5ea8d6bb55807ad2de17fff9b7547fedeaa6ca74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 13:12:58 +0000 Subject: [PATCH 05/11] build(deps): bump sphinx from 4.5.0 to 5.0.2 (#1470) --- docs-requirements.txt | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index f80c689cbf..fdb9fe783f 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,4 +1,4 @@ -sphinx==4.5.0 +sphinx==5.0.2 sphinx-rtd-theme sphinx-autodoc-typehints[type_comments]>=1.8.0 typing-extensions diff --git a/docs/conf.py b/docs/conf.py index b9bff46a05..f11efb4023 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,7 +67,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From 52e80f0c5c3b0ac9545e24eef0f06df9aaf9cbd0 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 8 Jul 2022 16:08:55 +0200 Subject: [PATCH 06/11] feat(tracing): Dynamic Sampling Context / Baggage continuation (#1485) * `Baggage` class implementing sentry/third party/mutable logic with parsing from header and serialization * Parse incoming `baggage` header while starting transaction and store it on the transaction * Extract `dynamic_sampling_context` fields and add to the `trace` field in the envelope header while sending the transaction * Propagate the `baggage` header (only sentry fields / no third party as per spec) [DSC Spec](https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/) --- docs/conf.py | 16 +-- sentry_sdk/client.py | 20 +++- sentry_sdk/tracing.py | 33 ++++++- sentry_sdk/tracing_utils.py | 114 +++++++++++++++++++--- tests/integrations/stdlib/test_httplib.py | 41 ++++++-- tests/tracing/test_baggage.py | 67 +++++++++++++ tests/tracing/test_integration_tests.py | 57 ++++++++--- 7 files changed, 294 insertions(+), 54 deletions(-) create mode 100644 tests/tracing/test_baggage.py diff --git a/docs/conf.py b/docs/conf.py index f11efb4023..c3ba844ec7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,9 +25,9 @@ # -- Project information ----------------------------------------------------- -project = u"sentry-python" -copyright = u"2019, Sentry Team and Contributors" -author = u"Sentry Team and Contributors" +project = "sentry-python" +copyright = "2019, Sentry Team and Contributors" +author = "Sentry Team and Contributors" release = "1.6.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. @@ -72,7 +72,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [u"_build", "Thumbs.db", ".DS_Store"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None @@ -140,8 +140,8 @@ ( master_doc, "sentry-python.tex", - u"sentry-python Documentation", - u"Sentry Team and Contributors", + "sentry-python Documentation", + "Sentry Team and Contributors", "manual", ) ] @@ -151,7 +151,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "sentry-python", u"sentry-python Documentation", [author], 1)] +man_pages = [(master_doc, "sentry-python", "sentry-python Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -163,7 +163,7 @@ ( master_doc, "sentry-python", - u"sentry-python Documentation", + "sentry-python Documentation", author, "sentry-python", "One line description of project.", diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 63a1205f57..510225aa9a 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -373,6 +373,12 @@ def capture_event( event_opt.get("contexts", {}).get("trace", {}).pop("tracestate", "") ) + dynamic_sampling_context = ( + event_opt.get("contexts", {}) + .get("trace", {}) + .pop("dynamic_sampling_context", {}) + ) + # Transactions or events with attachments should go to the /envelope/ # endpoint. if is_transaction or attachments: @@ -382,11 +388,15 @@ def capture_event( "sent_at": format_timestamp(datetime.utcnow()), } - tracestate_data = raw_tracestate and reinflate_tracestate( - raw_tracestate.replace("sentry=", "") - ) - if tracestate_data and has_tracestate_enabled(): - headers["trace"] = tracestate_data + if has_tracestate_enabled(): + tracestate_data = raw_tracestate and reinflate_tracestate( + raw_tracestate.replace("sentry=", "") + ) + + if tracestate_data: + headers["trace"] = tracestate_data + elif dynamic_sampling_context: + headers["trace"] = dynamic_sampling_context envelope = Envelope(headers=headers) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index f6f625acc8..fe53386597 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -215,7 +215,7 @@ def continue_from_environ( # type: (...) -> Transaction """ Create a Transaction with the given params, then add in data pulled from - the 'sentry-trace' and 'tracestate' headers from the environ (if any) + the 'sentry-trace', 'baggage' and 'tracestate' headers from the environ (if any) before returning the Transaction. This is different from `continue_from_headers` in that it assumes header @@ -238,7 +238,7 @@ def continue_from_headers( # type: (...) -> Transaction """ Create a transaction with the given params (including any data pulled from - the 'sentry-trace' and 'tracestate' headers). + the 'sentry-trace', 'baggage' and 'tracestate' headers). """ # TODO move this to the Transaction class if cls is Span: @@ -247,7 +247,17 @@ def continue_from_headers( "instead of Span.continue_from_headers." ) - kwargs.update(extract_sentrytrace_data(headers.get("sentry-trace"))) + # TODO-neel move away from this kwargs stuff, it's confusing and opaque + # make more explicit + baggage = Baggage.from_incoming_header(headers.get("baggage")) + kwargs.update({"baggage": baggage}) + + sentrytrace_kwargs = extract_sentrytrace_data(headers.get("sentry-trace")) + + if sentrytrace_kwargs is not None: + kwargs.update(sentrytrace_kwargs) + baggage.freeze + kwargs.update(extract_tracestate_data(headers.get("tracestate"))) transaction = Transaction(**kwargs) @@ -258,7 +268,7 @@ def continue_from_headers( def iter_headers(self): # type: () -> Iterator[Tuple[str, str]] """ - Creates a generator which returns the span's `sentry-trace` and + Creates a generator which returns the span's `sentry-trace`, `baggage` and `tracestate` headers. If the span's containing transaction doesn't yet have a @@ -274,6 +284,9 @@ def iter_headers(self): if tracestate: yield "tracestate", tracestate + if self.containing_transaction and self.containing_transaction._baggage: + yield "baggage", self.containing_transaction._baggage.serialize() + @classmethod def from_traceparent( cls, @@ -460,7 +473,7 @@ def get_trace_context(self): "parent_span_id": self.parent_span_id, "op": self.op, "description": self.description, - } + } # type: Dict[str, Any] if self.status: rv["status"] = self.status @@ -473,6 +486,12 @@ def get_trace_context(self): if sentry_tracestate: rv["tracestate"] = sentry_tracestate + # TODO-neel populate fresh if head SDK + if self.containing_transaction and self.containing_transaction._baggage: + rv[ + "dynamic_sampling_context" + ] = self.containing_transaction._baggage.dynamic_sampling_context() + return rv @@ -488,6 +507,7 @@ class Transaction(Span): # tracestate data from other vendors, of the form `dogs=yes,cats=maybe` "_third_party_tracestate", "_measurements", + "_baggage", ) def __init__( @@ -496,6 +516,7 @@ def __init__( parent_sampled=None, # type: Optional[bool] sentry_tracestate=None, # type: Optional[str] third_party_tracestate=None, # type: Optional[str] + baggage=None, # type: Optional[Baggage] **kwargs # type: Any ): # type: (...) -> None @@ -517,6 +538,7 @@ def __init__( self._sentry_tracestate = sentry_tracestate self._third_party_tracestate = third_party_tracestate self._measurements = {} # type: Dict[str, Any] + self._baggage = baggage def __repr__(self): # type: () -> str @@ -734,6 +756,7 @@ def _set_initial_sampling_decision(self, sampling_context): # Circular imports from sentry_sdk.tracing_utils import ( + Baggage, EnvironHeaders, compute_tracestate_entry, extract_sentrytrace_data, diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 2d31b9903e..aff5fc1076 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -16,13 +16,15 @@ to_string, from_base64, ) -from sentry_sdk._compat import PY2 +from sentry_sdk._compat import PY2, iteritems from sentry_sdk._types import MYPY if PY2: from collections import Mapping + from urllib import quote, unquote else: from collections.abc import Mapping + from urllib.parse import quote, unquote if MYPY: import typing @@ -211,27 +213,29 @@ def maybe_create_breadcrumbs_from_span(hub, span): def extract_sentrytrace_data(header): - # type: (Optional[str]) -> typing.Mapping[str, Union[str, bool, None]] + # type: (Optional[str]) -> Optional[typing.Mapping[str, Union[str, bool, None]]] """ Given a `sentry-trace` header string, return a dictionary of data. """ - trace_id = parent_span_id = parent_sampled = None + if not header: + return None - if header: - if header.startswith("00-") and header.endswith("-00"): - header = header[3:-3] + if header.startswith("00-") and header.endswith("-00"): + header = header[3:-3] - match = SENTRY_TRACE_REGEX.match(header) + match = SENTRY_TRACE_REGEX.match(header) + if not match: + return None - if match: - trace_id, parent_span_id, sampled_str = match.groups() + trace_id, parent_span_id, sampled_str = match.groups() + parent_sampled = None - if trace_id: - trace_id = "{:032x}".format(int(trace_id, 16)) - if parent_span_id: - parent_span_id = "{:016x}".format(int(parent_span_id, 16)) - if sampled_str: - parent_sampled = sampled_str != "0" + if trace_id: + trace_id = "{:032x}".format(int(trace_id, 16)) + if parent_span_id: + parent_span_id = "{:016x}".format(int(parent_span_id, 16)) + if sampled_str: + parent_sampled = sampled_str != "0" return { "trace_id": trace_id, @@ -413,6 +417,86 @@ def has_custom_measurements_enabled(): return bool(options and options["_experiments"].get("custom_measurements")) +class Baggage(object): + __slots__ = ("sentry_items", "third_party_items", "mutable") + + SENTRY_PREFIX = "sentry-" + SENTRY_PREFIX_REGEX = re.compile("^sentry-") + + # DynamicSamplingContext + DSC_KEYS = [ + "trace_id", + "public_key", + "sample_rate", + "release", + "environment", + "transaction", + "user_id", + "user_segment", + ] + + def __init__( + self, + sentry_items, # type: Dict[str, str] + third_party_items="", # type: str + mutable=True, # type: bool + ): + self.sentry_items = sentry_items + self.third_party_items = third_party_items + self.mutable = mutable + + @classmethod + def from_incoming_header(cls, header): + # type: (Optional[str]) -> Baggage + """ + freeze if incoming header already has sentry baggage + """ + sentry_items = {} + third_party_items = "" + mutable = True + + if header: + for item in header.split(","): + item = item.strip() + key, val = item.split("=") + if Baggage.SENTRY_PREFIX_REGEX.match(key): + baggage_key = unquote(key.split("-")[1]) + sentry_items[baggage_key] = unquote(val) + mutable = False + else: + third_party_items += ("," if third_party_items else "") + item + + return Baggage(sentry_items, third_party_items, mutable) + + def freeze(self): + # type: () -> None + self.mutable = False + + def dynamic_sampling_context(self): + # type: () -> Dict[str, str] + header = {} + + for key in Baggage.DSC_KEYS: + item = self.sentry_items.get(key) + if item: + header[key] = item + + return header + + def serialize(self, include_third_party=False): + # type: (bool) -> str + items = [] + + for key, val in iteritems(self.sentry_items): + item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(val) + items.append(item) + + if include_third_party: + items.append(self.third_party_items) + + return ",".join(items) + + # Circular imports if MYPY: diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index c90f9eb891..e59b245863 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -23,6 +23,7 @@ import mock # python < 3.3 from sentry_sdk import capture_message, start_transaction +from sentry_sdk.tracing import Transaction from sentry_sdk.integrations.stdlib import StdlibIntegration @@ -132,7 +133,17 @@ def test_outgoing_trace_headers( sentry_init(traces_sample_rate=1.0) + headers = {} + headers["baggage"] = ( + "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;" + ) + + transaction = Transaction.continue_from_headers(headers) + with start_transaction( + transaction=transaction, name="/interactions/other-dogs/new-dog", op="greeting.sniff", trace_id="12312012123120121231201212312012", @@ -140,14 +151,28 @@ def test_outgoing_trace_headers( HTTPSConnection("www.squirrelchasers.com").request("GET", "/top-chasers") - request_span = transaction._span_recorder.spans[-1] + (request_str,) = mock_send.call_args[0] + request_headers = {} + for line in request_str.decode("utf-8").split("\r\n")[1:]: + if line: + key, val = line.split(": ") + request_headers[key] = val - expected_sentry_trace = ( - "sentry-trace: {trace_id}-{parent_span_id}-{sampled}".format( - trace_id=transaction.trace_id, - parent_span_id=request_span.span_id, - sampled=1, - ) + request_span = transaction._span_recorder.spans[-1] + expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=transaction.trace_id, + parent_span_id=request_span.span_id, + sampled=1, ) + assert request_headers["sentry-trace"] == expected_sentry_trace + + expected_outgoing_baggage_items = [ + "sentry-trace_id=771a43a4192642f0b136d5159a501700", + "sentry-public_key=49d0f7386ad645858ae85020e393bef3", + "sentry-sample_rate=0.01337", + "sentry-user_id=Am%C3%A9lie", + ] - mock_send.assert_called_with(StringContaining(expected_sentry_trace)) + assert sorted(request_headers["baggage"].split(",")) == sorted( + expected_outgoing_baggage_items + ) diff --git a/tests/tracing/test_baggage.py b/tests/tracing/test_baggage.py new file mode 100644 index 0000000000..3c46ed5c63 --- /dev/null +++ b/tests/tracing/test_baggage.py @@ -0,0 +1,67 @@ +# coding: utf-8 +from sentry_sdk.tracing_utils import Baggage + + +def test_third_party_baggage(): + header = "other-vendor-value-1=foo;bar;baz, other-vendor-value-2=foo;bar;" + baggage = Baggage.from_incoming_header(header) + + assert baggage.mutable + assert baggage.sentry_items == {} + assert sorted(baggage.third_party_items.split(",")) == sorted( + "other-vendor-value-1=foo;bar;baz,other-vendor-value-2=foo;bar;".split(",") + ) + + assert baggage.dynamic_sampling_context() == {} + assert baggage.serialize() == "" + assert sorted(baggage.serialize(include_third_party=True).split(",")) == sorted( + "other-vendor-value-1=foo;bar;baz,other-vendor-value-2=foo;bar;".split(",") + ) + + +def test_mixed_baggage(): + header = ( + "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;" + ) + + baggage = Baggage.from_incoming_header(header) + + assert not baggage.mutable + + assert baggage.sentry_items == { + "public_key": "49d0f7386ad645858ae85020e393bef3", + "trace_id": "771a43a4192642f0b136d5159a501700", + "user_id": "Amélie", + "sample_rate": "0.01337", + } + + assert ( + baggage.third_party_items + == "other-vendor-value-1=foo;bar;baz,other-vendor-value-2=foo;bar;" + ) + + assert baggage.dynamic_sampling_context() == { + "public_key": "49d0f7386ad645858ae85020e393bef3", + "trace_id": "771a43a4192642f0b136d5159a501700", + "user_id": "Amélie", + "sample_rate": "0.01337", + } + + assert sorted(baggage.serialize().split(",")) == sorted( + ( + "sentry-trace_id=771a43a4192642f0b136d5159a501700," + "sentry-public_key=49d0f7386ad645858ae85020e393bef3," + "sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie" + ).split(",") + ) + + assert sorted(baggage.serialize(include_third_party=True).split(",")) == sorted( + ( + "sentry-trace_id=771a43a4192642f0b136d5159a501700," + "sentry-public_key=49d0f7386ad645858ae85020e393bef3," + "sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie," + "other-vendor-value-1=foo;bar;baz,other-vendor-value-2=foo;bar;" + ).split(",") + ) diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index 486651c754..80a8ba7a0c 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -1,6 +1,6 @@ +# coding: utf-8 import weakref import gc - import pytest from sentry_sdk import ( @@ -49,13 +49,13 @@ def test_basic(sentry_init, capture_events, sample_rate): @pytest.mark.parametrize("sampled", [True, False, None]) @pytest.mark.parametrize("sample_rate", [0.0, 1.0]) -def test_continue_from_headers(sentry_init, capture_events, sampled, sample_rate): +def test_continue_from_headers(sentry_init, capture_envelopes, sampled, sample_rate): """ Ensure data is actually passed along via headers, and that they are read correctly. """ sentry_init(traces_sample_rate=sample_rate) - events = capture_events() + envelopes = capture_envelopes() # make a parent transaction (normally this would be in a different service) with start_transaction( @@ -63,9 +63,17 @@ def test_continue_from_headers(sentry_init, capture_events, sampled, sample_rate ) as parent_transaction: with start_span() as old_span: old_span.sampled = sampled - headers = dict(Hub.current.iter_trace_propagation_headers(old_span)) tracestate = parent_transaction._sentry_tracestate + headers = dict(Hub.current.iter_trace_propagation_headers(old_span)) + headers["baggage"] = ( + "other-vendor-value-1=foo;bar;baz, " + "sentry-trace_id=771a43a4192642f0b136d5159a501700, " + "sentry-public_key=49d0f7386ad645858ae85020e393bef3, " + "sentry-sample_rate=0.01337, sentry-user_id=Amelie, " + "other-vendor-value-2=foo;bar;" + ) + # child transaction, to prove that we can read 'sentry-trace' and # `tracestate` header data correctly child_transaction = Transaction.continue_from_headers(headers, name="WRONG") @@ -77,6 +85,16 @@ def test_continue_from_headers(sentry_init, capture_events, sampled, sample_rate assert child_transaction.span_id != old_span.span_id assert child_transaction._sentry_tracestate == tracestate + baggage = child_transaction._baggage + assert baggage + assert not baggage.mutable + assert baggage.sentry_items == { + "public_key": "49d0f7386ad645858ae85020e393bef3", + "trace_id": "771a43a4192642f0b136d5159a501700", + "user_id": "Amelie", + "sample_rate": "0.01337", + } + # add child transaction to the scope, to show that the captured message will # be tagged with the trace id (since it happens while the transaction is # open) @@ -89,23 +107,36 @@ def test_continue_from_headers(sentry_init, capture_events, sampled, sample_rate # in this case the child transaction won't be captured if sampled is False or (sample_rate == 0 and sampled is None): - trace1, message = events + trace1, message = envelopes + message_payload = message.get_event() + trace1_payload = trace1.get_transaction_event() - assert trace1["transaction"] == "hi" + assert trace1_payload["transaction"] == "hi" else: - trace1, message, trace2 = events + trace1, message, trace2 = envelopes + trace1_payload = trace1.get_transaction_event() + message_payload = message.get_event() + trace2_payload = trace2.get_transaction_event() - assert trace1["transaction"] == "hi" - assert trace2["transaction"] == "ho" + assert trace1_payload["transaction"] == "hi" + assert trace2_payload["transaction"] == "ho" assert ( - trace1["contexts"]["trace"]["trace_id"] - == trace2["contexts"]["trace"]["trace_id"] + trace1_payload["contexts"]["trace"]["trace_id"] + == trace2_payload["contexts"]["trace"]["trace_id"] == child_transaction.trace_id - == message["contexts"]["trace"]["trace_id"] + == message_payload["contexts"]["trace"]["trace_id"] ) - assert message["message"] == "hello" + assert trace2.headers["trace"] == baggage.dynamic_sampling_context() + assert trace2.headers["trace"] == { + "public_key": "49d0f7386ad645858ae85020e393bef3", + "trace_id": "771a43a4192642f0b136d5159a501700", + "user_id": "Amelie", + "sample_rate": "0.01337", + } + + assert message_payload["message"] == "hello" @pytest.mark.parametrize( From 485a659b42e8830b8c8299c53fc51b36eb7be942 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 8 Jul 2022 14:11:47 +0000 Subject: [PATCH 07/11] release: 1.7.0 --- CHANGELOG.md | 11 +++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1261c08b68..e0fa08700b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 1.7.0 + +### Various fixes & improvements + +- feat(tracing): Dynamic Sampling Context / Baggage continuation (#1485) by @sl0thentr0py +- build(deps): bump sphinx from 4.5.0 to 5.0.2 (#1470) by @dependabot +- build(deps): bump pep8-naming from 0.11.1 to 0.13.0 (#1457) by @dependabot +- build(deps): bump actions/setup-python from 3 to 4 (#1465) by @dependabot +- build(deps): bump mypy from 0.950 to 0.961 (#1464) by @dependabot +- build(deps): bump actions/cache from 2 to 3 (#1478) by @dependabot + ## 1.6.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index c3ba844ec7..b3eb881196 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,7 +29,7 @@ copyright = "2019, Sentry Team and Contributors" author = "Sentry Team and Contributors" -release = "1.6.0" +release = "1.7.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 043740acd1..7ed88b674d 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -102,7 +102,7 @@ def _get_default_options(): del _get_default_options -VERSION = "1.6.0" +VERSION = "1.7.0" SDK_INFO = { "name": "sentry.python", "version": VERSION, diff --git a/setup.py b/setup.py index e1d3972d28..ed766b6df5 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="1.6.0", + version="1.7.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From 3fd8f12b90c338bda26316ce515c08e6340b1d39 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 8 Jul 2022 16:19:18 +0200 Subject: [PATCH 08/11] Edit changelog --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fa08700b..6218e29ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ ### Various fixes & improvements - feat(tracing): Dynamic Sampling Context / Baggage continuation (#1485) by @sl0thentr0py -- build(deps): bump sphinx from 4.5.0 to 5.0.2 (#1470) by @dependabot -- build(deps): bump pep8-naming from 0.11.1 to 0.13.0 (#1457) by @dependabot -- build(deps): bump actions/setup-python from 3 to 4 (#1465) by @dependabot -- build(deps): bump mypy from 0.950 to 0.961 (#1464) by @dependabot -- build(deps): bump actions/cache from 2 to 3 (#1478) by @dependabot + + The SDK now propagates the [W3C Baggage Header](https://www.w3.org/TR/baggage/) from + incoming transactions to outgoing requests. It also extracts + Sentry specific [sampling information](https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/) + and adds it to the transaction headers to enable Dynamic Sampling in the product. ## 1.6.0 From 21f25afa5c298129bdf35ee31bcdf6b716b2bb54 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 8 Jul 2022 16:20:45 +0200 Subject: [PATCH 09/11] Newline --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6218e29ef7..427c7cd884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ - feat(tracing): Dynamic Sampling Context / Baggage continuation (#1485) by @sl0thentr0py The SDK now propagates the [W3C Baggage Header](https://www.w3.org/TR/baggage/) from - incoming transactions to outgoing requests. It also extracts - Sentry specific [sampling information](https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/) + incoming transactions to outgoing requests. + It also extracts Sentry specific [sampling information](https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/) and adds it to the transaction headers to enable Dynamic Sampling in the product. ## 1.6.0 From e71609731ae14f9829553bdddc5b11111ed3d4bc Mon Sep 17 00:00:00 2001 From: Rob Young Date: Wed, 13 Jul 2022 13:23:29 +0100 Subject: [PATCH 10/11] Skip malformed baggage items (#1491) We are seeing baggage headers coming in with a single comma. This is obviously invalid but Sentry should error out. --- sentry_sdk/tracing_utils.py | 2 ++ tests/tracing/test_baggage.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index aff5fc1076..0b4e33c6ec 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -457,6 +457,8 @@ def from_incoming_header(cls, header): if header: for item in header.split(","): + if "=" not in item: + continue item = item.strip() key, val = item.split("=") if Baggage.SENTRY_PREFIX_REGEX.match(key): diff --git a/tests/tracing/test_baggage.py b/tests/tracing/test_baggage.py index 3c46ed5c63..185a085bf6 100644 --- a/tests/tracing/test_baggage.py +++ b/tests/tracing/test_baggage.py @@ -65,3 +65,13 @@ def test_mixed_baggage(): "other-vendor-value-1=foo;bar;baz,other-vendor-value-2=foo;bar;" ).split(",") ) + + +def test_malformed_baggage(): + header = "," + + baggage = Baggage.from_incoming_header(header) + + assert baggage.sentry_items == {} + assert baggage.third_party_items == "" + assert baggage.mutable From 0b2868c83d37f028a8223f775254309f1424bb5b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 13 Jul 2022 12:24:58 +0000 Subject: [PATCH 11/11] release: 1.7.1 --- CHANGELOG.md | 6 ++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427c7cd884..c1e78cbed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.7.1 + +### Various fixes & improvements + +- Skip malformed baggage items (#1491) by @robyoung + ## 1.7.0 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index b3eb881196..3316c2b689 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,7 +29,7 @@ copyright = "2019, Sentry Team and Contributors" author = "Sentry Team and Contributors" -release = "1.7.0" +release = "1.7.1" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 7ed88b674d..437f53655b 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -102,7 +102,7 @@ def _get_default_options(): del _get_default_options -VERSION = "1.7.0" +VERSION = "1.7.1" SDK_INFO = { "name": "sentry.python", "version": VERSION, diff --git a/setup.py b/setup.py index ed766b6df5..d06e6c9de9 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="1.7.0", + version="1.7.1", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python",