From cf9eda29fa713edaece4be50ce5208b04a8a4d7e Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 22 May 2019 02:04:01 +0200 Subject: [PATCH 01/46] feat: Prototype of session tracking --- sentry_sdk/consts.py | 2 + sentry_sdk/hub.py | 56 ++++++++++- sentry_sdk/integrations/celery.py | 25 +++-- sentry_sdk/integrations/django/__init__.py | 22 +++-- sentry_sdk/integrations/flask.py | 18 ++-- sentry_sdk/integrations/stdlib.py | 7 ++ sentry_sdk/integrations/wsgi.py | 9 +- sentry_sdk/scope.py | 17 +++- sentry_sdk/tracing.py | 109 +++++++++++++++------ sentry_sdk/transport.py | 2 +- tests/integrations/celery/test_celery.py | 6 +- tests/test_tracing.py | 26 +++++ 12 files changed, 231 insertions(+), 68 deletions(-) create mode 100644 tests/test_tracing.py diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index a64a8fc261..4362f1d3fd 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -38,6 +38,7 @@ "attach_stacktrace": bool, "ca_certs": Optional[str], "propagate_traces": bool, + "send_traces": bool, }, total=False, ) @@ -71,6 +72,7 @@ "attach_stacktrace": False, "ca_certs": None, "propagate_traces": True, + "traces_sample_rate": 0.0, } diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index cfaa97349c..a3b80efd3b 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -1,6 +1,8 @@ -import sys import copy +import random +import sys import weakref + from datetime import datetime from contextlib import contextmanager from warnings import warn @@ -8,6 +10,7 @@ from sentry_sdk._compat import with_metaclass from sentry_sdk.scope import Scope from sentry_sdk.client import Client +from sentry_sdk.tracing import Span from sentry_sdk.utils import ( exc_info_from_error, event_from_exception, @@ -344,6 +347,57 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): while len(scope._breadcrumbs) > max_breadcrumbs: scope._breadcrumbs.popleft() + @contextmanager + def span(self, span=None, **kwargs): + if span is None: + span = self.start_span(**kwargs) + + if span is None: + yield span + return + + try: + yield span + except Exception: + span.set_tag("success", False) + else: + span.set_tag("success", True) + finally: + span.finish() + self.capture_trace(span) + + def trace(self, *args, **kwargs): + return self.span(self.start_trace(*args, **kwargs)) + + def start_span(self, **kwargs): + _, scope = self._stack[-1] + span = scope.span + if span is not None: + return span.new_span(**kwargs) + return None + + def start_trace(self, transaction, **kwargs): + _, scope = self._stack[-1] + scope.span = span = Span.start_trace(transaction, **kwargs) + return span + + def capture_trace(self, span): + if span.transaction is None: + return None + + client = self.client + + if client is None: + return None + + sample_rate = client.options["traces_sample_rate"] + if sample_rate < 1.0 and random.random() >= sample_rate: + return None + + return self.capture_event( + {"type": "none", "spans": [s.to_json() for s in span._finished_spans]} + ) + @overload # noqa def push_scope(self): # type: () -> ContextManager[Scope] diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 7759e61e5c..ebddaad3f8 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -11,7 +11,7 @@ from sentry_sdk.hub import Hub from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from sentry_sdk.tracing import SpanContext +from sentry_sdk.tracing import Span from sentry_sdk._compat import reraise from sentry_sdk.integrations import Integration from sentry_sdk.integrations.logging import ignore_logger @@ -82,20 +82,30 @@ def _inner(*args, **kwargs): with hub.push_scope() as scope: scope._name = "celery" scope.clear_breadcrumbs() - _continue_trace(args[3].get("headers") or {}, scope) + span = _continue_trace(args[3].get("headers") or {}, scope) + + with capture_internal_exceptions(): + scope.transaction = task.name + scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) - return f(*args, **kwargs) + try: + return f(*args, **kwargs) + finally: + span.finish() + hub.capture_trace(span) return _inner def _continue_trace(headers, scope): if headers: - span_context = SpanContext.continue_from_headers(headers) + span = Span.continue_from_headers(headers) else: - span_context = SpanContext.start_trace() - scope.set_span_context(span_context) + span = Span.start_trace() + + scope.span = span + return span def _wrap_task_call(task, f): @@ -115,9 +125,6 @@ def _inner(*args, **kwargs): def _make_event_processor(task, uuid, args, kwargs, request=None): def event_processor(event, hint): - with capture_internal_exceptions(): - event["transaction"] = task.name - with capture_internal_exceptions(): extra = event.setdefault("extra", {}) extra["celery-job"] = { diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index fe8b7017a3..36481f9579 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -112,7 +112,19 @@ def sentry_patched_get_response(self, request): hub = Hub.current integration = hub.get_integration(DjangoIntegration) if integration is not None: + with hub.configure_scope() as scope: + # Rely on WSGI middleware to start a trace + try: + if integration.transaction_style == "function_name": + scope.transaction = transaction_from_function( + resolve(request.path).func + ) + elif integration.transaction_style == "url": + scope.transaction = LEGACY_RESOLVER.resolve(request.path) + except Exception: + pass + scope.add_event_processor( _make_event_processor(weakref.ref(request), integration) ) @@ -190,16 +202,6 @@ def event_processor(event, hint): if request is None: return event - try: - if integration.transaction_style == "function_name": - event["transaction"] = transaction_from_function( - resolve(request.path).func - ) - elif integration.transaction_style == "url": - event["transaction"] = LEGACY_RESOLVER.resolve(request.path) - except Exception: - pass - with capture_internal_exceptions(): DjangoRequestExtractor(request).extract_into_event(event) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 437e9fed0b..e71237be63 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -99,6 +99,16 @@ def _request_started(sender, **kwargs): app = _app_ctx_stack.top.app with hub.configure_scope() as scope: request = _request_ctx_stack.top.request + + # Rely on WSGI middleware to start a trace + try: + if integration.transaction_style == "endpoint": + scope.transaction = request.url_rule.endpoint # type: ignore + elif integration.transaction_style == "url": + scope.transaction = request.url_rule.rule # type: ignore + except Exception: + pass + weak_request = weakref.ref(request) scope.add_event_processor( _make_request_event_processor( # type: ignore @@ -151,14 +161,6 @@ def inner(event, hint): if request is None: return event - try: - if integration.transaction_style == "endpoint": - event["transaction"] = request.url_rule.endpoint # type: ignore - elif integration.transaction_style == "url": - event["transaction"] = request.url_rule.rule # type: ignore - except Exception: - pass - with capture_internal_exceptions(): FlaskRequestExtractor(request).extract_into_event(event) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 8b16c73ce4..5f3e7242f2 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -29,6 +29,7 @@ def putrequest(self, method, url, *args, **kwargs): return rv self._sentrysdk_data_dict = data = {} + self._sentrysdk_span = hub.start_span() host = self.host port = self.port @@ -61,6 +62,12 @@ def getresponse(self, *args, **kwargs): if "status_code" not in data: data["status_code"] = rv.status data["reason"] = rv.reason + + span = self._sentrysdk_span + if span is not None: + span.set_tag("status_code", rv.status) + span.finish() + hub.add_breadcrumb( type="http", category="httplib", data=data, hint={"httplib_response": rv} ) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index f91f10ac67..28531ea314 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -3,7 +3,7 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk._compat import PY2, reraise, iteritems -from sentry_sdk.tracing import SpanContext +from sentry_sdk.tracing import Span from sentry_sdk.integrations._wsgi_common import _filter_headers if False: @@ -78,17 +78,22 @@ def __call__(self, environ, start_response): hub = Hub(Hub.current) with hub: + span = None with capture_internal_exceptions(): with hub.configure_scope() as scope: scope.clear_breadcrumbs() scope._name = "wsgi" - scope.set_span_context(SpanContext.continue_from_environ(environ)) scope.add_event_processor(_make_wsgi_event_processor(environ)) + scope.span = span = Span.continue_from_environ(environ) try: rv = self.app(environ, start_response) except Exception: reraise(*_capture_exception(hub)) + finally: + if span is not None: + span.finish() + hub.capture_trace(span) return _ScopedResponse(hub, rv) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 0e934671dc..92d7eae756 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -83,15 +83,26 @@ def fingerprint(self, value): def transaction(self, value): """When set this forces a specific transaction name to be set.""" self._transaction = value + if self._span: + self._span.transaction = value @_attr_setter def user(self, value): """When set a specific user is bound to the scope.""" self._user = value - def set_span_context(self, span_context): - """Sets the span context.""" - self._span = span_context + @property + def span(self): + """Get/set current tracing span.""" + return self._span + + @span.setter + def span(self, span): + self._span = span + if span.transaction: + self._transaction = span.transaction + elif self._transaction: + span.transaction = self._transaction def set_tag(self, key, value): """Sets a tag for a key to a specific value.""" diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 37c1ee356d..0e92fd98ec 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,8 +1,14 @@ import re import uuid +from datetime import datetime + _traceparent_header_format_re = re.compile( - "^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})" "(-.*)?[ \t]*$" + "^[ \t]*" # whitespace + "([0-9a-f]{32})?" # trace_id + "-?([0-9a-f]{16})?" # span_id + "-?([01])?" # sampled + "[ \t]*$" # whitespace ) @@ -14,36 +20,65 @@ def get(self, key): return self.environ.get("HTTP_" + key.replace("-", "_").upper()) -class SpanContext(object): - def __init__(self, trace_id, span_id, recorded=False, parent=None): +class Span(object): + __slots__ = ( + "trace_id", + "span_id", + "ref", + "ref_type", + "sampled", + "transaction", + "_tags", + "_finished_spans", + "start", + "end", + ) + + def __init__( + self, trace_id, span_id, transaction=None, ref=None, ref_type=None, sampled=None + ): self.trace_id = trace_id self.span_id = span_id - self.recorded = recorded - self.parent = None + self.ref = ref + self.ref_type = ref_type + self.sampled = sampled + self.transaction = transaction + self._tags = {} + self._finished_spans = [] + self.start = datetime.now() + self.end = None def __repr__(self): - return "%s(trace_id=%r, span_id=%r, recorded=%r)" % ( + return "<%s(transaction=%r, trace_id=%r, span_id=%r, ref=%r)>" % ( self.__class__.__name__, + self.transaction, self.trace_id, self.span_id, - self.recorded, + self.ref, ) @classmethod - def start_trace(cls, recorded=False): + def start_trace(cls, transaction=None, sampled=None): return cls( - trace_id=uuid.uuid4().hex, span_id=uuid.uuid4().hex[16:], recorded=recorded + transaction=transaction, + trace_id=uuid.uuid4().hex, + span_id=uuid.uuid4().hex[16:], + sampled=sampled, ) - def new_span(self): + def new_span(self, ref_type="child"): if self.trace_id is None: - return SpanContext.start_trace() - return SpanContext( + return Span.start_trace() + + rv = Span( trace_id=self.trace_id, span_id=uuid.uuid4().hex[16:], - parent=self, - recorded=self.recorded, + ref=self, + ref_type=ref_type, + sampled=self.sampled, ) + rv._finished_spans = self._finished_spans + return rv @classmethod def continue_from_environ(cls, environ): @@ -54,7 +89,7 @@ def continue_from_headers(cls, headers): parent = cls.from_traceparent(headers.get("sentry-trace")) if parent is None: return cls.start_trace() - return parent.new_span() + return parent.new_span("follows_from") def iter_headers(self): yield "sentry-trace", self.to_traceparent() @@ -68,26 +103,38 @@ def from_traceparent(cls, traceparent): if match is None: return None - version, trace_id, span_id, trace_options, extra = match.groups() - - if int(trace_id, 16) == 0 or int(span_id, 16) == 0: - return None - - version = int(version, 16) - if version == 0: - if extra: - return None - elif version == 255: - return None + trace_id, span_id, sampled = match.groups() - options = int(trace_options, 16) + if trace_id is not None: + trace_id = int(trace_id, 16) + if span_id is not None: + span_id = int(span_id, 16) + if sampled is not None: + sampled = sampled == "1" - return cls(trace_id=trace_id, span_id=span_id, recorded=options & 1 != 0) + return cls(trace_id=trace_id, span_id=span_id, sampled=sampled) def to_traceparent(self): - return "%02x-%s-%s-%02x" % ( - 0, + return "%s-%s-%s" % ( self.trace_id, self.span_id, - self.recorded and 1 or 0, + "1" if not self.sampled else "0", ) + + def set_tag(self, key, value): + self._tags[key] = value + + def finish(self): + self.end = datetime.now() + self._finished_spans.append(self) + + def to_json(self): + return { + "trace_id": self.trace_id, + "span_id": self.span_id, + "ref_span_id": self.ref and self.ref.span_id or None, + "transaction": self.transaction, + "tags": self._tags, + "start": self.start, + "end": self.end, + } diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index fc071f0df6..ca6e995019 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -105,7 +105,7 @@ def _send_event(self, event): logger.debug( "Sending %s event [%s] to %s project:%s" % ( - event.get("level") or "error", + event.get("level") or "level-less", event["event_id"], self.parsed_dsn.host, self.parsed_dsn.project_id, diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index 07b4fd2db1..f81d41882e 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -6,7 +6,7 @@ from sentry_sdk import Hub, configure_scope from sentry_sdk.integrations.celery import CeleryIntegration -from sentry_sdk.tracing import SpanContext +from sentry_sdk.tracing import Span from celery import Celery, VERSION from celery.bin import worker @@ -63,7 +63,7 @@ def dummy_task(x, y): foo = 42 # noqa return x / y - span_context = SpanContext.start_trace() + span_context = Span.start_trace() with configure_scope() as scope: scope.set_span_context(span_context) @@ -92,7 +92,7 @@ def test_simple_no_propagation(capture_events, init_celery): def dummy_task(): 1 / 0 - span_context = SpanContext.start_trace() + span_context = Span.start_trace() with configure_scope() as scope: scope.set_span_context(span_context) dummy_task.delay() diff --git a/tests/test_tracing.py b/tests/test_tracing.py new file mode 100644 index 0000000000..ccb7f0b05b --- /dev/null +++ b/tests/test_tracing.py @@ -0,0 +1,26 @@ +import pytest + +from sentry_sdk import Hub + + +@pytest.mark.parametrize("sample_rate", [0.0, 1.0]) +def test_basic(sentry_init, capture_events, sample_rate): + sentry_init(traces_sample_rate=sample_rate) + events = capture_events() + + with Hub.current.trace("hi"): + with Hub.current.span(): + 1 / 0 + + with Hub.current.span(): + pass + + if sample_rate: + event, = events + + span1, span2, parent_span = event["spans"] + assert not span1["tags"]["success"] + assert span2["tags"]["success"] + assert parent_span["transaction"] == "hi" + else: + assert not events From 270ff43ce9b32ca66b42d9ea609fb2025809debc Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 28 May 2019 18:46:24 +0200 Subject: [PATCH 02/46] ref: Instrument SQL, and change schema as discussed --- sentry_sdk/hub.py | 32 ++++++---- sentry_sdk/integrations/django/__init__.py | 39 +++++++++--- sentry_sdk/integrations/stdlib.py | 10 ++- sentry_sdk/scope.py | 7 +-- sentry_sdk/tracing.py | 71 ++++++++++++---------- tests/test_tracing.py | 7 ++- 6 files changed, 103 insertions(+), 63 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index a3b80efd3b..2baf11b000 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -366,8 +366,8 @@ def span(self, span=None, **kwargs): span.finish() self.capture_trace(span) - def trace(self, *args, **kwargs): - return self.span(self.start_trace(*args, **kwargs)) + def trace(self, **kwargs): + return self.span(self.start_trace(**kwargs)) def start_span(self, **kwargs): _, scope = self._stack[-1] @@ -376,26 +376,34 @@ def start_span(self, **kwargs): return span.new_span(**kwargs) return None - def start_trace(self, transaction, **kwargs): + def start_trace(self, **kwargs): + span = Span.start_trace(**kwargs) + _, scope = self._stack[-1] - scope.span = span = Span.start_trace(transaction, **kwargs) + scope.span = span return span def capture_trace(self, span): - if span.transaction is None: - return None - - client = self.client - - if client is None: + if ( + span.transaction is None + or span.timestamp is None + or self.client is None + or span.sampled is False + ): return None - sample_rate = client.options["traces_sample_rate"] + sample_rate = self.client.options["traces_sample_rate"] if sample_rate < 1.0 and random.random() >= sample_rate: return None return self.capture_event( - {"type": "none", "spans": [s.to_json() for s in span._finished_spans]} + { + "type": "transaction", + "contexts": {"trace": span.get_trace_context()}, + "timestamp": span.timestamp, + "start_timestamp": span.start_timestamp, + "spans": [s.to_json() for s in span._finished_spans if s is not span], + } ) @overload # noqa diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 36481f9579..396914a81e 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import +import contextlib import sys import weakref @@ -318,10 +319,12 @@ def format_sql(sql, params): return sql, rv +@contextlib.contextmanager def record_sql(sql, params, cursor=None): # type: (Any, Any, Any) -> None hub = Hub.current if hub.get_integration(DjangoIntegration) is None: + yield return real_sql = None @@ -353,9 +356,33 @@ def record_sql(sql, params, cursor=None): except Exception: pass + span = None + if real_sql: with capture_internal_exceptions(): hub.add_breadcrumb(message=real_sql, category="query") + span = hub.start_span(op="sql.query", description=real_sql) + + if span is None: + yield + else: + try: + yield + finally: + span.set_tag("status", sys.exc_info()[1] is None) + span.finish() + + +@contextlib.contextmanager +def record_many_sql(sql, param_list, cursor): + ctxs = [record_sql(sql, params, cursor).__enter__() for params in param_list] + + try: + yield + finally: + einfo = sys.exc_info() + for ctx in ctxs: + ctx.__exit__(*einfo) def install_sql_hook(): @@ -373,21 +400,13 @@ def install_sql_hook(): # This won't work on Django versions < 1.6 return - def record_many_sql(sql, param_list, cursor): - for params in param_list: - record_sql(sql, params, cursor) - def execute(self, sql, params=None): - try: + with record_sql(sql, params, self.cursor): return real_execute(self, sql, params) - finally: - record_sql(sql, params, self.cursor) def executemany(self, sql, param_list): - try: + with record_many_sql(sql, param_list, self.cursor): return real_executemany(self, sql, param_list) - finally: - record_many_sql(sql, param_list, self.cursor) CursorWrapper.execute = execute CursorWrapper.executemany = executemany diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 5f3e7242f2..8e24d95684 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -28,9 +28,6 @@ def putrequest(self, method, url, *args, **kwargs): if hub.get_integration(StdlibIntegration) is None: return rv - self._sentrysdk_data_dict = data = {} - self._sentrysdk_span = hub.start_span() - host = self.host port = self.port default_port = self.default_port @@ -44,6 +41,11 @@ def putrequest(self, method, url, *args, **kwargs): url, ) + self._sentrysdk_data_dict = data = {} + self._sentrysdk_span = hub.start_span( + op="http", description="%s %s" % (real_url, method) + ) + for key, value in hub.iter_trace_propagation_headers(): self.putheader(key, value) @@ -66,6 +68,8 @@ def getresponse(self, *args, **kwargs): span = self._sentrysdk_span if span is not None: span.set_tag("status_code", rv.status) + for k, v in data.items(): + span.set_data(k, v) span.finish() hub.add_breadcrumb( diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 92d7eae756..f44234f92d 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -212,10 +212,9 @@ def _drop(event, cause, ty): event.setdefault("contexts", {}).update(self._contexts) if self._span is not None: - event.setdefault("contexts", {})["trace"] = { - "trace_id": self._span.trace_id, - "span_id": self._span.span_id, - } + contexts = event.setdefault("contexts", {}) + if not contexts.get("trace"): + contexts["trace"] = self._span.get_trace_context() exc_info = hint.get("exc_info") if hint is not None else None if exc_info is not None: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0e92fd98ec..4f34ad4ba3 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -24,29 +24,41 @@ class Span(object): __slots__ = ( "trace_id", "span_id", - "ref", - "ref_type", + "parent_span_id", + "same_process_as_parent", "sampled", "transaction", + "op", + "description", + "start_timestamp", + "timestamp", "_tags", + "_data", "_finished_spans", - "start", - "end", ) def __init__( - self, trace_id, span_id, transaction=None, ref=None, ref_type=None, sampled=None + self, + trace_id, + span_id, + parent_span_id=None, + same_process_as_parent=True, + sampled=None, + transaction=None, + op=None, + description=None, ): self.trace_id = trace_id self.span_id = span_id - self.ref = ref - self.ref_type = ref_type + self.parent_span_id = parent_span_id + self.same_process_as_parent = same_process_as_parent self.sampled = sampled self.transaction = transaction self._tags = {} + self._data = {} self._finished_spans = [] - self.start = datetime.now() - self.end = None + self.start_timestamp = datetime.now() + self.timestamp = None def __repr__(self): return "<%s(transaction=%r, trace_id=%r, span_id=%r, ref=%r)>" % ( @@ -58,24 +70,18 @@ def __repr__(self): ) @classmethod - def start_trace(cls, transaction=None, sampled=None): - return cls( - transaction=transaction, - trace_id=uuid.uuid4().hex, - span_id=uuid.uuid4().hex[16:], - sampled=sampled, - ) + def start_trace(cls, **kwargs): + return cls(trace_id=uuid.uuid4().hex, span_id=uuid.uuid4().hex[16:], **kwargs) - def new_span(self, ref_type="child"): + def new_span(self, **kwargs): if self.trace_id is None: return Span.start_trace() rv = Span( trace_id=self.trace_id, span_id=uuid.uuid4().hex[16:], - ref=self, - ref_type=ref_type, - sampled=self.sampled, + parent_span_id=self.span_id, + **kwargs ) rv._finished_spans = self._finished_spans return rv @@ -89,7 +95,7 @@ def continue_from_headers(cls, headers): parent = cls.from_traceparent(headers.get("sentry-trace")) if parent is None: return cls.start_trace() - return parent.new_span("follows_from") + return parent.new_span(same_process_as_parent=False) def iter_headers(self): yield "sentry-trace", self.to_traceparent() @@ -110,31 +116,34 @@ def from_traceparent(cls, traceparent): if span_id is not None: span_id = int(span_id, 16) if sampled is not None: - sampled = sampled == "1" + sampled = sampled != "0" return cls(trace_id=trace_id, span_id=span_id, sampled=sampled) def to_traceparent(self): - return "%s-%s-%s" % ( - self.trace_id, - self.span_id, - "1" if not self.sampled else "0", - ) + return "%s-%s-%s" % (self.trace_id, self.span_id, "1" if self.sampled else "0") def set_tag(self, key, value): self._tags[key] = value + def set_data(self, key, value): + self._data[key] = value + def finish(self): - self.end = datetime.now() + self.timestamp = datetime.now() self._finished_spans.append(self) def to_json(self): return { "trace_id": self.trace_id, "span_id": self.span_id, - "ref_span_id": self.ref and self.ref.span_id or None, + "parent_span_id": self.parent_span_id, "transaction": self.transaction, "tags": self._tags, - "start": self.start, - "end": self.end, + "data": self._data, + "start_timestamp": self.start_timestamp, + "timestamp": self.timestamp, } + + def get_trace_context(self): + return {"trace_id": self.trace_id, "span_id": self.span_id} diff --git a/tests/test_tracing.py b/tests/test_tracing.py index ccb7f0b05b..e3b91cd4ba 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -8,9 +8,10 @@ def test_basic(sentry_init, capture_events, sample_rate): sentry_init(traces_sample_rate=sample_rate) events = capture_events() - with Hub.current.trace("hi"): - with Hub.current.span(): - 1 / 0 + with Hub.current.trace(transaction="hi"): + with pytest.raises(ZeroDivisionError): + with Hub.current.span(): + 1 / 0 with Hub.current.span(): pass From d6daf15deaec56f01f7e653f8bf7e915eb9786f4 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 31 May 2019 10:37:38 +0200 Subject: [PATCH 03/46] fix: Fix CI --- sentry_sdk/tracing.py | 8 ++++---- tests/integrations/celery/test_celery.py | 14 +++++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 4f34ad4ba3..0c94fa3004 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -61,12 +61,12 @@ def __init__( self.timestamp = None def __repr__(self): - return "<%s(transaction=%r, trace_id=%r, span_id=%r, ref=%r)>" % ( + return "<%s(transaction=%r, trace_id=%r, span_id=%r, parent_span_id=%r)>" % ( self.__class__.__name__, self.transaction, self.trace_id, self.span_id, - self.ref, + self.parent_span_id, ) @classmethod @@ -112,9 +112,9 @@ def from_traceparent(cls, traceparent): trace_id, span_id, sampled = match.groups() if trace_id is not None: - trace_id = int(trace_id, 16) + trace_id = "%x" % (int(trace_id, 16),) if span_id is not None: - span_id = int(span_id, 16) + span_id = "%x" % (int(span_id, 16),) if sampled is not None: sampled = sampled != "0" diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index f81d41882e..c6fd4a6706 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -63,16 +63,14 @@ def dummy_task(x, y): foo = 42 # noqa return x / y - span_context = Span.start_trace() - with configure_scope() as scope: - scope.set_span_context(span_context) + span = Hub.current.start_trace() invocation(dummy_task, 1, 2) invocation(dummy_task, 1, 0) event, = events - assert event["contexts"]["trace"]["trace_id"] == span_context.trace_id - assert event["contexts"]["trace"]["span_id"] != span_context.span_id + assert event["contexts"]["trace"]["trace_id"] == span.trace_id + assert event["contexts"]["trace"]["span_id"] != span.span_id assert event["transaction"] == "dummy_task" assert event["extra"]["celery-job"] == dict( task_name="dummy_task", **expected_context @@ -92,13 +90,11 @@ def test_simple_no_propagation(capture_events, init_celery): def dummy_task(): 1 / 0 - span_context = Span.start_trace() - with configure_scope() as scope: - scope.set_span_context(span_context) + span = Hub.current.start_trace() dummy_task.delay() event, = events - assert event["contexts"]["trace"]["trace_id"] != span_context.trace_id + assert event["contexts"]["trace"]["trace_id"] != span.trace_id assert event["transaction"] == "dummy_task" exception, = event["exception"]["values"] assert exception["type"] == "ZeroDivisionError" From 51766376c4c563f1732de67e86bfaf7535a44e81 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 17:52:20 +0200 Subject: [PATCH 04/46] fix: Nicer debug log --- sentry_sdk/transport.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 821e8c637e..049df6fb87 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -105,12 +105,13 @@ def _send_event(self, event): f.write(json.dumps(event, allow_nan=False).encode("utf-8")) logger.debug( - "Sending %s event [%s] to %s project:%s" + "Sending event, type:%s level:%s event_id:%s project:%s host:%s" % ( - event.get("level") or "level-less", - event["event_id"], - self.parsed_dsn.host, + event.get("type") or "null", + event.get("level") or "null", + event.get("event_id") or "null", self.parsed_dsn.project_id, + self.parsed_dsn.host, ) ) response = self._pool.request( From 51b72f7131ad283433cacff730d02d90cd96e3fe Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 17:56:26 +0200 Subject: [PATCH 05/46] fix: Typing issues --- sentry_sdk/consts.py | 8 ++++---- sentry_sdk/integrations/django/__init__.py | 3 ++- tests/integrations/celery/test_celery.py | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 1a89ba25ce..981d6566ae 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -22,7 +22,7 @@ "release": Optional[str], "environment": Optional[str], "server_name": Optional[str], - "shutdown_timeout": int, + "shutdown_timeout": float, "integrations": List[Integration], "in_app_include": List[str], "in_app_exclude": List[str], @@ -31,7 +31,7 @@ "transport": Optional[ Union[Transport, Type[Transport], Callable[[Event], None]] ], - "sample_rate": int, + "sample_rate": float, "send_default_pii": bool, "http_proxy": Optional[str], "https_proxy": Optional[str], @@ -43,7 +43,7 @@ "attach_stacktrace": bool, "ca_certs": Optional[str], "propagate_traces": bool, - "send_traces": bool, + "traces_sample_rate": float, }, total=False, ) @@ -78,7 +78,7 @@ "ca_certs": None, "propagate_traces": True, "traces_sample_rate": 0.0, -} +} # type: ClientOptions SDK_INFO = { diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index e841b4746f..154273d97b 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -17,6 +17,7 @@ from typing import Optional from typing import Tuple from typing import Union + from typing import Generator from django.core.handlers.wsgi import WSGIRequest # type: ignore from django.http.response import HttpResponse # type: ignore @@ -328,7 +329,7 @@ def format_sql(sql, params): @contextlib.contextmanager def record_sql(sql, params, cursor=None): - # type: (Any, Any, Any) -> None + # type: (Any, Any, Any) -> Generator hub = Hub.current if hub.get_integration(DjangoIntegration) is None: yield diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index e7369e3f5a..1d2b8024b2 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -6,7 +6,6 @@ from sentry_sdk import Hub, configure_scope from sentry_sdk.integrations.celery import CeleryIntegration -from sentry_sdk.tracing import Span from celery import Celery, VERSION from celery.bin import worker From 7b8bbdc41ac25e2e422ff4e306973bcf39257e8b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 17:56:53 +0200 Subject: [PATCH 06/46] fix: Rethrow exception --- sentry_sdk/hub.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 41056aa8c4..8ca1fe567d 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -376,6 +376,7 @@ def span(self, span=None, **kwargs): yield span except Exception: span.set_tag("success", False) + raise else: span.set_tag("success", True) finally: From d06623342dee3022285037f9a165a83883f74321 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:00:07 +0200 Subject: [PATCH 07/46] fix: Fix more typing --- sentry_sdk/tracing.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0c94fa3004..0fdb9a25dc 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -3,6 +3,11 @@ from datetime import datetime +if False: + from typing import Any + from typing import Dict + from typing import List + _traceparent_header_format_re = re.compile( "^[ \t]*" # whitespace "([0-9a-f]{32})?" # trace_id @@ -54,9 +59,9 @@ def __init__( self.same_process_as_parent = same_process_as_parent self.sampled = sampled self.transaction = transaction - self._tags = {} - self._data = {} - self._finished_spans = [] + self._tags = {} # type: Dict[str, str] + self._data = {} # type: Dict[str, Any] + self._finished_spans = [] # type: List[Span] self.start_timestamp = datetime.now() self.timestamp = None @@ -109,14 +114,17 @@ def from_traceparent(cls, traceparent): if match is None: return None - trace_id, span_id, sampled = match.groups() + trace_id, span_id, sampled_str = match.groups() if trace_id is not None: trace_id = "%x" % (int(trace_id, 16),) if span_id is not None: span_id = "%x" % (int(span_id, 16),) - if sampled is not None: - sampled = sampled != "0" + + if sampled_str is not None: + sampled = sampled_str != "0" + else: + sampled = None return cls(trace_id=trace_id, span_id=span_id, sampled=sampled) From e8fadb6528fa4ea7cc818cde11f7cf37ee3feee9 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:02:06 +0200 Subject: [PATCH 08/46] fix: Warn on overwriting existing trace --- sentry_sdk/hub.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 8ca1fe567d..5aa6f48aed 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -395,9 +395,16 @@ def start_span(self, **kwargs): def start_trace(self, **kwargs): span = Span.start_trace(**kwargs) - _, scope = self._stack[-1] + + if scope.span is not None: + logger.warning( + "Creating new trace %s within existing trace %s", + span.trace_id, + scope.span.trace_id, + ) scope.span = span + return span def capture_trace(self, span): From 8919884f9e8c5859112b0d184889f07f63a88e41 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:04:03 +0200 Subject: [PATCH 09/46] fix: Add comments about when we discard a trace span --- sentry_sdk/hub.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 5aa6f48aed..b57900efc9 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -408,12 +408,22 @@ def start_trace(self, **kwargs): return span def capture_trace(self, span): - if ( - span.transaction is None - or span.timestamp is None - or self.client is None - or span.sampled is False - ): + if span.transaction is None: + # If this has no transaction set we assume there's a parent + # transaction for this span that would be flushed out eventually. + return None + + if span.timestamp is None: + # This transaction is not yet finished so we just discard it. + return None + + if self.client is None: + # We have no client and therefore nowhere to send this transaction + # event. + return None + + if span.sampled is False: + # Span is forcibly un-sampled return None sample_rate = self.client.options["traces_sample_rate"] From eecf4ab6cbea1595801f20a085873e8b116a0fa1 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:07:03 +0200 Subject: [PATCH 10/46] fix: Honor span.sampled=True --- sentry_sdk/hub.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index b57900efc9..dc9f0e9da9 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -426,9 +426,11 @@ def capture_trace(self, span): # Span is forcibly un-sampled return None - sample_rate = self.client.options["traces_sample_rate"] - if sample_rate < 1.0 and random.random() >= sample_rate: - return None + if span.sampled is None: + # span.sampled = True -> Span forcibly sampled + sample_rate = self.client.options["traces_sample_rate"] + if sample_rate < 1.0 and random.random() >= sample_rate: + return None return self.capture_event( { From 3eb206e4127742909d6dfbd3210dc878f91989c3 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:07:29 +0200 Subject: [PATCH 11/46] fix: Rename capture_trace to finish_trace --- sentry_sdk/hub.py | 4 ++-- sentry_sdk/integrations/celery.py | 2 +- sentry_sdk/integrations/wsgi.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index dc9f0e9da9..97afe45afa 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -381,7 +381,7 @@ def span(self, span=None, **kwargs): span.set_tag("success", True) finally: span.finish() - self.capture_trace(span) + self.finish_trace(span) def trace(self, **kwargs): return self.span(self.start_trace(**kwargs)) @@ -407,7 +407,7 @@ def start_trace(self, **kwargs): return span - def capture_trace(self, span): + def finish_trace(self, span): if span.transaction is None: # If this has no transaction set we assume there's a parent # transaction for this span that would be flushed out eventually. diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 8d9bdf61e5..84e5014b7d 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -101,7 +101,7 @@ def _inner(*args, **kwargs): return f(*args, **kwargs) finally: span.finish() - hub.capture_trace(span) + hub.finish_trace(span) return _inner diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 67dedaba1a..dfa8f6de54 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -93,7 +93,7 @@ def __call__(self, environ, start_response): finally: if span is not None: span.finish() - hub.capture_trace(span) + hub.finish_trace(span) return _ScopedResponse(hub, rv) From 3d638427aba512eacad1f69c850f6db9fa488dfc Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:08:39 +0200 Subject: [PATCH 12/46] fix: Finish span if caller didnt yet --- sentry_sdk/hub.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 97afe45afa..f41edfefe8 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -408,15 +408,15 @@ def start_trace(self, **kwargs): return span def finish_trace(self, span): + if span.timestamp is None: + # This transaction is not yet finished so we just finish it. + span.finish() + if span.transaction is None: # If this has no transaction set we assume there's a parent # transaction for this span that would be flushed out eventually. return None - if span.timestamp is None: - # This transaction is not yet finished so we just discard it. - return None - if self.client is None: # We have no client and therefore nowhere to send this transaction # event. From 6e9e5eedbe682d10470d6d9a28bea4ac30fa24da Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:12:04 +0200 Subject: [PATCH 13/46] ref(celery): Use hub.trace --- sentry_sdk/integrations/celery.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 84e5014b7d..23f535cc60 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -90,32 +90,19 @@ def _inner(*args, **kwargs): with hub.push_scope() as scope: scope._name = "celery" scope.clear_breadcrumbs() - span = _continue_trace(args[3].get("headers") or {}, scope) + scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) with capture_internal_exceptions(): + # Celery task objects are not a thing to be trusted. Even + # something such as attribute access can fail. scope.transaction = task.name - scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) - - try: + with hub.trace(Span.continue_from_headers(args[3].get("headers") or {})): return f(*args, **kwargs) - finally: - span.finish() - hub.finish_trace(span) return _inner -def _continue_trace(headers, scope): - if headers: - span = Span.continue_from_headers(headers) - else: - span = Span.start_trace() - - scope.span = span - return span - - def _wrap_task_call(task, f): # Need to wrap task call because the exception is caught before we get to # see it. Also celery's reported stacktrace is untrustworthy. From b422f13a891c1837f811c4453ce06abfa1b6a02a Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:13:37 +0200 Subject: [PATCH 14/46] fix: Rename span op to be compatible with ot --- sentry_sdk/integrations/django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 99af2fa3ae..4a8e51a9ad 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -373,7 +373,7 @@ def record_sql(sql, params, cursor=None): if real_sql: with capture_internal_exceptions(): hub.add_breadcrumb(message=real_sql, category="query") - span = hub.start_span(op="sql.query", description=real_sql) + span = hub.start_span(op="db.statement", description=real_sql) if span is None: yield From 26588b6f08be4468f8638ee6220acd13365ac94d Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:14:50 +0200 Subject: [PATCH 15/46] fix(django): Use hub.span --- sentry_sdk/integrations/django/__init__.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 4a8e51a9ad..b75ec18a94 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -371,18 +371,11 @@ def record_sql(sql, params, cursor=None): span = None if real_sql: - with capture_internal_exceptions(): - hub.add_breadcrumb(message=real_sql, category="query") - span = hub.start_span(op="db.statement", description=real_sql) - - if span is None: - yield - else: - try: + hub.add_breadcrumb(message=real_sql, category="query") + with hub.span(op="db.statement", description=real_sql): yield - finally: - span.set_tag("status", sys.exc_info()[1] is None) - span.finish() + else: + yield @contextlib.contextmanager From 6bee45063ac2839c0053c80ce0beff8bf084da8c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:15:23 +0200 Subject: [PATCH 16/46] fix: Rename success tag to error --- sentry_sdk/hub.py | 4 ++-- sentry_sdk/integrations/django/__init__.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index f41edfefe8..f724e17135 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -375,10 +375,10 @@ def span(self, span=None, **kwargs): try: yield span except Exception: - span.set_tag("success", False) + span.set_tag("error", True) raise else: - span.set_tag("success", True) + span.set_tag("error", False) finally: span.finish() self.finish_trace(span) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index b75ec18a94..d3cac12629 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -368,8 +368,6 @@ def record_sql(sql, params, cursor=None): except Exception: pass - span = None - if real_sql: hub.add_breadcrumb(message=real_sql, category="query") with hub.span(op="db.statement", description=real_sql): From 6d91f3f3b202e1ba1bda87cd4e1b6c7f199a2cff Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:17:09 +0200 Subject: [PATCH 17/46] fix: Type signature of hub.trace --- sentry_sdk/integrations/celery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 23f535cc60..c675a721e4 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -97,7 +97,9 @@ def _inner(*args, **kwargs): # something such as attribute access can fail. scope.transaction = task.name - with hub.trace(Span.continue_from_headers(args[3].get("headers") or {})): + with hub.trace( + span=Span.continue_from_headers(args[3].get("headers") or {}) + ): return f(*args, **kwargs) return _inner From 322a8f4636a5dcd4fba320b815e1bd14c2e0ce11 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:42:04 +0200 Subject: [PATCH 18/46] ref: Add layer to abstract breadcrumb vs spans --- sentry_sdk/integrations/django/__init__.py | 73 +++++++++------------- sentry_sdk/integrations/stdlib.py | 53 ++++++++-------- sentry_sdk/tracing.py | 29 +++++++++ 3 files changed, 85 insertions(+), 70 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index d3cac12629..fa302093ed 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -37,6 +37,7 @@ from sentry_sdk.hub import _should_send_default_pii from sentry_sdk.scope import add_global_event_processor from sentry_sdk.serializer import add_global_repr_processor +from sentry_sdk.tracing import record_sql_query from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -331,61 +332,49 @@ def format_sql(sql, params): return sql, rv -@contextlib.contextmanager -def record_sql(sql, params, cursor=None): +def record_sql(sql, param_list, cursor=None): # type: (Any, Any, Any) -> Generator hub = Hub.current if hub.get_integration(DjangoIntegration) is None: yield return - real_sql = None - real_params = None + formatted_queries = [] - try: - # Prefer our own SQL formatting logic because it's the only one that - # has proper value trimming. - real_sql, real_params = format_sql(sql, params) - if real_sql: - real_sql = format_and_strip(real_sql, real_params) - except Exception: - pass + for params in param_list: + real_sql = None + real_params = None - if not real_sql and cursor and hasattr(cursor, "mogrify"): - # If formatting failed and we're using psycopg2, it could be that we're - # looking at a query that uses Composed objects. Use psycopg2's mogrify - # function to format the query. We lose per-parameter trimming but gain - # accuracy in formatting. - # - # This is intentionally the second choice because we assume Composed - # queries are not widely used, while per-parameter trimming is - # generally highly desirable. try: - if cursor and hasattr(cursor, "mogrify"): - real_sql = cursor.mogrify(sql, params) - if isinstance(real_sql, bytes): - real_sql = real_sql.decode(cursor.connection.encoding) + # Prefer our own SQL formatting logic because it's the only one that + # has proper value trimming. + real_sql, real_params = format_sql(sql, params) + if real_sql: + real_sql = format_and_strip(real_sql, real_params) except Exception: pass - if real_sql: - hub.add_breadcrumb(message=real_sql, category="query") - with hub.span(op="db.statement", description=real_sql): - yield - else: - yield + if not real_sql and cursor and hasattr(cursor, "mogrify"): + # If formatting failed and we're using psycopg2, it could be that we're + # looking at a query that uses Composed objects. Use psycopg2's mogrify + # function to format the query. We lose per-parameter trimming but gain + # accuracy in formatting. + # + # This is intentionally the second choice because we assume Composed + # queries are not widely used, while per-parameter trimming is + # generally highly desirable. + try: + if cursor and hasattr(cursor, "mogrify"): + real_sql = cursor.mogrify(sql, params) + if isinstance(real_sql, bytes): + real_sql = real_sql.decode(cursor.connection.encoding) + except Exception: + pass + if real_sql: + formatted_queries.append(real_sql) -@contextlib.contextmanager -def record_many_sql(sql, param_list, cursor): - ctxs = [record_sql(sql, params, cursor).__enter__() for params in param_list] - - try: - yield - finally: - einfo = sys.exc_info() - for ctx in ctxs: - ctx.__exit__(*einfo) + return record_sql_queries(hub, formatted_queries) def install_sql_hook(): @@ -404,7 +393,7 @@ def install_sql_hook(): return def execute(self, sql, params=None): - with record_sql(sql, params, self.cursor): + with record_sql(sql, [params], self.cursor): return real_execute(self, sql, params) def executemany(self, sql, param_list): diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 8e24d95684..bf60551720 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -1,5 +1,6 @@ from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration +from sentry_sdk.tracing import record_http_request try: @@ -23,10 +24,9 @@ def install_httplib(): real_getresponse = HTTPConnection.getresponse def putrequest(self, method, url, *args, **kwargs): - rv = real_putrequest(self, method, url, *args, **kwargs) hub = Hub.current if hub.get_integration(StdlibIntegration) is None: - return rv + return real_putrequest(self, method, url, *args, **kwargs) host = self.host port = self.port @@ -41,40 +41,37 @@ def putrequest(self, method, url, *args, **kwargs): url, ) - self._sentrysdk_data_dict = data = {} - self._sentrysdk_span = hub.start_span( - op="http", description="%s %s" % (real_url, method) - ) + self._sentrysdk_recorder = record_http_request(hub, real_url, method) + self._sentrysdk_data_dict = self._sentrysdk_recorder.__enter__() - for key, value in hub.iter_trace_propagation_headers(): - self.putheader(key, value) + try: + rv = real_putrequest(self, method, url, *args, **kwargs) + + for key, value in hub.iter_trace_propagation_headers(): + self.putheader(key, value) + except Exception: + self._sentrysdk_recorder.__exit__(*sys.exc_info()) + self._sentrysdk_recorder = self._sentrysdk_data_dict = None + raise - data["url"] = real_url - data["method"] = method return rv def getresponse(self, *args, **kwargs): - rv = real_getresponse(self, *args, **kwargs) - hub = Hub.current - if hub.get_integration(StdlibIntegration) is None: - return rv - - data = getattr(self, "_sentrysdk_data_dict", None) or {} + recorder = getattr(self, "_sentrysdk_recorder", None) + data_dict = getattr(self, "_sentrysdk_data_dict", None) - if "status_code" not in data: - data["status_code"] = rv.status - data["reason"] = rv.reason + try: + rv = real_getresponse(self, *args, **kwargs) - span = self._sentrysdk_span - if span is not None: - span.set_tag("status_code", rv.status) - for k, v in data.items(): - span.set_data(k, v) - span.finish() + if recorder is not None and data_dict is not None: + data_dict["httplib_response"] = rv + data_dict["status_code"] = rv.status + data_dict["reason"] = rv.reason + finally: + if recorder is not None: + recorder.__exit__(*sys.exc_info()) + self._sentrysdk_recorder = self._sentrysdk_data_dict = None - hub.add_breadcrumb( - type="http", category="httplib", data=data, hint={"httplib_response": rv} - ) return rv HTTPConnection.putrequest = putrequest diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0fdb9a25dc..561f5c6a7b 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,5 +1,6 @@ import re import uuid +import contextlib from datetime import datetime @@ -155,3 +156,31 @@ def to_json(self): def get_trace_context(self): return {"trace_id": self.trace_id, "span_id": self.span_id} + + +@contextlib.contextmanager +def record_sql_query(hub, queries): + if not queries: + yield None + else: + for query in queries: + hub.add_breadcrumb(message=query, category="query") + + description = ";".join(queries) + with hub.span(op="db.statement", description=description) as span: + yield span + + +@contextlib.contextmanager +def record_http_request(hub, url, method): + data_dict = {"url": url, "method": method} + + with hub.span(op="http", description="%s %s" % (url, method)) as span: + try: + yield data_dict + finally: + httplib_response = data_dict.pop("httplib_response", None) + if "status_code" in data_dict: + span.set_tag("http.status_code", data_dict["status_code"]) + for k, v in data_dict.items(): + span.set_data(k, v) From d710109f5a685bced9db2a24439927a9b20159de Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:51:05 +0200 Subject: [PATCH 19/46] fix: Restore breadcrumb for http calls, fix tests --- sentry_sdk/integrations/django/__init__.py | 10 ++++++---- sentry_sdk/integrations/stdlib.py | 2 ++ sentry_sdk/tracing.py | 16 ++++++++++++---- tests/test_tracing.py | 7 ++++--- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index fa302093ed..12bc1963b0 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import contextlib import sys import weakref +import contextlib from django import VERSION as DJANGO_VERSION # type: ignore from django.db.models.query import QuerySet # type: ignore @@ -332,11 +332,12 @@ def format_sql(sql, params): return sql, rv +@contextlib.contextmanager def record_sql(sql, param_list, cursor=None): # type: (Any, Any, Any) -> Generator hub = Hub.current if hub.get_integration(DjangoIntegration) is None: - yield + yield None return formatted_queries = [] @@ -374,7 +375,8 @@ def record_sql(sql, param_list, cursor=None): if real_sql: formatted_queries.append(real_sql) - return record_sql_queries(hub, formatted_queries) + with record_sql_query(hub, formatted_queries): + yield def install_sql_hook(): @@ -397,7 +399,7 @@ def execute(self, sql, params=None): return real_execute(self, sql, params) def executemany(self, sql, param_list): - with record_many_sql(sql, param_list, self.cursor): + with record_sql(sql, param_list, self.cursor): return real_executemany(self, sql, param_list) CursorWrapper.execute = execute diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index bf60551720..75a843079b 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -1,3 +1,5 @@ +import sys + from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration from sentry_sdk.tracing import record_http_request diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 561f5c6a7b..18bc1c230e 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -180,7 +180,15 @@ def record_http_request(hub, url, method): yield data_dict finally: httplib_response = data_dict.pop("httplib_response", None) - if "status_code" in data_dict: - span.set_tag("http.status_code", data_dict["status_code"]) - for k, v in data_dict.items(): - span.set_data(k, v) + if span is not None: + if "status_code" in data_dict: + span.set_tag("http.status_code", data_dict["status_code"]) + for k, v in data_dict.items(): + span.set_data(k, v) + + hub.add_breadcrumb( + type="http", + category="httplib", + data=data_dict, + hint={"httplib_response": httplib_response}, + ) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index e3b91cd4ba..8c62ee6139 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -19,9 +19,10 @@ def test_basic(sentry_init, capture_events, sample_rate): if sample_rate: event, = events - span1, span2, parent_span = event["spans"] - assert not span1["tags"]["success"] - assert span2["tags"]["success"] + span1, span2 = event["spans"] + parent_span = event + assert span1["tags"]["error"] + assert not span2["tags"]["error"] assert parent_span["transaction"] == "hi" else: assert not events From de516eef7060f5ae2efe24e6875cf5328262e756 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:52:31 +0200 Subject: [PATCH 20/46] fix(wsgi): Use hub.trace --- sentry_sdk/integrations/wsgi.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index dfa8f6de54..25c2a337e1 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -78,22 +78,17 @@ def __call__(self, environ, start_response): hub = Hub(Hub.current) with hub: - span = None with capture_internal_exceptions(): with hub.configure_scope() as scope: scope.clear_breadcrumbs() scope._name = "wsgi" scope.add_event_processor(_make_wsgi_event_processor(environ)) - scope.span = span = Span.continue_from_environ(environ) - try: - rv = self.app(environ, start_response) - except BaseException: - reraise(*_capture_exception(hub)) - finally: - if span is not None: - span.finish() - hub.finish_trace(span) + with hub.trace(Span.continue_from_environ(environ)): + try: + rv = self.app(environ, start_response) + except BaseException: + reraise(*_capture_exception(hub)) return _ScopedResponse(hub, rv) From 1938a78384debc192215db11e1d2d202fac98d00 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:53:13 +0200 Subject: [PATCH 21/46] ref: Add sampling decision to Span repr --- sentry_sdk/tracing.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 18bc1c230e..1caa68482b 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -67,12 +67,16 @@ def __init__( self.timestamp = None def __repr__(self): - return "<%s(transaction=%r, trace_id=%r, span_id=%r, parent_span_id=%r)>" % ( - self.__class__.__name__, - self.transaction, - self.trace_id, - self.span_id, - self.parent_span_id, + return ( + "<%s(transaction=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r)>" + % ( + self.__class__.__name__, + self.transaction, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + ) ) @classmethod From 8be17f21a5f8e872782db3c76152af80f9b27978 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 18:56:22 +0200 Subject: [PATCH 22/46] fix: Add doc comment --- sentry_sdk/integrations/wsgi.py | 2 +- sentry_sdk/tracing.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 25c2a337e1..39136095e9 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -84,7 +84,7 @@ def __call__(self, environ, start_response): scope._name = "wsgi" scope.add_event_processor(_make_wsgi_event_processor(environ)) - with hub.trace(Span.continue_from_environ(environ)): + with hub.trace(span=Span.continue_from_environ(environ)): try: rv = self.app(environ, start_response) except BaseException: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 1caa68482b..2d3f460e62 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -64,6 +64,8 @@ def __init__( self._data = {} # type: Dict[str, Any] self._finished_spans = [] # type: List[Span] self.start_timestamp = datetime.now() + + #: End timestamp of span self.timestamp = None def __repr__(self): From 07151ce4a5267c26dd2dfb6e6152e05c78cf9348 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 19:09:25 +0200 Subject: [PATCH 23/46] fix: Fix httplib tests --- sentry_sdk/integrations/stdlib.py | 11 ++++++----- sentry_sdk/integrations/wsgi.py | 2 +- sentry_sdk/tracing.py | 15 +++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index 75a843079b..1836d0737e 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -43,8 +43,8 @@ def putrequest(self, method, url, *args, **kwargs): url, ) - self._sentrysdk_recorder = record_http_request(hub, real_url, method) - self._sentrysdk_data_dict = self._sentrysdk_recorder.__enter__() + recorder = record_http_request(hub, real_url, method) + data_dict = recorder.__enter__() try: rv = real_putrequest(self, method, url, *args, **kwargs) @@ -52,10 +52,12 @@ def putrequest(self, method, url, *args, **kwargs): for key, value in hub.iter_trace_propagation_headers(): self.putheader(key, value) except Exception: - self._sentrysdk_recorder.__exit__(*sys.exc_info()) - self._sentrysdk_recorder = self._sentrysdk_data_dict = None + recorder.__exit__(*sys.exc_info()) raise + self._sentrysdk_recorder = recorder + self._sentrysdk_data_dict = data_dict + return rv def getresponse(self, *args, **kwargs): @@ -72,7 +74,6 @@ def getresponse(self, *args, **kwargs): finally: if recorder is not None: recorder.__exit__(*sys.exc_info()) - self._sentrysdk_recorder = self._sentrysdk_data_dict = None return rv diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 39136095e9..fbc4358cc5 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -84,7 +84,7 @@ def __call__(self, environ, start_response): scope._name = "wsgi" scope.add_event_processor(_make_wsgi_event_processor(environ)) - with hub.trace(span=Span.continue_from_environ(environ)): + with hub.span(Span.continue_from_environ(environ)): try: rv = self.app(environ, start_response) except BaseException: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 2d3f460e62..891c868cf3 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,4 +1,5 @@ import re +import sys import uuid import contextlib @@ -186,15 +187,17 @@ def record_http_request(hub, url, method): yield data_dict finally: httplib_response = data_dict.pop("httplib_response", None) + if span is not None: if "status_code" in data_dict: span.set_tag("http.status_code", data_dict["status_code"]) for k, v in data_dict.items(): span.set_data(k, v) - hub.add_breadcrumb( - type="http", - category="httplib", - data=data_dict, - hint={"httplib_response": httplib_response}, - ) + if sys.exc_info()[1] is None: + hub.add_breadcrumb( + type="http", + category="httplib", + data=data_dict, + hint={"httplib_response": httplib_response}, + ) From 140c9e21a571b6b067501a924ed0eb2af2dbb3b9 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 19:10:41 +0200 Subject: [PATCH 24/46] fix: Fix basic tests --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 180a8f0ed9..c70d37a2ec 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -378,7 +378,7 @@ def test_transport_works(httpserver, request, capsys, caplog, debug): assert not err and not out assert httpserver.requests - assert any("Sending info event" in record.msg for record in caplog.records) == debug + assert any("Sending event" in record.msg for record in caplog.records) == debug @pytest.mark.tests_internal_exceptions From ad6cf7847481c48bb9a08057f6396d4c8e50fa4f Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 11 Jun 2019 20:00:26 +0200 Subject: [PATCH 25/46] ref: Add test for continue_from_headers --- sentry_sdk/hub.py | 10 ++++++---- sentry_sdk/tracing.py | 4 ++-- tests/test_tracing.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index f724e17135..907721282f 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -383,8 +383,8 @@ def span(self, span=None, **kwargs): span.finish() self.finish_trace(span) - def trace(self, **kwargs): - return self.span(self.start_trace(**kwargs)) + def trace(self, *args, **kwargs): + return self.span(self.start_trace(*args, **kwargs)) def start_span(self, **kwargs): _, scope = self._stack[-1] @@ -393,8 +393,10 @@ def start_span(self, **kwargs): return span.new_span(**kwargs) return None - def start_trace(self, **kwargs): - span = Span.start_trace(**kwargs) + def start_trace(self, span=None, **kwargs): + if span is None: + span = Span.start_trace(**kwargs) + _, scope = self._stack[-1] if scope.span is not None: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 891c868cf3..0b33c63594 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -125,9 +125,9 @@ def from_traceparent(cls, traceparent): trace_id, span_id, sampled_str = match.groups() if trace_id is not None: - trace_id = "%x" % (int(trace_id, 16),) + trace_id = "{:032x}".format(int(trace_id, 16)) if span_id is not None: - span_id = "%x" % (int(span_id, 16),) + span_id = "{:016x}".format(int(span_id, 16)) if sampled_str is not None: sampled = sampled_str != "0" diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 8c62ee6139..a4cc16724d 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -1,6 +1,7 @@ import pytest -from sentry_sdk import Hub +from sentry_sdk import Hub, capture_message +from sentry_sdk.tracing import Span @pytest.mark.parametrize("sample_rate", [0.0, 1.0]) @@ -26,3 +27,35 @@ def test_basic(sentry_init, capture_events, sample_rate): assert parent_span["transaction"] == "hi" else: assert not events + + +def test_continue_from_headers(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + with Hub.current.trace(transaction="hi"): + with Hub.current.span() as old_span: + headers = dict(Hub.current.iter_trace_propagation_headers()) + + span = Span.continue_from_headers(headers) + assert span is not None + assert span.trace_id == old_span.trace_id + + with Hub.current.trace(span): + with Hub.current.configure_scope() as scope: + scope.transaction = "ho" + + capture_message("hello") + + trace1, message, trace2 = events + + assert trace1["transaction"] == "hi" + assert trace2["transaction"] == "ho" + + assert ( + trace1["contexts"]["trace"]["trace_id"] + == trace2["contexts"]["trace"]["trace_id"] + == span.trace_id + == message["contexts"]["trace"]["trace_id"] + ) + assert message["message"] == "hello" From e3944c36c80679ac027e9d120cad3240b0a0f8d3 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 12 Jun 2019 15:45:38 +0200 Subject: [PATCH 26/46] fix: Add test for sampling propagation --- sentry_sdk/tracing.py | 10 ++++++++-- tests/test_tracing.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0b33c63594..3b805afc23 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -94,6 +94,7 @@ def new_span(self, **kwargs): trace_id=self.trace_id, span_id=uuid.uuid4().hex[16:], parent_span_id=self.span_id, + sampled=self.sampled, **kwargs ) rv._finished_spans = self._finished_spans @@ -129,7 +130,7 @@ def from_traceparent(cls, traceparent): if span_id is not None: span_id = "{:016x}".format(int(span_id, 16)) - if sampled_str is not None: + if sampled_str: sampled = sampled_str != "0" else: sampled = None @@ -137,7 +138,12 @@ def from_traceparent(cls, traceparent): return cls(trace_id=trace_id, span_id=span_id, sampled=sampled) def to_traceparent(self): - return "%s-%s-%s" % (self.trace_id, self.span_id, "1" if self.sampled else "0") + sampled = "" + if self.sampled is True: + sampled = "1" + if self.sampled is False: + sampled = "0" + return "%s-%s-%s" % (self.trace_id, self.span_id, sampled) def set_tag(self, key, value): self._tags[key] = value diff --git a/tests/test_tracing.py b/tests/test_tracing.py index a4cc16724d..3d6d5ef728 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -29,16 +29,27 @@ def test_basic(sentry_init, capture_events, sample_rate): assert not events -def test_continue_from_headers(sentry_init, capture_events): +@pytest.mark.parametrize("sampled", [True, False, None]) +def test_continue_from_headers(sentry_init, capture_events, sampled): sentry_init(traces_sample_rate=1.0) events = capture_events() - with Hub.current.trace(transaction="hi"): + with Hub.current.trace(transaction="hi") as old_trace: + old_trace.sampled = sampled with Hub.current.span() as old_span: headers = dict(Hub.current.iter_trace_propagation_headers()) + header = headers["sentry-trace"] + if sampled is True: + assert header.endswith("-1") + if sampled is False: + assert header.endswith("-0") + if sampled is None: + assert header.endswith("-") + span = Span.continue_from_headers(headers) assert span is not None + assert span.sampled == sampled assert span.trace_id == old_span.trace_id with Hub.current.trace(span): @@ -47,15 +58,19 @@ def test_continue_from_headers(sentry_init, capture_events): capture_message("hello") - trace1, message, trace2 = events + if sampled is False: + message, = events + else: + trace1, message, trace2 = events + + assert trace1["transaction"] == "hi" + assert trace2["transaction"] == "ho" - assert trace1["transaction"] == "hi" - assert trace2["transaction"] == "ho" + assert ( + trace1["contexts"]["trace"]["trace_id"] + == trace2["contexts"]["trace"]["trace_id"] + == span.trace_id + == message["contexts"]["trace"]["trace_id"] + ) - assert ( - trace1["contexts"]["trace"]["trace_id"] - == trace2["contexts"]["trace"]["trace_id"] - == span.trace_id - == message["contexts"]["trace"]["trace_id"] - ) assert message["message"] == "hello" From cb54a34140f8bd3471b38055fe74ec002fe5520a Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 12 Jun 2019 17:42:11 +0200 Subject: [PATCH 27/46] fix: Dont crash on AnnotatedValue --- sentry_sdk/tracing.py | 8 ++++-- sentry_sdk/utils.py | 64 ++++++++++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 3b805afc23..01e689aae4 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -5,7 +5,11 @@ from datetime import datetime +from sentry_sdk.utils import concat_strings + + if False: + from typing import Optional from typing import Any from typing import Dict from typing import List @@ -131,7 +135,7 @@ def from_traceparent(cls, traceparent): span_id = "{:016x}".format(int(span_id, 16)) if sampled_str: - sampled = sampled_str != "0" + sampled = sampled_str != "0" # type: Optional[bool] else: sampled = None @@ -179,7 +183,7 @@ def record_sql_query(hub, queries): for query in queries: hub.add_breadcrumb(message=query, category="query") - description = ";".join(queries) + description = concat_strings(queries) with hub.span(op="db.statement", description=description) as span: yield span diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 4fa78bdb64..fcc1d064f8 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -688,7 +688,36 @@ def format_and_strip( if not chunks: raise ValueError("No formatting placeholders found") - params = list(reversed(params)) + params = params[: len(chunks) - 1] + + if len(params) < len(chunks) - 1: + raise ValueError("Not enough params.") + + concat_chunks = [] + iter_chunks = iter(chunks) # type: Optional[Iterator] + iter_params = iter(params) # type: Optional[Iterator] + + while iter_chunks is not None or iter_params is not None: + if iter_chunks is not None: + try: + concat_chunks.append(next(iter_chunks)) + except StopIteration: + iter_chunks = None + + if iter_params is not None: + try: + concat_chunks.append(str(next(iter_params))) + except StopIteration: + iter_params = None + + return concat_strings( + concat_chunks, strip_string=strip_string, max_length=max_length + ) + + +def concat_strings( + chunks, strip_string=strip_string, max_length=MAX_FORMAT_PARAM_LENGTH +): rv_remarks = [] # type: List[Any] rv_original_length = 0 rv_length = 0 @@ -700,28 +729,25 @@ def realign_remark(remark): for i, x in enumerate(remark) ] - for chunk in chunks[:-1]: - rv.append(chunk) - rv_length += len(chunk) - rv_original_length += len(chunk) - if not params: - raise ValueError("Not enough params.") - param = params.pop() + for chunk in chunks: + if isinstance(chunk, AnnotatedValue): + # Assume it's already stripped! + stripped_chunk = chunk + chunk = chunk.value + else: + stripped_chunk = strip_string(chunk, max_length=max_length) - stripped_param = strip_string(param, max_length=max_length) - if isinstance(stripped_param, AnnotatedValue): + if isinstance(stripped_chunk, AnnotatedValue): rv_remarks.extend( - realign_remark(remark) for remark in stripped_param.metadata["rem"] + realign_remark(remark) for remark in stripped_chunk.metadata["rem"] ) - stripped_param = stripped_param.value - - rv_original_length += len(param) - rv_length += len(stripped_param) - rv.append(stripped_param) + stripped_chunk_value = stripped_chunk.value + else: + stripped_chunk_value = stripped_chunk - rv.append(chunks[-1]) - rv_length += len(chunks[-1]) - rv_original_length += len(chunks[-1]) + rv_original_length += len(chunk) + rv_length += len(stripped_chunk_value) # type: ignore + rv.append(stripped_chunk_value) # type: ignore rv_joined = u"".join(rv) assert len(rv_joined) == rv_length From 668fb747a6e2a26caa216ae27e85aa80812a5b4c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 12 Jun 2019 21:40:48 +0200 Subject: [PATCH 28/46] fix: Capture trace for wsgi requests --- sentry_sdk/integrations/wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index fbc4358cc5..25c2a337e1 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -84,7 +84,7 @@ def __call__(self, environ, start_response): scope._name = "wsgi" scope.add_event_processor(_make_wsgi_event_processor(environ)) - with hub.span(Span.continue_from_environ(environ)): + with hub.trace(Span.continue_from_environ(environ)): try: rv = self.app(environ, start_response) except BaseException: From 9c2188940e651b640563ed9964266238e8314120 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 12 Jun 2019 22:36:27 +0200 Subject: [PATCH 29/46] fix: Write description and op into trace --- sentry_sdk/tracing.py | 11 ++++++++++- tests/test_tracing.py | 8 ++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 01e689aae4..fa8f0f86dd 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -65,6 +65,8 @@ def __init__( self.same_process_as_parent = same_process_as_parent self.sampled = sampled self.transaction = transaction + self.op = op + self.description = description self._tags = {} # type: Dict[str, str] self._data = {} # type: Dict[str, Any] self._finished_spans = [] # type: List[Span] @@ -165,6 +167,8 @@ def to_json(self): "span_id": self.span_id, "parent_span_id": self.parent_span_id, "transaction": self.transaction, + "op": self.op, + "description": self.description, "tags": self._tags, "data": self._data, "start_timestamp": self.start_timestamp, @@ -172,7 +176,12 @@ def to_json(self): } def get_trace_context(self): - return {"trace_id": self.trace_id, "span_id": self.span_id} + return { + "trace_id": self.trace_id, + "span_id": self.span_id, + "op": self.op, + "description": self.description, + } @contextlib.contextmanager diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 3d6d5ef728..cdd7ea451c 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -11,10 +11,10 @@ def test_basic(sentry_init, capture_events, sample_rate): with Hub.current.trace(transaction="hi"): with pytest.raises(ZeroDivisionError): - with Hub.current.span(): + with Hub.current.span(op='foo', description='foodesc'): 1 / 0 - with Hub.current.span(): + with Hub.current.span(op='bar', description='bardesc'): pass if sample_rate: @@ -23,7 +23,11 @@ def test_basic(sentry_init, capture_events, sample_rate): span1, span2 = event["spans"] parent_span = event assert span1["tags"]["error"] + assert span1['op'] == 'foo' + assert span1['description'] == 'foodesc' assert not span2["tags"]["error"] + assert span2['op'] == 'bar' + assert span2['description'] == 'bardesc' assert parent_span["transaction"] == "hi" else: assert not events From eace10351fc879957f347296b43d1de91ccd953f Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 12 Jun 2019 22:36:45 +0200 Subject: [PATCH 30/46] fix: Formatting --- tests/test_tracing.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index cdd7ea451c..eb9cd30a9b 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -11,10 +11,10 @@ def test_basic(sentry_init, capture_events, sample_rate): with Hub.current.trace(transaction="hi"): with pytest.raises(ZeroDivisionError): - with Hub.current.span(op='foo', description='foodesc'): + with Hub.current.span(op="foo", description="foodesc"): 1 / 0 - with Hub.current.span(op='bar', description='bardesc'): + with Hub.current.span(op="bar", description="bardesc"): pass if sample_rate: @@ -23,11 +23,11 @@ def test_basic(sentry_init, capture_events, sample_rate): span1, span2 = event["spans"] parent_span = event assert span1["tags"]["error"] - assert span1['op'] == 'foo' - assert span1['description'] == 'foodesc' + assert span1["op"] == "foo" + assert span1["description"] == "foodesc" assert not span2["tags"]["error"] - assert span2['op'] == 'bar' - assert span2['description'] == 'bardesc' + assert span2["op"] == "bar" + assert span2["description"] == "bardesc" assert parent_span["transaction"] == "hi" else: assert not events From c4c220249aa76f2f67fee618ddcbe7e67f98eae5 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 14 Jun 2019 13:59:14 +0200 Subject: [PATCH 31/46] feat: Add tracing example, add rq integration --- examples/tracing/README.md | 14 + examples/tracing/events | 10 + examples/tracing/events.svg | 439 ++++++++++++++++++++++ examples/tracing/static/tracing.js | 519 ++++++++++++++++++++++++++ examples/tracing/templates/index.html | 57 +++ examples/tracing/traceviewer.py | 61 +++ examples/tracing/tracing.py | 73 ++++ sentry_sdk/hub.py | 1 + sentry_sdk/integrations/rq.py | 30 +- sentry_sdk/tracing.py | 9 +- 10 files changed, 1206 insertions(+), 7 deletions(-) create mode 100644 examples/tracing/README.md create mode 100644 examples/tracing/events create mode 100644 examples/tracing/events.svg create mode 100644 examples/tracing/static/tracing.js create mode 100644 examples/tracing/templates/index.html create mode 100644 examples/tracing/traceviewer.py create mode 100644 examples/tracing/tracing.py diff --git a/examples/tracing/README.md b/examples/tracing/README.md new file mode 100644 index 0000000000..ae7b79724a --- /dev/null +++ b/examples/tracing/README.md @@ -0,0 +1,14 @@ +To run this app: + +1. Have a Redis on the Redis default port (if you have Sentry running locally, + you probably already have this) +2. `pip install sentry-sdk flask rq` +3. `FLASK_APP=tracing flask run` +4. `FLASK_APP=tracing flask worker` +5. Go to `http://localhost:5000/` and enter a base64-encoded string (one is prefilled) +6. Hit submit, wait for heavy computation to end +7. `cat events | python traceviewer.py | dot -T svg > events.svg` +8. `open events.svg` + +The last two steps are for viewing the traces. Nothing gets sent to Sentry +right now because Sentry does not deal with this data yet. diff --git a/examples/tracing/events b/examples/tracing/events new file mode 100644 index 0000000000..f68ae2b8c2 --- /dev/null +++ b/examples/tracing/events @@ -0,0 +1,10 @@ +{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "index", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "968cff94913ebb07"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Upgrade-Insecure-Requests": "1", "Connection": "keep-alive", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "f9f4b21dd9da4c389426c1ffd2b62410", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "static", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "8eb30d5ae5f3403ba3a036e696111ec3", "span_id": "97e894108ff7a8cd"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/static/tracing.js", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache"}}, "event_id": "1c71c7cb32934550bb49f05b6c2d4052", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "index", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "b7627895a90b41718be82d3ad21ab2f4", "span_id": "9fa95b4ffdcbe177"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0", "Connection": "keep-alive", "Pragma": "no-cache", "Cache-Control": "no-cache"}}, "event_id": "1430ad5b0a0d45dca3f02c10271628f9", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:38Z", "transaction": "static", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"trace_id": "1636fdb33db84e7c9a4e606c1b176971", "span_id": "b682a29ead55075f"}}, "timestamp": "2019-06-14T14:01:38Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/static/tracing.js.map", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0", "Connection": "keep-alive"}}, "event_id": "72b1224307294e0fb6d6b1958076c4cc", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "compute", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "946edde6ee421874"}}, "timestamp": "2019-06-14T14:01:40Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/compute/aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "c72fd945c1174140a00bdbf6f6ed8fc5", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "bf5be759039ede9a"}}, "timestamp": "2019-06-14T14:01:40Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "e8c17b0cbe2045758aaffc2f11672fab", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "b2d56249f7fdf327"}}, "timestamp": "2019-06-14T14:01:40Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "6577f8056383427d85df5b33bf9ccc2c", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:41Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "ac62ff8ae1b2eda6"}}, "timestamp": "2019-06-14T14:01:41Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "c03dfbab8a8145eeaa0d1a1adfcfcaa5", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:40Z", "transaction": "tracing.decode_base64", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "worker"], "rq-job": {"kwargs": {"redis_key": "sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "encoded": "aGVsbG8gd29ybGQK"}, "args": [], "description": "tracing.decode_base64(encoded=u'aGVsbG8gd29ybGQK', redis_key='sentry-python-tracing-example-result:aGVsbG8gd29ybGQK')", "func": "tracing.decode_base64", "job_id": "fabff810-3dbb-45d3-987e-86395790dfa9"}}, "contexts": {"trace": {"parent_span_id": "946edde6ee421874", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "9c2a6db8c79068a2"}}, "timestamp": "2019-06-14T14:01:41Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "event_id": "2975518984734ef49d2f75db4e928ddc", "platform": "python", "spans": [{"start_timestamp": "2019-06-14T14:01:41Z", "same_process_as_parent": true, "description": "http://httpbin.org/base64/aGVsbG8gd29ybGQK GET", "tags": {"http.status_code": 200, "error": false}, "timestamp": "2019-06-14T14:01:41Z", "parent_span_id": "9c2a6db8c79068a2", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "op": "http", "data": {"url": "http://httpbin.org/base64/aGVsbG8gd29ybGQK", "status_code": 200, "reason": "OK", "method": "GET"}, "span_id": "8c931f4740435fb8"}], "breadcrumbs": [{"category": "httplib", "data": {"url": "http://httpbin.org/base64/aGVsbG8gd29ybGQK", "status_code": 200, "reason": "OK", "method": "GET"}, "type": "http", "timestamp": "2019-06-14T12:01:41Z"}, {"category": "rq.worker", "ty": "log", "timestamp": "2019-06-14T14:01:41Z", "level": "info", "data": {"asctime": "14:01:41"}, "message": "\u001b[32mdefault\u001b[39;49;00m: \u001b[34mJob OK\u001b[39;49;00m (fabff810-3dbb-45d3-987e-86395790dfa9)", "type": "default"}, {"category": "rq.worker", "ty": "log", "timestamp": "2019-06-14T14:01:41Z", "level": "info", "data": {"asctime": "14:01:41"}, "message": "Result is kept for 500 seconds", "type": "default"}], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} +{"start_timestamp": "2019-06-14T14:01:41Z", "transaction": "wait", "server_name": "apfeltasche.local", "extra": {"sys.argv": ["/Users/untitaker/projects/sentry-python/.venv/bin/flask", "run", "--reload"]}, "contexts": {"trace": {"parent_span_id": "bce14471e0e9654d", "trace_id": "a0fa8803753e40fd8124b21eeb2986b5", "span_id": "9d91c6558b2e4c06"}}, "timestamp": "2019-06-14T14:01:41Z", "modules": {"more-itertools": "5.0.0", "six": "1.12.0", "funcsigs": "1.0.2", "vine": "1.2.0", "tqdm": "4.31.1", "configparser": "3.7.4", "py-cpuinfo": "5.0.0", "pygments": "2.3.1", "attrs": "19.1.0", "pip": "19.0.3", "blinker": "1.4", "parso": "0.4.0", "django": "1.11.20", "click": "7.0", "requests-toolbelt": "0.9.1", "virtualenv": "16.4.3", "autoflake": "1.3", "tox": "3.7.0", "statistics": "1.0.3.5", "rq": "1.0", "flask": "1.0.2", "pkginfo": "1.5.0.1", "py": "1.8.0", "redis": "3.2.1", "celery": "4.2.1", "docutils": "0.14", "jedi": "0.13.3", "pytest": "4.4.1", "kombu": "4.4.0", "werkzeug": "0.14.1", "webencodings": "0.5.1", "toml": "0.10.0", "itsdangerous": "1.1.0", "certifi": "2019.3.9", "readme-renderer": "24.0", "wheel": "0.33.1", "pathlib2": "2.3.3", "python": "2.7.15", "urllib3": "1.24.1", "sentry-sdk": "0.9.0", "twine": "1.13.0", "pytest-benchmark": "3.2.2", "markupsafe": "1.1.1", "billiard": "3.5.0.5", "jinja2": "2.10", "coverage": "4.5.3", "bleach": "3.1.0", "pluggy": "0.9.0", "atomicwrites": "1.3.0", "filelock": "3.0.10", "pyflakes": "2.1.1", "pytz": "2018.9", "futures": "3.2.0", "pytest-cov": "2.7.1", "backports.functools-lru-cache": "1.5", "wsgiref": "0.1.2", "python-jsonrpc-server": "0.1.2", "python-language-server": "0.26.1", "future": "0.17.1", "chardet": "3.0.4", "amqp": "2.4.2", "setuptools": "40.8.0", "requests": "2.21.0", "idna": "2.8", "scandir": "1.10.0"}, "request": {"url": "http://127.0.0.1:5000/wait/sentry-python-tracing-example-result:aGVsbG8gd29ybGQK", "query_string": "", "method": "GET", "env": {"SERVER_NAME": "127.0.0.1", "SERVER_PORT": "5000"}, "headers": {"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Host": "127.0.0.1:5000", "Accept": "*/*", "Sentry-Trace": "00-a0fa8803753e40fd8124b21eeb2986b5-bce14471e0e9654d-00", "Connection": "keep-alive", "Referer": "http://127.0.0.1:5000/", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0"}}, "event_id": "339cfc84adf0405986514c808afb0f68", "platform": "python", "spans": [], "breadcrumbs": [], "type": "transaction", "sdk": {"version": "0.9.0", "name": "sentry.python", "packages": [{"version": "0.9.0", "name": "pypi:sentry-sdk"}], "integrations": ["argv", "atexit", "dedupe", "excepthook", "flask", "logging", "modules", "rq", "stdlib", "threading"]}} diff --git a/examples/tracing/events.svg b/examples/tracing/events.svg new file mode 100644 index 0000000000..33f9c98f00 --- /dev/null +++ b/examples/tracing/events.svg @@ -0,0 +1,439 @@ + + + + + + +mytrace + + + +213977312221895837199412816265326724789 + +trace:index (a0fa8803753e40fd8124b21eeb2986b5) + + + +10848326615985732359 + +span:index (968cff94913ebb07) + + + +213977312221895837199412816265326724789->10848326615985732359 + + + + + +10695730148961032308 + +span:compute (946edde6ee421874) + + + +213977312221895837199412816265326724789->10695730148961032308 + + + + + +13788869053623754394 + +span:wait (bf5be759039ede9a) + + + +213977312221895837199412816265326724789->13788869053623754394 + + + + + +12886313978623292199 + +span:wait (b2d56249f7fdf327) + + + +213977312221895837199412816265326724789->12886313978623292199 + + + + + +12421771694198418854 + +span:wait (ac62ff8ae1b2eda6) + + + +213977312221895837199412816265326724789->12421771694198418854 + + + + + +10129474377767673784 + +span:http://httpbin.org/base64/aGVsbG8gd29ybGQK GET (8c931f4740435fb8) + + + +213977312221895837199412816265326724789->10129474377767673784 + + + + + +11252927259328145570 + +span:tracing.decode_base64 (9c2a6db8c79068a2) + + + +213977312221895837199412816265326724789->11252927259328145570 + + + + + +11354074206287318022 + +span:wait (9d91c6558b2e4c06) + + + +213977312221895837199412816265326724789->11354074206287318022 + + + + + +189680067412161401408211119957991300803 + +trace:static (8eb30d5ae5f3403ba3a036e696111ec3) + + + +10946161693179750605 + +span:static (97e894108ff7a8cd) + + + +189680067412161401408211119957991300803->10946161693179750605 + + + + + +243760014067241244567037757667822711540 + +trace:index (b7627895a90b41718be82d3ad21ab2f4) + + + +11504827122213183863 + +span:index (9fa95b4ffdcbe177) + + + +243760014067241244567037757667822711540->11504827122213183863 + + + + + +29528545588201242414770090507008174449 + +trace:static (1636fdb33db84e7c9a4e606c1b176971) + + + +13151252664271832927 + +span:static (b682a29ead55075f) + + + +29528545588201242414770090507008174449->13151252664271832927 + + + + + +10695730148961032308->10848326615985732359 + + + + + +10695730148961032308->10946161693179750605 + + + + + +10695730148961032308->11504827122213183863 + + + + + +10695730148961032308->13151252664271832927 + + + + + +10695730148961032308->11252927259328145570 + + + + + +13610234804785734989 + +13610234804785734989 + + + +13610234804785734989->10695730148961032308 + + + + + +13610234804785734989->13788869053623754394 + + + + + +13610234804785734989->12886313978623292199 + + + + + +13610234804785734989->12421771694198418854 + + + + + +13610234804785734989->11354074206287318022 + + + + + +13788869053623754394->10848326615985732359 + + + + + +13788869053623754394->10946161693179750605 + + + + + +13788869053623754394->11504827122213183863 + + + + + +13788869053623754394->13151252664271832927 + + + + + +12886313978623292199->10848326615985732359 + + + + + +12886313978623292199->10946161693179750605 + + + + + +12886313978623292199->11504827122213183863 + + + + + +12886313978623292199->13151252664271832927 + + + + + +12421771694198418854->10848326615985732359 + + + + + +12421771694198418854->10946161693179750605 + + + + + +12421771694198418854->11504827122213183863 + + + + + +12421771694198418854->13151252664271832927 + + + + + +12421771694198418854->10695730148961032308 + + + + + +12421771694198418854->13788869053623754394 + + + + + +12421771694198418854->12886313978623292199 + + + + + +10129474377767673784->10848326615985732359 + + + + + +10129474377767673784->10946161693179750605 + + + + + +10129474377767673784->11504827122213183863 + + + + + +10129474377767673784->13151252664271832927 + + + + + +10129474377767673784->10695730148961032308 + + + + + +10129474377767673784->13788869053623754394 + + + + + +10129474377767673784->12886313978623292199 + + + + + +11252927259328145570->10848326615985732359 + + + + + +11252927259328145570->10946161693179750605 + + + + + +11252927259328145570->11504827122213183863 + + + + + +11252927259328145570->13151252664271832927 + + + + + +11252927259328145570->10129474377767673784 + + + + + +11354074206287318022->10848326615985732359 + + + + + +11354074206287318022->10946161693179750605 + + + + + +11354074206287318022->11504827122213183863 + + + + + +11354074206287318022->13151252664271832927 + + + + + +11354074206287318022->10695730148961032308 + + + + + +11354074206287318022->13788869053623754394 + + + + + +11354074206287318022->12886313978623292199 + + + + + diff --git a/examples/tracing/static/tracing.js b/examples/tracing/static/tracing.js new file mode 100644 index 0000000000..ad4dc9a822 --- /dev/null +++ b/examples/tracing/static/tracing.js @@ -0,0 +1,519 @@ +(function (__window) { +var exports = {}; +Object.defineProperty(exports, '__esModule', { value: true }); + +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +/* global Reflect, Promise */ + +var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); +}; + +function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +} + +var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; + +function __read(o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +} + +function __spread() { + for (var ar = [], i = 0; i < arguments.length; i++) + ar = ar.concat(__read(arguments[i])); + return ar; +} + +/** An error emitted by Sentry SDKs and related utilities. */ +var SentryError = /** @class */ (function (_super) { + __extends(SentryError, _super); + function SentryError(message) { + var _newTarget = this.constructor; + var _this = _super.call(this, message) || this; + _this.message = message; + // tslint:disable:no-unsafe-any + _this.name = _newTarget.prototype.constructor.name; + Object.setPrototypeOf(_this, _newTarget.prototype); + return _this; + } + return SentryError; +}(Error)); + +/** + * Checks whether given value's type is one of a few Error or Error-like + * {@link isError}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +/** + * Checks whether given value's type is an regexp + * {@link isRegExp}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +function isRegExp(wat) { + return Object.prototype.toString.call(wat) === '[object RegExp]'; +} + +/** + * Requires a module which is protected _against bundler minification. + * + * @param request The module path to resolve + */ +/** + * Checks whether we're in the Node.js or Browser environment + * + * @returns Answer to given question + */ +function isNodeEnv() { + // tslint:disable:strict-type-predicates + return Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'; +} +var fallbackGlobalObject = {}; +/** + * Safely get global scope object + * + * @returns Global scope object + */ +function getGlobalObject() { + return (isNodeEnv() + ? global + : typeof window !== 'undefined' + ? window + : typeof self !== 'undefined' + ? self + : fallbackGlobalObject); +} +/** JSDoc */ +function consoleSandbox(callback) { + var global = getGlobalObject(); + var levels = ['debug', 'info', 'warn', 'error', 'log', 'assert']; + if (!('console' in global)) { + return callback(); + } + var originalConsole = global.console; + var wrappedLevels = {}; + // Restore all wrapped console methods + levels.forEach(function (level) { + if (level in global.console && originalConsole[level].__sentry__) { + wrappedLevels[level] = originalConsole[level].__sentry_wrapped__; + originalConsole[level] = originalConsole[level].__sentry_original__; + } + }); + // Perform callback manipulations + var result = callback(); + // Revert restoration to wrapped state + Object.keys(wrappedLevels).forEach(function (level) { + originalConsole[level] = wrappedLevels[level]; + }); + return result; +} + +// TODO: Implement different loggers for different environments +var global$1 = getGlobalObject(); +/** Prefix for logging strings */ +var PREFIX = 'Sentry Logger '; +/** JSDoc */ +var Logger = /** @class */ (function () { + /** JSDoc */ + function Logger() { + this._enabled = false; + } + /** JSDoc */ + Logger.prototype.disable = function () { + this._enabled = false; + }; + /** JSDoc */ + Logger.prototype.enable = function () { + this._enabled = true; + }; + /** JSDoc */ + Logger.prototype.log = function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + if (!this._enabled) { + return; + } + consoleSandbox(function () { + global$1.console.log(PREFIX + "[Log]: " + args.join(' ')); // tslint:disable-line:no-console + }); + }; + /** JSDoc */ + Logger.prototype.warn = function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + if (!this._enabled) { + return; + } + consoleSandbox(function () { + global$1.console.warn(PREFIX + "[Warn]: " + args.join(' ')); // tslint:disable-line:no-console + }); + }; + /** JSDoc */ + Logger.prototype.error = function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + if (!this._enabled) { + return; + } + consoleSandbox(function () { + global$1.console.error(PREFIX + "[Error]: " + args.join(' ')); // tslint:disable-line:no-console + }); + }; + return Logger; +}()); +// Ensure we only have a single logger instance, even if multiple versions of @sentry/utils are being used +global$1.__SENTRY__ = global$1.__SENTRY__ || {}; +var logger = global$1.__SENTRY__.logger || (global$1.__SENTRY__.logger = new Logger()); + +// tslint:disable:no-unsafe-any + +/** + * Wrap a given object method with a higher-order function + * + * @param source An object that contains a method to be wrapped. + * @param name A name of method to be wrapped. + * @param replacement A function that should be used to wrap a given method. + * @returns void + */ +function fill(source, name, replacement) { + if (!(name in source)) { + return; + } + var original = source[name]; + var wrapped = replacement(original); + // Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work + // otherwise it'll throw "TypeError: Object.defineProperties called on non-object" + // tslint:disable-next-line:strict-type-predicates + if (typeof wrapped === 'function') { + try { + wrapped.prototype = wrapped.prototype || {}; + Object.defineProperties(wrapped, { + __sentry__: { + enumerable: false, + value: true, + }, + __sentry_original__: { + enumerable: false, + value: original, + }, + __sentry_wrapped__: { + enumerable: false, + value: wrapped, + }, + }); + } + catch (_Oo) { + // This can throw if multiple fill happens on a global object like XMLHttpRequest + // Fixes https://github.com/getsentry/sentry-javascript/issues/2043 + } + } + source[name] = wrapped; +} + +// Slightly modified (no IE8 support, ES6) and transcribed to TypeScript + +/** + * Checks if the value matches a regex or includes the string + * @param value The string value to be checked against + * @param pattern Either a regex or a string that must be contained in value + */ +function isMatchingPattern(value, pattern) { + if (isRegExp(pattern)) { + return pattern.test(value); + } + if (typeof pattern === 'string') { + return value.includes(pattern); + } + return false; +} + +/** + * Tells whether current environment supports Fetch API + * {@link supportsFetch}. + * + * @returns Answer to the given question. + */ +function supportsFetch() { + if (!('fetch' in getGlobalObject())) { + return false; + } + try { + // tslint:disable-next-line:no-unused-expression + new Headers(); + // tslint:disable-next-line:no-unused-expression + new Request(''); + // tslint:disable-next-line:no-unused-expression + new Response(); + return true; + } + catch (e) { + return false; + } +} +/** + * Tells whether current environment supports Fetch API natively + * {@link supportsNativeFetch}. + * + * @returns Answer to the given question. + */ +function supportsNativeFetch() { + if (!supportsFetch()) { + return false; + } + var global = getGlobalObject(); + return global.fetch.toString().indexOf('native') !== -1; +} + +/** SyncPromise internal states */ +var States; +(function (States) { + /** Pending */ + States["PENDING"] = "PENDING"; + /** Resolved / OK */ + States["RESOLVED"] = "RESOLVED"; + /** Rejected / Error */ + States["REJECTED"] = "REJECTED"; +})(States || (States = {})); + +/** + * Tracing Integration + */ +var Tracing = /** @class */ (function () { + /** + * Constructor for Tracing + * + * @param _options TracingOptions + */ + function Tracing(_options) { + if (_options === void 0) { _options = {}; } + this._options = _options; + /** + * @inheritDoc + */ + this.name = Tracing.id; + if (!Array.isArray(_options.tracingOrigins) || _options.tracingOrigins.length === 0) { + consoleSandbox(function () { + var defaultTracingOrigins = ['localhost', /^\//]; + // @ts-ignore + console.warn('Sentry: You need to define `tracingOrigins` in the options. Set an array of urls or patterns to trace.'); + // @ts-ignore + console.warn("Sentry: We added a reasonable default for you: " + defaultTracingOrigins); + _options.tracingOrigins = defaultTracingOrigins; + }); + } + } + /** + * @inheritDoc + */ + Tracing.prototype.setupOnce = function (_, getCurrentHub) { + if (this._options.traceXHR !== false) { + this._traceXHR(getCurrentHub); + } + if (this._options.traceFetch !== false) { + this._traceFetch(getCurrentHub); + } + if (this._options.autoStartOnDomReady !== false) { + getGlobalObject().addEventListener('DOMContentLoaded', function () { + Tracing.startTrace(getCurrentHub(), getGlobalObject().location.href); + }); + getGlobalObject().document.onreadystatechange = function () { + if (document.readyState === 'complete') { + Tracing.startTrace(getCurrentHub(), getGlobalObject().location.href); + } + }; + } + }; + /** + * Starts a new trace + * @param hub The hub to start the trace on + * @param transaction Optional transaction + */ + Tracing.startTrace = function (hub, transaction) { + hub.configureScope(function (scope) { + scope.startSpan(); + scope.setTransaction(transaction); + }); + }; + /** + * JSDoc + */ + Tracing.prototype._traceXHR = function (getCurrentHub) { + if (!('XMLHttpRequest' in getGlobalObject())) { + return; + } + var xhrproto = XMLHttpRequest.prototype; + fill(xhrproto, 'open', function (originalOpen) { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + // @ts-ignore + var self = getCurrentHub().getIntegration(Tracing); + if (self) { + self._xhrUrl = args[1]; + } + // tslint:disable-next-line: no-unsafe-any + return originalOpen.apply(this, args); + }; + }); + fill(xhrproto, 'send', function (originalSend) { + return function () { + var _this = this; + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + // @ts-ignore + var self = getCurrentHub().getIntegration(Tracing); + if (self && self._xhrUrl && self._options.tracingOrigins) { + var url_1 = self._xhrUrl; + var headers_1 = getCurrentHub().traceHeaders(); + // tslint:disable-next-line: prefer-for-of + var isWhitelisted = self._options.tracingOrigins.some(function (origin) { + return isMatchingPattern(url_1, origin); + }); + if (isWhitelisted && this.setRequestHeader) { + Object.keys(headers_1).forEach(function (key) { + _this.setRequestHeader(key, headers_1[key]); + }); + } + } + // tslint:disable-next-line: no-unsafe-any + return originalSend.apply(this, args); + }; + }); + }; + /** + * JSDoc + */ + Tracing.prototype._traceFetch = function (getCurrentHub) { + if (!supportsNativeFetch()) { + return; + } + + console.log("PATCHING FETCH"); + + // tslint:disable: only-arrow-functions + fill(getGlobalObject(), 'fetch', function (originalFetch) { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + // @ts-ignore + var self = getCurrentHub().getIntegration(Tracing); + if (self && self._options.tracingOrigins) { + console.log("blafalseq"); + var url_2 = args[0]; + var options = args[1] = args[1] || {}; + var whiteListed_1 = false; + self._options.tracingOrigins.forEach(function (whiteListUrl) { + if (!whiteListed_1) { + whiteListed_1 = isMatchingPattern(url_2, whiteListUrl); + console.log('a', url_2, whiteListUrl); + } + }); + if (whiteListed_1) { + console.log('aaaaaa', options, whiteListed_1); + if (options.headers) { + + if (Array.isArray(options.headers)) { + options.headers = __spread(options.headers, Object.entries(getCurrentHub().traceHeaders())); + } + else { + options.headers = __assign({}, options.headers, getCurrentHub().traceHeaders()); + } + } + else { + options.headers = getCurrentHub().traceHeaders(); + } + + console.log(options.headers); + } + } + + args[1] = options; + // tslint:disable-next-line: no-unsafe-any + return originalFetch.apply(getGlobalObject(), args); + }; + }); + // tslint:enable: only-arrow-functions + }; + /** + * @inheritDoc + */ + Tracing.id = 'Tracing'; + return Tracing; +}()); + +exports.Tracing = Tracing; + + + __window.Sentry = __window.Sentry || {}; + __window.Sentry.Integrations = __window.Sentry.Integrations || {}; + Object.assign(__window.Sentry.Integrations, exports); + + + + + + + + + + + + +}(window)); +//# sourceMappingURL=tracing.js.map diff --git a/examples/tracing/templates/index.html b/examples/tracing/templates/index.html new file mode 100644 index 0000000000..2aa95e789c --- /dev/null +++ b/examples/tracing/templates/index.html @@ -0,0 +1,57 @@ + + + + + + + +

Decode your base64 string as a service (that calls another service)

+ + A base64 string
+ + +

Output:

+
diff --git a/examples/tracing/traceviewer.py b/examples/tracing/traceviewer.py
new file mode 100644
index 0000000000..9c1435ff88
--- /dev/null
+++ b/examples/tracing/traceviewer.py
@@ -0,0 +1,61 @@
+import json
+import sys
+
+print("digraph mytrace {")
+print("rankdir=LR")
+
+all_spans = []
+
+for line in sys.stdin:
+    event = json.loads(line)
+    if event.get("type") != "transaction":
+        continue
+
+    trace_ctx = event["contexts"]["trace"]
+    trace_span = dict(trace_ctx)  # fake a span entry from transaction event
+    trace_span["description"] = event["transaction"]
+    trace_span["start_timestamp"] = event["start_timestamp"]
+    trace_span["timestamp"] = event["timestamp"]
+
+    if "parent_span_id" not in trace_ctx:
+        print(
+            '{} [label="trace:{} ({})"];'.format(
+                int(trace_ctx["trace_id"], 16),
+                event["transaction"],
+                trace_ctx["trace_id"],
+            )
+        )
+
+    for span in event["spans"] + [trace_span]:
+        print(
+            '{} [label="span:{} ({})"];'.format(
+                int(span["span_id"], 16), span["description"], span["span_id"]
+            )
+        )
+        if "parent_span_id" in span:
+            print(
+                "{} -> {};".format(
+                    int(span["parent_span_id"], 16), int(span["span_id"], 16)
+                )
+            )
+
+        print(
+            "{} -> {} [style=dotted];".format(
+                int(span["trace_id"], 16), int(span["span_id"], 16)
+            )
+        )
+
+        all_spans.append(span)
+
+
+for s1 in all_spans:
+    for s2 in all_spans:
+        if s1["start_timestamp"] > s2["timestamp"]:
+            print(
+                '{} -> {} [color="#efefef"];'.format(
+                    int(s1["span_id"], 16), int(s2["span_id"], 16)
+                )
+            )
+
+
+print("}")
diff --git a/examples/tracing/tracing.py b/examples/tracing/tracing.py
new file mode 100644
index 0000000000..c86feaee8c
--- /dev/null
+++ b/examples/tracing/tracing.py
@@ -0,0 +1,73 @@
+import json
+import flask
+import os
+import redis
+import rq
+import sentry_sdk
+import time
+import urllib3
+
+from sentry_sdk.integrations.flask import FlaskIntegration
+from sentry_sdk.integrations.rq import RqIntegration
+
+
+app = flask.Flask(__name__)
+redis_conn = redis.Redis()
+http = urllib3.PoolManager()
+queue = rq.Queue(connection=redis_conn)
+
+
+def write_event(event):
+    with open("events", "a") as f:
+        f.write(json.dumps(event))
+        f.write("\n")
+
+
+sentry_sdk.init(
+    integrations=[FlaskIntegration(), RqIntegration()],
+    traces_sample_rate=1.0,
+    debug=True,
+    transport=write_event,
+)
+
+
+def decode_base64(encoded, redis_key):
+    time.sleep(1)
+    r = http.request("GET", "http://httpbin.org/base64/{}".format(encoded))
+    redis_conn.set(redis_key, r.data)
+
+
+@app.route("/")
+def index():
+    with sentry_sdk.configure_scope() as scope:
+        return flask.render_template(
+            "index.html",
+            sentry_dsn=os.environ["SENTRY_DSN"],
+            traceparent=dict(sentry_sdk.Hub.current.iter_trace_propagation_headers()),
+        )
+
+
+@app.route("/compute/")
+def compute(input):
+    redis_key = "sentry-python-tracing-example-result:{}".format(input)
+    redis_conn.delete(redis_key)
+    queue.enqueue(decode_base64, encoded=input, redis_key=redis_key)
+
+    return redis_key
+
+
+@app.route("/wait/")
+def wait(redis_key):
+    result = redis_conn.get(redis_key)
+    if result is None:
+        return "NONE"
+    else:
+        redis_conn.delete(redis_key)
+        return "RESULT: {}".format(result)
+
+
+@app.cli.command("worker")
+def run_worker():
+    print("WORKING")
+    worker = rq.Worker([queue], connection=queue.connection)
+    worker.work()
diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index 907721282f..b9ba4ac43d 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -437,6 +437,7 @@ def finish_trace(self, span):
         return self.capture_event(
             {
                 "type": "transaction",
+                "transaction": span.transaction,
                 "contexts": {"trace": span.get_trace_context()},
                 "timestamp": span.timestamp,
                 "start_timestamp": span.start_timestamp,
diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py
index 815bc5c448..61c5d3a302 100644
--- a/sentry_sdk/integrations/rq.py
+++ b/sentry_sdk/integrations/rq.py
@@ -4,10 +4,12 @@
 
 from sentry_sdk.hub import Hub
 from sentry_sdk.integrations import Integration
+from sentry_sdk.tracing import Span
 from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
 
 from rq.timeouts import JobTimeoutException  # type: ignore
 from rq.worker import Worker  # type: ignore
+from rq.queue import Queue  # type: ignore
 
 if False:
     from typing import Any
@@ -15,7 +17,6 @@
     from typing import Callable
 
     from rq.job import Job  # type: ignore
-    from rq.queue import Queue  # type: ignore
 
     from sentry_sdk.utils import ExcInfo
 
@@ -43,7 +44,16 @@ def sentry_patched_perform_job(self, job, *args, **kwargs):
             with hub.push_scope() as scope:
                 scope.clear_breadcrumbs()
                 scope.add_event_processor(_make_event_processor(weakref.ref(job)))
-                rv = old_perform_job(self, job, *args, **kwargs)
+
+                with capture_internal_exceptions():
+                    scope.transaction = job.func_name
+
+                with hub.trace(
+                    span=Span.continue_from_headers(
+                        job.meta.get("_sentry_trace_headers") or {}
+                    )
+                ):
+                    rv = old_perform_job(self, job, *args, **kwargs)
 
             if self.is_horse:
                 # We're inside of a forked process and RQ is
@@ -63,6 +73,19 @@ def sentry_patched_handle_exception(self, job, *exc_info, **kwargs):
 
         Worker.handle_exception = sentry_patched_handle_exception
 
+        old_enqueue_job = Queue.enqueue_job
+
+        def sentry_patched_enqueue_job(self, job, **kwargs):
+            hub = Hub.current
+            if hub.get_integration(RqIntegration) is not None:
+                job.meta["_sentry_trace_headers"] = dict(
+                    hub.iter_trace_propagation_headers()
+                )
+
+            return old_enqueue_job(self, job, **kwargs)
+
+        Queue.enqueue_job = sentry_patched_enqueue_job
+
 
 def _make_event_processor(weak_job):
     # type: (Callable[[], Job]) -> Callable
@@ -70,9 +93,6 @@ def event_processor(event, hint):
         # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
         job = weak_job()
         if job is not None:
-            with capture_internal_exceptions():
-                event["transaction"] = job.func_name
-
             with capture_internal_exceptions():
                 extra = event.setdefault("extra", {})
                 extra["rq-job"] = {
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index fa8f0f86dd..45168a36a6 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -125,6 +125,9 @@ def from_traceparent(cls, traceparent):
         if not traceparent:
             return None
 
+        if traceparent.startswith("00-") and traceparent.endswith("-00"):
+            traceparent = traceparent[3:-3]
+
         match = _traceparent_header_format_re.match(traceparent)
         if match is None:
             return None
@@ -166,19 +169,21 @@ def to_json(self):
             "trace_id": self.trace_id,
             "span_id": self.span_id,
             "parent_span_id": self.parent_span_id,
+            "same_process_as_parent": self.same_process_as_parent,
             "transaction": self.transaction,
             "op": self.op,
             "description": self.description,
-            "tags": self._tags,
-            "data": self._data,
             "start_timestamp": self.start_timestamp,
             "timestamp": self.timestamp,
+            "tags": self._tags,
+            "data": self._data,
         }
 
     def get_trace_context(self):
         return {
             "trace_id": self.trace_id,
             "span_id": self.span_id,
+            "parent_span_id": self.parent_span_id,
             "op": self.op,
             "description": self.description,
         }

From f239d12f8dac155dfd363efcadb43f4d48822664 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Tue, 18 Jun 2019 17:58:16 +0200
Subject: [PATCH 32/46] fix: Write child spans into scope

---
 sentry_sdk/hub.py                        |  6 +++++-
 sentry_sdk/scope.py                      |  4 +---
 tests/integrations/celery/test_celery.py | 11 +++++------
 3 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index b9ba4ac43d..cad87e18ce 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -372,6 +372,10 @@ def span(self, span=None, **kwargs):
             yield span
             return
 
+        _, scope = self._stack[-1]
+        old_span = scope.span
+        scope.span = span
+
         try:
             yield span
         except Exception:
@@ -382,6 +386,7 @@ def span(self, span=None, **kwargs):
         finally:
             span.finish()
             self.finish_trace(span)
+            scope.span = old_span
 
     def trace(self, *args, **kwargs):
         return self.span(self.start_trace(*args, **kwargs))
@@ -405,7 +410,6 @@ def start_trace(self, span=None, **kwargs):
                 span.trace_id,
                 scope.span.trace_id,
             )
-        scope.span = span
 
         return span
 
diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py
index f7e77df5a8..28405e9339 100644
--- a/sentry_sdk/scope.py
+++ b/sentry_sdk/scope.py
@@ -106,10 +106,8 @@ def span(self):
     @span.setter
     def span(self, span):
         self._span = span
-        if span.transaction:
+        if span is not None and span.transaction:
             self._transaction = span.transaction
-        elif self._transaction:
-            span.transaction = self._transaction
 
     def set_tag(self, key, value):
         # type: (str, Any) -> None
diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py
index 1d2b8024b2..e9a9d63e9e 100644
--- a/tests/integrations/celery/test_celery.py
+++ b/tests/integrations/celery/test_celery.py
@@ -62,10 +62,9 @@ def dummy_task(x, y):
         foo = 42  # noqa
         return x / y
 
-    span = Hub.current.start_trace()
-
-    invocation(dummy_task, 1, 2)
-    invocation(dummy_task, 1, 0)
+    with Hub.current.trace() as span:
+        invocation(dummy_task, 1, 2)
+        invocation(dummy_task, 1, 0)
 
     event, = events
 
@@ -115,8 +114,8 @@ def test_simple_no_propagation(capture_events, init_celery):
     def dummy_task():
         1 / 0
 
-    span = Hub.current.start_trace()
-    dummy_task.delay()
+    with Hub.current.trace() as span:
+        dummy_task.delay()
 
     event, = events
     assert event["contexts"]["trace"]["trace_id"] != span.trace_id

From cc68a77eb6d810c59b11cc9c9bd59bd96b441afa Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 11:12:46 +0200
Subject: [PATCH 33/46] ref: Move SQL instrumentation out of Django

---
 examples/tracing/tracing.py                |  11 +--
 sentry_sdk/integrations/_sql_common.py     |  81 ++++++++++++++++
 sentry_sdk/integrations/django/__init__.py | 105 +++------------------
 sentry_sdk/tracing.py                      |   2 +-
 4 files changed, 101 insertions(+), 98 deletions(-)
 create mode 100644 sentry_sdk/integrations/_sql_common.py

diff --git a/examples/tracing/tracing.py b/examples/tracing/tracing.py
index c86feaee8c..b5ed98044d 100644
--- a/examples/tracing/tracing.py
+++ b/examples/tracing/tracing.py
@@ -39,12 +39,11 @@ def decode_base64(encoded, redis_key):
 
 @app.route("/")
 def index():
-    with sentry_sdk.configure_scope() as scope:
-        return flask.render_template(
-            "index.html",
-            sentry_dsn=os.environ["SENTRY_DSN"],
-            traceparent=dict(sentry_sdk.Hub.current.iter_trace_propagation_headers()),
-        )
+    return flask.render_template(
+        "index.html",
+        sentry_dsn=os.environ["SENTRY_DSN"],
+        traceparent=dict(sentry_sdk.Hub.current.iter_trace_propagation_headers()),
+    )
 
 
 @app.route("/compute/")
diff --git a/sentry_sdk/integrations/_sql_common.py b/sentry_sdk/integrations/_sql_common.py
new file mode 100644
index 0000000000..e8a5b40b7d
--- /dev/null
+++ b/sentry_sdk/integrations/_sql_common.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from sentry_sdk.utils import format_and_strip, safe_repr
+
+if False:
+    from typing import Any
+    from typing import Dict
+    from typing import List
+    from typing import Tuple
+    from typing import Optional
+
+
+class _FormatConverter(object):
+    def __init__(self, param_mapping):
+        # type: (Dict[str, int]) -> None
+
+        self.param_mapping = param_mapping
+        self.params = []  # type: List[Any]
+
+    def __getitem__(self, val):
+        # type: (str) -> str
+        self.params.append(self.param_mapping.get(val))
+        return "%s"
+
+
+def _format_sql_impl(sql, params):
+    # type: (Any, Any) -> Tuple[str, List[str]]
+    rv = []
+
+    if isinstance(params, dict):
+        # convert sql with named parameters to sql with unnamed parameters
+        conv = _FormatConverter(params)
+        if params:
+            sql = sql % conv
+            params = conv.params
+        else:
+            params = ()
+
+    for param in params or ():
+        if param is None:
+            rv.append("NULL")
+        param = safe_repr(param)
+        rv.append(param)
+
+    return sql, rv
+
+
+def format_sql(sql, params, cursor):
+    # type: (str, List[Any], Any) -> Optional[str]
+
+    real_sql = None
+    real_params = None
+
+    try:
+        # Prefer our own SQL formatting logic because it's the only one that
+        # has proper value trimming.
+        real_sql, real_params = _format_sql_impl(sql, params)
+        if real_sql:
+            real_sql = format_and_strip(real_sql, real_params)
+    except Exception:
+        pass
+
+    if not real_sql and cursor and hasattr(cursor, "mogrify"):
+        # If formatting failed and we're using psycopg2, it could be that we're
+        # looking at a query that uses Composed objects. Use psycopg2's mogrify
+        # function to format the query. We lose per-parameter trimming but gain
+        # accuracy in formatting.
+        #
+        # This is intentionally the second choice because we assume Composed
+        # queries are not widely used, while per-parameter trimming is
+        # generally highly desirable.
+        try:
+            if cursor and hasattr(cursor, "mogrify"):
+                real_sql = cursor.mogrify(sql, params)
+                if isinstance(real_sql, bytes):
+                    real_sql = real_sql.decode(cursor.connection.encoding)
+        except Exception:
+            pass
+
+    return real_sql or None
diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py
index 12bc1963b0..cf77cedcf4 100644
--- a/sentry_sdk/integrations/django/__init__.py
+++ b/sentry_sdk/integrations/django/__init__.py
@@ -3,7 +3,6 @@
 
 import sys
 import weakref
-import contextlib
 
 from django import VERSION as DJANGO_VERSION  # type: ignore
 from django.db.models.query import QuerySet  # type: ignore
@@ -13,11 +12,8 @@
     from typing import Any
     from typing import Callable
     from typing import Dict
-    from typing import List
     from typing import Optional
-    from typing import Tuple
     from typing import Union
-    from typing import Generator
 
     from django.core.handlers.wsgi import WSGIRequest  # type: ignore
     from django.http.response import HttpResponse  # type: ignore
@@ -37,12 +33,10 @@
 from sentry_sdk.hub import _should_send_default_pii
 from sentry_sdk.scope import add_global_event_processor
 from sentry_sdk.serializer import add_global_repr_processor
-from sentry_sdk.tracing import record_sql_query
+from sentry_sdk.tracing import record_sql_queries
 from sentry_sdk.utils import (
     capture_internal_exceptions,
     event_from_exception,
-    safe_repr,
-    format_and_strip,
     transaction_from_function,
     walk_exception_chain,
 )
@@ -50,6 +44,7 @@
 from sentry_sdk.integrations.logging import ignore_logger
 from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
 from sentry_sdk.integrations._wsgi_common import RequestExtractor
+from sentry_sdk.integrations._sql_common import format_sql
 from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
 from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
 
@@ -297,88 +292,6 @@ def _set_user_info(request, event):
         pass
 
 
-class _FormatConverter(object):
-    def __init__(self, param_mapping):
-        # type: (Dict[str, int]) -> None
-
-        self.param_mapping = param_mapping
-        self.params = []  # type: List[Any]
-
-    def __getitem__(self, val):
-        # type: (str) -> str
-        self.params.append(self.param_mapping.get(val))
-        return "%s"
-
-
-def format_sql(sql, params):
-    # type: (Any, Any) -> Tuple[str, List[str]]
-    rv = []
-
-    if isinstance(params, dict):
-        # convert sql with named parameters to sql with unnamed parameters
-        conv = _FormatConverter(params)
-        if params:
-            sql = sql % conv
-            params = conv.params
-        else:
-            params = ()
-
-    for param in params or ():
-        if param is None:
-            rv.append("NULL")
-        param = safe_repr(param)
-        rv.append(param)
-
-    return sql, rv
-
-
-@contextlib.contextmanager
-def record_sql(sql, param_list, cursor=None):
-    # type: (Any, Any, Any) -> Generator
-    hub = Hub.current
-    if hub.get_integration(DjangoIntegration) is None:
-        yield None
-        return
-
-    formatted_queries = []
-
-    for params in param_list:
-        real_sql = None
-        real_params = None
-
-        try:
-            # Prefer our own SQL formatting logic because it's the only one that
-            # has proper value trimming.
-            real_sql, real_params = format_sql(sql, params)
-            if real_sql:
-                real_sql = format_and_strip(real_sql, real_params)
-        except Exception:
-            pass
-
-        if not real_sql and cursor and hasattr(cursor, "mogrify"):
-            # If formatting failed and we're using psycopg2, it could be that we're
-            # looking at a query that uses Composed objects. Use psycopg2's mogrify
-            # function to format the query. We lose per-parameter trimming but gain
-            # accuracy in formatting.
-            #
-            # This is intentionally the second choice because we assume Composed
-            # queries are not widely used, while per-parameter trimming is
-            # generally highly desirable.
-            try:
-                if cursor and hasattr(cursor, "mogrify"):
-                    real_sql = cursor.mogrify(sql, params)
-                    if isinstance(real_sql, bytes):
-                        real_sql = real_sql.decode(cursor.connection.encoding)
-            except Exception:
-                pass
-
-        if real_sql:
-            formatted_queries.append(real_sql)
-
-    with record_sql_query(hub, formatted_queries):
-        yield
-
-
 def install_sql_hook():
     # type: () -> None
     """If installed this causes Django's queries to be captured."""
@@ -395,11 +308,21 @@ def install_sql_hook():
         return
 
     def execute(self, sql, params=None):
-        with record_sql(sql, [params], self.cursor):
+        hub = Hub.current
+        if hub.get_integration(DjangoIntegration) is None:
+            return
+
+        with record_sql_queries(hub, [format_sql(sql, params, self.cursor)]):
             return real_execute(self, sql, params)
 
     def executemany(self, sql, param_list):
-        with record_sql(sql, param_list, self.cursor):
+        hub = Hub.current
+        if hub.get_integration(DjangoIntegration) is None:
+            return
+
+        with record_sql_queries(
+            hub, [format_sql(sql, params, self.cursor) for params in param_list]
+        ):
             return real_executemany(self, sql, param_list)
 
     CursorWrapper.execute = execute
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 45168a36a6..1b7adc948d 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -190,7 +190,7 @@ def get_trace_context(self):
 
 
 @contextlib.contextmanager
-def record_sql_query(hub, queries):
+def record_sql_queries(hub, queries):
     if not queries:
         yield None
     else:

From 82d70583647db829a6f4eb82d3ee55c9b1d6df38 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 11:31:20 +0200
Subject: [PATCH 34/46] ref: Prepend label to SQL span description

---
 sentry_sdk/integrations/django/__init__.py | 8 ++++++--
 sentry_sdk/tracing.py                      | 6 ++++--
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py
index cf77cedcf4..79abcf4039 100644
--- a/sentry_sdk/integrations/django/__init__.py
+++ b/sentry_sdk/integrations/django/__init__.py
@@ -312,7 +312,9 @@ def execute(self, sql, params=None):
         if hub.get_integration(DjangoIntegration) is None:
             return
 
-        with record_sql_queries(hub, [format_sql(sql, params, self.cursor)]):
+        with record_sql_queries(
+            hub, [format_sql(sql, params, self.cursor)], label="Django: "
+        ):
             return real_execute(self, sql, params)
 
     def executemany(self, sql, param_list):
@@ -321,7 +323,9 @@ def executemany(self, sql, param_list):
             return
 
         with record_sql_queries(
-            hub, [format_sql(sql, params, self.cursor) for params in param_list]
+            hub,
+            [format_sql(sql, params, self.cursor) for params in param_list],
+            label="Django: ",
         ):
             return real_executemany(self, sql, param_list)
 
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 1b7adc948d..0793c04793 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -190,14 +190,16 @@ def get_trace_context(self):
 
 
 @contextlib.contextmanager
-def record_sql_queries(hub, queries):
+def record_sql_queries(hub, queries, label=""):
     if not queries:
         yield None
     else:
+        strings = [label]
         for query in queries:
             hub.add_breadcrumb(message=query, category="query")
+            strings.append(query)
 
-        description = concat_strings(queries)
+        description = concat_strings(strings)
         with hub.span(op="db.statement", description=description) as span:
             yield span
 

From 05d8a41435aa0661c765a8d39a00b0801b3f4cdf Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 12:55:53 +0200
Subject: [PATCH 35/46] fix: Fix bug in requests instrumentation

---
 sentry_sdk/integrations/stdlib.py | 19 +++++++++++++++----
 sentry_sdk/tracing.py             | 21 +++++++++++----------
 2 files changed, 26 insertions(+), 14 deletions(-)

diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py
index 1836d0737e..0bda98fd34 100644
--- a/sentry_sdk/integrations/stdlib.py
+++ b/sentry_sdk/integrations/stdlib.py
@@ -62,18 +62,29 @@ def putrequest(self, method, url, *args, **kwargs):
 
     def getresponse(self, *args, **kwargs):
         recorder = getattr(self, "_sentrysdk_recorder", None)
+
+        if recorder is None:
+            return real_getresponse(self, *args, **kwargs)
+
         data_dict = getattr(self, "_sentrysdk_data_dict", None)
 
         try:
             rv = real_getresponse(self, *args, **kwargs)
 
-            if recorder is not None and data_dict is not None:
+            if data_dict is not None:
                 data_dict["httplib_response"] = rv
                 data_dict["status_code"] = rv.status
                 data_dict["reason"] = rv.reason
-        finally:
-            if recorder is not None:
-                recorder.__exit__(*sys.exc_info())
+        except TypeError:
+            # python-requests provokes a typeerror to discover py3 vs py2 differences
+            #
+            # > TypeError("getresponse() got an unexpected keyword argument 'buffering'")
+            raise
+        except Exception:
+            recorder.__exit__(*sys.exc_info())
+            pass
+        else:
+            recorder.__exit__(None, None, None)
 
         return rv
 
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 0793c04793..d928316596 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -1,5 +1,4 @@
 import re
-import sys
 import uuid
 import contextlib
 
@@ -211,19 +210,21 @@ def record_http_request(hub, url, method):
     with hub.span(op="http", description="%s %s" % (url, method)) as span:
         try:
             yield data_dict
-        finally:
+        except Exception:
+            httplib_response = data_dict.pop("httplib_response", None)
+            raise
+        else:
             httplib_response = data_dict.pop("httplib_response", None)
 
+            hub.add_breadcrumb(
+                type="http",
+                category="httplib",
+                data=data_dict,
+                hint={"httplib_response": httplib_response},
+            )
+        finally:
             if span is not None:
                 if "status_code" in data_dict:
                     span.set_tag("http.status_code", data_dict["status_code"])
                 for k, v in data_dict.items():
                     span.set_data(k, v)
-
-            if sys.exc_info()[1] is None:
-                hub.add_breadcrumb(
-                    type="http",
-                    category="httplib",
-                    data=data_dict,
-                    hint={"httplib_response": httplib_response},
-                )

From 7a8b35dea4154a75b0e71fe856d72311762b0776 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 14:35:01 +0200
Subject: [PATCH 36/46] feat: WIP of redis integration

---
 sentry_sdk/hub.py                            |  5 ++-
 sentry_sdk/integrations/redis.py             | 39 ++++++++++++++++++++
 sentry_sdk/integrations/stdlib.py            |  2 +-
 sentry_sdk/tracing.py                        | 26 ++++++-------
 tests/integrations/requests/test_requests.py |  1 +
 tests/integrations/stdlib/test_httplib.py    |  3 ++
 6 files changed, 60 insertions(+), 16 deletions(-)
 create mode 100644 sentry_sdk/integrations/redis.py

diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index cad87e18ce..5f4f5dfd5f 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -10,7 +10,7 @@
 from sentry_sdk._compat import with_metaclass
 from sentry_sdk.scope import Scope
 from sentry_sdk.client import Client
-from sentry_sdk.tracing import Span
+from sentry_sdk.tracing import Span, maybe_create_breadcrumbs_from_span
 from sentry_sdk.utils import (
     exc_info_from_error,
     event_from_exception,
@@ -385,6 +385,7 @@ def span(self, span=None, **kwargs):
             span.set_tag("error", False)
         finally:
             span.finish()
+            maybe_create_breadcrumbs_from_span(self, span)
             self.finish_trace(span)
             scope.span = old_span
 
@@ -396,7 +397,7 @@ def start_span(self, **kwargs):
         span = scope.span
         if span is not None:
             return span.new_span(**kwargs)
-        return None
+        return Span.start_trace(**kwargs)
 
     def start_trace(self, span=None, **kwargs):
         if span is None:
diff --git a/sentry_sdk/integrations/redis.py b/sentry_sdk/integrations/redis.py
new file mode 100644
index 0000000000..a661faaa73
--- /dev/null
+++ b/sentry_sdk/integrations/redis.py
@@ -0,0 +1,39 @@
+from sentry_sdk import Hub
+from sentry_sdk.utils import capture_internal_exceptions
+from sentry_sdk.integrations import Integration
+
+
+class RedisIntegration(Integration):
+    identifier = "redis"
+
+    @staticmethod
+    def setup_once():
+        import redis
+
+        old_execute_command = redis.StrictRedis.execute_command
+
+        def sentry_patched_execute_command(self, name, *args, **kwargs):
+            hub = Hub.current
+
+            if hub.get_integration(RedisIntegration) is None:
+                return old_execute_command(self, name, *args, **kwargs)
+
+            description = name
+
+            with capture_internal_exceptions():
+                description_parts = [name]
+                for i, arg in enumerate(args):
+                    if i > 10:
+                        break
+
+                    description_parts.append(repr(arg))
+
+                description = " ".join(description_parts)
+
+            with hub.span(op="redis", description=description) as span:
+                if name and args and name.lower() in ("get", "set", "setex", "setnx"):
+                    span.set_tag("redis.key", args[0])
+
+                return old_execute_command(self, name, *args, **kwargs)
+
+        redis.StrictRedis.execute_command = sentry_patched_execute_command
diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py
index 0bda98fd34..290e8f169d 100644
--- a/sentry_sdk/integrations/stdlib.py
+++ b/sentry_sdk/integrations/stdlib.py
@@ -82,7 +82,7 @@ def getresponse(self, *args, **kwargs):
             raise
         except Exception:
             recorder.__exit__(*sys.exc_info())
-            pass
+            raise
         else:
             recorder.__exit__(None, None, None)
 
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index d928316596..776333e4a5 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -199,7 +199,7 @@ def record_sql_queries(hub, queries, label=""):
             strings.append(query)
 
         description = concat_strings(strings)
-        with hub.span(op="db.statement", description=description) as span:
+        with hub.span(op="db", description=description) as span:
             yield span
 
 
@@ -210,21 +210,21 @@ def record_http_request(hub, url, method):
     with hub.span(op="http", description="%s %s" % (url, method)) as span:
         try:
             yield data_dict
-        except Exception:
-            httplib_response = data_dict.pop("httplib_response", None)
-            raise
-        else:
-            httplib_response = data_dict.pop("httplib_response", None)
-
-            hub.add_breadcrumb(
-                type="http",
-                category="httplib",
-                data=data_dict,
-                hint={"httplib_response": httplib_response},
-            )
         finally:
             if span is not None:
                 if "status_code" in data_dict:
                     span.set_tag("http.status_code", data_dict["status_code"])
                 for k, v in data_dict.items():
                     span.set_data(k, v)
+
+
+def maybe_create_breadcrumbs_from_span(hub, span):
+    if span.op == "redis":
+        hub.add_breadcrumb(type="redis", category="redis", data=span._tags)
+    elif span.op == "http" and not span._tags.get("error"):
+        hub.add_breadcrumb(
+            type="http",
+            category="httplib",
+            data=span._data,
+            hint={"httplib_response": span._data.get("httplib_response")},
+        )
diff --git a/tests/integrations/requests/test_requests.py b/tests/integrations/requests/test_requests.py
index deaa8e3421..da2dfd7b06 100644
--- a/tests/integrations/requests/test_requests.py
+++ b/tests/integrations/requests/test_requests.py
@@ -23,4 +23,5 @@ def test_crumb_capture(sentry_init, capture_events):
         "method": "GET",
         "status_code": 418,
         "reason": "I'M A TEAPOT",
+        "httplib_response": crumb["data"]["httplib_response"],
     }
diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py
index 02c204d6e9..1ffd56dbde 100644
--- a/tests/integrations/stdlib/test_httplib.py
+++ b/tests/integrations/stdlib/test_httplib.py
@@ -32,6 +32,7 @@ def test_crumb_capture(sentry_init, capture_events):
         "method": "GET",
         "status_code": 200,
         "reason": "OK",
+        "httplib_response": crumb["data"]["httplib_response"],
     }
 
 
@@ -61,6 +62,7 @@ def before_breadcrumb(crumb, hint):
         "status_code": 200,
         "reason": "OK",
         "extra": "foo",
+        "httplib_response": crumb["data"]["httplib_response"],
     }
 
 
@@ -102,4 +104,5 @@ def test_httplib_misuse(sentry_init, capture_events):
         "method": "GET",
         "status_code": 200,
         "reason": "OK",
+        "httplib_response": crumb["data"]["httplib_response"],
     }

From ccec6cafe0e1908b6269d402e07d61625a2b2234 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 14:37:11 +0200
Subject: [PATCH 37/46] fix: Fix broken sampling logic

---
 sentry_sdk/client.py | 2 +-
 sentry_sdk/hub.py    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py
index e224a1a183..c7bc2790aa 100644
--- a/sentry_sdk/client.py
+++ b/sentry_sdk/client.py
@@ -196,7 +196,7 @@ def _should_capture(
 
         if (
             self.options["sample_rate"] < 1.0
-            and random.random() >= self.options["sample_rate"]
+            and random.random() <= self.options["sample_rate"]
         ):
             return False
 
diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index 5f4f5dfd5f..fd140019d5 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -436,7 +436,7 @@ def finish_trace(self, span):
         if span.sampled is None:
             # span.sampled = True -> Span forcibly sampled
             sample_rate = self.client.options["traces_sample_rate"]
-            if sample_rate < 1.0 and random.random() >= sample_rate:
+            if sample_rate < 1.0 and random.random() <= sample_rate:
                 return None
 
         return self.capture_event(

From 44508c53e31961e4d39f43ae0c1386d921e0ff74 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 14:47:10 +0200
Subject: [PATCH 38/46] test: Add tests for redis integration

---
 sentry_sdk/integrations/redis.py       |  2 ++
 tests/integrations/redis/test_redis.py | 24 ++++++++++++++++++++++++
 tox.ini                                |  5 +++++
 3 files changed, 31 insertions(+)
 create mode 100644 tests/integrations/redis/test_redis.py

diff --git a/sentry_sdk/integrations/redis.py b/sentry_sdk/integrations/redis.py
index a661faaa73..3bc3fa4b67 100644
--- a/sentry_sdk/integrations/redis.py
+++ b/sentry_sdk/integrations/redis.py
@@ -1,3 +1,5 @@
+from __future__ import absolute_import
+
 from sentry_sdk import Hub
 from sentry_sdk.utils import capture_internal_exceptions
 from sentry_sdk.integrations import Integration
diff --git a/tests/integrations/redis/test_redis.py b/tests/integrations/redis/test_redis.py
new file mode 100644
index 0000000000..12f25d925d
--- /dev/null
+++ b/tests/integrations/redis/test_redis.py
@@ -0,0 +1,24 @@
+from sentry_sdk import capture_message
+from sentry_sdk.integrations.redis import RedisIntegration
+
+from fakeredis import FakeStrictRedis
+
+
+def test_basic(sentry_init, capture_events):
+    sentry_init(integrations=[RedisIntegration()])
+    events = capture_events()
+
+    connection = FakeStrictRedis()
+
+    connection.get("foobar")
+    capture_message("hi")
+
+    event, = events
+    crumb, = event["breadcrumbs"]
+
+    assert crumb == {
+        "category": "redis",
+        "data": {"error": False, "redis.key": "foobar"},
+        "timestamp": crumb["timestamp"],
+        "type": "redis",
+    }
diff --git a/tox.ini b/tox.ini
index af53b4914d..5fed367c22 100644
--- a/tox.ini
+++ b/tox.ini
@@ -46,6 +46,8 @@ envlist =
 
     {py2.7,py3.7}-requests
 
+    {py2.7,py3.7}-redis
+
 [testenv]
 deps =
     -r test-requirements.txt
@@ -123,6 +125,8 @@ deps =
     tornado-5: tornado>=5,<6
     tornado-6: tornado>=6.0a1
 
+    redis: fakeredis
+
     linters: black
     linters: flake8
 
@@ -144,6 +148,7 @@ setenv =
     rq: TESTPATH=tests/integrations/rq
     aiohttp: TESTPATH=tests/integrations/aiohttp
     tornado: TESTPATH=tests/integrations/tornado
+    redis: TESTPATH=tests/integrations/redis
 
     COVERAGE_FILE=.coverage-{envname}
 passenv =

From 375c140d9c52f56ab916c18a8cfe61c3bf7e16ee Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 17:18:18 +0200
Subject: [PATCH 39/46] feat: Add subprocess integration

---
 sentry_sdk/integrations/stdlib.py            | 53 ++++++++++++++++++--
 sentry_sdk/tracing.py                        | 16 ++++--
 tests/conftest.py                            | 23 +++++----
 tests/integrations/redis/__init__.py         |  3 ++
 tests/integrations/stdlib/test_subprocess.py | 30 +++++++++++
 5 files changed, 108 insertions(+), 17 deletions(-)
 create mode 100644 tests/integrations/redis/__init__.py
 create mode 100644 tests/integrations/stdlib/test_subprocess.py

diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py
index 290e8f169d..ba8922d26e 100644
--- a/sentry_sdk/integrations/stdlib.py
+++ b/sentry_sdk/integrations/stdlib.py
@@ -1,8 +1,10 @@
+import os
+import subprocess
 import sys
 
 from sentry_sdk.hub import Hub
 from sentry_sdk.integrations import Integration
-from sentry_sdk.tracing import record_http_request
+from sentry_sdk.tracing import EnvironHeaders, record_http_request
 
 
 try:
@@ -17,10 +19,11 @@ class StdlibIntegration(Integration):
     @staticmethod
     def setup_once():
         # type: () -> None
-        install_httplib()
+        _install_httplib()
+        _install_subprocess()
 
 
-def install_httplib():
+def _install_httplib():
     # type: () -> None
     real_putrequest = HTTPConnection.putrequest
     real_getresponse = HTTPConnection.getresponse
@@ -90,3 +93,47 @@ def getresponse(self, *args, **kwargs):
 
     HTTPConnection.putrequest = putrequest
     HTTPConnection.getresponse = getresponse
+
+
+def _get_argument(args, kwargs, name, position, setdefault=None):
+    if name in kwargs:
+        rv = kwargs[name]
+        if rv is None and setdefault is not None:
+            rv = kwargs[name] = setdefault
+    elif position < len(args):
+        rv = args[position]
+        if rv is None and setdefault is not None:
+            rv = args[position] = setdefault
+    else:
+        rv = kwargs[name] = setdefault
+
+    return rv
+
+
+def _install_subprocess():
+    old_popen_init = subprocess.Popen.__init__
+
+    def sentry_patched_popen_init(self, *a, **kw):
+        hub = Hub.current
+        if hub.get_integration(StdlibIntegration) is None:
+            return old_popen_init(self, *a, **kw)
+
+        # do not setdefault! args is required by Popen, doing setdefault would
+        # make invalid calls valid
+        args = _get_argument(a, kw, "args", 0) or []
+        cwd = _get_argument(a, kw, "cwd", 10)
+
+        for k, v in hub.iter_trace_propagation_headers():
+            env = _get_argument(a, kw, "env", 11, {})
+            env["SUBPROCESS_" + k.upper().replace("-", "_")] = v
+
+        with hub.span(op="subprocess", description=" ".join(map(str, args))) as span:
+            span.set_tag("subprocess.cwd", cwd)
+
+            return old_popen_init(self, *a, **kw)
+
+    subprocess.Popen.__init__ = sentry_patched_popen_init
+
+
+def get_subprocess_traceparent_headers():
+    return EnvironHeaders(os.environ, prefix="SUBPROCESS_")
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 776333e4a5..21ac415f01 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -22,12 +22,13 @@
 )
 
 
-class _EnvironHeaders(object):
-    def __init__(self, environ):
+class EnvironHeaders(object):
+    def __init__(self, environ, prefix="HTTP_"):
         self.environ = environ
+        self.prefix = prefix
 
     def get(self, key):
-        return self.environ.get("HTTP_" + key.replace("-", "_").upper())
+        return self.environ.get(self.prefix + key.replace("-", "_").upper())
 
 
 class Span(object):
@@ -107,7 +108,7 @@ def new_span(self, **kwargs):
 
     @classmethod
     def continue_from_environ(cls, environ):
-        return cls.continue_from_headers(_EnvironHeaders(environ))
+        return cls.continue_from_headers(EnvironHeaders(environ))
 
     @classmethod
     def continue_from_headers(cls, headers):
@@ -228,3 +229,10 @@ def maybe_create_breadcrumbs_from_span(hub, span):
             data=span._data,
             hint={"httplib_response": span._data.get("httplib_response")},
         )
+    elif span.op == "subprocess":
+        hub.add_breadcrumb(
+            type="subprocess",
+            category="subprocess",
+            data=span._data,
+            hint={"popen_instance": span._data.get("popen_instance")},
+        )
diff --git a/tests/conftest.py b/tests/conftest.py
index 2f4ea5ebab..9c0c613daf 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -91,16 +91,19 @@ def assert_semaphore_acceptance(tmpdir):
     def inner(event):
         if not SEMAPHORE:
             return
-        # not dealing with the subprocess API right now
-        file = tmpdir.join("event")
-        file.write(json.dumps(dict(event)))
-        output = json.loads(
-            subprocess.check_output(
-                [SEMAPHORE, "process-event"], stdin=file.open()
-            ).decode("utf-8")
-        )
-        _no_errors_in_semaphore_response(output)
-        output.pop("_meta", None)
+
+        # Disable subprocess integration
+        with sentry_sdk.Hub(None):
+            # not dealing with the subprocess API right now
+            file = tmpdir.join("event")
+            file.write(json.dumps(dict(event)))
+            output = json.loads(
+                subprocess.check_output(
+                    [SEMAPHORE, "process-event"], stdin=file.open()
+                ).decode("utf-8")
+            )
+            _no_errors_in_semaphore_response(output)
+            output.pop("_meta", None)
 
     return inner
 
diff --git a/tests/integrations/redis/__init__.py b/tests/integrations/redis/__init__.py
new file mode 100644
index 0000000000..4752ef19b1
--- /dev/null
+++ b/tests/integrations/redis/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytest.importorskip("redis")
diff --git a/tests/integrations/stdlib/test_subprocess.py b/tests/integrations/stdlib/test_subprocess.py
new file mode 100644
index 0000000000..6480a28b7d
--- /dev/null
+++ b/tests/integrations/stdlib/test_subprocess.py
@@ -0,0 +1,30 @@
+import subprocess
+
+from sentry_sdk import Hub, capture_message
+from sentry_sdk.integrations.stdlib import StdlibIntegration
+
+
+def test_subprocess_basic(sentry_init, capture_events):
+    sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0)
+
+    with Hub.current.trace(transaction="foo", op="foo") as span:
+        output = subprocess.check_output(
+            "python -c '"
+            "import sentry_sdk; "
+            "from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; "
+            "sentry_sdk.init(); "
+            "print(dict(get_subprocess_traceparent_headers()))"
+            "'",
+            shell=True,
+        )
+
+    assert span.trace_id in output
+
+    events = capture_events()
+
+    capture_message("hi")
+
+    event, = events
+
+    crumb, = event["breadcrumbs"]
+    assert crumb == {}

From 3cd6b5261d48f988139b0ca55487b306e86a475e Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 17:30:36 +0200
Subject: [PATCH 40/46] Revert "fix: Fix broken sampling logic"

This reverts commit ccec6cafe0e1908b6269d402e07d61625a2b2234.
---
 sentry_sdk/client.py | 2 +-
 sentry_sdk/hub.py    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py
index c7bc2790aa..e224a1a183 100644
--- a/sentry_sdk/client.py
+++ b/sentry_sdk/client.py
@@ -196,7 +196,7 @@ def _should_capture(
 
         if (
             self.options["sample_rate"] < 1.0
-            and random.random() <= self.options["sample_rate"]
+            and random.random() >= self.options["sample_rate"]
         ):
             return False
 
diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index fd140019d5..5f4f5dfd5f 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -436,7 +436,7 @@ def finish_trace(self, span):
         if span.sampled is None:
             # span.sampled = True -> Span forcibly sampled
             sample_rate = self.client.options["traces_sample_rate"]
-            if sample_rate < 1.0 and random.random() <= sample_rate:
+            if sample_rate < 1.0 and random.random() >= sample_rate:
                 return None
 
         return self.capture_event(

From 73c116b0dbe6f6b1cab6a15b85d95e9aa8f0e1cd Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Wed, 19 Jun 2019 17:40:02 +0200
Subject: [PATCH 41/46] fix: Fix tests

---
 sentry_sdk/tracing.py                        | 21 +++++++++++++++++---
 tests/integrations/stdlib/test_subprocess.py |  9 +++++++--
 2 files changed, 25 insertions(+), 5 deletions(-)

diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 21ac415f01..d2c67cf9bc 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -1,6 +1,7 @@
 import re
 import uuid
 import contextlib
+import collections
 
 from datetime import datetime
 
@@ -22,13 +23,27 @@
 )
 
 
-class EnvironHeaders(object):
+class EnvironHeaders(collections.Mapping):
     def __init__(self, environ, prefix="HTTP_"):
         self.environ = environ
         self.prefix = prefix
 
-    def get(self, key):
-        return self.environ.get(self.prefix + key.replace("-", "_").upper())
+    def __getitem__(self, key):
+        return self.environ[self.prefix + key.replace("-", "_").upper()]
+
+    def __len__(self):
+        return sum(1 for _ in iter(self))
+
+    def __iter__(self):
+        for k in self.environ:
+            if not isinstance(k, str):
+                continue
+
+            k = k.replace("-", "_").upper()
+            if not k.startswith(self.prefix):
+                continue
+
+            yield k[len(self.prefix) :]
 
 
 class Span(object):
diff --git a/tests/integrations/stdlib/test_subprocess.py b/tests/integrations/stdlib/test_subprocess.py
index 6480a28b7d..22fc2e1192 100644
--- a/tests/integrations/stdlib/test_subprocess.py
+++ b/tests/integrations/stdlib/test_subprocess.py
@@ -18,7 +18,7 @@ def test_subprocess_basic(sentry_init, capture_events):
             shell=True,
         )
 
-    assert span.trace_id in output
+    assert span.trace_id in str(output)
 
     events = capture_events()
 
@@ -27,4 +27,9 @@ def test_subprocess_basic(sentry_init, capture_events):
     event, = events
 
     crumb, = event["breadcrumbs"]
-    assert crumb == {}
+    assert crumb == {
+        "category": "subprocess",
+        "data": {},
+        "timestamp": crumb["timestamp"],
+        "type": "subprocess",
+    }

From 1aa33831214b369cde690b3085155cdc808e3ab5 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Fri, 21 Jun 2019 13:39:10 +0200
Subject: [PATCH 42/46] build: Remove support for sanic 19

See https://github.com/huge-success/sanic/issues/1532#issuecomment-504394544
---
 tox.ini | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/tox.ini b/tox.ini
index af53b4914d..47243e9081 100644
--- a/tox.ini
+++ b/tox.ini
@@ -27,7 +27,7 @@ envlist =
     {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-falcon-1.4
     {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-falcon-2.0
 
-    {py3.5,py3.6,py3.7}-sanic-{0.8,18,19}
+    {py3.5,py3.6,py3.7}-sanic-{0.8,18}
 
     {pypy,py2.7,py3.5,py3.6,py3.7,py3.8}-celery-{4.1,4.2,4.3}
     {pypy,py2.7}-celery-3
@@ -79,7 +79,6 @@ deps =
 
     sanic-0.8: sanic>=0.8,<0.9
     sanic-18: sanic>=18.0,<19.0
-    sanic-19: sanic>=19.0,<20.0
     {py3.5,py3.6}-sanic: aiocontextvars==0.2.1
     sanic: aiohttp
 

From 76735519d0c44285252a21922442efb22cb82bd4 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Thu, 4 Jul 2019 13:03:09 +0200
Subject: [PATCH 43/46] fix: Linters

---
 sentry_sdk/hub.py                 | 37 ++++++++++++++++++++++---------
 sentry_sdk/integrations/redis.py  |  4 +++-
 sentry_sdk/integrations/stdlib.py |  2 +-
 sentry_sdk/tracing.py             | 10 +++++++--
 4 files changed, 39 insertions(+), 14 deletions(-)

diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index 52225a0a68..f4bbe5f8b8 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -426,14 +426,15 @@ def add_breadcrumb(
             scope._breadcrumbs.popleft()
 
     @contextmanager
-    def span(self, span=None, **kwargs):
+    def span(
+        self,
+        span=None,  # type: Optional[Span]
+        **kwargs  # type: Any
+    ):
+        # type: (...) -> Generator[Span, None, None]
         if span is None:
             span = self.start_span(**kwargs)
 
-        if span is None:
-            yield span
-            return
-
         _, scope = self._stack[-1]
         old_span = scope.span
         scope.span = span
@@ -451,17 +452,30 @@ def span(self, span=None, **kwargs):
             self.finish_trace(span)
             scope.span = old_span
 
-    def trace(self, *args, **kwargs):
-        return self.span(self.start_trace(*args, **kwargs))
+    def trace(
+        self,
+        span=None,  # type: Optional[Span]
+        **kwargs  # type: Any
+    ):
+        # type: (...) -> ContextManager[Span]
+        return self.span(self.start_trace(span=span, **kwargs))
 
-    def start_span(self, **kwargs):
+    def start_span(
+        self, **kwargs  # type: Any
+    ):
+        # type: (...) -> Span
         _, scope = self._stack[-1]
         span = scope.span
         if span is not None:
             return span.new_span(**kwargs)
         return Span.start_trace(**kwargs)
 
-    def start_trace(self, span=None, **kwargs):
+    def start_trace(
+        self,
+        span=None,  # type: Optional[Span]
+        **kwargs  # type: Any
+    ):
+        # type: (...) -> Span
         if span is None:
             span = Span.start_trace(**kwargs)
 
@@ -476,7 +490,10 @@ def start_trace(self, span=None, **kwargs):
 
         return span
 
-    def finish_trace(self, span):
+    def finish_trace(
+        self, span  # type: Span
+    ):
+        # type: (...) -> Optional[str]
         if span.timestamp is None:
             # This transaction is not yet finished so we just finish it.
             span.finish()
diff --git a/sentry_sdk/integrations/redis.py b/sentry_sdk/integrations/redis.py
index 3bc3fa4b67..5e10d3bd91 100644
--- a/sentry_sdk/integrations/redis.py
+++ b/sentry_sdk/integrations/redis.py
@@ -38,4 +38,6 @@ def sentry_patched_execute_command(self, name, *args, **kwargs):
 
                 return old_execute_command(self, name, *args, **kwargs)
 
-        redis.StrictRedis.execute_command = sentry_patched_execute_command
+        redis.StrictRedis.execute_command = (  # type: ignore
+            sentry_patched_execute_command
+        )
diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py
index ba8922d26e..1825297628 100644
--- a/sentry_sdk/integrations/stdlib.py
+++ b/sentry_sdk/integrations/stdlib.py
@@ -132,7 +132,7 @@ def sentry_patched_popen_init(self, *a, **kw):
 
             return old_popen_init(self, *a, **kw)
 
-    subprocess.Popen.__init__ = sentry_patched_popen_init
+    subprocess.Popen.__init__ = sentry_patched_popen_init  # type: ignore
 
 
 def get_subprocess_traceparent_headers():
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index d2c67cf9bc..740ec1836a 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -12,6 +12,7 @@
     from typing import Optional
     from typing import Any
     from typing import Dict
+    from typing import Mapping
     from typing import List
 
 _traceparent_header_format_re = re.compile(
@@ -23,8 +24,13 @@
 )
 
 
-class EnvironHeaders(collections.Mapping):
-    def __init__(self, environ, prefix="HTTP_"):
+class EnvironHeaders(collections.Mapping):  # type: ignore
+    def __init__(
+        self,
+        environ,  # type: Mapping[str, str]
+        prefix="HTTP_",  # type: str
+    ):
+        # type: (...) -> None
         self.environ = environ
         self.prefix = prefix
 

From 60ec0ccfeb1c1235b6f5304f65e5171fd0b59a46 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Thu, 4 Jul 2019 14:23:12 +0200
Subject: [PATCH 44/46] fix: Use sys.executable

---
 tests/integrations/stdlib/test_subprocess.py | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/tests/integrations/stdlib/test_subprocess.py b/tests/integrations/stdlib/test_subprocess.py
index 22fc2e1192..d336e50dee 100644
--- a/tests/integrations/stdlib/test_subprocess.py
+++ b/tests/integrations/stdlib/test_subprocess.py
@@ -1,4 +1,5 @@
 import subprocess
+import sys
 
 from sentry_sdk import Hub, capture_message
 from sentry_sdk.integrations.stdlib import StdlibIntegration
@@ -9,13 +10,14 @@ def test_subprocess_basic(sentry_init, capture_events):
 
     with Hub.current.trace(transaction="foo", op="foo") as span:
         output = subprocess.check_output(
-            "python -c '"
-            "import sentry_sdk; "
-            "from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; "
-            "sentry_sdk.init(); "
-            "print(dict(get_subprocess_traceparent_headers()))"
-            "'",
-            shell=True,
+            [
+                sys.executable,
+                "-c",
+                "import sentry_sdk; "
+                "from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; "
+                "sentry_sdk.init(); "
+                "print(dict(get_subprocess_traceparent_headers()))",
+            ]
         )
 
     assert span.trace_id in str(output)

From d4618231b68be7955118da3baf223e8999aa0d92 Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Thu, 4 Jul 2019 18:03:37 +0200
Subject: [PATCH 45/46] fix: Fix sampling

---
 sentry_sdk/hub.py                            | 58 +++++++-------------
 sentry_sdk/integrations/celery.py            |  9 +--
 sentry_sdk/integrations/rq.py                | 12 ++--
 sentry_sdk/integrations/wsgi.py              |  5 +-
 sentry_sdk/tracing.py                        | 21 +++----
 tests/integrations/celery/test_celery.py     |  4 +-
 tests/integrations/stdlib/test_subprocess.py |  2 +-
 tests/test_tracing.py                        | 27 +++++++--
 8 files changed, 66 insertions(+), 72 deletions(-)

diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index f4bbe5f8b8..fd33559e2c 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -432,8 +432,7 @@ def span(
         **kwargs  # type: Any
     ):
         # type: (...) -> Generator[Span, None, None]
-        if span is None:
-            span = self.start_span(**kwargs)
+        span = self.start_span(span=span, **kwargs)
 
         _, scope = self._stack[-1]
         old_span = scope.span
@@ -449,48 +448,31 @@ def span(
         finally:
             span.finish()
             maybe_create_breadcrumbs_from_span(self, span)
-            self.finish_trace(span)
+            self.finish_span(span)
             scope.span = old_span
 
-    def trace(
-        self,
-        span=None,  # type: Optional[Span]
-        **kwargs  # type: Any
-    ):
-        # type: (...) -> ContextManager[Span]
-        return self.span(self.start_trace(span=span, **kwargs))
-
     def start_span(
-        self, **kwargs  # type: Any
-    ):
-        # type: (...) -> Span
-        _, scope = self._stack[-1]
-        span = scope.span
-        if span is not None:
-            return span.new_span(**kwargs)
-        return Span.start_trace(**kwargs)
-
-    def start_trace(
         self,
         span=None,  # type: Optional[Span]
         **kwargs  # type: Any
     ):
         # type: (...) -> Span
+
         if span is None:
-            span = Span.start_trace(**kwargs)
+            _, scope = self._stack[-1]
 
-        _, scope = self._stack[-1]
+            if scope.span is not None:
+                span = scope.span.new_span(**kwargs)
+            else:
+                span = Span(**kwargs)
 
-        if scope.span is not None:
-            logger.warning(
-                "Creating new trace %s within existing trace %s",
-                span.trace_id,
-                scope.span.trace_id,
-            )
+        if span.sampled is None and span.transaction is not None:
+            sample_rate = self.client.options["traces_sample_rate"]
+            span.sampled = random.random() < sample_rate
 
         return span
 
-    def finish_trace(
+    def finish_span(
         self, span  # type: Span
     ):
         # type: (...) -> Optional[str]
@@ -508,16 +490,16 @@ def finish_trace(
             # event.
             return None
 
-        if span.sampled is False:
-            # Span is forcibly un-sampled
+        if not span.sampled:
+            # At this point a `sampled = None` should have already been
+            # resolved to a concrete decision. If `sampled` is `None`, it's
+            # likely that somebody used `with Hub.span(..)` on a
+            # non-transaction span and later decided to make it a transaction.
+            assert (
+                span.sampled is not None
+            ), "Need to set transaction when entering span!"
             return None
 
-        if span.sampled is None:
-            # span.sampled = True -> Span forcibly sampled
-            sample_rate = self.client.options["traces_sample_rate"]
-            if sample_rate < 1.0 and random.random() >= sample_rate:
-                return None
-
         return self.capture_event(
             {
                 "type": "transaction",
diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py
index 1603ed58de..255e60e13c 100644
--- a/sentry_sdk/integrations/celery.py
+++ b/sentry_sdk/integrations/celery.py
@@ -93,14 +93,15 @@ def _inner(*args, **kwargs):
             scope.clear_breadcrumbs()
             scope.add_event_processor(_make_event_processor(task, *args, **kwargs))
 
+            span = Span.continue_from_headers(args[3].get("headers") or {})
+            span.transaction = "unknown celery task"
+
             with capture_internal_exceptions():
                 # Celery task objects are not a thing to be trusted. Even
                 # something such as attribute access can fail.
-                scope.transaction = task.name
+                span.transaction = task.name
 
-            with hub.trace(
-                span=Span.continue_from_headers(args[3].get("headers") or {})
-            ):
+            with hub.span(span):
                 return f(*args, **kwargs)
 
     return _inner
diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py
index cefb41194c..d098a76be2 100644
--- a/sentry_sdk/integrations/rq.py
+++ b/sentry_sdk/integrations/rq.py
@@ -46,14 +46,14 @@ def sentry_patched_perform_job(self, job, *args, **kwargs):
                 scope.clear_breadcrumbs()
                 scope.add_event_processor(_make_event_processor(weakref.ref(job)))
 
+                span = Span.continue_from_headers(
+                    job.meta.get("_sentry_trace_headers") or {}
+                )
+
                 with capture_internal_exceptions():
-                    scope.transaction = job.func_name
+                    span.transaction = job.func_name
 
-                with hub.trace(
-                    span=Span.continue_from_headers(
-                        job.meta.get("_sentry_trace_headers") or {}
-                    )
-                ):
+                with hub.span(span):
                     rv = old_perform_job(self, job, *args, **kwargs)
 
             if self.is_horse:
diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py
index 7eb16c83de..441e53987a 100644
--- a/sentry_sdk/integrations/wsgi.py
+++ b/sentry_sdk/integrations/wsgi.py
@@ -85,7 +85,10 @@ def __call__(self, environ, start_response):
                     scope._name = "wsgi"
                     scope.add_event_processor(_make_wsgi_event_processor(environ))
 
-            with hub.trace(Span.continue_from_environ(environ)):
+            span = Span.continue_from_environ(environ)
+            span.transaction = environ.get("PATH_INFO") or "unknown http request"
+
+            with hub.span(span):
                 try:
                     rv = self.app(environ, start_response)
                 except BaseException:
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 740ec1836a..03ed83930c 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -71,8 +71,8 @@ class Span(object):
 
     def __init__(
         self,
-        trace_id,
-        span_id,
+        trace_id=None,
+        span_id=None,
         parent_span_id=None,
         same_process_as_parent=True,
         sampled=None,
@@ -80,8 +80,8 @@ def __init__(
         op=None,
         description=None,
     ):
-        self.trace_id = trace_id
-        self.span_id = span_id
+        self.trace_id = trace_id or uuid.uuid4().hex
+        self.span_id = span_id or uuid.uuid4().hex[16:]
         self.parent_span_id = parent_span_id
         self.same_process_as_parent = same_process_as_parent
         self.sampled = sampled
@@ -109,17 +109,10 @@ def __repr__(self):
             )
         )
 
-    @classmethod
-    def start_trace(cls, **kwargs):
-        return cls(trace_id=uuid.uuid4().hex, span_id=uuid.uuid4().hex[16:], **kwargs)
-
     def new_span(self, **kwargs):
-        if self.trace_id is None:
-            return Span.start_trace()
-
-        rv = Span(
+        rv = type(self)(
             trace_id=self.trace_id,
-            span_id=uuid.uuid4().hex[16:],
+            span_id=None,
             parent_span_id=self.span_id,
             sampled=self.sampled,
             **kwargs
@@ -135,7 +128,7 @@ def continue_from_environ(cls, environ):
     def continue_from_headers(cls, headers):
         parent = cls.from_traceparent(headers.get("sentry-trace"))
         if parent is None:
-            return cls.start_trace()
+            return cls()
         return parent.new_span(same_process_as_parent=False)
 
     def iter_headers(self):
diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py
index e9a9d63e9e..c9a9bae3f1 100644
--- a/tests/integrations/celery/test_celery.py
+++ b/tests/integrations/celery/test_celery.py
@@ -62,7 +62,7 @@ def dummy_task(x, y):
         foo = 42  # noqa
         return x / y
 
-    with Hub.current.trace() as span:
+    with Hub.current.span() as span:
         invocation(dummy_task, 1, 2)
         invocation(dummy_task, 1, 0)
 
@@ -114,7 +114,7 @@ def test_simple_no_propagation(capture_events, init_celery):
     def dummy_task():
         1 / 0
 
-    with Hub.current.trace() as span:
+    with Hub.current.span() as span:
         dummy_task.delay()
 
     event, = events
diff --git a/tests/integrations/stdlib/test_subprocess.py b/tests/integrations/stdlib/test_subprocess.py
index d336e50dee..5245b387e9 100644
--- a/tests/integrations/stdlib/test_subprocess.py
+++ b/tests/integrations/stdlib/test_subprocess.py
@@ -8,7 +8,7 @@
 def test_subprocess_basic(sentry_init, capture_events):
     sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0)
 
-    with Hub.current.trace(transaction="foo", op="foo") as span:
+    with Hub.current.span(transaction="foo", op="foo") as span:
         output = subprocess.check_output(
             [
                 sys.executable,
diff --git a/tests/test_tracing.py b/tests/test_tracing.py
index eb9cd30a9b..984a777159 100644
--- a/tests/test_tracing.py
+++ b/tests/test_tracing.py
@@ -9,7 +9,7 @@ def test_basic(sentry_init, capture_events, sample_rate):
     sentry_init(traces_sample_rate=sample_rate)
     events = capture_events()
 
-    with Hub.current.trace(transaction="hi"):
+    with Hub.current.span(transaction="hi"):
         with pytest.raises(ZeroDivisionError):
             with Hub.current.span(op="foo", description="foodesc"):
                 1 / 0
@@ -38,9 +38,9 @@ def test_continue_from_headers(sentry_init, capture_events, sampled):
     sentry_init(traces_sample_rate=1.0)
     events = capture_events()
 
-    with Hub.current.trace(transaction="hi") as old_trace:
-        old_trace.sampled = sampled
+    with Hub.current.span(transaction="hi") as old_trace:
         with Hub.current.span() as old_span:
+            old_span.sampled = sampled
             headers = dict(Hub.current.iter_trace_propagation_headers())
 
     header = headers["sentry-trace"]
@@ -52,18 +52,20 @@ def test_continue_from_headers(sentry_init, capture_events, sampled):
         assert header.endswith("-")
 
     span = Span.continue_from_headers(headers)
+    span.transaction = "WRONG"
     assert span is not None
     assert span.sampled == sampled
     assert span.trace_id == old_span.trace_id
 
-    with Hub.current.trace(span):
+    with Hub.current.span(span):
         with Hub.current.configure_scope() as scope:
             scope.transaction = "ho"
-
         capture_message("hello")
 
     if sampled is False:
-        message, = events
+        trace1, message = events
+
+        assert trace1["transaction"] == "hi"
     else:
         trace1, message, trace2 = events
 
@@ -78,3 +80,16 @@ def test_continue_from_headers(sentry_init, capture_events, sampled):
         )
 
     assert message["message"] == "hello"
+
+
+def test_sampling_decided_only_for_transactions(sentry_init, capture_events):
+    sentry_init(traces_sample_rate=0.5)
+
+    with Hub.current.span(transaction="hi") as trace:
+        assert trace.sampled is not None
+
+        with Hub.current.span() as span:
+            assert span.sampled == trace.sampled
+
+    with Hub.current.span() as span:
+        assert span.sampled is None

From 7bc185e0388fcbb5d197eed2427c944681d4701a Mon Sep 17 00:00:00 2001
From: Markus Unterwaditzer 
Date: Fri, 5 Jul 2019 12:54:29 +0200
Subject: [PATCH 46/46] fix: Emit old traceparent, fix linting issues

---
 sentry_sdk/consts.py              |  1 +
 sentry_sdk/hub.py                 | 14 +++++++++-----
 sentry_sdk/integrations/stdlib.py |  4 +---
 sentry_sdk/tracing.py             |  3 +++
 tests/test_tracing.py             |  4 ++--
 5 files changed, 16 insertions(+), 10 deletions(-)

diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
index 74463ad414..ae38d5f527 100644
--- a/sentry_sdk/consts.py
+++ b/sentry_sdk/consts.py
@@ -50,6 +50,7 @@ def __init__(
         ca_certs=None,  # type: Optional[str]
         propagate_traces=True,  # type: bool
         traces_sample_rate=0.0,  # type: float
+        traceparent_v2=False,  # type: bool
     ):
         # type: (...) -> None
         pass
diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index fd33559e2c..fde53c2e02 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -458,16 +458,16 @@ def start_span(
     ):
         # type: (...) -> Span
 
-        if span is None:
-            _, scope = self._stack[-1]
+        client, scope = self._stack[-1]
 
+        if span is None:
             if scope.span is not None:
                 span = scope.span.new_span(**kwargs)
             else:
                 span = Span(**kwargs)
 
         if span.sampled is None and span.transaction is not None:
-            sample_rate = self.client.options["traces_sample_rate"]
+            sample_rate = client and client.options["traces_sample_rate"] or 0
             span.sampled = random.random() < sample_rate
 
         return span
@@ -616,8 +616,12 @@ def iter_trace_propagation_headers(self):
         if not propagate_traces:
             return
 
-        for item in scope._span.iter_headers():
-            yield item
+        if client and client.options["traceparent_v2"]:
+            traceparent = scope._span.to_traceparent()
+        else:
+            traceparent = scope._span.to_legacy_traceparent()
+
+        yield "sentry-trace", traceparent
 
 
 GLOBAL_HUB = Hub()
diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py
index f89a33b1c8..66ab1265ce 100644
--- a/sentry_sdk/integrations/stdlib.py
+++ b/sentry_sdk/integrations/stdlib.py
@@ -1,15 +1,13 @@
 import os
 import subprocess
 import sys
+import platform
 
 from sentry_sdk.hub import Hub
 from sentry_sdk.integrations import Integration
 from sentry_sdk.scope import add_global_event_processor
 from sentry_sdk.tracing import EnvironHeaders, record_http_request
 
-import sys
-import platform
-
 try:
     from httplib import HTTPConnection  # type: ignore
 except ImportError:
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 03ed83930c..7aade7cd14 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -168,6 +168,9 @@ def to_traceparent(self):
             sampled = "0"
         return "%s-%s-%s" % (self.trace_id, self.span_id, sampled)
 
+    def to_legacy_traceparent(self):
+        return "00-%s-%s-00" % (self.trace_id, self.span_id)
+
     def set_tag(self, key, value):
         self._tags[key] = value
 
diff --git a/tests/test_tracing.py b/tests/test_tracing.py
index 984a777159..9ce22e20f3 100644
--- a/tests/test_tracing.py
+++ b/tests/test_tracing.py
@@ -35,10 +35,10 @@ def test_basic(sentry_init, capture_events, sample_rate):
 
 @pytest.mark.parametrize("sampled", [True, False, None])
 def test_continue_from_headers(sentry_init, capture_events, sampled):
-    sentry_init(traces_sample_rate=1.0)
+    sentry_init(traces_sample_rate=1.0, traceparent_v2=True)
     events = capture_events()
 
-    with Hub.current.span(transaction="hi") as old_trace:
+    with Hub.current.span(transaction="hi"):
         with Hub.current.span() as old_span:
             old_span.sampled = sampled
             headers = dict(Hub.current.iter_trace_propagation_headers())