Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 3 additions & 52 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
import threading
import warnings
from collections import namedtuple
from contextlib import contextmanager
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
from unittest import mock
from urllib.parse import parse_qs, urlparse

try:
Expand Down Expand Up @@ -85,7 +83,7 @@

if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Any, Callable, MutableMapping, Optional
from typing import Any, Callable, MutableMapping

try:
from httpx import (
Expand Down Expand Up @@ -395,7 +393,7 @@ def inner():

def append_event(envelope):
for item in envelope:
if item.headers.get("type") in ("event", "transaction"):
if item.headers.get("type") == "event":
events.append(item.payload.json)
return old_capture_envelope(envelope)

Expand Down Expand Up @@ -501,7 +499,7 @@ def inner():
old_capture_envelope = test_client.transport.capture_envelope

def append(envelope):
event = envelope.get_event() or envelope.get_transaction_event()
event = envelope.get_event()
if event is not None:
events_w.write(json.dumps(event).encode("utf-8"))
events_w.write(b"\n")
Expand Down Expand Up @@ -995,35 +993,6 @@ def inner(app, method: str, params, request_id: str):
return inner


@pytest.fixture()
def select_mcp_transactions():
def inner(events):
return [
event
for event in events
if event["type"] == "transaction"
and event["contexts"]["trace"]["op"] == "mcp.server"
]

return inner


@pytest.fixture()
def select_transactions_with_mcp_spans():
def inner(events, method_name):
return [
transaction
for transaction in events
if transaction.get("type") == "transaction"
and any(
span["data"].get("mcp.method.name") == method_name
for span in transaction.get("spans", [])
)
]

return inner


@pytest.fixture()
def json_rpc_sse():
class StreamingASGITransport(ASGITransport):
Expand Down Expand Up @@ -1767,24 +1736,6 @@ def werkzeug_set_cookie(client, servername, key, value):
client.set_cookie(key, value)


@contextmanager
def patch_start_tracing_child(
fake_transaction_is_none: bool = False,
) -> "Iterator[Optional[mock.MagicMock]]":
if not fake_transaction_is_none:
fake_transaction = mock.MagicMock()
fake_start_child = mock.MagicMock()
fake_transaction.start_child = fake_start_child
else:
fake_transaction = None
fake_start_child = None

with mock.patch(
"sentry_sdk.tracing_utils.get_current_span", return_value=fake_transaction
):
yield fake_start_child


class ApproxDict(dict):
def __eq__(self, other):
# For an ApproxDict to equal another dict, the other dict just needs to contain
Expand Down
24 changes: 3 additions & 21 deletions tests/integrations/fastmcp/test_fastmcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,13 +347,7 @@ def add_numbers(a: int, b: int) -> dict:
[(True, True), (True, False), (False, True), (False, False)],
)
async def test_fastmcp_tool_async(
sentry_init,
capture_items,
FastMCP,
send_default_pii,
include_prompts,
json_rpc,
select_transactions_with_mcp_spans,
sentry_init, capture_items, FastMCP, send_default_pii, include_prompts, json_rpc
):
"""Test that FastMCP async tool handlers create proper spans"""
sentry_init(
Expand Down Expand Up @@ -674,13 +668,7 @@ def read_file(path: str):
)
@pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids)
@pytest.mark.asyncio
async def test_fastmcp_resource_async(
sentry_init,
capture_items,
FastMCP,
json_rpc,
select_transactions_with_mcp_spans,
):
async def test_fastmcp_resource_async(sentry_init, capture_items, FastMCP, json_rpc):
"""Test that FastMCP async resource handlers create proper spans"""
sentry_init(
integrations=[MCPIntegration()],
Expand Down Expand Up @@ -882,13 +870,7 @@ def sse_tool(value: str) -> dict:


@pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids)
def test_fastmcp_http_transport(
sentry_init,
capture_items,
FastMCP,
json_rpc,
select_transactions_with_mcp_spans,
):
def test_fastmcp_http_transport(sentry_init, capture_items, FastMCP, json_rpc):
"""Test that FastMCP correctly detects HTTP transport"""
sentry_init(
integrations=[MCPIntegration()],
Expand Down
21 changes: 3 additions & 18 deletions tests/integrations/mcp/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,7 @@ async def test_tool(tool_name, arguments):
[(True, True), (True, False), (False, True), (False, False)],
)
async def test_tool_handler_streamable_http(
sentry_init,
capture_items,
send_default_pii,
include_prompts,
json_rpc,
select_transactions_with_mcp_spans,
sentry_init, capture_items, send_default_pii, include_prompts, json_rpc
):
"""Test that async tool handlers create proper spans"""
sentry_init(
Expand Down Expand Up @@ -688,12 +683,7 @@ async def test_prompt(name, arguments):
[(True, True), (True, False), (False, True), (False, False)],
)
async def test_prompt_handler_streamable_http(
sentry_init,
capture_items,
send_default_pii,
include_prompts,
json_rpc,
select_transactions_with_mcp_spans,
sentry_init, capture_items, send_default_pii, include_prompts, json_rpc
):
"""Test that async prompt handlers create proper spans"""
sentry_init(
Expand Down Expand Up @@ -890,12 +880,7 @@ async def test_resource(uri):


@pytest.mark.asyncio
async def test_resource_handler_streamable_http(
sentry_init,
capture_items,
json_rpc,
select_transactions_with_mcp_spans,
):
async def test_resource_handler_streamable_http(sentry_init, capture_items, json_rpc):
"""Test that async resource handlers create proper spans"""
sentry_init(
integrations=[MCPIntegration()],
Expand Down
185 changes: 0 additions & 185 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import subprocess
import sys
import time
from collections import Counter, defaultdict
from collections.abc import Mapping
from textwrap import dedent
from unittest import mock
Expand Down Expand Up @@ -1277,190 +1276,6 @@ def test_uwsgi_warnings(sentry_init, opt, missing_flags):
mock_logger.warning.assert_not_called()


class TestSpanClientReports:
"""
Tests for client reports related to spans.
"""

__test__ = False

@staticmethod
def span_dropper(spans_to_drop):
"""
Returns a function that can be used to drop spans from an event.
"""

def drop_spans(event, _):
event["spans"] = event["spans"][spans_to_drop:]
return event

return drop_spans

@staticmethod
def mock_transaction_event(span_count):
"""
Returns a mock transaction event with the given number of spans.
"""

return defaultdict(
mock.MagicMock,
type="transaction",
spans=[mock.MagicMock() for _ in range(span_count)],
)

def __init__(self, span_count):
"""Configures a test case with the number of spans dropped and whether the transaction was dropped."""
self.span_count = span_count
self.expected_record_lost_event_calls = Counter()
self.before_send = lambda event, _: event
self.event_processor = lambda event, _: event

def _update_resulting_calls(self, reason, drops_transactions=0, drops_spans=0):
"""
Updates the expected calls with the given resulting calls.
"""
if drops_transactions > 0:
self.expected_record_lost_event_calls[
(reason, "transaction", None, drops_transactions)
] += 1

if drops_spans > 0:
self.expected_record_lost_event_calls[
(reason, "span", None, drops_spans)
] += 1

def with_before_send(
self,
before_send,
*,
drops_transactions=0,
drops_spans=0,
):
self.before_send = before_send
self._update_resulting_calls(
"before_send",
drops_transactions,
drops_spans,
)

return self

def with_event_processor(
self,
event_processor,
*,
drops_transactions=0,
drops_spans=0,
):
self.event_processor = event_processor
self._update_resulting_calls(
"event_processor",
drops_transactions,
drops_spans,
)

return self

def run(self, sentry_init, capture_record_lost_event_calls):
"""Runs the test case with the configured parameters."""
sentry_init(before_send_transaction=self.before_send)
record_lost_event_calls = capture_record_lost_event_calls()

with sentry_sdk.isolation_scope() as scope:
scope.add_event_processor(self.event_processor)
event = self.mock_transaction_event(self.span_count)
sentry_sdk.get_client().capture_event(event, scope=scope)

# We use counters to ensure that the calls are made the expected number of times, disregarding order.
assert Counter(record_lost_event_calls) == self.expected_record_lost_event_calls


@pytest.mark.parametrize(
Comment thread
sentrivana marked this conversation as resolved.
"test_config",
(
TestSpanClientReports(span_count=10), # No spans dropped
TestSpanClientReports(span_count=0).with_before_send(
lambda e, _: None,
drops_transactions=1,
drops_spans=1,
),
TestSpanClientReports(span_count=10).with_before_send(
lambda e, _: None,
drops_transactions=1,
drops_spans=11,
),
TestSpanClientReports(span_count=10).with_before_send(
TestSpanClientReports.span_dropper(3),
drops_spans=3,
),
TestSpanClientReports(span_count=10).with_before_send(
TestSpanClientReports.span_dropper(10),
drops_spans=10,
),
TestSpanClientReports(span_count=10).with_event_processor(
lambda e, _: None,
drops_transactions=1,
drops_spans=11,
),
TestSpanClientReports(span_count=10).with_event_processor(
TestSpanClientReports.span_dropper(3),
drops_spans=3,
),
TestSpanClientReports(span_count=10).with_event_processor(
TestSpanClientReports.span_dropper(10),
drops_spans=10,
),
TestSpanClientReports(span_count=10)
.with_event_processor(
TestSpanClientReports.span_dropper(3),
drops_spans=3,
)
.with_before_send(
TestSpanClientReports.span_dropper(5),
drops_spans=5,
),
TestSpanClientReports(10)
.with_event_processor(
TestSpanClientReports.span_dropper(3),
drops_spans=3,
)
.with_before_send(
lambda e, _: None,
drops_transactions=1,
drops_spans=8, # 3 of the 11 (incl. transaction) spans already dropped
),
),
)
def test_dropped_transaction(sentry_init, capture_record_lost_event_calls, test_config):
test_config.run(sentry_init, capture_record_lost_event_calls)


def test_ignore_spans_warns_without_streaming(sentry_init):
with mock.patch("sentry_sdk.client.logger") as mock_logger:
sentry_init(ignore_spans=["/health"], trace_lifecycle="static")
mock_logger.warning.assert_any_call(
"The `ignore_spans` parameter only works when `trace_lifecycle` is set to `stream`.",
)


@pytest.mark.parametrize(
"options",
[
{"ignore_spans": ["/health"], "trace_lifecycle": "stream"},
{"ignore_spans": ["/health"], "_experiments": {"trace_lifecycle": "stream"}},
{},
],
)
def test_ignore_spans_does_not_warn(sentry_init, options):
with mock.patch("sentry_sdk.client.logger") as mock_logger:
sentry_init(**options)

ignore_spans_warnings = [
c for c in mock_logger.warning.call_args_list if "ignore_spans" in str(c)
]
assert ignore_spans_warnings == []


def make_options_transport_cls():
"""Make an options transport class that captures the options passed to it."""
# We need a unique class for each test so that the options are not
Expand Down
Loading
Loading