def test_passes_parent_sampling_decision_in_sampling_context( sentry_init, parent_sampling_decision ): sentry_init(traces_sample_rate=1.0) sentry_trace_header = ( "12312012123120121231201212312012-1121201211212012-{sampled}".format( sampled=int(parent_sampling_decision) ) ) transaction = Transaction.continue_from_headers( headers={"sentry-trace": sentry_trace_header}, name="dogpark" ) spy = mock.Mock(wraps=transaction) start_transaction(transaction=spy) # there's only one call (so index at 0) and kwargs are always last in a call # tuple (so index at -1) sampling_context = spy._set_initial_sampling_decision.mock_calls[0][-1][ "sampling_context" ] assert "parent_sampled" in sampling_context # because we passed in a spy, attribute access requires unwrapping assert sampling_context["parent_sampled"]._mock_wraps is parent_sampling_decision
def test_transaction_naming(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0) events = capture_events() # only transactions have names - spans don't with pytest.raises(TypeError): start_span(name="foo") assert len(events) == 0 # default name in event if no name is passed with start_transaction() as transaction: pass assert len(events) == 1 assert events[0]["transaction"] == "<unlabeled transaction>" # the name can be set once the transaction's already started with start_transaction() as transaction: transaction.name = "name-known-after-transaction-started" assert len(events) == 2 assert events[1]["transaction"] == "name-known-after-transaction-started" # passing in a name works, too with start_transaction(name="a"): pass assert len(events) == 3 assert events[2]["transaction"] == "a"
def test_nested_transaction_sampling_override(): with start_transaction(name="outer", sampled=True) as outer_transaction: assert outer_transaction.sampled is True with start_transaction(name="inner", sampled=False) as inner_transaction: assert inner_transaction.sampled is False assert outer_transaction.sampled is True
def test_continue_from_headers(sentry_init, capture_events, sampled, sample_rate): """ Ensure data is actually passed along via headers, and that they are read correctly. """ sentry_init(traces_sample_rate=sample_rate) events = capture_events() # make a parent transaction (normally this would be in a different service) with start_transaction( name="hi", sampled=True if sample_rate == 0 else None) as parent_transaction: with start_span() as old_span: old_span.sampled = sampled headers = dict( Hub.current.iter_trace_propagation_headers(old_span)) tracestate = parent_transaction._sentry_tracestate # child transaction, to prove that we can read 'sentry-trace' and # `tracestate` header data correctly child_transaction = Transaction.continue_from_headers(headers, name="WRONG") assert child_transaction is not None assert child_transaction.parent_sampled == sampled assert child_transaction.trace_id == old_span.trace_id assert child_transaction.same_process_as_parent is False assert child_transaction.parent_span_id == old_span.span_id assert child_transaction.span_id != old_span.span_id assert child_transaction._sentry_tracestate == tracestate # add child transaction to the scope, to show that the captured message will # be tagged with the trace id (since it happens while the transaction is # open) with start_transaction(child_transaction): with configure_scope() as scope: # change the transaction name from "WRONG" to make sure the change # is reflected in the final data scope.transaction = "ho" capture_message("hello") # in this case the child transaction won't be captured if sampled is False or (sample_rate == 0 and sampled is None): trace1, message = events assert trace1["transaction"] == "hi" else: trace1, message, trace2 = events assert trace1["transaction"] == "hi" assert trace2["transaction"] == "ho" assert (trace1["contexts"]["trace"]["trace_id"] == trace2["contexts"]["trace"]["trace_id"] == child_transaction.trace_id == message["contexts"]["trace"]["trace_id"]) assert message["message"] == "hello"
def test_nested_transaction_sampling_override(sentry_init, sampled): sentry_init(traces_sample_rate=1.0) with start_transaction(name="outer", sampled=sampled) as outer_transaction: assert outer_transaction.sampled is sampled with start_transaction(name="inner", sampled=(not sampled)) as inner_transaction: assert inner_transaction.sampled is not sampled assert outer_transaction.sampled is sampled
def test_continue_from_headers(sentry_init, capture_events, sampled, sample_rate): sentry_init(traces_sample_rate=sample_rate) events = capture_events() # make a parent transaction (normally this would be in a different service) with start_transaction(name="hi", sampled=True if sample_rate == 0 else None): with start_span() as old_span: old_span.sampled = sampled headers = dict(Hub.current.iter_trace_propagation_headers(old_span)) # test that the sampling decision is getting encoded in the header correctly 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("-") # child transaction, to prove that we can read 'sentry-trace' header data # correctly transaction = Transaction.continue_from_headers(headers, name="WRONG") assert transaction is not None assert transaction.parent_sampled == sampled assert transaction.trace_id == old_span.trace_id assert transaction.same_process_as_parent is False assert transaction.parent_span_id == old_span.span_id assert transaction.span_id != old_span.span_id # add child transaction to the scope, to show that the captured message will # be tagged with the trace id (since it happens while the transaction is # open) with start_transaction(transaction): with configure_scope() as scope: scope.transaction = "ho" capture_message("hello") if sampled is False or (sample_rate == 0 and sampled is None): trace1, message = events assert trace1["transaction"] == "hi" else: trace1, message, trace2 = events assert trace1["transaction"] == "hi" assert trace2["transaction"] == "ho" assert ( trace1["contexts"]["trace"]["trace_id"] == trace2["contexts"]["trace"]["trace_id"] == transaction.trace_id == message["contexts"]["trace"]["trace_id"] ) assert message["message"] == "hello"
def test_passes_custom_samling_context_from_start_transaction_to_traces_sampler( sentry_init, DictionaryContaining # noqa: N803 ): traces_sampler = mock.Mock() sentry_init(traces_sampler=traces_sampler) start_transaction(custom_sampling_context={"dogs": "yes", "cats": "maybe"}) traces_sampler.assert_any_call( DictionaryContaining({"dogs": "yes", "cats": "maybe"}) )
def test_start_transaction(sentry_init): sentry_init(traces_sample_rate=1.0) # you can have it start a transaction for you result1 = start_transaction(name="/interactions/other-dogs/new-dog", op="greeting.sniff") assert isinstance(result1, Transaction) assert result1.name == "/interactions/other-dogs/new-dog" assert result1.op == "greeting.sniff" # or you can pass it an already-created transaction preexisting_transaction = Transaction( name="/interactions/other-dogs/new-dog", op="greeting.sniff") result2 = start_transaction(preexisting_transaction) assert result2 is preexisting_transaction
def test_continue_from_headers(sentry_init, capture_events, sampled): sentry_init(traces_sample_rate=1.0, traceparent_v2=True) events = capture_events() with start_transaction(name="hi"): with start_span() as old_span: old_span.sampled = sampled 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("-") transaction = Transaction.continue_from_headers(headers, name="WRONG") assert transaction is not None assert transaction.sampled == sampled assert transaction.trace_id == old_span.trace_id assert transaction.same_process_as_parent is False assert transaction.parent_span_id == old_span.span_id assert transaction.span_id != old_span.span_id with start_transaction(transaction): with configure_scope() as scope: scope.transaction = "ho" capture_message("hello") if sampled is False: trace1, message = events assert trace1["transaction"] == "hi" else: trace1, message, trace2 = events assert trace1["transaction"] == "hi" assert trace2["transaction"] == "ho" assert ( trace1["contexts"]["trace"]["trace_id"] == trace2["contexts"]["trace"]["trace_id"] == transaction.trace_id == message["contexts"]["trace"]["trace_id"] ) assert message["message"] == "hello"
def get_with_pagination(self, path: str, *args: Any, **kwargs: Any) -> Sequence[JSONData]: """ Github uses the Link header to provide pagination links. Github recommends using the provided link relations and not constructing our own URL. https://docs.github.com/en/rest/guides/traversing-with-pagination """ try: with sentry_sdk.configure_scope() as scope: parent_span_id = scope.span.span_id trace_id = scope.span.trace_id except AttributeError: parent_span_id = None trace_id = None with sentry_sdk.start_transaction( op=f"{self.integration_type}.http.pagination", name= f"{self.integration_type}.http_response.pagination.{self.name}", parent_span_id=parent_span_id, trace_id=trace_id, sampled=True, ): output = [] resp = self.get(path, params={"per_page": self.page_size}) output.extend(resp) page_number = 1 while get_next_link(resp) and page_number < self.page_number_limit: resp = self.get(get_next_link(resp)) output.extend(resp) page_number += 1 return output
def test_crumb_capture_and_hint(sentry_init, capture_events): def before_breadcrumb(crumb, hint): crumb["data"]["extra"] = "foo" return crumb sentry_init(integrations=[HttpxIntegration()], before_breadcrumb=before_breadcrumb) clients = (httpx.Client(), httpx.AsyncClient()) for i, c in enumerate(clients): with start_transaction(): events = capture_events() url = "https://httpbin.org/status/200" if not asyncio.iscoroutinefunction(c.get): response = c.get(url) else: response = asyncio.get_event_loop().run_until_complete( c.get(url)) assert response.status_code == 200 capture_message("Testing!") (event, ) = events # send request twice so we need get breadcrumb by index crumb = event["breadcrumbs"]["values"][i] assert crumb["type"] == "http" assert crumb["category"] == "httplib" assert crumb["data"] == { "url": url, "method": "GET", "status_code": 200, "reason": "OK", "extra": "foo", }
def test_outgoing_trace_headers( sentry_init, monkeypatch, StringContaining # noqa: N803 ): # HTTPSConnection.send is passed a string containing (among other things) # the headers on the request. Mock it so we can check the headers, and also # so it doesn't try to actually talk to the internet. mock_send = mock.Mock() monkeypatch.setattr(HTTPSConnection, "send", mock_send) sentry_init(traces_sample_rate=1.0) with start_transaction( name="/interactions/other-dogs/new-dog", op="greeting.sniff", trace_id="12312012123120121231201212312012", ) as transaction: HTTPSConnection("www.squirrelchasers.com").request( "GET", "/top-chasers") request_span = transaction._span_recorder.spans[-1] expected_sentry_trace = ( "sentry-trace: {trace_id}-{parent_span_id}-{sampled}".format( trace_id=transaction.trace_id, parent_span_id=request_span.span_id, sampled=1, )) mock_send.assert_called_with(StringContaining(expected_sentry_trace))
def subscription_checker(**kwargs): """ Checks for subscriptions stuck in a transition status and attempts to repair them """ count = 0 with sentry_sdk.start_transaction( op="subscription_checker", name="subscription_checker", sampled=False, ): for subscription in QuerySubscription.objects.filter( status__in=( QuerySubscription.Status.CREATING.value, QuerySubscription.Status.UPDATING.value, QuerySubscription.Status.DELETING.value, ), date_updated__lt=timezone.now() - SUBSCRIPTION_STATUS_MAX_AGE, ): with sentry_sdk.start_span(op="repair_subscription") as span: span.set_data("subscription_id", subscription.id) span.set_data("status", subscription.status) count += 1 if subscription.status == QuerySubscription.Status.CREATING.value: create_subscription_in_snuba.delay(query_subscription_id=subscription.id) elif subscription.status == QuerySubscription.Status.UPDATING.value: update_subscription_in_snuba.delay(query_subscription_id=subscription.id) elif subscription.status == QuerySubscription.Status.DELETING.value: delete_subscription_from_snuba.delay(query_subscription_id=subscription.id) metrics.incr("snuba.subscriptions.repair", amount=count)
def do_GET(self): # pylint: disable=invalid-name """Respond to CC w/ the current image""" with sentry_sdk.start_transaction(op="http", name="GET"): self.send_response(200) self.send_header("Content-type", "image/png") self.end_headers() parent.image.save(self.wfile, "PNG", optimize=True)
def test_basic(sentry_init, capture_events, sample_rate): sentry_init(traces_sample_rate=sample_rate) events = capture_events() with start_transaction(name="hi") as transaction: transaction.set_status("ok") with pytest.raises(ZeroDivisionError): with start_span(op="foo", description="foodesc"): 1 / 0 with start_span(op="bar", description="bardesc"): pass if sample_rate: assert len(events) == 1 event = events[0] span1, span2 = event["spans"] parent_span = event assert span1["tags"]["status"] == "internal_error" assert span1["op"] == "foo" assert span1["description"] == "foodesc" assert "status" not in span2.get("tags", {}) assert span2["op"] == "bar" assert span2["description"] == "bardesc" assert parent_span["transaction"] == "hi" assert "status" not in event["tags"] assert event["contexts"]["trace"]["status"] == "ok" else: assert not events
def test_tolerates_traces_sampler_returning_a_boolean( sentry_init, traces_sampler_return_value): sentry_init(traces_sampler=mock.Mock( return_value=traces_sampler_return_value)) transaction = start_transaction(name="dogpark") assert transaction.sampled is traces_sampler_return_value
def test_simple(capture_events, celery, celery_invocation): events = capture_events() @celery.task(name="dummy_task") def dummy_task(x, y): foo = 42 # noqa return x / y with start_transaction() as transaction: celery_invocation(dummy_task, 1, 2) _, expected_context = celery_invocation(dummy_task, 1, 0) (event,) = events assert event["contexts"]["trace"]["trace_id"] == transaction.trace_id assert event["contexts"]["trace"]["span_id"] != transaction.span_id assert event["transaction"] == "dummy_task" assert "celery_task_id" in event["tags"] assert event["extra"]["celery-job"] == dict( task_name="dummy_task", **expected_context ) (exception,) = event["exception"]["values"] assert exception["type"] == "ZeroDivisionError" assert exception["mechanism"]["type"] == "celery" assert exception["stacktrace"]["frames"][0]["vars"]["foo"] == "42"
def request(self, *args, **kwargs): metrics.incr( "%s.http_request" % self.datadog_prefix, sample_rate=1.0, tags={self.integration_type: self.name}, ) try: with sentry_sdk.configure_scope() as scope: parent_span_id = scope.span.span_id trace_id = scope.span.trace_id except AttributeError: parent_span_id = None trace_id = None with sentry_sdk.start_transaction( op=f"{self.integration_type}.http", name=f"{self.integration_type}.http_response.{self.name}", parent_span_id=parent_span_id, trace_id=trace_id, sampled=True, ) as span: resp = ApiClient.request(self, *args, **kwargs) self.track_response_data(resp.status_code, span, None, resp) return resp
def process_event(cache_key, start_time=None, event_id=None, data_has_changed=None, **kwargs): """ Handles event processing (for those events that need it) This excludes symbolication via symbolicator service (see symbolicate_event). :param string cache_key: the cache key for the event data :param int start_time: the timestamp when the event was ingested :param string event_id: the event identifier :param boolean data_has_changed: set to True if the event data was changed in previous tasks """ with sentry_sdk.start_transaction( op="tasks.store.process_event", name="TaskProcessEvent", sampled=sample_process_event_apm(), ): return _do_process_event( cache_key=cache_key, start_time=start_time, event_id=event_id, process_task=process_event, data_has_changed=data_has_changed, )
def test_transaction_events(capture_events, init_celery, celery_invocation, task_fails): celery = init_celery(traces_sample_rate=1.0) @celery.task(name="dummy_task") def dummy_task(x, y): return x / y # XXX: For some reason the first call does not get instrumented properly. celery_invocation(dummy_task, 1, 1) events = capture_events() with start_transaction(name="submission") as transaction: celery_invocation(dummy_task, 1, 0 if task_fails else 1) if task_fails: error_event = events.pop(0) assert error_event["contexts"]["trace"][ "trace_id"] == transaction.trace_id assert error_event["exception"]["values"][0][ "type"] == "ZeroDivisionError" execution_event, submission_event = events assert execution_event["transaction"] == "dummy_task" assert submission_event["transaction"] == "submission" assert execution_event["type"] == submission_event["type"] == "transaction" assert execution_event["contexts"]["trace"][ "trace_id"] == transaction.trace_id assert submission_event["contexts"]["trace"][ "trace_id"] == transaction.trace_id if task_fails: assert execution_event["contexts"]["trace"][ "status"] == "internal_error" else: assert execution_event["contexts"]["trace"]["status"] == "ok" assert execution_event["spans"] == [] assert submission_event["spans"] == [{ u"description": u"dummy_task", u"op": "celery.submit", u"parent_span_id": submission_event["contexts"]["trace"]["span_id"], u"same_process_as_parent": True, u"span_id": submission_event["spans"][0]["span_id"], u"start_timestamp": submission_event["spans"][0]["start_timestamp"], u"timestamp": submission_event["spans"][0]["timestamp"], u"trace_id": text_type(transaction.trace_id), }]
def test_transactions(sentry_init, capture_events, render_span_tree): sentry_init(integrations=[SqlalchemyIntegration()], _experiments={"record_sql_params": True}) events = capture_events() Base = declarative_base() # noqa: N806 class Person(Base): __tablename__ = "person" id = Column(Integer, primary_key=True) name = Column(String(250), nullable=False) class Address(Base): __tablename__ = "address" id = Column(Integer, primary_key=True) street_name = Column(String(250)) street_number = Column(String(250)) post_code = Column(String(250), nullable=False) person_id = Column(Integer, ForeignKey("person.id")) person = relationship(Person) engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) # noqa: N806 session = Session() with start_transaction(name="test_transaction", sampled=True): with session.begin_nested(): session.query(Person).first() for _ in range(2): with pytest.raises(IntegrityError): with session.begin_nested(): session.add(Person(id=1, name="bob")) session.add(Person(id=1, name="bob")) with session.begin_nested(): session.query(Person).first() (event, ) = events assert (render_span_tree(event) == """\ - op=null: description=null - op="db": description="SAVEPOINT sa_savepoint_1" - op="db": description="SELECT person.id AS person_id, person.name AS person_name \\nFROM person\\n LIMIT ? OFFSET ?" - op="db": description="RELEASE SAVEPOINT sa_savepoint_1" - op="db": description="SAVEPOINT sa_savepoint_2" - op="db": description="INSERT INTO person (id, name) VALUES (?, ?)" - op="db": description="ROLLBACK TO SAVEPOINT sa_savepoint_2" - op="db": description="SAVEPOINT sa_savepoint_3" - op="db": description="INSERT INTO person (id, name) VALUES (?, ?)" - op="db": description="ROLLBACK TO SAVEPOINT sa_savepoint_3" - op="db": description="SAVEPOINT sa_savepoint_4" - op="db": description="SELECT person.id AS person_id, person.name AS person_name \\nFROM person\\n LIMIT ? OFFSET ?" - op="db": description="RELEASE SAVEPOINT sa_savepoint_4"\ """)
def get_full_info(self, request, pk=None, *args, **kwargs): with start_transaction(op="task", name="articles_get_full_info"): queryset = Article.objects.all() article = get_object_or_404(queryset, pk=pk) author = get_object_or_404(Author, id=article.author_id) article_data = ArticleSerializer(article).data author_data = AuthorSerializer(author).data article_data['author'] = author_data return Response(article_data, status=status.HTTP_200_OK)
def test_only_captures_transaction_when_sampled_is_true( sentry_init, sampling_decision, capture_events): sentry_init(traces_sampler=mock.Mock(return_value=sampling_decision)) events = capture_events() transaction = start_transaction(name="dogpark") transaction.finish() assert len(events) == (1 if sampling_decision else 0)
def test_warns_and_sets_sampled_to_false_on_invalid_traces_sampler_return_value( sentry_init, traces_sampler_return_value, StringContaining # noqa: N803 ): sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) with mock.patch.object(logger, "warning", mock.Mock()): transaction = start_transaction(name="dogpark") logger.warning.assert_any_call(StringContaining("Given sample rate is invalid")) assert transaction.sampled is False
def test_traces_sampler_doesnt_overwrite_explicitly_passed_sampling_decision( sentry_init, explicit_decision): # make traces_sampler pick the opposite of the explicit decision, to prove # that the explicit decision takes precedence traces_sampler = mock.Mock(return_value=not explicit_decision) sentry_init(traces_sampler=traces_sampler) transaction = start_transaction(name="dogpark", sampled=explicit_decision) assert transaction.sampled is explicit_decision
def __call__(self, function_name, **kws): with push_scope() as scope: transaction_name = "{0} || {1}".format( mlconfig.MLCHAIN_SERVER_NAME, function_name) scope.transaction = transaction_name with start_transaction(op="task", name=transaction_name): uid = self.init_context() request_context = {'api_version': self.server.version} try: headers, form, files, data = self.parse_data() mlchain_context['REQUESTS_HEADERS'] = headers mlchain_context['REQUESTS_FORM'] = form mlchain_context['REQUESTS_FILES'] = files mlchain_context['REQUESTS_DATA'] = data except Exception as ex: request_context['time_process'] = 0 output = self.normalize_output(self.base_format, function_name, {}, None, ex, request_context) return self.make_response(output) formatter = self.get_format(headers, form, files, data) start_time = time.time() try: if self.authentication is not None: self.authentication.check(headers) args, kwargs = formatter.parse_request( function_name, headers, form, files, data, request_context) func = self.server.model.get_function(function_name) kwargs = self.server.get_kwargs(func, *args, **kwargs) kwargs = self.server._normalize_kwargs_to_valid_format( kwargs, func) uid = self.init_context_with_headers(headers, uid) scope.set_tag("transaction_id", uid) logger.debug("Mlchain transaction id: {0}".format(uid)) output = self.server.model.call_function( function_name, uid, **kwargs) exception = None except MlChainError as ex: exception = ex output = None except Exception as ex: exception = ex output = None time_process = time.time() - start_time request_context['time_process'] = time_process output = self.normalize_output(formatter, function_name, headers, output, exception, request_context) return self.make_response(output)
def _transaction_wrapper(*fargs, **fkwargs): if not sentry_available: if pass_transaction: fkwargs["transaction"] = Mock() return function(*fargs, **fkwargs) with start_transaction(**kwargs) as transaction: if pass_transaction: fkwargs["transaction"] = transaction return function(*fargs, **fkwargs)
def test_ignores_inherited_sample_decision_when_traces_sampler_defined( sentry_init, parent_sampling_decision): # make traces_sampler pick the opposite of the inherited decision, to prove # that traces_sampler takes precedence traces_sampler = mock.Mock(return_value=not parent_sampling_decision) sentry_init(traces_sampler=traces_sampler) transaction = start_transaction(name="dogpark", parent_sampled=parent_sampling_decision) assert transaction.sampled is not parent_sampling_decision
def test_get_transaction_and_span_from_scope_regardless_of_sampling_decision( sentry_init, sampling_decision): sentry_init(traces_sample_rate=1.0) with start_transaction(name="/", sampled=sampling_decision): with start_span(op="child-span"): with start_span(op="child-child-span"): scope = Hub.current.scope assert scope.span.op == "child-child-span" assert scope.transaction.name == "/"
def test_no_double_sampling(sentry_init, capture_events): # Transactions should not be subject to the global/error sample rate. # Only the traces_sample_rate should apply. sentry_init(traces_sample_rate=1.0, sample_rate=0.0) events = capture_events() with start_transaction(name="/"): pass assert len(events) == 1